Å slette en side fra en PDF sletter ikke fontene, bildene eller innholdsstrømmene den brukte. losLab PDF Library rydder opp i dette med en mark-sweep-samler som går gjennom objektgrafen fremover fra trailerens røtter og fjerner hvert indirekte objekt ingenting lenger peker på. Den kjører ved en full lagring, er avslått som standard, og returnerer antallet objekter den droppet
Hvorfor krymper ikke filen når PDF-sider slettes?
Fordi sideslette er en referanseredigering, ikke en lagringsoperasjon. DeletePages(StartPage, PageCount) kobler sideobjekter løs fra sidetreet og reparerer disposisjonsoppføringene som pekte på dem. Det den ikke kan gjøre, er å avgjøre at fontprogrammet, innholdsstrømmen og bilde-XObjectet disse sidene brukte nå er døde, fordi ingenting i filen på slettetidspunktet registrerer hvem andre som fortsatt kan peke på dem. Disse objektene blir liggende i dokumentets objektliste, og en full lagring skriver hvert eneste av dem tilbake. Resultatet er klagen som innleder de fleste av disse supporttrådene: en kunde sletter nitti prosent av sidene, lagrer, og filen krymper med to prosent. Verre er det at lekkasjen forsterkes over tid. Last inn, slett, lagre, last inn igjen, slett igjen, lagre igjen, og filen vokser monotont mens sideantallet synker. Dette er et annet problem enn det som løses av font-subsetting og bildenedskalering, som gjør levende objekter mindre. Her er ikke objektene for store. De er ganske enkelt ikke lenger en del av dokumentet
Rotsettet er traileren, ikke sidetreet
PDF-objektgrafen har ingen felt for tilbakereferanser. Formatet definerer verken en referanseteller eller en liste med tilbakepekere, og /Parent-nøklene som faktisk finnes tilhører spesifikke strukturer som sidetreet, ikke objektgrafen som helhet. Ingenting i et indirekte objekt forteller deg hvem som peker på det, så spørsmålet «bruker noen fortsatt objekt 47» har nøyaktig ett svar: traverser fremover fra en kjent rot og se om du kommer dit. Derfor er samleren i losLab PDF Library en mark-sweep-samler og ikke et referansetellingsopplegg
Røttene kommer fra filens trailer (ISO 32000-1 §7.5.5). Tre nøkler bærer dem: /Root, dokumentkatalogen fra §7.7.2 som sidetreet, navn, disposisjoner, AcroForm og metadata alle henger fra; /Info, dokumentets informasjonsordbok; og /Encrypt, krypteringsordboken. De to gjenværende trailer-nøklene er blindspor. /ID er en array av to bytestrenger, og /Prev er et heltalls byte-offset til den forrige kryssreferanseseksjonen. Ingen av dem er en indirekte referanse, så ingen av dem bidrar til en rot. losLab PDF Library legger hele trailer-ordboken i køen fremfor tre navngitte nøkler, noe som ikke koster noe og holder enhver privat trailer-utvidelse i live
Selve traverseringen er iterativ snarere enn rekursiv. Når traverseringen møter en indirekte referanse, registrerer den bare objektnummer og generasjon, markerer den tilsvarende plassen og legger den på en FIFO-kø i stedet for å dereferere umiddelbart, noe som holder dype sidetrær og lange disposisjonskjeder unna kallstakken og hindrer at samme objekt dekodes to ganger. Direkte ordbøker, arrayer og strømordbøker legges på en andre kø beskyttet av et besøkt-sett, fordi ekte dokumenter inneholder ekte sykluser: en side sin /Parent peker tilbake på sin sidetreknute, og disposisjonselementer kjeder seg gjennom /Prev og /Next i begge retninger. Generasjonsnummer er en del av matchingen, ikke pynt. En referanse løses opp bare når objektnummer og generasjon begge stemmer overens; en referanse til et nummer som finnes i en annen generasjon behandles som det null-objektet spesifikasjonen krever, aldri som en levende kant
Hvordan aktiverer du søppelinnsamling ved en lagring?
Søppelinnsamling er opt-in og hører hjemme i lagringsalternativenes record. Den er som standard False fordi samleren er en destruktiv gjennomgang av objektgrafen, og intet bibliotek bør stille og rolig slette objekter en kaller aldri ba det undersøke
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 inngangspunkter når samme samler. SetGarbageCollect(1) setter flagget på det valgte dokumentet slik at en vanlig SaveToFile respekterer det, og GarbageCollectObjects kjører gjennomgangen umiddelbart og returnerer antallet fjernede herreløse indirekte objekter. Den umiddelbare formen er den du bør bruke når du vil ha et tall å logge eller assertere på, og det er verdt å sjekke, for en negativ returverdi er ikke et antall
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;
Denne feilbanen betyr mer enn den ser ut til. Objekter dekodes lazy, og et objekt som aldri har blitt dekodet, avslører ingen referanser i det hele tatt. Hvis samleren behandlet et ikke-dekodbart objekt som en tom node, ville den feie bort alt som bare var nåbart gjennom det. Derfor tvinger traverseringen frem dekoding etter hvert som den berører hvert objekt, og en eneste dekodingsfeil avbryter hele gjennomgangen med et negativt resultat og lar dokumentet forbli byte-identisk. Å feie gjennom en graf du bare delvis forstår er hvordan en samler gjør en skadet fil til en ødelagt en
Hva knekker en naiv PDF-samler?
To detaljer, og begge feiler stille i stedet for høylytt. Den første er objektstrømmer. Siden PDF 1.5 kan et ikke-strøm-objekt ligge komprimert inne i en /ObjStm-beholder (§7.5.7), og dens kryssreferanseoppføring er en type 2-oppføring som navngir beholderen pluss en indeks i den. Et komprimert objekt er dermed bare nåbart gjennom sin beholder. Merk medlemmet, fei bort beholderen fordi ingenting refererte til den som et dokumentobjekt, og du har skrevet en fil hvis xref peker inn i et objekt som ikke lenger finnes. Beholderen er strukturell lagring, ikke dokumentdata, så den vises aldri som en kant i objektgrafen du traverserer. losLab PDF Library håndterer dette ved å løsrive hvert overlevende komprimert medlem fra kildebeholderen sin før beholderne forsvinner, hvorpå lagringen pakker de overlevende inn i nye objektstrømmer. Den andre detaljen er hva et strømobjekt faktisk refererer til. Bytene er ikke en del av grafen. En innholdsstrøm som tegner tekst med /F1 12 Tf navngir en font ved ressursnavn, og det navnet slås opp via sidens /Resources-ordbok, så nåbarhetskanten går side → /Resources → /Font → fontobjekt, aldri gjennom strømmens innhold. De eneste referansene en strøm bidrar med kommer fra ordboken sin, hvor /Length, /Filter og /DecodeParms alle har lov til å være indirekte. En samler som parser strømbyter på jakt etter referanser, gjør et kostbart arbeid for ingenting; en samler som hopper over strømordbøker mister lengdeobjektet og ødelegger filen
Hva skjer med objektnumrene du frigjør
De blir ledige oppføringer, og de gjenbrukes ikke i samme lagring. Feiingen går gjennom objektlisten i synkende rekkefølge slik at slettinger holder indeksen stabil, bygger opp oppslagsindeksen på nytt én gang til slutt i stedet for etter hver fjerning, og for hvert fjernet objekt registrerer den nummeret i den ledige listen med generasjonen økt med én, akkurat slik §7.5.4 spesifiserer for en oppføring som senere kan gjenbrukes. En generasjon som allerede står på 65535 blir værende der, og det nummeret merkes som permanent pensjonert. Objektnumre komprimeres bevisst ikke. Etter en innsamling beholder filen hull: objekt 12 kan være ledig mens 13 og 14 er i bruk, og trailerens /Size rapporterer fortsatt det høyeste nummeret pluss én, ikke antallet gjenværende. Det er lovlig og normalt. Å renummerere ville spart en håndfull byte i kryssreferansetabellen og ville krevd at hver referanse i dokumentet ble skrevet om, som er den typen endring som stille og rolig ugyldiggjør alt som holder på objektnumre utenfra. Størrelsen du får tilbake, kommer fra objektinnholdet, ikke fra xref-tabellen
Når du aldri må kjøre samleren
Aldri på en inkrementell oppdatering. Samleren er sperret til fulle lagringer, og flagget leses rett og slett ikke når dokumentet blir lagt til, og den sperren er ikke en begrensning å omgå. En inkrementell oppdatering (§7.5.6) lar de opprinnelige bytene stå urørt og legger til en ny kryssreferanseseksjon lenket til den forrige gjennom /Prev. Hver tidligere revisjon peker fortsatt på objektene den alltid har pekt på, så et objekt som er unåbart i den nåværende revisjonen er høyst nåbart i en eldre en. Å slette det ville ødelegge hver revisjon unntatt den siste, og mekanikken bak dette dekkes i artikkelen om inkrementelle oppdateringer og lagring i append-modus. Samme resonnement utelukker søppelinnsamling på et signert dokument, fordi den fulle omskrivingen som gjør innsamling mulig, er nettopp det som ugyldiggjør signaturen
Det er også verdt å være klar på hva innsamling ikke er. Det er ikke en sanitizer. Samleren fjerner objekter ingenting refererer til; den har ingen mening om hvorvidt innholdet deres var sensitivt, og et objekt som fortsatt refereres til forblir det det var. Hvis målet er å gjøre informasjon ugjenkallelig snarere enn å gjøre filen mindre, er objektgrafen feil lag, og instruksjonsnivå-redigering og dokumentsanering er riktig lag. De to fungerer godt sammen i den rekkefølgen: rediger og saner først, samle deretter, slik at objektene redigeringen løsrev faktisk forlater filen. Samme sammenkobling finnes i API-et for ressursopprydding, hvor å sende med søppelinnsamlingsalternativet får opprydningen til å kjøre en innsamling etterpå og rapportere de herreløse objektene den fjernet i OrphanObjectsRemoved
En siste vane verdt å ta opp. Logg returverdien til GarbageCollectObjects i hvilken som helst batch-jobb som gjør sideslettingene dine, og følg med over noen uker med ekte dokumenter. En null på en fil du nettopp halverte betyr at noe lenger oppe fortsatt holder en referanse du ikke forventet, som oftest en navnetre-oppføring, et disposisjonsmål eller et AcroForm-felt som overlevde siden det var festet til. Samleren er den billigste nåbarhets-debuggeren du noensinne vil ha, fordi den svarer på spørsmålet selve PDF-formatet nekter å svare på
Søppelsamleren, lagringsalternativenes record og API-et for ressursopprydding beskrevet her er en del av losLab PDF Library for Delphi og C++Builder, hvis produktside inneholder den fullstendige referansen for lagringspipelinen, inkludert samspillet mellom innsamling, objektstrøm-pakking og linearisering