Techninis straipsnis

HotPDF CompressDocument: kompaktiški šriftų poaibiai Delphi

HotPDF THotPDF.CompressDocument yra vienas jungiklis, kuris priverčia BeginDoc pagaminti mažiausią be nuostolių PDF, kokį komponentas sugeba parašyti: FlateDecode maksimaliu lygiu, kryžminių nuorodų srautą su objektų srautais, šriftų poaibių sudarymą ir kompaktiškus šriftų poaibius, kurie paliktus glifus pernumeruoja už atviro /CIDToGIDMap. Tada EndDoc sugrąžina jūsų pačių nustatymus. Trys puslapiai Arial ir SimSun testinio dokumento nukrito nuo 10.2 MB iki 20 KB su identišku atvaizdavimu

Ką CompressDocument iš tikrųjų įjungia?

CompressDocument vienam dokumentui perrašo šešis rašytojo nustatymus, plus objektų srautų ribą, ir vėliau visus juos atkuria. BeginDoc metu, dar prieš apsispręsdama PDF versiją, HotPDF įsimena jūsų reikšmes ir nustato Compression į cmFlateDecode, CompressionLevel į clMaximum, įjungia EnableFontSubsetting ir CompactFontSubsetting bei paleidžia UseXRefStream ir UseObjectStreams (ISO 32000-1 §7.5.7 ir §7.5.8). Objektų srautams reikia PDF 1.5, tad senesnė Version pakeliama iki 1.5, kai ji neužrakinta. PDF/A-1 abu variantus draudžia, todėl PDF/A-1 dokumentas lieka prie klasikinės kryžminių nuorodų lentelės ir gauna tik Flate bei šriftų darbus. Vaizdai paliekami lygiai tokie, kokie buvo įdėti

HotPDF CompressDocument gyvavimo ciklo Delphi diagrama: BeginDoc įsimena rašytojo pačio reikšmes, vienam dokumentui perrašo šešis nustatymus, įskaitant Compression ir UseObjectStreams, o EndDoc kiekvieną pasiskolintą reikšmę atkuria savo išoriniausiame finally, kol pats CompressDocument atributas lieka True
Šeši rašytojo nustatymai ir objektų srautų riba pasiskolinami lygiai vienam dokumentui ir grąžinami, kai suveikia EndDoc, tad sugedusi ataskaita niekada nepalieka komponento įstrigusio maksimaliame glaudinime
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := 'invoice-2026-1042.pdf';
    Pdf.CompressDocument := True;    // pritaiko BeginDoc, atšaukia EndDoc
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-1042');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Atkūrimas vyksta išoriniame EndDoc finally, todėl išimtis ataskaitos viduryje nepalieka ilgaamžio komponento įstrigusio prie maksimalaus glaudinimo kitam darbui. Pats CompressDocument atributas lieka True; sugrįžta tik šeši jo pasiskolinti nustatymai. Su versija elgiamasi atidžiau. HotPDF savo pakėlimą iki 1.5 atšaukia tik tada, kai dokumentas vis dar baigiasi ties 1.5, tad kai kitas bruožas paleidimo metu patraukė failą iki 1.6 (tarkim, įdėtas OpenType šriftas), aukštesnė versija lieka – lygiai taip, kaip būtų be glaudinimo

Kodėl šriftų poaibiai be kompaktiškumo vis tiek dideli?

Klasikinis TrueType poaibis meta kontūrus, kurių niekada nededate, bet palieka kiekvieną glifo ID ten, kur jis buvo, ir būtent tas numeravimas jį laiko storą. Turinio srautas rodo CID, lygius originaliems GID, tad poaibiui tenka laikyti loca poslinkį ir hmtx įrašą kiekvienai vietai iki aukščiausio palikto glifo – tuščiai ar ne. Lotyniškam šriftui ta antkaina yra triukšmas. CJK šriftui, tokiam kaip SimSun, kurio ideogramos sėdi gilioje didžiulėje glifo lentelėje, du kinų simboliai vežioja lenteles, pritaikytas visam šriftui. Šriftų poaibių uždarumo taisyklės formuotiems glifams nusprendžia, kurie glifai išgyvena; kompaktiškumas yra apie tai, kiek išgyvenėliams kainuoja

CompactFontSubsetting paliktus glifus pernumeruoja į tankų diapazoną, prasidedantį nuo nulio, ir ant CIDFont parašo /CIDToGIDMap srautą, kurį ISO 32000-1 §9.7.4.2 apibrėžia kaip dviejų baitų GID lentelę, indeksuotą CID. Ta lentelė yra visas triukas. Turinio srautai, /W pločių masyvas ir ToUnicode CMap visi laiko originalius CID, tad parašyto nieko keisti nereikia; tik paieška iš CID į glifą persikelia į žemėlapį. Teste, kuris iškėlė šį bruožą, SimSun su dviem simboliais sumažėjo nuo 24.8 KB šriftų duomenų iki 3.1 KB

