Technical Article

Načítání hybridních PDF s křížovými odkazy z Wordu a Excelu v Delphi

Otevřete PDF vytvořené v programu Microsoft Word nebo Excel, listujte jím a nic nevypadá neobvykle. Načtěte jej do programu v Delphi, přečtěte počet stránek a číslo je správné. Poté jej uložte se zapnutým šifrováním a úloha selže s chybou EListError, případně se výstup otevře s varováním o poškozených křížových odkazech. Soubor nebyl nikdy poškozen. Jedná se o soubor s hybridními odkazy (hybrid-reference file) a právě tato struktura, která umožňuje patnáct let starému prohlížeči soubor otevřít, ochromí načítač, který přestane číst příliš brzy.

Toto je jeden z nejčastějších způsobů, jak se PDF pipeline, která prošla všemi interními testy, setká se souborem, který nedokáže znovu uložit. Všechny vstupy byly generovány interně, takže nebyly nikdy hybridní. První hybridní soubor dorazí v den, kdy zákazník přepošle fakturu exportovanou z tabulkového procesoru.

Co Word a Excel skutečně zapisují

ISO 32000-1 popisuje hybridní rozvržení odkazů v §7.5.8.4. Aplikace, která chce funkce PDF 1.5, jako jsou toky objektů (object streams), a zároveň chce umožnit čtečce PDF 1.4 soubor otevřít, zapíše informace o křížových odkazech dvakrát. Existuje klasická tabulka křížových odkazů s řádky ASCII s pevnou šířkou, které zakončovaly každé PDF až do verze 1.4, a stream křížových odkazů, který indexuje zbytek. Zakončení (trailer) klasické sekce obsahuje položku /XRefStm, jejíž hodnotou je bajtový offset tohoto streamu.

Rozdělení práce je záměrné. Objekty, ke kterým se stará čtečka musí dostat, včetně katalogu a stromu stránek, jsou adresovatelné z klasické tabulky. Objekty, které byly sloučeny do komprimovaných toků objektů, jsou v klasické tabulce označeny jako volné s položkou typu f, takže je čtečka PDF 1.4 přeskočí a nenarazí na strukturu, kterou nedokáže zpracovat. Jejich skutečné umístění se nachází pouze ve streamu křížových odkazů. Příznakem takového souboru je jeho konec: krátká klasická sekce, často nic víc než xref následovaný hlavičkou podsekce 0 0, jejíž trailer ukazuje na /XRefStm, kde se nacházejí skutečná obnovovací data.

Proč správný počet stránek nic nedokazuje

Vzhledem k tomu, že katalog a strom stránek jsou z klasické tabulky záměrně dostupné, načítač, který čte pouze tuto tabulku, najde /Root, projde strom stránek a nahlásí správný počet stránek. Vše, co stará čtečka potřebuje, je přítomno, takže soubor vypadá v pořádku. Chybějícími objekty jsou ty, které jsou zabaleny v tocích objektů: slovníky polí AcroForm, strukturní prvky tagovaného PDF a dlouhý řetězec malých slovníků, které starší prohlížeč nikdy nemusel vidět.

Této mezery si nevšimnete, dokud se něco těchto objektů nedotkne, a úplné opětovné uložení se dotkne všech z nich. Procházení dokumentu za účelem opětovného šifrování nebo přepisu je přesně tou operací, která postupně vyžaduje každé číslo objektu. Proto se symptom objevuje při ukládání a nikoli při načítání, daleko od své skutečné příčiny.

Pastí je detektor, který uvidí xref a zastaví se

Rychlým způsobem, jak rozhodnout o způsobu indexování souboru, je sledovat startxref a zkontrolovat první bajty, na které ukazuje. Klíčové slovo xref znamená klasickou tabulku; objekt streamu znamená stream křížových odkazů. Tento test je správný pro jakýkoli soubor, který se drží jednoho schématu. Je však nesprávný pro hybridní soubor, jehož startxref směřuje na klasickou sekci pouze proto, aby vyhověl starým čtečkám, zatímco /XRefStm v traileru této sekce je místem, kde je skutečně indexována většina dokumentu. Detektor, který při prvním výskytu xref vrátí „classic“, nikdy nepřečte /XRefStm a každý objekt, který existuje pouze v tomto streamu, se stane neviditelným.

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;

