UNPKG

@fin.cx/einvoice

Version:

A TypeScript module for creating, manipulating, and embedding XML data within PDF files specifically tailored for electronic invoice (einvoice) packages.

319 lines 21.1 kB
import { PDFDocument, PDFDict, PDFName, PDFRawStream, PDFArray, PDFString, pako } from '../../../plugins.js'; /** * Base class for PDF XML extractors with common functionality */ export class BaseXMLExtractor { constructor() { /** * Known XML file names for different invoice formats */ this.knownFileNames = [ 'factur-x.xml', 'zugferd-invoice.xml', 'ZUGFeRD-invoice.xml', 'xrechnung.xml', 'ubl-invoice.xml', 'invoice.xml', 'metadata.xml' ]; /** * Known XML formats to validate extracted content */ this.knownFormats = [ 'CrossIndustryInvoice', 'CrossIndustryDocument', 'Invoice', 'CreditNote', 'ubl:Invoice', 'ubl:CreditNote', 'rsm:CrossIndustryInvoice', 'rsm:CrossIndustryDocument', 'ram:CrossIndustryDocument', 'urn:un:unece:uncefact', 'urn:ferd:CrossIndustryDocument', 'urn:zugferd', 'urn:factur-x', 'factur-x.eu', 'ZUGFeRD', 'FatturaElettronica' ]; /** * Known XML end tags for extracting content from strings */ this.knownEndTags = [ '</CrossIndustryInvoice>', '</CrossIndustryDocument>', '</Invoice>', '</CreditNote>', '</rsm:CrossIndustryInvoice>', '</rsm:CrossIndustryDocument>', '</ram:CrossIndustryDocument>', '</ubl:Invoice>', '</ubl:CreditNote>', '</FatturaElettronica>' ]; } /** * Check if an XML string is valid * @param xmlString XML string to check * @returns True if the XML is valid */ isValidXml(xmlString) { try { // Basic checks for XML validity if (!xmlString || typeof xmlString !== 'string') { return false; } // Check if it starts with XML declaration or a valid element if (!xmlString.includes('<?xml') && !this.hasKnownXmlElement(xmlString)) { return false; } // Check if the XML string contains known invoice formats const hasKnownFormat = this.hasKnownFormat(xmlString); if (!hasKnownFormat) { return false; } // Check if the XML string contains binary data or invalid characters if (this.hasBinaryData(xmlString)) { return false; } // Check if the XML string is too short if (xmlString.length < 100) { return false; } // Check if XML has a proper structure (contains both opening and closing tags) if (!this.hasProperXmlStructure(xmlString)) { return false; } return true; } catch (error) { console.error('Error validating XML:', error); return false; } } /** * Check if the XML string contains a known element * @param xmlString XML string to check * @returns True if the XML contains a known element */ hasKnownXmlElement(xmlString) { for (const format of this.knownFormats) { // Check for opening tag of format if (xmlString.includes(`<${format}`)) { return true; } } return false; } /** * Check if the XML string contains a known format * @param xmlString XML string to check * @returns True if the XML contains a known format */ hasKnownFormat(xmlString) { for (const format of this.knownFormats) { if (xmlString.includes(format)) { return true; } } return false; } /** * Check if the XML string has a proper structure * @param xmlString XML string to check * @returns True if the XML has a proper structure */ hasProperXmlStructure(xmlString) { // Check for at least one matching opening and closing tag for (const endTag of this.knownEndTags) { const startTag = endTag.replace('/', ''); if (xmlString.includes(startTag) && xmlString.includes(endTag)) { return true; } } // If no specific tag is found but it has a basic XML structure return ((xmlString.includes('<?xml') && xmlString.includes('?>')) || (xmlString.match(/<[^>]+>/) !== null && xmlString.match(/<\/[^>]+>/) !== null)); } /** * Check if the XML string contains binary data * @param xmlString XML string to check * @returns True if the XML contains binary data */ hasBinaryData(xmlString) { // Check for common binary data indicators const binaryChars = ['\u0000', '\u0001', '\u0002', '\u0003', '\u0004', '\u0005']; const consecutiveNulls = '\u0000\u0000\u0000'; // Check for control characters that shouldn't be in XML if (binaryChars.some(char => xmlString.includes(char))) { return true; } // Check for consecutive null bytes which indicate binary data if (xmlString.includes(consecutiveNulls)) { return true; } // Check for high concentration of non-printable characters const nonPrintableCount = (xmlString.match(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g) || []).length; if (nonPrintableCount > xmlString.length * 0.05) { // More than 5% non-printable return true; } return false; } /** * Extract XML from a string * @param text Text to extract XML from * @param startIndex Index to start extraction from * @returns XML content or null if not found */ extractXmlFromString(text, startIndex = 0) { try { // Find the start of the XML document let xmlStartIndex = text.indexOf('<?xml', startIndex); // If no XML declaration, try to find known elements if (xmlStartIndex === -1) { for (const format of this.knownFormats) { const formatStartIndex = text.indexOf(`<${format.split(':').pop()}`, startIndex); if (formatStartIndex !== -1) { xmlStartIndex = formatStartIndex; break; } } // Still didn't find any start marker if (xmlStartIndex === -1) { return null; } } // Try to find the end of the XML document let xmlEndIndex = -1; for (const endTag of this.knownEndTags) { const endIndex = text.indexOf(endTag, xmlStartIndex); if (endIndex !== -1) { xmlEndIndex = endIndex + endTag.length; break; } } // If no known end tag found, try to use a heuristic approach if (xmlEndIndex === -1) { // Try to find the last closing tag const lastClosingTagMatch = text.slice(xmlStartIndex).match(/<\/[^>]+>(?!.*<\/[^>]+>)/); if (lastClosingTagMatch && lastClosingTagMatch.index !== undefined) { xmlEndIndex = xmlStartIndex + lastClosingTagMatch.index + lastClosingTagMatch[0].length; } else { return null; } } // Extract the XML content const xmlContent = text.substring(xmlStartIndex, xmlEndIndex); // Validate the extracted content if (this.isValidXml(xmlContent)) { return xmlContent; } return null; } catch (error) { console.error('Error extracting XML from string:', error); return null; } } /** * Decompress and decode XML content from a PDF stream * @param stream PDF stream containing XML data * @param fileName Name of the file (for logging) * @returns XML content or null if not valid */ async extractXmlFromStream(stream, fileName) { try { // Get the raw bytes from the stream const rawBytes = stream.getContents(); // First try without decompression (in case the content is not compressed) let xmlContent = this.tryDecodeBuffer(rawBytes); if (xmlContent && this.isValidXml(xmlContent)) { console.log(`Successfully extracted uncompressed XML from PDF file. File name: ${fileName}`); return xmlContent; } // Try with decompression try { const decompressedBytes = this.tryDecompress(rawBytes); if (decompressedBytes) { xmlContent = this.tryDecodeBuffer(decompressedBytes); if (xmlContent && this.isValidXml(xmlContent)) { console.log(`Successfully extracted decompressed XML from PDF file. File name: ${fileName}`); return xmlContent; } } } catch (decompressError) { console.log(`Decompression failed for ${fileName}: ${decompressError}`); } return null; } catch (error) { console.error('Error extracting XML from stream:', error); return null; } } /** * Try to decompress a buffer using different methods * @param buffer Buffer to decompress * @returns Decompressed buffer or null if decompression failed */ tryDecompress(buffer) { try { // Try pako inflate (for deflate/zlib compression) return pako.inflate(buffer); } catch (error) { // If pako fails, try other methods if needed console.warn('Pako decompression failed, might be uncompressed or using a different algorithm'); return null; } } /** * Try to decode a buffer to a string using different encodings * @param buffer Buffer to decode * @returns Decoded string or null if decoding failed */ tryDecodeBuffer(buffer) { try { // Try UTF-8 first let content = new TextDecoder('utf-8').decode(buffer); if (this.isPlausibleXml(content)) { return content; } // Try ISO-8859-1 (Latin1) content = this.decodeLatin1(buffer); if (this.isPlausibleXml(content)) { return content; } return null; } catch (error) { console.warn('Error decoding buffer:', error); return null; } } /** * Decode a buffer using ISO-8859-1 (Latin1) encoding * @param buffer Buffer to decode * @returns Decoded string */ decodeLatin1(buffer) { return Array.from(buffer) .map(byte => String.fromCharCode(byte)) .join(''); } /** * Check if a string is plausibly XML (quick check before validation) * @param content String to check * @returns True if the string is plausibly XML */ isPlausibleXml(content) { return content.includes('<') && content.includes('>') && (content.includes('<?xml') || this.knownFormats.some(format => content.includes(format))); } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmFzZS5leHRyYWN0b3IuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi90cy9mb3JtYXRzL3BkZi9leHRyYWN0b3JzL2Jhc2UuZXh0cmFjdG9yLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxXQUFXLEVBQUUsT0FBTyxFQUFFLE9BQU8sRUFBRSxZQUFZLEVBQUUsUUFBUSxFQUFFLFNBQVMsRUFBRSxJQUFJLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUU3Rzs7R0FFRztBQUNILE1BQU0sT0FBZ0IsZ0JBQWdCO0lBQXRDO1FBQ0U7O1dBRUc7UUFDZ0IsbUJBQWMsR0FBRztZQUNsQyxjQUFjO1lBQ2QscUJBQXFCO1lBQ3JCLHFCQUFxQjtZQUNyQixlQUFlO1lBQ2YsaUJBQWlCO1lBQ2pCLGFBQWE7WUFDYixjQUFjO1NBQ2YsQ0FBQztRQUVGOztXQUVHO1FBQ2dCLGlCQUFZLEdBQUc7WUFDaEMsc0JBQXNCO1lBQ3RCLHVCQUF1QjtZQUN2QixTQUFTO1lBQ1QsWUFBWTtZQUNaLGFBQWE7WUFDYixnQkFBZ0I7WUFDaEIsMEJBQTBCO1lBQzFCLDJCQUEyQjtZQUMzQiwyQkFBMkI7WUFDM0IsdUJBQXVCO1lBQ3ZCLGdDQUFnQztZQUNoQyxhQUFhO1lBQ2IsY0FBYztZQUNkLGFBQWE7WUFDYixTQUFTO1lBQ1Qsb0JBQW9CO1NBQ3JCLENBQUM7UUFFRjs7V0FFRztRQUNnQixpQkFBWSxHQUFHO1lBQ2hDLHlCQUF5QjtZQUN6QiwwQkFBMEI7WUFDMUIsWUFBWTtZQUNaLGVBQWU7WUFDZiw2QkFBNkI7WUFDN0IsOEJBQThCO1lBQzlCLDhCQUE4QjtZQUM5QixnQkFBZ0I7WUFDaEIsbUJBQW1CO1lBQ25CLHVCQUF1QjtTQUN4QixDQUFDO0lBMlNKLENBQUM7SUFsU0M7Ozs7T0FJRztJQUNPLFVBQVUsQ0FBQyxTQUFpQjtRQUNwQyxJQUFJLENBQUM7WUFDSCxnQ0FBZ0M7WUFDaEMsSUFBSSxDQUFDLFNBQVMsSUFBSSxP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