Nasadíte konvertor, ktorý označí každý súbor ako PDF/A-1b, zákazníkov systém evidencie ho rok ingestuje a potom audit pošle celú dávku cez veraPDF, pričom tretina sa vráti ako nezhodná. Nič nespadlo, nevyhodila sa žiadna výnimka a súbory sa na každom prehliadači na vašom stole otvárajú bez problémov. Len jednoducho nespĺňali štandard, ktorý ste na ne natlačili. Toto je bežný spôsob zlyhania archivného PDF, a preto tvrdenie „nastavili sme príznak“ nikdy nie je to isté ako „validuje to“
Prvá vec, ktorú treba pri PDFium a PDF/A pochopiť, je, že samotný engine s tým nemá nič spoločné. PDFium vykresľuje, parsuje a zapisuje PDF, ale jeho verejný povrch nemá žiadne ConvertToPDFA, žiadny zapisovač OutputIntent, žiadne XMP API. Každá časť archivnej zhody, XMP paket, OutputIntent a jeho ICC profil, katalógové markery, validácia, žije priamo v PDFiumPas, v jednotke z čistého Pascalu s približne 2 000 riadkami (FPdfPdfa.pas), ktorá parsuje uložené bajty a prepíše ich cez inkrementálnu aktualizáciu. Keď viete, kde sa táto práca deje, viete aj, kde sa skrývajú chyby, a neskrývajú sa v PDFium
Čo PDF/A skutočne vyžaduje a kde to bolí
PDF/A nie je jeden formát. ISO 19005 definuje tri časti (PDF/A-1, -2, -3) a v rámci každej z nich úrovne zhody, ktoré sľubujú odlišné veci. Úroveň B (basic) zaručuje iba to, že vizuálny vzhľad je reprodukovateľný. Úroveň A (accessible) pridáva nad B tagovaný strom štruktúry a Unicode mapovanie. Úroveň U, ktorá existuje len pre časti 2 a 3, sedí medzi nimi: spoľahlivý Unicode text bez plného stromu štruktúry. ISO 19005-1 úroveň U nemá, a knižnica toto obmedzenie kóduje priamo
Zopár pravidiel formátu je tých, ktoré v praxi skutočne bolia. Šifrovanie je úplne zakázané (ISO 19005-1 §6.1.3 a jeho nástupcovia): súbor PDF/A nemôže niesť slovník /Encrypt. Dokument musí deklarovať výstupné podmienky vykreslenia pomocou OutputIntent, ktorého cieľom je platný ICC profil (§6.2.3.2). Samotné tvrdenie o zhode sa musí objaviť ako XMP metadáta v schéme identifikácie PDF/A. Úroveň A navyše vyžaduje logickú štruktúru podľa §6.8, strom tagov, ktorý robí dokument strojovo čitateľným. Ak čokoľvek z toho chýba, overovač zhody súbor odmietne, aj keď sa vykresľuje dokonale
Jediné volanie, ktoré vytvorí archív
PDFiumPas sprístupňuje celé toto prepojenie za metódou TPdf.SaveAsPdfA. Jednoduché preťaženie berie cieľovú zhodu a predvolene používa PDF/A-1b, čo je správny default pre bežný prípad „urob z tohto niečo, čo sa bude dať navždy vykresliť“
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.Active := True;
// Predvolená zhoda je pac1b (PDF/A-1b)
if Pdf.SaveAsPdfA('invoice_archive.pdf') then
// súbor teraz nesie XMP, sRGB OutputIntent a katalógové markery
else
raise Exception.Create('PDF/A save failed');
finally
Pdf.Free;
end;
end;
Pod kapotou ide o dvojkrokový postup. SaveAsPdfA najprv požiada PDFium, aby dokument serializovalo pomocou FPDF_SaveAsCopy, a potom tento bajtový stream odovzdá funkcii InjectPdfAMarkers, ktorá pripojí XMP metadáta, sRGB OutputIntent s vloženým ICC profilom a prepísaný katalóg formou inkrementálnej aktualizácie. Zdroj sa číta od pozície nula a cieľ sa zapisuje od pozície nula; pôvodný strom objektov zostane nedotknutý a markery sa pripoja za existujúci %%EOF. Ak potrebujete bajty namiesto súboru, SaveAsPdfAToStream berie TStream a tie isté voľby
Voľba zhody pomocou záznamu options
Ak chcete cieliť konkrétnu časť a úroveň, odovzdajte záznam TPdfASaveOptions. Jeho pole Conformance berie hodnotu TPdfAConformance. Výpočtový typ pokrýva každú platnú kombináciu a nič iné: pac1b, pac1a pre časť 1; pac2b, pac2u, pac2a pre časť 2; pac3b, pac3u, pac3a pre časť 3, plus pacUnknown a pacNone pre stranu validácie. Neexistuje pac1u, pretože taká úroveň v štandarde neexistuje
var
Pdf: TPdf;
Opts: TPdfASaveOptions;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Active := True;
Opts := TPdfASaveOptions.Default;
Opts.Conformance := pac2u; // PDF/A-2u: spoľahlivý Unicode text
Opts.Title := 'Quarterly Report 2026';
Opts.Author := 'Finance';
// Ponechajte IccProfileData prázdne, aby sa použil vstavaný profil sRGB IEC61966-2.1
if not Pdf.SaveAsPdfA('report_a2u.pdf', Opts) then
raise Exception.Create('PDF/A-2u save failed');
finally
Pdf.Free;
end;
end;
Väčšinu záznamu môžete nechať prázdnu. Nechajte Title, Author, Subject, Keywords, Creator a Producer prázdne a SaveAsPdfA ich automaticky doplní zo slovníka Info dokumentu cez FPDF_GetMetaText. Nechajte CreationDate a ModDate prázdne a použije aktuálny čas UTC pre oba dátumy v XMP. Nechajte DocumentId a InstanceId prázdne a knižnica ich predvyplní z FPDF_GetFileIdentifier, s fallbackom na deterministické ID odvodené zo zdrojových bajtov. Pole, ktoré možno budete chcieť prepísať zámerne, je IccProfileData: prázdne znamená pribalený profil sRGB IEC61966-2.1, ale workflow pre CMYK alebo grayscale by mal dodať vlastný
Prečo sa úroveň A degraduje a prečo je to poctivá voľba
Tu je jemnosť, ktorá zaskočí ľudí čakajúcich, že príznak znamená záruku. Môžete požiadať o pac1a na dokumente, ktorý nemá strom tagov, ale PDF/A-1a vyžaduje logickú štruktúru podľa §6.8, a knižnica nedokáže vyrobiť strom štruktúry z netagovaného PDF. Namiesto toho, aby vydala súbor, ktorý tvrdí úroveň A a zároveň ju nespĺňa, SaveAsPdfA skontroluje, či existuje reálna tagovaná štruktúra (/StructTreeRoot plus /MarkInfo s /Marked true), a ak chýba, tvrdenie degraduje: pac1a sa zmení na pac1b, pac2a na pac2b a tak ďalej naprieč všetkými tromi časťami. Interné helpery sú PdfAIsLevelA a PdfADowngradeToLevelB
Dôvod stojí za to povedať priamo: súbor, ktorý poctivo deklaruje úroveň, ktorú skutočne spĺňa, je užitočnejší než súbor, ktorý klame o úrovni, ktorú nespĺňa. Úroveň U sa rieši inak. Detekcia skutočného pokrytia Unicode by znamenala naivný test „má to /ToUnicode“, ktorý by zbytočne degradoval legitímne dokumenty (kódovania WinAnsi a podobné sú z toho vyňaté), preto save strana zapisuje tvrdenie U presne tak, ako ho zadal volajúci, a prípadný rozpor necháva odhaliť validačnej strane. Ak potrebujete garantovaný archív úrovne A, otagujte dokument ešte pred konverziou; konvertor nevymyslí štruktúru, ktorá v súbore nie je
Úskalie ICC, ktoré odhalí len skutočný validátor
Toto zlyhanie prinieslo najtvrdšiu lekciu, pretože vlastný checker knižnice ním prešiel, kým veraPDF, referenčný validátor ISO 19005, nie. PDF/A vyžaduje, aby cieľový profil v OutputIntent bol platný stream ICCBased, a §6.2.3.2 núti overovač validovať tento stream ako farebný priestor. Stream ICCBased musí deklarovať /N, počet farebných zložiek. Skoršia verzia injektora zapísala slovník ICC streamu iba s /Length a bez /N, a veraPDF výsledok odmietlo s hláškou, že položka N chýba
Zákernosť spočívala v tom, že odmietnutie sa objavilo iba pri PDF/A-1b a -1a. Modely zhody pre časť 2 a časť 3 túto konkrétnu kontrolu na destination profile nespúšťali, takže identická injektovaná štruktúra validovala pod pac2b, pac3b a pac2u, ale zlyhala pod pac1b na ničom inom než hodnote pdfaid:part. Jednotkový test to nemohol nikdy odhaliť, pretože vlastné ValidatePdfACompliance kontrolovalo len to, či kľúč /DestOutputProfile existuje, nie čo je vo vnútri slovníka streamu. Interné testy zostali zelené; reálna archivná validácia zlyhala
Oprava je IccComponentCount, ktorá prečíta signatúru farebného priestoru dát na offsete 16 v hlavičke ICC a namapuje ju na počet zložiek: GRAY je 1, RGB , Lab a XYZ sú 3, CMYK je 4, s neznámym profilom predvolene na 3. Tento počet ide do slovníka streamu ako /N. Je vypočítaný, nie napevno zadaný ako 3, takže aj volajúci, ktorý dodá vlastný profil CMYK alebo grayscale cez IccProfileData, dostane správnu hodnotu. Širšia lekcia je metodická: checker v knižnici aj autoritatívny validátor majú každý svoje slepé miesta a výstup PDF/A treba testovať end-to-end proti referenčnej implementácii, napríklad veraPDF, nie dôverovať iba samokontrolám. Tej istej disciplíne inkrementálnych aktualizácií, ktorá stojí za čistými archívmi, sa venuje článok validating compressed object and xref streams, čo je dôležité, pretože moderné PDF, ktoré injektor spracúva, často stoja na cross-reference streamoch
Šifrovanie, xref streamy a ďalšie hrany
Keďže ISO 19005 šifrovanie zakazuje, save path ho pred zápisom odstráni. SaveAsPdfA pri serializácii aplikuje FPDF_REMOVE_SECURITY, takže šifrovaný zdrojový dokument, načítaný s heslom, sa cestou do archívu dešifruje. Pri nezašifrovanom dokumente je to no-op a nič nemení. Dôsledok je rovnaké obmedzenie, ktoré z opačnej strany presadzuje HotPDF: jeden súbor nemôže byť súčasne šifrovaný aj PDF/A. Keď workflow potrebuje oboje, odpoveďou sú dva artefakty: šifrovaná kópia na distribúciu a samostatná čistá kópia do archívu
Ešte jedna hrana zostáva neviditeľná, kým vás nehryzne: dokumenty PDF 1.5+, ktoré používajú čistý cross-reference stream a nenesú kľúčové slovo trailer. Injektor číta trailer, aby našiel zdrojové /Info a pripojil svoju inkrementálnu aktualizáciu, a musí akceptovať aj formu xref-streamu, inak by sa taký dokument skopíroval ďalej s markermi, ktoré by sa ticho stratili. ISO 32000-1 §7.5.6 výslovne povoľuje, aby po dokumente s xref-streamom nasledovala klasická inkrementálna aktualizácia s trailerom, kde /Prev ukazuje na offset xref-streamu, čo je presne štruktúra, ktorú injektor vytvára. Vlastné FPDF_SaveAsCopy z PDFium vždy zapíše klasický trailer, takže v bežnom prepojení injektor nikdy nenarazí na čistý zdroj s xref-streamom, ale read path to zvládne aj pre dokumenty, ktoré prídu odinakiaľ
Overte to skôr, než uveríte tvrdeniu
Knižnica obsahuje checker na úrovni bajtov, TPdf.ValidatePdfA, ktorý vracia TPdfAValidationResult. Jeho pole Conformance hlási zistenú úroveň a Issues je množina hodnôt TPdfAValidationIssue; pomocná metóda IsCompliant je true iba vtedy, keď bola zistená skutočná úroveň a množina problémov je prázdna. V dávke ho používajte ako rýchlu prvú bránu
var
Pdf: TPdf;
Res: TPdfAValidationResult;
Issue: TPdfAValidationIssue;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'invoice_archive.pdf';
Pdf.Active := True;
Res := Pdf.ValidatePdfA;
if Res.IsCompliant then
Writeln('Conformant: detected level ', Ord(Res.Conformance))
else
for Issue in Res.Issues do
Writeln('Issue: ', Ord(Issue));
finally
Pdf.Free;
end;
end;
Buďte úprimní v tom, čo tým získavate. Checker na úrovni bajtov s vysokou istotou zachytí štrukturálne problémy (chýbajúci OutputIntent, zakázanú akciu, prítomný /Encrypt, transparentnosť tam, kde ju časť 1 zakazuje), a detekcia vložených fontov používa heuristiku založenú na počtoch, ktorá zámerne hlási len signál s vysokou istotou namiesto toho, aby naháňala pokrytie po jednotlivých glyfoch. Čo nerobí, je analýza operátorov v content streamoch, pretože na to by bol potrebný plný parser obsahu, a to je zámerne mimo rozsah. Ako release gate spárujte checker v knižnici s veraPDF: checker je okamžitý a beží všade bez DLL, veraPDF je autoritatívny. Zapojeniu tejto dvojice do dávkového behu sa venuje článok batch preflight report CLI, a práve tam takáto validácia patrí v reálnom archivnom workflow
API SaveAsPdfA, InjectPdfAMarkers a ValidatePdfA ukázané tu sú súčasťou PDFium Component pre Delphi, C++Builder a Lazarus/FPC. Produktová stránka odkazuje na plný referenčný opis API vrátane kompletnej enum hodnoty zhody a options záznamu za týmito príkladmi