Du skriver en liten validator. Den åpner en PDF, søker til slutten, finner startxref, leser forskyvningen (offset), og forventer å lande på nøkkelordet xref med en kryssreferansetabell med fast bredde (fixed-width) under det. Fra den tabellen samler den objektforskyvninger (object offsets), skanner deretter bakover for trailer-nøkkelordet for å lære /Root og /Size. Det fungerer perfekt på hver fil du genererte for å teste den. Så ankommer en fil produsert av en gjeldende versjon av Word, eller av et bibliotek som retter seg mot PDF 1.5, og validatoren erklærer den som ødelagt. Det er intet xref-nøkkelord der forskyvningen peker, intet trailer-ordbokelement noe sted, og objekttabellen validatoren bygget er nesten tom. Filen er gyldig. Valideringen leser den gjennom en femten år gammel linse
Dette er den desidert vanligste grunnen til at en byte-nivå PDF-sjekk skrevet mot det klassiske oppsettet (classic layout) feiler på moderne dokumenter. Strukturen den avhenger av, klartekst-kryssreferansetabellen og trailer-nøkkelordet, ble gjort valgfri i PDF 1.5 og er ofte fraværende. To funksjoner erstattet det: kryssreferansestrømmen (cross-reference stream) og den komprimerte objektstrømmen (object stream). Begge er beskrevet i ISO 32000-1, og en validator som ikke vet om dem ser en sunn fil som en haug med manglende objekter
Hva PDF 1.5 endret angående filens hale (tail)
ISO 32000-1 §7.5.8 definerer kryssreferansestrømmen, og §7.5.7 definerer objektstrømmen av type /ObjStm. Sammen lar de en forfatter (writer) droppe de to strukturene en klassisk analysator (parser) baserer seg på. En PDF 1.5-fil kan ende med ingen xref-tabell overhodet. I dens sted er objektet som startxref peker til en vanlig strøm (stream object) hvis ordbok (dictionary) bærer /Type /XRef, og den strømmen inneholder kryssreferansedataene i en kompakt binær form. Det finnes heller intet trailer-nøkkelord, fordi traileren nå er strømmens egen ordbok. Nøklene en klassisk analysator jaktet på, /Root, /Size og /ID, lever inni den ordboken
Den andre endringen flytter selve objektene. I stedet for å skrive hvert indirekte objekt ved sin egen byte-forskyvning (byte offset), kan en forfatter pakke mange små objekter, sideordbøkene, annotasjonsordbøkene, strukturtreet, inn i en enkelt objektstrøm og komprimere hele beholderen med Flate. De individuelle objektene har ikke lenger en byte-forskyvning i filen. De har en posisjon inni en komprimert blob. En validator som skanner de rå bytene etter 1 0 obj finner dem aldri, fordi den teksten bare eksisterer etter oppblåsing (inflation). For en klassisk analysator har halve dokumentet rett og slett forsvunnet
Trailernøklene er klartekst, selv i en komprimert fil
Den beroligende delen er at å lese traileren til en kryssreferansestrøm krever ikke oppblåsing (inflating) av noe som helst. Et strømobjekt skrives som en ordbok etterfulgt av stream-nøkkelordet og deretter de komprimerte bytene. Ordboken er klartekst. Så når startxref peker på en kryssreferansestrøm, ser bytene umiddelbart etter objektnummeret ut som en vanlig ordbok, og /Root, /Size og /ID sitter der i klartekst, før stream-nøkkelordet og Flate-dataene begynner
Det betyr at en validator kan lære de tre faktaene den mest trenger, hvor katalogen (catalog) er, hvor mange objekter filen hevder, og filidentifikatoren, ved å bare analysere strømordboken (stream dictionary). Den trenger ikke å dekomprimere kryssreferansedataene, og den trenger ikke å tolke de binære oppføringene inni dem. Arbeidet som overvinner en naiv analysator er ikke å lese traileren; det er å finne objektene. Dette er to atskilte problemer, og å løse det første er billig
Objektstrømmer: en header, deretter en Flate-blob
En objektstrøm er en beholder (container). Ordboken dens bærer /Type /ObjStm, en /N-oppføring som gir antall objekter pakket inni, og en /First-oppføring som gir byte-forskyvningen, innenfor de oppblåste dataene (inflated data), der det første objektets kropp (body) starter. Den komprimerte nyttelasten, når den er blåst opp, begynner med en liten header av /N-heltallspar. Hvert par er et objektnummer og forskyvningen av det objektets kropp relativt til /First. Etter headeren kommer selve objektkroppene, sammensluttet (concatenated)
Å utvide (expanding) én er mekanisk når bytene først er blåst opp. Du leser ordboken for å få /N og /First, blåser opp strømmen med en Flate-dekoder, går gjennom de ledende /N-parene for å lære hvilket objektnummer som lever ved hvilken forskyvning, og deretter løfter du ut hver kropp som om det var et vanlig indirekte objekt. Den eneste virkelige avhengigheten er Flate-dekoderen, og du har allerede en: Delphi leverer System.ZLib, og Free Pascal leverer zstream-enheten, som begge pakker inn (wrap) zlib og blåser opp en rå Flate-strøm uten noe tredjepartskode. En rutine som legger hvert ekstraherte objekt til validatorens objekttabell får resten av validatoren, den delen som går gjennom /Root og sjekker sidetreet, til å oppføre seg akkurat som den ville gjort på en klassisk fil
Hva du ikke trenger å implementere
Det er lett å overvurdere arbeidet. Å lese trailer-nøklene fra en komprimert fil krever ikke dekoding av kryssreferansestrømmens binære oppføringer. §7.5.8 kryssreferansestrømmen bruker tre oppføringstyper (entry types), og type 2-oppføringen, den som sier dette objektet lever inni objektstrøm N ved indeks i
, er hva du ville dekodet for å bygge et fullt forskyvningskart (offset map). Du trenger det kartet for å løse opp (resolve) vilkårlige objekter etter nummer. Du trenger det ikke for å lese /Root, /Size og /ID, som er i klartekst-ordboken, og du trenger det ikke for å utvide objektstrømmer, fordi hver /ObjStm kunngjør sitt eget innhold gjennom /N og /First
Du trenger heller ikke å håndtere PNG- og TIFF-prediktor-funksjonene (predictor functions) som en kryssreferansestrøm kan anvende gjennom sin /DecodeParms bare for å få trailer-nøklene. Prediktorer filtrerer de binære kryssreferanseradene for å få dem til å komprimeres bedre; de har ingenting å gjøre med ordboken som kommer før strømmen. Den minimale oppgraderingen som gjør en klassisk validator moderne-PDF-bevisst er derfor liten: når startxref lander på en strøm fremfor xref-nøkkelordet, analyser strømordboken for trailer-nøklene, og utvid (expand) enhver /ObjStm-objekter du møter på slik at innholdet deres går inn i objekttabellen. Dekoding av type 2-oppføringer og prediktorer er en separat, større oppgave du kan utsette til du genuint trenger tilfeldig objektoppløsning (random object resolution)
Hvorfor en samsvarssjekk (compliance check) må utvide strømmer først
Dette slutter å være akademisk i det øyeblikket du kjører en profilsjekk. En PDF/A- eller PDF/X-validator inspiserer spesifikke objekter: dokumentkatalogen (document catalog) for en /OutputIntents-array, /Metadata-strømmen for en XMP-pakke med riktig identifikator, hver skrifttypebeskrivelse (font descriptor) for en innebygd skrifttypefil (embedded font file), traileren for en /ID. I en komprimert fil er de fleste av disse objektene inni objektstrømmer. En validator som ikke har utvidet objektstrømmene kan ikke se katalogens nøkler, kan ikke finne metadataene, og kan ikke telle opp (enumerate) skrifttypene. Den vil rapportere et perfekt samsvarende (conformant) dokument som manglende dets output intent, manglende dets XMP, og manglende halve strukturen, fordi beviset den trenger fortsatt sitter i en Flate-blob den aldri blåste opp
Rekkefølgen (order) har betydning. Utvidelse må skje før sjekkene kjøres, ikke samtidig med dem, fordi hver sjekk forutsetter at den kan nå et objekt via nummer. Hvis du kobler en profilsjekk direkte til en rå byteskanning, arver den den klassiske analysatorens blindhet og produserer falske brudd på nøyaktig de moderne filene som mest sannsynlig er velformede, siden de kom fra verktøykjeder (toolchains) nye nok til å skrive kryssreferansestrømmer i utgangspunktet
La PDFium gjøre analyseringen for deg
PDFium-komponenten analyserer kryssreferansestrømmer og objektstrømmer som en del av lastingen av et dokument, som er den praktiske måten å unngå å håndkode (hand-rolling) oppblås-og-utvid-trinnet (inflate-and-expand step). Når du laster en fil med TPdf-komponenten, er objektene pakket inn i /ObjStm-beholdere allerede oppløst (resolved), og valideringens inngangspunkter ser det fullt utvidede dokumentet. ValidatePdfA returnerer en TPdfAValidationResult-post (record) hvis Conformance-felt er en TPdfAConformance-verdi slik som pac1b eller pacNone, hvis Issues-felt er et sett med de spesifikke problemene som ble funnet, og hvis IsCompliant-metode er sann bare når et samsvarsnivå (conformance level) ble oppdaget og problemsettet er tomt. Fordi objektene ble utvidet under lasting, blir en /OutputIntents-array eller en innebygd skrifttype (embedded font) som levde inni en objektstrøm funnet, ikke rapportert 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 gjelder for ValidatePdfX, som returnerer en TPdfXValidationResult med samme form. Poenget med ruting (routing) gjennom PDFium er at den strukturelle dekomprimeringen beskrevet ovenfor skjer én gang, riktig, inni lasteren (loader), slik at valideringskoden din aldri ser forskjellen mellom en klassisk fil og en fullt komprimert en. Begge ankommer validatoren som et oppløst (resolved) sett av 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 minnet fremfor på disk, fungerer den samme last-så-valider-sekvensen gjennom LoadDocument(const Data: TBytes)-overbelastningen (overload), som tar det rå filinnholdet og analyserer (parses) kryssreferanse- og objektstrømmene på samme måte som filstien (file path) gjør. Lærepengen for en håndskrevet validator er den strukturelle regelen, ikke API-et: les trailer-nøklene fra strømordboken i klartekst, utvid hver /ObjStm med en Flate-dekoder før du går gjennom dokumentet, og behandle dekoding av de binære kryssreferanseoppføringene (binary cross-reference entries) som den større, valgfrie jobben det er
Så snart strukturen er utvidet, kan en validator drive resten av en arbeidsflyt (workflow) over den. For et kommandolinjebasert preflight-rammeverk (command-line preflight harness) som rapporterer samsvar over en mappe med inndata, se vår gjennomgang av bygging av et batch-preflight-rapport-CLI. Når validering er en port foran oppdeling av et stort dokument, passer teknikkene i vår guide til å dele opp PDF-dokumenter i flere filer naturlig med last-og-sjekk-mønsteret vist her. Begge bygger på laste- og valideringsflaten (validation surface) til PDFium-komponenten for Delphi og C++Builder