import { PDFDocument, AFRelationship } from '../../plugins.js'; import type { IPdf } from '../../interfaces/common.js'; /** * Error types for PDF embedding operations */ export enum PDFEmbedError { LOAD_ERROR = 'PDF loading failed', EMBED_ERROR = 'XML embedding failed', SAVE_ERROR = 'PDF saving failed', INVALID_INPUT = 'Invalid input parameters' } /** * Result of a PDF embedding operation */ export interface PDFEmbedResult { success: boolean; data?: Uint8Array; pdf?: IPdf; error?: { type: PDFEmbedError; message: string; originalError?: Error; }; } /** * Class for embedding XML into PDF files * Provides robust error handling and support for different PDF formats */ export class PDFEmbedder { /** * Embeds XML into a PDF * @param pdfBuffer PDF buffer * @param xmlContent XML content to embed * @param filename Filename for the embedded XML * @param description Description for the embedded XML * @returns Result with either modified PDF buffer or error information */ public async embedXml( pdfBuffer: Uint8Array | Buffer, xmlContent: string, filename: string = 'invoice.xml', description: string = 'XML Invoice' ): Promise { try { // Validate inputs if (!pdfBuffer || pdfBuffer.length === 0) { return this.createErrorResult(PDFEmbedError.INVALID_INPUT, 'PDF buffer is empty or undefined'); } if (!xmlContent) { return this.createErrorResult(PDFEmbedError.INVALID_INPUT, 'XML content is empty or undefined'); } // Ensure buffer is Uint8Array const pdfBufferArray = Buffer.isBuffer(pdfBuffer) ? new Uint8Array(pdfBuffer) : pdfBuffer; // Load the PDF let pdfDoc: PDFDocument; try { pdfDoc = await PDFDocument.load(pdfBufferArray, { ignoreEncryption: true, // Try to load encrypted PDFs updateMetadata: false // Don't automatically update metadata }); } catch (error) { return this.createErrorResult( PDFEmbedError.LOAD_ERROR, `Failed to load PDF: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined ); } // Normalize filename (lowercase with XML extension) filename = this.normalizeFilename(filename); // Convert the XML string to a Uint8Array const xmlBuffer = new TextEncoder().encode(xmlContent); try { // Use pdf-lib's .attach() to embed the XML pdfDoc.attach(xmlBuffer, filename, { mimeType: 'text/xml', description: description, creationDate: new Date(), modificationDate: new Date(), afRelationship: AFRelationship.Alternative, }); } catch (error) { return this.createErrorResult( PDFEmbedError.EMBED_ERROR, `Failed to embed XML: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined ); } // Save the modified PDF let modifiedPdfBytes: Uint8Array; try { modifiedPdfBytes = await pdfDoc.save({ addDefaultPage: false, // Don't add a page if the document is empty useObjectStreams: false, // Better compatibility with older PDF readers updateFieldAppearances: false // Don't update form fields }); } catch (error) { return this.createErrorResult( PDFEmbedError.SAVE_ERROR, `Failed to save modified PDF: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined ); } return { success: true, data: modifiedPdfBytes }; } catch (error) { // Catch any uncaught errors return this.createErrorResult( PDFEmbedError.EMBED_ERROR, `Unexpected error during XML embedding: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined ); } } /** * Creates an IPdf object with embedded XML * @param pdfBuffer PDF buffer * @param xmlContent XML content to embed * @param filename Filename for the embedded XML * @param description Description for the embedded XML * @param pdfName Name for the PDF * @param pdfId ID for the PDF * @returns Result with either IPdf object or error information */ public async createPdfWithXml( pdfBuffer: Uint8Array | Buffer, xmlContent: string, filename: string = 'invoice.xml', description: string = 'XML Invoice', pdfName: string = 'invoice.pdf', pdfId: string = `invoice-${Date.now()}` ): Promise { // Embed XML into PDF const embedResult = await this.embedXml(pdfBuffer, xmlContent, filename, description); // If embedding failed, return the error if (!embedResult.success || !embedResult.data) { return embedResult; } // Create IPdf object const pdfObject: IPdf = { name: pdfName, id: pdfId, metadata: { textExtraction: '', format: this.detectPdfFormat(xmlContent), embeddedXml: { filename: filename, description: description } }, buffer: embedResult.data }; return { success: true, pdf: pdfObject }; } /** * Ensures the filename is normalized according to PDF/A requirements * @param filename Filename to normalize * @returns Normalized filename */ private normalizeFilename(filename: string): string { // Convert to lowercase let normalized = filename.toLowerCase(); // Ensure it has .xml extension if (!normalized.endsWith('.xml')) { normalized = normalized.replace(/\.[^/.]+$/, '') + '.xml'; } // Replace invalid characters normalized = normalized.replace(/[^a-z0-9_.-]/g, '_'); return normalized; } /** * Tries to detect the format of the XML content * @param xmlContent XML content * @returns Format string or undefined */ private detectPdfFormat(xmlContent: string): string | undefined { if (xmlContent.includes('factur-x.eu') || xmlContent.includes('factur-x.xml')) { return 'factur-x'; } else if (xmlContent.includes('zugferd') || xmlContent.includes('ZUGFeRD')) { return 'zugferd'; } else if (xmlContent.includes('xrechnung')) { return 'xrechnung'; } else if (xmlContent.includes('