Att ta bort en sida ur en PDF tar inte bort dess teckensnitt, bilder eller innehållsströmmar. losLab PDF Library återvinner dem med en mark-sweep-insamlare som vandrar genom objektgrafen framåt från trailerns rötter och tar bort varje indirekt objekt som ingenting längre når. Den körs vid en fullständig sparning, är avstängd som standard, och returnerar antalet objekt den tog bort
Varför krymper filen inte när man tar bort PDF-sidor?
Det beror på att borttagning av en sida är en referensredigering, inte en lagringsoperation. DeletePages(StartPage, PageCount) kopplar loss sidobjekt från sidträdet och reparerar de dispositionsposter (outline) som pekade på dem. Vad den inte kan göra är att avgöra att teckensnittsprogrammet, innehållsströmmen och bild-XObjectet som dessa sidor använde nu är döda, eftersom ingenting i filen vid borttagningstillfället registrerar vem eller vad som fortfarande kan peka på dem. Dessa objekt blir kvar i dokumentets objektlista, och en fullständig sparning skriver ut alla igen. Resultatet är den klagan som inleder de flesta supporttrådar av det här slaget: en kund tar bort nittio procent av sidorna, sparar, och filen krymper med två procent. Värre är att läckan förvärras över tid. Läs in, ta bort, spara, läs in igen, ta bort igen, spara igen — filen växer monotont medan sidantalet sjunker. Detta är ett annat problem än det som löses av font-subsetting och bildnedskalning, vilka gör levande objekt mindre. Här är objekten inte för stora — de är helt enkelt inte längre en del av dokumentet
Rotmängden är trailern, inte sidträdet
PDF-objektgrafen har inget fält för bakåtreferenser. Formatet definierar varken referensräkning eller någon lista med bakåtpekare, och de /Parent-nycklar som faktiskt finns tillhör specifika strukturer som sidträdet, inte objektgrafen som helhet. Ingenting i ett indirekt objekt talar om vem som pekar på det, så frågan "använder någon fortfarande objekt 47" har exakt ett svar: vandra framåt från en känd rot och se om du når fram dit. Det är därför insamlaren i losLab PDF Library är en mark-sweep-insamlare och inte ett referensräkningsschema
Rötterna kommer från filens trailer (ISO 32000-1 §7.5.5). Tre nycklar bär dem: /Root, dokumentkatalogen enligt §7.7.2 som sidträdet, namnen, dispositionen, AcroForm och metadata alla hänger från; /Info, dokumentets informationsordbok; samt /Encrypt, krypteringsordboken. De två återstående trailer-nycklarna är villospår. /ID är en array med två bytesträngar, och /Prev är ett heltalsbyteoffset till föregående korsreferensavsnitt. Ingen av dem är en indirekt referens, så ingen bidrar med en rot. losLab PDF Library köar hela trailer-ordboken i stället för tre namngivna nycklar, vilket inte kostar något och håller eventuella privata trailer-tillägg vid liv
Själva vandringen är iterativ snarare än rekursiv. När traverseringen möter en indirekt referens registrerar den bara objektnumret och generationen, markerar motsvarande plats och lägger den i en FIFO-kö i stället för att dereferera direkt, vilket håller djupa sidträd och långa dispositionskedjor borta från anropsstacken och hindrar att samma objekt avkodas två gånger. Direkta ordböcker, arrayer och strömordböcker läggs i en andra kö som skyddas av en besökt-mängd, eftersom verkliga dokument innehåller genuina cykler: en sidas /Parent pekar tillbaka på dess sidträdsnod, och dispositionsposter kedjas via /Prev och /Next i båda riktningarna. Generationsnummer är en del av matchningen, inte dekoration. En referens löses upp endast när objektnummer och generation båda stämmer överens; en referens till ett nummer som finns i en annan generation behandlas som det nullobjekt specifikationen kräver, aldrig som en levande kant
Hur aktiverar man skräpsamling vid en sparning?
Skräpsamling är valbar (opt-in) och hör hemma i sparalternativens post. Den är som standard False, eftersom insamlaren är en destruktiv genomgång av objektgrafen och inget bibliotek bör tyst ta bort objekt som anroparen aldrig bad det granska
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;
Två andra ingångar når samma insamlare. SetGarbageCollect(1) sätter flaggan på det valda dokumentet så att en vanlig SaveToFile respekterar den, och GarbageCollectObjects kör passet omedelbart och returnerar antalet borttagna föräldralösa indirekta objekt. Den omedelbara formen är den man ska använda när man vill ha ett tal att logga eller kontrollera, och det är värt att kolla, eftersom ett negativt returvärde inte är ett antal
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 här felvägen betyder mer än den ser ut att göra. Objekt avkodas lat (lazy), och ett objekt som aldrig avkodats visar inga referenser alls. Om insamlaren behandlade ett oavkodningsbart objekt som en tom nod skulle den sopa bort allt som endast var nåbart genom det. Därför tvingar traverseringen fram avkodning när den rör vid varje objekt, och ett enda avkodningsfel avbryter hela passet med ett negativt resultat och lämnar dokumentet byte-identiskt. Att sopa igenom en graf man bara delvis förstår är hur en insamlare förvandlar en skadad fil till en förstörd en
Vad får en naiv PDF-insamlare att gå sönder?
Två detaljer, och båda misslyckas tyst i stället för högljutt. Den första är objektströmmar. Sedan PDF 1.5 kan ett icke-strömobjekt ligga komprimerat inuti en /ObjStm-behållare (§7.5.7), och dess korsreferenspost är en typ 2-post som namnger behållaren plus ett index inom den. Ett komprimerat objekt är därför bara nåbart genom sin behållare. Markera medlemmen, sopa bort behållaren eftersom ingenting refererade till den som ett dokumentobjekt, och du har skrivit en fil vars xref pekar in i ett objekt som inte längre finns. Behållaren är strukturell lagring, inte dokumentdata, så den dyker aldrig upp som en kant i objektgrafen du vandrar genom. losLab PDF Library hanterar detta genom att koppla loss varje överlevande komprimerad medlem från sin källbehållare innan behållarna försvinner, varefter sparningen packar om de överlevande i nya objektströmmar. Den andra detaljen är vad ett strömobjekt faktiskt refererar till. Byten är inte en del av grafen. En innehållsström som ritar text med /F1 12 Tf namnger ett teckensnitt via resursnamn, och det namnet löses upp via sidans /Resources-ordbok, så nåbarhetskanten går sida → /Resources → /Font → teckensnittsobjekt, aldrig via strömmens nyttolast. De enda referenser en ström bidrar med kommer från dess ordbok, där /Length, /Filter och /DecodeParms alla får vara indirekta. En insamlare som analyserar strömbyte i jakt på referenser lägger ner dyrt arbete i onödan; en insamlare som hoppar över strömordböcker tappar längdobjektet och korrumperar filen
Vad händer med objektnumren du frigör
De blir fria poster, och de återanvänds inte inom samma sparning. Genomgången vandrar objektlistan i fallande ordning så att borttagningar förblir indexstabila, bygger om uppslagsindexet en gång på slutet i stället för efter varje borttagning, och för varje borttaget objekt registrerar den numret i den fria listan med generationen ökad med ett, precis som §7.5.4 föreskriver för en post som senare kan återanvändas. En generation som redan står på 65535 stannar där, vilket markerar det numret som permanent pensionerat. Objektnummer packas medvetet inte ihop. Efter en insamling behåller filen hål: objekt 12 kan vara fritt medan 13 och 14 är i bruk, och trailerns /Size rapporterar fortfarande det högsta numret plus ett i stället för antalet överlevande. Det är lagligt och normalt. Omnumrering skulle spara en handfull byte i korsreferenstabellen och skulle kräva att varje referens i dokumentet skrevs om, vilket är den typ av ändring som tyst gör allt som håller objektnummer utifrån ogiltigt. Storleken du får tillbaka kommer från objektkropparna, inte från xref-tabellen
När du inte får köra insamlaren
Aldrig vid en inkrementell uppdatering. Insamlaren är spärrad till fullständiga sparningar, och flaggan läses helt enkelt inte när dokumentet bara utökas, och den spärren är ingen begränsning att kringgå. En inkrementell uppdatering (§7.5.6) lämnar de ursprungliga byten orörda och lägger till ett nytt korsreferensavsnitt kedjat till det föregående via /Prev. Varje tidigare revision pekar fortfarande på de objekt den alltid pekat på, så ett objekt som är onåbart i den aktuella revisionen är mycket väl nåbart i en äldre. Att ta bort det skulle förstöra varje revision utom den sista, och mekaniken bakom varför beskrivs i artikeln om inkrementella uppdateringar och sparning i append-läge. Samma resonemang utesluter skräpsamling på ett signerat dokument, eftersom den fullständiga omskrivningen som gör insamling möjlig är just det som ogiltigförklarar signaturen
Det är också värt att vara tydlig med vad insamling inte är. Det är ingen sanering (sanitizer). Insamlaren tar bort objekt som ingenting refererar till; den har ingen uppfattning om huruvida deras innehåll var känsligt, och ett objekt som fortfarande refereras förblir vad det var. Om målet är att göra information oåterkallelig i stället för att göra filen mindre är objektgrafen fel nivå, och instruktionsnivå-redigering (redaction) och dokumentsanering är rätt nivå. De två samverkar bra i den ordningen: redigera och sanera först, samla sedan in skräp, så att objekten som redigeringen kopplade loss faktiskt lämnar filen. Samma parning finns i resurs-rensnings-API:et, där att skicka med skräpsamlingsalternativet gör att rensningen kör en insamling efteråt och rapporterar de föräldralösa objekt den tog bort i OrphanObjectsRemoved
En sista vana värd att anta. Logga returvärdet från GarbageCollectObjects i vilket batch-jobb som helst som gör dina sidborttagningar, och håll koll på det under några veckor med riktiga dokument. En nolla på en fil du precis halverat betyder att något högre upp fortfarande håller en referens du inte förväntade dig, oftast en namnträdspost, ett dispositionsmål eller ett AcroForm-fält som överlevde sidan det var fäst vid. Insamlaren är den billigaste nåbarhetsdebuggern du någonsin kommer att ha, eftersom den besvarar frågan som själva PDF-formatet vägrar besvara
Skräpsamlaren, sparalternativ-posten och resurs-rensnings-API:et som beskrivs här är en del av losLab PDF Library för Delphi och C++Builder, vars produktsida innehåller den fullständiga referensen för sparpipelinen inklusive samspelet mellan insamling, packning av objektströmmar och linjärisering