Smazání stránky z PDF nesmaže její fonty, obrázky ani content streamy. losLab PDF Library je uvolňuje pomocí mark-sweep kolektoru, který prochází graf objektů dopředu od kořenů v trailer souboru a odstraňuje každý nepřímý objekt, ke kterému už nic neukazuje. Spouští se při full save, ve výchozím stavu je vypnutý a vrací počet objektů, které odstranil
Proč mazání stránek PDF nezmenší soubor?
Protože mazání stránky je úprava referencí, nikoli operace nad úložištěm. DeletePages(StartPage, PageCount) odpojí objekty stránek od stromu stránek a opraví položky osnovy (outline), které na ně odkazovaly. Co ale neumí, je rozhodnout, že fontový program, content stream a obrázkový XObject, které tyto stránky používaly, jsou nyní mrtvé, protože v okamžiku smazání nic v souboru nezaznamenává, kdo na ně ještě může odkazovat. Tyto objekty zůstávají v seznamu objektů dokumentu a full save je všechny znovu zapíše zpět. Výsledkem je stížnost, kterou začíná většina těchto podpůrných vláken: zákazník smaže devadesát procent stránek, uloží soubor a ten se zmenší o dvě procenta. Horší je, že tento únik se kumuluje. Načtení, smazání, uložení, znovu načtení, znovu smazání, znovu uložení – a soubor monotónně roste, zatímco počet stránek klesá. Jde o jiný problém, než jaký řeší subsetting fontů a downsampling obrázků, které zmenšují živé objekty. Zde objekty nejsou příliš velké. Prostě už nejsou součástí dokumentu
Kořenová množina je trailer, nikoli strom stránek
Graf objektů PDF nemá žádné pole se zpětnou referencí. Formát nedefinuje ani počet referencí, ani seznam zpětných ukazatelů, a klíče /Parent, které existují, patří ke konkrétním strukturám, jako je strom stránek, nikoli ke grafu objektů jako celku. Nic v nepřímém objektu neříká, kdo na něj ukazuje, takže otázka „používá ještě někdo objekt 47“ má přesně jednu odpověď: projít dopředu od známého kořene a zjistit, zda se k němu dostaneme. Proto je kolektor v losLab PDF Library typu mark-sweep, a nikoli refcount
Kořeny pocházejí z trailer souboru (ISO 32000-1 §7.5.5). Nesou je tři klíče: /Root, katalog dokumentu dle §7.7.2, na kterém visí strom stránek, jména, osnova, AcroForm i metadata; /Info, informační slovník dokumentu; a /Encrypt, šifrovací slovník. Zbylé dva klíče traileru jsou klamné. /ID je pole dvou bajtových řetězců a /Prev je celočíselný bajtový offset na předchozí sekci cross-reference. Ani jeden z nich není nepřímá reference, takže ani jeden nepřispívá kořenem. losLab PDF Library zařazuje do fronty celý slovník traileru místo tří pojmenovaných klíčů, což nic nestojí a udržuje naživu i případné soukromé rozšíření traileru
Samotný průchod je iterativní, nikoli rekurzivní. Když traverzace narazí na nepřímou referenci, zaznamená pouze číslo objektu a generaci, označí odpovídající slot a vloží jej do FIFO fronty místo okamžité dereference, což drží hluboké stromy stránek a dlouhé řetězce osnovy mimo zásobník volání a zabraňuje opakovanému dekódování téhož objektu. Přímé slovníky, pole a slovníky streamů jdou do druhé fronty hlídané množinou navštívených prvků, protože reálné dokumenty obsahují skutečné cykly: /Parent stránky ukazuje zpět na svůj uzel ve stromu stránek a položky osnovy se řetězí přes /Prev a /Next oběma směry. Čísla generací jsou součástí shody, nikoli ozdobou. Reference se vyřeší jen tehdy, když se shodují číslo objektu i generace zároveň; reference na číslo, které existuje v jiné generaci, se považuje za null objekt požadovaný specifikací, nikdy za živou hranu
Jak zapnout garbage collection při ukládání?
Garbage collection je opt-in a patří do záznamu save options. Ve výchozím stavu je False, protože kolektor je destruktivní průchod grafem objektů a žádná knihovna by neměla potichu mazat objekty, které volající nikdy nepožádal prozkoumat
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;
Ke stejnému kolektoru vedou ještě další dva vstupní body. SetGarbageCollect(1) nastaví příznak na vybraném dokumentu, takže jej běžné SaveToFile respektuje, a GarbageCollectObjects spustí průchod okamžitě a vrátí počet odstraněných osiřelých nepřímých objektů. Okamžitou formu použijte, když chcete číslo zalogovat nebo na něm postavit assert, a stojí za to jej kontrolovat, protože záporná návratová hodnota není 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;
Tato chybová cesta je důležitější, než se zdá. Objekty se dekódují líně a objekt, který ještě nebyl dekódován, nevystavuje vůbec žádné reference. Kdyby kolektor považoval nedekódovatelný objekt za prázdný uzel, smetl by vše, co je dosažitelné jen skrze něj. Traverzace proto při dotyku s každým objektem vynutí jeho dekódování a jediná chyba dekódování celý průchod přeruší se záporným výsledkem a dokument zůstane bajtově nezměněný. Smetat graf, kterému jen částečně rozumíte, je způsob, jak kolektor promění poškozený soubor ve zničený
Co rozbije naivní PDF kolektor?
Jsou dva detaily a oba selhávají potichu, nikoli nahlas. Prvním jsou object streamy. Od PDF 1.5 může non-stream objekt existovat komprimovaně uvnitř kontejneru /ObjStm (§7.5.7) a jeho záznam v cross-reference je typu 2, pojmenovávající kontejner plus index uvnitř něj. Komprimovaný objekt je proto dosažitelný jen přes svůj kontejner. Označíte-li člena, ale smetete kontejner, protože jej nic neodkazovalo jako objekt dokumentu, napíšete soubor, jehož xref ukazuje do objektu, který už neexistuje. Kontejner je strukturální úložiště, nikoli data dokumentu, takže se nikdy neobjeví jako hrana v grafu objektů, kterým procházíte. losLab PDF Library to řeší tak, že odpojí každého přeživšího komprimovaného člena od jeho zdrojového kontejneru dřív, než kontejnery zaniknou, a poté uložení znovu zabalí přeživší do nových object streamů. Druhým detailem je, na co stream objekt skutečně odkazuje. Bajty nejsou součástí grafu. Content stream, který kreslí text pomocí /F1 12 Tf, pojmenovává font podle názvu zdroje a tento název se řeší přes slovník stránky /Resources, takže hrana dosažitelnosti vede stránka → /Resources → /Font → objekt fontu, nikdy přes obsah streamu. Jediné reference, které stream přispívá, pocházejí z jeho slovníku, kde /Length, /Filter a /DecodeParms smí být nepřímé. Kolektor, který prohledává bajty streamu a hledá v nich reference, dělá drahou práci zbytečně; kolektor, který přeskočí slovníky streamů, přijde o objekt délky a poškodí soubor
Co se stane s uvolněnými čísly objektů
Stávají se z nich volné položky a ve stejném uložení se znovu nepoužijí. Smetání prochází seznam objektů sestupně, aby zůstaly indexy stabilní, přestavuje vyhledávací index jen jednou na konci místo po každém odstranění a pro každý odstraněný objekt zaznamená jeho číslo do seznamu volných s generací zvýšenou o jedna, přesně jak předepisuje §7.5.4 pro položku, která může být později znovu použita. Generace, která už je na 65535, tam zůstává a označuje dané číslo za trvale vyřazené. Čísla objektů se záměrně nekompaktují. Po sběru zůstávají v souboru díry: objekt 12 může být volný, zatímco 13 a 14 se používají, a trailer /Size stále udává nejvyšší číslo plus jedna, nikoli počet přeživších. To je legální a normální. Přečíslování by ušetřilo hrstku bajtů v tabulce cross-reference a vyžadovalo by přepsání každé reference v dokumentu, což je typ změny, která potichu znehodnotí cokoli, co drží čísla objektů zvenčí. Velikost, kterou dostanete zpět, pochází z těl objektů, nikoli z tabulky xref
Kdy kolektor nesmíte spustit
Nikdy při přírůstkové aktualizaci. Kolektor je vázán jen na full save a příznak se při připojování k dokumentu vůbec nečte – a tato zábrana není omezení, které je třeba obcházet. Přírůstková aktualizace (§7.5.6) ponechává původní bajty nedotčené a připojuje novou sekci cross-reference zřetězenou s předchozí přes /Prev. Každá dřívější revize stále ukazuje na objekty, na které vždy ukazovala, takže objekt, který je v aktuální revizi nedosažitelný, je v nějaké starší revizi naprosto dosažitelný. Jeho smazání by rozbilo každou revizi kromě té poslední a mechanika toho, proč tomu tak je, je popsána v článku o přírůstkových aktualizacích a ukládání v append módu. Stejná logika vylučuje garbage collection u podepsaného dokumentu, protože právě úplné přepsání, které sběr umožňuje, je to, co znehodnotí podpis
Stojí také za to ujasnit si, čím sběr není. Není to sanitizer. Kolektor odstraňuje objekty, na které nic neodkazuje; nemá žádný názor na to, zda byl jejich obsah citlivý, a objekt, na který se stále odkazuje, zůstává tím, čím byl. Pokud je cílem učinit informace neobnovitelnými spíše než zmenšit soubor, graf objektů je špatná vrstva a správnou volbou je redakce na úrovni instrukcí a sanitizace dokumentu. Obě techniky se dobře skládají v tomto pořadí: nejprve redakce a sanitizace, poté sběr, aby objekty, které redakce odpojila, skutečně opustily soubor. Stejné spárování existuje i v API pro čištění zdrojů, kde předání volby garbage-collect způsobí, že čištění poté spustí sběr a nahlásí odstraněné sirotky v OrphanObjectsRemoved
Poslední návyk, který stojí za osvojení. Logujte návratovou hodnotu GarbageCollectObjects v dávkové úloze, která provádí mazání stránek, a sledujte ji několik týdnů na reálných dokumentech. Nula u souboru, který jste právě zmenšili na polovinu, znamená, že někde výš stále drží referenci, kterou jste nečekali – obvykle položka name tree, cíl osnovy nebo pole AcroForm, které přežilo stránku, k níž bylo připojeno. Kolektor je nejlevnější debugger dosažitelnosti, jaký kdy budete mít, protože odpovídá na otázku, na kterou samotný formát PDF odmítá odpovědět
Zde popsaný garbage collector, záznam save options a API pro čištění zdrojů jsou součástí losLab PDF Library pro Delphi a C++Builder, jejíž produktová stránka obsahuje kompletní referenci save pipeline včetně interakce mezi sběrem, balením object streamů a linearizací