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 nemá žiadnu schopnosť pracovať s PDF/A. Vo verejnom rozhraní neexistuje ConvertToPDFA, žiadny zapisovač OutputIntent, žiadne XMP API. Každá časť PDF/A v tejto knižnici, strana zápisu aj strana kontroly, žije v čistom Pascale v 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; // množina TPdfAValidationIssue
function IsCompliant: Boolean; // True iba keď level <> unknown/none
end; // A Issues je prázdna
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, čo výslovne nie je úspech. Na to isté z vonkajšej strany upozorňuje aj dávkový nástroj preflight report CLI: 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. 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. Nahlásite, že sa „našlo“ /AA alebo /3D, pretože tri bajty vo vnútri JPEGu ich náhodou vyhláskovali. Vznikne továreň na false positive
Oprava je PdfStructureBytes: prejde súbor a vynuluje bajty medzi každým stream a 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,pvaiMissingIccProfileapvaiMixedDeviceColorSpaces, keď sa objavia súčasne DeviceRGB aj DeviceCMYK (6.2.3.3) - Tvrdé zákazy pre každú časť:
pvaiEncryptionPresent(slovník/Encryptje úplne zakázaný),pvaiJavaScriptPresent,pvaiForbiddenAction,pvaiAdditionalActions,pvaiLzwUsed,pvaiXfaPresent,pvaiNeedAppearancesTrue,pvaiForbiddenAnnotation - Fonty:
pvaiFontNotEmbeddeda prísnejšípvaiUnembeddedFont, pluspvaiUnicodeMappingMissingpre nárok na Level U bez/ToUnicode - Tagovanie:
pvaiLevelAStructureMissing, keď nárok na zhodu=A nemá tagovanú štruktúru
Šesť najnovších členov, pridaných na ordinaloch 24 až 29, pokrýva jemné prípady, na ktorých recenzenti reálne zakopávajú: pvaiTrappedTrue (/Trapped /True v slovníku Info, „falošný priateľ“, pretože hodnota musí byť False alebo Unknown), pvaiForbiddenActionSubtype (Sound alebo Movie použité ako action, nielen ako anotácia), pvaiTransparentColorSpace (iný ako Normal blend mode alebo /CA//ca nerovné 1,0), pvaiAnnotationDictViolation, pvaiUnembeddedFont a 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ť (skupina /Transparency alebo aktívna /SMask, 6.4), optional content (/OCProperties, 6.1.13) a vložené súbory (/EmbeddedFiles alebo /EF, 6.1.11). 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
Preto validátor číta číslo časti z markera pdfaid cez PdfAPartOf a tieto kontroly púšťa len za podmienky PartNo = 1. Kontroly blend módu a alfa kanálu anotácií pre nové problémy s transparentnosťou sú takisto obmedzené iba na časť 1:
if PartNo = 1 then
begin
if PdfHasName(Struct, '/BM') then
if not PdfHasBMNormal(Struct) then // povolené je len /Normal alebo /Compatible
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 (/Type /ObjStm) 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
PdfExpandObjectStreams túto medzeru uzatvára. Pred spustením akejkoľvek kontroly validátor vykoná Data := PdfExpandObjectStreams(Data). Rutina nájde každý ObjStm, prečíta jeho hlavičku /N a /First, aby získala čísla a offsety obsiahnutých objektov, telo rozbalí pomocou PdfInflate (RTL zlib, System.ZLib na Delphi a zstream na FPC) a každý obsiahnutý objekt pripojí ako obyčajný 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, žiť tam môžu iba slovníky bez streamu, takže expanzia vždy pracuje iba so slovníkmi a pripojené objekty nenesú žiadne kľúčové slovo stream, ktoré by narušilo krok odstraňovania tiel. A pretože pripojený obsah pristane až za %%EOF, spätné vyhľadávanie od startxref stále nájde pôvodný trailer. Samotný trailer cross-reference streamu bol vyriešený už skôr, vo v1.49.3, čítaním Root, Size a ID priamo zo slovníka xref-streamu v čistom texte, čo je téma rozobraná v sesterskom článku o validácii object a cross-reference streamov. Táto téma 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'), ale každý font prispieva dvomi tokenmi /FontDescriptor, jednou referenciou zo slovníka fontu a jedným /Type v samotnom objekte descriptora, takže počet bol 2N oproti N vloženým programom a test bol vždy pravdivý. Opravou je PdfCountDescriptorRefs, ktorá počíta iba formu referencie /FontDescriptor N G R, jednu na font, a vyvolá pvaiUnembeddedFont iba vtedy, keď je vložených programov fontov skutočne menej:
K := PdfCountDescriptorRefs(Struct); // jeden na slovník fontu
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 /DR AcroFormu, ako napríklad /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 (6.2.10) sú zámerne úplne mimo rozsah, pretože by si vyžiadali plné parsovanie obsahu namiesto byte scanu. 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 vkladá XMP, OutputIntent a ICC profil sRGB a čestne znižuje nárok na Level A bez tagovanej štruktúry, stavia na tej istej bytovej infraštruktúre. Obe polovice sú súčasťou PDFium Component pre Delphi, jediného balíka VCL nad čisto pascalovskou implementáciou PDF/A bez akéhokoľvek externého runtime na inštaláciu