Du skriver en liten validator. Den öppnar en PDF, söker sig till slutet, hittar startxref, läser förskjutningen och förväntar sig att landa på nyckelordet xref med en korsreferenstabell av fast bredd inunder den. 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 genererat för att testa det. Sedan anländer en fil producerad av en aktuell version av Word, eller av ett bibliotek inriktat mot PDF 1.5, och validatorn förklarar den trasig. Det finns inget xref-nyckelord där förskjutningen pekar, ingen trailer-ordlista någonstans, och objekttabellen validatorn byggde är nästan tom. Filen är giltig. Validatorn läser den genom en femton år gammal lins
Detta är den enskilt vanligaste anledningen till att en PDF-kontroll på bytenivå skriven mot den klassiska layouten misslyckas på moderna dokument. Strukturen den är beroende av, den i klartext skrivna korsreferenstabellen och nyckelordet trailer, gjordes valfria i PDF 1.5 och saknas ofta. Två funktioner ersatte den: korsreferensströmmen och den komprimerade objektströmmen. Båda beskrivs i ISO 32000-1, och en validator som inte känner till dem ser en frisk fil som en hög med saknade objekt
Vad PDF 1.5 ändrade gällande filens svans
ISO 32000-1 §7.5.8 definierar korsreferensströmmen, och §7.5.7 definierar objektströmmen av typ /ObjStm. Tillsammans låter de en författare utelämna de två strukturer en klassisk tolkare letar efter. En PDF 1.5-fil kanske slutar utan någon xref-tabell alls. I dess ställe är objektet som startxref pekar på ett vanligt strömobjekt vars ordlista bär /Type /XRef, och den strömmen håller korsreferensdata i en kompakt binär form. Det finns inget trailer-nyckelord heller, eftersom trailern nu är strömmens egen ordlista. Nycklarna en klassisk tolkare jagade efter, /Root, /Size och /ID, lever inuti den ordlistan
Den andra ändringen flyttar själva objekten. Istället för att skriva varje indirekt objekt på sin egen byte-förskjutning, kan en författare packa många små objekt, sidordlistorna, kommentarordlistorna, strukturträdet, till en enda objektström och komprimera hela behållaren med Flate. De enskilda objekten har inte längre en byte-förskjutning i filen. De har en position inuti en komprimerad blob. En validator som skannar råa byte efter 1 0 obj hittar dem aldrig, eftersom den texten endast existerar efter dekomprimering. För en klassisk tolkare har halva dokumentet helt enkelt försvunnit
Trailer-nycklarna är i klartext, även i en komprimerad fil
Det lugnande är att läsningen av trailern till en korsreferensström inte kräver dekomprimering av någonting. Ett strömobjekt skrivs som en ordlista följt av nyckelordet stream och sedan de komprimerade byten. Ordlistan är klartext. Så när startxref pekar på en korsreferensström, ser byten direkt efter objektnumret ut som en vanlig ordlista, och /Root, /Size och /ID sitter där i klartext, innan nyckelordet stream och Flate-datan börjar
Det betyder att en validator kan lära sig de tre fakta den mest behöver, var katalogen finns, hur många objekt filen gör anspråk på, och filidentifieraren, genom att bara tolka strömordlistan. Den behöver inte dekomprimera korsreferensdatan, och den behöver inte tolka de binära posterna inuti den. Arbetet som besegrar en naiv tolkare är inte att läsa trailern; det är att hitta objekten. Det är två separerbara problem, och att lösa det första är billigt
Objektströmmar: ett huvud, sedan en Flate-blob
En objektström är en behållare. Dess ordlista bär /Type /ObjStm, en /N-post som anger antalet objekt packade inuti, och en /First-post som anger byte-förskjutningen, inuti den dekomprimerade datan, där det första objektets kropp börjar. Den komprimerade lasten, väl dekomprimerad, börjar med ett litet huvud med /N heltalspar. Varje par är ett objektnummer och förskjutningen för det objektets kropp i förhållande till /First. Efter huvudet kommer själva objektkropparna, sammanfogade
Att expandera en är mekaniskt när byten väl är dekomprimerade. Du läser ordlistan för att hämta /N och /First, dekomprimerar strömmen med en Flate-avkodare, vandrar igenom de inledande /N-paren för att lära dig vilket objektnummer som bor vid vilken förskjutning, och lyfter sedan ut varje kropp som om det vore ett vanligt indirekt objekt. Det enda verkliga beroendet är Flate-avkodaren, och du har redan en: Delphi levereras med System.ZLib, och Free Pascal levereras med enheten zstream, som båda kapslar in zlib och dekomprimerar en rå Flate-ström utan någon tredjepartskod. En rutin som lägger till varje extraherat objekt i validatorns objekttabell får resten av validatorn, den del som vandrar /Root och kontrollerar sidträdet, att bete sig precis 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 trailer-nycklarna från en komprimerad fil kräver inte avkodning av korsreferensströmmens binära poster. §7.5.8 korsreferensströmmen använder tre posttyper, och typ 2-posten, den som säger detta objekt bor inuti objektström N på index i
, är vad du skulle avkoda för att bygga en fullständig förskjutningskarta. Du behöver den kartan för att lösa upp godtyckliga objekt via nummer. Du behöver den inte för att läsa /Root, /Size och /ID, vilka finns i klartextsordlistan, 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 de PNG- och TIFF-prediktorfunktioner som en korsreferensström kan applicera genom sina /DecodeParms bara för att få tag på trailer-nycklarna. Prediktorer filtrerar de binära korsreferensraderna för att få dem att komprimeras bättre; de har ingenting att göra med ordlistan som föregår strömmen. Den minimala uppgradering som gör en klassisk validator medveten om modern PDF är därför liten: när startxref landar på en ström snarare än nyckelordet xref, tolka strömordlistan efter trailer-nycklarna, och expandera eventuella /ObjStm-objekt du stöter på så att deras innehåll går in i objekttabellen. Att avkoda typ 2-poster och prediktorer är en separat, större uppgift som du kan skjuta upp tills du genuint behöver slumpmässig objektupplösning
Varför en efterlevnadskontroll måste expandera strömmar först
Detta slutar vara akademiskt i det ögonblick du kör en profilkontroll. En PDF/A- eller PDF/X-validator inspekterar specifika objekt: dokumentkatalogen för en /OutputIntents-array, /Metadata-strömmen för ett XMP-paket med rätt identifierare, varje typsnittsbeskrivare för en inbäddad typsnittsfil, trailern för ett /ID. I en komprimerad fil ligger de flesta av dessa objekt inuti objektströmmar. En validator som inte har expanderat objektströmmarna kan inte se katalogens nycklar, kan inte hitta metadatan, och kan inte räkna upp typsnitten. Den kommer att rapportera ett perfekt överensstämmande dokument som att det saknar sin output intent, saknar sin XMP, och saknar hälften av sin struktur, eftersom bevisen den behöver fortfarande sitter i en Flate-blob den aldrig dekomprimerade
Ordningen spelar roll. Expansion måste ske innan kontrollerna körs, inte vid sidan av dem, eftersom varje kontroll förutsätter att den kan nå ett objekt via nummer. Om du kopplar en profilkontroll direkt på en rå byteskanning, ärver den den klassiska tolkarens blindhet och producerar falska överträdelser på exakt de moderna filer som är mest sannolika att vara välformade, eftersom de kom ut ur verktygskedjor som var tillräckligt nya för att skriva korsreferensströmmar till att börja med
Låta PDFium göra tolkningen åt dig
PDFium Component tolkar korsreferensströmmar och objektströmmar som en del av att ladda ett dokument, vilket är det praktiska sättet att undvika att handskriva dekomprimerings- och expansionssteget. När du laddar en fil med TPdf-komponenten, är objekten som packats in i /ObjStm-behållare redan upplösta, och valideringens ingångspunkter ser det fullt expanderade dokumentet. ValidatePdfA returnerar en TPdfAValidationResult-post vars Conformance-fält är ett TPdfAConformance-värde såsom pac1b eller pacNone, vars Issues-fält är en mängd av de specifika problem som hittades, och vars IsCompliant-metod endast är sann när en efterlevnadsnivå detekterades och problemmängden är tom. Eftersom objekten expanderades under inläsningen, hittas en /OutputIntents-array eller ett inbäddat typsnitt som levde inuti en objektström, de rapporteras inte som saknade
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 ett TPdfXValidationResult med samma form. Poängen med att dirigera via PDFium är att den strukturella dekomprimeringen som beskrivs ovan sker en gång, korrekt, inuti inläsaren, så din valideringskod aldrig ser skillnaden mellan en klassisk fil och en fullt komprimerad sådan. Båda anländer till validatorn som en upplöst uppsättning objekt
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;
Om byten redan finns i minnet istället för på disk, fungerar samma ladda-sedan-validera-sekvens genom LoadDocument(const Data: TBytes)-överlagringen, 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 validator är den strukturella regeln, inte API:et: läs trailer-nycklarna från strömordlistan i klartext, expandera varje /ObjStm med en Flate-avkodare innan du vandrar igenom dokumentet, och behandla avkodningen av de binära korsreferensposterna som det större, valfria jobb det är
När strukturen väl är expanderad kan en validator driva resten av ett arbetsflöde över den. För en kommandoradssele för preflight som rapporterar efterlevnad över en mapp med indata, se vår genomgång om att bygga ett CLI för batch-preflight-rapporter. När validering är en grind inför att bryta isär ett stort dokument, paras teknikerna i vår guide till att dela PDF-dokument i flera filer naturligt med det ladda-och-kontrollera-mönster som visas här. Båda bygger på laddnings- och valideringsytan i PDFium Component för Delphi och C++Builder