Brisanje stranice iz PDF-a ne briše njezine fontove, slike ni tokove sadržaja. losLab PDF Library ih vraća pomoću mark-sweep sakupljača koji prolazi kroz graf objekata unaprijed od korijena trailera i uklanja svaki indirektni objekt koji ništa više ne referencira. Pokreće se pri punom spremanju, prema zadanim postavkama je isključen i vraća broj objekata koje je uklonio
Zašto brisanje PDF stranica ne smanjuje datoteku?
Zato što je brisanje stranice uređivanje referenci, a ne operacija nad pohranom. DeletePages(StartPage, PageCount) odspaja objekte stranica iz stabla stranica i popravlja unose obrisa koji su na njih pokazivali. Ono što ne može učiniti jest odlučiti da su font, tok sadržaja i XObject slike koje su te stranice koristile sada mrtvi, jer u trenutku brisanja ništa u datoteci ne bilježi tko još možda na njih pokazuje. Ti objekti ostaju u popisu objekata dokumenta, a puno spremanje ih sve ponovno zapisuje. Rezultat je pritužba koja pokreće većinu ovih niti podrške: klijent obriše devedeset posto stranica, spremi datoteku, a ona se smanji za dva posto. Gore od toga, curenje se akumulira. Učitaj, obriši, spremi, ponovno učitaj, ponovno obriši, ponovno spremi — datoteka monotono raste dok broj stranica pada. Ovo je drukčiji problem od onog koji rješavaju podskupljanje fontova i smanjenje razlučivosti slika, koji čine žive objekte manjima. Ovdje objekti nisu preveliki. Oni jednostavno više nisu dio dokumenta
Korijenski skup je trailer, a ne stablo stranica
Graf PDF objekata nema polje obrnute reference. Format ne definira brojač referenci niti popis povratnih pokazivača, a ključevi /Parent koji postoje pripadaju specifičnim strukturama poput stabla stranica, a ne grafu objekata kao cjelini. Ništa u indirektnom objektu ne govori tko na njega pokazuje, pa pitanje "koristi li još netko objekt 47" ima točno jedan odgovor: prolazak unaprijed od poznatog korijena i provjera stiže li se do njega. Zato je sakupljač u losLab PDF Library mark-sweep sakupljač, a ne shema brojanja referenci
Korijeni dolaze iz trailera datoteke (ISO 32000-1 §7.5.5). Tri ključa ih nose: /Root, katalog dokumenta iz §7.7.2 iz kojeg vise stablo stranica, imena, obrisi, AcroForm i metapodaci; /Info, rječnik s informacijama o dokumentu; te /Encrypt, rječnik enkripcije. Preostala dva ključa trailera su varke. /ID je polje od dva niza bajtova, a /Prev je cjelobrojni pomak bajtova do prethodne cross-reference sekcije. Nijedan od njih nije indirektna referenca, pa nijedan ne pridonosi korijenu. losLab PDF Library postavlja u red čekanja cijeli rječnik trailera umjesto tri imenovana ključa, što ništa ne košta, a održava živim svako privatno proširenje trailera
Sam prolazak je iterativan, a ne rekurzivan. Kad prolazak naiđe na indirektnu referencu, bilježi samo broj objekta i generaciju, označava odgovarajući slot i stavlja ga u FIFO red čekanja umjesto trenutne dereferencijacije, čime se dubine stabala stranica i dugački lanci obrisa drže izvan poziva funkcija i sprječava dvostruko dekodiranje istog objekta. Direktni rječnici, polja i rječnici tokova idu u drugi red zaštićen skupom posjećenih, jer stvarni dokumenti sadrže prave cikluse: /Parent stranice pokazuje natrag na njezin čvor stabla stranica, a stavke obrisa lančano se povezuju kroz /Prev i /Next u oba smjera. Brojevi generacije su dio podudaranja, a ne ukras. Referenca se razrješava samo kad se broj objekta i generacija oba podudaraju; referenca na broj koji postoji u drugoj generaciji tretira se kao null objekt koji specifikacija zahtijeva, nikada kao živi rub
Kako se uključuje sakupljanje smeća prilikom spremanja?
Sakupljanje smeća je opcionalno i pripada zapisu opcija spremanja. Zadana vrijednost je False jer je sakupljač destruktivni prolaz kroz graf objekata i nijedna biblioteka ne bi trebala tiho brisati objekte koje pozivatelj nikad nije zatražio da se pregledaju
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;
Još dvije ulazne točke pristupaju istom sakupljaču. SetGarbageCollect(1) postavlja zastavicu na odabranom dokumentu tako da je uobičajeni SaveToFile poštuje, a GarbageCollectObjects odmah pokreće prolaz i vraća broj uklonjenih napuštenih indirektnih objekata. Trenutni oblik je onaj koji koristite kad želite broj za bilježenje ili provjeru, a vrijedi ga provjeriti, jer negativan povrat 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;
Taj put neuspjeha važniji je nego što izgleda. Objekti se dekodiraju lijeno, a objekt koji nikad nije dekodiran ne izlaže nikakve reference. Kad bi sakupljač tretirao nedekodirani objekt kao prazan čvor, pomeo bi sve što je dostupno samo kroz njega. Zato prolazak prisilno dekodira svaki objekt čim ga dotakne, a jedna pogreška dekodiranja prekida cijeli prolaz s negativnim rezultatom i ostavlja dokument bajtno identičnim. Čišćenje grafa koji tek djelomično razumijete jest način na koji sakupljač oštećenu datoteku pretvori u uništenu
Što ruši naivan PDF sakupljač?
Dvije pojedinosti, i obje ne uspijevaju tiho umjesto glasno. Prva su tokovi objekata. Od PDF-a 1.5 objekt koji nije tok može živjeti komprimiran unutar kontejnera /ObjStm (§7.5.7), a njegov unos cross-reference tablice je unos tipa 2 koji imenuje kontejner plus indeks unutar njega. Komprimirani objekt je stoga dostupan samo kroz svoj kontejner. Označite člana, pomedite kontejner jer ništa na njega nije referenciralo kao dokumentski objekt, i imate zapisanu datoteku čiji xref pokazuje u objekt koji više ne postoji. Kontejner je strukturna pohrana, a ne podaci dokumenta, pa se nikad ne pojavljuje kao rub u grafu objekata kroz koji prolazite. losLab PDF Library ovo rješava odvajanjem svakog preživjelog komprimiranog člana od izvornog kontejnera prije nego kontejneri nestanu, nakon čega spremanje ponovno pakira preživjele u svježe tokove objekata. Druga pojedinost je što tok objekta zapravo referencira. Bajtovi nisu dio grafa. Tok sadržaja koji crta tekst pomoću /F1 12 Tf imenuje font po imenu resursa, a to ime se razrješava kroz rječnik /Resources stranice, pa rub dohvatljivosti ide stranica → /Resources → /Font → objekt fonta, nikad kroz sadržaj toka. Jedine reference koje tok pridonosi dolaze iz njegova rječnika, gdje /Length, /Filter i /DecodeParms mogu biti indirektni. Sakupljač koji parsira bajtove toka tražeći reference obavlja skup posao uzalud; sakupljač koji preskače rječnike tokova gubi objekt duljine i oštećuje datoteku
Što se događa s brojevima objekata koje oslobodite
Postaju slobodni unosi i ne ponovno se koriste u istom spremanju. Prolazak čišćenja prolazi kroz popis objekata silaznim redoslijedom kako bi brisanja ostala stabilna po indeksu, ponovno gradi indeks pretraživanja jednom na kraju umjesto nakon svakog uklanjanja, a za svaki uklonjeni objekt bilježi broj u popisu slobodnih s generacijom uvećanom za jedan, točno kako §7.5.4 propisuje za unos koji se kasnije može ponovno iskoristiti. Generacija koja je već na 65535 tamo ostaje, označavajući taj broj kao trajno povučen. Brojevi objekata namjerno se ne sažimaju. Nakon čišćenja datoteka zadržava rupe: objekt 12 može biti slobodan dok su 13 i 14 u upotrebi, a trailer /Size i dalje prijavljuje najviši broj plus jedan, a ne broj preživjelih. To je legalno i normalno. Ponovno numeriranje uštedjelo bi šačicu bajtova u cross-reference tablici, a zahtijevalo bi prepisivanje svake reference u dokumentu, što je vrsta promjene koja tiho poništava sve što izvan datoteke drži brojeve objekata. Veličina koju dobijete dolazi iz tijela objekata, a ne iz xref tablice
Kad sakupljač ne smijete pokrenuti
Nikad na inkrementalnom ažuriranju. Sakupljač je ograničen na puna spremanja, a zastavica se jednostavno ne čita kad se dokument nadopisuje, i to ograničenje nije nešto što treba zaobići. Inkrementalno ažuriranje (§7.5.6) ostavlja izvorne bajtove netaknutima i dodaje novu cross-reference sekciju uvezanu na prethodnu preko /Prev. Svaka ranija revizija i dalje pokazuje na objekte na koje je uvijek pokazivala, pa je objekt koji je nedostupan u trenutnoj reviziji vrlo dostupan u starijoj. Njegovim brisanjem pokvarile bi se sve revizije osim posljednje, a mehanika toga obrađena je u članku o inkrementalnim ažuriranjima i spremanju u načinu dodavanja. Isto rezoniranje isključuje sakupljanje smeća na potpisanom dokumentu, jer je puno prepisivanje koje omogućuje čišćenje upravo ono što poništava potpis
Vrijedi i jasno reći što čišćenje nije. Nije sanitizator. Sakupljač uklanja objekte koje ništa ne referencira; nema mišljenje o tome je li njihov sadržaj bio osjetljiv, a objekt koji se i dalje referencira ostaje ono što je i bio. Ako je cilj učiniti informacije neobnovljivima, a ne samo datoteku manjom, graf objekata je pogrešan sloj, a redakcija na razini instrukcija i sanitizacija dokumenta pravi je alat. Ta dva se dobro nadopunjuju u tom redoslijedu: prvo redigirajte i sanitizirajte, zatim čistite, tako da objekti koje je redakcija odspojila stvarno napuste datoteku. Isto sparivanje postoji u API-ju za čišćenje resursa, gdje prosljeđivanje opcije garbage-collect pokreće čišćenje nakon toga i prijavljuje uklonjene napuštene objekte u OrphanObjectsRemoved
Još jedna navika vrijedna usvajanja. Bilježite povratnu vrijednost GarbageCollectObjects u bilo kojem batch poslu koji obavlja brisanje stranica i promatrajte je nekoliko tjedana na stvarnim dokumentima. Nula na datoteci koju ste upravo prepolovili znači da nešto uzvodno i dalje drži referencu koju niste očekivali, obično stavku u stablu imena, odredište obrisa ili polje AcroForma koje je preživjelo stranicu na koju je bilo pričvršćeno. Sakupljač je najjeftiniji debugger dostupnosti koji ćete ikad imati, jer odgovara na pitanje koje sam PDF format odbija odgovoriti
Sakupljač smeća, zapis opcija spremanja i API za čišćenje resursa opisani ovdje dio su losLab PDF Library za Delphi i C++Builder, čija produktna stranica sadrži potpunu referencu procesa spremanja uključujući interakciju čišćenja, pakiranja tokova objekata i linearizacije