Tehnični članak

Zmanjšanje velikosti datoteke PDF v Delphiju: Pisave, slike, LZW

Za zmanjšanje velikosti datoteke PDF v Delphiju ponuja losLab PDF Library tri API-je, ki napadajo tri največje vire kopičenja odvečnih podatkov: SubsetEmbeddedFonts prepiše vsak program vgrajene pisave TrueType le na tiste znake, ki se dejansko prikazujejo v dokumentu, DownsampleImages ponovno vzorči rastrske slike, ki presegajo ciljno ločljivost DPI, in NormalizeLZWStreams zamenja starejšo kompresijo LZWDecode s FlateDecode. Vsak od them vrne število spremenjenih objektov, tako da vam ničla sporoči, da je bil korak brez učinka (no-op) in ne tihi neuspeh

Zakaj je moj združeni PDF večji od svojih izvornih datotek?

Združen ali programsko ustvarjen PDF je običajno prevelik iz enega od treh razlogov: popolnoma vgrajene pisave, slike z ločljivostjo, ki je daleč nad ločljivostjo njihovega prikaza, in tokovi podatkov, ki so še vedno stisnjeni s starejšim filtrom LZW. ISO 32000-1 §9.9 omogoča proizvajalcu, da vgradi celoten program pisave, in večina to tudi počne, ker je to varna privzeta možnost. Celotna pisava Arial FontFile2 obsega več sto kilobajtov; če jo vgradite v ducat izvornih datotek in jih združite, boste prenašali ducat kopij orisov znakov za črke, ki jih nihče ni vpisal. Združevanje samo po sebi ne ustvarja odvečnih podatkov, le koncentrira jih v eno datoteko, kjer skupna velikost končno postane vidna

Slike so drugi krivec. Skenirana slika širine 4800 slikovnih pik, postavljena v okvir velikosti četrtine strani, vsebuje približno 40-krat več slikovnih podatkov, kot jih lahko porabi tiskalniški cevovod ločljivosti 300 DPI. Tretji vzrok je manj opazen: tokovi, filtrirani z LZWDecode. ISO 32000-1 §7.4.4 določa tako LZWDecode kot FlateDecode in ugotavlja, da Flate običajno stisne vsaj tako dobro; v praksi je izhod Flate pri enakih podatkih dosledno manjši, LZW pa preživi predvsem v datotekah, ki so šle skozi orodja iz 90. let prejšnjega stoletja. Preostanek tega članka opisuje tri korake losLab PDF Library, ki popravijo vsako težavo, nato pa jih združi v en sam cevovod

Podnabor pisav z SubsetEmbeddedFonts

Klic SubsetEmbeddedFonts zmanjša vsako vgrajeno pisavo TrueType v naloženem dokumentu na znake, ki jih dokument dejansko uporablja. Za to ne potrebuje argumentov, saj seznam obdržanih znakov izpelje iz samih tokov vsebine. Interno ta korak pregleda tok vsebine vsake strani z GetTextRuns, zbere kode znakov, na katere se sklicuje vsak vir pisave, zgradi seznam obdržanih znakov in preda prvotni program pisave gonilniku Windows FontSub (CreateFontPackage) za ustvarjanje podnabora. Prepisani program zamenja tok FontFile2 na mestu, ime BaseFont pa dobi oznako LOSABC+ — konvencijo iz šestih velikih črk in znaka plus, ki jo določa ISO 32000-1 §9.6.4 za podnaborne pisave. Ta predpona tudi zagotavlja, da je klic idempotenten: če korak zaženete dvakrat, se že podnaborjene pisave prepoznajo in preskočijo, zato je varno, da ga vključite v paketno opravilo, ki lahko znova obišče datoteke

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;

