HotPDF zapisuje linearizované PDF súbory, layout, ktorý Acrobat označuje ako Fast Web View, cez vlastnosť LinearizeOutput na THotPDF. Jej nastavenie pred BeginDoc spôsobí, že HotPDF preusporiada hotový graf objektov tak, aby čítačka rozumejúca rozsahom bajtov mohla zobraziť prvú stranu po stiahnutí iba úvodnej časti súboru, namiesto sťahovania celého dokumentu vopred. Mechanizmus je ISO 32000-1 príloha F
Dôvod, prečo na tom záleží, je nenápadný. Bežný PDF umiestňuje svoju cross-reference tabuľku na koniec, takže prehliadač musí dosiahnuť posledný bajt, kým vie, kde čokoľvek je. Podajte prehliadaču 200-stranovú naskenovanú správu a používateľ zíza na spinner počas celého prenosu, hoci jediné, čo chcel, bola strana 1. Linearizácia to rieši zaplatením nákladu v čase zápisu. Tento článok sa venuje konkrétne tejto ceste zápisu, rozdeleniu, meraciemu cyklu a tvrdým limitom; pre koncepčné pozadie o tom, čo Fast Web View prináša, pokrýva danú pôdu skorší výklad PDF linearizácie a Fast Web View
Čo linearizovaný layout skutočne garantuje
Linearizovaný súbor je obyčajný PDF s extrémne konkrétnym fyzickým usporiadaním, a každá garancia, ktorú ponúka, pochádza z tohto usporiadania, nie z akéhokoľvek nového typu objektu. HotPDF vydáva časti v poradí, ktoré predpisuje príloha F: slovník parametrov linearizácie vnútri prvých 1024 bajtov, skorá cross-reference tabuľka, objekty na úrovni dokumentu, primárny hint stream, prvá strana a jej súkromné objekty, potom zvyšné strany, potom zdieľané objekty, potom všetko ostatné, a nakoniec hlavná cross-reference tabuľka
Rozdelenie je odvodené, nie deklarované. HotPDF prejde referenčný graf od každého objektu strany a pre každý nepriamy objekt zaznamená, koľko strán ho dosahuje a ktorá strana ho dosiahla ako prvá. Objekt použitý presne jednou stranou sa stane súkromným tejto strany. Objekt dosiahnutý viac než jednou sa stane zdieľaným. Katalóg, plus čokoľvek, na čo odkazuje pod /ViewerPreferences, /OpenAction, /Threads a /AcroForm, plus slovník šifrovania, keď je ochrana aktívna, tvoria skupinu úrovne dokumentu, ktorá musí predchádzať všetkému. Uzly stromu strán sa zámerne zadržiavajú, aby neznečistili sekciu prvej strany
Slovník parametrov nesie čísla, ktoré čítačka potrebuje skôr, než čokoľvek iné prečítala: /L pre celkovú dĺžku súboru, /H pre offset a dĺžku hint streamu, /O pre číslo objektu prvej strany, /E pre bajt, kde sekcia prvej strany končí, /N pre počet strán a /T pre offset položky hlavnej cross-reference tabuľky. Každé jedno z nich je offset bajtu do súboru, ktorý v momente, keď ho potrebujete zapísať, ešte neexistuje
Prečo musia offsety hint tabuľky konvergovať?
Pretože čísla v slovníku parametrov opisujú súbor, ktorý ich obsahuje, a zmena ktoréhokoľvek z nich mení súbor. To je hlavná ťažkosť linearizovaného writeru, a preto HotPDF meria opakovane namiesto toho, aby zapísal raz. Rozšírte /T zo 6 číslic na 7 a slovník parametrov narastie o bajt; hlavička narastie; každý objekt sa posunie; hlavná cross-reference tabuľka sa presunie; /T teraz potrebuje inú hodnotu. Layout musí dosiahnuť pevný bod skôr, než sa zaviaže čo i len jeden bajt skutočného výstupu
HotPDF to rieši ohraničenou iteráciou. Najprv serializuje každý objekt do počítacieho streamu, ktorý zaznamenáva dĺžku bez uchovávania bajtov, takže každý objekt má známu serializovanú veľkosť. Potom spustí prechod layoutu, ktorý priradí offsety skupine na úrovni dokumentu, hint streamu, skupine prvej strany, skupinám neskorších strán, zdieľanej skupine a zvyšku, a nahlási, kde by pristála hlavná cross-reference tabuľka. Tento výsledok sa spätne dodá ako vstup do ďalšieho prechodu. Slučka je ohraničená ôsmimi pokusmi, a nekonvergencia vyvolá výnimku namiesto vyprodukovania súboru s vierohodne vyzerajúcimi, no chybnými offsetmi
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ánia slučke v zaseknutí. Slovník parametrov sa zapisuje do pevného 384-bajtového slotu, doplneného medzerami, takže jeho vlastný rast nemôže nikdy destabilizovať layout; ak by text slovníka niekedy prekročil túto rezerváciu, HotPDF vyvolá výnimku namiesto ticho presunúť všetko. A po konvergencii HotPDF spustí ešte jeden potvrdzujúci prechod layoutu a znovu skontroluje dĺžku hint streamu, pretože samotný hint stream kóduje offsety, ktoré boli známe až po ustálení layoutu. Odmenou za všetko toto meranie je, že HotPDF nikdy nebufferuje druhú kópiu dokumentu: akonáhle sú offsety fixné, objekty sa serializujú priamo do cieľového streamu, s assertom na každej hranici sekcie, že zapísané bajty zodpovedajú sľúbenému offsetu
Zapnutie z Delphi
Povrch API je jeden Boolean, a jedinou požiadavkou je nastaviť ho skôr, než začne generovanie. LinearizeOutput je predvolene False, a prechod layoutu beží pri zápise dokumentu, takže jeho priradenie po EndDoc nič nedosiahne
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;
Jedna nasadzovacia výhrada prevažuje nad všetkým na strane kódu. Linearizácia sa oplatí iba vtedy, keď prenosová vrstva podporuje HTTP range requesty. Podávajte ten istý súbor z endpointu, ktorý ho streamuje celý naraz, alebo z konfigurácie CDN, ktorá Range ignoruje, a kúpili ste si iba pomalšiu cestu zápisu a väčší súbor bez akéhokoľvek prínosu viditeľného pre používateľa. Skontrolujte server skôr, než skontrolujete kód
Prečo linearizácia prepisuje UseXRefStream a UseObjectStreams?
Pretože linearizovaný writer potrebuje, aby mal každý objekt svoj vlastný priamo adresovateľný bajtový offset, a obe tieto funkcie mu to odoberajú. HotPDF preto vždy, keď je LinearizeOutput zapnuté, vydáva tradičné textové cross-reference tabuľky a rozbalené nepriame objekty, aj keď volajúci nastavil aj UseXRefStream alebo UseObjectStreams. Toto je zámerné prepísanie, nie konflikt, ktorý musíte riešiť sami
Zdôvodnenie vyplýva z hint tabuliek. Hint tabuľka opisuje, kde sekcia strany začína a aká je dlhá, takže čítačka môže vyžiadať presne tento rozsah. Objekt zabalený do kontajnera /ObjStm nemá vôbec žiadny nezávislý offset; existuje iba ako výrez vnútri iného komprimovaného streamu, ktorý sa musí stiahnuť a rozbaliť ako celok. Ak ste sa spoliehali na objektové streamy kvôli veľkosti súboru, uvedomte si, že linearizácia a kompresia tu ťahajú opačnými smermi, a prečítajte si tento kompromis v sprievodnom článku o objektových streamoch a inkrementálnych aktualizáciách v HotPDF. Rovnaké napätie formuje hybridne referencované súbory, ktoré existujú presne preto, aby starším čítačkám umožnili fungovať popri tabuľkách založených na streamoch, ako je pokryté v článku o hybridných cross-reference streamoch v PDF generovaných Office
Existuje aj minimálna verzia. Linearizácia vyžaduje PDF 1.2 alebo novší. Ak je zvolená verzia staršia, HotPDF ju automaticky zvýši, pokiaľ nie je nastavené StrictVersionLock, v tom prípade zápis vyvolá výnimku namiesto ticho povýšiť dokument, ktorý ste zámerne uzamkli
Múr 4 GiB, a prečo HotPDF radšej odmietne, než skráti
Hint tabuľky linearizácie ukladajú offsety ako 32-bitové hodnoty, takže linearizovaný súbor nemôže adresovať nič na hranici 4 GiB alebo za ňou, a HotPDF taký výstup odmietne s explicitnou výnimkou namiesto zápisu súboru s pretečenými offsetmi. Tento limit nie je implementačná voľba HotPDF; je to šírka polí, ktoré definuje príloha F
Kontrola sa uplatňuje na troch miestach, a všetky tri sú dôležité. HotPDF validuje každý objekt, akonáhle je známa jeho serializovaná dĺžka, validuje dĺžku každej sekcie strany počas budovania hint záznamov, a validuje konečnú dĺžku súboru po tom, ako je vymeraná hlavná cross-reference tabuľka. Skoré zlyhanie je celý zmysel: hint tabuľka s ticho skráteným offsetom vyprodukuje súbor, ktorý sa správne otvorí vo viewery, ktorý ho sťahuje celý naraz, a zlyhá iba pre klienta pracujúceho s bajtovými rozsahmi, ktorému mala linearizácia slúžiť, čo je najhorší možný spôsob zlyhania, pretože váš testovací viewer ho nikdy nezreprodukuje. Ak produkujete výstup s veľkosťou v jednotkách gigabajtov, linearizácia nie je ten správny nástroj, a smerom, kam sa treba pozrieť, je streamovací prístup opísaný v poznámkach o Direct File API pre workflow s veľkými PDF
Detekcia linearizácie na načítanom súbore
THotPDF.IsLoadedLinearized hlási, či bol aktuálne načítaný dokument už zapísaný v linearizovanej forme, a odpovedá na základe snímku zachyteného pred parsovaním, nie zo živého streamu. HotPDF prečíta prvých 1024 bajtov od pozície nula zdrojového streamu, prehľadá ich na prvé kľúčové slovo obj a potom na položku /Linearized s hodnotou 1, a výsledný boolean si uchová v cache
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;
Dve obmedzenia v tomto popise sú nosné. Detekcia sa nemôže spoliehať na pozíciu streamu, pretože v momente, keď sa aplikačný kód spýta, parser ju už posunul, a nemôže znovu čítať na požiadanie, pretože LoadFromFile po skončení načítania uvoľní interný zdrojový stream. Odtiaľ dizajn „zachyť pred parsovaním a ulož do cache“. Skenovanie je tiež zámerne doslovné, čo sa týka hodnoty: akceptuje sa iba /Linearized 1 alebo číselne ekvivalentná forma s celočíselnou nulovou zlomkovou časťou, pretože súbor, ktorého slovník parametrov hovorí niečo iné, nedáva sľub prílohy F
Pasca s Delphi záznamom, ktorá stojí za ukradnutie
Lokálne záznamy obsahujúce dynamické polia inicializujú svoje spravované polia a nič iné, a ak si popri poli držíte obyčajné pole Count, musíte ho vynulovať sami. Toto uhryzlo rozdelenie linearizácie počas vývoja, a je to typ chyby, ktorý stojí deň presne preto, že jedna platforma ju skrýva
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 poľa má počítané referencie, takže ho kompilátor vynuluje. Count vedľa neho je obyčajné celé číslo bez takejto garancie, a neinicializovaný Count pošle úplne prvé pridanie na ľubovoľný index. Pod Win32 slot na zásobníku náhodou obsahoval nulu, pridanie pristálo na indexe 0, a všetky testy prešli. Pod Win64 ten istý kód zapísal za koniec poľa. Poučenie sa zovšeobecňuje ďaleko za linearizáciu: keď záznam mieša spravované a nespravované polia, priraďte Default(TRecord) a prestaňte uvažovať o tom, ktoré polia kompilátor pokrýva, a nikdy nepovažujte zelený beh na Win32 za dôkaz, že inicializácia je správna
Členy LinearizeOutput a IsLoadedLinearized opísané tu sú súčasťou štandardného HotPDF Component pre Delphi a C++Builder; stránka produktu nesie úplnú referenciu vlastností, vrátane pravidiel interakcie s cross-reference streamami, objektovými streamami a uzamknutím verzie