Brisanje stranice iz PDF-a ne briše njene fontove, slike ni tokove sadržaja. losLab PDF Library ih oslobađa mark-sweep kolektorom koji obilazi graf objekata unapred, počev od korenova u trejleru, i uklanja svaki indirektni objekat do kog ništa više ne vodi. Pokreće se pri punom čuvanju, podrazumevano je isključen, i vraća broj objekata koje je uklonio
Zašto brisanje PDF stranica ne smanjuje fajl?
Zato što je brisanje stranice izmena reference, a ne operacija nad skladištem. DeletePages(StartPage, PageCount) odvezuje objekte stranica od stabla stranica i popravlja stavke sadržaja koje su na njih upućivale. Ono što ne može da uradi jeste da odluči da su font program, tok sadržaja i XObject slike koje su te stranice koristile sada mrtvi, jer u trenutku brisanja ništa u fajlu ne beleži ko još možda upućuje na njih. Ti objekti ostaju u listi objekata dokumenta, a puno čuvanje sve njih ponovo ispisuje. Rezultat je pritužba kojom počinje većina ovih support niti: korisnik obriše devedeset procenata stranica, sačuva, a fajl se smanji za dva procenta. Gore od toga, curenje se gomila. Učitaj, obriši, sačuvaj, ponovo učitaj, ponovo obriši, ponovo sačuvaj — fajl monotono raste dok broj stranica pada. Ovo je drugačiji problem od onog koji rešavaju subsetovanje fontova i smanjenje rezolucije slika, koji čine žive objekte manjim. Ovde objekti nisu preveliki. Oni jednostavno više nisu deo dokumenta
Koren je trejler, ne stablo stranica
Graf PDF objekata nema polje za povratnu referencu. Format ne definiše brojač referenci niti listu povratnih pokazivača, a ključevi /Parent koji postoje pripadaju konkretnim strukturama poput stabla stranica, ne grafu objekata kao celini. Ništa u indirektnom objektu ne govori ko na njega upućuje, pa pitanje „da li iko još koristi objekat 47" ima tačno jedan odgovor: krenuti unapred od poznatog korena i videti da li se do njega stiže. Zato je kolektor u losLab PDF Library mark-sweep kolektor, a ne šema sa brojanjem referenci
Korenovi dolaze iz trejlera fajla (ISO 32000-1 §7.5.5). Tri ključa ih nose: /Root, katalog dokumenta iz §7.7.2 o koji vise stablo stranica, imena, sadržaj, AcroForm i metapodaci; /Info, rečnik informacija o dokumentu; i /Encrypt, rečnik enkripcije. Preostala dva ključa trejlera su mamci. /ID je niz od dva bajt-stringa, a /Prev je celobrojni bajt-ofset ka prethodnoj cross-reference sekciji. Nijedan od njih nije indirektna referenca, pa nijedan ne doprinosi korenu. losLab PDF Library stavlja u red čekanja ceo rečnik trejlera umesto tri imenovana ključa, što ništa ne košta i drži živom svaku privatnu ekstenziju trejlera
Sam obilazak je iterativan, ne rekurzivan. Kada obilazak naiđe na indirektnu referencu, beleži samo broj objekta i generaciju, označava odgovarajući slot i gura ga u FIFO red umesto da ga odmah dereferencira, čime duboka stabla stranica i dugi lanci sadržaja ostaju van call stacka i sprečava se dvostruko dekodiranje istog objekta. Direktni rečnici, nizovi i rečnici tokova idu u drugi red čuvan skupom posećenih, jer stvarni dokumenti sadrže prave cikluse: /Parent stranice pokazuje nazad na svoj čvor u stablu stranica, a stavke sadržaja se lančano povezuju kroz /Prev i /Next u oba smera. Brojevi generacije su deo poklapanja, ne ukras. Referenca se razrešava samo kada se broj objekta i generacija oba slažu; referenca na broj koji postoji u drugoj generaciji tretira se kao null objekat koji specifikacija zahteva, nikada kao živa grana
Kako se uključuje sakupljanje smeća pri čuvanju?
Sakupljanje smeća je opt-in i pripada zapisu opcija čuvanja. Podrazumevano je False jer je kolektor destruktivan prolazak kroz graf objekata i nijedna biblioteka ne bi trebalo ćutke da briše objekte koje je pozivalac nikada nije tražio da ispita
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;
Dve druge ulazne tačke dovode do istog kolektora. SetGarbageCollect(1) postavlja zastavicu na izabrani dokument tako da je obična SaveToFile poštuje, a GarbageCollectObjects odmah pokreće prolaz i vraća broj uklonjenih napuštenih indirektnih objekata. Neposredni oblik je onaj koji treba koristiti kada želite broj za logovanje ili assert, a vredi ga proveriti, jer negativan povratak nije broj
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;
Ta putanja neuspeha znači više nego što deluje. Objekti se dekodiraju lenjo, a objekat koji nikada nije dekodiran ne otkriva nijednu referencu. Kada bi kolektor tretirao objekat koji se ne može dekodirati kao prazan čvor, pomeo bi sve što je dostupno samo kroz njega. Zato obilazak forsira dekodiranje pri svakom dodiru objekta, a jedna jedina greška u dekodiranju prekida ceo prolaz sa negativnim rezultatom i ostavlja dokument bajt-identičan. Čišćenje grafa koji samo delimično razumete je način na koji kolektor pretvara oštećen fajl u uništen
Šta lomi naivan PDF kolektor?
Dva detalja, i oba otkazuju tiho, a ne glasno. Prvi su tokovi objekata. Od PDF 1.5, ne-tok objekat može da živi kompresovan unutar /ObjStm kontejnera (§7.5.7), a njegova cross-reference stavka je stavka tipa 2 koja imenuje kontejner plus indeks unutar njega. Kompresovan objekat je stoga dostupan samo kroz svoj kontejner. Označite člana, počistite kontejner jer ga ništa nije referenciralo kao objekat dokumenta, i dobili ste fajl čiji xref pokazuje na objekat koji više ne postoji. Kontejner je strukturno skladište, ne podatak dokumenta, pa se nikada ne pojavljuje kao grana u grafu objekata koji obilazite. losLab PDF Library ovo rešava tako što odvaja svakog preživelog kompresovanog člana od izvornog kontejnera pre nego što kontejneri nestanu, nakon čega čuvanje ponovo pakuje preživele u sveže tokove objekata. Drugi detalj je na šta tok objekta zapravo upućuje. Bajtovi nisu deo grafa. Tok sadržaja koji crta tekst sa /F1 12 Tf imenuje font po imenu resursa, a to ime se razrešava kroz rečnik /Resources stranice, pa grana dostupnosti ide stranica → /Resources → /Font → objekat fonta, nikada kroz sadržaj toka. Jedine reference koje tok doprinosi dolaze iz njegovog rečnika, gde su /Length, /Filter i /DecodeParms svi dozvoljeni da budu indirektni. Kolektor koji parsira bajtove toka tražeći reference radi skup posao uzalud; kolektor koji preskače rečnike tokova gubi objekat dužine i kvari fajl
Šta se dešava sa brojevima objekata koje oslobodite
Postaju slobodne stavke, i ne koriste se ponovo u istom čuvanju. Čišćenje obilazi listu objekata u opadajućem redosledu tako da brisanja ostaju indeksno stabilna, ponovo izgrađuje indeks pretrage jednom na kraju umesto posle svakog uklanjanja, i za svaki uklonjeni objekat beleži broj u listi slobodnih sa generacijom uvećanom za jedan, tačno kako §7.5.4 propisuje za stavku koja kasnije može biti ponovo iskorišćena. Generacija koja je već na 65535 tu i ostaje, obeležavajući taj broj kao trajno povučen. Brojevi objekata se namerno ne sabijaju. Posle sakupljanja fajl zadržava rupe: objekat 12 može biti slobodan dok su 13 i 14 u upotrebi, a trejler /Size i dalje prijavljuje najviši broj plus jedan, a ne broj preživelih. To je legalno i normalno. Prenumerisanje bi uštedelo šačicu bajtova u cross-reference tabeli i zahtevalo bi prepisivanje svake reference u dokumentu, što je vrsta izmene koja tiho ukida sve što izvan dokumenta drži brojeve objekata. Veličina koju dobijete potiče od tela objekata, ne od xref tabele
Kada kolektor ne smete pokrenuti
Nikada pri inkrementalnom ažuriranju. Kolektor je ograničen na puna čuvanja i zastavica se jednostavno ne čita kada se dokumentu dodaje sadržaj, a to ograničenje nije prepreka koju treba zaobići. Inkrementalno ažuriranje (§7.5.6) ostavlja originalne bajtove netaknutim i dodaje novu cross-reference sekciju ulančanu na prethodnu preko /Prev. Svaka ranija revizija i dalje pokazuje na objekte na koje je oduvek pokazivala, pa objekat koji je nedostupan u trenutnoj reviziji je itekako dostupan u nekoj starijoj. Njegovo brisanje bi pokvarilo svaku reviziju osim poslednje, a mehanika toga je obrađena u članku o inkrementalnim ažuriranjima i čuvanju u append-modu. Isto rezonovanje isključuje sakupljanje smeća na potpisanom dokumentu, jer je puno prepisivanje koje omogućava sakupljanje samo po sebi ono što poništava potpis
Vredi takođe jasno reći šta sakupljanje nije. Nije sanitizer. Kolektor uklanja objekte na koje ništa ne upućuje; nema stav o tome da li je njihov sadržaj bio osetljiv, a objekat na koji se i dalje upućuje ostaje šta god da je bio. Ako je cilj da se informacija učini nepovrativom, a ne da se fajl smanji, graf objekata je pogrešan sloj, a redakcija na nivou instrukcija i sanitizacija dokumenta je pravi. Njih dvoje se dobro slažu u tom redosledu: prvo redakcija i sanitizacija, pa onda sakupljanje, tako da objekti koje je redakcija odvojila zaista napuste fajl. Isto uparivanje postoji u API-ju za čišćenje resursa, gde prosleđivanje opcije garbage-collect čini da čišćenje potom pokrene sakupljanje i prijavi uklonjene napuštene objekte u OrphanObjectsRemoved
Poslednja navika vredna usvajanja. Logujte povratnu vrednost GarbageCollectObjects u bilo kom batch poslu koji obavlja vaša brisanja stranica, i pratite je nekoliko nedelja na stvarnim dokumentima. Nula na fajlu koji ste upravo prepolovili znači da nešto uzvodno i dalje drži referencu koju niste očekivali, obično stavku u name tree, odredište u sadržaju ili AcroForm polje koje je preživelo stranicu na koju je bilo vezano. Kolektor je najjeftiniji debager dostupnosti koji ćete ikada imati, jer odgovara na pitanje na koje sam PDF format odbija da odgovori
Sakupljač smeća, zapis opcija čuvanja i API za čišćenje resursa opisani ovde deo su losLab PDF Library za Delphi i C++Builder, čija proizvodna stranica nosi kompletnu referencu save-pipeline-a, uključujući interakciju između sakupljanja, pakovanja tokova objekata i linearizacije