De PDFium Component for Delphi (PDFiumPas) verkleint PDF-bestanden met drie herschrijf-API's op TPdf: SaveAsReachable laat objecten vallen waarnaar niets verwijst, SaveAsDeduplicated voegt byte-identieke fonts, beelden en Form XObjects samen, en SaveAsFlateRecompressed codeert bestaande Flate-streams opnieuw op maximale compressie. Elk geeft een rapportrecord terug zodat u ziet wat er is verwijderd en hoeveel bytes het opleverde
De gebruikelijke aanleiding is een supportticket met de tekst "we verwijderden twintig pagina's en het bestand werd groter". Iemand opende een contract, haalde een bijlage weg, voegde een voorblad samen en sloeg op. Elke stap was een incrementele update, dus de oude objecten zitten nog in het bestand, en wanneer dat document later wordt geladen en met FPDF_SaveAsCopy wordt weggeschreven, komen ze gewoon terug. PDFium serialiseert wat er in zijn in-memory objecttabel zit; het heeft geen publieke aanroep voor het oplijsten van indirecte objecten, het verwijderen van een object of het herschrijven van verwijzingen. Alles in dit artikel draait daarom in de PDFiumPas-wrapperlaag bovenop de bytes die PDFium produceert
Waarom houdt FPDF_SaveAsCopy verwijderde pagina's in het bestand?
FPDF_SaveAsCopy houdt weesobjecten vast omdat PDFium elke levende entry in de cross-reference-tabel in zijn document laadt, ongeacht of er nog iets naartoe wijst. ISO 32000-1 §7.5.6 beschrijft incrementele updates als het toevoegen van nieuwe objectversies en een nieuwe xref-sectie aan het einde van het bestand. Een pagina verwijderen bewerkt alleen de paginaboom; het paginawoordenboek, zijn content stream en fonts of beelden die alleen door die pagina werden gebruikt, staan nog steeds als in-use-objecten geregistreerd. Een volledige niet-incrementele opslag verandert daar niets aan, want vanuit het gezichtspunt van de objecttabel zijn die objecten volstrekt geldig
Dezelfde restanten verschijnen na paginasamenvoegingen, form-afvlakking door andere tools en reparatieopslagen van beschadigde bestanden. Ze zijn onzichtbaar in elke viewer en voegen niets toe behalve bytes. Bij gescande documenten kan één weesgeraakte pagina-image enkele megabytes beslaan
Mark-and-sweep met SaveAsReachable
TPdf.SaveAsReachable verwijdert weesobjecten met een klassieke mark-and-sweep-passage: hij laat PDFium een niet-incrementele kopie van het huidige document schrijven, parst die kopie in een objectgrafiek, markeert alles dat vanaf de roots bereikbaar is en schrijft één klassieke revisie die alleen de gemarkeerde objecten bevat. Objecten binnen objectstreams worden onderweg gematerialiseerd als gewone indirecte objecten, dus de uitvoer gebruikt altijd een kale xref-tabel
De roots kiezen is waar een naïeve implementatie de mist ingaat. De voor de hand liggende seed is de Catalog die de trailer via /Root noemt. Van daaruit bereikt de wandeling de paginaboom, paginaresources, fonts, beelden, Form XObjects, AcroForm, de structuurboom, outlines, naamstructures, optional content, XMP-metadata, output intents en ingebedde bestanden. Maar het documentinformatiewoordenboek wordt verwezen door de trailer-entry /Info (ISO 32000-1 §7.5.5), niet door de Catalog. Seed uitsluitend vanuit /Root en u verwijdert stilletjes de Title, Author en Producer van elk bestand dat u verwerkt. PDFiumPas seedt de wandeling met zowel /Root als /Info, en de herbouwde trailer neemt /Info alleen mee wanneer dat object het echt overleefd heeft
De wandelaar is een depth-first-traversering met een expliciete stack en een bezochtenset, dus referentiecycli (ouderlinks in de paginaboom, annotation-/P-terugwijzingen) kunnen hem niet laten hangen. Een verwijzing naar een objectnummer dat al vrij is, wordt simpelweg niet gevolgd in plaats van als fout behandeld, wat de passage tolerant houdt voor licht beschadigde invoer
Objectnummers worden behouden, niet hernummerd
TPdf.SaveAsReachable houdt elk overlevend object op zijn oorspronkelijke objectnummer en generatie. Geveegde objecten worden als vrije entries in de xref-tabel weggeschreven (ISO 32000-1 §7.5.4), de gaten tussen levende nummers worden opgevuld met vrije entries, en object 0 blijft het hoofd van de vrije lijst. Omdat er niets wordt hernummerd, blijft elke overlevende verwijzing correct zonder één byte aan herschrijving, en er is geen oud-naar-nieuw-toewijzing die verkeerd kan. De kosten zijn enkele ongebruikte xref-regels van 20 bytes per stuk, en die ruil is het waard
uses
PDFium, FPdfCompress;
procedure PruneOrphans(const SourcePdf, TargetPdf: string);
var
Pdf: TPdf;
Report: TPdfPruneReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourcePdf;
Pdf.Active := True;
if not Pdf.SaveAsReachable(TargetPdf, Report) then
raise Exception.Create(Report.ErrorMessage);
Writeln(Format('%d bytes -> %d bytes, kept %d object(s), pruned %d',
[Report.SourceByteCount, Report.OutputByteCount,
Report.KeptObjectCount, Report.PrunedObjectCount]));
finally
Pdf.Free;
end;
end;
TPdfPruneReport voert SourceByteCount mee (de grootte van de niet-incrementele kopie die PDFium produceerde), OutputByteCount, KeptObjectCount, PrunedObjectCount, Succeeded en ErrorMessage. De bestandsnaam-overload schrijft naar een tijdelijk bestand naast het doel en vervangt het doel alleen wanneer de herschrijving slaagde, dus een mislukte run laat nooit een afgekapte PDF achter
Welke bestanden weigeren deze API's?
Alle drie de herschrijf-API's weigeren versleutelde documenten en standaard ook ondertekende. Versleutelde invoer faalt met Succeeded = False en een foutmelding in plaats van een exception: een herschrijving in één revisie kan de objectnummers, bytebereiken en per-object-sleutels waar het versleutelingswoordenboek van afhangt niet vasthouden, dus de uitvoer zou onleesbaar zijn. Een document met digitale handtekeningen geeft EPdfError tenzij u AllowSignedDocument = True doorgeeft, want een volledige herschrijving maakt het ondertekende bytebereik altijd ongeldig. Die vlag zetten betekent dat u een bestand accepteert waarvan de handtekeningen als gebroken zullen rapporteren; dat is de juiste keuze voor een archiefkopie, nooit voor een contract dat u op het punt staat terug te sturen
Hoe beslist SaveAsDeduplicated dat twee beelden hetzelfde zijn?
TPdf.SaveAsDeduplicated behandelt twee resources pas als identiek wanneer hun woordenboeken, hun volledige bereikbare subgrafieken en hun rauwe streambytes allemaal matchen. Hij draait eerst dezelfde bereikbaarheidswandeling, fingerprintt daarna elk bereikbaar font, image XObject en Form XObject, voegt elke groep matches samen op het lid met het laagste objectnummer en herschrijft elke verwijzing naar die overlever. Samengevoegde duplicaten worden vrije xref-entries, net als weesobjecten. Dit is de fix voor het klassieke rapport "logo 400 keer ingebed", geproduceerd door mail-merge-tools die dezelfde resources op elke pagina opnieuw inbedden
De fingerprint is een 64-bits FNV-1a-hash, recursief berekend. Voor elke kandidaat pakt de wandelaar de objectbody, vervangt elke N G R-verwijzing door de fingerprint van het object waarnaar hij wijst, en hasht het resultaat. Daardoor is de hash onafhankelijk van objectnummering: twee kopieën van hetzelfde font waarvan de descriptors toevallig object 12 en 87 zijn, geven nog steeds dezelfde waarde. Resultaten worden gememoïzeerd zodat gedeelde subobjecten één keer gehasht worden, en een bezochtenset geeft voor terugkoppelingen een vaste sentinel terug zodat cyclische grafieken aflopen
De subtiele bug zit in de streampayload. Om verwijzingen veilig te vinden, scant PDFiumPas een kopie van elk object waarin streamdata, strings en commentaar met spaties zijn leeggemaakt; anders zou een reeks binaire pixeldata die er toevallig uitziet als 12 0 R als verwijzing worden behandeld. Hasht u alleen die opgeschoonde kopie, dan klappen twee beelden met identieke woordenboeken (dezelfde /Width, /Height, /BitsPerComponent, /Filter, /Length) maar verschillende pixels samen tot één fingerprint, en wordt de tweede foto op de pagina stilletjes vervangen door de eerste. SaveAsDeduplicated voegt de rauwe bytes tussen stream en endstream toe aan de gehashte data, zodat de pixels deel uitmaken van de identiteit. Kandidaten worden bovendien per categorie gegroepeerd voordat er vergeleken wordt, dus een zeldzame hashbotsing tussen een font en een beeld kan ze nooit samenvoegen
Het herschrijven van verwijzingen hergebruikt dezelfde opgeschoonde-scan-truc omgekeerd: verwijzingen worden gelokaliseerd in de leeggemaakte kopie, maar de bytes worden uit de originele body gekopieerd, dus alleen woordenboek- en array-structuur wordt ooit gewijzigd en binaire payloads gaan onaangeroerd door
procedure MergeDuplicateResources(Pdf: TPdf; const TargetPdf: string);
var
Report: TPdfDedupReport;
begin
if not Pdf.SaveAsDeduplicated(TargetPdf, Report) then
raise Exception.Create(Report.ErrorMessage);
Writeln(Format('fonts %d, images %d, forms %d merged; %d -> %d bytes',
[Report.MergedFontCount, Report.MergedImageCount,
Report.MergedFormXObjectCount,
Report.SourceByteCount, Report.OutputByteCount]));
end;
Lees TPdfDedupReport zorgvuldig. Naast de drie Merged*Count-velden heeft hij KeptObjectCount en PrunedObjectCount, en in dit rapport telt PrunedObjectCount alleen samengevoegde duplicaten. Onbereikbare objecten worden nog steeds laten vallen, maar ze zitten niet in dat getal. Heeft u het aantal weesobjecten nodig voor logging, haal het dan uit een aparte SaveAsReachable-run. De andere beperking is dat matching byte-exact is op de gecodeerde stream: hetzelfde logo gecomprimeerd door twee verschillende producenten op twee verschillende Flate-niveaus wordt niet samengevoegd
Flate-streams opnieuw comprimeren op maximaal niveau
TPdf.SaveAsFlateRecompressed infleert elke bereikbare FlateDecode-stream en defleert hem opnieuw op het hoogste compressieniveau, en houdt het resultaat alleen aan wanneer hij kleiner is en uw drempels haalt. Veel producenten schrijven Flate op een snel default niveau, dus content streams en ingebedde fontprogramma's krimpen vaak nog verder. Objectnummers en generaties blijven staan; alleen de payload en zijn directe /Length veranderen
TPdfFlateRecompressOptions heeft twee velden en geen default-constructor, dus zet beide. MinSavingBytes is de minimale absolute besparing per stream, en MinSavingRatio is een fractie van de oorspronkelijke lengte (0.05 betekent 5%); een ratio van 0 schakelt de ratiotest uit. De herbouwde stream wordt geschreven met stream, CRLF, de nieuwe payload, CRLF en endstream, dus het resultaat hangt niet af van de regeleinden van het originele bestand
procedure RecompressFlate(Pdf: TPdf; const TargetPdf: string);
var
Options: TPdfFlateRecompressOptions;
Report: TPdfFlateRecompressReport;
begin
Options.MinSavingBytes := 256; // negeer kleine winsten
Options.MinSavingRatio := 0.05; // en alles onder de 5%
if not Pdf.SaveAsFlateRecompressed(TargetPdf, Options, Report) then
raise Exception.Create(Report.ErrorMessage);
Writeln(Format('recompressed %d, skipped %d, failed %d; streams %d -> %d bytes',
[Report.RecompressedCount, Report.SkippedCount, Report.FailedCount,
Report.OriginalStreamBytes, Report.RecompressedStreamBytes]));
end;
De randgevallen zijn bewust conservatief. Een stream waarvan /Length een indirecte verwijzing is, wordt overgeslagen, want de herschrijving moet een direct integer bewaren. Een payload die niet infleert telt mee in FailedCount en wordt ongewijzigd gekopieerd. Een Flate-stream die de drempels niet haalt telt mee in SkippedCount. DCT-, JPX-, JBIG2- en CCITT-beelden komen nooit op de kandidatenlijst, dus deze passage raakt JPEG-kwaliteit nooit aan. Voor lossy beeldbesparingen gebruikt u adaptive image resampling in PDFiumPas vóór de definitieve opslag
Eén portabiliteitsvalkuil uit het schrijven van die drempeltest: de eerste opzet vergeleek Single(Saving)/Single(OrigLen) met Saving gedeclareerd als Int64. Delphi accepteert het, maar FPC 3.2.2 stopt met Illegal type conversion: "Int64" to "Single", want in FPC is die syntaxis een typecast in plaats van een numerieke conversie. Gewoon Saving / OrigLen is echte deling in beide compilers en vergelijkt rechtstreeks met een Single, dus de casts waren simpelweg overbodig
In welke volgorde moeten de drie API's draaien?
Draai eerst SaveAsDeduplicated, laad de uitvoer opnieuw en draai daarna SaveAsFlateRecompressed. Deduplicatie omvat de bereikbaarheidsveeg al, dus een aparte SaveAsReachable-passage ervoor is overbodig; houd SaveAsReachable over voor klussen waar u alleen weesopruiming en een exact gepruind aantal wilt. Recomprimeren als tweede betekent dat het dure inflate-en-deflate-werk één keer per overlevende stream draait in plaats van één keer per duplicaat. De recompressiepassage loopt de grafiek bovendien opnieuw af vanuit /Root en /Info, wat font-descriptors of fontbestanden opruimt waar alleen een samengevoegd duplicaatfont nog naar wees
procedure ShrinkPdf(const SourcePdf, TargetPdf: string);
var
Pdf: TPdf;
TempPdf: string;
begin
TempPdf := ChangeFileExt(TargetPdf, '.dedup.pdf');
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourcePdf;
Pdf.Active := True;
MergeDuplicateResources(Pdf, TempPdf); // prune + merge
Pdf.Active := False;
Pdf.FileName := TempPdf;
Pdf.Active := True;
RecompressFlate(Pdf, TargetPdf); // deflatteer overlevers opnieuw
finally
Pdf.Free;
DeleteFile(TempPdf);
end;
end;
Past u ook redacties toe, doe dat vóór dit alles, want het fysiek verwijderen van inhoud is wat in de eerste plaats nieuwe weesobjecten creëert; het artikel over operator-level redaction en resource pruning behandelt de redactiekant. En laad het definitieve bestand altijd opnieuw als gezondheidscontrole in uw pijplijn: een paginaaantal dat matcht met de bron is goedkope verzekering voordat u iets overschrijft waar een klant om geeft
FAQ
Hernummert SaveAsReachable objecten of verandert hij verwijzingen?
Nee. TPdf.SaveAsReachable houdt elk overlevend object op zijn oorspronkelijke nummer en generatie en markeert verwijderde objecten als vrije xref-entries, dus geen enkele verwijzing hoeft te worden herschreven
Kan SaveAsDeduplicated per ongeluk twee verschillende beelden samenvoegen?
Niet op matching woordenboeken alleen. De fingerprint omvat de rauwe streambytes en de volledige gerefereerde subgrafiek, en kandidaten worden alleen binnen dezelfde categorie vergeleken (font, beeld, Form XObject)
Houden deze API's mijn digitale handtekeningen geldig?
Nee. Elke API voert een volledige herschrijving uit, die het ondertekende bytebereik breekt, dus ondertekende bestanden worden geweigerd tenzij u AllowSignedDocument = True doorgeeft en ongeldige handtekeningen in de uitvoer accepteert
Alle drie de API's worden geleverd met de PDFium Component for Delphi and C++Builder, naast de redaction- en image-resampling-features die elders op deze blog worden behandeld