Technický článek

Snížení velikosti souborů PDF v Delphi: Písma, obrázky, LZW

Pro snížení velikosti souborů PDF v Delphi poskytuje knihovna losLab PDF Library tři API, která útočí na tři největší zdroje nadbytečné velikosti: SubsetEmbeddedFonts přepíše program každého vloženého TrueType písma pouze na glyfy, které dokument skutečně vykresluje, DownsampleImages převzorkuje rastrové obrázky přesahující cílové DPI a NormalizeLZWStreams nahradí zastaralou kompresi LZWDecode za FlateDecode. Každá funkce vrací počet objektů, které změnila, takže nula značí, že průchod byl bezvýsledný, spíše než tiché selhání

Sloučené nebo programově generované PDF bývá obvykle příliš velké z jednoho ze tří důvodů: plně vložená písma, obrázky navzorkované vysoko nad rozlišení zobrazení a datové proudy (streamy) stále komprimované starším filtrem LZW. Norma ISO 32000-1 §9.9 umožňuje tvůrci vložit kompletní program písma a většina z nich tak činí, protože se jedná o bezpečné výchozí nastavení. Kompletní písmo Arial v FontFile2 dosahuje stovek kilobajtů; vložte jej do tuctu zdrojových souborů, ty sloučíte a přenášíte tucet kopií obrysů glyfů pro znaky, které nikdo nenapsal. Samotné slučování tento odpad nevytváří, pouze jej koncentruje do jediného souboru, kde se celkový objem konečně stává viditelným

Obrázky jsou druhým viníkem. Skener s šířkou 4800 pixelů umístěný do rámce o velikosti čtvrtiny stránky přenáší přibližně 40krát více obrazových dat, než dokáže tisková fronta o rozlišení 300 DPI využít. Třetí důvod je nenápadnější: streamy filtrované pomocí LZWDecode. Norma ISO 32000-1 §7.4.4 definuje jak LZWDecode, tak FlateDecode a uvádí, že Flate obvykle komprimuje přinejmenším stejně dobře; v prací je výstup Flate u stejných dat trvale menší a LZW přežívá většinou v souborech, které v určitém okamžiku prošly nástroji z 90. let. Zbytek tohoto článku popisuje tři průchody v knihovně losLab PDF Library, které jednotlivé problémy řeší, a poté je kombinuje do jedné linky

Vytváření podsad písem pomocí SubsetEmbeddedFonts

Metoda SubsetEmbeddedFonts zmenší každé vložené TrueType písmo v načteném dokumentu na znaky, které dokument skutečně využívá. Nevyžaduje žádné argumenty, protože seznam zachovávaných znaků odvozuje ze samotných streamů obsahu. Interně tento průchod prochází stream obsahu každé stránky pomocí GetTextRuns, shromažďuje kódy znaků odkazované pod každým prostředkem písma, sestavuje seznam k zachování a předává původní program písma enginu Windows FontSub (CreateFontPackage) k vytvoření podsady. Přepsaný program nahradí původní stream FontFile2 na místě a název BaseFont získá značku LOSABC+, což je konvence šesti velkých písmen a znaménka plus definovaná normou ISO 32000-1 §9.6.4 pro písma s podsadou. Tento prefix je také tím, co činí volání idempotentním: spusťte průchod dvakrát a již ořezaná písma budou rozpoznána a přeskočena, takže je bezpečné jej zapojit do dávkové úlohy, která může soubory zpracovávat opakovaně

var
  Lib: TPDFlib;
  Fonts: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('merged-report.pdf', '') = 1 then
    begin
      Fonts := Lib.SubsetEmbeddedFonts;
      // Fonts = number of FontFile2 programs rewritten;
      // 0 means nothing embedded, or everything already subsetted
      Lib.SaveToFile('merged-report-subset.pdf');
    end;
  finally
    Lib.Free;
  end;
end;

