Tehnički članak

HotPDF CompressDocument: kompaktni font podskupovi u Delphi

HotPDF THotPDF.CompressDocument je jedan prekidač koji od BeginDoc-a čini da komponenta napiše najmanji bezgubitnički PDF koji ume: FlateDecode na najvišem nivou, cross-reference stream sa object streamovima, font subsetting i kompaktne font podskupove koji zadržanim glifovima dodele nove, zbijene brojeve iza eksplicitnog /CIDToGIDMap-a. EndDoc potom vraća vaša sopstvena podešavanja. Test dokument od tri stranice sa Arial-om i SimSun-om pao je sa 10,2 MB na 20 KB uz identično renderovanje

Šta CompressDocument zapravo uključuje?

CompressDocument pregazi šest pisanih podešavanja, plus limit object streamova, za jedan dokument i potom ih sve vraća. Na BeginDoc-u, pre nego što se PDF verzija učvrsti, HotPDF zapamti vaše vrednosti i postavi Compression na cmFlateDecode, CompressionLevel na clMaximum, uključi EnableFontSubsetting i CompactFontSubsetting, i aktivira UseXRefStream plus UseObjectStreams (ISO 32000-1 §7.5.7 i §7.5.8). Object streamovi traže PDF 1.5, pa starija Version diže na 1.5 kad nije zaključana. PDF/A-1 zabranjuje obe strukture, pa PDF/A-1 dokument zadržava klasičnu cross-reference tabelu i dobija samo Flate i font posao. Slike ostaju tačno onakve kakve ste ih ugradili

Dijagram životnog ciklusa HotPDF CompressDocument-a u Delphiju: BeginDoc zabeleži sopstvene vrednosti pisca, pregazi šest podešavanja uključujući Compression i UseObjectStreams za jedan dokument, a EndDoc vraća svaku pozajmljenu vrednost u svojem najspoljašnjem finally dok CompressDocument svojstvo samo ostaje True
Šest pisanih podešavanja i limit object streamova pozajmljeni su za tačno jedan dokument i vraćaju se kad EndDoc proradi, pa neuspeli izveštaj nikada ne ostavlja komponentu zaglavljenu na maksimalnoj kompresiji
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := 'invoice-2026-1042.pdf';
    Pdf.CompressDocument := True;    // primenjuje BeginDoc, poništava EndDoc
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-1042');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Vraćanje se dešava u najspoljašnjem finally-ju EndDoc-a, pa izuzetak usred izveštaja ne ostavlja dugovečnu komponentu zaglavljenu na maksimalnoj kompresiji za sledeći posao. Svoje svojstvo CompressDocument ostaje True; vraća se samo šest podešavanja koja je pozajmila. Verzija se rešava pažljivije. HotPDF poništava sopstveno dizanje na 1.5 samo ako dokument i dalje završava na 1.5, pa kada je neka druga funkcija tokom rada gurnula fajl na 1.6 (recimo ugrađeni OpenType font), viša verzija ostaje, tačno kao što bi ostala i bez kompresije

Zašto su font podskupovi i dalje veliki bez kompakcije?

Klasični TrueType podskup baca konture koje nikada ne crtate, ali zadržava svaki glyph ID na njegovom mestu, i upravo to numerisanje ga drži teškim. Content stream pokazuje CID-ove jednake originalnim GID-ovima, pa podskup mora da drži loca offset i hmtx unos za svaki slot do najvišeg glifa koji zadrži, prazan ili ne. Za latinično pismo taj teret je šum. Za CJK pismo poput SimSun-a, čiji ideogrami sede duboko u vrlo velikoj tabeli glifova, dva kineska znaka vuku za sobom tabele dimenzionisane za ceo font. Pravila zatvaranja font podskupa za oblikovane glifove odlučuje koji glifovi prežive; kompakcija je pitanje koliko preživele koštaju

CompactFontSubsetting zadržanim glifovima dodeljuje zbijen opseg počevši od nule i upisuje /CIDToGIDMap stream na CIDFont, koji ISO 32000-1 §9.7.4.2 definiše kao tabelu dvobajtnih GID-ova indeksiranih CID-om. Ta tabela je ceo trik. Content streamovi, niz širina /W i ToUnicode CMap svi zadržavaju originalne CID-ove, pa ništa već upisano ne mora da se menja; samo pretraga od CID-a do glifa seli se u mapu. U testu koji je motivisao funkciju, SimSun sa dva znaka pao je sa 24,8 KB font podataka na 3,1 KB

Poređenje raštrkanog HotPDF font podskupa, koji čuva loca i hmtx unose za svaki originalni glyph ID do najvišeg zadržanog GID-a, sa CompactFontSubsetting izlazom koji zadržane glifove gusto ponovo numeriše od nule i mapira CID-ove kroz CIDToGIDMap stream dok content streamovi, /W i ToUnicode ostaju nepromenjeni
Ponovno numerisanje izvlači trošak iz font programa u jedan sitan map stream — dva SimSun znaka pala su sa 24,8 KB na 3,1 KB a da jedan bajt već upisanog sadržaja nije dirnut

Kompakcija ima čvrste granice, i tiho se povlači umesto da padne. HotPDF gradi kompaktne podskupove samo za Type 0 TrueType pisma, i ona postavljena kroz SetFont sa uključenim subsettingom i pismo registrovano kroz RegisterUnicodeTTF. Jednostavni TrueType font nalazi glifove kroz cmap unutar font programa, što bi ponovno numerisanje slomilo, pa zadržava raštrkani podskup. Ni OpenType-CFF pisma nemaju kompakt putanju. Kompaktni build koji omane vraća se na raštrkani podskup umesto da podigne grešku. Svojstvo je podrazumevano isključeno, pa postojeći izlaz ostaje bajt-identičan, dok pod PDF/A-om registrovano Unicode pismo uvek dobija kompaktan podskup

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

