Technický článek

HotPDF CompressDocument: kompaktní podmnožiny fontů v Delphi

HotPDF THotPDF.CompressDocument je jediný přepínač, který donutí BeginDoc vyrobit nejmenší bezztrátové PDF, jaké komponenta umí napsat: FlateDecode na maximální úrovni, cross-reference stream s object streams, subsetting fontů a kompaktní podmnožiny fontů, které přečíslují ponechané glyfy za explicitním /CIDToGIDMap. EndDoc pak vrátí vaše vlastní nastavení. Testovací dokument o třech stránkách s Arial a SimSun se smrskl z 10.2 MB na 20 KB při identickém vykreslení

Co CompressDocument doopravdy zapíná?

CompressDocument přebije šest nastavení writeru, plus strop object streamů, pro jeden dokument a potom je všem vrátí. V BeginDoc, dřív než se usadí PDF verze, si HotPDF poznamená 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 streams potřebují PDF 1.5, takže starší Version se zvedne na 1.5, pokud není uzamčená. PDF/A-1 obě struktury zakazuje, takže PDF/A-1 dokument si nechává klasickou cross-reference tabulku a dostane jen práci s Flate a fonty. Obrázky zůstanou přesně takové, jaké jste je vložili

Diagram životního cyklu CompressDocument v HotPDF v Delphi: BeginDoc si poznamená vlastní hodnoty writeru, přebije šest nastavení včetně Compression a UseObjectStreams pro jeden dokument a EndDoc vrátí každou vypůjčenou hodnotu ve svém nejvnejším finally, zatímco vlastnost CompressDocument sama zůstává True
Šest nastavení writeru a strop object streamů se půjčuje na přesně jeden dokument a vrací se, když běží EndDoc, takže neúspěšný report nikdy nenechá komponentu zaseknutou v maximální komprimaci
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;

Restaurace probíhá v nejvnejším finally v EndDoc, takže výjimka v půlce reportu nenechá dlouho žijící komponentu zaseknutou na maximální komprimaci pro příští job. Vlastnost CompressDocument sama zůstává True; vrátí se jen šest nastavení, která si půjčila. S verzí se zachází pozorněji. HotPDF ruší vlastní zvednutí na 1.5 jen tehdy, pokud dokument stále končí na 1.5, takže když jiná funkce posunula soubor na 1.6 během běhu (řekněme vložený OpenType font), vyšší verze zůstane, úplně jako by bez komprimace

Proč jsou podmnožiny fontů bez kompakce pořád velké?

Klasická TrueType podmnožina zahodí obrysy, které nikdy nekreslíte, ale každé glyph ID nechá tam, kde bylo, a právě tohle číslování ji drží těžkou. Content stream ukazuje CIDy rovné původním GIDům, takže podmnožina musí držet offset loca a položku hmtx pro každý slot až po nejvyšší ponechaný glyf, prázdný nebo ne. U latinského řezu je ta režie šum. U CJK řezu jako SimSun, jehož ideografy sedí hluboko ve velmi velké tabulce glyfů, dva čínské znaky vláčejí tabulky dimenzované na celý font. Pravidla uzávěru podmnožiny fontů pro tvarované glyfy rozhodují, kteří glyfové přežijí; kompakce je o tom, kolik stojí přeživší

CompactFontSubsetting přečísluje ponechané glyfy do hustého rozsahu začínajícího nulou a na CIDFont zapíše stream /CIDToGIDMap, který ISO 32000-1 §9.7.4.2 definuje jako tabulku dvoubajtových GIDů indexovanou podle CID. Tahle tabulka je celý trik. Content streamy, pole šířek /W i ToUnicode CMap si nechávají původní CIDy, takže nic už napsaného se nemusí měnit; do mapy se přesune jen vyhledávání z CIDu na glyf. V testu, který funkci motivoval, se SimSun se dvěma znaky smrskl z 24.8 KB fontových dat na 3.1 KB

Srovnání řídké fontové podmnožiny HotPDF, která drží položky loca a hmtx pro každé původní glyph ID až po nejvyšší ponechaný GID, s výstupem CompactFontSubsetting, který ponechané glyfy hustě přečísluje od nuly a mapuje CIDy přes stream CIDToGIDMap, zatímco content streamy, /W a ToUnicode zůstávají beze změny
Přečíslování přesune náklady z font programu do jednoho malého map streamu — dva znaky SimSun spadly z 24.8 KB na 3.1 KB aniž se dotkly jediného bajtu už napsaného obsahu

Kompakce má tvrdé meze a spíš se potichu degraduje, než selže. HotPDF staví kompaktní podmnožiny jen pro řezy Type 0 TrueType, jak pro ty nastavené přes SetFont se zapnutým subsettingem, tak pro řez registrovaný přes RegisterUnicodeTTF. Jednoduchý TrueType font hledá glyfy přes cmap uvnitř font programu, což by přečíslování rozbilo, takže si nechává řídkou podmnožinu. Řezy OpenType-CFF kompaktní cestu nemají taky. Kompaktní sestava, která selže, sestoupí na řídkou podmnožinu místo vyhození výjimky. Vlastnost je defaultně vypnutá, takže stávající výstup zůstává bajtově identický, zatímco pod PDF/A dostane registrovaný Unicode řez vždy kompaktní podmnožinu

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

Jak packed writer vymačká strukturu souboru?

