Techninis straipsnis

PDF dydžio mažinimas Delphi: šriftai, paveikslėliai, LZW

PDF failo dydžiui Delphi aplinkoje sumažinti losLab PDF Library siūlo tris API, kurios puola tris didžiausius išsipūtimo šaltinius: SubsetEmbeddedFonts perrašo kiekvieną įterptą TrueType šrifto programą, palikdamas tik tuos glifus, kuriuos dokumentas iš tikrųjų atvaizduoja, DownsampleImages persampuoja taškinius paveikslėlius, viršijančius tikslinį DPI, o NormalizeLZWStreams pakeičia senovinį LZWDecode glaudinimą į FlateDecode. Kiekviena grąžina pakeistų objektų skaičių, todėl nulis pasako, kad praėjimas nieko nerado, o ne kad jis tyliai žlugo

Kodėl sujungtas PDF yra didesnis už savo šaltinio failus?

Sujungtas arba programiškai sugeneruotas PDF paprastai būna per didelis dėl vienos iš trijų priežasčių: pilnai įterptų šriftų, paveikslėlių, sampuotų kur kas aukščiau nei jų rodymo raiška, ir srautų, vis dar suglaudintų senovine LZW filtracija. ISO 32000-1 §9.9 leidžia gamintojui įterpti visą šrifto programą, ir dauguma gamintojų būtent taip ir daro, nes tai saugus numatytasis pasirinkimas. Pilna Arial FontFile2 užima šimtus kilobaitų; įterpkite ją į tuziną šaltinio failų, sujunkite juos ir nešiositės tuziną glifų kontūrų kopijų simboliams, kurių niekas neįvedė. Pats sujungimas švaistymo nesukuria, jis tik sutelkia jį į vieną failą, kuriame bendra suma pagaliau tampa matoma

Paveikslėliai yra antrasis kaltininkas. 4800 pikselių pločio skenuotė, įdėta į ketvirtadalio puslapio rėmelį, atgabena maždaug 40 kartų daugiau pikselių duomenų, nei gali panaudoti 300 DPI spausdinimo grandinė. Trečiasis kaltininkas tylesnis: srautai, filtruoti LZWDecode. ISO 32000-1 §7.4.4 apibrėžia ir LZWDecode, ir FlateDecode bei pažymi, kad Flate paprastai glaudina bent jau taip pat gerai; praktikoje Flate išvestis su tais pačiais duomenimis nuosekliai būna mažesnė, o LZW daugiausia išlieka failuose, kurie kada nors savo istorijoje perėjo per dešimtojo dešimtmečio įrankius. Likusi šio straipsnio dalis pereina per tris losLab PDF Library praėjimus, taisančius kiekvieną problemą, o tada sujungia juos į vieną grandinę

Apžvalginė diagrama, siejanti sujungto PDF išsipūtimo šaltinius su PDF Library for Delphi optimizavimo praėjimais šriftams, paveikslėliams ir LZW srautams
Kiekvienas praėjimas taikosi į vieną klasikinį išsipūtimo šaltinį ir grąžina, kiek objektų perrašė, o nulis praneša apie jau liekną failą, ne apie tylų žlugimą

Šriftų poaibiai su SubsetEmbeddedFonts

SubsetEmbeddedFonts sutraukia kiekvieną įkelto dokumento įterptą TrueType šriftą iki tų simbolių, kuriuos dokumentas iš tikrųjų naudoja, ir jam nereikia jokių argumentų, nes paliekamų glifų sąrašą jis išveda iš pačių turinio srautų. Viduje praėjimas su GetTextRuns apeina kiekvieno puslapio turinio srautą, surenka simbolių kodus, minimus prie kiekvieno šrifto ištekliaus, sudaro paliekamų glifų sąrašą ir perduoda pradinę šrifto programą Windows FontSub varikliui (CreateFontPackage), kad šis pagamintų poaibį. Perrašyta programa pakeičia FontFile2 srautą vietoje, o BaseFont vardas gauna LOSABC+ žymą, tai yra šešių didžiųjų raidžių ir pliuso konvenciją, kurią ISO 32000-1 §9.6.4 apibrėžia poaibiniams šriftams. Būtent tas priešdėlis daro iškvietimą idempotentinį: paleidus praėjimą du kartus, jau poaibiniai šriftai atpažįstami ir praleidžiami, todėl jį saugu įpinti į paketinį darbą, kuris gali grįžti prie tų pačių failų

PDF Library for Delphi grandinė, rodanti, kaip losLab PDF Library iš teksto atkarpų išveda paliekamų glifų sąrašą ir per FontSub variklį pagamina pažymėtą poaibinį šriftą
SubsetEmbeddedFonts apeina kiekvieno puslapio teksto atkarpas, paduoda išvestą glifų sąrašą FontSub varikliui, pažymi perrašytas programas LOSABC+ žyma ir saugiai jas praleidžia vėlesniuose paleidimuose
var
  Lib: TPDFlib;
  Fonts: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('merged-report.pdf', '') = 1 then
    begin
      Fonts := Lib.SubsetEmbeddedFonts;
      // Fonts = perrašytų FontFile2 programų skaičius;
      // 0 reiškia, kad nieko neįterpta arba viskas jau poaibinta
      Lib.SaveToFile('merged-report-subset.pdf');
    end;
  finally
    Lib.Free;
  end;
