Technisch artikel

PDF-grootte verkleinen in Delphi: fonts, beelden, LZW

Om de PDF-bestandsgrootte in Delphi te verkleinen biedt losLab PDF Library drie API's die de drie grootste bronnen van opgeblazen bestanden aanpakken: SubsetEmbeddedFonts herschrijft elk ingesloten TrueType-lettertypeprogramma terug tot de glyphs die het document daadwerkelijk rendert, DownsampleImages hersamplet rasterafbeeldingen die een doel-DPI overschrijden, en NormalizeLZWStreams vervangt de verouderde LZWDecode-compressie door FlateDecode. Elke functie retourneert het aantal gewijzigde objecten, dus een nul vertelt u dat de passage niets te doen had in plaats van stilzwijgend te zijn mislukt

Waarom is mijn samengevoegde PDF groter dan de bronbestanden?

Een samengevoegde of programmatisch gegenereerde PDF is meestal om een van drie redenen te groot: volledig ingesloten lettertypen, afbeeldingen die ver boven hun weergaveresolutie zijn gesampled, en streams die nog met het verouderde LZW-filter zijn gecomprimeerd. ISO 32000-1 §9.9 staat een producent toe het complete lettertypeprogramma in te sluiten, en de meeste producenten doen precies dat omdat het de veilige standaardkeuze is. Een complete Arial-FontFile2 loopt op tot honderden kilobytes; sluit die in een dozijn bronbestanden in, voeg ze samen, en u sleept een dozijn kopieën mee van glyph-contouren voor tekens die niemand heeft getypt. Het samenvoegen zelf veroorzaakt de verspilling niet, het concentreert die alleen in één bestand waar het totaal eindelijk zichtbaar wordt

Afbeeldingen zijn de tweede boosdoener. Een scan van 4800 pixels breed die in een kader van een kwart pagina wordt geplaatst, bevat ruwweg 40 keer meer pixeldata dan een afdrukpijplijn van 300 DPI kan gebruiken. De derde is stiller: streams gefilterd met LZWDecode. ISO 32000-1 §7.4.4 specificeert zowel LZWDecode als FlateDecode, en merkt op dat Flate doorgaans minstens even goed comprimeert; in de praktijk is Flate-uitvoer consistent kleiner op dezelfde data, en LZW overleeft vooral in bestanden die ergens in hun geschiedenis door gereedschap uit de jaren negentig zijn gegaan. De rest van dit artikel loopt de drie passages van losLab PDF Library langs die elk probleem oplossen, en combineert ze daarna tot één pijplijn

Overzichtsdiagram dat de oorzaken van een opgeblazen samengevoegde PDF koppelt aan de optimalisatiepassages van PDF Library for Delphi voor lettertypen, afbeeldingen en LZW-streams
Elke passage richt zich op één klassieke bron van opgeblazenheid en meldt hoeveel objecten zij heeft herschreven, waarbij nul een al slank bestand aangeeft en geen stilzwijgende mislukking

Font-subsetting met SubsetEmbeddedFonts

SubsetEmbeddedFonts krimpt elk ingesloten TrueType-lettertype in een geladen document tot de tekens die het document daadwerkelijk gebruikt, en heeft geen argumenten nodig omdat het de bewaarlijst uit de contentstreams zelf afleidt. Intern doorloopt de passage de contentstream van elke pagina met GetTextRuns, verzamelt de tekencodes waarnaar onder elke lettertyperesource wordt verwezen, bouwt een bewaarlijst op, en geeft het originele lettertypeprogramma door aan de Windows FontSub-engine (CreateFontPackage) om een subset te produceren. Het herschreven programma vervangt de FontFile2-stream ter plaatse, en de BaseFont-naam krijgt een LOSABC+-tag, de conventie van zes hoofdletters plus plusteken die ISO 32000-1 §9.6.4 voor subsetlettertypen definieert. Dat voorvoegsel maakt de aanroep ook idempotent: draai de passage tweemaal en reeds gesubsette lettertypen worden herkend en overgeslagen, dus het inbouwen in een batchtaak die bestanden opnieuw kan bezoeken is veilig

Pijplijn van PDF Library for Delphi die laat zien hoe losLab PDF Library een bewaarlijst van glyphs afleidt uit tekstruns en via de FontSub-engine een getagd subsetlettertype produceert
SubsetEmbeddedFonts loopt de tekstruns van elke pagina langs, voert de afgeleide bewaarlijst aan FontSub, tagt herschreven programma's met LOSABC+, en slaat ze bij volgende runs veilig over
var
  Lib: TPDFlib;
  Fonts: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('merged-report.pdf', '') = 1 then
    begin
      Fonts := Lib.SubsetEmbeddedFonts;
      // Fonts = aantal herschreven FontFile2-programma's;
      // 0 betekent niets ingesloten, of alles al gesubset
      Lib.SaveToFile('merged-report-subset.pdf');
    end;
  finally
    Lib.Free;
  end;
