Otevřete PDF, které vyprodukoval Microsoft Word nebo Excel, prolistujte si ho a neuvidíte nic neobvyklého. Načtěte ho do programu v Delphi, přečtěte počet stránek a číslo bude správné. Potom jej znovu uložte se zapnutým šifrováním a úloha selže s chybou EListError, nebo se výstup otevře s varováním o poškozených křížových odkazech (cross-reference). Soubor přitom nikdy nebyl poškozený. Jde o hybridně referenční soubor (hybrid-reference) a stejná struktura, která umožňuje patnáct let starému prohlížeči jeho otevření, je přesně ta struktura, která porazí zavaděč (loader), který přestane číst příliš brzy
Toto je jeden z nejčastějších způsobů, jak se PDF pipeline, jež prošla všemi interními testy, setká se souborem, který nedokáže znovu zpracovat a uložit (round-trip). Všechny vstupní soubory byly do té doby generovány interně (in-house), takže nikdy nebyly hybridní. První hybridní soubor dorazí v den, kdy zákazník přepošle fakturu exportovanou z tabulkového procesoru
Co Word a Excel vlastně zapisují
Norma ISO 32000-1 popisuje hybridně-referenční uspořádání v §7.5.8.4. Aplikace, která vyžaduje funkce PDF 1.5, jako jsou objektové proudy (object streams), a zároveň chce umožnit čtečce PDF 1.4 otevřít soubor, zapisuje křížové odkazy dvakrát. Existuje klasická tabulka křížových odkazů – řádky ASCII o pevné šířce, které ukončovaly každé PDF až do verze 1.4 – a křížový odkazovací proud (cross-reference stream), který indexuje zbytek. Informace na konci dokumentu (trailer) u klasické sekce nese položku /XRefStm, jejíž hodnotou je bajtový ofset daného proudu
Toto rozdělení práce je záměrné. Objekty, ke kterým se musí starší čtečka dostat (jako například katalog nebo strom stránek), jsou adresovatelné z klasické tabulky. Objekty, které byly sbaleny do komprimovaných objektových proudů, jsou v klasické tabulce označeny jako volné položkou s typem f, takže je čtečka 1.4 prostě přeskočí a nikdy neklopýtne o strukturu, kterou nedokáže rozparsovat. Jejich skutečná umístění žijí pouze v křížovém odkazovacím proudu. Poznávacím znamením takového souboru je jeho samotný konec (tail): krátká klasická sekce, často jen slovo xref následované hlavičkou podsekce 0 0, jejíž trailer odkazuje na /XRefStm, kde sedí skutečná data pro obnovu
Proč správný počet stránek nic nedokazuje
Protože katalog a strom stránek jsou schválně dostupné z klasické tabulky, zavaděč, který čte pouze tuto tabulku, najde /Root, projde strom stránek a ohlásí správný počet stránek. Vše, co stará čtečka potřebuje, je na místě, takže se soubor zdá být v pořádku. Chybějící objekty jsou ty, které byly zabaleny do objektových proudů: slovníky polí AcroForm, strukturní prvky pro tagged-PDF, nekonečná spousta malých slovníků, které legacy prohlížeč nikdy nemusel vidět
Tuto mezeru nezaznamenáte, dokud na tyto objekty něco nesáhne a kompletní opětovné uložení (resave) sáhne na všechny. Procházení dokumentu za účelem opětovného zašifrování nebo přepsání je přesně ta operace, která si vyžádá postupně každé číslo objektu, a to je ten důvod, proč se symptom objeví až při uložení a nikoli při načítání, tedy daleko od své příčiny
Past spočívá v detektoru, který uvidí xref a skončí
Laciným způsobem, jak rozhodnout, jak je soubor indexován, je sledovat startxref a zkontrolovat první bajty, na které odkazuje. Klíčové slovo xref znamená klasickou tabulku; stream objekt znamená křížový odkazovací proud. Tento test je správný pro každý soubor, který se zavazuje k jednomu ze schémat. U hybridního souboru je ale chybný, protože jeho startxref míří na klasickou sekci z jediného důvodu: uspokojit starší čtečky, přičemž ale /XRefStm v traileru této sekce je místem, kde je fakticky indexována většina dokumentu. Detektor, který na prvním zjištěném slově xref vrátí odpověď "klasický", už /XRefStm nikdy nečte a všechny objekty, které žijí pouze v proudu, se tak stávají neviditelnými
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;
Když je ve hře detektor, který svou práci brzy vzdá (early-exit detector), načítání vypadá v pořádku a opětovné uložení (resave) je místem, kde se chybějící objekty začnou hlásit o slovo. Řešením však není číst na začátku více bajtů; řešením je rozpoznat hybridní trailer a sledovat cestu za /XRefStm předtím, než se rozhodnete, že zpracování souboru je hotové
Pořadí sloučení nelze obejít (Merge order is not negotiable)
Jakmile jsou načteny oba indexy, mohou být sloučeny (merged) pouze jedním směrem. Křížový odkazovací proud musí být sloučen jako první a klasické položky se kolem něj musí vyplnit. Důvodem je drobný klam v samotném srdci tohoto formátu. Hybridní soubor označí v klasické tabulce své komprimované objekty jako volné, aby je staré čtečky ignorovaly. Zavaděč (loader), který ctí pravidlo „první vyhrává“ (first-seen-wins policy) a přečte klasickou tabulku jako první, zaznamená tato čísla objektů jako volná, a potom vyhodí položky z proudu, které je ve skutečnosti lokalizují, protože daná místa (slots) už jsou zkrátka obsazená. Otočte pořadí a z proudu získané záznamy typu 2 (což je vždy číslo objektového proudu plus příslušný index) si vybojují místa, která jim měla patřit, a teprve kolem nich se poskládají klasické záznamy
Stejná disciplína je ochranou před staršími revizemi křísícími už dříve smazané objekty. Inkrementální updaty se totiž řetězí pozpátku skrze parametr /Prev a prázdný záznam typu 0 je onou zmíněnou ochranou (sentinel), že některá novější sekce odeslala číslo objektu do penze. Nelze dopustit, aby pozdější starší sekce v řetězci tento hlídací prvek přepsala zastaralým (stale) umístěním. Když k principu "první vyhrává" přistoupíte autoritativně u všech zjištěných značek pro uvolněné bloky, tak smazané prostě zůstanou smazané; pokud k němu ale přistoupíte lehkovážně, tak i pouhá vlastní historie souboru dokáže oživit obsah smazaný v nejnovější revizi
Co to znamená u HotPDF
Tento engine za vás řeší hybridně-referenční soubory a dělá to na každé cestě, která musí zpracovávat data křížových odkazů. Načtěte dokument pomocí LoadFromFile nebo LoadFromStream, proveďte příslušné změny a zavolejte SaveLoadedDocument; nebo jednoduše spusťte jednorázovou operaci jako EncryptFile, která sama přečte vstup a rovnou zapíše výstup. V obou případech obnova automaticky přečte atribut /XRefStm, zkombinuje proudovou sekci (stream section) před klasickými údaji, ještě před spuštěním zápisu vyřeší i objekty vložené v proudech a teprve s tím vším je postupně očísluje. Problematika se úplně prvně objevila v trase provádění šifrování na 256bitových standardech AES, protože proces při šifrování přepisuje každý z dostupných elementů z dokumentu a logicky tímto striktně nařizuje (demands) i dřívější nalezení umístění každého z těchto daných objektů
// 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]);
Drobný detail, který zde ale stojí za odnesení si sebou (carrying away), se skrývá trochu výše proti proudu aplikačního rozhraní (upstream of the API). Soubory pocházející z Wordu, z Excelu, z aplikace PowerPoint i z obrovské spousty zpracovatelských tras na styl "Uložit jako PDF" se dělají standardně do hybridních forem, proto loader odzkoušený i v generování z výstupů z testů, nemusel po celou dobu na tento stav narazit. Zaneste si tak z důvodů u ověřování na test do své testovací základny též opravdické nativně naexportované záznamy z programů sady Office spíše než si tam dávat po ruce pouze soubory na kterých do té doby běžel a sám je předtím nakódil u sebe (own code produced)
Zkoumání podezřelého souboru
Dvě nezávislé prohlídky dokáží celou spornou záležitost poměrně bleskově utnout. Otevřete si testovaný soubor pod zobrazením pro soustavu hextabulky a rovnou si prohlédněte záznam kousku po úplně posledním vyskytnutí prvku přes startxref; zobrazený úsek souboru z formy hybridního PDF se odhalí kusem úseku zakončení do traileru s daným slovem /XRefStm pro sekci v dictionary. Dalším krokem nabízí způsob u prozkoumávání načítání při zapsaném rozpočtu spočítaných prvků objektů s rozsahem z číselného objektového množství vůči ohlášce (highest object number) hodnoty ze slovníku pod trailer parametrem /Size z tohoto porovnání za prověřením vůči proklamované informaci z prohlášení. Mohutná objevená propast znamená hromadu číhání schovávajících se neodhalených součástí formátu u vnitřních proudů (hiding in streams), jež pro zavaděč nezbývá silou zobrazení načíst, a tím vzniká přesně takovýto chybějící počet (shortfall), z něhož na závěr pramení selhání do zápisu s ukládáním
Tento ocas s koncovkou ve zcela standardním typickém exportním listu do PDF na procesoru z Excelu vám to při ukázce první pomoci hned zpřístupní k obeznámení se se zjištěnou (check concrete) realitou. Na koncovce na ohlášení z celého obsahu u úplně posledního parametru slova za heslem pro xref ze slova bude k mání neupravovaný prostý čistý záznam z celého kódování pomocí ASCII, tím pádem na podepsanou signaturu můžete s jistotou snadno nahlédnout čtením u vyhozeného hextabulkového výčtu z příkazu k posouzení z do očí bijící formy z datových bodů z přidaných přikreslených ukazatelů s vymyšleným posunem (offsets illustrative, annotations added)
xref
0 0 % empty classic subsection: no rows at all
trailer
<< /Size 216 % one past the highest object number in use
/Root 1 0 R
/Info 15 0 R
/ID [<5C9A...> <5C9A...>]
/XRefStm 87325 % byte offset of the cross-reference stream
>>
startxref
88710 % points at the classic section above
%%EOF
Podsekce 0 0 je tím nejlepším ukazatelem (tell): prázdná klasická tabulka obsahující přesně nula položek tam totiž trůní z toho důvodu, aby ponesla onen konec k zápisu, z koncovky pod (trailer). Záznam konce dokumentu zase navíc visí u spuštění proto k upotřebení (mainly to say) ohlášky na promluvu přes /XRefStm 87325. Detekční uzel na zastavení chodu po zahlédnutí klíčového slova pod xref se za chodu k místu po načtení podívá pod tuto masku bez obsahu do vymyšleného indexu prázdna (index of nothing). Rozhodnete-li s tím spíše než po upřené pozorování jen zapsat k prozkoumání malý script formou programu raději u postupu formou čitelnosti z programu než spíše na volnou kontrolu, tato značka setrvává posazena po umístění od samotných zakončení v samotném dvoukilovajtovém limitu rozmezí bajtové vzdálenosti ze završené konečné hloubky a díky úměrně odměřeného poskočení v řádce limitního dozadu odvinutého nahlížení dostatečně (bounded backward read is enough) rychle
// Returns the /XRefStm offset from the file's tail, or -1 if the
// marker is absent (the file is not hybrid, or not a PDF at all)
function FindXRefStm(const FileName: string): Int64;
var
FS: TFileStream;
Tail: AnsiString;
Len, P: Integer;
begin
Result := -1;
FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Len := 2048; // the trailer lives in the tail
if FS.Size < Len then
Len := Integer(FS.Size);
FS.Position := FS.Size - Len; // bounded backward read: 2 KB max
SetLength(Tail, Len);
FS.ReadBuffer(Tail[1], Len);
finally
FS.Free;
end;
P := Pos(AnsiString('/XRefStm'), Tail);
if P = 0 then
Exit; // no hybrid marker in the tail
Inc(P, Length('/XRefStm'));
while (P <= Len) and (Tail[P] in [' ', #9, #13, #10]) do
Inc(P); // skip whitespace after the key
Result := 0;
while (P <= Len) and (Tail[P] in ['0'..'9']) do
begin
Result := Result * 10 + Ord(Tail[P]) - Ord('0');
Inc(P);
end;
end;
// Usage: a non-negative result names the byte where the stream starts
if FindXRefStm('Invoice_XLS.pdf') >= 0 then
Writeln('hybrid-reference file: resave will need the /XRefStm section');
Tento průzkum (probe) berte jako třídění (triage), nikoli jako parsování: řekne vám jen to, které soubory v dávce vyžadují pozornost předtím, než se spustí proces opětovného uložení, a nic víc. Co potom musí zavaděč udělat s ofsetem, který najde, sledovat řetězec sekcí, sloučit položky proudu před těmi klasickými a respektovat hlídací prvky volných položek, to krok za krokem prochází náš doprovodný článek o manipulaci s hybridně referenčními PDF z aplikací sady Office v Delphi
Strana příběhu z pohledu zapisovače, tedy jak se objektové proudy a komprimované křížové odkazy vůbec vytvářejí, je pokryta v našem článku o objektových proudech a inkrementálních aktualizacích. Když je dotyčný hybridní soubor zároveň velmi velký, techniky načítání z průvodce rozhraním Direct File API pro rozsáhlá pracovní schémata (workflows) v PDF vám umožní jej prozkoumat, aniž byste ho museli celý načítat do paměti. Obojí se přirozeně doplňuje s procesem obnovy (recovery) popsaným v tomto článku, a to vše je dodáváno jako součást komponenty HotPDF pro Delphi a C++Builder, bok po boku s aplikačními rozhraními (APIs) pro načítání, editaci, šifrování a podepisování, o kterých se na tomto blogu píše jinde