Odborný článok

PDF/A Preflight Validation in Delphi with PDFium VCL

Brána ingestie archívu odmietla dávku súborov „PDF/A-2b“, hoci sa na každom prehliadači na stole otvárali bez problémov. Dodávateľ prisahal, že sú konformné. Neboli: každý z nich niesol JavaScript action ukrytý v katalógu, presne ten typ veci, ktorý letmý pohľad nikdy neodhalí a plný validátor PDF/A, napríklad veraPDF, odhalí okamžite. Háčik bol v tom, že nikto nechcel do dávkovej služby v Delphi pridávať java toolchain len kvôli jednej binárnej otázke na súbor. Túto medzeru vypĺňa ValidatePdfACompliance v PDFium Component a stojí za to pochopiť, ako dochádza k verdiktu bez toho, aby sa vôbec kompletne parsoval content stream

Prečo na to samotné PDFium nestačí

Prvá vec, ktorú treba priznať: pribalené pdfium.dll has no PDF/A capability at all. There is no ConvertToPDFA, no OutputIntent writer, no XMP API in the public surface. Every part of PDF/A in this library, both the writing side and the checking side, lives in pure Pascal in FPdfPdfa.pas a funguje cez parsovanie na úrovni bajtov plus inkrementálnu aktualizáciu. Keď teda voláte validátor, nepýtate sa nič rendereru z Chromiumu. Spúšťate pascalovský token scanner nad štrukturálnymi bajtmi súboru

Verejné API je zámerne malé. Jedna funkcia prečíta stream od pozície 0 a vráti záznam:

function ValidatePdfACompliance(Source: TStream): TPdfAValidationResult;

type
  TPdfAValidationResult = record
    Conformance: TPdfAConformance;        // pacUnknown, pacNone, pac1b, pac2u, ...
    Issues: TPdfAValidationIssues;        // a set of TPdfAValidationIssue
    function IsCompliant: Boolean;        // True only when level <> unknown/none
  end;                                    // AND Issues is empty

IsCompliant kóduje pravidlo, na ktorom v gate záleží: súbor prejde iba vtedy, keď bola zistená skutočná úroveň zhody a množina problémov je prázdna. Parsovanie, ktoré uspeje, ale nenájde marker pdfaid, sa vyhodnotí ako pacNone, which is explicitly not a pass. This is the same point the batch preflight report CLI zdôrazňuje zvonka to isté: prázdny zoznam nálezov pri nerozpoznanom súbore nie je čistý účet zdravia

Odstránenie tiel streamov pred akýmkoľvek token scanom

Toto je najdôležitejší detail implementácie a zároveň ten, ktorý sa najľahšie pokazí, ak si vlastný scanner píšete sami. Detektor nachádza porušenia vyhľadávaním ohraničených name tokenov, vecí ako /JavaScript, /LZWDecode, /BM. If you scan the raw file bytes, the embedded binary stream bodies, compressed images, ICC profiles, font programs, will randomly contain byte sequences that look like those tokens. You will report /AA or /3D. Ak skenujete surové bajty súboru, vložené binárne telá streamov, komprimované obrázky, ICC profily a font programy budú náhodne obsahovať sekvencie bajtov, ktoré vyzerajú ako tieto tokeny. Vznikne továreň na false positive

Oprava je PdfStructureBytes: it walks the file and blanks the bytes between every stream and endstream na medzery, pričom štruktúru slovníkov nechá nedotknutú. Až potom sa spustí sken. Každá kontrola name tokenov vo validátore pracuje nad touto odizolovanou kópiou. Ak si z tohto článku odnesiete jedinú myšlienku, nech je to táto. Tá istá disciplína sa zrkadlí aj vo validátore PDF/UA, ktorý si túto rutinu drží vo vlastnej kópii, pretože oba štandardy sa vyvíjajú nezávisle

29 problémov a čo každý z nich znamená

