Brána pro příjem archivů odmítla dávku souborů "PDF/A-2b", které se na každém prohlížeči na stole otevíraly bez potíží. Dodavatel přísahal, že jsou v pořádku. Nebyly: každý z nich nesl akci JavaScriptu ukrytou v katalogu, přesně ten typ věci, který běžné oko snadno přehlédne a plný validátor PDF/A jako veraPDF okamžitě zachytí. Zádrhel byl v tom, že nikdo nechtěl kvůli jediné odpovědi ano/ne na soubor přidávat do dávkové služby v Delphi nástrojový řetězec v Javě. To je mezera ValidatePdfACompliance kterou vyplňuje PDFium Component, a stojí za to pochopit, jak dospívá k verdiktu, aniž by kdy plně parsoval obsahový stream
Proč na to samotný PDFium nestačí
První věc, kterou je fér říct: přibalený pdfium.dll vůbec neumí PDF/A. Není tu žádný ConvertToPDFA, žádný zapisovač OutputIntent a žádné veřejné XMP API. Všechny části PDF/A v této knihovně, jak zapisování, tak kontrola, žijí v čistém Pascalu v FPdfPdfa.pas a fungují na parsování po bajtech plus postupné aktualizaci. Když tedy zavoláte validátor, neptáte se na nic renderovacího enginu Chromium. Procházíte pascalovým tokenovým skenerem strukturální bajty souboru
Veřejné API je záměrně malé. Jedna funkce načte stream od pozice 0 a vrátí záznam:
function ValidatePdfACompliance(Source: TStream): TPdfAValidationResult;
type
TPdfAValidationResult = record
Conformance: TPdfAConformance; // pacUnknown, pacNone, pac1b, pac2u, ...
Issues: TPdfAValidationIssues; // množina TPdfAValidationIssue
function IsCompliant: Boolean; // True jen když level <> unknown/none
end; // a zároveň Issues je prázdné
IsCompliant vyjadřuje pravidlo, které v bráně rozhoduje: soubor projde jen tehdy, když byla zjištěna skutečná úroveň shody a sada problémů je prázdná. Parse, který uspěje, ale nenajde žádný marker pdfaid, se vyhodnotí jako pacNone, což výslovně neznamená úspěch. Tentýž bod zvenčí říká CLI pro dávkový preflight report: prázdný seznam nálezů u nerozpoznaného souboru není čistý zdravotní list
Odstranění těl streamů před jakýmkoli tokenovým skenem
Tady je jediný nejdůležitější implementační detail, a zároveň ten, který si snadno pokazíte, pokud píšete vlastní skener. Detektor nachází porušení hledáním ohraničených jmenných tokenů, věcí jako /JavaScript, /LZWDecode, /BM. Pokud skenujete syrové bajty souboru, budou vložená binární těla streamů, komprimované obrázky, ICC profily i fontové programy náhodně obsahovat byteové sekvence, které těmto tokenům připomínají. Budete hlásit /AA nebo /3D „nalezeno“, protože tři bajty uvnitř JPEG náhodou složily to slovo. To je továrna na falešně pozitivní výsledky
Řešením je PdfStructureBytes: prochází soubor a přepisuje bajty mezi každým klíčovým slovem stream a endstream na mezery, přičemž strukturu slovníku zachová beze změny. Teprve potom se spustí sken. Každá kontrola jmenného tokenu ve validátoru pracuje s touto očištěnou kopií. Pokud si z tohoto článku odnesete jedinou věc, pak právě tuhle. Stejnou disciplínu zrcadlí i validátor PDF/UA, který si udržuje vlastní kopii rutiny, protože oba standardy se vyvíjejí nezávisle
29 problémů a co který znamená
TPdfAValidationIssue je zdokumentovaný kontrakt. Číselné hodnoty jsou zamrzlé, protože na nich závisí testy DUnitX, ukázky i vrstva reportů, takže nová zjištění se vždy jen přidávají na konec. K verzi v1.63.0 je tu 29 členů. Dělí se do několika rodin:
- Metadata a identita:
pvaiMissingXmpMetadata,pvaiMissingPdfAIdentifier,pvaiMissingTrailerId(ISO 19005-1 6.1.3),pvaiMissingXmpDates - Barva a výstup:
pvaiMissingOutputIntent,pvaiMissingIccProfileapvaiMixedDeviceColorSpaceskdyž se současně objeví DeviceRGB i DeviceCMYK (6.2.3.3) - Přísné zákazy pro každou část:
pvaiEncryptionPresent(slovník/Encryptje zcela zakázán),pvaiJavaScriptPresent,pvaiForbiddenAction,pvaiAdditionalActions,pvaiLzwUsed,pvaiXfaPresent,pvaiNeedAppearancesTrue,pvaiForbiddenAnnotation - Písma:
pvaiFontNotEmbeddeda přísnějšípvaiUnembeddedFont, pluspvaiUnicodeMappingMissingpro tvrzení Level U bez/ToUnicode - Značkování:
pvaiLevelAStructureMissingkdyž tvrzení o shodě=A nemá žádnou značkovanou strukturu
Šest nejnovějších členů, přidaných na pozice 24 až 29, pokrývá jemné případy, na které recenzenti skutečně narážejí: pvaiTrappedTrue (hodnota /Trapped /True v Info slovníku, „falešný přítel“, protože hodnota musí být False nebo Unknown), pvaiForbiddenActionSubtype (Sound nebo Movie použité jako akce, ne jen jako anotace), pvaiTransparentColorSpace (režim míchání jiný než Normal nebo /CA//ca není rovno 1.0), pvaiAnnotationDictViolation, pvaiUnembeddedFont a pvaiMixedDeviceColorSpaces
Podmíněné kontroly podle části: A-1 je přísná, A-2 a A-3 jsou mírnější
PDF/A není jedna sada pravidel. Tři věci, které PDF/A-1 zakazuje, jsou od PDF/A-2 dál výslovně povoleny: transparentnost (skupina /Transparency nebo aktivní /SMask, 6.4), volitelný obsah (/OCProperties, 6.1.13) a vložené soubory (/EmbeddedFiles nebo /EF, 6.1.11). Naivní validátor, který všechny tři označí u každého souboru, hromadně odmítne zcela platné dokumenty PDF/A-2
Validátor tedy čte číslo části z příznaku pdfaid prostřednictvím PdfAPartOf a všechny tyto kontroly povolí jen při PartNo = 1. Kontroly režimu prolnutí a alfa kanálu anotací pro nové problémy s průhledností jsou podobně jen pro část 1:
if PartNo = 1 then
begin
if PdfHasName(Struct, '/BM') then
if not PdfHasBMNormal(Struct) then // povolené jen /Normal nebo /Compatible
Include(Result.Issues, pvaiTransparentColorSpace);
if PdfHasCaNotOne(Struct, '/CA') or PdfHasCaNotOne(Struct, '/ca') then
Include(Result.Issues, pvaiTransparentColorSpace);
end;
Stojí za zmínku jedno konzervativní výchozí pravidlo: když chybí úplně značka pdfaid, část se považuje za 1, tedy za nejpřísnější. Důvod je ten, že neidentifikovaný soubor by měl podléhat nejpřísnějším pravidlům, ne být jen tak puštěn dál. JavaScript, zakázané akce, LZW, XFA, NeedAppearances, zakázané anotace a nevložená písma zůstávají zakázaná pro každou část, takže tyto kontroly nikdy nestojí za branou
Rozbalování objektových streamů, aby se nic neschovalo
PDF 1.5 zavedlo stream křížových odkazů a objektový stream (/Type /ObjStm), a pro naivní skener bajtů vytvářejí slepé místo. Katalog, OutputIntent, slovník akce, cokoli, co samo není stream, může být uvnitř ObjStm komprimováno pomocí Flate. Projdete syrovou strukturu a neuvidíte z toho nic, pak nahlásíte čistý soubor, který je všechno, jen ne čistý
PdfExpandObjectStreams tuto mezeru uzavírá. Než se spustí jakákoli kontrola, validátor provede Data := PdfExpandObjectStreams(Data). Rutina najde každý ObjStm, přečte jeho /N a /First hlavičku, aby získala čísla obsažených objektů a jejich offsety, a rozbalí tělo pomocí PdfInflate (zlib z RTL, System.ZLib v Delphi a zstream ve FPC), a připojí každý obsažený objekt jako běžný N 0 obj ... endobj na konec kopie bajtů. Stávající kontroly tokenů pak tyto objekty najdou beze změny své logiky
Dvě omezení z toho dělají něco čistého, ne křehkého. Streamové objekty, Metadata, ICC profil a programy písem nemohou být v objektovém streamu, mohou tam být jen slovníky, které samy nejsou streamy, takže rozbalování se vždy týká jen slovníků a připojené objekty nenesou žádné stream klíčové slovo, které by narušilo fázi odstraňování těla. A protože přidaný obsah dopadne až za %%EOF, zpětné hledání od startxref stále najde původní trailer. Samotný trailer cross-reference streamu už byl vyřešen dříve, ve verzi v1.49.3, tím, že se Root, Size a ID četly přímo z prostého slovníku xref-streamu, tématem rozvedeným v doprovodném článku o ověřování object and cross-reference streams; práce na object streamu musela přidat jen krok rozbalení, bez potřeby dekódovat xref položky typu 2 nebo rozmotávat PNG prediktor
Poctivá omezení kontrol na úrovni bajtů
Tohle je předběžný nástroj, ne certifikovaný validátor, a hranice jsou skutečné. Vkládání písem je heuristika počítání a dostat ji správně si vyžádalo opravu, která stojí za zmínku. Původní kontrola používala PdfCountName('/FontDescriptor'), ale každé písmo přispívá dvěma /FontDescriptor tokeny, jednou referencí ze slovníku písma a jedním /Type v samotném objektu deskriptoru, takže počet byl 2N proti N vloženým programům a test byl vždy pravdivý. Oprava je PdfCountDescriptorRefs, který počítá jen /FontDescriptor N G R referenční formu, jednu na písmo, a vyvolá pvaiUnembeddedFont jen tehdy, když je vložených programů skutečně méně:
K := PdfCountDescriptorRefs(Struct); // jeden na slovník písma
Emb := PdfCountName(Struct, '/FontFile')
+ PdfCountName(Struct, '/FontFile2')
+ PdfCountName(Struct, '/FontFile3');
if (K > 0) and (Emb < K) then
Include(Result.Issues, pvaiUnembeddedFont);
I po opravě je to hrubé: smíšený dokument, kde má každý deskriptor náhodou nějaký FontFile, může i tak pustit jednotlivé nevyhovující písmo. Rozbalování objektových streamů má také známý vedlejší účinek, odhalí výchozí zdroje standardu 14, které nese AcroForm /DR, například /Helv, a heuristika je poctivě označí jako nevložené, i když je veraPDF pustí dál, protože se ve skutečnosti nikdy nepoužívají při vykreslování. Kontroly na úrovni operátorů content streamu (6.2.10) jsou zcela mimo rozsah, protože by vyžadovaly plný rozbor obsahu, ne jen kontrolu bajtů. Berte validátor jako rychlou první bránu bez závislostí, která zachytí porušení, jež injekce markeru neumí opravit, a pro finální certifikaci si nechte plnohodnotný validátor
Takhle vypadá kontrolní polovina příběhu. Doplňující zápisová část, kde SaveAsPdfA vkládá XMP, OutputIntent a sRGB ICC profil a poctivě snižuje požadavek Level A, který nemá žádnou tagovanou strukturu, stojí na stejné byteové mechanice. Obě poloviny jsou součástí PDFium Component for Delphi, jediného balíčku VCL nad čistě Pascalovou implementací PDF/A bez externího runtime k instalaci