xinvoice/ts/formats/pdf/pdf.embedder.ts

242 lines
7.1 KiB
TypeScript
Raw Permalink Normal View History

import { PDFDocument, AFRelationship } from '../../plugins.js';
2025-04-03 15:53:08 +00:00
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;
};
}
2025-04-03 15:53:08 +00:00
/**
* Class for embedding XML into PDF files
* Provides robust error handling and support for different PDF formats
2025-04-03 15:53:08 +00:00
*/
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
2025-04-03 15:53:08 +00:00
*/
public async embedXml(
pdfBuffer: Uint8Array | Buffer,
xmlContent: string,
filename: string = 'invoice.xml',
description: string = 'XML Invoice'
): Promise<PDFEmbedResult> {
2025-04-03 15:53:08 +00:00
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;
2025-04-03 15:53:08 +00:00
// 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);
2025-04-03 15:53:08 +00:00
// 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
);
}
2025-04-03 15:53:08 +00:00
// 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
);
}
2025-04-03 15:53:08 +00:00
return {
success: true,
data: modifiedPdfBytes
};
2025-04-03 15:53:08 +00:00
} 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
);
2025-04-03 15:53:08 +00:00
}
}
/**
* 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
2025-04-03 15:53:08 +00:00
*/
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<PDFEmbedResult> {
// 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;
}
2025-04-03 15:53:08 +00:00
// Create IPdf object
const pdfObject: IPdf = {
2025-04-03 15:53:08 +00:00
name: pdfName,
id: pdfId,
metadata: {
textExtraction: '',
format: this.detectPdfFormat(xmlContent),
embeddedXml: {
filename: filename,
description: description
}
2025-04-03 15:53:08 +00:00
},
buffer: embedResult.data
};
return {
success: true,
pdf: pdfObject
2025-04-03 15:53:08 +00:00
};
}
/**
* 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('<Invoice') || xmlContent.includes('<CreditNote')) {
return 'ubl';
} else if (xmlContent.includes('FatturaElettronica')) {
return 'fatturapa';
}
return undefined;
}
/**
* Creates an error result object
* @param type Error type
* @param message Error message
* @param originalError Original error object
* @returns Error result
*/
private createErrorResult(
type: PDFEmbedError,
message: string,
originalError?: Error
): PDFEmbedResult {
console.error(`PDF Embedder Error (${type}): ${message}`);
if (originalError) {
console.error(originalError);
}
return {
success: false,
error: {
type,
message,
originalError
}
};
}
}