Kako sabijeni pisac cedi strukturu fajla?

Kad fontovi i streamovi postanu mali, rečnici i cross-reference podaci postaju najveći preostali trošak, pa ih pisac object streamova iza CompressDocument-a takođe dotera. Vodič o object streamovima i inkrementalnim ispravkama pokriva sam format kontejnera; kompresiona putanja dodaje četiri usavršavanja preko toga:

  • Kompaktna sintaksa po ISO 32000-1 §7.2.2: razmak se upisuje samo između dva tokena koja bi inače sastavljeni prošli kao obični znakovi, pa /Type /Page postaje /Type/Page
  • Polja cross-reference streama uzimaju bilo koju širinu koju §7.5.8.2 dopušta, pa fajl ispod 16 MB čuva svaki offset u 3 bajta umesto 4
  • Do 250 objekata ide u svaki object stream umesto uobičajenih 100, osim ako svoj limit postavite kroz ConfigureAdaptiveObjectStreamPacking
  • Kad fajl nije šifrovan, i Catalog i Info rečnik pakuju se u object streamove; šifrovani ih izlaz drži na najvišem nivou

Kompaktna sintaksa je došla sa zamkom vrednom poznavanja ako proširujete pisca. Potpisivanje popunjava potpis posle upisa fajla tražeći po bajtovima literalne placeholdere /ByteRange ( i /Contents <, a kompaktan pravopis pretvorio bi ih u /ByteRange( i /Contents<, što pretraga nikada ne bi našla. Rečnici potpisa (Type Sig ili DocTimeStamp, FT Sig) i rečnik šifrovanja zato zadržavaju razmaknuti raspored. Srodni defekat pogođao je buildove pre v2.766.41: svako čuvanje kroz object streamove, CompressDocument uključeno, počinjalo je sa dve %PDF- header linije, pa nadogradite ako strogan validator označi vaš izlaz

Može li se komprimovati PDF koji je već učitan?

Može, kroz overload sa opcijama CompressLoadedDocument(Options, Info), koji na postojećem fajlu provodi iste bezgubitničke korake. Sa THPDFLoadedDocumentCompressionOptions.Default uklanja nekorišćene resurse stranica, spaja identične fontove i forme, radi font subsetting ugrađenih fontova sa uključenim kompaktnim podskupovima, ponovo komprimuje nefiltrirane, Flate, LZW, ASCII i RunLength streamove Flate-om kad je rezultat manji, i nameće sledećem čuvanju object streamove. HighRatioFlate je podrazumevano isključen, a object streamovi se preskaču za PDF/A-1 i inkrementalna čuvanja. Overload CompressLoadedDocument bez parametara je stariji, uži poziv koji samo Flate-komprimuje nekompromisovane streamove

Tok HotPDF CompressLoadedDocument-a u Delphiju: poziv uklanja nekorišćene resurse stranica, spaja identične fontove i forme, radi subsetting ugrađenih fontova kompaktnim podskupovima, ponovo komprimuje streamove Flate-om samo kad je rezultat manji i uključuje object streamove za sledeće čuvanje, dok polja potpisa okidaju RefusedBySignaturePolicy i fajl ostavljaju nedirnutim
Svaki korak prepravlja bajtove koje potpis pokriva, pa se ceo dokument odbija osim ako izričito dopustite invalidaciju — Info.BytesSaved tada sabira samo posao na resursima, fontovima i streamovima
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 učitanoj putanji bitne su dve granice. Svaki korak prepravlja bajtove koje potpis pokriva, pa se dokument sa poljima potpisa odbija kao celina: poziv vrati 0, postavi RefusedBySignaturePolicy i ništa ne menja, osim ako postavite AllowSignatureInvalidation, posle čega vam Info.SignaturesInvalidated kaže šta ste odustali. Kompakcija je ovde i konzervativnija nego na putanji kreiranja. HotPDF kompaktno samo font programe koje koriste isključivo CIDFontType2 fontovi sa Identity /CIDToGIDMap-om, gde je CID jednak GID-u, i preskače programe sa postojećim map streamom, /CIDSet-om, ili tabelama obojenih glifova poput COLR, sbix, CBDT ili SVG, jer bi kompaktan rebuild bacio slojeve boje. Primetite i da Info.BytesSaved sabira samo korake resursa, fontova i streamova; dobitak object streamova pokazuje se pri upisu fajla

Kakve rezultate možete očekivati u praksi?

Dobitak prati koliko fajla čini nekompresovana struktura i preveliki font podaci, a ne koliko stranica ima. Uzorak od tri stranice sa Arial-om i SimSun-om smanjen je sa 10,2 MB na 20 KB kad je generisan sa CompressDocument-om, i sa 10,2 MB na 19,8 KB kad je nekompresovani original učitan i proveden kroz CompressLoadedDocument, uz identično renderovanje u oba slučaja. PDF koji je već kompaktan jedva mrda: u regresionom skupu takvi fajlovi sačuvani su u rasponu od -0,07% do +0,06% prvobitne veličine. Fajlovi puni fotografija malo dobijaju, jer nijedna putanja ne dira slikove podatke

Ako svake noći generišete iste CJK izveštaje, uparite kompaktne podskupove sa trajnim font podskup kešom na disku da se posao subsettinga ne ponavlja pri svakom pokretanju, a komprimovane izlaze poredite po sadržaju objekata umesto po bajtovima, jer jedna promenjena vrednost ponovo Flate-komprimuje ceo object stream. Potpune reference svojstava i zapisa nalaze se na stranici proizvoda HotPDF Delphi PDF komponente