Műszaki cikk

HotPDF CompressDocument: kompakt font szubhalmazok Delphiben

A HotPDF THotPDF.CompressDocument egyetlen kapcsoló, ami azt éri el, hogy a BeginDoc a legkisebb veszteségmentes PDF-et gyártja, amit a komponens írni tud: FlateDecode maximum szinten, cross-reference streammel object streamekkel, font szubsettinggel és kompakt font szubhalmazokkal, amik a megtartott glyphokat újraszámozzák egy explicit /CIDToGIDMap mögé. Az EndDoc ezután visszateszi a saját beállításaidat. Egy háromoldalas Arial és SimSun tesztdokumentum 10,2 MB-ról 20 KB-ra esett, azonos rendereléssel

Mit kapcsol be valójában a CompressDocument?

A CompressDocument hat íróbeállítást, plusz az object-stream capet, felülbírál egyetlen dokumentum erejéig, és utána mindet visszaállítja. A BeginDoc-nál, mielőtt a PDF verzió rögzülne, a HotPDF feljegyzi az értékeidet, és a Compression-t cmFlateDecode-re, a CompressionLevel-t clMaximum-ra állítja, bekapcsolja az EnableFontSubsetting-et és a CompactFontSubsetting-et, valamint engedélyezi a UseXRefStream-et és a UseObjectStreams-t (ISO 32000-1 §7.5.7 és §7.5.8). Az object streamek PDF 1.5-öt igényelnek, így egy régebbi Version 1.5-re emelkedik, ha nincs zárolva. A PDF/A-1 mindkét struktúrát tiltja, így egy PDF/A-1 dokumentum megtartja a klasszikus cross-reference tábláját, és csak a Flate és a font munkát kapja. A képek pontosan úgy maradnak, ahogy beágyaztad őket

A HotPDF CompressDocument életciklusának ábrája Delphiben: a BeginDoc feljegyzi az író saját értékeit, egyetlen dokumentumra felülbírál hat beállítást, köztük a Compression-t és a UseObjectStreams-t, az EndDoc pedig minden kölcsönvett értéket visszaállít a legkülső finally-jében, miközben a CompressDocument tulajdonság maga True marad
Hat íróbeállítás és az object-stream cap pontosan egy dokumentum erejéig van kikölcsönözve, és visszaadódik, amikor az EndDoc lefut, így egy elbukó riport sosem hagyja a komponenst maximális tömörítésen ragadni
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := 'invoice-2026-1042.pdf';
    Pdf.CompressDocument := True;    // a BeginDoc alkalmazza, az EndDoc visszavonja
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-1042');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

A visszaállítás az EndDoc legkülső finally-jében történik, így egy riport félúton bekövetkező exceptionje nem hagy egy hosszú életű komponenst maximális tömörítésen a következő feladatra. A CompressDocument tulajdonság maga True marad; csak a hat kölcsönvett beállítás megy vissza. A verzióval nagyobb gonddal bánnak. A HotPDF a saját 1.5-re emelését csak akkor vonja vissza, ha a dokumentum továbbra is 1.5-nél ér véget, így amikor egy másik funkció a futás közben 1.6-ra tolta a fájlt (mondjuk egy beágyazott OpenType font), a magasabb verzió megmarad, pontosan úgy, ahogy tömörítés nélkül is lett volna

Miért maradnak nagyok a font szubhalmazok kompaktálás nélkül?

Egy klasszikus TrueType szubhalmaz eldobja azokat a körvonalakat, amiket sosem rajzolsz, de minden glyph ID-t megtart a helyén, és ez a számozás az, ami nehézzé teszi. A content stream az eredeti GID-okkal megegyező CID-eket mutat, így a szubhalmaznak minden slothoz meg kell tartania egy loca offsetet és egy hmtx bejegyzést a legmagasabb megtartott glyphig, üresen is. Latin betűtípusnál ez a teher zaj. Olyan CJK betűtípusnál, mint a SimSun, aminek az ideogramjai egy nagyon nagy glyph tábla mélyén ülnek, két kínai karakter az egész fontra méretezett táblákat cipelt magával. A font szubhalmaz lezárási szabályok shaped glyphokra döntik el, mely glyphok élnek túl; a kompaktálás arról szól, mennyibe kerülnek a túlélők

