Odborný článok

Zmenšenie veľkosti súboru PDF v Delphi: Písma, obrázky, LZW

Na zmenšenie veľkosti súborov PDF v Delphi poskytuje losLab PDF Library tri API, ktoré útočia na tri najväčšie zdroje nafukovania veľkosti: SubsetEmbeddedFonts prepíše každý vložený program písma TrueType len na glyfy, ktoré dokument skutočne vykresľuje, DownsampleImages prevzorkuje rastrové obrázky, ktoré prekračujú cieľové DPI, a NormalizeLZWStreams nahradí staršiu kompresiu LZWDecode za FlateDecode. Každé z nich vracia počet objektov, ktoré zmenilo, takže nula vám napovie, že prechod crashed bol prázdnou operáciou (no-op) a nie tichým zlyhaním

Prečo je moje zlúčené PDF väčšie ako jeho zdrojové súbory?

Zlúčené alebo programovo vygenerované PDF býva nadrozmerné zvyčajne z jedného z troch dôvodov: úplne vložené písma, obrázky vzorkované vysoko nad ich zobrazovacím rozlíšením a toky údajov (streams) stále komprimované starším filtrom LZW. Norma ISO 32000-1 §9.9 umožňuje tvorcom vložiť celý program písma a väčšina tvorcov to robí, pretože je to bezpečné predvolené nastavenie. Kompletný Arial v FontFile2 dosahuje stovky kilobajtov; vložte ho do tucta zdrojových súborov, zlúčte ich a prenášate tucet kópií obrysov glyfov pre znaky, ktoré nikto nenapísal. Zlúčenie samo o sebe nevytvára tento odpad, iba ho koncentruje do jedného súboru, kde sa celkový objem nakoniec stane viditeľným

Obrázky sú druhým vinníkom. Sken s šírkou 4800 pixelov umiestnený do rámca na štvrtinu strany prenáša približne 40-krát viac pixelových údajov, než dokáže využiť tlačový systém s rozlíšením 300 DPI. Tretí dôvod je nenápadnejší: toky filtrované pomocou LZWDecode. Norma ISO 32000-1 §7.4.4 definuje filtre LZWDecode aj FlateDecode a uvádza, že Flate zvyčajne komprimuje minimálne rovnako dobre; v praxi je výstup Flate pri rovnakých údajoch trvalo menší a LZW prežíva väčšinou v súboroch, ktoré v minulosti prešli nástrojmi z 90. rokov. Zvyšok tohto článku popisuje tri prechody knižnice losLab PDF Library, ktoré riešia jednotlivé problémy, a potom ich spája do jednej linky

Vytváranie podmnožín písiem pomocou SubsetEmbeddedFonts

SubsetEmbeddedFonts zmenší každé vložené písmo TrueType v načítanom dokumente na znaky, ktoré dokument skutočne používa. Nepotrebuje žiadne argumenty, pretože zoznam zachovaných znakov odvodzuje priamo z tokov obsahu. Interne tento krok prechádza tok obsahu každej stránky pomocou GetTextRuns, zhromažďuje kódy znakov odkazované pod každým prostriedkom písma, zostavuje zoznam na ponechanie a odovzdáva pôvodný program písma modulu Windows FontSub (CreateFontPackage) na vytvorenie podmnožiny. Prepísaný program nahradí prúd FontFile2 priamo na mieste a názov BaseFont získa značku LOSABC+, čo je konvencia šiestich veľkých písmen a znamienka plus, ktorú definuje ISO 32000-1 §9.6.4 pre písma s podmnožinou. Tento prefix tiež robí volanie idempotentným: spustite prechod dvakrát a už spracované písma s podmnožinou budú rozpoznané a preskočené, takže ich začlenenie do dávkovej úlohy, ktorá môže opakovane pristupovať k súborom, je bezpečné