end;

Dvi įgyvendinimo smulkmenos vertos dėmesio, nes jos paaiškina šio API ribas. Pirma, praėjimas taikosi į FontFile2, taigi apima įterptas TrueType programas; šriftai, įterpti kaip Type 1 ar plikas CFF, paliekami neliesti, o ne rizikuojami. Antra, jis remiasi FontSub, todėl SubsetEmbeddedFonts veikia tik Windows aplinkoje. Subtilesnis dalykas iš įgyvendinimo: ar šriftas tinka, sprendžiama iš tikrųjų išsprendžiant FontDescriptor → FontFile2 nuorodų grandinę, o ne pasitikint įterpimo požymio euristika, nes įkelto dokumento šriftai niekada nepraėjo kūrimo pusės apskaitos, kuri tokius požymius nustato. Jei išspręstas srautas egzistuoja, šriftas yra kandidatas; jei ne, jis praleidžiamas be klaidos

Sąžininga kaina tokia: poaibinis šriftas turi tik tuos glifus, kurie egzistavo poaibinimo metu. Jei vėliau koks nors tolesnis įrankis ar jūsų pačių kodas prideda teksto tuo pačiu šriftu, bet kuris už poaibio ribų esantis simbolis neturės kontūro ir bus atvaizduotas kaip trūkstamas glifas. Poaibinkite kaip paskutinį turinį keičiantį žingsnį, niekada ne prieš redagavimo etapą. Ta pati atsarga galioja, jei planuojate šriftą vėliau ištraukti pakartotiniam naudojimui; straipsnis apie teksto, paveikslėlių ir šriftų ištraukimą su PDF Library for Delphi aptaria, ką ištraukta poaibinė programa gali ir ko negali jums duoti

Kaip DownsampleImages nusprendžia, kuriuos paveikslėlius mažinti?

DownsampleImages(MaxDPI, Quality, Filter) persampuoja tik tuos paveikslėlius, kuriuos gali užtikrintai pavadinti per daug sampuotais, ir naudoja sąmoningai konservatyvų DPI įvertį. PDF paveikslėlio XObject saugo pikselių matmenis, bet jokios patikimos fizinės raiškos, o bet kokia DPI žyma iš pradinio paveikslėlio retai išgyvena įkėlimo, redagavimo ir įrašymo ciklą. Todėl praėjimas įvertina SrcDPI = PixelWidth / 8.5, iš esmės klausdamas: jei šis paveikslėlis apimtų visą Letter puslapio plotį, kokia būtų jo raiška? Liečiami tik tie paveikslėliai, kurių įvertis viršija MaxDPI. Šis nuokrypis yra tyčinis: puslapyje nedideliu formatu įdėtas paveikslėlis turi tikrą DPI, didesnį už įvertį, todėl praėjimas verčiau suveikia per retai, nei sugadina spausdinimo kokybės medžiagą, kurios negali išmatuoti

Quality nuo 1 iki 100 parenka JPEG perkodavimo kokybę, o 0 palieka išvestį kaip nenuostolingą PNG tipo Flate; Filter parenka persampavimo branduolį: 0 reiškia dėžutės vidurkį, 1 – tiesinę interpoliaciją. Skenuotiems biuro dokumentams DownsampleImages(150, 75, 1) yra protingas atspirties taškas; visam kam, kas gali būti spausdinama iš naujo, pakelkite MaxDPI iki 300 arba praėjimą praleiskite visai. Raiškos mažinimas yra vienintelis nuostolingas žingsnis iš trijų, todėl jo vieta yra už nustatymo, kurį jūsų naudotojai gali išjungti

Delphi PDF paveikslėlių raiškos mažinimo sprendimų eiga, lyginanti konservatyvų SrcDPI įvertį su MaxDPI prieš persampuojant paveikslėlį
Konservatyvus DPI įvertis daro prielaidą, kad paveikslėlis apima visą Letter puslapį, todėl persampuojami tik tie paveikslėliai, dėl kurių biblioteka tikra, o ribiniai atvejai lieka neliesti

Senų LZW srautų vertimas su NormalizeLZWStreams

NormalizeLZWStreams yra nemokamas laimėjimas: jis nenuostolingai išglaudina kiekvieną LZWDecode srautą ir vietoje perglaudina jį FlateDecode, grąžindamas konvertuotų srautų skaičių. Jis susitvarko ir su vienu /Filter /LZWDecode įrašu, ir su LZW, esančiu filtrų grandinės masyve, kur pakeičiama tik LZW grandis, o likusi grandinė išsaugoma. Prediktoriaus parametrai (Predictor, Columns, Colors, BitsPerComponent) nuskaitomi iš srauto DecodeParms ir perduodami išglaudinimo varikliui, todėl prediktoriumi koduoti paveikslėlio duomenys apsuka ratą teisingai. Kadangi abu filtrai yra bitas į bitą tikslūs kodekai, iškoduoti baitai prieš ir po yra identiški; keičiasi tik konteinerio glaudinimas, ir būtent todėl šį praėjimą saugu leisti besąlygiškai kiekvienam failui

