einvoice/test/suite/einvoice_pdf-operations/test.pdf-12.version-compatibility.ts

587 lines
18 KiB
TypeScript

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 = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>PDF-VER-${ver.version}</ID>
<Note>Test invoice for PDF ${ver.version}</Note>
<PDFVersion>${ver.version}</PDFVersion>
</Invoice>`;
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('<data>Primary</data>', 'utf8'),
'primary.xml',
{ mimeType: 'application/xml' }
);
await pdfDoc.attach(
Buffer.from('<data>Secondary</data>', '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: '<invoice><id>SIMPLE</id></invoice>',
options: { mimeType: 'application/xml' }
},
{
name: 'With description (1.4+)',
file: 'described.xml',
content: '<invoice><id>DESCRIBED</id></invoice>',
options: {
mimeType: 'application/xml',
description: 'Invoice with description'
}
},
{
name: 'With relationship (1.7+)',
file: 'related.xml',
content: '<invoice><id>RELATED</id></invoice>',
options: {
mimeType: 'application/xml',
description: 'Invoice with AFRelationship',
afRelationship: AFRelationship.Data
}
},
{
name: 'With dates (1.4+)',
file: 'dated.xml',
content: '<invoice><id>DATED</id></invoice>',
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 = `<?xml version="1.0"?>
<invoice>
<number>2025-001</number>
<date>2025-01-25</date>
<amount>1234.56</amount>
</invoice>`;
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('<data>test</data>', '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<string, number> = {};
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('<data>New attachment</data>', '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) <test>');
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(`<item>${i}</item>`, '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();