Teknisk artikkel

PDF/A-preflightvalidering i Delphi med PDFium VCL

En kontroll i arkivinntaket avviste et parti med "PDF/A-2b"-filer som åpnet uten problemer i alle viserne på pulten. Leverandøren sverget på at de var konforme. Det var de ikke: hver av dem bar på en JavaScript-handling gjemt i katalogen, den typen ting et tilfeldig blikk aldri fanger og en full PDF/A-validator som veraPDF markerer umiddelbart. Haken er at ingen ville trekke inn en Java-toolchain i en Delphi-batchtjeneste bare for å svare på ett ja-eller-nei-spørsmål per fil. Det er gapet ValidatePdfACompliance i PDFium Component fyller, og det er verdt å forstå hvordan den kommer frem til et svar uten noensinne å parse en innholdsstrøm fullt ut.

Hvorfor PDFium ikke kan svare på dette alene

Det første man må være ærlig om: den innebygde pdfium.dll har ingen PDF/A-funksjonalitet i det hele tatt. Det finnes verken ConvertToPDFA, noen OutputIntent-skriver eller noe XMP-API i den offentlige overflaten. Alt i PDF/A i dette biblioteket, både skrivingen og kontrollen, lever i ren Pascal i FPdfPdfa.pas og arbeider med byte-nivåparsing pluss inkrementell oppdatering. Når du kaller validatoren, spør du altså ikke Chromium-rendereren om noe. Du kjører en Pascal-token-skanner over filens strukturelle bytes.

Det offentlige API-et er bevisst lite. Én funksjon leser en strøm fra posisjon 0 og returnerer en record:

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 koder regelen som betyr noe i en kontroll: en fil er bestått bare når et reelt konformitetsnivå er oppdaget og issue-settet er tomt. En parsing som lykkes men ikke finner noe pdfaid-merke ender som pacNone, som eksplisitt ikke er en passering. Det er samme poeng som den batch-preflightrapport-CLI-en viser utenfra: en tom funnliste på en fil uten gjenkjent signatur er ikke en ren helseattest.

Å fjerne strømlegemer før noen token-skanning

Her er den viktigste implementasjonsdetaljen, og den enkleste å gjøre feil hvis du skriver din egen skanner. Detektoren finner brudd ved å søke etter avgrensede navnetoken, ting som /JavaScript, /LZWDecode, /BM. Hvis du skanner råfilens bytes direkte, vil de innebygde binærstrømmene, komprimerte bilder, ICC-profiler og fontprogrammer tilfeldig inneholde byte-sekvenser som ser ut som disse tokenene. Du ender opp med å rapportere at /AA eller /3D ble "funnet" fordi tre bytes inne i en JPEG tilfeldigvis stavet det. Det er en fabrikk for falske positiver.

Fiksen er PdfStructureBytes: den går gjennom filen og blanker ut bytes mellom hvert stream- og endstream-nøkkelord med mellomrom, samtidig som ordbokstrukturen blir stående urørt. Først etter det kjører skanningen. Hver kontroll av navnetoken i validatoren opererer på denne rensede kopien. Hvis du tar med deg én idé fra denne artikkelen, så ta med den. Den samme disiplinen speiles i PDF/UA-validatoren, som holder sin egen kopi av rutinen fordi de to standardene utvikler seg uavhengig av hverandre.

De 29 feilene og hva hver av dem betyr

TPdfAValidationIssue er en dokumentert kontrakt. Ordinalene er låst fordi DUnitX-tester, demoene og rapportlaget alle er avhengige av dem, så nye funn legges bare til på slutten. Per v1.63.0 finnes det 29 medlemmer. De faller i noen få familier:

  • Metadata og identitet: pvaiMissingXmpMetadata, pvaiMissingPdfAIdentifier, pvaiMissingTrailerId (ISO 19005-1 6.1.3), pvaiMissingXmpDates.
  • Farge og output: pvaiMissingOutputIntent, pvaiMissingIccProfile og pvaiMixedDeviceColorSpaces når både DeviceRGB og DeviceCMYK opptrer (6.2.3.3).
  • Harde forbud for alle deler: pvaiEncryptionPresent (en /Encrypt-ordbok er fullstendig forbudt), pvaiJavaScriptPresent, pvaiForbiddenAction, pvaiAdditionalActions, pvaiLzwUsed, pvaiXfaPresent, pvaiNeedAppearancesTrue, pvaiForbiddenAnnotation.
  • Fonter: pvaiFontNotEmbedded og den strengere pvaiUnembeddedFont, pluss pvaiUnicodeMappingMissing for et Level U-krav uten /ToUnicode.
  • Tagging: pvaiLevelAStructureMissing når et conformance=A-krav ikke har noen tagget struktur.