Retasis HotPDF šriftų poaibis, laikantis loca ir hmtx įrašus kiekvienam originaliam glifo ID iki aukščiausio palikto GID, lyginamas su CompactFontSubsetting išvestimi, kuri paliktus glifus tankiai pernumeruoja nuo nulio ir CID susieja per CIDToGIDMap srautą, kol turinio srautai, /W ir ToUnicode lieka nepakitę
Pernumeravimas perkelia kainą iš šriftų programos į vieną mažą žemėlapio srautą – du SimSun simboliai nukrito nuo 24.8 KB iki 3.1 KB nepaliendami nė vieno baito jau parašyto turinio

Kompaktiškumas turi kietas ribas ir gedimo atveju tyliai suprastėja, o ne kelia klaidos. HotPDF kompaktiškus poaibius stato tik Type 0 TrueType šriftams – ir tiems, nustatytiems per SetFont su įjungtu poaibių sudarymu, ir šriftui, užregistruotam per RegisterUnicodeTTF. Paprastas TrueType šriftas savo glifus randa per cmap šriftų programos viduje, kurį pernumeravimas sulaužytų, tad jis lieka prie reto poaibio. OpenType-CFF šriftams kompaktiško kelio taip pat nėra. Kompaktiška statyba, kuri nepavyksta, grįžta prie reto poaibio vietoj klaidos kelio. Atributas pagal nutylėjimą išjungtas, tad egzistuojanti išvestis lieka baitas į baitą identiška, o po PDF/A užregistruotas Unicode šriftas visuomet gauna kompaktišką poaibį

