Tehnički članak

Smanjenje veličine PDF datoteke u Delphiju: Fontovi, slike, LZW

Za smanjenje veličine PDF datoteke u Delphiju, losLab PDF Library nudi tri API-ja koji napadaju tri najveća izvora prevelike veličine datoteke: SubsetEmbeddedFonts prepisuje svaki ugrađeni program TrueType fonta smanjujući ga samo na glifove koje dokument stvarno prikazuje, DownsampleImages smanjuje uzorkovanje rasterskih slika koje premašuju ciljani DPI, a NormalizeLZWStreams zamenjuje zastarelu LZWDecode kompresiju sa FlateDecode. Svaki od njih vraća broj objekata koje je promenio, pa nula govori da je prolaz bio prazna operacija (no-op), a ne tihi neuspeh

Zašto je moj spojeni PDF veći od svojih izvornih datoteka?

Spojeni ili programski generisani PDF obično je prevelik iz jednog od tri razloga: potpuno ugrađenih fontova, slika uzorkovanih daleko iznad njihove rezolucije prikaza i tokova koji su još uvek komprimovani starim LZW filterom. ISO 32000-1 §9.9 dopušta proizvođaču ugradnju celovitog programa fonta, i većina proizvođača radi upravo to jer je to sigurna podrazumevana postavka. Celovita Arial FontFile2 datoteka teži stotinama kilobajta; ugradite je u desetak izvornih datoteka, spojite ih, i prenosite desetak kopija obrisa glifova za znakove koje niko nije upisao. Spajanje samo po sebi ne stvara otpad, ono ga samo koncentriše u jednu datoteku u kojoj ukupni iznos konačno postaje vidljiv

Slike su drugi krivac. Skenirani dokument širine 4800 piksela smešten u okvir od četvrtine stranice prenosi otprilike 40 puta više slikovnih podataka nego što pipeline za štampu od 300 DPI može da iskoristi. Treći je tiši: tokovi filtrirani sa LZWDecode. ISO 32000-1 §7.4.4 specificira i LZWDecode i FlateDecode, te napominje da Flate obično komprimuje barem jednako dobro; u praktičnom radu Flate izlaz je dosledno manji na istim podacima, a LZW preživljava uglavnom u datotekama koje su u nekom trenutku svoje istorije prošle kroz alate iz 1990-ih. Ostatak ovog članka prolazi kroz tri prolaza losLab PDF Library-ja koji rešavaju svaki problem, a zatim ih kombinuje u jedan pipeline

Izdvajanje podskupa fontova sa SubsetEmbeddedFonts

SubsetEmbeddedFonts smanjuje svaki ugrađeni TrueType font u učitanom dokumentu na znakove koje dokument stvarno koristi, i ne treba nikakve argumente jer popis znakova za zadržavanje izvodi iz samih tokova sadržaja. Interno, ovaj prolaz prolazi kroz tok sadržaja svake stranice sa GetTextRuns, prikuplja kodove znakova referencirane pod svakim resursom fonta, gradi popis za zadržavanje i predaje izvorni program fonta Windows FontSub mehanizmu (CreateFontPackage) radi proizvodnje podskupa. Prepisani program zamenjuje tok FontFile2 na licu mesta, a naziv BaseFont dobija oznaku LOSABC+, konvenciju od šest velikih slova i znaka plus koju ISO 32000-1 §9.6.4 definiše za podskupove fontova. Taj je prefiks takođe ono što poziv čini idempotentnim: pokrenite prolaz dvaput i već izdvojeni podskupovi fontova biće prepoznati i preskočeni, pa je njihovo povezivanje u serijski posao koji može ponovo posetiti datoteke sigurno

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 detalja implementacije vredi znati jer objašnjavaju granice API-ja. Prvo, prolaz cilja na FontFile2, tako da pokriva ugrađene TrueType programe; fontovi ugrađeni kao Type 1 ili bare CFF ostaju netaknuti kako se ne bi rizikovalo. Drugo, oslanja se na FontSub, što čini SubsetEmbeddedFonts dostupnim samo za Windows. Suptilnija tačka iz implementacije: kvalifikuje li se font odlučuje se stvarnim razrešavanjem referentnog lanca FontDescriptorFontFile2, a ne poverenjem u heuristiku zastavice ugradnje, jer fontovi u učitanom dokumentu nikada nisu prošli kroz knjigovodstvo na strani stvaranja koje postavlja takve zastavice. Ako razrešeni tok postoji, font je kandidat; ako ne, preskače se bez greške

Iskreni kompromis: podskup fonta sadrži samo glifove prisutne u trenutku izdvajanja podskupa. Ako nizvodni alat, ili vaš sopstveni kod, kasnije doda tekst u tom istom fontu, bilo koji znak van podskupa neće imati obris i prikazaće se kao nedostajući glif. Izdvajajte podskupove kao poslednji korak promene sadržaja, nikada pre faze uređivanja. Isti oprez važi i ako planirate kasnije ponovo da izvučete font radi ponovne upotrebe; članak o izdvajanju teksta, slika i fontova pomoću PDFlibPas pokriva šta vam izvučeni program podskupa može, a šta ne može dati

Kako DownsampleImages odlučuje koje slike treba smanjiti?

