Öppna en PDF som skapats av Microsoft Word eller Excel, bläddra igenom den och ingenting ser ovanligt ut. Ladda in den i ett Delphi-program, läs av sidantalet och siffran stämmer. Spara sedan om den med kryptering aktiverad och jobbet misslyckas med ett EListError, eller så öppnas utdata med en varning om skadad korsreferens. Filen var aldrig trasig. Det är en hybrid-referensfil, och själva strukturen som gör att ett femton år gammalt visningsprogram kan öppna den är samma struktur som besegrar en laddare som slutar läsa för tidigt
Detta är ett av de vanligaste sätten som en PDF-pipeline som klarat alla interna tester stöter på en fil den inte kan hantera hela vägen runt. Indata genererades alla internt, så de var aldrig hybrider. Den första hybridfilen anländer den dag en kund vidarebefordrar en faktura som exporterats från ett kalkylblad
Vad Word och Excel faktiskt skriver
ISO 32000-1 beskriver hybrid-referenslayouten i §7.5.8.4. Ett program som vill ha PDF 1.5-funktioner som objektströmmar, men ändå tillåta en PDF 1.4-läsare att öppna filen, skriver korsreferensinformationen två gånger. Det finns en klassisk korsreferenstabell, de ASCII-rader med fast bredd som avslutade varje PDF fram till version 1.4, och det finns en korsreferensström som indexerar resten. Trailern i den klassiska sektionen bär på en /XRefStm-post vars värde är byte-offseten för den strömmen
Arbetsfördelningen är avsiktlig. Objekt som en äldre läsare måste nå, bland dem katalogen och sidträdet, är adresserbara från den klassiska tabellen. Objekt som har vikts in i komprimerade objektströmmar markeras som lediga i den klassiska tabellen med en post av typen f, så att en 1.4-läsare hoppar rakt förbi dem och aldrig snubblar över en struktur den inte kan tolka. Deras verkliga platser finns endast i korsreferensströmmen. Signaturen för en sådan fil är dess slut: en kort klassisk sektion, ofta inget annat än xref följt av en 0 0-undersektionsrubrik, vars trailer pekar på den /XRefStm där de faktiska återställningsdata finns
Varför ett korrekt sidantal inte bevisar någonting
Eftersom katalogen och sidträdet medvetet är nåbara från den klassiska tabellen hittar en laddare som bara läser den tabellen /Root, går igenom sidträdet och rapporterar rätt antal sidor. Allt som en äldre läsare behöver finns där, så filen verkar frisk. Objekten som försvann är de som packats in i objektströmmar: AcroForm-fältdicionärer, taggade PDF-strukturelement och den långa svansen av små dicionärer som aldrig behövde vara synliga för ett äldre visningsprogram
Du märker inte glappet förrän något rör vid dessa objekt, och en jackpot-omsparande rör vid dem alla. Att gå igenom dokumentet för att återkryptera eller skriva om det är precis den operation som efterfrågar varje objektnummer i tur och ordning, vilket är anledningen till att symptomet visar sig vid spartillfället snarare än vid laddningstillfället, långt från dess orsak
Fällan är en detektor som ser xref och stoppar
Det billiga sättet att avgöra hur en fil indexeras är att följa startxref och inspektera de första bytes den pekar på. Nyckelordet xref innebär en klassisk tabell; ett strömobjekt innebär en korsreferensström. Det testet är korrekt för alla filer som håller sig till ett system. Det är fel för en hybridfil, vars startxref pekar på en klassisk sektion med det enda syftet att tillfredsställa äldre läsare, medan /XRefStm i den sektionens trailer är där större delen av dokumentet faktiskt indexeras. En detektor som returnerar "classic" vid den första xref den möter läser aldrig /XRefStm, och varje objekt som bara finns i strömmen blir osynligt
var
Pdf: THotPDF;
PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf'); // count is correct
// inspect or edit the loaded document here
Pdf.SaveLoadedDocument('Invoice_secured.pdf'); // walks every object
finally
Pdf.Free;
end;
end;
Med detektorn för tidig avslutning på plats ser inläsningen bra ut och omsparingen är platsen där de saknade objekten ger sig till känna. Lösningen är inte att läsa fler bytes i början; det handlar om att känna igen hybrid-trailern och följa /XRefStm innan man beslutar att filen är klar
Sammanslagningsordningen är inte förhandlingsbar
När båda indexen har lästs kan de endast kombineras i en riktning. Korsreferensströmmen måste slås samman först, med de klassiska posterna ifyllda runt den. Orsaken är det lilla bedrägeriet i hjärtat av formatet. En hybridfil markerar sina komprimerade objekt som lediga i den klassiska tabellen så att äldre läsare ignorerar dem. En laddare som följer en princip om att först sedda vinner och läser den klassiska tabellen först kommer att registrera dessa objektnummer som lediga och sedan förkasta strömströmmarna som faktiskt lokaliserar dem, eftersom platserna redan är upptagna. Omvänd ordningen och typ 2-posterna från strömmen, var och en bestående av ett objektströmsnummer plus ett index, vinner platserna de är tänkta att äga, och de klassiska posterna lägger sig runt dem
Samma disciplin skyddar mot att en äldre revision återuppväcker ett borttaget objekt. Inkrementella uppdateringar länkar bakåt genom /Prev, och en ledig post av typ 0 är en vaktpost som anger att en nyare sektion har pensionerat ett objektnummer. En senare, äldre sektion i kedjan får inte tillåtas att skriva över den vaktposten med en föråldrad plats. Behandla först sedda som auktoritativt för lediga markeringar och det borttagna objektet förblir borttaget; behandla det vårdslöst och filens egen historik återupplivar innehåll som den senaste revisionen tog bort
Vad detta innebär i HotPDF
Motorn löser hybrid-referensfiler åt dig, och den gör det på varje sökväg som måste tolka korsreferensdata. Ladda ett dokument med LoadFromFile eller LoadFromStream, gör dina ändringar och anropa SaveLoadedDocument; eller kör en engångsåtgärd som EncryptFile som läser indata och skriver utdata. Oavsett vilket läser återställningen /XRefStm, slår samman strömsektionen före de klassiska posterna och löser objekten som finns i strömmar innan skrivningen räknar upp dem. AES-256-krypteringssökvägen är där problemet först visade sig, eftersom kryptering av ett dokument skriver om varje objekt och därmed kräver att varje objekt redan har lokaliserats
// One-shot: read the hybrid input, write an AES-256 encrypted copy
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
'owner-secret', '', aes256, [prPrint, prFillAnnotations]);
Detaljen som är värd att komma ihåg finns uppströms om API:et. Filer som kommer från Word, Excel, PowerPoint och en lång lista av "Spara som PDF"-pipelines är rutinmässigt hybrider, så en laddare som du bara testar mot utdata från din egen generator kanske aldrig möter en i tester. Förse dina testmiljöer med dokument som exporterats från riktiga Office-program, inte bara med filer som din egen kod har skapat
Att kontrollera en fil du misstänker
Två inspektioner avgör frågan snabbt. Öppna filen i en hex-vy och läs bytes efter den sista startxref; en hybridfil visar en kort klassisk sektion vars trailer-dicionär innehåller /XRefStm. Eller jämför det objektantal som en fullständig tolkning rapporterar mot det högsta objektnummer som /Size deklarerar i trailern. Ett stort glapp innebär att objekt gömmer sig i strömmar som laddaren inte har öppnat, vilket är samma underskott som förvandlas till ett misslyckande vid sparande senare
Skrivarens sida av denna historia, hur objektströmmar och komprimerade korsreferenser skapas från första början, beskrivs i vår artikel om objektströmmar och inkrementella uppdateringar. När hybridfilen i fråga också är mycket stor, tillåter laddningsteknikerna i genomgången av Direct File API för stora PDF-arbetsflöden dig att inspektera den utan att läsa in hela filen i minnet. Båda hör naturligt ihop med den återställning som beskrivs här, vilken levereras som en del av HotPDF Component för Delphi och C++Builder tillsammans med de API:er för laddning, redigering, kryptering och signering som beskrivs på andra ställen i denna blogg