einvoice/test/suite/einvoice_pdf-operations/test.pdf-11.pdfa-compliance.ts

588 lines
18 KiB
TypeScript
Raw Normal View History

2025-05-25 19:45:37 +00:00
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as plugins from '../plugins.js';
import { EInvoice } from '../../../ts/index.js';
2025-05-28 10:15:48 +00:00
import { CorpusLoader } from '../../helpers/corpus.loader.js';
import { PerformanceTracker as StaticPerformanceTracker } from '../performance.tracker.js';
import { rgb } from 'pdf-lib';
2025-05-25 19:45:37 +00:00
2025-05-28 10:15:48 +00:00
// Simple instance-based performance tracker for this test
class SimplePerformanceTracker {
private measurements: Map<string, number[]> = 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 () => {
2025-05-25 19:45:37 +00:00
const startTime = performance.now();
2025-05-28 10:15:48 +00:00
const { PDFDocument } = plugins;
2025-05-25 19:45:37 +00:00
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 = `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
<rsm:ExchangedDocument>
<ram:ID xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100">INV-2025-001</ram:ID>
<ram:TypeCode xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100">380</ram:TypeCode>
<ram:IssueDateTime xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100">
<udt:DateTimeString xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100" format="102">20250125</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
</rsm:CrossIndustryInvoice>`;
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);
2025-05-28 10:15:48 +00:00
});
2025-05-25 19:45:37 +00:00
2025-05-28 10:15:48 +00:00
tap.test('PDF-11: PDF/A-1b compliance check', async () => {
2025-05-25 19:45:37 +00:00
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,
2025-05-28 10:15:48 +00:00
color: rgb(0, 0, 0) // RGB color space
2025-05-25 19:45:37 +00:00
});
// Add text without transparency
page.drawText('No transparency allowed in PDF/A-1b', {
x: 50,
y: 700,
size: 12,
font: helveticaFont,
2025-05-28 10:15:48 +00:00
color: rgb(0, 0, 0),
2025-05-25 19:45:37 +00:00
opacity: 1.0 // Full opacity required
});
// Draw rectangle without transparency
page.drawRectangle({
x: 50,
y: 600,
width: 200,
height: 50,
2025-05-28 10:15:48 +00:00
color: rgb(0.9, 0.9, 0.9),
borderColor: rgb(0, 0, 0),
2025-05-25 19:45:37 +00:00
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);
2025-05-28 10:15:48 +00:00
});
2025-05-25 19:45:37 +00:00
2025-05-28 10:15:48 +00:00
tap.test('PDF-11: PDF/A metadata requirements', async () => {
2025-05-25 19:45:37 +00:00
const startTime = performance.now();
const { PDFDocument } = plugins;
const pdfDoc = await PDFDocument.create();
// Required XMP metadata for PDF/A
const xmpMetadata = `<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/"
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
<dc:title>
<rdf:Alt>
<rdf:li xml:lang="x-default">PDF/A Compliant Invoice</rdf:li>
</rdf:Alt>
</dc:title>
<dc:creator>
<rdf:Seq>
<rdf:li>EInvoice System</rdf:li>
</rdf:Seq>
</dc:creator>
<dc:description>
<rdf:Alt>
<rdf:li xml:lang="x-default">Invoice with PDF/A compliance</rdf:li>
</rdf:Alt>
</dc:description>
<pdfaid:part>3</pdfaid:part>
<pdfaid:conformance>B</pdfaid:conformance>
<xmp:CreateDate>2025-01-25T10:00:00Z</xmp:CreateDate>
<xmp:ModifyDate>2025-01-25T10:00:00Z</xmp:ModifyDate>
<xmp:MetadataDate>2025-01-25T10:00:00Z</xmp:MetadataDate>
<pdf:Producer>EInvoice PDF/A Generator</pdf:Producer>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>`;
// 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);
2025-05-28 10:15:48 +00:00
});
2025-05-25 19:45:37 +00:00
2025-05-28 10:15:48 +00:00
tap.test('PDF-11: Color space compliance', async () => {
2025-05-25 19:45:37 +00:00
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,
2025-05-28 10:15:48 +00:00
color: rgb(0.8, 0.2, 0.2)
2025-05-25 19:45:37 +00:00
});
// Grayscale
page.drawText('Device Gray Color Space', {
x: 50,
y: 700,
size: 14,
2025-05-28 10:15:48 +00:00
color: rgb(0.5, 0.5, 0.5)
2025-05-25 19:45:37 +00:00
});
// Test color accuracy
const colors = [
2025-05-28 10:15:48 +00:00
{ 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) }
2025-05-25 19:45:37 +00:00
];
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,
2025-05-28 10:15:48 +00:00
color: rgb(0, 0, 0)
2025-05-25 19:45:37 +00:00
});
yPos -= 30;
});
// Add OutputIntent description
page.drawText('OutputIntent: sRGB IEC61966-2.1', {
x: 50,
y: 400,
size: 10,
2025-05-28 10:15:48 +00:00
color: rgb(0, 0, 0)
2025-05-25 19:45:37 +00:00
});
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);
2025-05-28 10:15:48 +00:00
});
2025-05-25 19:45:37 +00:00
2025-05-28 10:15:48 +00:00
tap.test('PDF-11: Font embedding compliance', async () => {
2025-05-25 19:45:37 +00:00
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();
2025-05-28 10:15:48 +00:00
// 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`);
2025-05-25 19:45:37 +00:00
2025-05-28 10:15:48 +00:00
// 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
2025-05-25 19:45:37 +00:00
const elapsed = performance.now() - startTime;
performanceTracker.addMeasurement('font-embedding', elapsed);
2025-05-28 10:15:48 +00:00
});
2025-05-25 19:45:37 +00:00
2025-05-28 10:15:48 +00:00
tap.test('PDF-11: PDF/A-3 with ZUGFeRD attachment', async () => {
2025-05-25 19:45:37 +00:00
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 = `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017#conformant#urn:zugferd.de:2p1:extended</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>2025-001</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20250125</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
</rsm:CrossIndustryInvoice>`;
// 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
2025-05-28 10:15:48 +00:00
const einvoice = await EInvoice.fromPdf(pdfBytes);
2025-05-25 19:45:37 +00:00
console.log('Created PDF/A-3 compliant ZUGFeRD invoice');
const elapsed = performance.now() - startTime;
performanceTracker.addMeasurement('zugferd-pdfa3', elapsed);
2025-05-28 10:15:48 +00:00
});
2025-05-25 19:45:37 +00:00
2025-05-28 10:15:48 +00:00
tap.test('PDF-11: Corpus PDF/A compliance check', async () => {
2025-05-25 19:45:37 +00:00
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
};
2025-05-28 10:15:48 +00:00
// 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}`);
}
}
2025-05-25 19:45:37 +00:00
// Sample PDFs for PDF/A compliance indicators
2025-05-28 10:15:48 +00:00
const sampleSize = Math.min(40, allPdfFiles.length);
const sample = allPdfFiles.slice(0, sampleSize);
2025-05-25 19:45:37 +00:00
for (const file of sample) {
try {
2025-05-28 10:15:48 +00:00
const content = await CorpusLoader.loadFile(file.path);
2025-05-25 19:45:37 +00:00
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++;
2025-05-28 10:15:48 +00:00
console.log(`Potential PDF/A file: ${file.path}`);
2025-05-25 19:45:37 +00:00
}
processedCount++;
} catch (error) {
2025-05-28 10:15:48 +00:00
console.log(`Error checking ${file.path}:`, error.message);
2025-05-25 19:45:37 +00:00
}
}
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);
2025-05-28 10:15:48 +00:00
});
2025-05-25 19:45:37 +00:00
2025-05-28 10:15:48 +00:00
tap.test('PDF-11: Performance Summary', async () => {
2025-05-25 19:45:37 +00:00
// Print performance summary
performanceTracker.printSummary();
// Performance assertions
const avgTime = performanceTracker.getAverageTime();
expect(avgTime).toBeLessThan(400); // PDF/A operations may take longer
});
tap.start();