Odborný článok

HotPDF CompressDocument: kompaktné subsety fontov v Delphi

HotPDF THotPDF.CompressDocument je jediný prepínač, ktorý prinúti BeginDoc vyrobiť najmenší bezstratový PDF, aký komponent dokáže zapísať: FlateDecode na maximálnej úrovni, cross-reference stream s object streamami, font subsetting a kompaktné subsety fontov, ktoré prečíslujú ponechané glyfy za explicitným /CIDToGIDMap. EndDoc potom vráti vaše vlastné nastavenia. Trojstranový testovací dokument s Arial a SimSun klesol z 10,2 MB na 20 kB pri identickom vykreslení

Čo CompressDocument vlastne zapína?

CompressDocument prebije šesť nastavení zapisovača, plus strop object streamov, pre jeden dokument a potom všetky obnoví. Pri BeginDoc, skôr než sa PDF verzia ustáli, si HotPDF zapamätá vaše hodnoty a nastaví Compression na cmFlateDecode, CompressionLevel na clMaximum, zapne EnableFontSubsetting a CompactFontSubsetting a povolí UseXRefStream plus UseObjectStreams (ISO 32000-1 §7.5.7 a §7.5.8). Object streamy potrebujú PDF 1.5, takže staršia Version sa zvýši na 1.5, keď nie je zamknutá. PDF/A-1 obom štruktúram zakazuje, takže dokument PDF/A-1 si nechá klasickú cross-reference tabuľku a získa len Flate a font prácu. Obrázky ostanú presne tak, ako ste ich vložili

Diagram životného cyklu CompressDocument v HotPDF v Delphi: BeginDoc si zapíše vlastné hodnoty zapisovača, prebije šesť nastavení vrátane Compression a UseObjectStreams pre jeden dokument a EndDoc obnoví každú požičanú hodnotu vo svojom najvnejšom finally, zatiaľ čo samotná vlastnosť CompressDocument ostane True
Šesť nastavení zapisovača a strop object streamov sa požičia na presne jeden dokument a vráti sa, keď beží EndDoc, takže neúspešný report nikdy nenechá komponent zaseknutý na maximálnej kompresii
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := 'invoice-2026-1042.pdf';
    Pdf.CompressDocument := True;    // aplikuje BeginDoc, ruší EndDoc
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-1042');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Obnova prebieha vo najvnejšom finally metódy EndDoc, takže výnimka v polovici reportu nenechá dlho žijúci komponent zaseknutý na maximálnej kompresii pre ďalšiu úlohu. Samotná vlastnosť CompressDocument ostane True; vracajú sa len šesť nastavení, ktoré požičala. S verziou sa narába o pozornejšie. HotPDF ruší vlastné zvýšenie na 1.5, len keď dokument stále končí na 1.5, takže keď iná funkcia posunula súbor počas behu na 1.6 (povedzme vložený OpenType font), vyššia verzia ostane, presne ako by ostala bez kompresie

Prečo sú subsety fontov bez kompaktizácie stále veľké?

Klasický TrueType subset zahodí obrysy, ktoré nikdy nekreslíte, ale každé glyph ID nechá tam, kde bolo, a práve to číslovanie ho drží ťažké. Content stream ukazuje CID rovné pôvodným GID, takže subset musí držať offset loca a záznam hmtx pre každý slot až po najvyšší glyf, ktorého si drží, či je prázdny alebo nie. Pri latinskom písme je tá réžia hluk. Pri CJK písme ako SimSun, ktorého ideografy sedia hlboko vo veľmi veľkej glyfovej tabuľke, dva čínske znaky ťahajú so sebou tabuľky dimenzované na celý font. O tom, ktoré glyfy prežijú, rozhodujú pravidlá uzáveru subsetu pre tvarované glyfy; kompaktizácia je o tom, koľko stoja prežívší

