Techninis straipsnis

PDF failo dydžio sumažinimas Delphi aplinkoje: šriftai, vaizdai, LZW

Norėdami sumažinti PDF failo dydį Delphi aplinkoje, „losLab PDF Library“ pateikia tris API, kurios nukreiptos į tris didžiausius išsipūtimo šaltinius: SubsetEmbeddedFonts perrašo kiekvieną įterptą „TrueType“ šrifto programą iki glifų, kuriuos dokumentas iš tikrųjų atvaizduoja, DownsampleImages pakeičia taškinių vaizdų, viršijančių tikslinį DPI, raišką, o NormalizeLZWStreams pakeičia senąjį LZWDecode suspaudimą FlateDecode. Kiekviena funkcija grąžina pakeistų objektų skaičių, todėl nulis rodo, kad veiksmas neatliko jokių pakeitimų, o ne tylią nesėkmę

Sujungtas arba programiškai sugeneruotas PDF failas dažniausiai būna per didelis dėl vienos iš trijų priežasčių: pilnai įterptų šriftų, vaizdų, kurių raiška yra daug didesnė nei jų rodymo raiška, ir srautų, kurie vis dar suspausti senoviniu LZW filtru. ISO 32000-1 §9.9 leidžia kūrėjui įterpti pilną šrifto programą, ir dauguma jų daro būtent tai, nes tai yra saugi numatytoji parinktis. Pilnas „Arial“ FontFile2 failas užima kelis šimtų kilobaitų; įterpkite jį į keliolika šaltinio failų, sujunkite juos ir nešiositės keliolika kopijų glifų kontūrų simboliams, kurių niekas neįvedė. Pats sujungimas nesukuria atliekų, jis tik sutelkia jas į vieną failą, kur galiausiai matomas bendras dydis

Vaizdai yra antrasis kaltininkas. 4800 pikselių pločio nuskaitytas vaizdas, patalpintas į ketvirčio puslapio rėmelį, pateikia maždaug 40 kartų daugiau taškų duomenų nei gali sunaudoti 300 DPI spausdinimo procesas. Trečiasis yra tylesnis: srautai, filtruoti naudojant LZWDecode. ISO 32000-1 §7.4.4 apibrėžia tiek LZWDecode, tiek FlateDecode ir pažymi, kad Flate paprastai suspaudžia bent jau taip pat gerai; praktikoje Flate išvestis yra nuosekliai mažesnė tiems patiems duomenims, o LZW išlieka dažniausiai failuose, kurie tam tikru istorijos momentu buvo apdoroti dešimtojo dešimtmečio įrankiais. Likusioje straipsnio dalyje aprašomi trys „losLab PDF Library“ žingsniai, ištaisantys kiekvieną problemą, o po to jie sujungiami į vieną srautą

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

Sujungus PDF failus dydis gali padidėti dėl pasikartojančių šriftų, vaizdų ir srautų, net jei kiekvienas šaltinis atskirai buvo mažas. Prieš optimizuojant verta atskirti šriftų dubliavimą, vaizdų raišką ir srautų filtrus, nes kiekvienai priežasčiai reikia kito veiksmo

Šriftų subgrupavimas naudojant „SubsetEmbeddedFonts“

SubsetEmbeddedFonts sumažina kiekvieną įterptą „TrueType“ šriftą įkeltame dokumente iki simbolių, kuriuos dokumentas iš tikrųjų naudoja, ir tam nereikia jokių argumentų, nes išsaugotinų simbolių sąrašą ji gauna iš pačių turinio srautų. Viduje funkcija pereina kiekvieno puslapio turinio srautą su GetTextRuns, surenka simbolių kodus, nurodytus prie kiekvieno šrifto ištekliaus, sukuria išsaugotinų simbolių sąrašą ir atiduoda pradinę šrifto programą „Windows FontSub“ varikliui (CreateFontPackage), kad sukurtų subgrupę. Perrašyta programa pakeičia FontFile2 srautą vietoje, o BaseFont pavadinimas gauna žymą LOSABC+ – šešių didžiųjų raidžių ir pliuso ženklo konvenciją, kurią ISO 32000-1 §9.6.4 apibrėžia subgrupuotiems šriftams. Šis prefiksas taip pat užtikrina funkcijos idempotentiškumą: paleiskite žingsnį du kartus ir jau subgrupuoti šriftai bus atpažinti bei praleisti, todėl saugu ją įtraukti į grupinį darbą, kuris gali iš naujo apdoroti failus

