@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
JavaScript
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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