HotPDF zapisuje linearizované soubory PDF, tedy rozvržení, které Acrobat označuje jako Fast Web View, přes vlastnost LinearizeOutput na THotPDF. Nastavení před BeginDoc přiměje HotPDF přeuspořádat hotový graf objektů tak, aby čtečka podporující byte-range mohla zobrazit první stránku po stažení pouze úvodní části souboru, místo aby musela nejprve stáhnout celý dokument. Mechanismus popisuje ISO 32000-1 Annex F
Důvod, proč na tom záleží, není nijak okázalý. Běžný PDF má svou cross-reference tabulku na konci, takže prohlížeč musí dosáhnout posledního bajtu, než zjistí, kde co je. Pošlete-li prohlížeči 200stránkovou naskenovanou zprávu, uživatel sleduje spinner po celou dobu přenosu, přestože chtěl jen první stránku. Linearizace tento problém řeší tím, že cenu zaplatí už při zápisu. Tento článek se zabývá právě touto zápisovou cestou — rozdělením, měřicí smyčkou a pevnými limity; koncepční pozadí toho, co Fast Web View přináší, popisuje dřívější vysvětlení linearizace PDF a Fast Web View
Co linearizované rozvržení skutečně zaručuje
Linearizovaný soubor je obyčejný PDF s velmi konkrétním fyzickým uspořádáním a každá záruka, kterou nabízí, plyne právě z tohoto uspořádání, nikoli z nějakého nového typu objektu. HotPDF vypisuje jednotlivé části v pořadí předepsaném Annex F: linearizační parametrický slovník v prvních 1024 bajtech, dřívou cross-reference tabulku, objekty na úrovni dokumentu, primární hint stream, první stránku a její soukromé objekty, poté zbývající stránky, poté sdílené objekty, poté vše ostatní a nakonec hlavní cross-reference tabulku
Rozdělení se odvozuje, nezadává se ručně. HotPDF prochází graf odkazů od každého objektu stránky a pro každý nepřímý objekt zaznamenává, kolik stránek k němu vede a která stránka k němu vedla jako první. Objekt používaný přesně jednou stránkou se stává soukromým pro tuto stránku. Objekt, ke kterému vede více stránek, se stává sdíleným. Katalog spolu se vším, na co odkazuje pod /ViewerPreferences, /OpenAction, /Threads a /AcroForm, plus šifrovací slovník při aktivní ochraně, tvoří skupinu na úrovni dokumentu, která musí předcházet všemu ostatnímu. Uzly stromu stránek jsou záměrně zadrženy, aby neznečistily sekci první stránky
Parametrický slovník nese čísla, která čtečka potřebuje dřív, než si přečte cokoli dalšího: /L pro celkovou délku souboru, /H pro offset a délku hint streamu, /O pro číslo objektu první stránky, /E pro bajt, kde končí sekce první stránky, /N pro počet stránek a /T pro offset položky hlavní cross-reference tabulky. Každá z těchto hodnot je offset v bajtech do souboru, který v okamžiku, kdy je potřeba je zapsat, ještě neexistuje
Proč musí offsety hint table konvergovat?
Protože čísla v parametrickém slovníku popisují soubor, který je obsahuje, a změna kteréhokoli z nich mění soubor samotný. To je hlavní obtíž linearizovaného writeru a proto HotPDF měří opakovaně místo toho, aby zapisoval jen jednou. Rozšíří-li se /T ze 6 číslic na 7, parametrický slovník naroste o bajt; hlavička naroste; každý objekt se posune; hlavní cross-reference tabulka se přesune; /T nyní potřebuje jinou hodnotu. Rozvržení musí dosáhnout pevného bodu ještě předtím, než je zapsán jediný bajt skutečného výstupu
HotPDF to řeší omezenou iterací. Nejprve serializuje každý objekt do počítacího streamu, který zaznamenává délku bez uchovávání bajtů, takže každý objekt má známou serializovanou velikost. Poté proběhne krok rozvržení, který přiřadí offsety skupině na úrovni dokumentu, hint streamu, skupině první stránky, pozdějším skupinám stránek, sdílené skupině a zbytku, a nahlásí, kam by dopadla hlavní cross-reference tabulka. Tento výsledek se vrátí jako vstup do dalšího průchodu. Smyčka je omezena na osm pokusů a nekonvergence vyvolá výjimku místo toho, aby vytvořila soubor s věrohodně vypadajícími, avšak chybnými offsety
CandidateMainOffset := 0;
for Attempt := 0 to 7 do
begin
CalculateLayout(CandidateMainOffset, FirstXRefData,
HintOffset, EndFirstPage, NewMainOffset);
if NewMainOffset = CandidateMainOffset then
Break;
CandidateMainOffset := NewMainOffset;
end;
if NewMainOffset <> CandidateMainOffset then
raise Exception.Create('Linearization layout did not converge');
Dva detaily brání tomu, aby se smyčka zacyklila. Parametrický slovník se zapisuje do pevného slotu o délce 384 bajtů vyplněného mezerami, takže jeho vlastní růst nikdy nemůže rozvržení destabilizovat; pokud by text slovníku někdy tuto rezervu překročil, HotPDF vyvolá výjimku místo toho, aby tiše posunul vše ostatní. A po konvergenci HotPDF ještě jednou spustí potvrzující krok rozvržení a znovu zkontroluje délku hint streamu, protože samotný hint stream kóduje offsety, které byly známé teprve po ustálení rozvržení. Odměnou za veškeré toto měření je, že HotPDF nikdy nebufferuje druhou kopii dokumentu: jakmile jsou offsety pevně dané, objekty se serializují přímo do cílového streamu, s kontrolou na každé hranici sekce, že zapsané bajty odpovídají slíbenému offsetu
Zapnutí z Delphi
Rozhraní API je jediná hodnota typu Boolean a jediným požadavkem je nastavit ji před začátkem generování. LinearizeOutput má výchozí hodnotu False a krok rozvržení proběhne při zápisu dokumentu, takže přiřazení po EndDoc nic nezmění
var
PDF: THotPDF;
begin
PDF := THotPDF.Create(nil);
try
PDF.FileName := 'fast-view.pdf';
PDF.Version := pdf17;
PDF.LinearizeOutput := True; // must precede BeginDoc
PDF.BeginDoc;
PDF.Canvas.TextOut(72, 72, 'First page');
PDF.EndDoc;
finally
PDF.Free;
end;
end;
Jedno nasazovací upozornění převažuje nad vším ostatním na straně kódu. Linearizace se vyplatí pouze tehdy, když přenosová vrstva podporuje HTTP range requesty. Podáváte-li stejný soubor z endpointu, který jej streamuje vcelku, nebo z konfigurace CDN, jež ignoruje hlavičku Range, pak jste si koupili jen pomalejší zápis a větší soubor bez jakéhokoli přínosu pro uživatele. Nejprve zkontrolujte server, teprve pak kód
Proč linearizace přepisuje UseXRefStream a UseObjectStreams?
Protože linearizovaný writer potřebuje, aby každý objekt měl svůj vlastní přímo adresovatelný offset v bajtech, a obě tyto funkce mu to znemožňují. HotPDF proto při zapnutém LinearizeOutput vždy vypisuje tradiční textové cross-reference tabulky a nesbalené nepřímé objekty, a to i v případě, že volající zároveň nastavil UseXRefStream nebo UseObjectStreams. Jde o záměrné přepsání, ne o konflikt, který byste museli řešit sami
Zdůvodnění vychází z hint tables. Hint table popisuje, kde sekce stránky začíná a jak je dlouhá, aby si čtečka mohla vyžádat přesně tento rozsah. Objekt zabalený do kontejneru /ObjStm nemá žádný samostatný offset; existuje pouze jako výřez uvnitř jiného komprimovaného streamu, který se musí stáhnout a rozbalit jako celek. Pokud jste s objektovými streamy počítali kvůli velikosti souboru, mějte na paměti, že linearizace a komprese zde táhnou v opačných směrech; kompromis podrobně popisuje související článek o objektových streamech a přírůstkových aktualizacích v HotPDF. Totéž napětí formuje hybridní referenční soubory, které existují právě proto, aby starší čtečky fungovaly vedle streamových tabulek, jak popisuje článek o hybridních cross-reference streamech v PDF generovaných kancelářskými aplikacemi
Existuje i minimální požadovaná verze. Linearizace vyžaduje PDF 1.2 nebo novější. Je-li zvolená verze starší, HotPDF ji automaticky zvýší, pokud není nastaveno StrictVersionLock; v takovém případě zápis vyvolá výjimku, místo aby tiše povýšil dokument, jehož verzi jste zafixovali záměrně
Zeď 4 GiB a proč HotPDF raději odmítne, než aby zkracoval
Hint tables při linearizaci ukládají offsety jako 32bitové hodnoty, takže linearizovaný soubor nemůže adresovat nic na hranici 4 GiB nebo za ní, a HotPDF takový výstup odmítne explicitní výjimkou, místo aby zapsal soubor s přetečenými offsety. Tento limit není implementační volbou HotPDF; je to šířka polí definovaných v Annex F
Kontrola se provádí na třech místech a všechna tři jsou důležitá. HotPDF ověřuje každý objekt, jakmile je známa jeho serializovaná délka, ověřuje délku každé sekce stránky při sestavování hint entries a ověřuje konečnou délku souboru poté, co je určena velikost hlavní cross-reference tabulky. Selhat brzy je celý smysl: hint table s tiše zkráceným offsetem produkuje soubor, který se v prohlížeči stahujícím jej vcelku otevře správně, ale selže právě u byte-range klienta, kterému měla linearizace sloužit — což je nejhorší možný typ selhání, protože ve vašem testovacím prohlížeči se nikdy neprojeví. Pokud produkujete výstup o velikosti řádu gigabajtů, linearizace není ten správný nástroj, a směrem, kam se podívat, je streamovací přístup popsaný v poznámkách o Direct File API pro velké PDF workflow
Detekce linearizace u načteného souboru
THotPDF.IsLoadedLinearized udává, zda byl aktuálně načtený dokument již zapsán v linearizované podobě, a odpovídá na základě snímku pořízeného před parsováním, ne z živého streamu. HotPDF přečte prvních 1024 bajtů od pozice nula ve zdrojovém streamu, prohledá je na první klíčové slovo obj a poté na položku /Linearized s hodnotou 1, a výsledný booleovský výsledek uloží do mezipaměti
var
PDF: THotPDF;
PageCount: Integer;
begin
PDF := THotPDF.Create(nil);
try
PageCount := PDF.LoadFromFile('incoming.pdf');
if (PageCount > 0) and (not PDF.IsLoadedLinearized) then
Writeln('Source is not Fast Web View ready');
finally
PDF.Free;
end;
end;
Dva požadavky v tomto popisu jsou zásadní. Detekce se nemůže spoléhat na pozici v streamu, protože v okamžiku, kdy se aplikační kód na tuto otázku zeptá, parser ji už posunul, a nemůže znovu číst na vyžádání, protože LoadFromFile po dokončení načítání uvolní interní zdrojový stream. Odtud návrh „zachytit před parsováním a uložit do mezipaměti“. Skenování je také záměrně doslovné, pokud jde o hodnotu: přijímá se pouze /Linearized 1 nebo číselně ekvivalentní tvar se zcela nulovou zlomkovou částí, protože soubor, jehož parametrický slovník říká něco jiného, nesplňuje slib z Annex F
Past v Delphi záznamech, kterou stojí za to si odnést
Lokální záznamy obsahující dynamická pole inicializují své spravované položky a nic víc — pokud vedle pole udržujete obyčejnou položku Count, musíte ji vynulovat sami. Toto přesně zavinilo problém v rozdělování při linearizaci ve fázi vývoje a jde o druh chyby, která stojí celý den práce právě proto, že ji jedna platforma skrývá
type
THPDFLinearIndexList = record
Values: THPDFIntegerArray; // managed field: cleared for you
Count: Integer; // plain field: whatever was on the stack
end;
// Required, not cosmetic:
Part4 := Default(THPDFLinearIndexList);
Part6 := Default(THPDFLinearIndexList);
Part8 := Default(THPDFLinearIndexList);
Part9 := Default(THPDFLinearIndexList);
Pole dynamického pole má počítání referencí, takže je kompilátor vynuluje. Položka Count vedle něj je obyčejné celé číslo bez takové záruky, a neinicializovaná hodnota Count pošle úplně první přidání na libovolný index. Na Win32 zásobníkový slot náhodou obsahoval nulu, přidání skončilo na indexu 0 a všechny testy prošly. Na Win64 stejný kód zapsal za konec pole. Ponaučení platí daleko za hranice linearizace: mísí-li záznam spravovaná a nespravovaná pole, přiřaďte Default(TRecord) a přestaňte uvažovat o tom, která pole kompilátor pokrývá — a nikdy neberte úspěšný běh na Win32 jako důkaz správné inicializace
Zde popsané členy LinearizeOutput a IsLoadedLinearized jsou součástí standardní komponenty HotPDF Component pro Delphi a C++Builder; stránka produktu obsahuje kompletní referenci vlastností, včetně pravidel interakce s cross-reference streamy, objektovými streamy a zamykáním verzí