Dva detaily implementace stojí za zmínku, protože vysvětlují hranice tohoto API. Za prvé, průchod cílí na FontFile2, takže pokrývá vložené programy TrueType; písma vložená jako Type 1 nebo čisté CFF jsou ponechána beze změny, aby se předešlo riziku poškození. Za druhé, spoléhá na FontSub, což omezuje použití SubsetEmbeddedFonts pouze na Windows. Jemnější bod z implementace: to, zda písmo splňuje podmínky, se určuje skutečným rozborem řetězce odkazů FontDescriptorFontFile2, nikoli důvěrou v heuristiku příznaků vložení, protože písma v načteném dokumentu nikdy neprošla evidencí na straně vytváření, která tyto příznaky nastavuje. Pokud rozlišený stream existuje, písmo je kandidátem; pokud ne, je bez chyb přeskočeno

Upřímný kompromis: písmo s podsadou obsahuje pouze glyfy přítomné v době provádění podsady. Pokud následný nástroj nebo váš vlastní kód později přidá text ve stejném písmu, jakýkoli znak mimo podsadu nebude mít obrys a vykreslí se jako chybějící glyf. Vytváření podsady provádějte jako poslední krok měnící obsah, nikdy před fází úprav. Stejná opatrnost platí, pokud plánujete písmo později vytáhnout pro opětovné použití; článek o extrakci textu, obrázků a písem pomocí PDFlibPas popisuje, co vám extrahovaný program s podsadou může a nemůže poskytnout

Jak se v DownsampleImages rozhoduje, které obrázky se zmenší?

Metoda DownsampleImages(MaxDPI, Quality, Filter) převzorkuje pouze ty obrázky, které může s jistotou označit za nadvzorkované, a to s využitím záměrně konzervativního odhadu DPI. Obrazový objekt XObject v PDF ukládá rozměry pixelů, ale žádné spolehlivé fyzické rozlišení a jakákoli značka DPI ze zdrojového obrázku zřídka přežije cyklus načíst-upravit-uložit. Průchod tedy odhaduje SrcDPI = PixelWidth / 8.5, čímž se v podstatě ptá: pokud by se tento obrázek rozprostíral na celou šířku stránky formátu Letter, jaké by bylo jeho rozlišení? Dotknou se pouze obrázky, jejichž odhad překročí MaxDPI. Toto zkreslení je záměrné: obrázek umístěný na stránce v malé velikosti má skutečné DPI vyšší než odhad, takže průchod spíše nezareaguje, než aby degradoval grafiku tiskové kvality, kterou nedokáže změřit

Parametr Quality od 1 do 100 vybírá kvalitu opětovného kódování JPEG, zatímco 0 zachovává výstup jako bezztrátový Flate ve stylu PNG; Filter volí algoritmus převzorkování, 0 pro průměr boxu a 1 pro bilineární. Pro naskenované kancelářské dokumenty je DownsampleImages(150, 75, 1) rozumným výchozím bodem; u všeho, co se může znovu tisknout, zvyšte MaxDPI na 300 nebo tento průchod zcela přeskočte. Převzorkování je jediným ztrátovým krokem z těchto tří, proto by mělo být zařazeno za nastavení, které mohou vaši uživatelé vypnout

Převod starších LZW streamů pomocí NormalizeLZWStreams

Metoda NormalizeLZWStreams je snadným vítězstvím: bezztrátově dekomprimuje každý stream LZWDecode a znovu jej komprimuje pomocí FlateDecode přímo na místě a vrací počet převedených streamů. Zvládá jak samostatnou položku /Filter /LZWDecode, tak LZW vyskytující se uvnitř pole řetězce filtrů, kde je nahrazen pouze článek LZW a zbytek řetězce je zachován. Parametry prediktoru (Predictor, Columns, Colors, BitsPerComponent) se načítají z DecodeParms streamu a předávají se dekompresoru, takže obrazová data kódovaná prediktorem projdou cyklem správně. Vzhledem k tomu, že oba filtry jsou bitově přesné kodeky, jsou dekódované bajty před a po identické; mění se pouze komprese kontejneru, a proto je tento průchod bezpečné spouštět bezpodmínečně na každém souboru