end;

Twee implementatiedetails zijn het kennen waard omdat ze de grenzen van de API verklaren. Ten eerste richt de passage zich op FontFile2, dus zij dekt ingesloten TrueType-programma's; lettertypen die als Type 1 of kaal CFF zijn ingesloten blijven ongemoeid in plaats van dat er risico wordt genomen. Ten tweede leunt zij op FontSub, wat SubsetEmbeddedFonts Windows-only maakt. Een subtieler punt uit de implementatie: of een lettertype in aanmerking komt wordt beslist door de verwijzingsketen FontDescriptor → FontFile2 daadwerkelijk te resolven, niet door een heuristiek met een ingesloten-vlag te vertrouwen, omdat lettertypen in een geladen document nooit door de boekhouding aan de aanmaakzijde zijn gegaan die zulke vlaggen zet. Bestaat de geresolvede stream, dan is het lettertype een kandidaat; zo niet, dan wordt het zonder fout overgeslagen

De eerlijke afweging: een subsetlettertype bevat alleen de glyphs die op het moment van subsetten aanwezig waren. Als een gereedschap verderop, of uw eigen code, later tekst in datzelfde lettertype toevoegt, heeft elk teken buiten de subset geen contour en zal het als een ontbrekende glyph worden gerenderd. Subset als de laatste stap die inhoud wijzigt, nooit vóór een bewerkingsfase. Dezelfde waarschuwing geldt als u van plan bent het lettertype er later weer uit te halen voor hergebruik; het artikel over tekst, afbeeldingen en lettertypen extraheren met PDF Library for Delphi behandelt wat een geëxtraheerd subsetprogramma u wel en niet kan geven

Hoe bepaalt DownsampleImages welke afbeeldingen krimpen?

DownsampleImages(MaxDPI, Quality, Filter) hersamplet alleen de afbeeldingen die zij met vertrouwen oversampled kan noemen, met een bewust conservatieve DPI-schatting. Een PDF-afbeelding-XObject slaat pixelafmetingen op maar geen betrouwbare fysieke resolutie, en een DPI-tag uit de bronafbeelding overleeft zelden een cyclus van laden, bewerken en opslaan. Daarom schat de passage SrcDPI = PixelWidth / 8.5, wat feitelijk de vraag stelt: als deze afbeelding de volledige breedte van een Letter-pagina zou beslaan, wat zou dan haar resolutie zijn? Alleen afbeeldingen waarvan de schatting MaxDPI overschrijdt worden aangeraakt. De vertekening is opzettelijk: een afbeelding die klein op de pagina is geplaatst heeft een werkelijke DPI die hoger ligt dan de schatting, dus de passage grijpt liever te weinig in dan dat zij een asset van drukkwaliteit degradeert die zij niet kan meten

Quality van 1 tot 100 selecteert de kwaliteit van de JPEG-hercodering, terwijl 0 de uitvoer als verliesloze Flate in PNG-stijl behoudt; Filter kiest de hersamplingkernel, 0 voor een boxgemiddelde en 1 voor bilineair. Voor gescand kantoorpapierwerk is DownsampleImages(150, 75, 1) een verstandig startpunt; voor alles wat mogelijk opnieuw wordt afgedrukt, verhoogt u MaxDPI naar 300 of slaat u de passage helemaal over. Downsampling is de enige lossy stap van de drie, dus zij hoort achter een instelling te zitten die uw gebruikers kunnen uitzetten

Beslissingsstroom van Delphi PDF-image-downsampling die een conservatieve SrcDPI-schatting vergelijkt met MaxDPI voordat een afbeelding wordt hersampled
Een conservatieve DPI-schatting gaat ervan uit dat de afbeelding een volledige Letter-pagina beslaat, dus alleen afbeeldingen waarover de bibliotheek zeker is worden hersampled terwijl grensgevallen ongemoeid blijven

Verouderde LZW-streams omzetten met NormalizeLZWStreams

NormalizeLZWStreams is de gratis winst: zij decomprimeert elke LZWDecode-stream verliesloos en hercomprimeert die ter plaatse met FlateDecode, en retourneert het aantal omgezette streams. Zij verwerkt zowel één enkele /Filter /LZWDecode-vermelding als LZW dat in een filterketenarray voorkomt, waarbij alleen de LZW-schakel wordt vervangen en de rest van de keten behouden blijft. Predictorparameters (Predictor, Columns, Colors, BitsPerComponent) worden uit de DecodeParms van de stream gelezen en doorgegeven aan de decompressor, zodat predictor-gecodeerde afbeeldingsdata correct het rondje maakt. Omdat beide filters bit-exacte codecs zijn, zijn de gedecodeerde bytes voor en na identiek; alleen de containercompressie verandert, en daarom is het veilig deze passage onvoorwaardelijk op elk bestand te draaien

