import * as plugins from './plugins.js'; import * as interfaces from './interfaces.js'; import { PDFDocument, PDFDict, PDFName, PDFRawStream, PDFArray, PDFString, } from 'pdf-lib'; import { ZugferdXmlEncoder } from './classes.encoder.js'; export class XInvoice { private xmlString: string; private letterData: plugins.tsclass.business.ILetter; private pdfUint8Array: Uint8Array; private encoderInstance = new ZugferdXmlEncoder(); private decoderInstance constructor() { } public async addPdfBuffer(pdfBuffer: Uint8Array | Buffer): Promise { this.pdfUint8Array = Uint8Array.from(pdfBuffer); } public async addXmlString(xmlString: string): Promise { this.xmlString = xmlString; } public async addLetterData(letterData: plugins.tsclass.business.ILetter): Promise { this.letterData = letterData; } public async getXInvoice(): Promise { // lets check requirements if (!this.pdfUint8Array) { throw new Error('No PDF buffer provided!'); } if (!this.xmlString || !this.letterData) { // TODO: check if document already has xml throw new Error('No XML string or letter data provided!'); } try { const pdfDoc = await PDFDocument.load(this.pdfUint8Array); // Convert the XML string to a Uint8Array const xmlBuffer = new TextEncoder().encode(this.xmlString); // Use pdf-lib's .attach() to embed the XML pdfDoc.attach(xmlBuffer, plugins.path.basename('invoice.xml'), { mimeType: 'application/xml', description: 'XRechnung XML Invoice', }); // Save back into this.pdfUint8Array const modifiedPdfBytes = await pdfDoc.save(); this.pdfUint8Array = modifiedPdfBytes; console.log(`PDF Buffer updated with new XML attachment!`); } catch (error) { console.error('Error embedding XML into PDF:', error); throw error; } } /** * Reads only the raw XML part from the PDF and returns it as a string. */ public async getXmlData(): Promise { try { const pdfDoc = await PDFDocument.load(this.pdfUint8Array); const namesDictObj = pdfDoc.catalog.lookup(PDFName.of('Names')); if (!(namesDictObj instanceof PDFDict)) { throw new Error('No Names dictionary found in PDF!'); } const embeddedFilesDictObj = namesDictObj.lookup(PDFName.of('EmbeddedFiles')); if (!(embeddedFilesDictObj instanceof PDFDict)) { throw new Error('No EmbeddedFiles dictionary found!'); } const filesSpecObj = embeddedFilesDictObj.lookup(PDFName.of('Names')); if (!(filesSpecObj instanceof PDFArray)) { throw new Error('No files specified in EmbeddedFiles dictionary!'); } let xmlFile: PDFRawStream | undefined; for (let i = 0; i < filesSpecObj.size(); i += 2) { const fileNameObj = filesSpecObj.lookup(i); const fileSpecObj = filesSpecObj.lookup(i + 1); if (!(fileNameObj instanceof PDFString)) { continue; } if (!(fileSpecObj instanceof PDFDict)) { continue; } const efDictObj = fileSpecObj.lookup(PDFName.of('EF')); if (!(efDictObj instanceof PDFDict)) { continue; } const maybeStream = efDictObj.lookup(PDFName.of('F')); if (maybeStream instanceof PDFRawStream) { // If you only want a file named 'invoice.xml': // if (fileNameObj.value() === 'invoice.xml') { ... } xmlFile = maybeStream; break; } } if (!xmlFile) { throw new Error('XML file stream not found!'); } const xmlCompressedBytes = xmlFile.getContents().buffer; const xmlBytes = plugins.pako.inflate(xmlCompressedBytes); const xmlContent = new TextDecoder('utf-8').decode(xmlBytes); return xmlContent; } catch (error) { console.error('Error extracting or parsing embedded XML from PDF:', error); throw error; } } public async getParsedXmlData(): Promise { const smartxmlInstance = new plugins.smartxml.SmartXml(); if (!this.xmlString && !this.pdfUint8Array) { throw new Error('No XML string or PDF buffer provided!'); } let localXmlString = this.xmlString; if (!localXmlString) { localXmlString = await this.getXmlData(); } return smartxmlInstance.parseXmlToObject(localXmlString); } /** * Example method to parse the embedded XML into a structured IInvoice. * Right now, it just returns mock data. * Replace with your own XML parsing. */ private parseXmlToInvoice(xmlContent: string): interfaces.IXInvoice { // e.g. parse using DOMParser, xml2js, fast-xml-parser, etc. // For now, returning placeholder data: return { InvoiceNumber: '12345', DateIssued: '2023-04-01', Seller: { Name: 'Seller Co', Address: { Street: '1234 Market St', City: 'Sample City', PostalCode: '12345', Country: 'DE', }, Contact: { Email: 'contact@sellerco.com', Phone: '123-456-7890', }, }, Buyer: { Name: 'Buyer Inc', Address: { Street: '5678 Trade Rd', City: 'Trade City', PostalCode: '67890', Country: 'DE', }, Contact: { Email: 'info@buyerinc.com', Phone: '987-654-3210', }, }, Items: [ { Description: 'Item 1', Quantity: 10, UnitPrice: 9.99, TotalPrice: 99.9, }, ], TotalAmount: 99.9, }; } }