TPdfAValidationIssue je zdokumentovaný kontrakt. Ordinaly sú zmrazené, pretože od nich závisia DUnitX testy, demá aj report vrstva, takže nové nálezy sa vždy len pripájajú na koniec. K verzii v1.63.0 má enum 29 členov. Padajú do niekoľkých rodín:

  • Metadáta a identita: pvaiMissingXmpMetadata, pvaiMissingPdfAIdentifier, pvaiMissingTrailerId (ISO 19005-1 6.1.3), pvaiMissingXmpDates
  • Farba a výstup: pvaiMissingOutputIntent, pvaiMissingIccProfile, and pvaiMixedDeviceColorSpaces when both DeviceRGB and DeviceCMYK appear (6.2.3.3)
  • Hard prohibitions for every part: pvaiEncryptionPresent (an /Encrypt dictionary is forbidden outright), pvaiJavaScriptPresent, pvaiForbiddenAction, pvaiAdditionalActions, pvaiLzwUsed, pvaiXfaPresent, pvaiNeedAppearancesTrue, pvaiForbiddenAnnotation
  • Fonts: pvaiFontNotEmbedded and the stricter pvaiUnembeddedFont, plus pvaiUnicodeMappingMissing for a Level U claim without /ToUnicode
  • Tagging: pvaiLevelAStructureMissing when a conformance=A claim has no tagged structure

The six newest members, added at ordinals 24 through 29, cover the subtle cases reviewers actually trip over: pvaiTrappedTrue (a /Trapped /True v Info dictionary, falošný priateľ, pretože hodnota musí byť False alebo Unknown, pvaiForbiddenActionSubtype (Sound or Movie used as an action, not just an annotation), pvaiTransparentColorSpace (a non-Normal blend mode or a /CA//ca not equal to 1.0), pvaiAnnotationDictViolation, pvaiUnembeddedFont, and pvaiMixedDeviceColorSpaces

Gating podľa časti: A-1 je prísne, A-2 a A-3 povoľujú viac

PDF/A nie je jedna kniha pravidiel. Tri veci, ktoré PDF/A-1 zakazuje, sú od PDF/A-2 výslovne povolené: transparentnosť, optional content a embedded files. Naivný validátor, ktorý všetky tri veci označí ako porušenie pri každom súbore, bude hromadne odmietať úplne platné dokumenty PDF/A-2./Transparency group or an active /SMask, 6.4), optional content (/OCProperties, 6.1.13), and embedded files (/EmbeddedFiles or /EF, 6.1.11). A naive validator that flags all three for every file will reject perfectly valid PDF/A-2 documents en masse

Preto validátor číta číslo časti z markera pdfaid cez PdfAPartOf a tieto kontroly púšťa len za podmienok daných príslušnou časťou.PartNo = 1Kontroly nových problémov s transparentnosťou sú rovnako obmedzené iba na časť 1

if PartNo = 1 then
begin
  if PdfHasName(Struct, '/BM') then
    if not PdfHasBMNormal(Struct) then          // only /Normal or /Compatible allowed
      Include(Result.Issues, pvaiTransparentColorSpace);
  if PdfHasCaNotOne(Struct, '/CA') or PdfHasCaNotOne(Struct, '/ca') then
    Include(Result.Issues, pvaiTransparentColorSpace);
end;

Jedno konzervatívne predvolené správanie stojí za zmienku: keď marker pdfaid chýba úplne, časť sa považuje za 1, teda za najprísnejšiu. Úvaha je jednoduchá: neidentifikovaný súbor má byť držaný na najprísnejších pravidlách, nie mávnutý ďalej. JavaScript, zakázané akcie, LZW, XFA, NeedAppearances, zakázané anotácie a nevložené fonty zostávajú zakázané pre všetky časti, preto tieto kontroly nikdy nesedia za gate

Rozbalenie object streamov, aby sa v nich nič neschovalo

PDF 1.5 zaviedlo cross-reference stream aj object stream a tie vytvárajú slepé miesto pre naivný byte scanner. Katalóg, OutputIntent, action dictionary, skrátka čokoľvek, čo samo nie je stream, môže byť Flate-komprimované vo vnútri ObjStm. Ak skenujete surovú štruktúru, neuvidíte z toho nič a nahlásite čistý súbor, ktorý čistý vôbec nie je./Type /ObjStm), and they create a blind spot for a naive byte scanner. A catalog, an OutputIntent, an action dictionary, anything that is not itself a stream, can be Flate-compressed inside an ObjStm. Scan the raw structure and you will see none of it, then report a clean file that is anything but

