UNPKG

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
// 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 };