DownsampleImages(MaxDPI, Quality, Filter) ponovno uzorkuje samo one slike koje može sa sigurnošću nazvati preuzorkovanim, koristeći namerno konzervativnu procenu DPI-ja. PDF slikovni XObject pohranjuje dimenzije piksela, ali ne i pouzdanu fizičku rezoluciju, a bilo koja oznaka DPI iz izvorne slike retko preživi ciklus učitavanja-uređivanja-čuvanja. Stoga prolaz procenjuje SrcDPI = PixelWidth / 8.5, zapravo pitajući: ako bi ova slika obuhvatala punu širinu Letter stranice, kolika bi bila njena rezolucija? Dotiču se samo slike čija procena premašuje MaxDPI. Pristrasnost je namerna: slika postavljena mala na stranici ima stvarni DPI viši od procene, pa se prolaz radije nedovoljno aktivira nego da pogorša kvalitet štampe resursa koji ne može da izmeri

Quality od 1 do 100 odabire kvalitet ponovnog kodiranja JPEG-a, dok 0 zadržava izlaz kao Flate bez gubitaka u stilu PNG-a; Filter bira jezgro ponovnog uzorkovanja, 0 za prosek kutije (box average) i 1 za bilinearno. Za skeniranu kancelarijsku dokumentaciju, DownsampleImages(150, 75, 1) je razumna početna tačka; za sve što se može ponovo štampati, podignite MaxDPI na 300 ili u potpunosti preskočite prolaz. Smanjivanje uzorkovanja je jedini korak sa gubicima od ova tri, pa mu je mesto iza postavke koju vaši korisnici mogu isključiti

Pretvaranje starih LZW tokova sa NormalizeLZWStreams

NormalizeLZWStreams je besplatni dobitak: bez gubitaka dekomprimuje svaki LZWDecode tok i ponovno ga komprimuje sa FlateDecode, na licu mesta, vraćajući broj pretvorenih tokova. Rukuje i pojedinačnim unosom /Filter /LZWDecode i LZW-om koji se pojavljuje unutar niza lanca filtera, gde se zamenjuje samo LZW karika, a ostatak lanca se čuva. Parametri prediktora (Predictor, Columns, Colors, BitsPerComponent) čitaju se iz DecodeParms toka i prosleđuju dekompresoru, tako da se slikovni podaci kodirani prediktorom ispravno vraćaju. Budući da su oba filtera bit-tačni kodeci, dekodovani bajtovi su identični pre i posle; menja se samo kompresija kontejnera, zbog čega je ovaj prolaz sigurno pokrenuti bezuslovno na svakoj datoteci

Na dokumentu bez LZW tokova poziv jednostavno vraća 0 i ne dira ništa, što regresioni paket biblioteke izričito proverava: sveže stvorena datoteka koja sadrži samo Flate mora prijaviti nula pretvaranja. To garantuje praznu operaciju (no-op) kada se prolaz nalazi u pipeline-u koji obrađuje hiljade heterogenih datoteka, neke iz 2024. a neke iz 1998

Kompletni pipeline za optimizaciju veličine u Delphiju

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;

Provjerite pipeline na način na koji biblioteka proverava samu sebe: povratno putovanje (round-trip). Regresioni testovi verzije v3.130 stvaraju dokument, čuvaju ga, ponovo učitavaju, pokreću optimizaciju, ponovo čuvaju, a zatim potvrđuju tri stvari: izlaz je manji, vraćeni brojevi odgovaraju očekivanjima, a ponovno učitavanje optimizovane datoteke i dalje je analizira i renderuje. Reprodukcija te petlje stvori-optimizuj-ponovno_učitaj na uzorku sopstvenih produkcijskih datoteka i upoređivanje izdvojenog teksta pre i posle investicija je od jednog sata koja hvata integracijske greške mnogo pre nego što kupac otvori neispravan 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;

Gde se pipeline uklapa u tok rada spajanja? Nakon spajanja, ne tokom njega. Prvo spajanje i optimizacija jedinstvenog rezultata znači da se svaki ugrađeni font jednom izdvoji u podskup u odnosu na uniju svih korišćenih znakova, umesto po izvornoj datoteci. Ako je propusnost spajanja usko grlo, PDFlibPas nudi brzi put na nivou bajtova koji izbegava potpunu analizu objekta, opisan u članku o brzom spajanju PDF-a sa pomeranjem referenci bajtova; a za ulaze prevelike da bi se u potpunosti držali u memoriji, spajanje i deljenje sa direktnim pristupom za velike PDF-ove pokriva put strujanja. Oba se prirodno uparuju sa konačnim prolazom optimizacije na spojenom izlazu

Što ova tri prolaza neće učiniti

Optimizacioni trojac losLab PDF Library-ja namerno isključuje sve što menja semantiku dokumenta. SubsetEmbeddedFonts ne objedinjuje duplicirane fontove iz spojenih izvora u jedan program, već smanjuje svaki nezavisno; deduplikacija je drugačija, rizičnija transformacija. DownsampleImages će zaobići sliku čija konzervativna procena DPI-ja ostaje ispod praga čak i kada bi čovek mogao reći da je prevelika za svoj okvir. I niti jedan od prolaza ne dira strukturu dokumenta, pa datoteka napuhnuta hiljadama napuštenih objekata zahteva čuvanje u stilu prepisivanja, a ne ove prolaze na nivou toka. Unutar tih granica, kombinacija izdvajanja podskupa fontova, smanjenja uzorkovanja slika i normalizacije LZW u Flate uklanja tri klasična izvora napuhavanja PDF-a sa po jednim predvidljivim pozivom API-ja. Tri funkcije dolaze kao deo losLab PDF Library-ja za Delphi, C# i VB.NET, zajedno sa API-jima za spajanje, izdvajanje i renderiranje o kojima se raspravljalo gore