Technical Article

Validering af komprimerede PDF-filer: Objekt- og XRef-strømme

Du skriver en lille validator. Den åbner en PDF, søger til slutningen, finder startxref, læser forskydningen (offset) og forventer at lande på nøgleordet xref med en krydsreferencetabel med fast bredde nedenunder. Fra den tabel indsamler den objektforskydninger og scanner derefter bagud efter nøgleordet trailer for at finde /Root og /Size. Det fungerer perfekt på alle filer, du genererede for at teste det. Derefter ankommer en fil produceret af en aktuel version af Word, eller af et bibliotek rettet mod PDF 1.5, og validatoren erklærer den for ødelagt. Der er intet xref-nøgleord, hvor forskydningen peger hen, ingen trailer-ordbog nogen steder, og objekttabellen, som validatoren byggede, er næsten tom. Filen er gyldig. Validatoren læser den gennem en femten år gammel linse.

Dette er den absolut mest almindelige årsag til, at en PDF-kontrol på byte-niveau skrevet mod det klassiske layout fejler på moderne dokumenter. Den struktur, den afhænger af, nemlig krydsreferencetabellen i klartekst og nøgleordet trailer, blev gjort valgfri i PDF 1.5 og er ofte fraværende. Two funktioner erstattede den: krydsreferencestrømmen (cross-reference stream) og den komprimerede objektstrøm (compressed object stream). Begge er beskrevet i ISO 32000-1, og en validator, der ikke kender til dem, ser en sund fil som en bunke manglende objekter.

Hvad PDF 1.5 ændrede ved filens bagende

ISO 32000-1 §7.5.8 definerer krydsreferencestrømmen, og §7.5.7 definerer objektstrømmen af typen /ObjStm. Tilsammen lader de en writer udelade de to strukturer, som en klassisk parser søger efter. En PDF 1.5-fil kan ende helt uden xref-tabel. I stedet er det objekt, som startxref peger på, et almindeligt strømobjekt (stream object), hvis ordbog bærer /Type /XRef, og den strøm indeholder krydsreferencedataene i en kompakt binær form. Der er heller intet trailer-nøgleord, fordi traileren nu er strømmens egen ordbog. De nøgler, som en klassisk parser ledte efter, nemlig /Root, /Size og /ID, bor inde i den ordbog.

Den anden ændring flytter selve objekterne. I stedet for at skrive hvert indirekte objekt ved sin egen byte-forskydning, kan en writer pakke mange små objekter – sideordbøgerne, annoteringsordbøgerne, strukturens træ – ind i en enkelt objektstrøm og komprimere hele beholderen med Flate. De enkelte objekter har ikke længere en byte-forskydning i filen. De har en position inde i en komprimeret blob. En validator, der scanner de rå bytes efter 1 0 obj, finder dem aldrig, fordi den tekst først eksisterer efter inflation (dekomprimering). For en klassisk parser er halvdelen af dokumentet simpelthen forsvundet.

Trailer-nøglerne er klartekst, selv i en komprimeret fil

Det beroligende er, at læsning af traileren i en krydsreferencestrøm ikke kræver dekomprimering af noget. Et strømobjekt skrives som en ordbog efterfulgt af nøgleordet stream og derefter de komprimerede bytes. Ordbogen er i klartekst. Så når startxref peger på en krydsreferencestrøm, de bytes umiddelbart efter objektnummeret ser ud som en almindelig ordbog, og /Root, /Size og /ID sidder der i det åbne, før nøgleordet stream og Flate-dataene begynder.

Det betyder, at en validator kan lære de tre fakta, den har mest brug for – hvor kataloget er, hvor mange objekter filen hævder at have, og filidentifikatoren – ved kun at parse strømmens ordbog. Den behøver ikke at dekomprimere krydsreferencedataene, og den behøver ikke at fortolke de binære poster i dem. Det arbejde, der overmander en naiv parser, er ikke at læse traileren; det er at finde objekterne. Det er to adskillelige problemer, og det er billigt at løse det første.