Jakmile jsou fonty a streamy malé, slovníky a cross-reference data se stanou největší zbývající položkou, takže object-stream writer za CompressDocument nařeže i ty. Průvodce object streams a inkrementálními aktualizacemi popisuje samotný formát kontejneru; kompresní cesta přidává čtyři vylepšení navrch:

  • Kompaktní syntaxe podle ISO 32000-1 §7.2.2: mezera se zapíše jen mezi dva tokeny, které by jinak srostly jako běžné znaky, takže /Type /Page se stane /Type/Page
  • Pole cross-reference streamu nabírají libovolnou šířku, kterou §7.5.8.2 dovoluje, takže soubor pod 16 MB ukládá každý offset do 3 bajtů místo 4
  • Až 250 objektů jde do každého object streamu místo obvyklých 100, pokud si vlastní strop nenastavíte přes ConfigureAdaptiveObjectStreamPacking
  • Když soubor není zašifrovaný, Catalog i Info slovník se sbalí do object streamů taky; šifrovaný výstup je nechává na nejvyšší úrovni

Kompaktní syntaxe přišla s pastí, kterou stojí za znát, pokud writer rozšiřujete. Podepisování doplňuje podpis až po zapsání souboru tím, že v bajtech hledá literální placeholdery /ByteRange ( a /Contents <, a kompaktní hláskování by z nich udělalo /ByteRange( a /Contents<, které hledání nikdy nenajde. Slovníky podpisů (Type Sig nebo DocTimeStamp, FT Sig) a šifrovací slovník si proto nechávají rozestoupanou podobu. Související vada postihla buildy před v2.766.41: každé uložení s object streams, CompressDocument nevyjímaje, začínalo dvěma řádky hlavičky %PDF-, takže upgradujte, pokud přísný validator váš výstup označí

Dá se zkomprimovat PDF, které už je načtené?

Ano, přes overload s options CompressLoadedDocument(Options, Info), který pustí tytéž bezztrátové kroky nad existujícím souborem. S THPDFLoadedDocumentCompressionOptions.Default odstraní nepoužívané zdroje stránek, sloučí identické fonty a formuláře, podmnožinuje vložené fonty s kompaktními podmnožinami, znovu zkomprimuje nefiltrované, Flate, LZW, ASCII i RunLength streamy Flate, když je výsledek menší, a přepne příští uložení na object streams. HighRatioFlate je defaultně vypnuté a object streams se přeskočí u PDF/A-1 a inkrementálních uložení. Overload CompressLoadedDocument bez parametrů je starší, užší volání, které jen zkomprimuje nekomprimované streamy přes Flate

Průběh CompressLoadedDocument v HotPDF v Delphi: volání odstraní nepoužívané zdroje stránek, sloučí identické fonty a formuláře, podmnožinuje vložené fonty s kompaktními podmnožinami, zkomprimuje streamy Flate jen když je výsledek menší a zapne object streams pro příští uložení, zatímco podpisová pole spustí RefusedBySignaturePolicy a soubor nechají nedotčený
Každý krok přepisuje bajty, které kryje podpis, takže celý dokument se odmítne, pokud explicitně nepovolíte invalidaci — Info.BytesSaved pak sečte jen práci se zdroji, fonty a streamy
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čtené cestě záleží na dvou mezích. Každý krok přepisuje bajty, které kryje podpis, takže dokument s podpisovými poli se odmítne v celku: volání vrátí 0, nastaví RefusedBySignaturePolicy a nic nezmění, pokud nenastavíte AllowSignatureInvalidation, po čemž Info.SignaturesInvalidated řekne, čeho jste se vzdali. Kompakce je tady taky konzervativnější než na cestě tvorby. HotPDF kompaktuje jen font programy používané výhradně fonty CIDFontType2 s Identity /CIDToGIDMap, kde se CID rovná GID, a přeskočí programy s existujícím map streamem, s /CIDSet nebo s tabulkami barevných glyfů jako COLR, sbix, CBDT či SVG, protože kompaktní přestavba by barevné vrstvy zahodila. Všimněte si také, že Info.BytesSaved sčítá jen kroky se zdroji, fonty a streamy; zisk z object streamů se ukáže, až se soubor zapíše

Jakých výsledků se v praxi dočkáte?

Zisky sledují, jak velká část souboru je nekomprimovaná struktura a nafouknutá fontová data, ne kolik má stránek. Vzorek o třech stránkách s Arial a SimSun se smrskl z 10.2 MB na 20 KB, když se generoval s CompressDocument, a z 10.2 MB na 19.8 KB, když se nekomprimovaný originál načetl a pustil přes CompressLoadedDocument, obojí při identickém vykreslení. PDF, které už je kompaktní, se sotva pohně: v regresní sadě si takové soubory polepšily v rozmezí -0.07 % až +0.06 % původní velikosti. Soubory plné fotek si přidají málo, protože ani jedna cesta nezasahuje do obrazových dat

Pokud generujete tytéž CJK reporty každou noc, spojte kompaktní podmnožiny s perzistentní cache podmnožin fontů na disku, aby se subsetting práce neopakoval při každém běhu, a diffujte komprimované výstupy podle obsahu objektů, ne bajtů, protože jedna změněná položka znovu zFlateuje celý object stream. Kompletní reference vlastností a recordů najdete na stránce produktu HotPDF Delphi PDF component