Op een document zonder LZW-streams retourneert de aanroep eenvoudigweg 0 en raakt niets aan, wat de regressiesuite van de bibliotheek expliciet uitoefent: een vers aangemaakt bestand met alleen Flate moet nul conversies melden. Die garantie van niets-doen telt wanneer de passage in een pijplijn zit die duizenden heterogene bestanden verwerkt, sommige uit 2024 en sommige uit 1998

De complete pijplijn voor grootteoptimalisatie in Delphi

De drie passages combineren tot één functie van laden, optimaliseren en opslaan, en de volgorde doet er minder toe dan u zou verwachten omdat zij op disjuncte objecttypen werken: lettertypen, afbeelding-XObjects en streamfilters. Subsetting als eerste draaien blijft niettemin de nette keuze, aangezien dat de passage is met een beperking op de bewerkingsvolgorde

function OptimizePDF(const Src, Dst: string): Boolean;
var
  Lib: TPDFlib;
  Fonts, Images, Streams: Integer;
begin
  Result := False;
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(Src, '') <> 1 then
      Exit;
    Fonts   := Lib.SubsetEmbeddedFonts;        // TrueType FontFile2 -> subset
    Images  := Lib.DownsampleImages(150, 75, 1); // >150 DPI -> JPEG q75, bilineair
    Streams := Lib.NormalizeLZWStreams;        // LZWDecode -> FlateDecode
    Result := Lib.SaveToFile(Dst) = 1;
    // Log Fonts/Images/Streams: drie nullen betekenen dat het bestand al slank was
  finally
    Lib.Free;
  end;
end;

Verifieer de pijplijn zoals de bibliotheek zichzelf verifieert: met een rondgang. De regressietests van v3.130 maken een document aan, slaan het op, laden het opnieuw, draaien de optimalisatie, slaan opnieuw op, en stellen dan drie dingen vast: de uitvoer is kleiner, de geretourneerde aantallen komen overeen met de verwachtingen, en een herlading van het geoptimaliseerde bestand parseert en rendert nog steeds. Die lus van aanmaken, optimaliseren en herladen reproduceren tegen een steekproef van uw eigen productiebestanden, en de geëxtraheerde tekst voor en na vergelijken, is een investering van een uur die integratiefouten opvangt lang voordat een klant een kapotte factuur opent

// Rondgangcontrole: het geoptimaliseerde bestand moet nog schoon laden
Lib := TPDFlib.Create;
try
  Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
  Assert(Lib.GetPageCount > 0);
finally
  Lib.Free;
end;

Waar past de pijplijn in een samenvoegworkflow? Na het samenvoegen, niet tijdens. Eerst samenvoegen en het enkele resultaat optimaliseren betekent dat elk ingesloten lettertype één keer wordt gesubset tegen de vereniging van alle gebruikte tekens, in plaats van per bronbestand. Als de doorvoer van het samenvoegen de bottleneck is, biedt PDF Library for Delphi een snel pad op byteniveau dat volledige objectparsing vermijdt, beschreven in het artikel over snel PDF's samenvoegen met byteverwijzingsverschuiving; en voor invoer die te groot is om volledig in het geheugen te houden behandelt samenvoegen en splitsen met directe toegang voor grote PDF's de streamingroute. Beide combineren natuurlijk met een afsluitende optimalisatiepassage op de samengevoegde uitvoer

Wat de drie passages niet zullen doen

Het optimalisatietrio van losLab PDF Library sluit bewust alles uit wat de betekenis van het document verandert. SubsetEmbeddedFonts voegt dubbele lettertypen uit samengevoegde bronnen niet samen tot één programma, maar krimpt elk onafhankelijk; deduplicatie is een andere, riskantere transformatie. DownsampleImages zal een afbeelding overslaan waarvan de conservatieve DPI-schatting onder de drempel blijft, zelfs wanneer een mens kan zien dat zij te groot is voor haar kader. En geen van de passages raakt de documentstructuur aan, dus een bestand dat is opgeblazen door duizenden verweesde objecten heeft een opslag in herschrijfstijl nodig in plaats van deze passages op streamniveau. Binnen die grenzen verwijdert de combinatie van font-subsetting, image-downsampling en normalisatie van LZW naar Flate de drie klassieke bronnen van opgeblazen PDF's met elk één voorspelbare API-aanroep. De drie functies worden geleverd als onderdeel van losLab PDF Library voor Delphi, C# en VB.NET, naast de eerder besproken API's voor samenvoegen, extractie en rendering