import { decodePDFRawStream, PDFDict, PDFDocument, PDFHexString, PDFName, PDFRawStream, PDFString, } from "pdf-lib" export type ElectronicInvoiceSyntax = "cii" | "ubl-invoice" | "ubl-credit-note" export type ElectronicInvoiceDetection = { container: "pdf" | "xml" syntax: ElectronicInvoiceSyntax xml: Buffer attachmentName: string | null } type FileLike = { name?: string | null path?: string | null mimeType?: string | null } const decodePdfText = (value: unknown): string | null => { if (value instanceof PDFString || value instanceof PDFHexString) { return value.decodeText() } return null } const detectXmlSyntax = (xml: Buffer): ElectronicInvoiceSyntax | null => { const sample = xml.subarray(0, Math.min(xml.length, 64 * 1024)).toString("utf8") if (/<(?:[A-Za-z_][\w.-]*:)?CrossIndustryInvoice(?:\s|>)/i.test(sample)) { return "cii" } if ( /<(?:[A-Za-z_][\w.-]*:)?Invoice(?:\s|>)/i.test(sample) && /urn:oasis:names:specification:ubl:schema:xsd:Invoice-2/i.test(sample) ) { return "ubl-invoice" } if ( /<(?:[A-Za-z_][\w.-]*:)?CreditNote(?:\s|>)/i.test(sample) && /urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2/i.test(sample) ) { return "ubl-credit-note" } return null } const extractXmlFromFileSpec = (fileSpec: PDFDict): { name: string | null, xml: Buffer } | null => { const context = fileSpec.context const embeddedFiles = context.lookupMaybe(fileSpec.get(PDFName.of("EF")), PDFDict) if (!embeddedFiles) return null const stream = context.lookup( embeddedFiles.get(PDFName.of("UF")) || embeddedFiles.get(PDFName.of("F")), ) if (!(stream instanceof PDFRawStream)) return null const name = decodePdfText( context.lookup(fileSpec.get(PDFName.of("UF")) || fileSpec.get(PDFName.of("F"))), ) const contents = Buffer.from(decodePDFRawStream(stream).decode()) return { name, xml: contents } } const detectEmbeddedInvoice = async (pdf: Buffer): Promise => { let document: PDFDocument try { document = await PDFDocument.load(pdf, { ignoreEncryption: true, updateMetadata: false, }) } catch { return null } for (const [, object] of document.context.enumerateIndirectObjects()) { if (!(object instanceof PDFDict)) continue const type = object.get(PDFName.of("Type")) if (!(type instanceof PDFName) || type.asString() !== "/Filespec") continue const embedded = extractXmlFromFileSpec(object) if (!embedded) continue const syntax = detectXmlSyntax(embedded.xml) if (!syntax) continue return { container: "pdf", syntax, xml: embedded.xml, attachmentName: embedded.name, } } return null } const hasPdfFileType = (file: FileLike, data: Buffer) => { const filename = String(file.name || file.path || "").toLowerCase() const mimeType = String(file.mimeType || "").toLowerCase().split(";")[0].trim() return filename.endsWith(".pdf") || mimeType === "application/pdf" || data.subarray(0, 5).toString("ascii") === "%PDF-" } export const detectElectronicInvoice = async ( data: Buffer, file: FileLike, ): Promise => { if (hasPdfFileType(file, data)) { return detectEmbeddedInvoice(data) } const beginsLikeXml = data.subarray(0, 1024).toString("utf8").replace(/^\uFEFF/, "").trimStart().startsWith("<") const syntax = beginsLikeXml ? detectXmlSyntax(data) : null if (syntax) { return { container: "xml", syntax, xml: data, attachmentName: file.name || file.path?.split("/").pop() || null, } } return null }