A CompactFontSubsetting a megtartott glyphokat nullától induló sűrű tartományba számozza újra, és egy /CIDToGIDMap streamet ír a CIDFontra, amit az ISO 32000-1 §9.7.4.2-e CID által indexelt, kétbájtos GID-okból álló táblaként definiál. Ez a táblázat az egész trükk. A content streamek, a /W szélességtömb és a ToUnicode CMap mind az eredeti CID-eket tartják, így semminek, ami már megíródott, nem kell változnia; csak a CID-ből glyphba menő keresés költözik a térképbe. A funkciót megihlető tesztben a két karakteres SimSun 24,8 KB font adatról 3,1 KB-ra csökkent

Egy ritka HotPDF font szubhalmaz, ami a legmagasabb megtartott GID-ig minden eredeti glyph ID-hoz megtartja a loca és hmtx bejegyzéseket, összehasonlítva a CompactFontSubsetting kimenetével, ami a megtartott glyphokat nullától sűrűn számozza újra, a CID-eket pedig egy CIDToGIDMap streamen keresztül képezi le, miközben a content streamek, a /W és a ToUnicode változatlan marad
Az újraszámozás a költséget a font programból egyetlen kis map streambe költözteti — két SimSun karakter 24,8 KB-ról 3,1 KB-ra esett anélkül, hogy a már megírt tartalom egyetlen bájtját is megérintette volna

A kompaktálásnak kemény határai vannak, és csendben visszaesik ahelyett, hogy elbukna. A HotPDF csak Type 0 TrueType betűtípusokhoz épít kompakt szubhalmazokat, mind azokhoz, amiket SetFont-tal állítottál be szubsettinggel, mind a RegisterUnicodeTTF-en keresztül regisztrált betűtípushoz. Egy egyszerű TrueType font a glyphjait a font programon belüli cmapen keresztül találja meg, amit az újraszámozás megtörne, így megtartja a ritka szubhalmazt. Az OpenType-CFF betűtípusoknak sincs kompakt útvonala. Egy elbukó kompakt építés a ritka szubhalmazra esik vissza exception dobása helyett. A tulajdonság alapból ki van kapcsolva, így a meglévő kimenet bájtonként azonos marad, PDF/A alatt pedig a regisztrált Unicode betűtípus mindig kompakt szubhalmazt kap

