import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as plugins from '../plugins.js'; import { EInvoice } from '../../../ts/index.js'; import { CorpusLoader } from '../../helpers/corpus.loader.js'; import { PerformanceTracker as StaticPerformanceTracker } from '../performance.tracker.js'; import { rgb } from 'pdf-lib'; // Simple instance-based performance tracker for this test class SimplePerformanceTracker { private measurements: Map = new Map(); private name: string; constructor(name: string) { this.name = name; } addMeasurement(key: string, time: number): void { if (!this.measurements.has(key)) { this.measurements.set(key, []); } this.measurements.get(key)!.push(time); } getAverageTime(): number { let total = 0; let count = 0; for (const times of this.measurements.values()) { for (const time of times) { total += time; count++; } } return count > 0 ? total / count : 0; } printSummary(): void { console.log(`\n${this.name} - Performance Summary:`); for (const [key, times] of this.measurements) { const avg = times.reduce((a, b) => a + b, 0) / times.length; const min = Math.min(...times); const max = Math.max(...times); console.log(` ${key}: avg=${avg.toFixed(2)}ms, min=${min.toFixed(2)}ms, max=${max.toFixed(2)}ms (${times.length} runs)`); } console.log(` Overall average: ${this.getAverageTime().toFixed(2)}ms`); } } const performanceTracker = new SimplePerformanceTracker('PDF-11: PDF/A Compliance'); tap.test('PDF-11: PDF/A-3 Creation and Validation', async () => { const startTime = performance.now(); const { PDFDocument } = plugins; const pdfDoc = await PDFDocument.create(); // PDF/A-3 allows embedded files (required for ZUGFeRD/Factur-X) // Set PDF/A identification pdfDoc.setTitle('PDF/A-3 Compliant Invoice'); pdfDoc.setAuthor('EInvoice System'); pdfDoc.setSubject('Electronic Invoice with embedded XML'); pdfDoc.setKeywords(['PDF/A-3', 'ZUGFeRD', 'Factur-X', 'invoice']); pdfDoc.setCreator('EInvoice PDF/A Generator'); pdfDoc.setProducer('PDFLib with PDF/A-3 compliance'); // Add required metadata for PDF/A const creationDate = new Date('2025-01-25T10:00:00Z'); const modDate = new Date('2025-01-25T10:00:00Z'); pdfDoc.setCreationDate(creationDate); pdfDoc.setModificationDate(modDate); // Create page with required elements for PDF/A const page = pdfDoc.addPage([595, 842]); // A4 // Use embedded fonts (required for PDF/A) const helveticaFont = await pdfDoc.embedFont('Helvetica'); // Add content page.drawText('PDF/A-3 Compliant Invoice', { x: 50, y: 750, size: 20, font: helveticaFont }); page.drawText('Invoice Number: INV-2025-001', { x: 50, y: 700, size: 12, font: helveticaFont }); page.drawText('This document complies with PDF/A-3 standard', { x: 50, y: 650, size: 10, font: helveticaFont }); // Add required OutputIntent for PDF/A // Note: pdf-lib doesn't directly support OutputIntent // In production, a specialized library would be needed // Embed invoice XML (allowed in PDF/A-3) const xmlContent = ` INV-2025-001 380 20250125 `; await pdfDoc.attach( Buffer.from(xmlContent, 'utf8'), 'invoice.xml', { mimeType: 'application/xml', description: 'ZUGFeRD invoice data', afRelationship: plugins.AFRelationship.Data, creationDate: creationDate, modificationDate: modDate } ); const pdfBytes = await pdfDoc.save(); // Verify basic structure expect(pdfBytes.length).toBeGreaterThan(0); console.log('Created PDF/A-3 structure (full compliance requires specialized tools)'); const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('pdfa3-creation', elapsed); }); tap.test('PDF-11: PDF/A-1b compliance check', async () => { const startTime = performance.now(); const { PDFDocument } = plugins; const pdfDoc = await PDFDocument.create(); // PDF/A-1b: Basic compliance (visual appearance preservation) pdfDoc.setTitle('PDF/A-1b Test Document'); pdfDoc.setCreationDate(new Date()); const page = pdfDoc.addPage(); // PDF/A-1b requirements: // - All fonts must be embedded // - No transparency // - No JavaScript // - No audio/video // - No encryption // - Proper color space definition const helveticaFont = await pdfDoc.embedFont('Helvetica'); page.drawText('PDF/A-1b Compliant Document', { x: 50, y: 750, size: 16, font: helveticaFont, color: rgb(0, 0, 0) // RGB color space }); // Add text without transparency page.drawText('No transparency allowed in PDF/A-1b', { x: 50, y: 700, size: 12, font: helveticaFont, color: rgb(0, 0, 0), opacity: 1.0 // Full opacity required }); // Draw rectangle without transparency page.drawRectangle({ x: 50, y: 600, width: 200, height: 50, color: rgb(0.9, 0.9, 0.9), borderColor: rgb(0, 0, 0), borderWidth: 1, opacity: 1.0 }); const pdfBytes = await pdfDoc.save(); // Check for PDF/A-1b violations const pdfString = pdfBytes.toString('binary'); // Check for prohibited features const violations = []; if (pdfString.includes('/JS')) violations.push('JavaScript detected'); if (pdfString.includes('/Launch')) violations.push('External launch action detected'); if (pdfString.includes('/Sound')) violations.push('Sound annotation detected'); if (pdfString.includes('/Movie')) violations.push('Movie annotation detected'); if (pdfString.includes('/Encrypt')) violations.push('Encryption detected'); console.log('PDF/A-1b compliance check:'); if (violations.length === 0) { console.log('No obvious violations detected'); } else { console.log('Potential violations:', violations); } const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('pdfa1b-compliance', elapsed); }); tap.test('PDF-11: PDF/A metadata requirements', async () => { const startTime = performance.now(); const { PDFDocument } = plugins; const pdfDoc = await PDFDocument.create(); // Required XMP metadata for PDF/A const xmpMetadata = ` PDF/A Compliant Invoice EInvoice System Invoice with PDF/A compliance 3 B 2025-01-25T10:00:00Z 2025-01-25T10:00:00Z 2025-01-25T10:00:00Z EInvoice PDF/A Generator `; // Set standard metadata pdfDoc.setTitle('PDF/A Compliant Invoice'); pdfDoc.setAuthor('EInvoice System'); pdfDoc.setSubject('Invoice with PDF/A compliance'); pdfDoc.setKeywords(['PDF/A', 'invoice', 'compliant']); const page = pdfDoc.addPage(); page.drawText('Document with PDF/A Metadata', { x: 50, y: 750, size: 16 }); // Note: pdf-lib doesn't support direct XMP metadata embedding // This would require post-processing or a specialized library console.log('PDF/A metadata structure defined (requires specialized tools for embedding)'); const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('pdfa-metadata', elapsed); }); tap.test('PDF-11: Color space compliance', async () => { const startTime = performance.now(); const { PDFDocument } = plugins; const pdfDoc = await PDFDocument.create(); const page = pdfDoc.addPage(); // PDF/A requires proper color space definitions // Test different color spaces // Device RGB (most common for screen display) page.drawText('Device RGB Color Space', { x: 50, y: 750, size: 14, color: rgb(0.8, 0.2, 0.2) }); // Grayscale page.drawText('Device Gray Color Space', { x: 50, y: 700, size: 14, color: rgb(0.5, 0.5, 0.5) }); // Test color accuracy const colors = [ { name: 'Pure Red', rgb: rgb(1, 0, 0) }, { name: 'Pure Green', rgb: rgb(0, 1, 0) }, { name: 'Pure Blue', rgb: rgb(0, 0, 1) }, { name: 'Black', rgb: rgb(0, 0, 0) }, { name: 'White', rgb: rgb(1, 1, 1) } ]; let yPos = 600; colors.forEach(color => { page.drawRectangle({ x: 50, y: yPos, width: 30, height: 20, color: color.rgb }); page.drawText(color.name, { x: 90, y: yPos + 5, size: 10, color: rgb(0, 0, 0) }); yPos -= 30; }); // Add OutputIntent description page.drawText('OutputIntent: sRGB IEC61966-2.1', { x: 50, y: 400, size: 10, color: rgb(0, 0, 0) }); const pdfBytes = await pdfDoc.save(); console.log('Created PDF with color space definitions for PDF/A'); const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('color-space', elapsed); }); tap.test('PDF-11: Font embedding compliance', async () => { const startTime = performance.now(); const { PDFDocument } = plugins; const pdfDoc = await PDFDocument.create(); // PDF/A requires all fonts to be embedded const page = pdfDoc.addPage(); // Embed standard fonts const helvetica = await pdfDoc.embedFont('Helvetica'); const helveticaBold = await pdfDoc.embedFont('Helvetica-Bold'); const helveticaOblique = await pdfDoc.embedFont('Helvetica-Oblique'); const timesRoman = await pdfDoc.embedFont('Times-Roman'); const courier = await pdfDoc.embedFont('Courier'); // Use embedded fonts page.drawText('Helvetica Regular (Embedded)', { x: 50, y: 750, size: 14, font: helvetica }); page.drawText('Helvetica Bold (Embedded)', { x: 50, y: 720, size: 14, font: helveticaBold }); page.drawText('Helvetica Oblique (Embedded)', { x: 50, y: 690, size: 14, font: helveticaOblique }); page.drawText('Times Roman (Embedded)', { x: 50, y: 660, size: 14, font: timesRoman }); page.drawText('Courier (Embedded)', { x: 50, y: 630, size: 14, font: courier }); // Test font subset embedding page.drawText('Font Subset Test: €£¥§¶•', { x: 50, y: 580, size: 14, font: helvetica }); const pdfBytes = await pdfDoc.save(); // Check that the PDF was created successfully with fonts // pdf-lib handles font embedding internally for standard fonts console.log(`PDF size: ${pdfBytes.length} bytes`); // A PDF with text content should be larger than a minimal empty PDF expect(pdfBytes.length).toBeGreaterThan(1000); // Also verify the PDF is valid expect(pdfBytes[0]).toEqual(0x25); // % expect(pdfBytes[1]).toEqual(0x50); // P expect(pdfBytes[2]).toEqual(0x44); // D expect(pdfBytes[3]).toEqual(0x46); // F const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('font-embedding', elapsed); }); tap.test('PDF-11: PDF/A-3 with ZUGFeRD attachment', async () => { const startTime = performance.now(); const { PDFDocument, AFRelationship } = plugins; const pdfDoc = await PDFDocument.create(); // Configure for ZUGFeRD/Factur-X compliance pdfDoc.setTitle('ZUGFeRD Invoice PDF/A-3'); pdfDoc.setAuthor('ZUGFeRD Generator'); pdfDoc.setSubject('Electronic Invoice with embedded XML'); pdfDoc.setKeywords(['ZUGFeRD', 'PDF/A-3', 'Factur-X', 'electronic invoice']); pdfDoc.setCreator('EInvoice ZUGFeRD Module'); const page = pdfDoc.addPage(); const helvetica = await pdfDoc.embedFont('Helvetica'); // Invoice header page.drawText('RECHNUNG / INVOICE', { x: 50, y: 750, size: 20, font: helvetica }); page.drawText('Rechnungsnummer / Invoice No: 2025-001', { x: 50, y: 700, size: 12, font: helvetica }); page.drawText('Rechnungsdatum / Invoice Date: 25.01.2025', { x: 50, y: 680, size: 12, font: helvetica }); // ZUGFeRD XML attachment const zugferdXml = ` urn:cen.eu:en16931:2017#conformant#urn:zugferd.de:2p1:extended 2025-001 380 20250125 `; // Attach with proper relationship for ZUGFeRD await pdfDoc.attach( Buffer.from(zugferdXml, 'utf8'), 'zugferd-invoice.xml', { mimeType: 'application/xml', description: 'ZUGFeRD Invoice Data', afRelationship: AFRelationship.Data } ); const pdfBytes = await pdfDoc.save(); // Test loading const einvoice = await EInvoice.fromPdf(pdfBytes); console.log('Created PDF/A-3 compliant ZUGFeRD invoice'); const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('zugferd-pdfa3', elapsed); }); tap.test('PDF-11: Corpus PDF/A compliance check', async () => { const startTime = performance.now(); let pdfaCount = 0; let processedCount = 0; const complianceIndicators = { 'PDF/A identification': 0, 'Embedded fonts': 0, 'No encryption': 0, 'Metadata present': 0, 'Color space defined': 0 }; // Get PDF files from different categories const categories = ['ZUGFERD_V1_CORRECT', 'ZUGFERD_V2_CORRECT', 'ZUGFERD_V2_FAIL', 'UNSTRUCTURED'] as const; const allPdfFiles: Array<{ path: string; size: number }> = []; for (const category of categories) { try { const files = await CorpusLoader.loadCategory(category); const pdfFiles = files.filter(f => f.path.toLowerCase().endsWith('.pdf')); allPdfFiles.push(...pdfFiles); } catch (error) { console.log(`Could not load category ${category}: ${error.message}`); } } // Sample PDFs for PDF/A compliance indicators const sampleSize = Math.min(40, allPdfFiles.length); const sample = allPdfFiles.slice(0, sampleSize); for (const file of sample) { try { const content = await CorpusLoader.loadFile(file.path); const pdfString = content.toString('binary'); // Check for PDF/A indicators let isPdfA = false; if (pdfString.includes('pdfaid:part') || pdfString.includes('PDF/A')) { isPdfA = true; complianceIndicators['PDF/A identification']++; } if (pdfString.includes('/Type /Font') && pdfString.includes('/FontFile')) { complianceIndicators['Embedded fonts']++; } if (!pdfString.includes('/Encrypt')) { complianceIndicators['No encryption']++; } if (pdfString.includes('/Metadata') || pdfString.includes('xmpmeta')) { complianceIndicators['Metadata present']++; } if (pdfString.includes('/OutputIntent') || pdfString.includes('/ColorSpace')) { complianceIndicators['Color space defined']++; } if (isPdfA) { pdfaCount++; console.log(`Potential PDF/A file: ${file.path}`); } processedCount++; } catch (error) { console.log(`Error checking ${file.path}:`, error.message); } } console.log(`Corpus PDF/A analysis (${processedCount} PDFs):`); console.log(`- Potential PDF/A files: ${pdfaCount}`); console.log('Compliance indicators:', complianceIndicators); const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('corpus-pdfa', elapsed); }); tap.test('PDF-11: Performance Summary', async () => { // Print performance summary performanceTracker.printSummary(); // Performance assertions const avgTime = performanceTracker.getAverageTime(); expect(avgTime).toBeLessThan(400); // PDF/A operations may take longer }); tap.start();