566 lines
17 KiB
TypeScript
566 lines
17 KiB
TypeScript
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||
|
import * as plugins from '../plugins.js';
|
||
|
import { EInvoice } from '../../../ts/index.js';
|
||
|
import { CorpusLoader } from '../corpus.loader.js';
|
||
|
import { PerformanceTracker } from '../performance.tracker.js';
|
||
|
|
||
|
tap.test('PDF-12: PDF Version Compatibility - should handle different PDF versions correctly', async (t) => {
|
||
|
// PDF-12: Verify compatibility across different PDF versions (1.3 - 1.7)
|
||
|
// This test ensures the system works with various PDF specifications
|
||
|
|
||
|
const performanceTracker = new PerformanceTracker('PDF-12: PDF Version Compatibility');
|
||
|
const corpusLoader = new CorpusLoader();
|
||
|
|
||
|
t.test('Create PDFs with different version headers', async () => {
|
||
|
const startTime = performance.now();
|
||
|
|
||
|
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: { red: 0, green: 0, blue: 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('binary').substring(0, 100);
|
||
|
console.log(`Created PDF (declared as ${ver.version}), header: ${pdfString.substring(0, 8)}`);
|
||
|
|
||
|
// Test processing
|
||
|
const einvoice = new EInvoice();
|
||
|
try {
|
||
|
await einvoice.loadFromPdfBuffer(pdfBytes);
|
||
|
const xml = einvoice.getXmlString();
|
||
|
expect(xml).toContain(`PDF-VER-${ver.version}`);
|
||
|
} catch (error) {
|
||
|
console.log(`Version ${ver.version} processing error:`, error.message);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const elapsed = performance.now() - startTime;
|
||
|
performanceTracker.addMeasurement('version-creation', elapsed);
|
||
|
});
|
||
|
|
||
|
t.test('Feature compatibility across versions', async () => {
|
||
|
const startTime = performance.now();
|
||
|
|
||
|
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: { red: 1, green: 0, blue: 0 },
|
||
|
opacity: 0.5
|
||
|
});
|
||
|
page.drawRectangle({
|
||
|
x: 100,
|
||
|
y: 650,
|
||
|
width: 100,
|
||
|
height: 100,
|
||
|
color: { red: 0, green: 0, blue: 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();
|
||
|
page.drawText('Unicode: 中文 العربية ελληνικά', {
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
const elapsed = performance.now() - startTime;
|
||
|
performanceTracker.addMeasurement('feature-compatibility', elapsed);
|
||
|
});
|
||
|
|
||
|
t.test('Cross-version attachment compatibility', async () => {
|
||
|
const startTime = performance.now();
|
||
|
|
||
|
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(`✓ ${test.name}`, { x: 70, y: yPos, size: 10 });
|
||
|
yPos -= 20;
|
||
|
}
|
||
|
|
||
|
const pdfBytes = await pdfDoc.save();
|
||
|
|
||
|
// Test extraction
|
||
|
const einvoice = new EInvoice();
|
||
|
await einvoice.loadFromPdfBuffer(pdfBytes);
|
||
|
console.log('Cross-version attachment test completed');
|
||
|
|
||
|
const elapsed = performance.now() - startTime;
|
||
|
performanceTracker.addMeasurement('attachment-compatibility', elapsed);
|
||
|
});
|
||
|
|
||
|
t.test('Backward compatibility', async () => {
|
||
|
const startTime = performance.now();
|
||
|
|
||
|
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: { red: 0, green: 0, blue: 0 }
|
||
|
});
|
||
|
|
||
|
// Basic shapes without transparency
|
||
|
page.drawRectangle({
|
||
|
x: 72,
|
||
|
y: 600,
|
||
|
width: 468,
|
||
|
height: 100,
|
||
|
borderColor: { red: 0, green: 0, blue: 0 },
|
||
|
borderWidth: 1
|
||
|
});
|
||
|
|
||
|
// Simple lines
|
||
|
page.drawLine({
|
||
|
start: { x: 72, y: 650 },
|
||
|
end: { x: 540, y: 650 },
|
||
|
thickness: 1,
|
||
|
color: { red: 0, green: 0, blue: 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: { red: 0, green: 0, blue: 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
|
||
|
const einvoice = new EInvoice();
|
||
|
await einvoice.loadFromPdfBuffer(pdfBytes);
|
||
|
|
||
|
console.log('Created backward compatible PDF (1.3 features only)');
|
||
|
|
||
|
const elapsed = performance.now() - startTime;
|
||
|
performanceTracker.addMeasurement('backward-compatibility', elapsed);
|
||
|
});
|
||
|
|
||
|
t.test('Version detection in corpus', async () => {
|
||
|
const startTime = performance.now();
|
||
|
let processedCount = 0;
|
||
|
const versionStats: Record<string, number> = {};
|
||
|
const featureStats = {
|
||
|
transparency: 0,
|
||
|
embeddedFiles: 0,
|
||
|
javascript: 0,
|
||
|
forms: 0,
|
||
|
compression: 0
|
||
|
};
|
||
|
|
||
|
const files = await corpusLoader.getAllFiles();
|
||
|
const pdfFiles = files.filter(f => f.endsWith('.pdf'));
|
||
|
|
||
|
// Analyze PDF versions in corpus
|
||
|
const sampleSize = Math.min(50, pdfFiles.length);
|
||
|
const sample = pdfFiles.slice(0, sampleSize);
|
||
|
|
||
|
for (const file of sample) {
|
||
|
try {
|
||
|
const content = await corpusLoader.readFile(file);
|
||
|
const pdfString = content.toString('binary');
|
||
|
|
||
|
// Extract PDF version from header
|
||
|
const versionMatch = pdfString.match(/%PDF-(\d\.\d)/);
|
||
|
if (versionMatch) {
|
||
|
const version = versionMatch[1];
|
||
|
versionStats[version] = (versionStats[version] || 0) + 1;
|
||
|
}
|
||
|
|
||
|
// Check for version-specific features
|
||
|
if (pdfString.includes('/Group') && pdfString.includes('/S /Transparency')) {
|
||
|
featureStats.transparency++;
|
||
|
}
|
||
|
|
||
|
if (pdfString.includes('/EmbeddedFiles')) {
|
||
|
featureStats.embeddedFiles++;
|
||
|
}
|
||
|
|
||
|
if (pdfString.includes('/JS') || pdfString.includes('/JavaScript')) {
|
||
|
featureStats.javascript++;
|
||
|
}
|
||
|
|
||
|
if (pdfString.includes('/AcroForm')) {
|
||
|
featureStats.forms++;
|
||
|
}
|
||
|
|
||
|
if (pdfString.includes('/Filter') && pdfString.includes('/FlateDecode')) {
|
||
|
featureStats.compression++;
|
||
|
}
|
||
|
|
||
|
processedCount++;
|
||
|
} catch (error) {
|
||
|
console.log(`Error analyzing ${file}:`, error.message);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
console.log(`Corpus version analysis (${processedCount} PDFs):`);
|
||
|
console.log('PDF versions found:', versionStats);
|
||
|
console.log('Feature usage:', featureStats);
|
||
|
|
||
|
// Most common version
|
||
|
const sortedVersions = Object.entries(versionStats).sort((a, b) => b[1] - a[1]);
|
||
|
if (sortedVersions.length > 0) {
|
||
|
console.log(`Most common version: PDF ${sortedVersions[0][0]} (${sortedVersions[0][1]} files)`);
|
||
|
}
|
||
|
|
||
|
const elapsed = performance.now() - startTime;
|
||
|
performanceTracker.addMeasurement('corpus-versions', elapsed);
|
||
|
});
|
||
|
|
||
|
t.test('Version upgrade scenarios', async () => {
|
||
|
const startTime = performance.now();
|
||
|
|
||
|
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: { red: 0, green: 0.5, blue: 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
|
||
|
const einvoice = new EInvoice();
|
||
|
await einvoice.loadFromPdfBuffer(upgradedBytes);
|
||
|
console.log('Version upgrade test completed');
|
||
|
|
||
|
const elapsed = performance.now() - startTime;
|
||
|
performanceTracker.addMeasurement('version-upgrade', elapsed);
|
||
|
});
|
||
|
|
||
|
t.test('Compatibility edge cases', async () => {
|
||
|
const startTime = performance.now();
|
||
|
|
||
|
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();
|
||
|
|
||
|
const einvoice = new EInvoice();
|
||
|
await einvoice.loadFromPdfBuffer(pdfBytes);
|
||
|
console.log(`✓ ${edgeCase.name} - Success`);
|
||
|
} catch (error) {
|
||
|
console.log(`✗ ${edgeCase.name} - Failed:`, error.message);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const elapsed = performance.now() - startTime;
|
||
|
performanceTracker.addMeasurement('edge-cases', elapsed);
|
||
|
});
|
||
|
|
||
|
// Print performance summary
|
||
|
performanceTracker.printSummary();
|
||
|
|
||
|
// Performance assertions
|
||
|
const avgTime = performanceTracker.getAverageTime();
|
||
|
expect(avgTime).toBeLessThan(500); // Version compatibility tests may vary
|
||
|
});
|
||
|
|
||
|
tap.start();
|