Pdf.EnableFontSubsetting := True;
Pdf.CompactFontSubsetting := True;   // CompressDocument nélkül is használható
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('SimSun', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, WideString('Total: '#$4E2D#$6587));
Pdf.EndDoc;

Hogyan szorítja össze a pakolt író a fájlstruktúrát?

Ha a fontok és a streamek kicsik lettek, a szótárak és a cross-reference adat lesz a legnagyobb megmaradó költség, így az object-stream író, ami a CompressDocument mögött áll, ezeket is nyírja. Az object streamek és inkrementális frissítések útmutatója magát a konténerformátumot tárgyalja; a tömörítési útvonal négy finomítást tesz hozzá:

  • Kompakt szintaxis az ISO 32000-1 §7.2.2 szerint: szóköz csak két olyan token közé íródik, amik különben regulárius karakterként összefolynának, így a /Type /Page /Type/Page lesz
  • A cross-reference stream mezői bármilyen szélességet felvesznek, amit a §7.5.8.2 enged, így egy 16 MB alatti fájl minden offsetet 3 bájton tárol 4 helyett
  • Object streamenként legfeljebb 250 objektum kerül be a szokásos 100 helyett, hacsak a ConfigureAdaptiveObjectStreamPacking-en át nem állítasz saját capet
  • Ha a fájl nincs titkosítva, a Catalog és az Info szótár is object streamekbe pakolódik; a titkosított kimenet a felső szinten tartja őket

A kompakt szintaxis egy csapdával jött, amit érdemes ismerni, ha az írót bővíted. Az aláírás a fájl megírása után tölti ki a szignatúrát, a bájtok között megkeresve a literális placeholdereket, a /ByteRange ( és a /Contents < stringeket, a kompakt helyesírás viszont /ByteRange(-re és /Contents<-re fordítaná őket, amit a keresés sosem talál. A szignatúra szótárak (Type Sig vagy DocTimeStamp, FT Sig) és a titkosítási szótár ezért megtartja a szóközös elrendezést. Egy kapcsolódó hiba a v2.766.41 előtti buildeket érintette: minden object-stream mentés, a CompressDocument is, két %PDF- fejlécsorral indult, így frissíts, ha egy szigorú validátor jelzi a kimenetedet

Tömöríthető egy már betöltött PDF?

Igen, az opciós overloadon, a CompressLoadedDocument(Options, Info)-n át, ami ugyanazokat a veszteségmentes lépéseket futtatja egy meglévő fájlon. A THPDFLoadedDocumentCompressionOptions.Default-dal törli a nem használt oldalforrásokat, fésüli az azonos fontokat és formákat, szubhalmazozza a beágyazott fontokat bekapcsolt kompakt szubhalmazokkal, újratömöríti a szűretlen, Flate, LZW, ASCII és RunLength streameket Flate-tel, ha az eredmény kisebb, és a következő mentést object streamekre állítja. A HighRatioFlate alapból ki van kapcsolva, az object streamek pedig PDF/A-1 és inkrementális mentéseknél kimaradnak. A paraméternélküli CompressLoadedDocument overload a régebbi, szűkebb hívás, ami csak a tömörítetlen streameket Flate-eli

A HotPDF CompressLoadedDocument menete Delphiben: a hívás törli a nem használt oldalforrásokat, fésüli az azonos fontokat és formákat, kompakt szubhalmazokkal szubhalmazozza a beágyazott fontokat, csak akkor tömöríti újra a streameket Flate-tel, ha az eredmény kisebb, és a következő mentésre bekapcsolja az object streameket, miközben a szignatúramezők RefusedBySignaturePolicy-t váltanak ki, és békén hagyják a fájlt
Minden lépés olyan bájtokat ír újra, amiket aláírás fed, így a teljes dokumentum elutasítódik, hacsak kifejezetten nem engedélyezed az érvénytelenítést — az Info.BytesSaved ekkor csak az erőforrás, font és stream munkát adja össze
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;

A betöltött útvonalon két határ számít. Minden lépés olyan bájtokat ír újra, amiket aláírás fed, így szignatúramezőket tartalmazó dokumentum egészként elutasítódik: a hívás 0-t ad vissza, beállítja a RefusedBySignaturePolicy-t, és semmit sem változtat, hacsak nem állítod be az AllowSignatureInvalidation-t, ami után az Info.SignaturesInvalidated megmondja, mit adtál fel. A kompaktálás itt is visszafogottabb, mint a létrehozási útvonalon. A HotPDF csak olyan font programokat kompaktál, amiket kizárólag Identity /CIDToGIDMap-os CIDFontType2 fontok használnak, ahol CID = GID, és kihagyja a már létező map streamet, /CIDSet-et vagy olyan color glyph táblákat hordozó programokat, mint a COLR, sbix, CBDT vagy SVG, mert a kompakt újraépítés eldobná a szín rétegeket. Jegyezd meg továbbá, hogy az Info.BytesSaved csak az erőforrás, font és stream lépéseket adja össze; az object-stream nyereség a fájl megírásakor mutatkozik

Milyen eredményt várhat a gyakorlatban?

A nyereség azt követi, mekkora része a fájlnak a tömörítetlen struktúra és a túlméretezett font adat, nem azt, hány oldalas. A háromoldalas Arial és SimSun minta 10,2 MB-ról 20 KB-ra zsugorodott, amikor CompressDocument-tel gyártották, és 10,2 MB-ról 19,8 KB-ra, amikor a tömörítetlen eredetit töltötték be és a CompressLoadedDocument-en vitték át, mindkét irányban azonos rendereléssel. Egy már kompakt PDF alig mozdul: a regressziós halmazban az ilyen fájlok az eredeti méretük -0,07%-ától +0,06%-áig takarítottak meg. A fotósúlyos fájlok keveset nyernek, mert egyik útvonal sem nyúl a képadatokhoz

Ha minden este ugyanazokat a CJK riportokat gyártod, párosítsd a kompakt szubhalmazokat a lemezen tárolt tartós font szubhalmaz cache-sel, hogy a szubsetting munka ne ismétlődjön futásonként, és a tömörített kimeneteket objektumtartalom szerint diffeld, ne bájt szerint, mert egy megváltozott mező egy egész object streamet újrátömörít. A teljes tulajdonság- és rekordreferencia a HotPDF Delphi PDF component termékoldalon van