PDF 1.5 object streamy zbalia mnoho malých indirect objektov do jedného Flate-komprimovaného kontajnera a losLab PDF Library ich zapisuje pri plnom uložení pomocou príznaku PackObjectStreams. Zisk je reálny: stovky slovníkov stránok, fontov a anotácií, z ktorých každý stojí desiatky nekomprimovaných bajtov, sa zredukujú na hrsť komprimovaných blobov. Cena je taká, že každý zabalený objekt teraz potrebuje cross-reference stream, ktorý ho popíše
Práve v tejto druhej polovici sa writery lámu. Vytvorenie kontajnera /ObjStm je aritmetika; naučiť cross-reference mechanizmus ukazovať doň je redesign. Writer, ktorý vytvorí dokonale platný kontajner a potom popíše jeho členov obyčajnými type-1 offsetmi, vytvoril súbor, ktorý Acrobat otvorí presne na tak dlho, aby ho vyhlásil za poškodený. Tieto dve vlastnosti sú jedna vlastnosť a tento článok pokrýva stranu zápisu oboch, ako sú definované v ISO 32000-1 §7.5.7 a §7.5.8
Čo skutočne obsahuje kontajner ObjStm
Object stream je stream, ktorého dekódované bajty sú dve zreťazené oblasti, a ISO 32000-1 §7.5.7 dáva slovníku presne tri kľúče, na ktorých záleží pri konštrukcii. /Type /ObjStm ho identifikuje, /N udáva počet členov a /First udáva bajtovú dĺžku oblasti hlavičky — ekvivalentne offset, na ktorom začína telo. Hlavička sú medzerou oddelené páry čísla objektu a offsetu; telo sú členovia serializovaní za sebou, pričom každý offset sa meria od začiatku tela, nie od začiatku dekódovaného payloadu. Čítanie plne dekódovaného kontajnera to robí zjavným: nižšie je /First 14, pretože tri riadky hlavičky zaberajú štrnásť bajtov, a objekt 7 sedí 55 bajtov vnútri tela, pretože objekt 4 sa serializoval na 54 znakov plus separátor
// Decoded payload of: 12 0 obj << /Type /ObjStm /N 3 /First 14
// /Filter /FlateDecode /Length 118 >> stream
4 0
7 55
9 90
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
<< /Type /ExtGState /CA 1 /ca 1 >>
[ 0 0 595 842 ]
Dve pravidlá členstva sú absolútne a obe pochádzajú priamo z §7.5.7. Stream objekt nikdy nemôže byť členom, pretože stream nesie surové bajty, ktoré by museli byť vnorené vnútri iného streamu. A člen musí byť kompletná hodnota objektu, nikdy nie holá indirect reference — skomprimovaný objekt, ktorý je len 5 0 R, vytvára indirekciu, ktorú reader nedokáže vyriešiť bez toho, aby už vedel, kam ukazuje. losLab PDF Library obidva prípady vyfiltruje počas zberu kandidátov, spolu so šifrovacím slovníkom a objektom 0, a potom zabalí, čo prežije, po skupinách 200 na kontajner. Tento strop je rozhodnutie o náhodnom prístupe, nie limit špecifikácie: reader, ktorý chce jedného člena, musí nafúknuť celý kontajner, takže predimenzované kontajnery robia malé vyhľadávania drahými
Prečo musia členovia ObjStm používať type-2 cross-reference záznamy?
Pretože zabalený objekt nemá žiadny súborový offset, ktorý by sa dal zaznamenať. ISO 32000-1 §7.5.8 na to odpovedá tromi typmi záznamov v binárnom cross-reference streame: typ 0 pre voľné objekty, typ 1 pre bežné používané objekty uložené na bajtovom offsete a typ 2 pre skomprimované objekty, ktorých dve dátové polia nesú číslo objektu kontajnera a index člena vnútri neho. V klasickej plaintext tabuľke xref neexistuje spôsob, ako vyjadriť zabalený objekt, čo je presne dôvod, prečo PDF 1.5 zaviedol obe vlastnosti spolu
Poradie, ktoré nasleduje, potkne takmer každú prvú implementáciu, vrátane našej. Bežné objekty dostávajú type-1 záznamy. Samotné kontajnery /ObjStm dostávajú type-1 záznamy, pretože kontajner je úplne normálny indirect stream objekt zapísaný na skutočnom offsete. Iba členovia dostávajú type-2 záznamy. A cross-reference stream je sám osebe indirect objekt v súbore, takže potrebuje vlastný type-1 záznam ukazujúci na offset, kam bol práve zapísaný — ten istý offset, ktorý zaznamenáva startxref. Skoršia verzia nášho writera vylučovala čísla objektov kontajnerov z zápisovej slučky namiesto vylučovania členov, a výsledkom bol súbor s cross-reference streamom a bez akýchkoľvek object streamov: štrukturálne koherentný, sémanticky prázdny, downstream odmietnutý. Hodnota /Size skrýva zodpovedajúcu chybu o jedna, keďže je to najvyššie číslo objektu plus jedna a cross-reference stream je alokovaný ako najvyššie číslo objektu, takže sa musí tiež počítať
Určenie veľkosti poľa /W: prečo štyri bajty nestačia
Pole /W deklaruje bajtovú šírku každého z troch polí a losLab PDF Library ho zapisuje ako /W [1 Field2 Field3] s poľom 1 pevne na jeden bajt pre kód typu a poľom 3 pevne na dva bajty, čo pokrýva čísla generácie až do 65535 aj indexy členov. Pole 2 je to, ktoré nemôže byť konštanta, pretože nesie dve nesúvisiace veličiny: v type-1 zázname je to bajtový offset ohraničený len veľkosťou súboru, kým v type-2 zázname je to číslo objektu kontajnera a v type-0 zázname je to ďalší voľný objekt v reťazci. Pevné štvorbajtové pole 2 funguje v poriadku, kým súbor neprekročí 4 GB, po čom sa každý offset za hranicou potichu skráti a celá tabuľka sa stane odpadom. Writer preto prehľadá zostavenú tabuľku pre najväčšiu hodnotu, akú kedy bude nosiť ktorýkoľvek slot poľa 2, vrátane offsetu samotného cross-reference streamu, a rozšíri pole až na osem bajtov
// Field 2 must hold the largest byte offset AND the largest
// ObjStm container number AND the largest free-chain target.
MaxField2Value := XRefStart;
for X := 0 to MaxObj do
begin
if XRefTable[X].InUse and (XRefTable[X].ObjStrNum > 0) then
Field2Value := XRefTable[X].ObjStrNum // type-2: container number
else
Field2Value := XRefTable[X].ObjPos; // type-1 offset / type-0 next-free
if Field2Value > MaxField2Value then
MaxField2Value := Field2Value;
end;
Field2 := 4;
while (Field2 < 8) and
(MaxField2Value > ((Int64(1) shl (Field2 * 8)) - 1)) do
Inc(Field2);
Field3 := 2; // generation numbers and member indices both fit
Keď sú šírky známe, veľkosť payloadu je známa presne, takže writer vopred alokuje celý buffer a naplní ho podľa indexu; pripájanie záznamov bajt po bajte do AnsiString mení konštrukciu tabuľky na kvadratickú, čo si nikto nevšimne pri desaťstránkovej faktúre a všetci si to všimnú pri dokumente s dvestotisíc objektmi. Dva ďalšie detaily udržia prísnych readerov spokojnými. /Index deklaruje, ktoré rozsahy čísel objektov tabuľka pokrýva, a pri plnom prepísaní je to jednoducho [0 N] bez medzier. A každý slot, ktorý writer skutočne nevydal, musí predvolene byť voľný namiesto používaného: objekt 0 stojí na čele voľného reťazca, každý voľný slot sa napája na ďalší, a slot, ktorý kedysi držal vymazaný objekt, si ponechá svoje číslo generácie zvýšené o jedna. Sprievodná poznámka o bezpečnosti pamäte pri parsovaní nedôveryhodných PDF uvádza rovnaký argument o hraniciach zo strany čítania
Prečo cross-reference stream nesmie byť nikdy šifrovaný?
Pretože reader ho musí spracovať skôr, než môže vedieť, ako čokoľvek dešifrovať. Cross-reference stream je to, čo readerovi hovorí, kde žije slovník /Encrypt; keby jeho bajty boli samy šifrované, reader by potreboval kľúč súboru, aby našiel objekt, ktorý kľúč súboru popisuje. losLab PDF Library to vynucuje jedným predikátom: ShouldCryptStreamData vráti False vždy, keď stream slovník nesie /Type /XRef, takže výnimka platí bez ohľadu na to, ktorá cesta sa dostane k serializátoru
Kontajner /ObjStm dostáva opačné zaobchádzanie a táto asymetria je zámerná. Kontajner sa šifruje ako celok, kľúčovaný podľa vlastného čísla objektu, presne ako každý iný stream. Jeho členovia nie sú šifrovaní jednotlivo — sú zabalení vo svojej dešifrovanej plaintext forme a jeden prechod cez zostavený kontajner ich pokryje, vrátane reťazcov. Dvojité šifrovanie členov produkuje súbor, ktorý sa dešifruje na ciphertext, a pretože vonkajšia vrstva uspeje, zlyhanie sa prejaví ako chyba parsovania hlboko v grafe objektov namiesto chyby autentifikácie. Jeden objekt potom ostáva úplne mimo schémy: v šifrovanom dokumente sa Catalog uchováva ako priamy type-1 objekt a nikdy sa nezabaľuje, pretože jeho zabalenie by prinútilo loader nafúknuť a dešifrovať object stream, aby sa dostal ku koreňu dokumentu, ešte pred tým, než je plne vybudovaný dešifrovací kontext, ktorý koreň pomáha ustanoviť
Zapnutie packovania z Delphi
Verejný prepínač je PackObjectStreams, vystavený ako pole na TPDFlibSaveOptions, ako samostatný setter SetPackObjectStreams a ako vlastnosť na objekte dokumentu. Predvolene je zapnutý a automaticky sa viaže na verziu: writer zabaľuje len vtedy, keď je dokument už PDF 1.5 alebo novší, a volá interný guard minimálnej verzie, takže zabalený dokument sa povýši na 1.5 namiesto toho, aby bol nesprávne označený. Po uložení GetLastSaveUsedObjectStreams nahlási, či sa brána skutočne otvorila, čo je assert, ktorý chcete v regresnom teste, nie porovnanie veľkosti bajtov
var
Doc: TPDFlib;
Options: TPDFlibSaveOptions;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('report.pdf', '') <= 0 then
Exit;
Doc.SetInformation(0, '1.5'); // packing is gated on PDF 1.5+
FillChar(Options, SizeOf(Options), 0);
Options.CompressContent := True;
Options.GarbageCollect := True; // drop orphans before packing
Options.PackObjectStreams := True;
if Doc.SaveToFileOptions('report-packed.pdf', Options) = 1 then
if Doc.GetLastSaveUsedObjectStreams = 1 then
Writeln('Saved with ObjStm containers and an xref stream');
finally
Doc.Free;
end;
end;
Na poradí medzi packovaním a garbage collection záleží. Analýza dosiahnuteľnosti musí bežať prvá, pretože člen, ktorý prežije do kontajnera, so sebou stiahne aj kontajner — ak je zabalený živý objekt, číslo jeho kontajnera je z definície dosiahnuteľné, a odmetenie kontajnera preč by odrezalo člena bez spôsobu, ako ho nájsť. Spustenie kolektora ako prvého tiež znamená, že mŕtve objekty sa do kontajnera nikdy nedostanú, čo je odkiaľ pochádza kumulujúci sa zisk vo veľkosti. Packovanie dopĺňa ostatné páky na veľkosť, nenahrádza ich; prehľad optimalizácie veľkosti súboru PDF a subsettingu fontov pokrýva páky, ktoré pôsobia na obsah streamov, kým object streamy pôsobia na štruktúru
Hranice, o ktorých treba vedieť pred zapnutím
Inkrementálne uloženia nikdy nepackujú. Inkrementálna aktualizácia pripája nové objekty a novú cross-reference sekciu, pričom ponecháva skoršie revízie fyzicky nedotknuté, takže prebalenie existujúcich objektov do nových kontajnerov by osirotilo type-1 záznamy, na ktoré predchádzajúca revízia stále odkazuje; losLab PDF Library vypína packovanie vždy, keď je aktívny append mód, a článok o inkrementálnych aktualizáciách a streamovaní v append móde pokrýva túto cestu v plnom rozsahu. Dokumenty pod PDF 1.5 bezpodmienečne ponechávajú plaintext cross-reference tabuľku: konzument 1.4 nemá poňatia, čo /ObjStm znamená, a potichu povýšiť dokument, pretože writer uprednostnil menší súbor, by bol nesprávny obchod urobený za volajúceho. Jeden voliteľný kľúč, ktorý zámerne nevydávame, je /Extends, ktorý ISO 32000-1 §7.5.7 definuje, aby kontajner mohol pomenovať predchodcu a readery mohli traktovať reťaz kontajnerov ako logickú skupinu. Je skutočne voliteľný, každý kontajner, ktorý zapisujeme, je samostatný a nezávisle dekódovateľný, a jeho vynechanie odstraňuje z writera celú triedu chýb s cyklami a visiacimi referenciami — hoci readery samozrejme musia stále rešpektovať /Extends, keď sa s ním stretnú v súboroch od iných producentov
Packovanie object streamov a výstup cross-reference streamu sa dodávajú ako súčasť losLab PDF Library pre Delphi a C++Builder, popri garbage collectore a optimalizátore content streamov, s ktorými sa skladajú; produktová stránka nesie kompletnú referenciu možností ukladania