Teknisk artikel

PDF/A-preflightvalidering i Delphi med PDFium VCL

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

PDF/A preflight-pipeline til Delphi med PDFium Component: den rå PDF strippes for streamkroppe, objektstrømme oppustes, og en Pascal tokenscan over strukturbytes udfylder én TPdfAValidationResult, hvis IsCompliant-port kræver et detekteret niveau og et tomt issuesæt
ValidatePdfACompliance parser aldrig en content stream: den skanner strukturelle bytes med stream-bodyer blændet af og objektstreams oppustet, og et bestået kræver et detekteret overensstemmelsesniveau plus et tomt issue-sæt

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, og pvaiMixedDeviceColorSpaces, 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: pvaiFontNotEmbedded og den strengere pvaiUnembeddedFont, plus pvaiUnicodeMappingMissing for 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

Diagram over de 29 TPdfAValidationIssue-koder i PDFium Component PDF/A-validatoren til Delphi, grupperet i metadata og identitet, farve og output, hårde forbud, skrifttyper, tagging og de seks nyeste findings som pvaiTrappedTrue og pvaiTransparentColorSpace
De 29 issue-koder deler sig i fem familier plus de seks nyeste medlemmer, med hårde forbud som kryptering, JavaScript og LZW, der gælder for hver PDF/A-del

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

Delbevidst PDF/A-gating i Delphi: den erklærede pdfaid-del føder en PartNo = 1-port, der aktiverer transparens-, optional content- og embedded files-kontrollerne kun for PDF/A-1, mens JavaScript, LZW, XFA og uindlejrede skrifttyper forbliver forbudt for hver del
Partbevidst gating læser pdfaid part-nummeret og anvender kun gennemsigtigheds-, optional content- og embedded file-tjek på PDF/A-1, mens filer uden markør holdes til de strengeste regler

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