Dve podrobnosti implementacije sta vredni omembe, saj pojasnjujeja meje API-ja. Prvič, korak je usmerjen na FontFile2, zato pokriva vgrajene programe TrueType; pisave, vgrajene kot Type 1 or goli CFF, ostanejo nedotaknjene, da se izognemo tveganju. Drugič, zanaša se na FontSub, zaradi česar je SubsetEmbeddedFonts na voljo le v sistemu Windows. Subtilnejša točka implementacije: ali je pisava primerna, se odloči z dejansko razrešitvijo verige sklicev FontDescriptorFontFile2 in ne z zaupanjem hevristiki zastavic za vgradnjo, saj pisave v naloženem dokumentu nikoli niso šle skozi knjigovodstvo na strani ustvarjanja, ki nastavi te zastavice. Če razrešeni tok obstaja, je pisava kandidat; če ne, se preskoči brez napake

Iskrena menjava: podnaborna pisava vsebuje le znake, ki so bili prisotni ob ustvarjanju podnabora. Če naslednje orodje (ali vaša koda) pozneje doda besedilo v isti pisavi, kateri koli znak izven podnabora nima orisa in se bo prikazal kot manjkajoči znak. Podnabor naredite kot zadnji korak spreminjanja vsebine, nikoli pred fazo urejanja. Enako opozorilo velja, če nameravate pisavo pozneje izvleči za ponovno uporabo; članek o ekstrakciji besedila, slik in pisav s PDFlibPas opisuje, kaj vam ekstrahiran program podnabora lahko in česa ne more dati

Kako DownsampleImages določi, katere slike zmanjšati?

DownsampleImages(MaxDPI, Quality, Filter) ponovno vzorči le tiste slike, za katere lahko zanesljivo ugotovi, da so prevelike, pri čemer uporablja namerno konservativno oceno DPI. Slika PDF XObject hrani dimenzije slikovnih pik, vendar ne zanesljive fizične ločljivosti, oznaka DPI iz izvorne slike pa le redko preživi cikel nalaganja-urejanja-shranjevanja. Zato ta korak oceni SrcDPI = PixelWidth / 8.5, s čimer se vpraša: če bi ta slika obsegala celotno širino strani Letter, kakšna bi bila njena ločljivost? Ponovno se vzorčijo le slike, katerih ocena presega MaxDPI. Ta pristranskost je namerna: slika, ki je na strani postavljena majhna, ima dejansko ločljivost DPI višjo od ocene, zato se korak raje sproži premalo kot pa da bi poslabšal kakovost tiskalnega vira, ki ga ne more izmeriti

Parameter Quality od 1 do 100 izbere kakovost ponovnega kodiranja JPEG, medtem ko 0 ohrani izhod kot stiskanje Flate brez izgub v slogu PNG; Filter izbere jedro ponovnega vzorčenja, 0 za povprečje polja in 1 za bilinearno. Za skenirano pisarniško dokumentacijo je DownsampleImages(150, 75, 1) razumna začetna točka; za vse, kar se lahko ponovno tiska, dvignite MaxDPI na 300 ali popolnoma preskočite ta korak. Zmanjševanje ločljivosti je edini korak z izgubo podatkov med temi tremi, zato spada za nastavitev, ki jo vaši uporabniki lahko izklopijo

Pretvorba starih tokov LZW z NormalizeLZWStreams

NormalizeLZWStreams je enostavna zmaga: brez izgub dekompresira vsak tok LZWDecode in ga na mestu ponovno stisne s FlateDecode, pri čemer vrne število pretvorjenih tokov. Upravlja tako z enim vnosom /Filter /LZWDecode kot z LZW, ki se pojavi znotraj polja verige filtrov, kjer se zamenja le povezava LZW, preostali del verige pa se ohrani. Parametri prediktorja (Predictor, Columns, Colors, BitsPerComponent) se preberejo iz tokovnih DecodeParms in prenesejo v dekompresor, tako da se s prediktorjem kodirani podatki slik pravilno prenesejo. Ker sta oba filtra natančna kodeka na ravni bitov, so dekomprimirani bajti pred pretvorbo in po njej enaki; spremeni se le stiskanje vsebnika, zato je ta korak varno izvajati brezpogojno na vsaki datoteki

