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.

770 lines (757 loc) 27.5 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/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 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, 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 parseSchema = (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) => { 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) => { 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); }); }; const processAdditionalXml = (value2) => { if (field.additionalXml && !!value2) { for (const [aKey, aValue] of Object.entries(field.additionalXml)) { const resolvedXPath = resolveXPath(aKey, localGroupIndices); if (Array.isArray(value2)) { value2.forEach((_, index) => { const itemXPath = resolvedXPath.replace( /\[i\]/, `[${index + siblingOffset}]` ); const xmlPart = buildXmlStructure( itemXPath, aValue, localGroupIndices ); xml = mergeXml(xml, xmlPart); }); } else { const xmlPart = buildXmlStructure( resolvedXPath, aValue, 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( value, field.shape || {}, options, localGroupIndices, fullData ); xml = mergeXml(xml, childXml); } else { const xmlPart = buildXmlStructure( resolvedXPath, value, localGroupIndices ); xml = mergeXml(xml, xmlPart); } } processAdditionalXml(value); if (field?.type === "object" && field?.shape) { const childXml = parseSchema( 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( transformedItem, field.shape || {}, options, newGroupIndices, fullData ); xml = mergeXml(xml, childXml); if (field.additionalXml) { processAdditionalXml(value); } }); } }; 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) { throw new ZugferdError( "INVALID_FIELD", `${key} - ${error.errors[0].message}` ); } rawValue = data2; } const _value = field.type !== "object" ? rawValue ?? field.defaultValue : typeof rawValue === "object" ? Object.keys(rawValue).length <= 0 ? field.defaultValue : rawValue : rawValue; const value = field.transform?.input ? field.transform.input(_value) : _value; processField(field, value, key); } 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 = (doc, options) => { const parser = new XMLBuilder( defu( { ignoreAttributes: false, attributeNamePrefix: "@", textNodeName: "#", format: true, suppressBooleanAttributes: false, suppressEmptyNode: true }, options ) ); return parser.build(doc); }; // 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 = (pdfDoc, metadata) => { let xmp = formatXml({ "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"?>`; 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); return pdfDoc; }; var addPdfTrailerInfoId = (pdfDoc, trailerInfoId) => { const hash = crypto.createHash("SHA256").update(trailerInfoId + (/* @__PURE__ */ new Date()).toISOString()).digest(); const hashArr = Array.from(new Uint8Array(hash)); const hashHex = hashArr.map((b) => b.toString(16).padStart(2, "0")).join(""); const permanentDocId = PDFHexString.of(hashHex); const changingDocId = permanentDocId; pdfDoc.context.trailerInfo.ID = pdfDoc.context.obj([ permanentDocId, changingDocId ]); return pdfDoc; }; var fixPdfLinkAnnotations = (pdfDoc) => { const pages = pdfDoc.getPages(); for (const page of pages) { 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")) { 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 = (pdfDoc) => { const rootRef = pdfDoc.context.obj({ Marked: true }); pdfDoc.catalog.set(PDFName.of("MarkInfo"), rootRef); return pdfDoc; }; var addPdfStructTreeRoot = (pdfDoc) => { 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 = (pdfDoc) => { 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]) ); return pdfDoc; }; var getRawAttachments = (pdfDoc) => { if (!pdfDoc.catalog.has(PDFName.of("Names"))) { return []; } const Names = pdfDoc.catalog.lookup(PDFName.of("Names"), PDFDict); if (!Names.has(PDFName.of("EmbeddedFiles"))) { return []; } const EmbeddedFiles = Names.lookup(PDFName.of("EmbeddedFiles"), PDFDict); if (!EmbeddedFiles.has(PDFName.of("Names"))) { 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 }); } return rawAttachments; }; var getPdfAttachments = (pdfDoc) => { const rawAttachments = getRawAttachments(pdfDoc); return rawAttachments.map(({ fileName, fileSpec }) => { const stream = fileSpec.lookup(PDFName.of("EF"), PDFDict).lookup(PDFName.of("F"), PDFStream); 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) { return true; } return ctx.options.profile.validate(data); }; // 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; }; return { toObj, toXML, 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); await pdfDoc.attach(Buffer.from(xml), profile.documentFileName, { mimeType: "application/xml", description: "Factur-X", creationDate: metadata.createDate, modificationDate: metadata.modifyDate, afRelationship: AFRelationship.Alternative }); 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); } 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: String(parseInt(profile.version)) } }); for (const item of opts.additionalFiles || []) { const { filename, content, relationship, ...fileOptions } = item; await pdfDoc.attach(content, filename, { ...fileOptions, afRelationship: !!relationship ? AFRelationship[relationship] : AFRelationship.Unspecified }); } return await pdfDoc.save(); } }; }; // src/init.ts var init = (options) => { const ctx = { options, ...getInternalTools(options) }; const context = { ...ctx, document: { create: createDocumentFactory(ctx, options), validate: validateDocumentFactory(ctx) } }; return context; }; var getInternalTools = (_options) => ({ parseSchema, mergeSchemas, xml: { format: formatXml }, pdf: { addMetadata: addPdfMetadata, addTrailerInfoId: addPdfTrailerInfoId, fixLinkAnnotations: fixPdfLinkAnnotations, addStructTreeRoot: addPdfStructTreeRoot, addMarkInfo: addPdfMarkInfo, addICC: addPdfICC, getAttachments: getPdfAttachments } }); // src/zugferd.ts var zugferd = (options) => { const context = init(options); const handlers = getPluginHandlers(context, options); const ctx = { context, create: context.document.create, validate: context.document.validate }; return { ...ctx, ...handlers ?? {} }; }; var getPluginHandlers = (ctx, options) => { const pluginHandlers = (options.plugins || []).reduce( (acc, plugin) => { return { ...acc, ...plugin(ctx) }; }, {} ); return pluginHandlers; }; export { ZugferdError, zugferd };