Stojí za to poznať dva detaily implementácie, pretože vysvetľujú hranice tohto API. Po prvé, prechod sa zameriava na FontFile2, takže pokrýva vložené programy TrueType; písma vložené ako Type 1 alebo čisté CFF sa ponechávajú bez zmien, aby sa predišlo rizikám. Po druhé, spolieha sa na FontSub, čo robí SubsetEmbeddedFonts funkciou dostupnou len pre Windows. Jemnejší bod z implementácie: to, či písmo spĺňa podmienky, sa určuje skutočným vyriešením reťazca odkazov FontDescriptorFontFile2, not by trusting an embedded-flag heuristic, pretože písma v načítanom dokumente nikdy neprešli účtovníctvom na strane vytvárania, ktoré takéto príznaky nastavuje. Ak vyriešený prúd existuje, písmo je kandidátom; ak nie, preskočí sa bez chyby

Korektný kompromis: písmo s podmnožinou obsahuje iba tie glyfy, ktoré boli prítomné v čase vytvárania podmnožiny. Ak neskorší nástroj alebo váš vlastný kód neskôr pridá text v rovnakom písme, akýkoľvek znak mimo podmnožiny nebude mať obrys a vykreslí sa ako chýbajúci glyf. Podmnožinu vytvárajte ako posledný krok meniaci obsah, nikdy nie pred fázou úprav. Rovnaká opatrnosť platí, ak plánujete písmo neskôr znova vytiahnuť na opätovné použitie; článok o extracii textu, obrázkov a písiem pomocou PDFlibPas rozoberá, čo vám extrahovaný program s podmnožinou môže a nemôže poskytnúť

Ako DownsampleImages rozhoduje, ktoré obrázky zmenšiť?

Metóda DownsampleImages(MaxDPI, Quality, Filter) prevzorkuje iba tie obrázky, ktoré môže s istotou označiť za prevzorkované, a to s použitím zámerne konzervatívneho odhadu DPI. PDF objekt obrázka (image XObject) ukladá rozmery v pixeloch, ale nie dôveryhodné fyzické rozlíšenie, a akýkoľvek príznak DPI zo zdrojového obrázka zriedkakedy prežije cyklus načítania-úpravy-uloženia. Prechod preto odhaduje SrcDPI = PixelWidth / 8.5, čím sa v podstate pýta: ak by tento obrázok pokrýval celú šírku strany formátu Letter, aké by bolo jeho rozlíšenie? Upravujú sa iba tie obrázky, ktorých odhad presahuje MaxDPI. Toto skreslenie je zámerné: obrázok umiestnený na strane ako malý má skutočné DPI vyššie ako odhad, takže prechod radšej vynechá podozrivý obrázok, než by mal znehodnotiť grafický prvok v tlačovej kvalite, ktorý nedokáže zmerať

Parameter Quality od 1 do 100 určuje kvalitu opätovného kódovania JPEG, zatiaľ čo 0 zachováva výstup ako bezstratový Flate v štýle PNG; Filter volí jadro prevzorkovania, 0 pre krabicový priemer (box average) a 1 pre bilineárny. Pre skenované kancelárske dokumenty je DownsampleImages(150, 75, 1) rozumným východiskovým bodom; pre čokoľvek, čo sa môže znova tlačiť, zvýšte MaxDPI na 300 alebo tento krok úplne vynechajte. Zníženie rozlíšenia (downsampling) je jediným stratovým krokom z týchto troch, preto by malo byť skryté za nastavením, ktoré vaši používatelia môžu vypnúť

Prevod starších tokov LZW pomocou NormalizeLZWStreams

Metóda NormalizeLZWStreams predstavuje jednoduché víťazstvo: bezstratovo dekomprimuje každý tok LZWDecode — a znova ho komprimuje pomocou FlateDecode priamo na mieste, pričom vracia počet konvertovaných tokov. Zvláda ako samostatný záznam /Filter /LZWDecode, tak aj LZW nachádzajúci sa v poli reťazca filtrov, kde sa nahrádza iba článok LZW a zvyšok reťazca zostáva zachovaný. Parametre prediktora (Predictor, Columns, Colors, BitsPerComponent) sa načítajú z DecodeParms toku a odovzdajú sa dekompresoru, so predictor-encoded image data round-trips correctly. Keďže oba filtre sú bitovo presné kodeky, dekódované bajty sú pred a po identické; mení sa iba kontajnerová kompresia, preto je tento krok bezpečné spustiť bezpodmienečne na každom súbore

