242 lines
7.1 KiB
TypeScript
242 lines
7.1 KiB
TypeScript
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<PDFEmbedResult> {
|
|
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<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;
|
|
}
|
|
|
|
// 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('<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
|
|
}
|
|
};
|
|
}
|
|
} |