Verta žinoti dvi įgyvendinimo detales, nes jos paaiškina API ribas. Pirma, žingsnis nukreiptas į FontFile2, rodo, kad jis apima įterptas „TrueType“ programas; šriftai, įterpti kaip „Type 1“ arba grynas CFF, paliekami nepaliesti, kad būtų išvengta rizikos. Antra, ji remiasi „FontSub“, todėl SubsetEmbeddedFonts veikia tik „Windows“ aplinkoje. Subtilesnė įgyvendinimo detalė: ar šriftas yra tinkamas, nustatoma iš tikrųjų išsprendus FontDescriptorFontFile2 nuorodų grandinę, o ne pasitikint įterpimo vėliavos heuristika, nes šriftai įkeltame dokumente niekada nepraėjo kūrimo pusės apskaitos, kuri nustato tokias vėliavas. Jei išspręstas srautas egzistuoja, šriftas yra kandidatas; jei ne, jis praleidžiamas be klaidos

Sąžiningas kompromisas: subgrupuotas šriftas turi tik tuos glifus, kurie buvo subgrupavimo metu. Jei vėlesnis įrankis arba jūsų pačių kodas vėliau prideda tekstą tuo pačiu šriftu, bet koks simbolis už subgrupės ribų neturės kontūro ir bus atvaizduojamas kaip trūkstamas glifas. Atlikite subgrupavimą kaip paskutinį turinio keitimo žingsnį, niekada prieš redagavimo etapą. Tas pats atsargumas taikomas, jei planuojate vėliau išgauti šriftą pakartotiniam naudojimui; straipsnyje apie teksto, vaizdų ir šriftų išgavimą naudojant PDFlibPas aptariama, ką gali ir ko negali suteikti išgauta subgrupės programa

Kaip „DownsampleImages“ nusprendžia, kuriuos vaizdus sumažinti?

DownsampleImages(MaxDPI, Quality, Filter) pakeičia tik tų vaizdų raišką, kuriuos ji gali užtikrintai pavadinti per didelės raiškos vaizdais, naudodama sąmoningai konservatyvų DPI įvertinimą. PDF vaizdo XObject saugo pikselių matmenis, bet neturi patikimos fizinės raiškos, o bet kokia šaltinio vaizdo DPI žyma retai išlieka po įkėlimo-redagavimo-išsaugojimo ciklo. Todėl žingsnis įvertina SrcDPI = PixelWidth / 8.5, iš esmės klausdamas: jei šis vaizdas apimtų visą „Letter“ puslapio plotį, kokia būtų jo raiška? Keičiami tik tie vaizdai, kurių įvertinimas viršija MaxDPI. Šis šališkumas yra tyčinis: vaizdas, patalpintas mažas puslapyje, turi tikrąjį DPI, didesnį už įvertinimą, todėl žingsnis veikiau nesuveikia nei sugadina spaudos kokybės objektą, kurio negali išmatuoti

Quality nuo 1 iki 100 pasirenka JPEG pakartotinio kodavimo kokybę, o 0 išlaiko išvestį kaip beprarastį PNG stiliaus Flate; Filter pasirenka raiškos keitimo branduolį: 0 dėžutės vidurkiui ir 1 dvitiesiniam (bilinear). Nuskenuotiems biuro dokumentams DownsampleImages(150, 75, 1) yra protingas atspirties taškas; viskam, kas gali būti perspausdinta, pakelkite MaxDPI iki 300 or praleiskite šį žingsnį. Raiškos sumažinimas yra vienintelis prarastasis žingsnis iš trijų, todėl jis turėtų būti už nustatymo, kurį jūsų vartotojai gali išjungti

Senų LZW srautų konvertavimas naudojant „NormalizeLZWStreams“

NormalizeLZWStreams yra nemokama pergalė: ji be praradimų išspaudžia kiekvieną LZWDecode srautą ir vietoje iš naujo suspaudžia jį su FlateDecode, grąžindama konvertuotų srautų skaičių. Ji apdoroja tiek vieną įrašą /Filter /LZWDecode, tiek LZW, esantį filtrų grandinės masyve, kur pakeičiama tik LZW grandis, o likusi grandinės dalis išsaugoma. Prediktoriaus parametrai (Predictor, Columns, Colors, BitsPerComponent) nuskaitomi iš srauto DecodeParms ir perduodami dekompresoriui, todėl prediktoriumi koduoti vaizdo duomenys teisingai atlieka ciklą. Kadangi abu filtrai yra bitų tikslumo kodekai, išspausti baitai yra identiški prieš ir po; keičiasi tik konteinerio suspaudimas, todėl šį žingsnį saugu vykdyti besąlygiškai kiekvienam failui