S detektorem předčasného ukončení vypadá načtení v pořádku a až při opětovném uložení se chybějící objekty přihlásí o slovo. Náprava nespočívá v načtení více bajtů na začátku; je třeba rozpoznat hybridní trailer a sledovat /XRefStm předtím, než se rozhodne, že je soubor hotov.

Pořadí sloučení je neměnné

Jakmile jsou oba indexy načteny, lze je kombinovat pouze v jednom směru. Stream křížových odkazů musí být sloučen jako první a klasické položky se doplní kolem něj. Důvodem je drobná finta v jádru formátu. Hybridní soubor označuje své komprimované objekty v klasické tabulce jako volné, aby je staré čtečky ignorovaly. Načítač, který ctí zásadu „první nalezený vyhrává“ a čte nejprve klasickou tabulku, zaznamená tato čísla objektů jako volná a poté zahodí položky streamu, které je skutečně lokalizují, protože pozice jsou již obsazené. Obrátíte-li pořadí, položky typu 2 ze streamu (každá představuje číslo objektového streamu a index) získají pozice, které mají vlastnit, a klasické položky se uspořádají kolem nich.

Stejná disciplína brání tomu, aby starší revize oživila smazaný objekt. Inkrementální aktualizace se řetězí zpět přes /Prev a volná položka typu 0 je značkou, že novější sekce vyřadila číslo objektu. Pozdější, starší sekce v řetězci nesmí mít možnost přepsat tuto značku neaktuálním umístěním. Pokud považujete první nalezený volný marker za autoritativní, smazaný objekt zůstane smazán. Pokud k tomu přistoupíte neopatrně, historie souboru oživí obsah, který poslední revize odstranila.

Co to znamená v HotPDF

Engine řeší hybridní soubory odkazů za vás, a to na každé cestě, která musí analyzovat data křížových odkazů. Načtěte dokument pomocí LoadFromFile nebo LoadFromStream, proveďte změny a zavolejte SaveLoadedDocument; nebo spusťte jednorázovou operaci, jako je EncryptFile, která načte vstup a zapíše výstup. V obou případech obnova načte /XRefStm, sloučí sekvenci streamu před klasickými položkami a vyřeší objekty žijící ve streamech předtím, než je zápis vyjmenuje. Cesta šifrování AES-256 je místem, kde se problém poprvé projevil, protože šifrování dokumentu přepisuje každý objekt, což vyžaduje, aby byl každý objekt již lokalizován.

// 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]);

Důležitý detail leží nad samotným rozhraním API. Soubory, které pocházejí z Wordu, Excelu, PowerPointu a dlouhého seznamu nástrojů typu „Uložit jako PDF“, jsou běžně hybridní. Načítač, který testujete pouze proti výstupům vlastního generátoru, se s nimi při testování nemusí vůbec setkat. Doplňte své testovací vzorky o dokumenty exportované z reálných aplikací Office, nikoli pouze o soubory vytvořené vaším vlastním kódem.

Kontrola podezřelého souboru

Dvě kontroly na tuto otázku rychle odpoví. Otevřete soubor v hexadecimálním zobrazení a přečtěte bajty za posledním startxref; hybridní soubor ukazuje krátkou klasickou sekci, jejíž trailer slovník obsahuje /XRefStm. Nebo porovnejte počet objektů nahlášený úplnou analýzou s nejvyšším číslem objektu, které deklaruje /Size v traileru. Velký rozdíl znamená, že se objekty skrývají ve streamech, které načítač neotevřel, což je stejný nedostatek, který se později změní v selhání při ukládání.

Strana zapisovače, tedy jak se toky objektů a komprimované křížové odkazy vůbec vytvářejí, je popsána v našem článku o tocích objektů a inkrementálních aktualizacích. Pokud je dotyčný hybridní soubor velmi velký, načítací techniky v průvodci Direct File API pro zpracování velkých PDF vám umožní jej prozkoumat, aniž byste jej museli celý načítat do paměti. Oba postupy přirozeně doplňují zde popsanou obnovu, která je dodávána jako součást HotPDF Component pro Delphi a C++Builder společně s rozhraními API pro načítání, úpravy, šifrování a podepisování popsanými jinde na tomto blogu.