Dokumentui be LZW srautų iškvietimas tiesiog grąžina 0 ir nieko neliečia, o bibliotekos regresijos rinkinys tai tikrina atvirai: ką tik sukurtas vien Flate failas privalo pranešti nulį konversijų. Ta nieko nedarymo garantija svarbi, kai praėjimas sėdi grandinėje, apdorojančioje tūkstančius nevienalyčių failų, vienus iš 2024, kitus iš 1998 metų

Pilna dydžio optimizavimo grandinė Delphi aplinkoje

Trys praėjimai susijungia į vieną įkėlimo, optimizavimo ir įrašymo funkciją, o tvarka svarbi mažiau, nei galėtumėte tikėtis, nes jie dirba su nepersidengiančiais objektų tipais: šriftais, paveikslėlių XObject ir srautų filtrais. Poaibinimą vis dėlto tvarkingiau leisti pirmą, nes būtent jis turi redagavimo tvarkos apribojimą

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 -> poaibis
    Images  := Lib.DownsampleImages(150, 75, 1); // >150 DPI -> JPEG q75, tiesinis
    Streams := Lib.NormalizeLZWStreams;        // LZWDecode -> FlateDecode
    Result := Lib.SaveToFile(Dst) = 1;
    // Registruokite Fonts/Images/Streams: trys nuliai reiškia, kad failas jau buvo lieknas
  finally
    Lib.Free;
  end;
end;

Grandinę tikrinkite taip, kaip save tikrina pati biblioteka: apsukdami ratą. v3.130 regresijos testai sukuria dokumentą, jį įrašo, vėl įkelia, paleidžia optimizavimą, įrašo dar kartą, o tada tvirtina tris dalykus: kad išvestis mažesnė, kad grąžinti skaičiai atitinka lūkesčius ir kad optimizuoto failo pakartotinis įkėlimas vis dar išanalizuojamas bei atvaizduojamas. To sukūrimo, optimizavimo ir pakartotinio įkėlimo ciklo atkartojimas su jūsų pačių gamybinių failų imtimi, palyginant ištrauktą tekstą prieš ir po, yra vienos valandos investicija, sugaunanti integracijos klaidas gerokai anksčiau, nei klientas atsivers sugadintą sąskaitą

// Rato patikra: optimizuotas failas privalo įkeltis švariai
Lib := TPDFlib.Create;
try
  Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
  Assert(Lib.GetPageCount > 0);
finally
  Lib.Free;
end;

Kur ši grandinė įsiterpia į sujungimo darbo eigą? Po sujungimo, ne jo metu. Sujungus pirma ir optimizavus vieną rezultatą, kiekvienas įterptas šriftas poaibinamas vieną kartą pagal visų panaudotų simbolių sąjungą, o ne kiekvienam šaltinio failui atskirai. Jei siaura vieta yra sujungimo našumas, PDF Library for Delphi siūlo baitų lygio greitąjį kelią, išvengiantį pilno objektų analizavimo, aprašytą straipsnyje apie greitą PDF sujungimą su baitinių nuorodų postūmiu; o įvestims, per didelėms, kad tilptų atmintyje, tiesioginės prieigos sujungimas ir skaidymas dideliems PDF aptaria srautinį kelią. Abu natūraliai dera su baigiamuoju optimizavimo praėjimu ant sujungtos išvesties

Ko šie trys praėjimai nedarys

losLab PDF Library optimizavimo trejetas sąmoningai neapima nieko, kas keistų dokumento semantiką. SubsetEmbeddedFonts nesuvienija pasikartojančių šriftų iš sujungtų šaltinių į vieną programą, jis kiekvieną sutraukia atskirai; dublikatų šalinimas yra kita, rizikingesnė transformacija. DownsampleImages praleis paveikslėlį, kurio konservatyvus DPI įvertis lieka žemiau slenksčio, net kai žmogus matytų, kad jis per didelis savo rėmeliui. Ir nė vienas praėjimas neliečia dokumento struktūros, todėl failui, išpūstam tūkstančių našlaičių objektų, reikia perrašančio įrašymo, o ne šių srautų lygio praėjimų. Šiose ribose šriftų poaibinimo, paveikslėlių raiškos mažinimo ir LZW keitimo į Flate derinys pašalina tris klasikinius PDF išsipūtimo šaltinius po vieną nuspėjamą API iškvietimą kiekvienam. Šios trys funkcijos pateikiamos kaip losLab PDF Library, skirtos Delphi, C# ir VB.NET, dalis kartu su aukščiau aptartais sujungimo, ištraukimo ir atvaizdavimo API