node-zugferd
Version:
A Node.js library for creating ZUGFeRD/Factur-X compliant documents. Generating XML and embedding it into PDF/A files, enabling seamless e-invoicing and digital document compliance.
959 lines (945 loc) • 35.2 kB
JavaScript
// src/formatter/xml/formatter.ts
import defu from "defu";
import { XMLBuilder } from "fast-xml-parser";
// src/error.ts
var ZugferdError = class extends Error {
constructor(code, message, options) {
super(message, options);
this.code = code;
this.code = code;
this.name = "ZugferdError";
}
};
// src/utils/logger.ts
var levels = ["info", "success", "warn", "error", "debug"];
var shouldPublishLog = (currentLogLevel, logLevel) => {
return levels.indexOf(logLevel) <= levels.indexOf(currentLogLevel);
};
var colors = {
reset: "\x1B[0m",
bright: "\x1B[1m",
dim: "\x1B[2m",
underscore: "\x1B[4m",
blink: "\x1B[5m",
reverse: "\x1B[7m",
hidden: "\x1B[8m",
fg: {
black: "\x1B[30m",
red: "\x1B[31m",
green: "\x1B[32m",
yellow: "\x1B[33m",
blue: "\x1B[34m",
magenta: "\x1B[35m",
cyan: "\x1B[36m",
white: "\x1B[37m"
},
bg: {
black: "\x1B[40m",
red: "\x1B[41m",
green: "\x1B[42m",
yellow: "\x1B[43m",
blue: "\x1B[44m",
magenta: "\x1B[45m",
cyan: "\x1B[46m",
white: "\x1B[47m"
}
};
var levelColors = {
info: colors.fg.blue,
success: colors.fg.green,
warn: colors.fg.yellow,
error: colors.fg.red,
debug: colors.fg.magenta
};
var formatMessage = (level, message) => {
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
return `${colors.dim}${timestamp}${colors.reset} ${levelColors[level]}${level.toUpperCase()}${colors.reset} ${colors.dim}\u2022${colors.reset} ${colors.bright}node-zugferd ${colors.dim}|${colors.reset} ${message}`;
};
var createLogger = (options) => {
const enabled = options?.disabled !== true;
const logLevel = options?.level ?? "error";
const LogFunc = (level, message, args = []) => {
if (!enabled || !shouldPublishLog(logLevel, level)) {
return;
}
const formattedMessage = formatMessage(level, message);
if (!options || typeof options.log !== "function") {
if (level === "error") {
console.error(formattedMessage, ...args);
} else if (level === "warn") {
console.warn(formattedMessage, ...args);
} else {
console.log(formattedMessage, ...args);
}
return;
}
options.log(level === "success" ? "info" : level, message, ...args);
};
return Object.fromEntries(
levels.map((level) => [
level,
(...[message, ...args]) => LogFunc(level, message, args)
])
);
};
// src/formatter/xml/formatter.ts
var findFieldByKey = (obj, key) => {
if (typeof obj !== "object" || obj === null) return void 0;
if (obj.key === key) return obj;
for (const value of Object.values(obj)) {
if (typeof value === "object" && value !== null) {
const found = findFieldByKey(value, key);
if (found !== void 0) return found;
}
}
return void 0;
};
var hasValue = (val, fieldType) => {
if (val === void 0 || val === null) return false;
const typeToCheck = Array.isArray(fieldType) ? fieldType[0] : fieldType;
if (typeToCheck === "string" && val === "") return false;
if (typeToCheck === "object") {
if (Array.isArray(val)) return true;
if (typeof val === "object" && Object.keys(val).length === 0) return false;
}
return true;
};
var applyMask = (schema, mask) => {
const res = Object.entries(mask).reduce(
(acc, [key, value]) => {
if (Array.isArray(value) && value.length) {
const nestedSchema = findFieldByKey(schema, value[0]);
if (nestedSchema) {
acc[key] = {
...nestedSchema,
shape: applyMask(nestedSchema.shape || nestedSchema, value[1])
};
}
} else {
const nestedSchema = findFieldByKey(schema, value);
if (nestedSchema !== void 0) {
acc[key] = nestedSchema;
}
}
return acc;
},
{}
);
return res;
};
var updateDefaultValues = (base, override) => {
const result = {};
for (const key in base) {
const baseField = base[key];
const overrideField = override[key];
if (baseField && overrideField) {
if (baseField.shape && overrideField.shape) {
result[key] = {
...baseField,
shape: updateDefaultValues(baseField.shape, overrideField.shape)
};
} else {
result[key] = {
...baseField,
defaultValue: overrideField.defaultValue ?? baseField.defaultValue
};
}
} else {
result[key] = baseField;
}
}
return result;
};
var mergeSchemas = (profile) => {
if (!profile.extends) {
return profile.mask ? applyMask(profile.schema, profile.mask) : profile.schema;
}
const mergedExtensions = defu(
{},
...profile.extends?.map((p) => p.schema) || []
);
const mergedSchema = updateDefaultValues(
defu({}, mergedExtensions, profile.schema),
profile.schema
);
return profile.mask ? applyMask(mergedSchema, profile.mask) : mergedSchema;
};
var collectAdditionalXmlFields = (def, data, localGroupIndices, fullData) => {
const additionalFields = [];
for (const [key, field] of Object.entries(def)) {
const rawValue = data[key];
const _value = field.type !== "object" ? rawValue ?? field.defaultValue : typeof rawValue === "object" && rawValue !== null ? Object.keys(rawValue).length <= 0 ? field.defaultValue : rawValue : rawValue ?? field.defaultValue;
const value = field.transform?.input ? field.transform.input(_value) : _value;
if (field.additionalXml && rawValue !== void 0 && hasValue(value, field.type)) {
additionalFields.push({
position: key,
field,
value,
rawValue
});
}
}
return additionalFields;
};
var parseSchema = (ctx, data, def, options, parentGroupIndices = {}, fullData = null) => {
options.groupIndices ??= {};
let xml = {
"?xml": {
"@version": "1.0",
"@encoding": "UTF-8"
},
"rsm:CrossIndustryInvoice": {
"@xmlns:rsm": "urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100",
"@xmlns:qdt": "urn:un:unece:uncefact:data:standard:QualifiedDataType:100",
"@xmlns:ram": "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100",
"@xmlns:xs": "http://www.w3.org/2001/XMLSchema",
"@xmlns:udt": "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100"
}
};
const localGroupIndices = { ...parentGroupIndices };
const processField = (field, value, key, rawValue) => {
ctx.logger.debug(
`[${parseSchema.name}] Processing field ${colors.bright}${key}${colors.reset}${field.key ? ` (${field.key})` : ""} ${Array.isArray(field.type) || field.type !== "object" && !field.type.endsWith("[]") ? `${colors.fg.yellow}${value}${colors.reset}` : ""}`
);
if (field.group) {
localGroupIndices[field.group] = localGroupIndices[field.group] || 0;
}
let siblingOffset = 0;
if (field.sibling && fullData) {
const siblings = field.sibling(fullData, localGroupIndices);
if (Array.isArray(siblings)) {
siblingOffset = siblings.length;
} else if (siblings !== void 0 && siblings !== null) {
siblingOffset = 1;
}
}
const handleArrayField = (arrayValue, fieldXPath) => {
arrayValue.forEach((item, arrayIndex) => {
ctx.logger.debug(
`[${parseSchema.name}] Array item ${arrayIndex} for field: ${colors.bright}${key}${colors.reset}${field.key ? ` (${field.key})` : ""}`
);
const transformedItem = field.transform?.input ? field.transform.input(item) : item;
const itemXPath = fieldXPath.replace(
/\[i\]/,
`[${arrayIndex + siblingOffset}]`
);
const xmlPart = buildXmlStructure(
itemXPath,
transformedItem,
localGroupIndices
);
xml = mergeXml(xml, xmlPart);
});
};
if (field?.xpath) {
const resolvedXPath = resolveXPath(field.xpath, localGroupIndices);
if (typeof field.type === "string" && field.type.endsWith("[]") && Array.isArray(value)) {
handleArrayField(value, resolvedXPath);
} else if (typeof value === "object" && !Array.isArray(value)) {
const childXml = parseSchema(
ctx,
value,
field.shape || {},
options,
localGroupIndices,
fullData
);
xml = mergeXml(xml, childXml);
} else {
const xmlPart = buildXmlStructure(
resolvedXPath,
value,
localGroupIndices
);
xml = mergeXml(xml, xmlPart);
}
}
if (field?.type === "object" && field?.shape) {
const childXml = parseSchema(
ctx,
value || {},
field.shape,
options,
localGroupIndices,
fullData
);
xml = mergeXml(xml, childXml);
}
if (field?.type === "object[]" && field?.shape) {
const arrayValue = Array.isArray(value) ? value : [];
arrayValue.forEach((item, arrayIndex) => {
const newGroupIndices = { ...localGroupIndices };
if (field.group) {
newGroupIndices[field.group] = arrayIndex + siblingOffset;
}
const transformedItem = field.transform?.input ? field.transform.input(item) : item;
const childXml = parseSchema(
ctx,
transformedItem,
field.shape || {},
options,
newGroupIndices,
fullData
);
xml = mergeXml(xml, childXml);
});
}
};
const additionalXmlFields = collectAdditionalXmlFields(
def,
data,
localGroupIndices,
fullData
);
for (const [key, field] of Object.entries(def)) {
let rawValue = data[key];
if (field.validator) {
const { data: data2, success, error } = field.validator.safeParse(rawValue);
if (!success) {
ctx.logger.debug(
`[${parseSchema.name}] Validation failed for ${colors.bright}${key}${colors.reset}${field.key ? ` (${field.key})` : ""}: ${error.errors[0].message}`
);
throw new ZugferdError(
"INVALID_FIELD",
`${key} - ${error.errors[0].message}`
);
}
rawValue = data2;
}
const _value = field.type !== "object" ? rawValue ?? field.defaultValue : typeof rawValue === "object" && rawValue !== null ? Object.keys(rawValue).length <= 0 ? field.defaultValue : rawValue : rawValue ?? field.defaultValue;
const value = field.transform?.input ? field.transform.input(_value) : _value;
if (!hasValue(value, field.type)) {
ctx.logger.debug(
`[${parseSchema.name}] Skipping field without value: ${colors.bright}${key}${colors.reset}${field.key ? ` (${field.key})` : ""}`
);
continue;
}
processField(field, value, key, rawValue);
const additionalField = additionalXmlFields.find((f) => f.position === key);
if (additionalField && additionalField.field.additionalXml) {
ctx.logger.debug(
`[${parseSchema.name}] Processing additionalXml for field: ${colors.bright}${key}${colors.reset}${additionalField.field.key ? ` (${additionalField.field.key})` : ""}`
);
const additionalXml = parseSchema(
ctx,
data,
additionalField.field.additionalXml,
options,
localGroupIndices,
fullData
);
xml = mergeXml(xml, additionalXml);
}
}
return xml;
};
var resolveXPath = (xpath, groupIndices) => {
return xpath.replace(/\[([^\]]+)\]/g, (match, group) => {
if (groupIndices[group] !== void 0) {
return `[${groupIndices[group]}]`;
}
return match;
});
};
var mergeXml = (target, source) => {
for (const key in source) {
if (key in target) {
if (key === "#" || key.startsWith("@")) {
target[key] = source[key];
} else if (Array.isArray(target[key]) || Array.isArray(source[key])) {
const targetArray = Array.isArray(target[key]) ? target[key] : [target[key]];
const sourceArray = Array.isArray(source[key]) ? source[key] : [source[key]];
target[key] = targetArray.map((item, index) => {
if (sourceArray[index] === void 0) {
return item;
} else if (typeof item === "object" && typeof sourceArray[index] === "object") {
return mergeXml(item, sourceArray[index]);
} else {
return sourceArray[index];
}
});
if (sourceArray.length > targetArray.length) {
target[key] = target[key].concat(
sourceArray.slice(targetArray.length)
);
}
} else if (typeof target[key] === "object" && typeof source[key] === "object") {
target[key] = mergeXml(target[key], source[key]);
} else {
target[key] = source[key];
}
} else {
target[key] = source[key];
}
}
return target;
};
var buildXmlStructure = (xpath, value, groupIndices) => {
const resolvedXPath = resolveXPath(xpath, groupIndices);
const parts = resolvedXPath.split("/").filter(Boolean);
const result = {};
let current = result;
parts.forEach((part, index) => {
const match = part.match(/^(.+?)(?:\[(\d+|i)\])?$/);
if (!match) return;
const [, nodeName, arrayIndex] = match;
const isAttribute = nodeName.startsWith("@");
if (value === void 0 || value === null) {
return;
}
if (index === parts.length - 1) {
if (arrayIndex !== void 0) {
const idx = arrayIndex === "i" ? 0 : Number.parseInt(arrayIndex, 10);
current[nodeName] = current[nodeName] || [];
current[nodeName][idx] = isAttribute ? String(value) : { "#": String(value) };
} else {
current[nodeName] = isAttribute ? String(value) : { "#": String(value) };
}
} else {
if (arrayIndex !== void 0) {
const idx = arrayIndex === "i" ? 0 : Number.parseInt(arrayIndex, 10);
current[nodeName] = current[nodeName] || [];
current[nodeName][idx] = current[nodeName][idx] || {};
current = current[nodeName][idx];
} else {
current[nodeName] = current[nodeName] || {};
current = current[nodeName];
}
}
});
return result;
};
var formatXml = (ctx, doc, options) => {
ctx.logger.debug(`[${formatXml.name}] Formatting XML started`);
const parser = new XMLBuilder(
defu(
{
ignoreAttributes: false,
attributeNamePrefix: "@",
textNodeName: "#",
format: true,
suppressBooleanAttributes: false,
suppressEmptyNode: true
},
options
)
);
const result = parser.build(doc);
ctx.logger.debug(`[${formatXml.name}] Formatting XML finished`);
return result;
};
// src/formatter/pdf/formatter.ts
import {
decodePDFRawStream,
PDFArray,
PDFDict,
PDFHexString,
PDFName,
PDFNumber,
PDFStream,
PDFString
} from "pdf-lib";
import crypto from "crypto";
// src/utils/color-profile.ts
var COLOR_PROFILE = `
AAAL0AAAAAACAAAAbW50clJHQiBYWVogB98AAgAPAAAAAAAAYWNzcAAAAAAAAAAAAAAAAAAAAAAA
AAABAAAAAAAAAAAAAPbWAAEAAAAA0y0AAAAAPQ6y3q6Tl76bZybOjApDzgAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAQZGVzYwAAAUQAAABjYlhZWgAAAagAAAAUYlRSQwAAAbwAAAgMZ1RS
QwAAAbwAAAgMclRSQwAAAbwAAAgMZG1kZAAACcgAAACIZ1hZWgAAClAAAAAUbHVtaQAACmQAAAAU
bWVhcwAACngAAAAkYmtwdAAACpwAAAAUclhZWgAACrAAAAAUdGVjaAAACsQAAAAMdnVlZAAACtAA
AACHd3RwdAAAC1gAAAAUY3BydAAAC2wAAAA3Y2hhZAAAC6QAAAAsZGVzYwAAAAAAAAAJc1JHQjIw
MTQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAAAkoAAAD4QAALbPY3VydgAAAAAAAAQA
AAAABQAKAA8AFAAZAB4AIwAoAC0AMgA3ADsAQABFAEoATwBUAFkAXgBjAGgAbQByAHcAfACBAIYA
iwCQAJUAmgCfAKQAqQCuALIAtwC8AMEAxgDLANAA1QDbAOAA5QDrAPAA9gD7AQEBBwENARMBGQEf
ASUBKwEyATgBPgFFAUwBUgFZAWABZwFuAXUBfAGDAYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB
8gH6AgMCDAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKEAo4CmAKiAqwCtgLBAssC1QLgAusC9QMA
AwsDFgMhAy0DOANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPTA+AD7AP5BAYEEwQgBC0EOwRIBFUE
YwRxBH4EjASaBKgEtgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3BYYFlgWmBbUFxQXVBeUF9gYG
BhYGJwY3BkgGWQZqBnsGjAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0B4YHmQesB78H0gflB/gI
CwgfCDIIRghaCG4IggiWCKoIvgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnPCeUJ+woRCicKPQpU
CmoKgQqYCq4KxQrcCvMLCwsiCzkLUQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyODKcMwAzZDPMN
DQ0mDUANWg10DY4NqQ3DDd4N+A4TDi4OSQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+zD88P7BAJ
ECYQQxBhEH4QmxC5ENcQ9RETETERTxFtEYwRqhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxNDE2MT
gxOkE8UT5RQGFCcUSRRqFIsUrRTOFPAVEhU0FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdB
F2UXiReuF9IX9xgbGEAYZRiKGK8Y1Rj6GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2Mb
ihuyG9ocAhwqHFIcexyjHMwc9R0eHUcdcB2ZHcMd7B4WHkAeah6UHr4e6R8THz4faR+UH78f6iAV
IEEgbCCYIMQg8CEcIUghdSGhIc4h+yInIlUigiKvIt0jCiM4I2YjlCPCI/AkHyRNJHwkqyTaJQkl
OCVoJZclxyX3JicmVyaHJrcm6CcYJ0kneierJ9woDSg/KHEooijUKQYpOClrKZ0p0CoCKjUqaCqb
Ks8rAis2K2krnSvRLAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5MLoIuty7uLyQvWi+RL8cv/jA1MGww
pDDbMRIxSjGCMbox8jIqMmMymzLUMw0zRjN/M7gz8TQrNGU0njTYNRM1TTWHNcI1/TY3NnI2rjbp
NyQ3YDecN9c4FDhQOIw4yDkFOUI5fzm8Ofk6Njp0OrI67zstO2s7qjvoPCc8ZTykPOM9Ij1hPaE9
4D4gPmA+oD7gPyE/YT+iP+JAI0BkQKZA50EpQWpBrEHuQjBCckK1QvdDOkN9Q8BEA0RHRIpEzkUS
RVVFmkXeRiJGZ0arRvBHNUd7R8BIBUhLSJFI10kdSWNJqUnwSjdKfUrESwxLU0uaS+JMKkxyTLpN
Ak1KTZNN3E4lTm5Ot08AT0lPk0/dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRCVI9U21Uo
VXVVwlYPVlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114Xcle
Gl5sXr1fD19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GNDY5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9
Z5Nn6Wg/aJZo7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1gbbluEm5rbsRvHm94b9FwK3CGcOBx
OnGVcfByS3KmcwFzXXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gReG54zHkqeYl553pGeqV7BHtj
e8J8IXyBfOF9QX2hfgF+Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INXg7qEHYSAhOOFR4Wrhg6G
cobXhzuHn4gEiGmIzokziZmJ/opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82j56QBpBukNaRP5Go
khGSepLjk02TtpQglIqU9JVflcmWNJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuvnByciZz3nWSd
0p5Anq6fHZ+Ln/qgaaDYoUehtqImopajBqN2o+akVqTHpTilqaYapoum/adup+CoUqjEqTepqaoc
qo+rAqt1q+msXKzQrUStuK4trqGvFq+LsACwdbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5tvC3
aLfguFm40blKucK6O7q1uy67p7whvJu9Fb2Pvgq+hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTO
xUvFyMZGxsPHQce/yD3IvMk6ybnKOMq3yzbLtsw1zLXNNc21zjbOts83z7jQOdC60TzRvtI/0sHT
RNPG1EnUy9VO1dHWVdbY11zX4Nhk2OjZbNnx2nba+9uA3AXcit0Q3ZbeHN6i3ynfr+A24L3hROHM
4lPi2+Nj4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ6lvq5etw6/vshu0R7ZzuKO6070DvzPBY8OXx
cvH/8ozzGfOn9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX+uf7d/wH/Jj9Kf26/kv+3P9t//9kZXNj
AAAAAAAAAC5JRUMgNjE5NjYtMi0xIERlZmF1bHQgUkdCIENvbG91ciBTcGFjZSAtIHNSR0IAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAAAAAUAAAAAAA
AG1lYXMAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlhZWiAAAAAAAAAAngAAAKQAAACH
WFlaIAAAAAAAAG+iAAA49QAAA5BzaWcgAAAAAENSVCBkZXNjAAAAAAAAAC1SZWZlcmVuY2UgVmll
d2luZyBDb25kaXRpb24gaW4gSUVDIDYxOTY2LTItMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFla
IAAAAAAAAPbWAAEAAAAA0y10ZXh0AAAAAENvcHlyaWdodCBJbnRlcm5hdGlvbmFsIENvbG9yIENv
bnNvcnRpdW0sIDIwMTUAAHNmMzIAAAAAAAEMRAAABd////MmAAAHlAAA/Y////uh///9ogAAA9sA
AMB1
`.trim();
// src/utils/helper.ts
var base64ToUint8Array = (base64) => new Uint8Array([...atob(base64)].map((char) => char.charCodeAt(0)));
// src/formatter/pdf/formatter.ts
var addPdfMetadata = (ctx, pdfDoc, metadata) => {
ctx.logger.debug(
`[${addPdfMetadata.name}] Starting with metadata:`,
metadata
);
let xmp = formatXml(ctx, {
"x:xmpmeta": {
"@xmlns:x": "adobe:ns:meta/",
"rdf:RDF": {
"@xmlns:rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"rdf:Description": [
{
"@xmlns:pdfaid": "http://www.aiim.org/pdfa/ns/id/",
"@rdf:about": "",
"pdfaid:part": "3",
"pdfaid:conformance": "B"
},
{
"@xmlns:dc": "http://purl.org/dc/elements/1.1/",
"@rdf:about": "",
"dc:format": "application/pdf",
"dc:date": {
"rdf:Seq": {
"rdf:li": metadata.now.toISOString()
}
},
"dc:creator": {
"rdf:Seq": {
"rdf:li": metadata.creator
}
}
},
{
"@xmlns:pdf": "http://ns.adobe.com/pdf/1.3/",
"@rdf:about": "",
"pdf:Producer": metadata.producer || "jslno/node-zugferd git+https://github.com/jslno/node-zugferd",
"pdf:PDFVersion": "1.7"
},
{
"@xmlns:xmp": "http://ns.adobe.com/xap/1.0/",
"@rdf:about": "",
"xmp:CreatorTool": metadata.creator,
"xmp:CreateDate": metadata.createDate.toISOString(),
"xmp:ModifyDate": metadata.modifyDate.toISOString(),
"xmp:MetadataDate": metadata.now.toISOString()
},
{
"@xmlns:pdfaExtension": "http://www.aiim.org/pdfa/ns/extension/",
"@xmlns:pdfaSchema": "http://www.aiim.org/pdfa/ns/schema#",
"@xmlns:pdfaProperty": "http://www.aiim.org/pdfa/ns/property#",
"@rdf:about": "",
"pdfaExtension:schemas": {
"rdf:Bag": {
"rdf:li": {
"@rdf:parseType": "Resource",
"pdfaSchema:schema": "Factur-X PDFA Extension Schema",
"pdfaSchema:namespaceURI": "urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#",
"pdfaSchema:prefix": "fx",
"pdfaSchema:property": {
"rdf:Seq": {
"rdf:li": [
{
"@rdf:parseType": "Resource",
"pdfaProperty:name": "DocumentFileName",
"pdfaProperty:valueType": "Text",
"pdfaProperty:category": "external",
"pdfaProperty:description": "The name of the embedded XML document"
},
{
"@rdf:parseType": "Resource",
"pdfaProperty:name": "DocumentType",
"pdfaProperty:valueType": "Text",
"pdfaProperty:category": "external",
"pdfaProperty:description": "The type of the hybrid document in capital letters, e.g. INVOICE or ORDER"
},
{
"@rdf:parseType": "Resource",
"pdfaProperty:name": "Version",
"pdfaProperty:valueType": "Text",
"pdfaProperty:category": "external",
"pdfaProperty:description": "The actual version of the standard applying to the embedded XML Document"
},
{
"@rdf:parseType": "Resource",
"pdfaProperty:name": "ConformanceLevel",
"pdfaProperty:valueType": "Text",
"pdfaProperty:category": "external",
"pdfaProperty:description": "The conformance level of the embedded XML document"
}
]
}
}
}
}
}
},
{
"@xmlns:fx": "urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#",
"@xmlns:about": "",
"fx:DocumentType": metadata.facturX.documentType,
"fx:DocumentFileName": metadata.facturX.documentFileName,
"fx:Version": metadata.facturX.version,
"fx:ConformanceLevel": metadata.facturX.conformanceLevel
}
]
}
}
});
xmp = `<?xpacket begin="\uFEFF" id="W5M0MpCehiHzreSzNTczkc9d"?>${xmp}<?xpacket end="w"?>`;
ctx.logger.debug(
`[${addPdfMetadata.name}] Generated XMP length: ${colors.bright}${xmp.length}${colors.reset}`
);
const metadataStream = pdfDoc.context.stream(xmp, {
Type: "Metadata",
Subtype: "XML",
Length: xmp.length
});
const metadataStreamRef = pdfDoc.context.register(metadataStream);
pdfDoc.catalog.set(PDFName.of("Metadata"), metadataStreamRef);
ctx.logger.debug(`[${addPdfMetadata.name}] Metadata stream registered`);
return pdfDoc;
};
var addPdfTrailerInfoId = (ctx, pdfDoc, trailerInfoId) => {
trailerInfoId = trailerInfoId + (/* @__PURE__ */ new Date()).toISOString();
ctx.logger.debug(
`[${addPdfTrailerInfoId.name}] Creating trailer info ID for: ${colors.bright}${trailerInfoId}${colors.reset}`
);
const hash = crypto.createHash("SHA256").update(trailerInfoId).digest();
const hashArr = Array.from(new Uint8Array(hash));
const hashHex = hashArr.map((b) => b.toString(16).padStart(2, "0")).join("");
ctx.logger.debug(
`[${addPdfTrailerInfoId.name}] Generated SHA256 hex: ${colors.bright}${hashHex}${colors.reset}`
);
const permanentDocId = PDFHexString.of(hashHex);
const changingDocId = permanentDocId;
pdfDoc.context.trailerInfo.ID = pdfDoc.context.obj([
permanentDocId,
changingDocId
]);
ctx.logger.debug(`[${addPdfTrailerInfoId.name}] Trailer info ID set`);
return pdfDoc;
};
var fixPdfLinkAnnotations = (ctx, pdfDoc) => {
ctx.logger.debug(`[${fixPdfLinkAnnotations.name}] Fixing link annotations`);
const pages = pdfDoc.getPages();
ctx.logger.debug(
`[${fixPdfLinkAnnotations.name}] Page count: ${colors.bright}${pages.length}${colors.reset}`
);
for (const [pageIndex, page] of pages.entries()) {
const annotations = page.node.get(PDFName.of("Annots"));
if (annotations instanceof PDFArray) {
for (let i = 0; i < annotations.size(); ++i) {
const annotationRef = annotations.get(i);
const annotation = page.node.context.lookup(annotationRef);
const subtype = annotation.get(PDFName.of("Subtype"));
if (subtype === PDFName.of("Link")) {
ctx.logger.debug(
`[${fixPdfLinkAnnotations.name}] Found link annotation on page ${colors.bright}${pageIndex + 1}${colors.reset}`
);
const flagsObj = annotation.get(PDFName.of("F"));
const flags = flagsObj instanceof PDFNumber ? flagsObj.asNumber() : 0;
annotation.set(PDFName.of("F"), PDFNumber.of(flags | 4));
}
}
}
}
return pdfDoc;
};
var addPdfMarkInfo = (ctx, pdfDoc) => {
ctx.logger.debug(`[${addPdfMarkInfo.name}] Adding MarkInfo`);
const rootRef = pdfDoc.context.obj({ Marked: true });
pdfDoc.catalog.set(PDFName.of("MarkInfo"), rootRef);
return pdfDoc;
};
var addPdfStructTreeRoot = (ctx, pdfDoc) => {
ctx.logger.debug(`[${addPdfStructTreeRoot.name}] Adding StructTreeRoot`);
const structTreeRoot = pdfDoc.context.obj({
Type: PDFName.of("StructTreeRoot")
});
const structTreeRootRef = pdfDoc.context.register(structTreeRoot);
pdfDoc.catalog.set(PDFName.of("StructTreeRoot"), structTreeRootRef);
return pdfDoc;
};
var addPdfICC = (ctx, pdfDoc) => {
ctx.logger.debug(`[${addPdfICC.name}] Adding ICC profile`);
const profile = base64ToUint8Array(COLOR_PROFILE);
const profileStream = pdfDoc.context.stream(profile, {
Length: profile.length
});
const profileStreamRef = pdfDoc.context.register(profileStream);
const outputIntent = pdfDoc.context.obj({
Type: "OutputIntent",
S: "GTS_PDFA1",
OutputConditionIdentifier: PDFString.of("sRGB"),
DestOutputProfile: profileStreamRef
});
const outputIntentRef = pdfDoc.context.register(outputIntent);
pdfDoc.catalog.set(
PDFName.of("OutputIntents"),
pdfDoc.context.obj([outputIntentRef])
);
ctx.logger.debug(`[${addPdfICC.name}] ICC profile registered`);
return pdfDoc;
};
var getRawAttachments = (ctx, pdfDoc) => {
ctx.logger.debug(`[${getRawAttachments.name}] Extracting raw attachments`);
if (!pdfDoc.catalog.has(PDFName.of("Names"))) {
ctx.logger.debug(`[${getRawAttachments.name}] No Names dictionary found`);
return [];
}
const Names = pdfDoc.catalog.lookup(PDFName.of("Names"), PDFDict);
if (!Names.has(PDFName.of("EmbeddedFiles"))) {
ctx.logger.debug(`[${getRawAttachments.name}] No EmbeddedFiles found`);
return [];
}
const EmbeddedFiles = Names.lookup(PDFName.of("EmbeddedFiles"), PDFDict);
if (!EmbeddedFiles.has(PDFName.of("Names"))) {
ctx.logger.debug(
`[${getRawAttachments.name}] No Names array found for EmbeddedFiles`
);
return [];
}
const EFNames = EmbeddedFiles.lookup(PDFName.of("Names"), PDFArray);
const rawAttachments = [];
for (let i = 0; i < EFNames.size(); i += 2) {
const fileName = EFNames.lookup(i);
const fileSpec = EFNames.lookup(i + 1, PDFDict);
rawAttachments.push({
fileName,
fileSpec
});
ctx.logger.debug(
`[${getRawAttachments.name}] Found attachment: ${colors.bright}${fileName.decodeText()}${colors.reset}`
);
}
return rawAttachments;
};
var getPdfAttachments = (ctx, pdfDoc) => {
ctx.logger.debug(`[${getPdfAttachments.name}] Getting PDF attachments`);
const rawAttachments = getRawAttachments(ctx, pdfDoc);
return rawAttachments.map(({ fileName, fileSpec }) => {
const stream = fileSpec.lookup(PDFName.of("EF"), PDFDict).lookup(PDFName.of("F"), PDFStream);
ctx.logger.debug(
`[${getPdfAttachments.name}] Decoding attachment: ${colors.bright}${fileName.decodeText()}${colors.reset}`
);
return {
name: fileName.decodeText(),
data: decodePDFRawStream(stream).decode()
};
});
};
// src/types/schema.ts
import "zod";
// src/document/create.ts
import { AFRelationship, PDFDocument as PDFDocument2 } from "pdf-lib";
// src/document/validate.ts
var validateDocumentFactory = (ctx) => async (data) => {
if (ctx.options.strict === false) {
ctx.logger.debug(
`[${validateDocumentFactory.name}] Strict mode disabled, skipping validation`
);
return true;
}
ctx.logger.debug(`[${validateDocumentFactory.name}] Validating`);
const result = await ctx.options.profile.validate(data);
ctx.logger.debug(
`[${validateDocumentFactory.name}] Validation result: ${result ? `${colors.fg.green}\u2714${colors.reset}` : `${colors.fg.red}\u274C${colors.reset}`}`
);
return result;
};
// src/document/create.ts
var createDocumentFactory = (ctx, options) => (data) => {
const toObj = () => options.profile.parse({
context: ctx,
data
});
const toXML = async () => {
const xml = ctx.xml.format(toObj());
await validateDocumentFactory(ctx)(xml);
return xml;
};
const embedInPdf = async (pdf, opts = {}) => {
const xml = await toXML();
let { metadata } = opts;
const { profile } = options;
const now = /* @__PURE__ */ new Date();
metadata ??= {};
metadata.createDate ??= now;
metadata.modifyDate ??= metadata.createDate;
let pdfDoc = pdf instanceof PDFDocument2 ? pdf : await PDFDocument2.load(pdf);
ctx.logger.debug(
`[${embedInPdf.name}] Attaching ${colors.bright}${profile.documentFileName}${colors.reset}`
);
await pdfDoc.attach(Buffer.from(xml), profile.documentFileName, {
mimeType: "application/xml",
description: "Factur-X",
creationDate: metadata.createDate,
modificationDate: metadata.modifyDate,
afRelationship: AFRelationship.Alternative
});
ctx.logger.debug(
`[${embedInPdf.name}] Attached ${colors.bright}${profile.documentFileName}${colors.reset}`
);
ctx.logger.debug(`[${embedInPdf.name}] Setting PDF metadata`);
if (!!metadata.author) {
pdfDoc.setAuthor(metadata.author);
}
pdfDoc.setCreationDate(metadata.createDate);
pdfDoc.setModificationDate(metadata.modifyDate);
if (!!metadata.creator) {
pdfDoc.setCreator(metadata.creator);
}
if (!!metadata.keywords) {
pdfDoc.setKeywords(metadata.keywords);
}
if (!!metadata.language) {
pdfDoc.setLanguage(metadata.language);
}
if (!!metadata.producer) {
pdfDoc.setProducer(metadata.producer);
}
if (!!metadata.subject) {
pdfDoc.setSubject(metadata.subject);
}
if (!!metadata.title) {
pdfDoc.setTitle(metadata.title);
}
ctx.logger.debug(`[${embedInPdf.name}] Applying PDF/A-3b enhancements`);
pdfDoc = ctx.pdf.addTrailerInfoId(pdfDoc, metadata.subject || "");
pdfDoc = ctx.pdf.addMarkInfo(pdfDoc);
pdfDoc = ctx.pdf.addStructTreeRoot(pdfDoc);
pdfDoc = ctx.pdf.fixLinkAnnotations(pdfDoc);
pdfDoc = ctx.pdf.addICC(pdfDoc);
ctx.pdf.addMetadata(pdfDoc, {
...metadata,
createDate: metadata.createDate,
modifyDate: metadata.modifyDate,
now,
facturX: {
conformanceLevel: profile.conformanceLevel,
documentFileName: profile.documentFileName,
documentType: profile.documentType ?? "INVOICE",
version: profile.version
}
});
if (opts.additionalFiles?.length && opts.additionalFiles.length > 0) {
ctx.logger.debug(
`[${embedInPdf.name}] Attaching ${colors.bright}${opts.additionalFiles.length}${colors.reset} additional file(s)`
);
for (const item of opts.additionalFiles) {
const { filename, content, relationship, ...fileOptions } = item;
await pdfDoc.attach(content, filename, {
...fileOptions,
afRelationship: !!relationship ? AFRelationship[relationship] : AFRelationship.Unspecified
});
}
} else {
ctx.logger.debug(`[${embedInPdf.name}] No additional files to attach`);
}
ctx.logger.debug(`[${embedInPdf.name}] Saving PDF`);
return await pdfDoc.save();
};
return {
toObj,
toXML,
embedInPdf
};
};
// src/init.ts
var init = (options) => {
const logger = createLogger(options?.logger);
if (options.strict === false) {
logger.warn(
"Validation disabled (strict: false). The generated XML will not be checked against the XSD schema and may be non-compliant."
);
}
const ctx = {
options,
logger,
...getInternalTools({
options,
logger
})
};
const context = {
...ctx,
document: {
create: createDocumentFactory(ctx, options),
validate: validateDocumentFactory(ctx)
}
};
return context;
};
var getInternalTools = (ctx) => ({
parseSchema: parseSchema.bind(null, ctx),
mergeSchemas,
xml: {
format: formatXml.bind(null, ctx)
},
pdf: {
addMetadata: addPdfMetadata.bind(null, ctx),
addTrailerInfoId: addPdfTrailerInfoId.bind(null, ctx),
fixLinkAnnotations: fixPdfLinkAnnotations.bind(null, ctx),
addStructTreeRoot: addPdfStructTreeRoot.bind(null, ctx),
addMarkInfo: addPdfMarkInfo.bind(null, ctx),
addICC: addPdfICC.bind(null, ctx),
getAttachments: getPdfAttachments.bind(null, ctx)
}
});
// src/zugferd.ts
var zugferd = (options) => {
const context = init(options);
const handlers = getPluginHandlers(context, options);
const ctx = {
context,
options,
create: context.document.create,
validate: context.document.validate,
$Infer: {
Schema: {}
}
};
return {
...ctx,
...handlers ?? {}
};
};
var getPluginHandlers = (ctx, options) => {
const pluginHandlers = (options.plugins || []).reduce(
(acc, plugin) => {
return {
...acc,
...plugin(ctx)
};
},
{}
);
return pluginHandlers;
};
export {
ZugferdError,
zugferd
};