Pdf.EnableFontSubsetting := True;
Pdf.CompactFontSubsetting := True;   // naudinga ir be CompressDocument
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('SimSun', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, WideString('Total: '#$4E2D#$6587));
Pdf.EndDoc;

Kaip supakuotas rašytojas suspaudžia failo struktūrą?

Kai šriftai ir srautai sumažėja, žodynai ir kryžminių nuorodų duomenys tampa didžiausia likusia išlaida, tad objektų srautų rašytojas, stovintis už CompressDocument, ir juos apkarpą. Objektų srautų ir prieaugio atnaujinimų vadove aprašytas pats konteinerio formatas; glaudinimo kelias ant viršaus prideda keturis patobulinimus:

  • Kompaktiška sintaksė pagal ISO 32000-1 §7.2.2: tarpas rašomas tik tarp dviejų žetonų, kurie kitu atveju susilietų kaip paprasti simboliai, tad /Type /Page virsta /Type/Page
  • Kryžminių nuorodų srauto laukai įgyja bet kokį §7.5.8.2 leistą plotį, tad iki 16 MB failas kiekvieną poslinkį saugo 3 baituose vietoj 4
  • Iki 250 objektų telpa į kiekvieną objektų srautą vietoj įprastų 100, nebent savo ribą nustatote per ConfigureAdaptiveObjectStreamPacking
  • Kai failas neužšifruotas, Catalog ir Info žodynas taip pat supakuojami į objektų srautus; užšifruota išvestis juos palieka viršutiniame lygyje

Kompaktiška sintaksė atsinešė spąstus, vertus žinoti, jei rašytoją pratęsiate. Pasirašymas užpildo parašą tada, kai failas jau parašytas, ieškodamas baituose literalų /ByteRange ( ir /Contents <, o kompaktiška rašyba tuos fragmentus paverstų /ByteRange( ir /Contents<, kurių paieška niekada nerastų. Parašų žodynai (Type Sig arba DocTimeStamp, FT Sig) ir šifravimo žodynas todėl lieka prie atstų rašybos. Susijęs defektas kamavo laidasy iki v2.766.41: kiekvienas objektų srautų išsaugojimas, CompressDocument įskaitant, prasidėdavo dviem %PDF- antraštės eilutėmis, tad atsinaujinkite, jei griežtas validatorius peikia jūsų išvestį

Ar galima suglaudinti jau pakrautą PDF?

Galima, per parinkčių perkrodą CompressLoadedDocument(Options, Info), kuris tuos pačius be nuostolių žingsnius atlieka egzistuojančiam failui. Su THPDFLoadedDocumentCompressionOptions.Default jis pašalina nenaudojamus puslapių išteklius, sulieja identiškus šriftus ir formas, sudaro įdėtų šriftų poaibius su kompaktiškais poaibiais, perspaudžia nefiltruotus, Flate, LZW, ASCII ir RunLength srautus Flate, kai rezultatas mažesnis, ir priverčia kitą išsaugojimą naudoti objektų srautus. HighRatioFlate pagal nutylėjimą išjungtas, o objektų srautai praleidžiami PDF/A-1 ir prieaugio išsaugojimams. Be parametrų likęs CompressLoadedDocument perkroda yra senesnis, siauresnis iškvietimas, kuris tik Flate suspaudžia nesuspaustus srautus

HotPDF CompressLoadedDocument Delphi eiga: iškvietimas pašalina nenaudojamus puslapių išteklius, sulieja identiškus šriftus ir formas, sudaro įdėtų šriftų poaibius su kompaktiškais poaibiais, perspaudžia srautus Flate tik kai rezultatas mažesnis, ir įjungia objektų srautus kitam išsaugojimui, kol parašo laukai suaktyvina RefusedBySignaturePolicy ir palieka failą nepaliestą
Kiekvienas žingsnis perrašo baitus, kuriuos dengia parašas, tad visas dokumentas atmetamas, nebent aiškiai leidžiate negaliojimą – tada Info.BytesSaved sumuoja tik išteklių, šriftų ir srautų darbą
var
  Doc: THotPDF;
  Options: THPDFLoadedDocumentCompressionOptions;
  Info: THPDFLoadedDocumentCompressionInfo;
begin
  Doc := THotPDF.Create(nil);
  try
    Doc.AutoLaunch := False;
    Doc.LoadFromFile('quarterly-report.pdf');
    Options := THPDFLoadedDocumentCompressionOptions.Default;
    Doc.CompressLoadedDocument(Options, Info);
    if Info.RefusedBySignaturePolicy then
      Writeln(Format('Left untouched: %d signature fields', [Info.SignatureCount]))
    else
    begin
      Writeln(Format('Compact fonts: %d, stream bytes saved: %d',
        [Info.Fonts.CompactSubsetFontCount, Info.BytesSaved]));
      Doc.SaveLoadedDocument('quarterly-report-compact.pdf');
    end;
  finally
    Doc.Free;
  end;
end;

Pakrautame kelyje dvi ribos svarbios. Kiekvienas žingsnis perrašo baitus, kuriuos dengia parašas, tad dokumentas su parašo laukais atmetamas kaip visuma: iškvietimas grąžina 0, nustato RefusedBySignaturePolicy ir nieko nekeičia, nebent nustatote AllowSignatureInvalidation, po ko Info.SignaturesInvalidated pasako, ką atidavėte. Kompaktiškumas čia ir konservatyvesnis nei kūrimo kelyje. HotPDF glaudina tik šriftų programas, naudojamas vien tik CIDFontType2 šriftų su Identity /CIDToGIDMap, kur CID lygus GID, ir praleidžia programas su esamu žemėlapio srautu, /CIDSet ar spalvinėmis glifų lentelėmis, tokiomis kaip COLR, sbix, CBDT arba SVG, nes kompaktiška perstatyba numestų spalvų sluoksnius. Atkreipkite dėmesį ir tai, kad Info.BytesSaved sumuoja tik išteklių, šriftų ir srautų žingsnius; objektų srautų laimėjimas pasirodo, kai failas parašomas

Kokių rezultatų tikėtis praktikoje?

Laimėjimai seka tai, kiek failo yra nesuspaustos struktūros ir išpūstų šriftų duomenų, o ne tai, kiek jis turi puslapių. Trys puslapiai Arial ir SimSun pavyzdžio sumažėjo nuo 10.2 MB iki 20 KB, kai buvo gaminamas su CompressDocument, ir nuo 10.2 MB iki 19.8 KB, kai nesuspaustas originalas buvo pakrautas ir praleistas pro CompressLoadedDocument, abiem atvejais su identišku atvaizdavimu. PDF, kuris jau kompaktiškas, vos pajuda: regresijų rinkinyje tokie failai išsaugojo nuo -0.07% iki +0.06% savo originalaus dydžio. Nuotraukų pilni failai laimi mažai, nes nė vienas kelias vaizdų duomenų neliečia

Jei tuos pačius CJK ataskaitas gaminate kas naktį, kompaktiškus poaibius suporuokite su nuolatine šriftų poaibių atmintine diske, kad poaibių darbas nekartotųsi kiekvienam paleidimui, o suglaudintas išvestis lyginkite pagal objektų turinį, o ne pagal baitus, nes vienas pakeistas laukas priverčia visą objektų srautą vėl praeiti pro Flate. Pilnos atributų ir įrašų nuorodos yra HotPDF Delphi PDF komponento produkto puslapyje