Du skriver en liten validerare. Den öppnar en PDF, söker till slutet, hittar startxref, läser offset och förväntar sig att landa på nyckelordet xref med en korsreferenstabell med fast bredd under sig. Från den tabellen samlar den in objektförskjutningar, och skannar sedan bakåt efter nyckelordet trailer för att lära sig /Root och /Size. Det fungerar perfekt på varje fil du genererade för att testa den. Sedan anländer en fil som skapats av en aktuell version av Word, eller av ett bibliotek som är inriktat på PDF 1.5, och valideraren förklarar den trasig. Det finns inget xref-nyckelord där offseten pekar, ingen trailer-ordbok någonstans, och objekttabellen som valideraren byggde är nästan tom. Filen är giltig. Valideraren läser den genom en femton år gammal lins.
Detta är den enskilt vanligaste orsaken till att en kontroll på byte-nivå skriven mot den klassiska layouten misslyckas på moderna dokument. Strukturen den beror på, den vanliga korsreferenstabellen i klartext och nyckelordet trailer, gjordes valfri i PDF 1.5 och saknas ofta. Två funktioner ersatte den: korsreferensströmmen (cross-reference stream) och den komprimerade objektströmmen (compressed object stream). Båda beskrivs i ISO 32000-1, och en validerare som inte känner till dem ser en frisk fil som en hög med saknade objekt.
Vad PDF 1.5 ändrade i filens slut
ISO 32000-1 §7.5.8 definierar korsreferensströmmen, och §7.5.7 definierar objektströmmen av typen /ObjStm. Tillsammans låter de en skrivare släppa de två strukturer som en klassisk parser söker efter. En PDF 1.5-fil kan sluta utan någon xref-tabell alls. I dess ställe är objektet som startxref pekar på ett vanligt strömobjekt vars ordbok bär på /Type /XRef, och den strömmen innehåller korsreferensdatan i en kompakt binär form. Det finns inte heller något trailer-nyckelord, eftersom trailern nu är strömmens egen ordbok. Nycklarna en klassisk parser jagade efter, /Root, /Size och /ID, bor inuti den ordboken.
Den andra ändringen flyttar själva objekten. Istället för att skriva varje indirekt objekt vid sin egen byteförskjutning (offset) kan en skrivare packa många små objekt – sidsidorna, annoteringsordböckerna, strukturens träd – i en enskild objektström och komprimera hela behållaren med Flate. De enskilda objekten har inte längre en byteförskjutning i filen. De har en position inuti en komprimerad binär data. En validerare som skannar de råa byten efter 1 0 obj hittar dem aldrig, eftersom den texten endast existerar efter uppblåsning (inflation). För en klassisk parser har halva dokumentet helt enkelt försvunnit.
Trailernycklarna är klartext, även i en komprimerad fil
Den lugnande delen är att läsa trailern för en korsreferensström inte kräver att man blåser upp någonting. Ett strömobjekt skrivs som en ordbok följd av nyckelordet stream och sedan de komprimerade byten. Ordboken är i klartext. Så när startxref pekar på en korsreferensström ser byten omedelbart efter objektnumret ut som en vanlig ordbok, och /Root, /Size och /ID sitter där helt synliga innan nyckelordet stream och Flate-datan börjar.
Det betyder att en validerare kan lära sig de tre fakta den mest behöver – var katalogen är, hur många objekt filen hävdar och filidentifieraren – genom att endast tolka strömmens ordbok. Den behöver inte dekomprimera korsreferensdatan, och den behöver inte tolka de binära posterna inuti den. Arbetet som besegrar en naiv parser är inte att läsa trailern; det är att hitta objekten. Detta är två separerbara problem, och att lösa det första är billigt.
Objektströmmar: ett huvud, sedan en Flate-data
En objektström är en behållare. Dess ordbok bär /Type /ObjStm, en /N-post som anger antalet objekt som är packade inuti, och en /First-post som anger byteförskjutningen, inom den uppblåsta datan, där det första objektets kropp startar. Den komprimerade nyttolasten börjar, när den väl har blåsts upp, med ett litet huvud av /N heltalspar. Varje par är ett objektnummer och det objektets förskjutning i förhållande till /First. Efter huvudet kommer själva objektkropparna, sammanfogade.
Att expandera en sådan är mekaniskt när byten väl är uppblåsta. Du läser ordboken för att få /N och /First, blåser upp strömmen med en Flate-avkodare, går igenom de ledande /N-paren för att lära dig vilket objektnummer som bor vid vilken förskjutning, och lyfter sedan ut varje kropp som om den vore ett vanligt indirekt objekt. Det enda verkliga beroendet är Flate-avkodaren, och du har redan en: Delphi levererar System.ZLib, och Free Pascal levererar enheten zstream, vilka båda omsluter zlib och blåser upp en rå Flate-ström utan någon kod från tredje part. En rutin som lägger till varje extraherat objekt till validerarens objekttabell gör att resten av valideraren, den del som går igenom /Root och kontrollerar sidträdet, beter sig exakt som den skulle göra på en klassisk fil.
Vad du inte behöver implementera
Det är lätt att överskatta arbetet. Att läsa trailernycklarna från en komprimerad fil kräver inte att man avkodar korsreferensströmmens binära poster. Korsreferensströmmen i §7.5.8 använder tre posttyper, och typ 2-posten, den som säger detta objekt bor inuti objektström N vid index i
, är den du skulle avkoda för att bygga en komplett förskjutningskarta. Du behöver den kartan för att lösa godtyckliga objekt efter nummer. Du behöver den inte för att läsa /Root, /Size och /ID, vilka finns i klartextordboken, och du behöver den inte för att expandera objektströmmar, eftersom varje /ObjStm tillkännager sitt eget innehåll genom /N och /First.
Du behöver inte heller hantera prediktorfunktionerna för PNG och TIFF som en korsreferensström kan tillämpa genom sina /DecodeParms bara för att få trailernycklarna. Prediktorer filtrerar de binära korsreferensraderna för att få dem att komprimeras bättre; de har ingenting att göra med ordboken som föregår strömmen. Den minimala uppgradering som gör en klassisk validerare medveten om modern PDF är därför liten: när startxref landar på en ström snarare än nyckelordet xref, tolka strömmens ordbok efter trailernycklarna, och expandera alla /ObjStm-objekt du stöter på så dass deras innehåll kommer in i objekttabellen. Att avkoda typ 2-poster och prediktorer är en separat, större uppgift som du kan skjuta upp tills du verkligen behöver slumpmässig objektsupplösning.
Varför en kontroll av efterlevnad måste expandera strömmar först
Detta slutar vara akademiskt i samma ögonblick som du kör en profilkontroll. En PDF/A- eller PDF/X-validerare inspekterar specifika objekt: dokumentkatalogen för en /OutputIntents-array, /Metadata-strömmen för ett XMP-paket med rätt identifierare, varje typsnittsbeskrivning (font descriptor) för en inbäddad typsnittsfil, trailern för ett /ID. I en komprimerad fil finns de flesta av dessa objekt inuti objektströmmar. En validerare som inte har expanderat objektströmmarna kan inte se katalogens nycklar, kan inte hitta metadata och kan inte räkna upp typsnitten. Den kommer att rapportera ett helt kompatibelt dokument som att det saknar sitt output intent, saknar sin XMP och saknar halva sin struktur, eftersom bevisen den behöver fortfarande ligger i en Flate-data den aldrig blåste upp.
Ordningen är viktig. Expansion måste ske innan kontrollerna körs, inte vid sidan av dem, eftersom varje kontroll antar att den kan nå ett objekt efter nummer. Om du kopplar en efterlevnadskontroll direkt till en rå byteskanning ärver den den klassiska parserns blindhet och producerar falska överträdelser på exakt de moderna filer som är mest sannolika att vara välformade, eftersom de kom från verktygskedjor som var nya nog för att överhuvudtaget skriva korsreferensströmmar.
Låta PDFium sköta tolkningen åt dig
PDFium-komponenten tolkar korsreferensströmmar och objektströmmar som en del av att läsa in ett dokument, vilket är det praktiska sättet att undvika att bygga egna steg för att blåsa upp och expandera. När du läser in en fil med TPdf-komponenten är objekten som är packade i /ObjStm-behållare redan lösta, och valideringsstartpunkterna ser det fullt expanderade dokumentet. ValidatePdfA returnerar en TPdfAValidationResult-post vars Conformance-fält är ett TPdfAConformance-värde som pac1b eller pacNone, vars Issues-fält är en mängd av de specifika problem som hittats, och vars IsCompliant-metod är sann endast när en efterlevnadsnivå upptäcktes och mängden problem är tom. Eftersom objekten expanderades vid inläsningen hittas en /OutputIntents-array eller ett inbäddat typsnitt som bodde inuti en objektström, istället för att rapporteras som saknat.
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;
Detsamma gäller för ValidatePdfX, som returnerar en TPdfXValidationResult med samma form. Poängen med att dirigera detta genom PDFium är att den strukturella dekomprimeringen som beskrivs ovan sker en gång, korrekt, inuti inläsaren, så att din valideringskod aldrig ser skillnaden mellan en klassisk fil och en helt komprimerad. Båda anländer till valideraren som en löst mängd objekt.
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;
Om byten redan finns i minnet snarare än på disken fungerar samma sekvens för inläsning-och-validering genom överlagringen LoadDocument(const Data: TBytes), som tar det råa filinnehållet och tolkar dess korsreferens- och objektströmmar på samma sätt som filsökvägen gör. Lärdomen för en handskriven validerare är den strukturella regeln, inte API:et: läs trailernycklarna från strömmens ordbok i klartext, expandera varje /ObjStm-objekt med en Flate-avkodare innan du går igenom dokumentet, och behandla avkodning av binära korsreferensposter som det större, valfria jobb det är.
När väl strukturen har expanderats kan en validerare driva resten av arbetsflödet över den. För en kommandoradsbaserad efterlevnadskontroll som rapporterar efterlevnad över en mapp med indata, se vår genomgång av hur man bygger en batch-preflight-rapport CLI. När validering är en grind innan man bryter isär ett stort dokument, paras teknikerna i vår guide om att dela upp PDF-dokument i flera filer naturligt ihop med inläsnings- och kontrollmönstret som visas här. Båda bygger på inläsnings- och valideringsytan för PDFium Component för Delphi och C++Builder.