Na dokumente bez tokov LZW volanie jednoducho vráti 0 a ničoho sa nedotkne, čo explicitne overuje sada regresných testov knižnice: čerstvo vytvorený súbor iba s Flate kompresiou musí nahlásiť nula konverzií. Táto záruka prázdnej operácie je dôležitá, keď je prechod súčasťou linky, ktorá spracováva tisíce rôznorodých súborov, z ktorých niektoré sú z roku 2024 a iné z roku 1998

Kompletná linka pre optimalizáciu veľkosti v Delphi

Tieto tri prechody sa kombinujú do jednej funkcie načítania-optimalizácie-uloženia a na poradí záleží menej, než by ste očakávali, pretože pracujú s navzájom nesúvisiacimi typmi objektov: písma, objekty obrázkov (image XObjects) a filtre tokov. Spustenie vytvárania podmnožín ako prvého kroku je však stále tou najčistejšou voľbou, keďže ide o prechod s obmedzením poradia úprav

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, bilinear
    Streams := Lib.NormalizeLZWStreams;        // LZWDecode -> FlateDecode
    Result := Lib.SaveToFile(Dst) = 1;
    // Log Fonts/Images/Streams: three zeros mean the file was already lean
  finally
    Lib.Free;
  end;
end;

Overte linku spôsobom, akým sa overuje knižnica sama: cez round-trip. Regresné testy verzie v3.130 vytvoria dokument, uložia ho, načítajú znova, spusia optimalizáciu, opäť uložia a potom overia tri veci: výstup je menší, vrátené počty zodpovedajú očakávaniam a opätovné načítanie optimalizovaného súboru stále funguje. Implementácia takéhoto cyklu nad vzorkou vašich vlastných produkčných súborov a porovnanie extrahovaného textu pred a po je jednohodinová investícia, ktorá zachytí chyby integrácie dlho predtým, ako zákazník otvorí nefunkčnú faktúru

// Round-trip check: the optimized file must still load cleanly
Lib := TPDFlib.Create;
try
  Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
  Assert(Lib.GetPageCount > 0);
finally
  Lib.Free;
end;

Kam táto linka zapadá v procese zlučovania? Až po zlúčení, nie počas neho. Zlúčenie ako prvý krok a následná optimalizácia jediného výsledku znamená, že každé použité písmo sa podmnoží iba raz pre zjednotenie všetkých znakov namiesto zmenšovania pre každý zdrojový súbor osobitne. Ak je úzkym hrdlom priepustnosť spájania, PDFlibPas ponúka rýchlu cestu na úrovni bajtov, ktorá sa vyhýba plnej analýze objektov, opísanú v článku o rýchlom zlučovaní PDF s posunom bajtových referencií; a pre vstupy, ktoré sú príliš veľké na to, aby sa dali celé držať v pamäti, sa zaoberá príručka o zlučovaní a rozdeľovaní veľkých PDF s priamym prístupom k súborom. Obe riešenia sa prirodzene spájajú s finálnym optimalizačným prechodom nad zlúčeným výstupom

Čo tieto tri prechody neurobia

Optimalizačná trojica knižnice losLab PDF Library zámerne vylučuje čokoľvek, čo by menilo sémantiku dokumentu. SubsetEmbeddedFonts nezjednocuje duplicitné písma naprieč zlúčenými zdrojmi do jedného programu, ale zmenšuje každé nezávisle; deduplikácia je iná, rizikovejšia transformácia. DownsampleImages vynechá obrázok, ktorého konzervatívny odhad DPI zostáva pod prahovou hodnotou, aj keď človek vidí, že je pre svoj rámec príliš veľký. A žiaden z prechodov sa nedotýka štruktúry dokumentu, takže súbor nafúknutý tisíckami osamotených objektov vyžaduje uloženie v štýle prepisu a nie tieto prechody na úrovni tokov. V rámci týchto limitov odstraňuje kombinácia vytvárania podmnožín písiem, znižovania rozlíšenia obrázkov a normalizácie LZW na Flate tri klasické zdroje nafukovania PDF, a to každé pomocou jedného predvídateľného volania API. Tieto tri funkcie sa dodávajú ako súčasť losLab PDF Library pre Delphi, C# a VB.NET, spolu s API pre spájanie, extrakciu a vykresľovanie, o ktorých sme hovorili vyššie