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 under. Fra den tabellen samler den objektsforskyvninger, og skanner deretter bakover etter nøkkelordet trailer for å lære /Root og /Size. Det fungerer perfekt på hver fil du genererte for å teste den. Deretter 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 ikke noe xref-nøkkelord der forskyvningen peker, ingen trailer-ordbok noen steder, og objekttabellen validatoren bygde er nesten tom. Filen er gyldig. Validatoren leser den gjennom et femten år gammelt linse.
Dette er den desidert vanligste årsaken til at en PDF-sjekk på byte-nivå skrevet mot det klassiske oppsettet feiler på moderne dokumenter. Strukturen den er avhengig av, den rene tekst-kryssreferansetabellen og nøkkelordet trailer, ble gjort valgfri i PDF 1.5 og er ofte fraværende. To funksjoner erstattet den: kryssreferansestrømmen (cross-reference stream) og den komprimerte objektstrømmen (compressed 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 ved filens hale
ISO 32000-1 §7.5.8 definerer kryssreferansestrømmen, og §7.5.7 definerer objektstrømmen av typen /ObjStm. Sammen lar de en skriver droppe de to strukturene en klassisk parser baserer seg på. En PDF 1.5-fil kan ende helt uten xref-tabell. I stedet er objektet startxref peker på, et vanlig strømobjekt (stream object) hvis ordbok bærer /Type /XRef, og den strømmen inneholder kryssreferansedataene i en kompakt binær form. Det er heller ikke noe trailer-nøkkelord, fordi traileren nå er strømmens egen ordbok. Nøklene en klassisk parser 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 byteforskyvning, kan en skriver pakke mange små objekter, sideordbøkene, annoteringsordbøkene, strukturtreet, inn i en enkelt objektstrøm og komprimere hele beholderen med Flate. De enkelte objektene har ikke lenger en byteforskyvning i filen. De har en posisjon inne i en komprimert blob. En validator som skanner de rå bytene etter 1 0 obj finner dem aldri, fordi den teksten bare eksisterer etter inflasjon (oppblåsing). For en klassisk parser har halve dokumentet rett og slett forsvunnet.
Trailer-nøklene er ren tekst, selv i en komprimert fil
Den beroligende delen er at å lese traileren til en kryssreferansestrøm ikke krever å dekomprimere (inflate) noe. Et strømobjekt skrives som en ordbok etterfulgt av nøkkelordet stream og deretter de komprimerte bytene. Ordboken er ren tekst. Så når startxref peker på en kryssreferansestrøm, ser bytene rett etter objektnummeret ut som en vanlig ordbok, og /Root, /Size og /ID sitter der klart og tydelig, før nøkkelordet stream og Flate-dataene begynner.
Det betyr at en validator kan lære de tre faktaene den mest trenger – hvor katalogen er, hvor mange objekter filen hevder å ha, og filidentifikatoren – ved å bare tolke strømordboken. Den trenger ikke å dekomprimere kryssreferansedataene, og den trenger ikke å tolke de binære oppføringene inni den. Arbeidet som overvinner en naiv parser er ikke å lese traileren; det er å finne objektene. Dette er to adskilte problemer, og å løse det første er enkelt.
Objektstrømmer: en header, deretter en Flate-blob
En objektstrøm is en beholder. Dens ordbok bærer /Type /ObjStm, en /N-oppføring som gir antall objekter pakket inni, og en /First-oppføring som gir byteforskyvningen, innenfor de oppblåste dataene, der det første objektets kropp starter. Den komprimerte nyttelasten begynner, etter dekomprimering, med en liten header med /N heltallspar. Hvert par er et objektnummer og forskyvningen til det objektets kropp i forhold til /First. Etter headeren kommer selve objektkroppene, koblet sammen.
Å utvide en slik er mekanisk når bytene er dekomprimert. Du leser ordboken for å få /N og /First, dekomprimerer strømmen med en Flate-dekoder, går gjennom de ledende /N-parene for å lære hvilket objektnummer som lever ved hvilken forskyvning, og løfter deretter hver kropp ut som om den 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, der begge pakker inn zlib og dekomprimerer en rå Flate-strøm uten noen tredjepartskode. En rutine som legger til hvert utpakkede objekt i 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 de binære oppføringene i kryssreferansestrømmen. Kryssreferansestrømmen i §7.5.8 bruker tre oppføringstyper, og type 2-oppføringen – den som sier dette objektet lever inni objektstrøm N ved indeks i
– er det du ville dekodet for å bygge et komplett forskyvningskart. Du trenger det kartet for å løse vilkårlige objekter etter nummer. Du trenger det ikke for å lese /Root, /Size og /ID, som er i den rene tekstordboken, og du trenger det ikke for å utvide objektstrømmer, fordi hver /ObjStm kunngjør sitt eget innhold via /N og /First.
Du trenger heller ikke å håndtere PNG- og TIFF-prediktorfunksjonene (predictor functions) som en kryssreferansestrøm kan bruke via sin /DecodeParms bare for å få trailer-nøklene. Prediktorer filtrerer de binære kryssreferanseradene for å få dem til å komprimere bedre; de har ingenting å gjøre med ordboken som kommer før strømmen. Den minimale oppgraderingen som gjør en klassisk validator oppmerksom på moderne PDF er derfor liten: når startxref lander på en strøm i stedet for nøkkelordet xref, tolk strømordboken for trailer-nøklene, og utvid eventuelle /ObjStm-objekter du støter på, slik at innholdet deres kommer inn i objekttabellen. Dekoding av type 2-oppføringer og prediktorer er en separat, større oppgave du kan utsette til du virkelig trenger tilfeldig objektløsning.
Hvorfor en samsvarskontroll må utvide strømmer først
Dette slutter å være akademisk i det øyeblikket du kjører en profilkontroll. En PDF/A- eller PDF/X-validator inspiserer spesifikke objekter: dokumentkatalogen for et /OutputIntents-array, /Metadata-strømmen for en XMP-pakke med riktig identifikator, hver fontbeskrivelse for en innebygd fontfil, 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, finner ikke metadataene og kan ikke telle fontene. Den vil rapportere et helt samsvarende dokument som om det mangler utdataintensjon, mangler XMP og mangler halve strukturen sin, fordi bevisene den trenger fortsatt sitter i en Flate-blob den aldri dekomprimerte.
Rekkefølgen betyr noe. Utvidelsen må skje før kontrollene kjører, ikke ved siden av dem, fordi hver kontroll antar at den kan nå et objekt etter nummer. Hvis du kobler en profilkontroll direkte til en rå byteskanning, arver den den klassiske parserens blindhet og produserer falske brudd på akkurat de moderne filene som mest sannsynlig er godt utformet, siden de kom ut av verktøykjeder som var nye nok til å skrive kryssreferansestrømmen i utgangspunktet.
Ved å la PDFium gjøre tolkingen for deg, ser valideringsinngangspunktene det fullt utvidede dokumentet. ValidatePdfA returnerer en TPdfAValidationResult-post der Conformance-feltet er en TPdfAConformance-verdi som pac1b eller pacNone, der Issues-feltet is et sett med de spesifikke problemene som ble funnet, og der IsCompliant-metoden er sann bare når et samsvarsnivå ble oppdaget og settet med problemer er tomt. Fordi objektene ble utvidet under innlasting, blir et /OutputIntents-array eller en innebygd font som levde inne i en objektstrøm funnet, og ikke rapportert 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 gjelder for ValidatePdfX, som returnerer en TPdfXValidationResult med samme form. Poenget med å rute gjennom PDFium er at den strukturelle dekomprimeringen beskrevet ovenfor skjer én gang, på en korrekt måte, inni innlasteren (loaderen), slik at valideringskoden din aldri ser forskjellen på en klassisk fil og en fullt komprimert en. Begge ankommer validatoren som et løst sett med 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 bytene allerede er i minnet i stedet for på disken, fungerer den samme innlastings-og-valideringssekvensen via LoadDocument(const Data: TBytes)-overloaden, som tar det rå filinnholdet og tolker dets kryssreferanse- og objektstrømmer på samme måte som filbanen gjør. Leksjonen for en håndskrevet validator er den strukturelle regelen, ikke API-et: les trailer-nøklene fra strømordboken i ren tekst, utvid hver /ObjStm med en Flate-dekoder før du går gjennom dokumentet, og behandle dekoding av de binære kryssreferanseoppføringene som den større, valgfrie jobben det er.
Når strukturen er utvidet, en validator kan drive resten av en arbeidsflyt over den. For en kommandolinje-preflight-ramme som rapporterer samsvar over en mappe med inndata, se vår gjennomgang om å bygge en kommandolinje-rapport for batch-preflight. Når validering er en port før et stort dokument deles opp, pares teknikkene i vår guide for deling av PDF-dokumenter i flere filer naturlig med innlastings-og-sjekk-mønsteret som vises her. Begge bygger på innlastings- og valideringsflaten til PDFium Component for Delphi og C++Builder.