V dokumentu brez tokov LZW klic preprosto vrne 0 in se ničesar ne dotakne, kar izrecno preizkuša regresijski paket knjižnice: na novo ustvarjena datoteka, ki vsebuje le Flate stiskanje, mora poročati o nič pretvorbah. To jamstvo no-op je pomembno, ko je ta korak del cevovoda, ki obdeluje na tisoče različnih datotek, nekaterih iz leta 2024 in nekaterih iz leta 1998

Celoten cevovod za optimizacijo velikosti v Delphiju

Trije koraki se združijo in tvorijo eno funkcijo nalaganja-optimizacije-shranjevanja, vrstni red pa je manj pomemben, kot bi pričakovali, saj delujejo na različnih vrstah objektov: pisavah, slikovnih XObjectih in filtrih tokov. Izvedba podnabora na prvem mestu je še vedno urejena izbira, saj je to korak z omejitvijo vrstnega reda urejanja

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;

Preverite cevovod tako, kot se preverja sama knjižnica: s povratnim potovanjem (round-trip). Regresijski testi različice v3.130 ustvarijo dokument, ga shranijo, znova naložijo, zaženejo optimizacijo, znova shranijo in nato potrdijo tri stvari: izhod je manjši, vrnjeno štetje ustreza pričakovanjem, ponovno naložena optimizirana datoteka pa se še vedno analizira in prikazuje. Poustvarjanje te zanke ustvari-optimiziraj-ponovno-naloži na vzorcu vaših lastnih produkcijskih datotek ter primerjava izvlečenega besedila pred pretvorbo in po njej je enourna naložba, ki ujame napake integracije dolgo preden stranka odpre poškodovan račun

// 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;

Kje se cevovod nahaja v delovnem toku združevanja? Po združevanju in ne med njim. Združevanje najprej in nato optimizacija enega rezultata pomeni, da se vsaka vgrajena pisava podnabori enkrat glede na unijo vseh uporabljenih znakov, namesto za vsako izvorno datoteko posebej. Če je ozko grlo prepustnost združevanja, ponuja PDFlibPas hitro pot na ravni bajtov, ki se izogne popolni analizi objektov, opisano v članku o hitrem združevanju PDF-jev s premikom bajtnih sklicev v Delphiju; za vhode, ki so preveliki za pomnilnik, pa neposreden dostop do združevanja in razdeljevanju velikih PDF-jev pokriva pretočno pot. Obe možnosti se naravno ujemata s končnim korakom optimizacije na združenem izhodu

Česa trije koraki ne bodo naredili

Optimizacijski trio losLab PDF Library namerno izključuje vse, kar spreminja semantiko dokumenta. Klic SubsetEmbeddedFonts ne združuje podvojenih pisav med združenimi viri v en program, temveč vsako zmanjša neodvisno; združevanje podvojenih podatkov je drugačna, bolj tvegana pretvorba. Klic DownsampleImages bo šel mimo slike, katere konservativna ocena DPI ostaja pod pragom, tudi ko bi človek lahko ugotovil, da je prevelika za svoj okvir. In noben od teh korakov se ne dotika strukture dokumenta, zato datoteka, ki je prevelika zaradi tisočih osirotelih objektov, potrebuje shranjevanje z vnovičnim zapisom (rewrite) namesto teh korakov na ravni toka. V teh mejah kombinacija podnabora pisav, zmanjševanja ločljivosti slik in normalizacije iz LZW v Flate odstrani tri klasične vire prevelikih datotek PDF z enim samim klicem API-ja za vsakega od njih. Te tri funkcije so del knjižnice losLab PDF Library za Delphi, C# in VB.NET, skupaj z API-ji za združevanje, ekstrakcijo in upodabljanje, o katerih smo govorili zgoraj