535 lines
17 KiB
TypeScript
535 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-11: PDF/A Compliance - should ensure PDF/A standard compliance', async (t) => {
|
||
// PDF-11: Verify PDF/A compliance for long-term archiving
|
||
// This test ensures PDFs meet PDF/A standards for electronic invoicing
|
||
|
||
const performanceTracker = new PerformanceTracker('PDF-11: PDF/A Compliance');
|
||
const corpusLoader = new CorpusLoader();
|
||
|
||
t.test('Create PDF/A-3 compliant document', async () => {
|
||
const startTime = performance.now();
|
||
|
||
const { PDFDocument, PDFName } = plugins;
|
||
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);
|
||
});
|
||
|
||
t.test('PDF/A-1b compliance check', async () => {
|
||
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,
|
||
color: { red: 0, green: 0, blue: 0 } // RGB color space
|
||
});
|
||
|
||
// Add text without transparency
|
||
page.drawText('No transparency allowed in PDF/A-1b', {
|
||
x: 50,
|
||
y: 700,
|
||
size: 12,
|
||
font: helveticaFont,
|
||
color: { red: 0, green: 0, blue: 0 },
|
||
opacity: 1.0 // Full opacity required
|
||
});
|
||
|
||
// Draw rectangle without transparency
|
||
page.drawRectangle({
|
||
x: 50,
|
||
y: 600,
|
||
width: 200,
|
||
height: 50,
|
||
color: { red: 0.9, green: 0.9, blue: 0.9 },
|
||
borderColor: { red: 0, green: 0, blue: 0 },
|
||
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);
|
||
});
|
||
|
||
t.test('PDF/A metadata requirements', async () => {
|
||
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);
|
||
});
|
||
|
||
t.test('Color space compliance', async () => {
|
||
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,
|
||
color: { red: 0.8, green: 0.2, blue: 0.2 }
|
||
});
|
||
|
||
// Grayscale
|
||
page.drawText('Device Gray Color Space', {
|
||
x: 50,
|
||
y: 700,
|
||
size: 14,
|
||
color: { red: 0.5, green: 0.5, blue: 0.5 }
|
||
});
|
||
|
||
// Test color accuracy
|
||
const colors = [
|
||
{ name: 'Pure Red', rgb: { red: 1, green: 0, blue: 0 } },
|
||
{ name: 'Pure Green', rgb: { red: 0, green: 1, blue: 0 } },
|
||
{ name: 'Pure Blue', rgb: { red: 0, green: 0, blue: 1 } },
|
||
{ name: 'Black', rgb: { red: 0, green: 0, blue: 0 } },
|
||
{ name: 'White', rgb: { red: 1, green: 1, blue: 1 } }
|
||
];
|
||
|
||
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,
|
||
color: { red: 0, green: 0, blue: 0 }
|
||
});
|
||
|
||
yPos -= 30;
|
||
});
|
||
|
||
// Add OutputIntent description
|
||
page.drawText('OutputIntent: sRGB IEC61966-2.1', {
|
||
x: 50,
|
||
y: 400,
|
||
size: 10,
|
||
color: { red: 0, green: 0, blue: 0 }
|
||
});
|
||
|
||
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);
|
||
});
|
||
|
||
t.test('Font embedding compliance', async () => {
|
||
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();
|
||
|
||
// Check font embedding
|
||
const pdfString = pdfBytes.toString('binary');
|
||
const fontCount = (pdfString.match(/\/Type\s*\/Font/g) || []).length;
|
||
console.log(`Embedded fonts count: ${fontCount}`);
|
||
|
||
expect(fontCount).toBeGreaterThan(0);
|
||
|
||
const elapsed = performance.now() - startTime;
|
||
performanceTracker.addMeasurement('font-embedding', elapsed);
|
||
});
|
||
|
||
t.test('PDF/A-3 with ZUGFeRD attachment', async () => {
|
||
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
|
||
const einvoice = new EInvoice();
|
||
await einvoice.loadFromPdfBuffer(pdfBytes);
|
||
|
||
console.log('Created PDF/A-3 compliant ZUGFeRD invoice');
|
||
|
||
const elapsed = performance.now() - startTime;
|
||
performanceTracker.addMeasurement('zugferd-pdfa3', elapsed);
|
||
});
|
||
|
||
t.test('Corpus PDF/A compliance check', async () => {
|
||
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
|
||
};
|
||
|
||
const files = await corpusLoader.getAllFiles();
|
||
const pdfFiles = files.filter(f => f.endsWith('.pdf'));
|
||
|
||
// Sample PDFs for PDF/A compliance indicators
|
||
const sampleSize = Math.min(40, 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');
|
||
|
||
// 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++;
|
||
console.log(`Potential PDF/A file: ${file}`);
|
||
}
|
||
|
||
processedCount++;
|
||
} catch (error) {
|
||
console.log(`Error checking ${file}:`, error.message);
|
||
}
|
||
}
|
||
|
||
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);
|
||
});
|
||
|
||
// Print performance summary
|
||
performanceTracker.printSummary();
|
||
|
||
// Performance assertions
|
||
const avgTime = performanceTracker.getAverageTime();
|
||
expect(avgTime).toBeLessThan(400); // PDF/A operations may take longer
|
||
});
|
||
|
||
tap.start(); |