CompactFontSubsetting prečísluje ponechané glyfy do hustého rozsahu začínajúceho nulou a zapíše na CIDFont stream /CIDToGIDMap, ktorý ISO 32000-1 §9.7.4.2 definuje ako tabuľku dvojbajtových GID indexovaných CID. Tá tabuľka je celý trik. Content streamy, šírkové pole /W aj ToUnicode CMap si držia pôvodné CID, takže nič už zapísané sa nemusí meniť; do mapy sa presunie len vyhľadanie z CID na glyf. V teste, ktorý funkciu motivoval, klesol SimSun s dvoma znakmi z 24,8 kB fontových dát na 3,1 kB

Porovnanie riedkeho font subsetu HotPDF, ktorý drží záznamy loca a hmtx pre každé pôvodné glyph ID až po najvyššie ponechané GID, s výstupom CompactFontSubsetting, ktorý prečísluje ponechané glyfy husto od nuly a mapuje CID cez stream CIDToGIDMap, zatiaľ čo content streamy, /W aj ToUnicode ostanú nezmenené
Prečíslovanie presunie réžiu z font programu do jedného malého map streamu — dva znaky SimSun klesli z 24,8 kB na 3,1 kB bez toho, aby sa zmenil jediný bajt už zapísaného obsahu

Kompaktizácia má pevné hranice a degraduje potichu namiesto toho, aby padla. HotPDF stavia kompaktné subsety len pre písma Type 0 TrueType, a to tak tie nastavené cez SetFont so zapnutým subsettingom, ako aj písmo zaregistrované cez RegisterUnicodeTTF. Jednoduchý TrueType font nachádza svoje glyfy cez cmap vnútri font programu, čo by prečíslovanie rozbilo, takže si drží riedky subset. Ani písma OpenType-CFF nemajú kompaktnú cestu. Kompaktné zostavenie, ktoré zlyhá, spadne späť na riedky subset namiesto vyhodenia chyby. Vlastnosť je predvolene vypnutá, takže existujúci výstup ostanú bajtovo identický, zatiaľ čo pod PDF/A dostane zaregistrované Unicode písmo kompaktný subset vždy