Objektstrømme: et hoved, derefter en Flate-blob

En objektstrøm er en beholder. Dens ordbog bærer /Type /ObjStm, en /N-post, der angiver antallet af pakkede objekter indeni, og en /First-post, der angiver byte-forskydningen inde i de dekomprimerede data, hvor det første objekts krop starter. Den komprimerede payload, når den er dekomprimeret, begynder med et lille hoved af /N heltalspar. Hvert par er et objektnummer og forskydningen af det objekts krop i forhold til /First. Efter hovedet kommer selve objektkroppene i forlængelse af hinanden.

At udvide en af dem er mekanisk, når først bytes er dekomprimeret. Du læser ordbogen for at få /N og /First, dekomprimerer strømmen med en Flate-dekoder, gennemgår de førende /N-par for at lære, hvilket objektnummer der bor på hvilken forskydning, og løfter derefter hver krop ud, som om det var et almindeligt indirekte objekt. Den eneste reelle afhængighed er Flate-dekoderen, og du har allerede én: Delphi leverer System.ZLib, og Free Pascal leverer zstream-enheden, som begge pakker zlib ind og dekomprimerer en rå Flate-strøm uden nogen tredjepartskode. En rutine, der tilføjer hvert udtrukket objekt til validatorens objekttabel, får resten af validatoren – den del, der gennemgår /Root og kontrollerer sidetræet – til at opføre sig nøjagtig, som den ville på en klassisk fil.

Hvad du ikke behøver at implementere

Det er nemt at overvurdere arbejdet. Aflæsning af trailer-nøglerne fra en komprimeret fil kræver ikke afkodning af krydsreferencestrømmens binære poster. §7.5.8-krydsreferencestrømmen bruger tre posttyper, og type 2-posten, den der siger dette objekt bor inde i objektstrøm N ved indeks i, er den, du ville afkode for at bygge et fuldt forskydningskort. Du har brug for det kort til at løse vilkårlige objekter efter nummer. Du har ikke brug for det til at læse /Root, /Size og /ID, som findes i klartekst-ordbogen, og du har ikke brug for det til at udvide objektstrømme, fordi hver /ObjStm annoncerer sit eget indhold via /N og /First.

Du behøver heller ikke at håndtere de PNG- og TIFF-prædiktorfunktioner, som en krydsreferencestrøm kan anvende via sine /DecodeParms blot for at få trailer-nøglerne. Prædiktorer filtrerer de binære krydsreferencerækker for at få dem til at komprimere bedre; de har intet at gøre med den ordbog, der går forud for strømmen. Den minimale opgradering, der gør en klassisk validator opmærksom på moderne PDF, er derfor lille: Når startxref lander på en strøm frem for xref-nøgleordet, parses strømordbogen for trailer-nøglerne, og eventuelle /ObjStm-objekter, du støder på, udvides, så deres indhold kommer ind i objekttabellen. Afkodning af type 2-poster og prædiktorer er en separat, større opgave, som du kan udskyde, indtil du reelt har brug for vilkårlig objektopløsning.

Hvorfor en overholdelseskontrol skal udvide strømme først

Dette ophører med at være akademisk i det øjeblik, du kører en profilkontrol. En PDF/A- eller PDF/X-validator inspicerer specifikke objekter: dokumentkataloget for et /OutputIntents-array, /Metadata-strømmen for en XMP-pakke med den rigtige identifikator, enhver skrifttypebeskrivelse (font descriptor) for en indlejret skrifttypefil, traileren for et /ID. I en komprimeret fil er de fleste af disse objekter inde i objektstrømme. En validator, der ikke har udvidet objektstrømmene, kan ikke se katalogets nøgler, kan ikke finde metadataene og kan ikke oliste skrifttyperne. Den vil rapportere et fuldstændig konformt dokument som manglende dets output-hensigt (output intent), manglende dets XMP og manglende halvdelen af sin struktur, fordi de beviser, den har brug for, stadig sidder i en Flate-blob, den aldrig dekomprimerede.

