587 lines
18 KiB
TypeScript
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(); |