Dokumente, kuriame nėra LZW srautų, iškvietimas tiesiog grąžina 0 ir nieko neliečia, ką bibliotekos regresijos testų rinkinys aiškiai išbando: naujai sukurtas tik Flate failas turi pranešti apie nulį konversijų. Ši nieko nekeičianti garantija yra svarbi, kai žingsnis yra apdorojimo sraute, kuris apdoroja tūkstančius įvairialupių failų, kai kurie iš jų sukurti 2024 m., o kai kurie – 1998 m

Pilnas dydžio optimizavimo srautas Delphi aplinkoje

Trys žingsniai susijungia į vieną įkėlimo-optimizavimo-išsaugojimo funkciją, o tvarka yra mažiau svarbi nei galbūt tikitės, nes jie veikia su nesusijusiais objektų tipais: šriftais, vaizdo XObjects ir srautų filtrais. Subgrupavimo vykdymas pirmiausia vis tiek yra tvarkingas pasirinkimas, nes tai yra žingsnis su redagavimo tvarkos apribojimu

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;

Patikrinkite srautą taip, jak biblioteka tikrina pati save: ciklu (round-trip). Versijos v3.130 regresijos testai sukuria dokumentą, jį išsaugo, įkelia iš naujo, paleidžia optimizavimą, vėl išsaugo ir patvirtina tris dalykus: išvestis yra mažesnė, grąžinti skaičiai atitinka lūkesčius, o optimizuoto failo pakartotinis įkėlimas vis tiek leidžia jį analizuoti ir atvaizduoti. Šio sukūrimo-optimizavimo-pakartotinio įkėlimo ciklo atkūrimas su jūsų pačių gamybinių failų pavyzdžiu ir išgauto teksto palyginimas prieš ir po yra vienos valandos investicija, kuri padeda pastebėti integracijos klaidas dar gerokai prieš klientui atidarant sugadintą sąskaitą faktūrą

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

Kur šis srautas tinka sujungimo darbo eigoje? Po sujungimo, o ne jo metu. Pirmiausia sujungus ir optimizuojant vienintelį rezultatą, kiekvienas įterptas šriftas yra subgrupuojamas vieną kartą pagal visų naudotų simbolių sąjungą, o ne kiekvienam šaltinio failui. Jei sujungimo pralaidumas yra butelio kaklelis, „PDFlibPas“ siūlo baitų lygio greitąjį kelią, kuris leidžia išvengti pilno objektų analizavimo, aprašytą straipsnyje apie greitą PDF sujungimą su baitų nuorodų poslinkiu; o įvestims, kurios yra per didelės, kad jas būtų galima visiškai laikyti atmintyje, vadovas apie tiesioginės prieigos sujungimą ir skaidymą dideliems PDF failams aprašo srautinį kelią. Abu metodai natūraliai dera su galutiniu optimizavimo žingsniu sujungtoje išvestyje

Ko šie trys žingsniai nepadarys

„losLab PDF Library“ optimizavimo trijulė sąmoningai neapima nieko, kas keičia dokumento semantiką. SubsetEmbeddedFonts nesujungia dubliuotų šriftų visuose sujungtuose šaltiniuose į vieną programą, ji sumažina kiekvieną atskirai; dubliavimo pašalinimas yra kitoks, rizikingesnis transformavimas. DownsampleImages praleis vaizdą, kurio konservatyvus DPI įvertinimas lieka žemiau slenksčio, net jei žmogus matytų, kad jis yra per didelis savo rėmeliui. Be to, nei vienas iš žingsnių neliečia dokumento struktūros, todėl failui, išpūstam tūkstančių našlaičių objektų, reikalingas perrašymo stiliaus išsaugojimas, o ne šie srauto lygio žingsniai. Šiose ribose šriftų subgrupavimo, vaizdų raiškos mažinimo ir LZW-į-Flate normalizavimo derinys pašalina tris klasikinius PDF išsipūtimo šaltinius su vienu nuspėjamu API iškvietimu kiekvienam. Šios trys funkcijos pateikiamos kaip „losLab PDF Library“, skirtos Delphi, C# ir VB.NET, dalis kartu su sujungimo, išgavimo ir atvaizdavimo API, aptartomis aukščiau