Du skriver en lille validator. Den åbner en PDF, søger til slutningen, finder startxref, læser offsettet og forventer at lande på nøgleordet xref med en krydsreferencetabel (cross-reference table) med fast bredde (fixed-width) under det. Fra den tabel indsamler den objekt-offsets, hvorefter den scanner baglæns for at finde nøgleordet trailer for at lære /Root og /Size at kende. Det fungerer perfekt på hver fil, du har genereret for at teste det. Så ankommer en fil produceret af en aktuel version af Word eller af et bibliotek, der retter sig mod PDF 1.5, og validatoren erklærer den ødelagt. Der er intet xref-nøgleord, hvor offsettet peger hen, ingen trailer-ordbog (dictionary) nogen steder, og objekttabellen (the object table), som validatoren byggede, er næsten tom. Filen er gyldig (valid). Validatoren læser den gennem en femten år gammel linse
Dette er den absolut mest almindelige grund til, at et PDF-tjek på byteniveau (byte-level PDF check) skrevet op mod det klassiske layout fejler på moderne dokumenter. Strukturen, den afhænger af, krydsreferencetabellen i klartekst (the plaintext cross-reference table) og trailer-nøgleordet, blev gjort valgfri i PDF 1.5 og er ofte fraværende. To funktioner erstattede den: krydsreference-streamen (the cross-reference stream) og den komprimerede objekt-stream (the 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 hale (the file tail)
ISO 32000-1 §7.5.8 definerer krydsreference-streamen, og §7.5.7 definerer objekt-streamen af typen /ObjStm. Sammen lader de en skriver (writer) droppe de to strukturer, en klassisk parser bygger på (keys on). En PDF 1.5-fil ender muligvis slet ikke med nogen xref-tabel. I dens sted er det objekt, som startxref peger på, et almindeligt stream-objekt, hvis ordbog bærer /Type /XRef, og den stream indeholder krydsreferencedataene (the cross-reference data) i en kompakt binær form. Der er heller intet trailer-nøgleord, fordi traileren nu er streamens egen ordbog. De nøgler (keys), en klassisk parser jagtede, /Root, /Size og /ID, lever inde i den ordbog
Den anden ændring flytter selve objekterne. I stedet for at skrive hvert indirekte objekt (indirect object) ved dets eget byte-offset, kan en skriver pakke mange små objekter, sideordbøgerne (the page dictionaries), annotationsordbøgerne (the annotation dictionaries), strukturtræet (the structure tree), ind i en enkelt objekt-stream og komprimere hele beholderen med Flate. De enkelte objekter har ikke længere et byte-offset i filen. De har en position inde i en komprimeret blob. En validator, der scanner de rå bytes for 1 0 obj, finder dem aldrig, fordi den tekst kun eksisterer efter inflation (dekomprimering). For en klassisk parser er halvdelen af dokumentet simpelthen forsvundet
Trailer-nøglerne (The trailer keys) er klartekst, selv i en komprimeret fil
Den betryggende del er, at læsning af traileren på en krydsreference-stream ikke kræver, at man inflater (dekomprimerer) noget. Et stream-objekt skrives som en ordbog efterfulgt af stream-nøgleordet og derefter de komprimerede bytes. Ordbogen er klartekst. Så når startxref peger på en krydsreference-stream, ser bytene umiddelbart efter objektnummeret ud som en almindelig ordbog, og /Root, /Size og /ID sidder der i klartekst, før stream-nøgleordet og Flate-dataene begynder
Det betyder, at en validator kan lære de tre fakta, den mest har brug for, hvor kataloget (the catalog) er, hvor mange objekter filen hævder (claims), og filidentifikatoren (the file identifier), ved kun at parse stream-ordbogen. Den behøver ikke at dekomprimere krydsreferencedataene, og den behøver ikke at fortolke (interpret) de binære indtastninger (binary entries) i den. Det arbejde, der besejrer en naiv parser, er ikke at læse traileren; det er at finde objekterne. Det er to adskillelige problemer, og at løse det første er billigt
Objekt-streams: en header, derefter en Flate-blob
En objekt-stream er en beholder (container). Dens ordbog bærer /Type /ObjStm, en /N-indtastning, der angiver antallet af objekter pakket indeni, og en /First-indtastning, der angiver byte-offsettet, inden for de inflaterede data, hvor det første objekts krop (body) starter. Det komprimerede payload begynder, når det er inflateret, med en lille header af /N heltalspar (integer pairs). Hvert par er et objektnummer og offsettet for det objekts krop i forhold til /First. Efter headeren kommer selve objektkroppene, sammenkædet (concatenated)
At udvide (Expanding) en er mekanisk, når bytene er inflateret. Du læser ordbogen for at få /N og /First, inflater streamen med en Flate-dekoder (Flate decoder), gennemgår de forreste /N par for at lære, hvilket objektnummer der bor på hvilket offset, 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 en: Delphi leveres med System.ZLib, og Free Pascal leveres med zstream-enheden, som begge indpakker zlib og inflater en rå Flate-stream uden nogen tredjepartskode. En rutine, der tilføjer (appends) hvert udtrukket objekt til validatorens objekttabel, får resten af validatoren, den del der gennemgår (walks) /Root og tjekker sidetræet (the page tree), til at opføre sig præcis, som den ville på en klassisk fil
Hvad du ikke behøver at implementere
Det er let at overvurdere (overestimate) arbejdet. At læse trailer-nøglerne fra en komprimeret fil kræver ikke, at man dekoder krydsreference-streamens binære indtastninger. Krydsreference-streamen i §7.5.8 bruger tre indtastningstyper (entry types), og type 2-indtastningen, den der siger dette objekt lever inde i objekt-stream N ved indeks i
, er det, du ville dekode for at bygge et fuldt offset-kort (offset map). Du har brug for det kort for at opløse (resolve) vilkårlige objekter efter nummer. Du har ikke brug for det til at læse /Root, /Size og /ID, som er i klartekst-ordbogen, og du har ikke brug for det til at udvide objekt-streams, fordi hver /ObjStm annoncerer sit eget indhold gennem /N og /First
Du behøver heller ikke at håndtere de PNG- og TIFF-prædiktorfunktioner (predictor functions), som en krydsreference-stream kan anvende gennem sin /DecodeParms, bare for at få trailer-nøglerne. Prædiktorer filtrerer de binære krydsreference-rækker for at få dem til at komprimere bedre; de har intet at gøre med ordbogen, der går forud for streamen. Den minimale opgradering, der gør en klassisk validator moderne-PDF-bevidst (modern-PDF aware), er derfor lille: når startxref lander på en stream snarere end xref-nøgleordet, så pars stream-ordbogen for trailer-nøgler, og udvid alle /ObjStm objekter, du støder på, så deres indhold kommer ind i objekttabellen. Dekodning af type 2-indtastninger og prædiktorer er en separat, større opgave, du kan udskyde (defer), indtil du reelt har brug for tilfældig objektopløsning (random object resolution)
Hvorfor et overholdelsestjek (compliance check) skal udvide streams først
Dette holder op med at være akademisk det øjeblik, du kører et profiltjek (profile check). En PDF/A- eller PDF/X-validator inspicerer specifikke objekter: dokumentkataloget (the document catalog) for et /OutputIntents-array, /Metadata-streamen for en XMP-pakke med den rette identifikator, hver skrifttypebeskrivelse (font descriptor) for en indlejret skrifttypefil (embedded font file), traileren for en /ID. I en komprimeret fil er de fleste af de objekter inde i objekt-streams. En validator, der ikke har udvidet objekt-streamene, kan ikke se katalogets nøgler, kan ikke finde metadataene og kan ikke opregne (enumerate) skrifttyperne. Den vil rapportere et perfekt overensstemmende (conformant) dokument som manglende dets output-hensigt (output intent), manglende dets XMP, og manglende halvdelen af dets struktur, fordi det bevis, den har brug for, stadig sidder i en Flate-blob, som den aldrig inflaterede
Rækkefølgen betyder noget. Udvidelse skal ske, før tjekkene kører, ikke sideløbende med dem, fordi hvert tjek antager, at det kan nå et objekt efter nummer. Hvis du forbinder (wire) et profiltjek direkte til en rå bytescanning, arver (inherits) det den klassiske parsers blindhed og producerer falske overtrædelser (false violations) på præcis de moderne filer, der er mest tilbøjelige til at være velformede (well formed), da de kom ud af toolchains, der var nye nok til at skrive krydsreference-streams i første omgang
At lade PDFium gøre parsingen for dig
PDFium-komponenten parser krydsreference-streams og objekt-streams som en del af indlæsningen af et dokument, hvilket er den praktiske måde at undgå at håndkodes (hand-rolling) inflate-og-udvid (inflate-and-expand) trinnet. Når du indlæser en fil med TPdf-komponenten, er objekterne, der er pakket ind i /ObjStm-beholdere, allerede opløst, og validerings-adgangspunkterne (the validation entry points) ser det fuldt udvidede dokument. ValidatePdfA returnerer en TPdfAValidationResult-record, hvis Conformance-felt er en TPdfAConformance-værdi såsom pac1b eller pacNone, hvis Issues-felt er et sæt (a set) af de specifikke problemer fundet, og hvis IsCompliant-metode kun er sand, når et overholdelsesniveau (a conformance level) blev detekteret, og problem-sættet (the issue set) er tomt. Fordi objekterne blev udvidet under indlæsning, findes et /OutputIntents-array eller en indlejret skrifttype, der levede inde i en objekt-stream, 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 den samme form. Pointen med at rute (routing) gennem PDFium er, at den strukturelle dekomprimering (the structural decompression) beskrevet ovenfor sker én gang, korrekt, inde i indlæseren (the loader), så din valideringskode aldrig ser forskellen mellem en klassisk fil og en fuldt komprimeret en. Begge ankommer til validatoren som et opløst sæt af objekter
function PdfXConformanceName(C: TPdfXConformance): string;
begin
case C of
pxc1a: Result := 'PDF/X-1a';
pxc3 : Result := 'PDF/X-3';
pxc4 : Result := 'PDF/X-4';
else
Result := 'none';
end;
end;
var
Pdf: TPdf;
R : TPdfXValidationResult;
Issue: TPdfXValidationIssue;
IssueCount: Integer;
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: ', PdfXConformanceName(R.Conformance))
else
begin
IssueCount := 0;
for Issue in R.Issues do // Issues is a set: count its members
Inc(IssueCount);
Writeln('Not conformant; issue count = ', IssueCount);
end;
finally
Pdf.Free;
end;
end;
Hvis bytene allerede er i hukommelsen frem for på disk, virker den samme indlæs-så-valider (load-then-validate) sekvens gennem LoadDocument(const Data: TBytes)-overloaden (the overload), som tager det rå filindhold og parser dets krydsreference- og objekt-streams på samme måde, som filstien gør. Hovedpointen (The takeaway) for en håndskreven validator er den strukturelle regel, ikke API'et: læs trailer-nøglerne fra stream-ordbogen i klartekst, udvid hver /ObjStm med en Flate-dekoder, før du gennemgår dokumentet, og behandl dekodning af de binære krydsreference-indtastninger som det større, valgfrie job, det er
Når strukturen er udvidet, kan en validator drive (drive) resten af et workflow over den. For en kommandolinje-preflight-opsætning (command-line preflight harness), der rapporterer overholdelse på tværs af en mappe med input, se vores gennemgang af opbygningen af en batch preflight report CLI. Når validering er en port (gate) forud for at bryde et stort dokument fra hinanden, parres teknikkerne i vores guide til at opdele PDF-dokumenter i flere filer naturligt med det indlæs-og-tjek-mønster (load-and-check pattern), der er vist her. Begge bygger på indlæsnings- og valideringsoverfladen i PDFium-komponenten til Delphi og C++Builder