PdfExpandObjectStreams túto medzeru uzatvára. Pred spustením akejkoľvek kontroly validátor vykoná Data := PdfExpandObjectStreams(Data). The routine finds every ObjStm, reads its /N and /First header to get the contained object numbers and offsets, inflates the body with PdfInflate (the RTL zlib, System.ZLib on Delphi and zstream on FPC), and appends each contained object as an ordinary N 0 obj ... endobj na koniec kópie bajtov. Existujúce tokenové kontroly potom tieto objekty nájdu bez jedinej zmeny logiky

Dve obmedzenia z toho robia čisté riešenie namiesto krehkého hacku. Stream objekty, Metadata, ICC profil aj font programy, nemôžu žiť vo vnútri object streamu, takže expanzia vždy pracuje iba so slovníkmi a pripojené objekty nenesú žiadne stream keyword to disturb the body-stripping pass. And because the appended content lands after %%EOF, the reverse search from startxref still finds the original trailer. The cross-reference stream trailer itself was already handled earlier, in v1.49.3, by reading Root, Size, and ID straight from the plaintext xref-stream dictionary, a topic explored in the companion piece on validating object and cross-reference streams. Téma xref-streamových trailerov bola vyriešená už skôr, takže object-stream práca potrebovala pridať len krok inflate bez potreby dekódovať type-2 xref entries alebo rozmotávať PNG predictor

Poctivé limity checkeru na úrovni bajtov

Toto je preflight nástroj, nie certifikovaný validátor, a jeho hranice sú skutočné. Detekcia vložených fontov je heuristika založená na počtoch a aj tá si vyžiadala opravu, ktorú sa oplatí poznať. Pôvodná kontrola používala PdfCountName('/FontDescriptor'), but each font contributes two /FontDescriptor tokens, one reference from the font dictionary and one /Type in the descriptor object itself, so the count was 2N against N embedded programs and the test was always true. The fix is PdfCountDescriptorRefs, which counts only the /FontDescriptor N G R reference form, one per font, and raises pvaiUnembeddedFont iba vtedy, keď je vložených programov fontov skutočne menej:

K := PdfCountDescriptorRefs(Struct);                 // one per font dict
Emb := PdfCountName(Struct, '/FontFile')
     + PdfCountName(Struct, '/FontFile2')
     + PdfCountName(Struct, '/FontFile3');
if (K > 0) and (Emb < K) then
  Include(Result.Issues, pvaiUnembeddedFont);

Aj po oprave zostáva hrubá: zmiešaný dokument, v ktorom má každý descriptor nejaký FontFile, môže stále prepustiť jednotlivý nezhodný font. Expanzia object streamov má aj známy vedľajší efekt: odkryje štandard-14 default resources, ktoré nesie AcroForm /DR carries, such as /Helv, a heuristika ich poctivo nahlási ako nevložené, hoci veraPDF ich prepustí, pretože sa nikdy reálne nepoužijú na vykreslenie. Kontroly na úrovni operátorov content streamu sú zámerne mimo rozsah. Berte validátor ako rýchlu prvú bránu bez externých závislostí, ktorá zachytí tie porušenia, ktoré injekcia markerov nevie opraviť, a plný validátor si nechajte na finálnu certifikáciu

Toto je kontrolná polovica príbehu. Komplementárna zapisovacia strana, kde SaveAsPdfA injects the XMP, OutputIntent, and sRGB ICC profile and honestly downgrades a Level A request that has no tagged structure, builds on the same byte-level machinery. Both halves ship in the PDFium Component for Delphi, stavia na tej istej bytovej infraštruktúre. Obe polovice sú súčasťou