De seks nyeste medlemmene, lagt til på ordinalene 24 til 29, dekker de mer subtile tilfellene gjennomlesere faktisk snubler i: pvaiTrappedTrue (en /Trapped /True i Info-ordboken, en "falsk venn" siden verdien må være False eller Unknown), pvaiForbiddenActionSubtype (Sound eller Movie brukt som en handling, ikke bare en annotasjon), pvaiTransparentColorSpace (en blend mode som ikke er Normal eller en /CA//ca som ikke er lik 1.0), pvaiAnnotationDictViolation, pvaiUnembeddedFont og pvaiMixedDeviceColorSpaces.

Partspesifikk styring: A-1 er streng, A-2 og A-3 er løsere

PDF/A er ikke én regelbok. Tre ting som PDF/A-1 forbyr, er uttrykkelig tillatt fra PDF/A-2 og utover: transparens, enten en /Transparency-gruppe eller en aktiv /SMask, 6.4, valgfritt innhold (/OCProperties, 6.1.13) og innebygde filer (/EmbeddedFiles eller /EF, 6.1.11). En naiv validator som flagger alle tre for hver fil, vil avvise helt gyldige PDF/A-2-dokumenter i bulk.

Validatoren leser derfor delenummeret fra pdfaid-merket gjennom PdfAPartOf og legger disse sjekkene bak PartNo = 1. Kontrollene for blend mode og annotasjonsalfa i de nye transparensfeilene er på samme måte bare for del 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;

En konservativ standardverdi fortjener også å bli nevnt: når det ikke finnes noe pdfaid-merke i det hele tatt, behandles delen som 1, altså den strengeste. Begrunnelsen er at en uidentifisert fil bør holdes til de tetteste reglene i stedet for å slippes gjennom. JavaScript, forbudte handlinger, LZW, XFA, NeedAppearances, forbudte annotasjoner og ikke-innfelte fonter forblir forbudt for alle deler, så disse kontrollene ligger aldri bak porten.

Å utvide objektstrømmer så ingenting kan gjemme seg

PDF 1.5 introduserte kryssreferansestrømmen og objektstrømmen (/Type /ObjStm), og de skaper en blind flekk for en naiv byte-skanner. Et katalogobjekt, en OutputIntent, en action-ordbok, alt som ikke er en strøm i seg selv, kan være Flate-komprimert inne i en ObjStm. Skanner du den rå strukturen, ser du ingen av dem, og ender opp med å rapportere en ren fil som alt annet enn ren.

PdfExpandObjectStreams tetter det gapet. Før noen kontroll kjøres, gjør validatoren Data := PdfExpandObjectStreams(Data). Rutinen finner hver ObjStm, leser /N- og /First-headeren for å hente de innholdte objektnumrene og offsetene, inflaterer kroppen med PdfInflate (RTL-zlib-en, System.ZLib på Delphi og zstream på FPC), og legger hvert innholdte objekt til på slutten av en kopi av byte-strømmen som et vanlig N 0 obj ... endobj. De eksisterende token-kontrollene finner deretter disse objektene uten at logikken deres endres.

To begrensninger gjør dette rent i stedet for skjørt. Strømmeobjekter, Metadata, ICC-profil og fontprogrammer kan ikke ligge i en objektstrøm, bare ikke-strømmeordbøker kan det, så utvidingen håndterer bare ordbøker og de appenderte objektene bærer ikke noe stream-nøkkelord som forstyrrer passet som fjerner strømlegemer. Og fordi det appenderte innholdet havner etter %%EOF, finner det bakovergående søket fra startxref fortsatt den opprinnelige traileren. Selve kryssreferansestrøm-traileren ble allerede håndtert tidligere, i v1.49.3, ved å lese Root, Size og ID direkte fra den tekstlige xref-stream-ordboken, et tema som er gjennomgått i følgeteksten om validering av objekt- og kryssreferansestrømmer; arbeidet med objektstrømmer trengte bare å legge til inflateringssteget, uten behov for å dekode type-2 xref-poster eller pakke ut en PNG-prediktor.

De ærlige grensene for en byte-nivåkontroll

Dette er et preflight-verktøy, ikke en sertifisert validator, og grensene er reelle. Fontinnbygging er en telleheuristikk, og det tok en korreksjon som er verdt å kjenne til for å få den riktig. Den opprinnelige sjekken brukte PdfCountName('/FontDescriptor'), men hver font bidrar med to /FontDescriptor-token, én referanse fra fontordboken og én /Type i descriptor-objektet selv, så tellingen ble 2N mot N innebygde fontprogrammer og testen var alltid sann. Løsningen er PdfCountDescriptorRefs, som bare teller referanseformen /FontDescriptor N G R, én per font, og hever pvaiUnembeddedFont bare når de innebygde programmene faktisk er færre:

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);

Selv etter korrigeringen er den grov: et blandet dokument der hver descriptor tilfeldigvis har en FontFile, kan fortsatt slippe gjennom en enkelt font som ikke er konform. Utviding av objektstrømmer har også en kjent bivirkning, den eksponerer standard-14-standardressursene som en AcroForm /DR bærer, som /Helv, og heuristikken rapporterer dem pliktoppfyllende som ikke innebygde selv om veraPDF lar dem passere fordi de aldri faktisk brukes til gjengivelse. Kontroller på operatørnivå i innholdsstrømmen (6.2.10) ligger helt utenfor omfanget, siden de ville kreve full parsing av innholdet i stedet for en byte-skanning. Se på validatoren som en rask, avhengighetsfri første sjekk som fanger brudd som markørinjeksjon ikke kan fikse, og spar en full validator til endelig sertifisering.

Dette er kontrollsiden av historien. Den komplementære skrivesiden, der SaveAsPdfA injiserer XMP, OutputIntent og sRGB ICC-profil og ærlig nedgraderer et Level A-krav som mangler tagget struktur, bygger på den samme byte-nivåmaskinen. Begge deler leveres i PDFium Component for Delphi, én enkelt VCL-pakke over en ren Pascal PDF/A-implementasjon uten noe eksternt runtime å installere.