Distribuisci un convertitore che contrassegna ogni file come PDF/A-1b, il sistema di archiviazione del cliente li acquisisce per un anno, e poi un audit passa l'intero lotto attraverso veraPDF e un terzo di essi torna non conforme. Non è andato in crash nulla, non è stata sollevata alcuna eccezione, i file si aprono correttamente in ogni visualizzatore sulla tua scrivania. Semplicemente non erano lo standard che avevi impresso su di essi. Questa è la normale modalità di errore del PDF per archiviazione, ed è il motivo per cui "abbiamo impostato il flag" non equivale mai a "valida"
La prima cosa da capire su PDFium e PDF/A è che il motore non c'entra nulla. PDFium renderizza, analizza e scrive PDF, ma la sua superficie pubblica non ha ConvertToPDFA, nessun writer OutputIntent, nessuna API XMP. Ogni parte della conformità archivistica, il pacchetto XMP, l'OutputIntent e il relativo profilo ICC, i marcatori del catalogo, la validazione, vive in PDFiumPas stesso, in un'unità puramente Pascal di circa 2.000 righe (FPdfPdfa.pas) che analizza i byte salvati e li riscrive tramite un aggiornamento incrementale. Sapere dove avviene il lavoro ti dice dove si nascondono i bug, e non si nascondono in PDFium
Che cosa richiede davvero PDF/A e dove morde
PDF/A non è un solo formato. ISO 19005 definisce tre parti (PDF/A-1, -2, -3) e, all'interno di ciascuna, livelli di conformità che promettono cose diverse. Il livello B (base) garantisce solo che l'aspetto visivo sia riproducibile. Il livello A (accessibile) aggiunge un albero di struttura marcato e la mappatura Unicode sopra B. Il livello U, che esiste solo per le parti 2 e 3, si colloca in mezzo: testo Unicode affidabile senza l'intero albero di struttura. ISO 19005-1 non ha il livello U, un vincolo che la libreria codifica direttamente
Alcune regole del formato sono quelle che in pratica fanno male. La cifratura è vietata in modo assoluto (ISO 19005-1 §6.1.3 e successivi): un file PDF/A non può contenere un /Encrypt dizionario. Il documento deve dichiarare una condizione di rendering in uscita tramite un OutputIntent la cui destinazione sia un profilo ICC valido (§6.2.3.2). La dichiarazione di conformità stessa deve comparire come metadati XMP sotto lo schema di identificazione PDF/A. Il livello A richiede inoltre la struttura logica §6.8, l'albero dei tag che rende il documento leggibile dalle macchine. Se ne manca anche solo uno, un validatore di conformità rifiuta il file anche se si renderizza perfettamente
La singola chiamata che produce un archivio
PDFiumPas espone l'intera pipeline dietro TPdf.SaveAsPdfA. Il sovraccarico semplice prende una conformità di destinazione e per impostazione predefinita usa PDF/A-1b, che è il valore predefinito corretto per il caso comune di "rendi questo leggibile per sempre"
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('invoice.pdf');
// Default conformance is pac1b (PDF/A-1b)
if Pdf.SaveAsPdfA('invoice_archive.pdf') then
// file now carries XMP, sRGB OutputIntent, and catalog markers
else
raise Exception.Create('PDF/A save failed');
finally
Pdf.Free;
end;
end;
Sotto il cofano questo è un passaggio in due fasi. SaveAsPdfA chiede prima a PDFium di serializzare il documento con FPDF_SaveAsCopy, quindi passa quel flusso di byte a InjectPdfAMarkers, che aggiunge i metadati XMP, l'OutputIntent sRGB con il profilo ICC incorporato e un catalogo riscritto come aggiornamento incrementale. La sorgente viene letta dalla posizione zero e la destinazione viene scritta dalla posizione zero; l'albero degli oggetti originale resta intatto e i marcatori si inseriscono dopo il %%EOF esistente. Se ti servono i byte invece di un file, SaveAsPdfAToStream accetta un TStream e le stesse opzioni
Scelta della conformità con il record delle opzioni
Per puntare a una parte e a un livello specifici, passa un TPdfASaveOptions record. Il suo Conformance campo accetta un TPdfAConformance valore. L'enumerazione copre ogni combinazione valida e nient'altro: pac1b, pac1a per la parte 1; pac2b, pac2u, pac2a per la parte 2; pac3b, pac3u, pac3a per la parte 3, più pacUnknown e pacNone per il lato validazione. Non esiste pac1u, perché quel livello non esiste nello standard
var
Pdf: TPdf;
Opts: TPdfASaveOptions;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('report.pdf');
Opts := TPdfASaveOptions.Default;
Opts.Conformance := pac2u; // PDF/A-2u: reliable Unicode text
Opts.Title := 'Quarterly Report 2026';
Opts.Author := 'Finance';
// Leave IccProfileData empty to use the built-in sRGB IEC61966-2.1 profile
if not Pdf.SaveAsPdfA('report_a2u.pdf', Opts) then
raise Exception.Create('PDF/A-2u save failed');
finally
Pdf.Free;
end;
end;
La maggior parte del record può restare vuota. LasciaTitle, Author, Subject, Keywords, e Creator vuoti e Producer auto-compila i valori dal dizionario Info del documento tramite SaveAsPdfA. Lascia FPDF_GetMetaText e CreationDate vuoti e usa l'ora UTC corrente per entrambe le date XMP. Lascia ModDate e DocumentId vuoti e la libreria li precompila da InstanceId, ripiegando su un ID deterministico derivato dai byte sorgente. L'unico campo che potresti voler sovrascrivere deliberatamente è FPDF_GetFileIdentifier: vuoto significa il profilo sRGB IEC61966-2.1 incluso, ma un flusso di lavoro CMYK o in scala di grigi dovrebbe fornire il proprio.IccProfileDataPerché il livello A degrada e perché è la scelta onesta
Ecco una sottigliezza che coglie di sorpresa chi si aspetta che un flag sia una garanzia. Puoi chiedere
su un documento che non ha un albero dei tag, ma PDF/A-1a richiede la struttura logica §6.8, e la libreria non può creare un albero di struttura da un PDF privo di tag. Invece di emettere un file che dichiara il livello A ma non lo rispetta, pac1a controlla la presenza di una reale struttura marcata (SaveAsPdfA più /StructTreeRoot con /MarkInfo) e, se manca, declassa la dichiarazione: /Marked true diventa pac1a, pac1b diventa pac2a, e così via su tutte e tre le parti. Gli helper interni sono pac2b e PdfAIsLevelA.PdfADowngradeToLevelBIl ragionamento vale la pena di dirlo apertamente: un file che dichiara onestamente il livello che soddisfa è più utile di uno che mente sul livello che non soddisfa. Il livello U viene gestito in modo diverso. Rilevare una copertura Unicode autentica richiederebbe un test ingenuo del tipo "contiene
" che degraderebbe eccessivamente documenti legittimi (WinAnsi e codifiche simili sono esenti), quindi il lato salvataggio emette la dichiarazione U così come l'ha dichiarata il chiamante e lascia che la discrepanza venga segnalata invece dal lato validazione. Se ti serve un archivio Level A garantito, marca il documento prima di convertirlo; il convertitore non inventerà una struttura che non c'è./ToUnicodeL'insidia ICC che solo un validatore reale intercetta
Questo è il guasto che ha insegnato la lezione più dura, perché il controllo interno della libreria lo passava mentre veraPDF, il validatore di riferimento ISO 19005, no. PDF/A richiede che il profilo di destinazione dell'OutputIntent sia uno stream ICCBased valido, e §6.2.3.2 impone a un verificatore di convalidare quello stream come spazio colore. Uno stream ICCBased deve dichiarare
, il numero di componenti colore. Una prima versione dell'iniettore scriveva il dizionario dello stream ICC con solo /N e nessun /Length, e veraPDF rifiutava il risultato con "The N entry (value null)... is missing"./NCiò che lo rendeva insidioso è che il rifiuto scattava solo per PDF/A-1b e -1a. I modelli di conformità delle parti 2 e 3 non eseguivano quel controllo particolare sul profilo di destinazione, quindi la stessa struttura iniettata veniva convalidata con
, pac2b, e pac3b ma falliva con pac2u solo per il pac1b valore. Un test unitario non avrebbe mai potuto vederlo, perché il controllo interno della libreriapdfaid:part valore. Un test unitario non avrebbe mai potuto vederlo, perché il controllo interno della libreriaValidatePdfACompliance controllava solo che la /DestOutputProfile chiave esistesse, non cosa ci fosse dentro il dizionario dello stream. I test interni restavano verdi; la vera convalida archivistica falliva
La correzione è IccComponentCount, che legge la firma dello spazio colore dati all'offset 16 dell'intestazione ICC e la mappa a un conteggio di componenti: GRAY è 1, RGB , Lab , e XYZ sono 3, CMYK è 4, con un profilo sconosciuto che ricade su 3. Quel conteggio finisce nel dizionario dello stream come /N. È calcolato, non hard-coded a 3, così che un chiamante che fornisce un profilo CMYK o in scala di grigi tramite IccProfileData ottenga comunque il valore corretto. La lezione più ampia è metodologica: il controllo interno della libreria e un validatore autorevole hanno ciascuno i loro punti ciechi, e l'output PDF/A va testato end-to-end contro un'implementazione di riferimento come veraPDF invece di fidarsi di autocontrolli. La stessa disciplina dell'aggiornamento incrementale dietro archivi puliti è trattata in convalidare flussi di oggetti e xref compressi, che conta perché i PDF moderni che l'iniettore consuma sono spesso costruiti su flussi di cross-reference
Cifratura, xref stream e altri bordi
Poiché ISO 19005 vieta la cifratura, il percorso di salvataggio la rimuove prima di scrivere. SaveAsPdfA applica FPDF_REMOVE_SECURITY durante la serializzazione, quindi una sorgente cifrata (caricata con la sua password) viene decrittata mentre entra nell'archivio. Su un documento non cifrato è un'operazione nulla e non cambia nulla. La conseguenza è lo stesso vincolo che HotPDF impone dal lato opposto: un singolo file non può essere contemporaneamente cifrato e PDF/A. Quando un flusso di lavoro ha bisogno di entrambe le cose, la risposta sono due artefatti, una copia cifrata per la distribuzione e una copia pulita separata per l'archivio
Un altro bordo è invisibile finché non morde: documenti PDF 1.5+ che usano un puro flusso di cross-reference e non portano alcuna trailer keyword. L'iniettore legge il trailer per trovare il /Info sorgente e aggiungervi il proprio aggiornamento incrementale, e deve accettare la forma con xref-stream, altrimenti un documento del genere verrebbe copiato con i marcatori silenziosamente scartati. ISO 32000-1 §7.5.6 consente esplicitamente a un aggiornamento incrementale con trailer classico di seguire un documento xref-stream, con /Prev che punta all'offset dello xref-stream, che è esattamente la struttura che l'iniettore emette. La stessa FPDF_SaveAsCopy di PDFium scrive sempre un trailer classico, quindi nel normale percorso l'iniettore non incontra mai una sorgente pure xref-stream, ma il percorso di lettura lo gestisce per i documenti che arrivano da altrove
Verificare prima di fidarsi della dichiarazione
La libreria include un verificatore a livello di byte, TPdf.ValidatePdfA, che restituisce un TPdfAValidationResult. Il suo Conformance riporta il livello rilevato e Issues è un insieme di valori TPdfAValidationIssue; il metodo di convenienza IsCompliant è vero solo quando è stato rilevato un livello reale e l'insieme dei problemi è vuoto. Eseguilo come primo filtro rapido in una batch
var
Pdf: TPdf;
Res: TPdfAValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('invoice_archive.pdf');
Res := Pdf.ValidatePdfA;
if Res.IsCompliant then
Writeln('Conformant: detected level ', Ord(Res.Conformance))
else
Writeln('Issues found: ', SizeOf(Res.Issues), ' flags set');
finally
Pdf.Free;
end;
end;
Sii onesto su ciò che questo ti offre. Il verificatore a livello di byte intercetta i problemi strutturali (un OutputIntent mancante, un'azione vietata, un /Encrypt presente, trasparenza dove la parte 1 la proibisce) con alta affidabilità, e il rilevamento dell'incorporamento dei font usa un'euristica sul conteggio che segnala deliberatamente solo un indicatore ad alta affidabilità invece di inseguire la copertura per glifo. Ciò che non fa è l'analisi degli operatori dei content stream, che richiederebbe un parser completo dei contenuti ed è fuori ambito per progettazione. Per una soglia di rilascio, abbina il verificatore della libreria a veraPDF: il verificatore è istantaneo e gira ovunque senza DLL, veraPDF è autorevole. Collegare questa coppia a una batch run è l'oggetto della report CLI di preflight della batch, ed è lì che questa validazione appartiene in un vero flusso di lavoro archivistico
Le SaveAsPdfA, InjectPdfAMarkers, e ValidatePdfA API mostrate qui sono incluse con PDFium Component per Delphi, C++Builder e Lazarus/FPC. La pagina del prodotto collega la documentazione API completa, inclusa l'enumerazione completa della conformità e il record delle opzioni dietro questi esempi