At slette en side fra en PDF sletter ikke dens fonte, billeder eller indholdsstrømme. losLab PDF Library genvinder dem med en mark-sweep-collector, der gennemgår objektgrafen fremad fra trailer-rødderne og fjerner hvert indirekte objekt, intet når. Den kører ved en fuld gem, den er som standard slået fra, og den returnerer antallet af objekter, den droppede
Hvorfor formindsker sletning af PDF-sider ikke filen?
Fordi sidesletning er en referenceredigering, ikke en lagringsoperation. DeletePages(StartPage, PageCount) afkobler sideobjekter fra sidetræet og reparerer de outline-poster, der pegede på dem. Hvad den ikke kan gøre, er at afgøre, at fontprogrammet, indholdsstrømmen og billed-XObjektet de sider brugte, nu er døde, fordi der i sletningsøjeblikket intet i filen registrerer, hvem ellers der måske stadig peger på dem. De objekter forbliver i dokumentets objektliste, og en fuld gem skriver hver af dem tilbage ud. Resultatet er klagen, der starter de fleste af disse supporttråde: en kunde sletter halvfems procent af siderne, gemmer, og filen formindskes med to procent. Værre, lækket akkumuleres. Indlæs, slet, gem, indlæs igen, slet igen, gem igen, og filen vokser monotont, mens sideantallet falder. Dette er et andet problem end det, der løses af font-undersætning og billed-nedsampling, som gør levende objekter mindre. Her er objekterne ikke for store. De er simpelthen ikke længere del af dokumentet
Rodmængden er trailer'en, ikke sidetræet
PDF-objektgrafen har intet omvendt-reference-felt. Formatet definerer ingen referencetælling og ingen tilbage-pointer-liste, og de /Parent-nøgler, der findes, hører til specifikke strukturer såsom sidetræet, ikke til objektgrafen som helhed. Intet i et indirekte objekt fortæller dig, hvem der peger på det, så spørgsmålet "bruger nogen stadig objekt 47" har præcis ét svar: traversér fremad fra en kendt rod og se om man ankommer. Det er hvorfor collectoren i losLab PDF Library er en mark-sweep-collector og ikke en refcount-ordning
Rødderne kommer fra fil-trailer'en (ISO 32000-1 §7.5.5). Tre nøgler bærer dem: /Root, dokumentkataloget fra §7.7.2, hvorfra sidetræet, navne, outlines, AcroForm og metadata alle hænger; /Info, dokumentinformations-dictionaryet; og /Encrypt, krypterings-dictionaryet. De to resterende trailer-nøgler er lokkeduer. /ID er et array af to bytestrenge, og /Prev er et heltal byteoffset til den forrige krydsreferencesektion. Ingen af dem er en indirekte reference, så ingen bidrager med en rod. losLab PDF Library sætter hele trailer-dictionaryet i kø frem for tre navngivne nøgler, hvilket ikke koster noget og holder enhver privat trailer-udvidelse i live
Selve gennemgangen er iterativ frem for rekursiv. Når traverseringen møder en indirekte reference, registrerer den kun objektnummeret og generationen, markerer den tilsvarende plads og skubber den ind i en FIFO-kø i stedet for at dereferere med det samme, hvilket holder dybe sidetræer og lange outline-kæder ude af kaldestakken og stopper det samme objekt fra at blive afkodet to gange. Direkte dictionaries, arrays og strøm-dictionaries går på en anden kø vogtet af et besøgt-sæt, fordi rigtige dokumenter indeholder ægte cykler: en sides /Parent peger tilbage på dens sidetræ-node, og outline-elementer kæder gennem /Prev og /Next i begge retninger. Generationsnumre er en del af matchet, ikke dekoration. En reference opløser kun, når objektnummer og generation begge stemmer overens; en reference til et nummer, der findes ved en anden generation, behandles som det null-objekt, specifikationen kræver, aldrig som en levende kant
Hvordan aktiverer man garbage collection ved en gem?
Garbage collection er opt-in og hører til gem-options-posten. Den er som standard False, fordi collectoren er en destruktiv gennemgang over objektgrafen, og intet bibliotek bør stille slette objekter en kalder aldrig bad det undersøge
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;
To andre indgangspunkter når den samme collector. SetGarbageCollect(1) sætter flaget på det valgte dokument, så en almindelig SaveToFile respekterer det, og GarbageCollectObjects kører gennemgangen med det samme og returnerer antallet af forældreløse indirekte objekter fjernet. Den umiddelbare form er den, man bruger, når man vil have et tal at logge eller assertere på, og det er værd at tjekke, fordi et negativt tal ikke er en optælling
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;
Den fejlsti betyder mere end den ser ud til. Objekter afkodes dovent, og et objekt, der aldrig er blevet afkodet, eksponerer slet ingen referencer. Hvis collectoren behandlede et ikke-afkodet objekt som en tom node, ville den feje alt væk, der kun kan nås gennem det. Så traverseringen tvinger afkodning, mens den rører hvert objekt, og en enkelt afkodningsfejl afbryder hele gennemgangen med et negativt resultat og lader dokumentet være byte-identisk. At feje en graf man kun delvist forstår, er hvordan en collector gør en beskadiget fil til en ødelagt en
Hvad ødelægger en naiv PDF-collector?
To detaljer, og begge fejler stille frem for højlydt. Den første er objektstrømme. Siden PDF 1.5 kan et ikke-strøm-objekt bo komprimeret inde i en /ObjStm-container (§7.5.7), og dets krydsreference-post er en type 2-post, der navngiver containeren plus et indeks inden i den. Et komprimeret objekt kan derfor kun nås gennem sin container. Markér medlemmet, fej containeren fordi intet refererede den som et dokumentobjekt, og man har skrevet en fil, hvis xref peger ind i et objekt, der ikke længere findes. Containeren er strukturel lagring, ikke dokumentdata, så den optræder aldrig som en kant i den objektgraf, man gennemgår. losLab PDF Library håndterer dette ved at afkoble hvert overlevende komprimeret medlem fra dets kildecontainer, før containerne forsvinder, hvorefter gemningen genpakker de overlevende ind i friske objektstrømme. Den anden detalje er hvad et strøm-objekt faktisk refererer. Bytesne er ikke en del af grafen. En indholdsstrøm der tegner tekst med /F1 12 Tf navngiver en font ved ressourcenavn, og det navn opløses gennem sidens /Resources-dictionary, så nåbarhedskanten løber side → /Resources → /Font → fontobjekt, aldrig gennem strøm-payloaden. De eneste referencer en strøm bidrager med kommer fra dens dictionary, hvor /Length, /Filter og /DecodeParms alle er tilladt at være indirekte. En collector, der parser strøm-bytes på udkig efter referencer, laver dyrt arbejde for intet; en collector, der springer strøm-dictionaries over, mister længde-objektet og korrumperer filen
Hvad sker der med de objektnumre, man frigør
De bliver frie poster, og de genbruges ikke i den samme gemning. Fejningen gennemgår objektlisten i faldende rækkefølge, så sletninger forbliver indeks-stabile, genopbygger opslagsindekset én gang til sidst i stedet for efter hver fjernelse, og for hvert fjernet objekt registrerer nummeret i den frie liste med dens generation inkrementeret med én, netop som §7.5.4 specificerer for en post, der senere kan genbruges. En generation allerede ved 65535 forbliver der, hvilket markerer det nummer som permanent pensioneret. Objektnumre er bevidst ikke pakket sammen. Efter en indsamling beholder filen huller: objekt 12 kan være frit, mens 13 og 14 er i brug, og trailer'ens /Size rapporterer stadig det højeste nummer plus én frem for det overlevende antal. Det er lovligt og normalt. Omnummerering ville spare en håndfuld bytes i krydsreferencetabellen og ville kræve omskrivning af hver reference i dokumentet, hvilket er den slags ændring, der stille ugyldiggør alt, der holder objektnumre udefra. Størrelsen man får tilbage, kommer fra objektkropperne, ikke fra xref-tabellen
Hvornår man ikke må køre collectoren
Aldrig på en inkrementel opdatering. Collectoren er porteret til fulde gemninger, og flaget læses simpelthen ikke, når dokumentet bliver tilføjet til, og den port er ikke en begrænsning at arbejde omkring. En inkrementel opdatering (§7.5.6) lader de originale bytes være urørte og tilføjer en ny krydsreferencesektion kædet til den forrige gennem /Prev. Hver tidligere revision peger stadig på de objekter, den altid pegede på, så et objekt, der er uopnåeligt i den nuværende revision, er højst opnåeligt i en ældre en. At slette det ville ødelægge hver revision undtagen den sidste, og mekanikken bag hvorfor dækkes i artiklen om inkrementelle opdateringer og append-mode-gemninger. Den samme logik udelukker garbage collection på et signeret dokument, fordi den fulde omskrivning, der gør indsamling mulig, selv er hvad der ugyldiggør signaturen
Det er også værd at være klar over, hvad indsamling ikke er. Det er ikke en sanitizer. Collectoren fjerner objekter, intet refererer; den har ingen holdning til om deres indhold var følsomt, og et objekt, der stadig refereres, forbliver hvad det var. Hvis målet er at gøre information ugenoprettelig frem for at gøre filen mindre, er objektgrafen det forkerte lag, og instruktions-niveau-redigering og dokument-sanitizing er det rigtige. De to sammensættes godt i den rækkefølge: redigér og sanitisér først, indsaml derefter, så de objekter, redigeringen afkoblede, faktisk forlader filen. Den samme parring findes i ressource-udrensnings-API'en, hvor at overgive garbage-collect-optionen får udrensningen til at køre en indsamling bagefter og rapportere de forældreløse, den fjernede, i OrphanObjectsRemoved
En sidste vane værd at antage. Log returværdien af GarbageCollectObjects i hvilket batch-job der end udfører dine sidesletninger, og hold øje med den over nogle ugers rigtige dokumenter. Et nul på en fil man lige har halveret betyder, at noget opstrøms stadig holder en reference, man ikke forventede, som regel en navnetræ-post, en outline-destination eller et AcroForm-felt, der overlevede den side, det var fastgjort til. Collectoren er den billigste nåbarheds-debugger, man nogensinde vil have, fordi den besvarer det spørgsmål, selve PDF-formatet nægter at besvare
Garbage collectoren, gem-options-posten og ressource-udrensnings-API'en beskrevet her er del af losLab PDF Library til Delphi og C++Builder, hvis produktside bærer den fulde gem-pipeline-reference inklusive samspillet mellem indsamling, objektstrøm-pakning og linearisering