Rækkefølgen betyder noget. Udvidelser skal ske, før kontrollerne kører, ikke sideløbende med dem, fordi enhver kontrol antager, at den kan nå et objekt efter nummer. Hvis du forbinder en profilkontrol direkte til en rå byte-scanning, den arver den klassiske parsers blindhed og producerer falske overtrædelser på netop de moderne filer, der mest sandsynligt er velformede, da de kom ud af værktøjskæder, der var nye nok til overhovedet at skrive krydsreferencestrømme.

Lad PDFium klare parsingen for dig

PDFium-komponenten parser krydsreferencestrømme og objektstrømme som en del af indlæsningen af et dokument, hvilket er den praktiske måde at undgå at håndrulle dekomprimerings- og udvidelsestrinnet på. Når du indlæser en fil med TPdf-komponenten, de objekter, der er pakket i /ObjStm-beholdere, allerede løst, og valideringsindgangspunkterne ser det fuldt udvidede dokument. ValidatePdfA returnerer en TPdfAValidationResult-post, hvis Conformance-felt er en TPdfAConformance-værdi som f.eks. pac1b eller pacNone, hvis Issues-felt er et sæt af de specifikke fundne problemer, og hvis IsCompliant-metode kun er sand, når et overholdelsesniveau blev deteketret, og sættet af problemer er tomt. Fordi objekterne blev udvidet under indlæsningen, et /OutputIntents-array eller en indlejret skrifttype, der boede inde i en objektstrøm, findes, og rapporteres ikke som manglende.

uses
  PDFium, FPdfPdfa;

function CheckPdfA(const FileName: string): TPdfAValidationResult;
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;            // parses xref/object streams on load
    Result := Pdf.ValidatePdfA;    // sees the expanded object table
  finally
    Pdf.Free;
  end;
end;

Det samme gælder for ValidatePdfX, som returnerer en TPdfXValidationResult med samme form. Pointen med at dirigere det gennem PDFium er, at den strukturelle dekomprimering beskrevet ovenfor sker én gang, korrekt, inde i indlæseren (loaderen), så din valideringskode aldrig ser forskellen på en klassisk fil og en fuldt komprimeret. Begge ankommer til validatoren som et løst sæt af objekter.

var
  Pdf: TPdf;
  R  : TPdfXValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'Press_Ready.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePdfX;
    if R.IsCompliant then
      Writeln('PDF/X conformance: ', Ord(R.Conformance))
    else
      Writeln('Not conformant; issue count = ', SizeOf(R.Issues));
  finally
    Pdf.Free;
  end;
end;

Hvis bytes allerede er i hukommelsen frem for på disken, fungerer den samme indlæs-og-valider-sekvens via LoadDocument(const Data: TBytes)-overloaden, som tager det rå filindhold og parser dets krydsreference- og objektstrømme på samme måde som filstien. Læren for en håndskrevet validator er den strukturelle regel, ikke API'en: læs trailer-nøglerne fra strømordbogen i klartekst, udvid enhver /ObjStm med en Flate-dekoder, før du gennemgår dokumentet, og behandl afkodning af de binære krydsreferenceposter som den større, valgfrie opgave, det er.

Når strukturen er udvidet, kan en validator drive resten af en arbejdsgang hen over den. For en kommandolinje-preflight-opsætning, der rapporterer overholdelse på tværs af en mappe med input, se vores gennemgang af opbygning af en batch preflight-rapport CLI. Når validering er en barriere forud for opdeling af et stort dokument, de teknikker i vores vejledning til at opdele PDF-dokumenter i flere filer parres naturligt med det indlæs-og-kontroller-mønster, der er vist her. Begge bygger på indlæsnings- og valideringsoverfladen for PDFium Component til Delphi og C++Builder.