Ștergerea unei pagini dintr-un PDF nu-i șterge fonturile, imaginile sau stream-urile de conținut. losLab PDF Library le recuperează cu un colector mark-sweep care parcurge graful de obiecte înainte, pornind de la rădăcinile din trailer, și elimină fiecare obiect indirect pe care nimic nu-l atinge. Rulează la o salvare completă, e dezactivat implicit, și returnează numărul de obiecte pe care le-a eliminat
De ce ștergerea paginilor PDF nu micșorează fișierul?
Pentru că ștergerea de pagină e o editare de referință, nu o operație de stocare. DeletePages(StartPage, PageCount) deconectează obiectele de pagină din arborele de pagini și repară intrările de outline care indicau spre ele. Ce nu poate face e să decidă că programul de font, stream-ul de conținut și XObject-ul de imagine pe care le foloseau acele pagini sunt acum moarte, pentru că, la momentul ștergerii, nimic din fișier nu înregistrează cine altcineva ar mai putea indica spre ele. Acele obiecte rămân în lista de obiecte a documentului, iar o salvare completă le scrie pe toate înapoi. Rezultatul e plângerea care pornește majoritatea acestor fire de suport: un client șterge nouăzeci la sută din pagini, salvează, iar fișierul se micșorează cu doi la sută. Mai rău, scurgerea se compune. Încarcă, șterge, salvează, încarcă din nou, șterge din nou, salvează din nou, iar fișierul crește monoton, în timp ce numărul de pagini scade. Asta e o problemă diferită de cea rezolvată de subsetarea fonturilor și downsampling-ul imaginilor, care fac obiectele vii mai mici. Aici obiectele nu sunt prea mari. Pur și simplu nu mai fac parte din document
Setul de rădăcini e trailer-ul, nu arborele de pagini
Graful de obiecte PDF nu are niciun câmp de referință inversă. Formatul nu definește niciun contor de referințe și nicio listă de pointeri înapoi, iar cheile /Parent care chiar există aparțin unor structuri specifice, precum arborele de pagini, nu grafului de obiecte ca întreg. Nimic dintr-un obiect indirect nu-ți spune cine indică spre el, așa că întrebarea „mai folosește cineva obiectul 47” are exact un răspuns: parcurge înainte de la o rădăcină cunoscută și vezi dacă ajungi acolo. De asta colectorul din losLab PDF Library e un colector mark-sweep, nu o schemă de refcount
Rădăcinile vin din trailer-ul fișierului (ISO 32000-1 §7.5.5). Trei chei le poartă: /Root, catalogul documentului din §7.7.2, de care atârnă arborele de pagini, numele, outline-urile, AcroForm și metadatele; /Info, dicționarul de informații al documentului; și /Encrypt, dicționarul de criptare. Celelalte două chei de trailer sunt momeli. /ID e un array de două șiruri de bytes, iar /Prev e un offset întreg în bytes spre secțiunea de cross-reference anterioară. Niciuna nu e o referință indirectă, așa că niciuna nu contribuie cu o rădăcină. losLab PDF Library pune în coadă întregul dicționar de trailer, nu trei chei numite, ceea ce nu costă nimic și ține în viață orice extensie privată de trailer
Parcurgerea în sine e iterativă, nu recursivă. Când traversarea întâlnește o referință indirectă, înregistrează doar numărul de obiect și generația, marchează slot-ul corespunzător și îl pune într-o coadă FIFO, în loc să o dereferențieze imediat, ceea ce ține arborii de pagini adânci și lanțurile lungi de outline departe de stiva de apeluri și oprește decodarea aceluiași obiect de două ori. Dicționarele directe, array-urile și dicționarele de stream merg pe o a doua coadă păzită de un set de vizitare, pentru că documentele reale conțin cicluri genuine: un /Parent de pagină indică înapoi spre nodul lui din arborele de pagini, iar elementele de outline se înlănțuie prin /Prev și /Next în ambele direcții. Numerele de generație fac parte din potrivire, nu sunt decorație. O referință se rezolvă doar când numărul de obiect și generația sunt ambele de acord; o referință la un număr care există la o altă generație e tratată ca obiectul null cerut de specificație, niciodată ca o muchie vie
Cum activezi garbage collection la o salvare?
Garbage collection e opt-in și aparține înregistrării de opțiuni de salvare. Implicit e False, pentru că colectorul e o trecere distructivă peste graful de obiecte, iar nicio bibliotecă n-ar trebui să șteargă în tăcere obiecte pe care apelantul nu i-a cerut niciodată să le examineze
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;
Alte două puncte de intrare ajung la același colector. SetGarbageCollect(1) setează flag-ul pe documentul selectat, astfel încât un SaveToFile obișnuit îl respectă, iar GarbageCollectObjects rulează trecerea imediat și returnează numărul de obiecte indirecte orfane eliminate. Forma imediată e cea de folosit atunci când vrei un număr de logat sau de verificat prin assert, și merită verificată, pentru că o întoarcere negativă nu e o numărătoare
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;
Acel traseu de eșec contează mai mult decât pare. Obiectele sunt decodate leneș, iar un obiect care nu a fost niciodată decodat nu expune deloc referințe. Dacă colectorul ar trata un obiect nedecodabil ca un nod gol, ar mătura tot ce e accesibil doar prin el. Așa că traversarea forțează decodarea pe măsură ce atinge fiecare obiect, iar o singură eroare de decodare abandonează întreaga trecere cu un rezultat negativ și lasă documentul identic byte cu byte. A mătura un graf pe care-l înțelegi doar parțial e cum un colector transformă un fișier deteriorat într-unul distrus
Ce strică un colector PDF naiv?
Două detalii, și ambele eșuează silențios, nu zgomotos. Primul e stream-urile de obiecte. Din PDF 1.5, un obiect care nu e stream poate trăi comprimat în interiorul unui container /ObjStm (§7.5.7), iar intrarea lui de cross-reference e o intrare de tip 2 care numește containerul plus un index în interiorul lui. Un obiect comprimat e deci accesibil doar prin containerul lui. Marchează membrul, mătură containerul pentru că nimic nu l-a referit ca obiect de document, și ai scris un fișier al cărui xref indică spre un obiect care nu mai există. Containerul e stocare structurală, nu date de document, așa că nu apare niciodată ca muchie în graful de obiecte pe care-l parcurgi. losLab PDF Library tratează asta detașând fiecare membru comprimat supraviețuitor din containerul lui sursă înainte ca containerele să dispară, după care salvarea re-împachetează supraviețuitorii în stream-uri de obiecte noi. Al doilea detaliu e la ce referă de fapt un obiect stream. Bytes-ii nu fac parte din graf. Un stream de conținut care desenează text cu /F1 12 Tf numește un font după numele resursei, iar acel nume e rezolvat prin dicționarul /Resources al paginii, așa că muchia de accesibilitate merge pagină → /Resources → /Font → obiect font, niciodată prin payload-ul stream-ului. Singurele referințe pe care le contribuie un stream vin din dicționarul lui, unde /Length, /Filter și /DecodeParms pot fi toate indirecte. Un colector care parsează bytes-ii de stream căutând referințe face o muncă scumpă degeaba; un colector care sare peste dicționarele de stream pierde obiectul de lungime și corupe fișierul
Ce se întâmplă cu numerele de obiect pe care le eliberezi
Devin intrări libere, și nu sunt refolosite în aceeași salvare. Măturarea parcurge lista de obiecte în ordine descrescătoare, astfel încât ștergerile rămân stabile ca index, reconstruiește indexul de căutare o singură dată la sfârșit, în loc de după fiecare eliminare, și pentru fiecare obiect eliminat înregistrează numărul în lista liberă, cu generația incrementată cu unu, exact așa cum specifică §7.5.4 pentru o intrare care poate fi refolosită mai târziu. O generație deja la 65535 rămâne acolo, marcând acel număr ca retras permanent. Numerele de obiect nu sunt compactate, deliberat. După o colectare, fișierul păstrează găuri: obiectul 12 poate fi liber, în timp ce 13 și 14 sunt în uz, iar /Size din trailer tot raportează cel mai mare număr plus unu, nu numărul supraviețuitorilor. Asta e legal și normal. Renumerotarea ar economisi câțiva bytes în tabelul de cross-reference și ar cere rescrierea fiecărei referințe din document, ceea ce e genul de schimbare care invalidează tacit orice ține numere de obiect din exterior. Dimensiunea pe care o primești înapoi vine din corpurile obiectelor, nu din tabelul xref
Când nu trebuie să rulezi colectorul
Niciodată la o actualizare incrementală. Colectorul e restricționat la salvările complete, iar flag-ul pur și simplu nu e citit atunci când documentul e în curs de adăugare, iar acea restricție nu e o limitare de ocolit. O actualizare incrementală (§7.5.6) lasă bytes-ii originali neatinși și adaugă o secțiune nouă de cross-reference înlănțuită la cea anterioară prin /Prev. Fiecare revizie anterioară încă indică spre obiectele spre care a indicat mereu, așa că un obiect inaccesibil în revizia curentă e foarte accesibil într-una mai veche. A-l șterge ar strica fiecare revizie în afară de ultima, iar mecanica acestui lucru e acoperită în articolul despre actualizările incrementale și salvările în mod append. Același raționament exclude garbage collection pe un document semnat, pentru că rescrierea completă care face posibilă colectarea e chiar ce invalidează semnătura
Merită și clarificat ce nu e colectarea. Nu e un sanitizer. Colectorul elimină obiecte pe care nimic nu le referă; nu are nicio opinie despre dacă conținutul lor era sensibil, iar un obiect care e încă referit rămâne orice era. Dacă scopul e să faci informația irecuperabilă, nu să faci fișierul mai mic, graful de obiecte e nivelul greșit, iar redactarea la nivel de instrucțiune și sanitizarea documentului e cel corect. Cele două se compun bine în această ordine: redactează și sanitizează întâi, apoi colectează, astfel încât obiectele pe care redactarea le-a detașat chiar să părăsească fișierul. Aceeași asociere există în API-ul de purjare de resurse, unde a trece opțiunea de garbage-collect face ca purjarea să ruleze o colectare după, și să raporteze orfanii pe care i-a eliminat în OrphanObjectsRemoved
Un ultim obicei demn de adoptat. Loghează valoarea de întoarcere a lui GarbageCollectObjects în orice job de lot care face ștergerile tale de pagini, și urmărește-o pe parcursul câtorva săptămâni de documente reale. Un zero pe un fișier pe care tocmai l-ai tăiat la jumătate înseamnă că ceva mai în amonte încă ține o referință la care nu te așteptai, de obicei o intrare de arbore de nume, o destinație de outline sau un câmp AcroForm care a supraviețuit paginii de care era atașat. Colectorul e cel mai ieftin debugger de accesibilitate pe care-l vei avea vreodată, pentru că răspunde la întrebarea pe care formatul PDF însuși refuză s-o răspundă
Garbage collector-ul, înregistrarea de opțiuni de salvare și API-ul de purjare de resurse descrise aici fac parte din losLab PDF Library pentru Delphi și C++Builder, a cărei pagină de produs conține referința completă a pipeline-ului de salvare, inclusiv interacțiunea dintre colectare, împachetarea de stream-uri de obiecte și linearizare