Pdf.EnableFontSubsetting := True;
Pdf.CompactFontSubsetting := True;   // použiteľné aj bez CompressDocument
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('SimSun', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, WideString('Total: '#$4E2D#$6587));
Pdf.EndDoc;

Ako zhutnený zapisovač vymiesti štruktúru súboru?

Keď sú fonty a streamy malé, slovníky a cross-reference dáta sa stanú najväčšou zostávajúcou položkou, takže object-stream zapisovač za CompressDocument oreže aj tie. Formát kontajnera samotný popisuje sprievodca object streamami a inkrementálnymi aktualizáciami; kompresná cesta pridáva štyri vylepšenia navyše:

  • Kompaktná syntax podľa ISO 32000-1 §7.2.2: medzera sa zapisuje len medzi dva tokeny, ktoré by inak stiekli dokopy ako obyčajné znaky, takže /Type /Page sa stane /Type/Page
  • Polia cross-reference streamu zaberú ľubovoľnú šírku, ktorú dovoľuje §7.5.8.2, takže súbor pod 16 MB ukladá každý offset v 3 bajtoch namiesto 4
  • Do každého object streamu vchádza až 250 objektov namiesto obvyklých 100, pokiaľ si vlastný strop nenastavíte cez ConfigureAdaptiveObjectStreamPacking
  • Keď súbor nie je šifrovaný, Catalog aj Info slovník sa pribalia do object streamov tiež; šifrovaný výstup ich necháva na najvyššej úrovni

Kompaktná syntax si priniesla pascu, ktorú stoja za poznanie, keď zapisovač rozširujete. Podpisovanie doplní podpis až po zapísaní súboru tak, že v bajtoch hľadá literálne zástupné znaky /ByteRange ( a /Contents <, a kompaktné hláskovanie by z nich urobilo /ByteRange( a /Contents<, čo hľadanie nikdy nenájde. Slovníky podpisov (Type Sig alebo DocTimeStamp, FT Sig) aj šifrovací slovník si preto držia rozmedzené rozloženie. Súvisiaci defekt postihol zostavenia pred v2.766.41: každé uloženie s object streamami, vrátane CompressDocument, začínalo dvoma hlavičkovými riadkami %PDF-, takže urobte upgrade, keď prísny validator označí váš výstup

Dá sa komprimovať PDF, ktoré je už načítané?

Áno, cez overload s options CompressLoadedDocument(Options, Info), ktorý prevedie tie isté bezstratové kroky na existujúcom súbore. S THPDFLoadedDocumentCompressionOptions.Default odstráni nepoužívané zdroje strán, zlúči identické fonty a formuláre, subsetuje vložené fonty so zapnutými kompaktnými subsetmi, prekomprimuje nefiltrované, Flate, LZW, ASCII a RunLength streamy cez Flate, keď je výsledok menší, a prinúti ďalšie uloženie použiť object streamy. HighRatioFlate je predvolene vypnuté a object streamy sa preskočia pri PDF/A-1 a inkrementálnych ukladaniach. Overload CompressLoadedDocument bez parametrov je staršie, užšie volanie, ktoré cez Flate komprimuje len nekomprimované streamy

Priebeh CompressLoadedDocument v HotPDF v Delphi: volanie odstráni nepoužívané zdroje strán, zlúči identické fonty a formuláre, subsetuje vložené fonty kompaktnými subsetmi, prekomprimuje streamy cez Flate len keď je výsledok menší a zapne object streamy pre ďalšie uloženie, zatiaľ čo podpisové polia spustia RefusedBySignaturePolicy a nechajú súbor nedotknutý
Každý krok prepisuje bajty, ktoré kryje podpis, takže celý dokument sa odmietne, pokiaľ nepovolíte invalidáciu explicitne — Info.BytesSaved potom sčítava len prácu so zdrojmi, fontmi a streamami
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;

Na načítanej ceste majú význam dve hranice. Každý krok prepisuje bajty, ktoré kryje podpis, takže dokument s podpisovými poliami sa odmietne ako celok: volanie vráti 0, nastaví RefusedBySignaturePolicy a nič nezmení, pokiaľ nenastavíte AllowSignatureInvalidation, po čom vám Info.SignaturesInvalidated povie, čím ste prišli. Kompaktizácia je tu tiež konzervatívnejšia než na ceste tvorenia. HotPDF kompaktizuje len font programy používané výhradne fontmi CIDFontType2 s Identity /CIDToGIDMap, kde CID sa rovná GID, a preskočí programy s existujúcim map streamom, s /CIDSet alebo s farebnými glyfovými tabuľkami ako COLR, sbix, CBDT či SVG, lebo kompaktná prestavba by zahodila farebné vrstvy. Všimnite si tiež, že Info.BytesSaved sčítava len kroky zdrojov, fontov a streamov; zisk object streamov sa ukáže, keď sa súbor zapisuje

Akých výsledkov sa dať čakať v praxi?

Úspory sledujú, koľko súboru tvorí nekomprimovaná štruktúra a nafúkané fontové dáta, nie to, koľko má strán. Trojstranová vzorka Arial a SimSun sa scvrkla z 10,2 MB na 20 kB pri generovaní s CompressDocument a z 10,2 MB na 19,8 kB, keď sa nekomprimovaný originál načítal a prebehol cez CompressLoadedDocument, v oboch prípadoch s identickým vykreslením. PDF, ktoré je už kompaktné, sa len tak nepohne: v regresnej sade sa také súbory uložili v rozsahu -0,07 % až +0,06 % pôvodnej veľkosti. Súbory plné fotiek získajú málo, lebo žiadna z ciest nesiahne na obrazové dáta

Ak každú noc generujete tie isté CJK reporty, skombinujte kompaktné subsety s trvalou cache subsetov fontov na disku, aby sa subsettingová práca neopakovala pri každom behu, a porovnávajte komprimované výstupy podľa obsahu objektov namiesto bajtov, keďže jedna zmenená hodnota re-Flate celý object stream. Úplné referencie vlastností a recordov sú na produktovej stránke HotPDF Delphi PDF component