import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as plugins from '../plugins.js'; import { EInvoice } from '../../../ts/index.js'; import { rgb } from 'pdf-lib'; tap.test('PDF-12: Create PDFs with different version headers', async () => { const { PDFDocument } = plugins; // Test different PDF versions const versions = [ { version: '1.3', features: 'Basic PDF features, Acrobat 4.x compatible' }, { version: '1.4', features: 'Transparency, Acrobat 5.x compatible' }, { version: '1.5', features: 'Object streams, Acrobat 6.x compatible' }, { version: '1.6', features: 'OpenType fonts, Acrobat 7.x compatible' }, { version: '1.7', features: 'XFA forms, ISO 32000-1:2008 standard' } ]; for (const ver of versions) { const pdfDoc = await PDFDocument.create(); // Note: pdf-lib doesn't allow direct version setting // PDFs are typically created as 1.7 by default pdfDoc.setTitle(`PDF Version ${ver.version} Test`); pdfDoc.setSubject(ver.features); const page = pdfDoc.addPage([595, 842]); page.drawText(`PDF Version ${ver.version}`, { x: 50, y: 750, size: 24 }); page.drawText(`Features: ${ver.features}`, { x: 50, y: 700, size: 12 }); // Add version-specific content if (parseFloat(ver.version) >= 1.4) { // Transparency (PDF 1.4+) page.drawRectangle({ x: 50, y: 600, width: 200, height: 50, color: rgb(0, 0, 1), opacity: 0.5 // Transparency }); } // Add invoice XML const xmlContent = ` PDF-VER-${ver.version} Test invoice for PDF ${ver.version} ${ver.version} `; await pdfDoc.attach( Buffer.from(xmlContent, 'utf8'), 'invoice.xml', { mimeType: 'application/xml', description: `Invoice for PDF ${ver.version}` } ); const pdfBytes = await pdfDoc.save(); // Check version in output const pdfString = pdfBytes.toString().substring(0, 100); console.log(`Created PDF (declared as ${ver.version}), header: ${pdfString.substring(0, 8)}`); // Test processing try { const einvoice = await EInvoice.fromPdf(Buffer.from(pdfBytes)); // Check if XML was extracted successfully const format = einvoice.getFormat(); if (format && format !== 'unknown') { // Don't try to convert to other formats as the test XML is minimal console.log(`Version ${ver.version} - Successfully extracted XML, format: ${format}`); } else { console.log(`Version ${ver.version} - No format detected`); } } catch (error) { console.log(`Version ${ver.version} processing error:`, error.message); } } }); tap.test('PDF-12: Feature compatibility across versions', async () => { const { PDFDocument } = plugins; // Test version-specific features const featureTests = [ { name: 'Basic Features (1.3+)', test: async (pdfDoc: any) => { const page = pdfDoc.addPage(); // Basic text and graphics page.drawText('Basic Text', { x: 50, y: 700, size: 14 }); page.drawLine({ start: { x: 50, y: 680 }, end: { x: 200, y: 680 }, thickness: 1 }); } }, { name: 'Transparency (1.4+)', test: async (pdfDoc: any) => { const page = pdfDoc.addPage(); // Overlapping transparent rectangles page.drawRectangle({ x: 50, y: 600, width: 100, height: 100, color: rgb(1, 0, 0), opacity: 0.5 }); page.drawRectangle({ x: 100, y: 650, width: 100, height: 100, color: rgb(0, 0, 1), opacity: 0.5 }); } }, { name: 'Embedded Files (1.4+)', test: async (pdfDoc: any) => { // Multiple embedded files await pdfDoc.attach( Buffer.from('Primary', 'utf8'), 'primary.xml', { mimeType: 'application/xml' } ); await pdfDoc.attach( Buffer.from('Secondary', 'utf8'), 'secondary.xml', { mimeType: 'application/xml' } ); } }, { name: 'Unicode Support (1.5+)', test: async (pdfDoc: any) => { const page = pdfDoc.addPage(); try { // Standard fonts may not support all Unicode characters page.drawText('Unicode: 中文 العربية ελληνικά', { x: 50, y: 600, size: 14 }); } catch (error) { // Fallback to ASCII if Unicode fails console.log('Unicode text failed (expected with standard fonts), using fallback'); page.drawText('Unicode: [Chinese] [Arabic] [Greek]', { x: 50, y: 600, size: 14 }); } } } ]; for (const feature of featureTests) { console.log(`Testing: ${feature.name}`); const pdfDoc = await PDFDocument.create(); pdfDoc.setTitle(feature.name); await feature.test(pdfDoc); const pdfBytes = await pdfDoc.save(); expect(pdfBytes.length).toBeGreaterThan(0); } }); tap.test('PDF-12: Cross-version attachment compatibility', async () => { const { PDFDocument, AFRelationship } = plugins; // Test attachment features across versions const pdfDoc = await PDFDocument.create(); pdfDoc.setTitle('Cross-Version Attachment Test'); const page = pdfDoc.addPage(); page.drawText('PDF with Various Attachment Features', { x: 50, y: 750, size: 16 }); // Test different attachment configurations const attachmentTests = [ { name: 'Simple attachment (1.3+)', file: 'simple.xml', content: 'SIMPLE', options: { mimeType: 'application/xml' } }, { name: 'With description (1.4+)', file: 'described.xml', content: 'DESCRIBED', options: { mimeType: 'application/xml', description: 'Invoice with description' } }, { name: 'With relationship (1.7+)', file: 'related.xml', content: 'RELATED', options: { mimeType: 'application/xml', description: 'Invoice with AFRelationship', afRelationship: AFRelationship.Data } }, { name: 'With dates (1.4+)', file: 'dated.xml', content: 'DATED', options: { mimeType: 'application/xml', description: 'Invoice with timestamps', creationDate: new Date('2025-01-01'), modificationDate: new Date('2025-01-25') } } ]; let yPos = 700; for (const test of attachmentTests) { await pdfDoc.attach( Buffer.from(test.content, 'utf8'), test.file, test.options ); page.drawText(`[OK] ${test.name}`, { x: 70, y: yPos, size: 10 }); yPos -= 20; } const pdfBytes = await pdfDoc.save(); // Test extraction try { const einvoice = await EInvoice.fromPdf(Buffer.from(pdfBytes)); console.log('Cross-version attachment test completed - extracted XML'); } catch (error) { // Expected to fail as we're using minimal test XML console.log('Cross-version attachment extraction error:', error.message); } }); tap.test('PDF-12: Backward compatibility', async () => { const { PDFDocument } = plugins; // Create PDF with only features from older versions const pdfDoc = await PDFDocument.create(); pdfDoc.setTitle('Backward Compatible PDF'); pdfDoc.setAuthor('Legacy System'); pdfDoc.setSubject('PDF 1.3 Compatible Invoice'); const page = pdfDoc.addPage([612, 792]); // US Letter // Use only basic features available in PDF 1.3 const helvetica = await pdfDoc.embedFont('Helvetica'); // Simple text page.drawText('Legacy Compatible Invoice', { x: 72, y: 720, size: 18, font: helvetica, color: rgb(0, 0, 0) }); // Basic shapes without transparency page.drawRectangle({ x: 72, y: 600, width: 468, height: 100, borderColor: rgb(0, 0, 0), borderWidth: 1 }); // Simple lines page.drawLine({ start: { x: 72, y: 650 }, end: { x: 540, y: 650 }, thickness: 1, color: rgb(0, 0, 0) }); // Basic invoice data (no advanced features) const invoiceLines = [ 'Invoice Number: 2025-001', 'Date: January 25, 2025', 'Amount: $1,234.56', 'Status: PAID' ]; let yPos = 620; invoiceLines.forEach(line => { page.drawText(line, { x: 80, y: yPos, size: 12, font: helvetica, color: rgb(0, 0, 0) }); yPos -= 20; }); // Simple XML attachment const xmlContent = ` 2025-001 2025-01-25 1234.56 `; await pdfDoc.attach( Buffer.from(xmlContent, 'utf8'), 'invoice.xml', { mimeType: 'text/xml' } // Basic MIME type ); const pdfBytes = await pdfDoc.save(); // Verify it can be processed try { const einvoice = await EInvoice.fromPdf(Buffer.from(pdfBytes)); console.log('Created backward compatible PDF (1.3 features only)'); } catch (error) { // Expected to fail as we're using minimal test XML console.log('Backward compatible PDF processing error:', error.message); } }); tap.test('PDF-12: Version detection with test PDFs', async () => { const { PDFDocument } = plugins; // Create test PDFs with different features to analyze const testPdfs = [ { name: 'PDF with transparency', create: async () => { const doc = await PDFDocument.create(); const page = doc.addPage(); page.drawRectangle({ x: 50, y: 50, width: 100, height: 100, color: rgb(1, 0, 0), opacity: 0.5 }); return doc.save(); } }, { name: 'PDF with embedded files', create: async () => { const doc = await PDFDocument.create(); doc.addPage(); await doc.attach( Buffer.from('test', 'utf8'), 'test.xml', { mimeType: 'application/xml' } ); return doc.save(); } }, { name: 'PDF with forms', create: async () => { const doc = await PDFDocument.create(); const page = doc.addPage(); // Note: pdf-lib doesn't support creating forms directly page.drawText('Form placeholder', { x: 50, y: 700, size: 12 }); return doc.save(); } } ]; const versionStats: Record = {}; const featureStats = { transparency: 0, embeddedFiles: 0, compression: 0 }; for (const testPdf of testPdfs) { console.log(`Creating and analyzing: ${testPdf.name}`); const pdfBytes = await testPdf.create(); // Extract PDF version from header more carefully // Look at the first 10 bytes where the PDF header should be const headerBytes = pdfBytes.slice(0, 20); const headerString = Buffer.from(headerBytes).toString('latin1'); // Use latin1 to preserve bytes const versionMatch = headerString.match(/%PDF-(\d\.\d)/); if (versionMatch) { const version = versionMatch[1]; versionStats[version] = (versionStats[version] || 0) + 1; console.log(` Found PDF version: ${version}`); } // Check for version-specific features by searching in binary data const pdfString = Buffer.from(pdfBytes).toString('latin1'); // Use latin1 encoding if (pdfString.includes('/Group') && pdfString.includes('/S /Transparency')) { featureStats.transparency++; } if (pdfString.includes('/EmbeddedFiles')) { featureStats.embeddedFiles++; } if (pdfString.includes('/Filter') && pdfString.includes('/FlateDecode')) { featureStats.compression++; } } console.log('Test PDF version analysis:'); console.log('PDF versions found:', versionStats); console.log('Feature usage:', featureStats); // Test that we created valid PDFs (either version was detected or features were found) const hasVersions = Object.keys(versionStats).length > 0; const hasFeatures = Object.values(featureStats).some(count => count > 0); expect(hasVersions || hasFeatures).toEqual(true); }); tap.test('PDF-12: Version upgrade scenarios', async () => { const { PDFDocument } = plugins; // Simulate upgrading PDF from older to newer version console.log('Testing version upgrade scenarios:'); // Create "old" PDF (simulated) const oldPdf = await PDFDocument.create(); oldPdf.setTitle('Old PDF (1.3 style)'); const page1 = oldPdf.addPage(); page1.drawText('Original Document', { x: 50, y: 700, size: 16 }); page1.drawText('Created with PDF 1.3 features only', { x: 50, y: 650, size: 12 }); const oldPdfBytes = await oldPdf.save(); // "Upgrade" by loading and adding new features const upgradedPdf = await PDFDocument.load(oldPdfBytes); upgradedPdf.setTitle('Upgraded PDF (1.7 features)'); // Add new page with modern features const page2 = upgradedPdf.addPage(); page2.drawText('Upgraded Content', { x: 50, y: 700, size: 16 }); // Add transparency (1.4+ feature) page2.drawRectangle({ x: 50, y: 600, width: 200, height: 50, color: rgb(0, 0.5, 1), opacity: 0.7 }); // Add multiple attachments (enhanced in later versions) await upgradedPdf.attach( Buffer.from('New attachment', 'utf8'), 'new_data.xml', { mimeType: 'application/xml', description: 'Added during upgrade', afRelationship: plugins.AFRelationship.Supplement } ); const upgradedBytes = await upgradedPdf.save(); console.log(`Original size: ${oldPdfBytes.length} bytes`); console.log(`Upgraded size: ${upgradedBytes.length} bytes`); // Test both versions work try { const einvoice = await EInvoice.fromPdf(Buffer.from(upgradedBytes)); console.log('Version upgrade test completed - PDF processed successfully'); } catch (error) { // Expected to fail as we're using minimal test XML console.log('Version upgrade processing error:', error.message); } }); tap.test('PDF-12: Compatibility edge cases', async () => { const { PDFDocument } = plugins; // Test edge cases that might cause compatibility issues const edgeCases = [ { name: 'Empty pages', test: async () => { const pdf = await PDFDocument.create(); pdf.addPage(); // Empty page pdf.addPage(); // Another empty page return pdf.save(); } }, { name: 'Very long text', test: async () => { const pdf = await PDFDocument.create(); const page = pdf.addPage(); const longText = 'Lorem ipsum '.repeat(1000); page.drawText(longText.substring(0, 1000), { x: 50, y: 700, size: 8 }); return pdf.save(); } }, { name: 'Special characters in metadata', test: async () => { const pdf = await PDFDocument.create(); pdf.setTitle('Test™ © ® € £ ¥'); pdf.setAuthor('Müller & Associés'); pdf.setSubject('Invoice (2025) '); pdf.addPage(); return pdf.save(); } }, { name: 'Maximum attachments', test: async () => { const pdf = await PDFDocument.create(); pdf.addPage(); // Add multiple small attachments for (let i = 0; i < 10; i++) { await pdf.attach( Buffer.from(`${i}`, 'utf8'), `file${i}.xml`, { mimeType: 'application/xml' } ); } return pdf.save(); } } ]; for (const edgeCase of edgeCases) { try { console.log(`Testing edge case: ${edgeCase.name}`); const pdfBytes = await edgeCase.test(); try { const einvoice = await EInvoice.fromPdf(Buffer.from(pdfBytes)); console.log(`[OK] ${edgeCase.name} - PDF created and processed`); } catch (extractError) { // Many edge cases won't have valid XML, which is expected console.log(`[OK] ${edgeCase.name} - PDF created, extraction failed (expected):`, extractError.message); } } catch (error) { console.log(`✗ ${edgeCase.name} - Failed:`, error.message); } } }); tap.start();