Vymazanie stránky z PDF nevymaže jej fonty, obrázky ani content streamy. losLab PDF Library ich uvoľňuje pomocou mark-sweep kolektora, ktorý prechádza graf objektov od koreňov v trailer-i a odstraňuje každý indirect object, ku ktorému už nič nevedie. Beží pri plnom uložení, je predvolene vypnutý a vracia počet objektov, ktoré odstránil
Prečo vymazanie stránok PDF nezmenší súbor?
Pretože vymazanie stránky je úprava referencie, nie operácia so storage. DeletePages(StartPage, PageCount) odpojí objekty stránok od stromu stránok a opraví položky osnovy (outline), ktoré na ne odkazovali. Čo nedokáže, je rozhodnúť, že font program, content stream a image XObject, ktoré tieto stránky používali, sú teraz mŕtve, pretože v okamihu vymazania nič v súbore nezaznamenáva, kto na ne ešte môže odkazovať. Tieto objekty ostávajú v zozname objektov dokumentu a plné uloženie zapíše každý jeden z nich späť. Výsledkom je sťažnosť, ktorou začína väčšina týchto support vlákien: zákazník vymaže deväťdesiat percent stránok, uloží a súbor sa zmenší o dve percentá. Horšie je, že únik sa kumuluje. Načítať, vymazať, uložiť, znova načítať, znova vymazať, znova uložiť, a súbor monotónne rastie, zatiaľ čo počet stránok klesá. Toto je iný problém než ten, ktorý rieši subsetting fontov a downsampling obrázkov, ktoré zmenšujú živé objekty. Tu objekty nie sú príliš veľké — jednoducho už nie sú súčasťou dokumentu
Koreňová množina je trailer, nie strom stránok
Graf objektov PDF nemá pole spätnej referencie. Formát nedefinuje počet referencií ani zoznam spätných ukazovateľov a existujúce kľúče /Parent patria konkrétnym štruktúram, ako je strom stránok, nie grafu objektov ako celku. Nič v indirect objekte nehovorí, kto naň odkazuje, takže otázka „používa ešte niekto objekt 47" má presne jednu odpoveď: prejsť dopredu od známeho koreňa a zistiť, či sa tam dostaneme. Preto je kolektor v losLab PDF Library mark-sweep kolektor, nie schéma s počítadlom referencií
Korene pochádzajú z trailer-a súboru (ISO 32000-1 §7.5.5). Nesú ich tri kľúče: /Root, katalóg dokumentu z §7.7.2, na ktorom visí strom stránok, mená, osnova, AcroForm a metadáta; /Info, informačný slovník dokumentu; a /Encrypt, šifrovací slovník. Zvyšné dva kľúče trailer-a sú návnady. /ID je pole dvoch bajtových reťazcov a /Prev je celočíselný bajtový offset na predchádzajúcu cross-reference sekciu. Ani jeden z nich nie je indirect reference, takže ani jeden neprispieva ako koreň. losLab PDF Library zaraďuje do fronty celý slovník trailer-a namiesto troch pomenovaných kľúčov, čo nič nestojí a udrží pri živote akékoľvek súkromné rozšírenie trailer-a
Samotný prechod je iteratívny, nie rekurzívny. Keď prechod narazí na indirect reference, zaznamená len číslo objektu a generáciu, označí príslušný slot a vloží ho do FIFO fronty namiesto okamžitého dereferencovania, čím udrží hlboké stromy stránok a dlhé reťazce osnovy mimo call stacku a zabráni dvojitému dekódovaniu toho istého objektu. Priame slovníky, polia a stream slovníky idú do druhej fronty strážené množinou navštívených, pretože skutočné dokumenty obsahujú skutočné cykly: /Parent stránky odkazuje späť na svoj uzol v strome stránok a položky osnovy sa reťazia cez /Prev a /Next oboma smermi. Čísla generácie sú súčasťou zhody, nie dekorácia. Referencia sa vyrieši len vtedy, keď sa zhoduje číslo objektu aj generácia; referencia na číslo, ktoré existuje v inej generácii, sa považuje za null objekt, ako to vyžaduje špecifikácia, nikdy nie za živú hranu
Ako zapnúť garbage collection pri ukladaní?
Garbage collection je opt-in a patrí do záznamu možností uloženia. Predvolene je False, pretože kolektor je deštruktívny prechod cez graf objektov a žiadna knižnica by nemala potichu mazať objekty, ktoré ju volajúci nikdy nepožiadal preskúmať
var
Pdf: TPDFlib;
Opt: TPDFlibSaveOptions;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('report-500pages.pdf', '') <> 1 then
Exit;
Pdf.DeletePages(11, 490); // keep the first ten pages
FillChar(Opt, SizeOf(Opt), 0);
Opt.CompressContent := True;
Opt.CompressFonts := True;
Opt.OptimizeContentStreams := True;
Opt.PackObjectStreams := True;
Opt.GarbageCollect := True; // drop everything the pages left behind
Pdf.SaveToFileOptions('report-10pages.pdf', Opt);
finally
Pdf.Free;
end;
end;
Dva ďalšie vstupné body vedú k rovnakému kolektoru. SetGarbageCollect(1) nastaví príznak na vybranom dokumente, takže bežný SaveToFile ho rešpektuje, a GarbageCollectObjects spustí prechod okamžite a vráti počet odstránených osirotených indirect objektov. Okamžitú formu použite, keď chcete číslo na logovanie alebo assert, a oplatí sa ju kontrolovať, pretože záporná návratová hodnota nie je počet
var
Removed: Integer;
begin
Pdf.DeletePages(11, 490);
Removed := Pdf.GarbageCollectObjects;
if Removed < 0 then
// The graph could not be fully decoded. Nothing was swept and the
// document is unchanged; save it without GC or reject the input.
LogWarning('object graph incomplete, GC skipped')
else
LogInfo(Format('reclaimed %d orphaned objects', [Removed]));
end;
Táto zlyhaná vetva je dôležitejšia, než sa zdá. Objekty sa dekódujú lenivo (lazy) a objekt, ktorý ešte nikdy nebol dekódovaný, neodhaľuje žiadne referencie vôbec. Keby kolektor traktoval nedekódovateľný objekt ako prázdny uzol, zametol by preč všetko, čo je dosiahnuteľné len cez neho. Preto prechod pri dotyku každého objektu vynúti dekódovanie a jediná chyba dekódovania preruší celý prechod so záporným výsledkom a ponechá dokument bajtovo identický. Zametanie grafu, ktorému len čiastočne rozumiete, je spôsob, ako sa z poškodeného súboru stane zničený
Čo pokazí naivný PDF kolektor?
Dva detaily, a oba zlyhávajú potichu, nie nahlas. Prvým sú object streamy. Od PDF 1.5 môže non-stream objekt žiť skomprimovaný vo vnútri kontajnera /ObjStm (§7.5.7) a jeho cross-reference záznam je typ 2, ktorý pomenúva kontajner plus index vnútri neho. Skomprimovaný objekt je teda dosiahnuteľný len cez svoj kontajner. Označte člena, zameťte kontajner, pretože naň nič neodkazovalo ako na objekt dokumentu, a máte zapísaný súbor, ktorého xref ukazuje do objektu, ktorý už neexistuje. Kontajner je štrukturálny storage, nie dáta dokumentu, takže sa nikdy neobjaví ako hrana v grafe objektov, ktorý prechádzate. losLab PDF Library to rieši tak, že odpojí každého prežívajúceho skomprimovaného člena od jeho zdrojového kontajnera skôr, než kontajnery zmiznú, po čom uloženie zabalí prežívajúcich do nových object streamov. Druhým detailom je to, na čo stream objekt naozaj odkazuje. Bajty nie sú súčasťou grafu. Content stream, ktorý kreslí text pomocou /F1 12 Tf, pomenúva font podľa mena resource, a toto meno sa vyrieši cez slovník stránky /Resources, takže hrana dosiahnuteľnosti vedie stránka → /Resources → /Font → objekt fontu, nikdy nie cez obsah streamu. Jediné referencie, ktoré stream prispieva, pochádzajú z jeho slovníka, kde /Length, /Filter a /DecodeParms smú byť všetky indirect. Kolektor, ktorý parsuje bajty streamu a hľadá referencie, robí zbytočne drahú prácu; kolektor, ktorý preskočí stream slovníky, stratí objekt dĺžky a poškodí súbor
Čo sa stane s číslami objektov, ktoré uvoľníte
Stanú sa voľnými záznamami a v tom istom uložení sa neznovu použijú. Sweep prechádza zoznam objektov v zostupnom poradí, aby vymazania zostali stabilné z hľadiska indexu, na konci rebuild-ne vyhľadávací index raz namiesto po každom odstránení, a pre každý odstránený objekt zaznamená číslo do zoznamu voľných s jeho generáciou zvýšenou o jedna, presne ako predpisuje §7.5.4 pre záznam, ktorý sa neskôr môže znovu použiť. Generácia, ktorá je už na 65535, tam ostáva, čím sa dané číslo trvalo vyraďuje. Čísla objektov sa zámerne nekompaktujú. Po zbere súbor ponecháva diery: objekt 12 môže byť voľný, kým 13 a 14 sa používajú, a trailer /Size stále hlási najvyššie číslo plus jedna, nie počet preživších. To je legálne a normálne. Prečíslovanie by ušetrilo hrsť bajtov v cross-reference tabuľke a vyžadovalo by prepísanie každej referencie v dokumente, čo je typ zmeny, ktorá potichu znehodnotí čokoľvek, čo drží čísla objektov mimo súboru. Veľkosť, ktorú získate späť, pochádza z tiel objektov, nie z xref tabuľky
Kedy kolektor nesmiete spustiť
Nikdy pri inkrementálnej aktualizácii. Kolektor je viazaný na plné uloženia a príznak sa jednoducho nečíta, keď sa do dokumentu pripisuje, a táto podmienka nie je obmedzenie, ktoré treba obchádzať. Inkrementálna aktualizácia (§7.5.6) ponecháva pôvodné bajty nedotknuté a pripája novú cross-reference sekciu reťazenú na predchádzajúcu cez /Prev. Každá skoršia revízia stále odkazuje na objekty, na ktoré vždy odkazovala, takže objekt, ktorý je v aktuálnej revízii nedosiahnuteľný, je v staršej revízii úplne dosiahnuteľný. Jeho vymazanie by pokazilo každú revíziu okrem poslednej a mechanika toho, prečo, je pokrytá v článku o inkrementálnych aktualizáciách a ukladaní v append móde. Rovnaká úvaha vylučuje garbage collection na podpísanom dokumente, pretože plné prepísanie, ktoré zber umožňuje, je práve tým, čo znehodnocuje podpis
Oplatí sa tiež jasne povedať, čím zber nie je. Nie je to sanitizer. Kolektor odstraňuje objekty, na ktoré nič neodkazuje; nemá žiadny názor na to, či bol ich obsah citlivý, a objekt, na ktorý sa stále odkazuje, ostáva čímkoľvek bol. Ak je cieľom urobiť informácie nenávratne stratenými namiesto zmenšenia súboru, graf objektov je nesprávna vrstva a správnym nástrojom je redakcia na úrovni inštrukcií a sanitizácia dokumentu. Tieto dva postupy sa dobre skladajú v tomto poradí: najprv redigovať a sanitizovať, potom zbierať, aby objekty, ktoré redakcia odpojila, skutočne opustili súbor. Rovnaké párovanie existuje v API na čistenie resources, kde odovzdanie voľby garbage-collect spôsobí, že čistenie následne spustí zber a nahlási odstránené osirotelé objekty v OrphanObjectsRemoved
Ešte jeden zvyk, ktorý sa oplatí osvojiť. Logujte návratovú hodnotu GarbageCollectObjects v akejkoľvek dávkovej úlohe, ktorá vykonáva vaše mazanie stránok, a sledujte ju niekoľko týždňov na skutočných dokumentoch. Nula na súbore, ktorý ste práve prepolili, znamená, že niečo vyššie v reťazci stále drží referenciu, ktorú ste nečakali, zvyčajne položku v name tree, cieľ osnovy alebo pole AcroForm, ktoré prežilo stránku, ku ktorej bolo pripojené. Kolektor je najlacnejší debugger dosiahnuteľnosti, aký kedy budete mať, pretože odpovedá na otázku, ktorú samotný formát PDF odmieta zodpovedať
Garbage collector, záznam možností uloženia a API na čistenie resources popísané tu sú súčasťou losLab PDF Library pre Delphi a C++Builder, ktorej produktová stránka nesie kompletnú referenciu pipeline ukladania vrátane interakcie medzi zberom, packovaním object streamov a linearizáciou