En indgangskontrol til arkivet afviste en batch "PDF/A-2b"-filer, som åbnede fint i alle fremvisere på skrivebordet. Leverandøren svor, at de var konforme. Det var de ikke: hver fil havde en JavaScript-handling gemt i cataloget, den slags et flygtigt blik aldrig fanger, og en fuld PDF/A-validator som veraPDF flager på et øjeblik. Fælden er, at ingen ville bolte en Java-toolchain på en Delphi-batchtjeneste bare for at besvare ét ja/nej-spørgsmål pr. fil. Det er det hul, ValidatePdfACompliance i PDFium Component udfylder, og det er værd at forstå, hvordan den når frem til en afgørelse uden nogensinde at parse en content stream helt igennem
Hvorfor PDFium selv ikke kan svare på dette
Det første, man skal være ærlig om: den medfølgende pdfium.dll har slet ingen PDF/A-kapabilitet. Der er ingen ConvertToPDFA, ingen OutputIntent-writer, ingen XMP-API i den offentlige overflade. Hver del af PDF/A i dette bibliotek, både skrivesiden og kontrolsiden, bor i ren Pascal i FPdfPdfa.pas og virker ved byte-niveau-parsing plus trinvis opdatering. Så når du kalder validatoren, spørger du ikke Chromiums renderer om noget. Du kører en Pascal-token-scanner over filens strukturelle bytes
Den offentlige API er bevidst lille. Én funktion læser en stream fra position 0 og returnerer en record:
function ValidatePdfACompliance(Source: TStream): TPdfAValidationResult;
type
TPdfAValidationResult = record
Conformance: TPdfAConformance; // pacUnknown, pacNone, pac1b, pac2u, ...
Issues: TPdfAValidationIssues; // et sæt af TPdfAValidationIssue
function IsCompliant: Boolean; // True kun når niveau <> unknown/none
end; // OG Issues er tomt
IsCompliant koder den regel, der betyder noget i en port: en fil er en pass kun, når et reelt overensstemmelsesniveau blev registreret, og problemsættet er tomt. En parsing, der lykkes, men ikke finder nogen pdfaid-markør, opløses til pacNone, hvilket eksplicit ikke er en pass. Det er samme pointe, som batch-preflight-rapport-CLI'en gør udefra: en tom fundliste på en ukendt fil er ikke en ren sundhedsattest
Fjern stream-legemer, før du scanner tokens
Her er den enkelt vigtigste implementeringsdetalje, og den, der er lettest at få galt, hvis du skriver din egen scanner. Detektoren finder overtrædelser ved at søge efter afgrænsede navnetokens, ting som /JavaScript, /LZWDecode, /BM. Scanner du de rå filbytes, vil de indlejrede binære stream-kroppe, komprimerede billeder, ICC-profiler, fontprogrammer, tilfældigt indeholde bytesekvenser, der ligner de tokens. Du vil rapportere /AA eller /3D som "fundet", fordi tre bytes inde i et JPEG tilfældigvis stavede det. Det er en falsk-positiv-fabrik
Rettelsen er PdfStructureBytes: den gennemgår filen og tømmer bytesene mellem hvert stream- og endstream-nøgleord til mellemrum, mens dictionary-strukturen lades intakt. Først derefter kører scanningen. Hvert navnetoken-tjek i validatoren opererer på denne strippede kopi. Tager du kun én idé med fra denne artikel, så tag den. Den samme disciplin genspejles i PDF/UA-validatoren, som holder sin egen kopi af rutinen, fordi de to standarder udvikler sig uafhængigt
De 29 problemer og hvad hver enkelt betyder
TPdfAValidationIssue er en dokumenteret kontrakt. Ordinalerne er frosne, fordi DUnitX-tests, demoerne og rapportlaget alle afhænger af dem, så nye fund tilføjes altid kun til slutningen. Fra v1.63.0 er der 29 medlemmer. De falder i nogle få familier:
- Metadata og identitet:
pvaiMissingXmpMetadata,pvaiMissingPdfAIdentifier,pvaiMissingTrailerId(ISO 19005-1 6.1.3),pvaiMissingXmpDates - Farve og output:
pvaiMissingOutputIntent,pvaiMissingIccProfile, ogpvaiMixedDeviceColorSpaces, når både DeviceRGB og DeviceCMYK optræder (6.2.3.3) - Hårde forbud for hver del:
pvaiEncryptionPresent(et/Encrypt-dictionary er forbudt helt og aldeles),pvaiJavaScriptPresent,pvaiForbiddenAction,pvaiAdditionalActions,pvaiLzwUsed,pvaiXfaPresent,pvaiNeedAppearancesTrue,pvaiForbiddenAnnotation - Fonte:
pvaiFontNotEmbeddedog den strengerepvaiUnembeddedFont, pluspvaiUnicodeMappingMissingfor en Niveau U-påstand uden/ToUnicode - Tagging:
pvaiLevelAStructureMissing, når en conformance=A-påstand ingen tagget struktur har
De seks nyeste medlemmer, tilføjet ved ordinaler 24 til 29, dækker de subtile tilfælde, gennemlæsere rent faktisk snubler over: pvaiTrappedTrue (en /Trapped /True i Info-dictionaryet, en "falsk ven", eftersom værdien skal være False eller Unknown), pvaiForbiddenActionSubtype (Sound eller Movie brugt som en action, ikke kun en annotation), pvaiTransparentColorSpace (en ikke-Normal blend mode eller et /CA//ca forskelligt fra 1.0), pvaiAnnotationDictViolation, pvaiUnembeddedFont, og pvaiMixedDeviceColorSpaces
Delbevidst gating: A-1 er streng, A-2 og A-3 er mere lempelige
PDF/A er ikke ét regelsæt. Tre ting, som PDF/A-1 forbyder, er eksplicit tilladt fra PDF/A-2 og fremad: transparens (en /Transparency-gruppe eller en aktiv /SMask, 6.4), valgfrit indhold (/OCProperties, 6.1.13), og indlejrede filer (/EmbeddedFiles eller /EF, 6.1.11). En naiv validator, der flager alle tre for hver fil, vil afvise fuldstændig gyldige PDF/A-2-dokumenter en masse
Så validatoren læser delnummeret fra pdfaid-markøren gennem PdfAPartOf og porter de tjek bag PartNo = 1. Blend-mode- og annotation-alpha-tjekkene for de nye transparensproblemer er tilsvarende kun del-1:
if PartNo = 1 then
begin
if PdfHasName(Struct, '/BM') then
if not PdfHasBMNormal(Struct) then // kun /Normal eller /Compatible tilladt
Include(Result.Issues, pvaiTransparentColorSpace);
if PdfHasCaNotOne(Struct, '/CA') or PdfHasCaNotOne(Struct, '/ca') then
Include(Result.Issues, pvaiTransparentColorSpace);
end;
Én konservativ standard fortjener en bemærkning: når der slet ingen pdfaid-markør er, behandles delen som 1, den strengeste. Ræsonnementet er, at en uidentificeret fil bør holdes til de strammeste regler frem for at blive vinket igennem. JavaScript, forbudte handlinger, LZW, XFA, NeedAppearances, forbudte annotationer og ikke-indlejrede fonte forbliver forbudt for hver del, så de tjek sidder aldrig bag porten
Udvid objektstrømme, så intet skjuler sig
PDF 1.5 introducerede krydsreferencestreamen og objektstreamen (/Type /ObjStm), og de skaber en blind vinkel for en naiv bytescanner. Et catalog, en OutputIntent, et action-dictionary, alt, der ikke selv er en stream, kan være Flate-komprimeret inde i en ObjStm. Scanner du den rå struktur, vil du se intet af det og så rapportere en ren fil, der er alt andet end det
PdfExpandObjectStreams lukker det hul. Før noget tjek kører, gør validatoren Data := PdfExpandObjectStreams(Data). Rutinen finder hver ObjStm, læser dens /N- og /First-header for at få de indeholdte objektnumre og offsets, inflaterer kroppen med PdfInflate (RTL'ens zlib, System.ZLib på Delphi og zstream på FPC), og tilføjer hvert indeholdt objekt som et almindeligt N 0 obj ... endobj til slutningen af en kopi af bytesene. De eksisterende token-tjek finder derefter de objekter uden ændring i deres logik
To begrænsninger gør dette rent frem for skrøbeligt. Stream-objekter, Metadata, ICC-profilen og fontprogrammerne kan ikke bo i en objektstream, kun ikke-stream-dictionaries kan, så udvidelsen håndterer altid kun dictionaries, og de tilføjede objekter bærer intet stream-nøgleord til at forstyrre krop-strip-passet. Og fordi det tilføjede indhold lander efter %%EOF, finder den omvendte søgning fra startxref stadig den oprindelige trailer. Selve krydsreferencestream-trailer'en var allerede håndteret tidligere, i v1.49.3, ved at læse Root, Size og ID direkte fra det klartekst xref-stream-dictionary, et emne udforsket i sidestykket om validering af objekt- og krydsreferencestreams; objektstream-arbejdet skulle kun tilføje inflate-trinnet, uden behov for at afkode type-2-xref-poster eller udrulle en PNG-prediktor
De ærlige grænser for en bytebaseret kontrol
Dette er et preflight-værktøj, ikke en certificeret validator, og grænserne er reelle. Font-indlejring er en tælleheuristik, og at få den rigtig krævede en rettelse, det er værd at kende. Det oprindelige tjek brugte PdfCountName('/FontDescriptor'), men hver font bidrager med to /FontDescriptor-tokens, én reference fra font-dictionaryet og ét /Type i selve descriptor-objektet, så tællingen var 2N mod N indlejrede programmer, og testen var altid sand. Rettelsen er PdfCountDescriptorRefs, som kun tæller referenceformen /FontDescriptor N G R, én pr. font, og rejser kun pvaiUnembeddedFont, når indlejrede programmer reelt er færre:
K := PdfCountDescriptorRefs(Struct); // én pr. font-dictionary
Emb := PdfCountName(Struct, '/FontFile')
+ PdfCountName(Struct, '/FontFile2')
+ PdfCountName(Struct, '/FontFile3');
if (K > 0) and (Emb < K) then
Include(Result.Issues, pvaiUnembeddedFont);
Selv rettet er det groft: et blandet dokument, hvor hver descriptor tilfældigvis har en eller anden FontFile, kan stadig lade en enkelt ikke-konform font glide igennem. At udvide objektstreams har også en kendt sideeffekt: det eksponerer de standard-14 standardressourcer, en AcroForm /DR bærer, som /Helv, og heuristikken rapporterer pligtskyldigt dem som ikke indlejret, selvom veraPDF lader dem passere, fordi de aldrig faktisk bruges til at rendre. Content-stream-operator-niveau-tjek (6.2.10) er helt uden for scope, eftersom de ville kræve fuld content-parsing frem for en bytescanning. Behandl validatoren som en hurtig, afhængighedsfri første port, der fanger de overtrædelser, markørinjektion ikke kan fikse, og reservér en fuld validator til den endelige certificering
Det er kontrolhalvdelen af historien. Den komplementære skrivesiden, hvor SaveAsPdfA injicerer XMP'en, OutputIntenten og sRGB ICC-profilen og ærligt nedgraderer en Niveau A-anmodning, der ingen tagget struktur har, bygger på det samme byte-niveau-maskineri. Begge halvdele følger med i PDFium Component for Delphi, en enkelt VCL-pakke oven på en pure-Pascal PDF/A-implementering uden ekstern runtime at installere