U dokumentu bez streamů LZW volání jednoduše vrátí 0 a ničeho se nedotkne, což explicitně prověřuje regresní sada knihovny: čerstvě vytvořený soubor obsahující pouze Flate musí vykázat nula konverzí. Tato záruka no-op je důležitá, pokud je průchod součástí linky zpracovávající tisíce různorodých souborů, z nichž některé jsou z roku 2024 a jiné z roku 1998

Kompletní linka pro optimalizaci velikosti v Delphi

Tyto tři průchody se spojují do jediné funkce načíst-optimalizovat-uložit a na jejich pořadí záleží méně, než byste čekali, protože pracují na disjunktních typech objektů: písma, obrazové objekty XObject a filtry streamů. Provedení podsady jako první je stále nejčistší volbou, protože se jedná o průchod s omezením pořadí ú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;

Ověřte linku způsobem, jakým knihovna ověřuje sama sebe: cyklem tam a zpět (round-trip). Regresní testy verze v3.130 vytvoří dokument, uloží jej, znovu načtou, spustí optimalizaci, znovu uloží a poté potvrdí tři věci: výstup je menší, vrácené počty odpovídají očekávání a opětovné načtení optimalizovaného souboru se stále analyzuje a vykresluje. Reprodukce této smyčky vytvořit-optimalizovat-znovu_načíst na vzorku vašich vlastních produkčních souborů a porovnání extrahovaného textu před a po je jednohodinová investice, která zachytí chyby integrace dlouho předtím, než zákazník otevře poškozenou fakturu

// 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 se optimalizační linka hodí v pracovním postupu slučování? Po sloučení, nikoli během něj. Nejprve sloučit a poté optimalizovat jediný výsledek znamená, že každé vložené písmo se ořeže na podsadu pouze jednou vůči sjednocení všech použitých znaků, namísto pro každý zdrojový soubor. Pokud je úzkým hrdlem propustnost slučování, nabízí PDFlibPas rychlou cestu na bajtové úrovni, která se vyhýbá úplné analýze objektů, popsanou v článku o rychlém slučování PDF s posunem bajtových referencí; a pro vstupy příliš velké na to, aby se plně vešly do paměti, popisuje streamovací cestu průvodce pro slučování a rozdělování velkých PDF s přímým přístupem k souborům. Obě možnosti se přirozeně párují s finálním optimalizačním průchodem nad sloučeným výstupem

Co tyto tři průchody neudělají

Trio optimalizací knihovny losLab PDF Library záměrně vylučuje cokoli, co mění sémantiku dokumentu. SubsetEmbeddedFonts nesjednocuje duplicitní písma napříč sloučenými zdroji do jednoho programu, zmenšuje každé nezávisle; deduplikace je jiná, rizikovější transformace. DownsampleImages přejde obrázek, jehož konzervativní odhad DPI zůstane pod prahovou hodnotou, i když by člověk poznal, že je pro svůj rámec příliš velký. A žádný z průchodů se nedotýká struktury dokumentu, takže soubor nafouknutý tisíci osiřelých objektů vyžaduje spíše uložení ve stylu přepisu než tyto průchody na úrovni streamu. V rámci těchto limitů odstraňuje kombinace ořezávání písem na podsady, převzorkování obrázků a normalizace LZW na Flate tři klasické zdroje nadbytečné velikosti PDF, a to vždy jedním předvídatelným voláním API. Tyto tři funkce jsou dodávány jako součást knihovny losLab PDF Library pro Delphi, C# a VB.NET, společně s rozhraními API pro slučování, extrakci a vykreslování diskutovanými výše