Tehnički članak

Podskup TrueType fonta preko fontsub.dll u Delphi

HotXLS, Excel komponenta za Delphi i C++Builder, smanjuje veličinu ugrađenog PDF fonta pravljenjem podskupa TrueType fonta: pri izvozu u PDF poziva funkciju CreateFontPackage iz Windows sistemske biblioteke fontsub.dll da ponovo izgradi ugrađeni TrueType font samo oko Unicode kodnih tačaka koje radni list zaista koristi, umesto da isporuči ceo fajl fonta. Izveštaju sa dvesta redova kineskih naziva proizvoda možda je potrebno samo nekoliko stotina različitih Han znakova, dok CJK fontovi koje Windows isporučuje često zauzimaju od 5 do 20 MB svaki. Ako se ugradi ceo font, on sam može biti veći od svih ostalih objekata u PDF-u zajedno

fontsub.dll nije biblioteka za koju je većina Delphi programera ikada čula, i za to postoji razlog: Microsoft je isporučuje kao malu, slabo dokumentovanu pomoćnu DLL biblioteku, a ne kao poznati Win32 API. HotXLS je tretira kao opcionalnu mogućnost, a ne kao čvrstu zavisnost, pa način na koji je izvoznik učitava, poziva i koristi rezervni put kada nedostaje govori podjednako o defanzivnom Windows programiranju i formatima fontova, a oba dela vredi detaljno proći

Zašto Unicode tekst toliko uvećava HotXLS PDF izvoz?

HotXLS PDF izvoznik poseže za ugrađenim TrueType fontom samo kada tekst radnog lista izlazi izvan WinAnsi skupa, a ostatak vremena ostaje na ugrađenoj porodici Helvetica, što je podrazumevana putanja detaljno obrađena u vodiču za izvoz radnog lista u PDF. WinAnsi dovoljno dobro pokriva zapadnoevropski tekst, pa mnoge radne sveske uopšte ne zahtevaju ugrađivanje fonta: PDF samo navodi Helvetica po imenu, a čitač je lokalno obezbeđuje, tako da fajl ostaje mali. Čim ćelija sadrži nešto što WinAnsi ne može da predstavi, poput kineskog naziva proizvoda, korejske beleške ili slučajnog simbola u komentaru, izvoznik mora da ugradi stvarni program fonta, jer PDF čitač nema rezervni izvor glifova za znakove izvan standardnih 14 fontova

HotXLS automatski pronalazi taj font, pretražujući Windows Fonts fasciklu za kratkom listom instaliranih kandidata, uključujući CJK fontove koje Windows isporučuje za prikaz kineskog i korejskog teksta, osim ako svojstvo UnicodeFontFile izvoznika već pokazuje na određeni fajl, a pronađeni font se ugrađuje ceo pre nego što podskup uopšte počne. Zahtev za ugrađivanjem specifičan je za PDF: HotXLS RTF i HTML izvozne putanje čuvaju Unicode tekst nepromenjenim tako što kodne tačke zapisuju u tok bajtova, umesto da isporučuju program fonta, zbog čega problem veličine opisan u ovom članku nema ekvivalent u ta dva formata

uses
  lxHandle, lxPDF;

var
  Book: TXLSWorkbook;
  Exporter: TXLSPDFExport;
begin
  Book := TXLSWorkbook.Create;
  try
    Book.Open('catalog-cn.xlsx');
    Exporter := TXLSPDFExport.Create;
    try
      // Optional: pin a specific CJK-capable font instead of the
      // exporter's automatic Windows\Fonts scan.
      Exporter.UnicodeFontFile := 'C:\Windows\Fonts\simhei.ttf';
      Exporter.SaveAsPDF(Book.ActiveSheet, 'catalog-cn.pdf');
    finally
      Exporter.Free;
    end;
  finally
    Book.Free;
  end;
end;

Šta je fontsub.dll i zašto ne napisati sopstveni alat za podskup?

fontsub.dll je mala Windows sistemska biblioteka, isporučena još od Windows XP, koja ovde izlaže jednu relevantnu funkciju: CreateFontPackage. Kada joj se proslede bajtovi izvornog TrueType fonta i lista Unicode kodnih tačaka koje treba zadržati, vraća minimalni font koji i dalje ispunjava sva ograničenja formata fonta: indeksi glifova su prenumerisani, glyf i loca su ponovo izgrađeni samo oko zadržanih obrisa, a hmtx i cmap su prepisani da im odgovaraju. HotXLS deklariše tip pokazivača na funkciju direktno prema tom ugovoru

const
  TTFCFP_FLAGS_SUBSET = 1;
  TTFMFP_SUBSET = 0;
  TTFCFP_MS_PLATFORMID = 3;
  TTFCFP_UNICODE_CHAR_SET = 1;

type
  TCreateFontPackage = function(puchSrcBuffer: Pointer; ulSrcBufferSize: Cardinal;
    var puchFontPackageBuffer: PAnsiChar; var pulFontPackageBufferSize: Cardinal;
    var pulBytesWritten: Cardinal; usFlags, usTTCIndex, usSubsetFormat,
    usSubsetLanguage, usSubsetPlatform, usSubsetEncoding: Word;
    pusSubsetKeepList: PWordArray; usSubsetKeepListCount: Word;
    lpfnAllocate, lpfnReAllocate, lpfnFree, reserved: Pointer): Cardinal; cdecl;

Ručno pisanje onoga što radi CreateFontPackage, umesto njegovog pozivanja, značilo bi implementaciju ispravnog TrueType alata za podskup: obilazak složenih glifova radi uključivanja svake komponente na koju se zadržani glif poziva, ponovnu izgradnju pomeraja loca nakon uklanjanja obrisa, poštovanje bitova dozvole za ugrađivanje u tabeli OS/2 fonta i ispravan rad sa svim neobičnim fontovima koji se mogu naći na računaru korisnika. Microsoft je taj problem već rešio i isporučuje rešenje kao deo samog Windows sistema, pa HotXLS plaća dinamičkim učitavanjem i pokazivačem na funkciju za poziv sistemske DLL biblioteke koju Microsoft održava, testira uz sopstveni sistem za iscrtavanje fontova i besplatno distribuira svakom računaru; ponovna implementacija iste logike značila bi održavanje parsera binarnog formata sa decenijama graničnih slučajeva, za mogućnost koja je važna samo kada je font veliki

Izrada liste za zadržavanje iz stvarno iscrtanih glifova

HotXLS pravi listu za podskup iz mape koju je već održavao iz drugog razloga, pa vođenje evidencije ne donosi dodatni trošak. Svaki put kada kod za iscrtavanje stranice nacrta znak kome je potreban ugrađeni Unicode font, pronalazi indeks glifa tog znaka i upisuje par u FUnicodeGlyphMap, tabelu glifova i kodnih tačaka koja takođe pokreće PDF ToUnicode CMap, pa kopiranje iz gotovog dokumenta vraća originalni tekst umesto sirovih ID-jeva glifova. Kada se tokovi sadržaja stranice završe, ta mapa već navodi tačno skup Unicode kodnih tačaka koje je dokument koristio, ni više ni manje

var
  keepList: array of Word;
  keepCount, i: Integer;
  codePoint: LongWord;
begin
  SetLength(keepList, FUnicodeGlyphMap.Count);
  keepCount := 0;
  for i := 0 to FUnicodeGlyphMap.Count - 1 do
  begin
    codePoint := LongWord(StrToIntDef('$' + FUnicodeGlyphMap.ValueFromIndex[i], 0));
    if codePoint > 0 then
    begin
      keepList[keepCount] := Word(codePoint);
      Inc(keepCount);
    end;
  end;
end;

Pri završetku, HotXLS drugi put prolazi kroz istu mapu da napravi listu za zadržavanje koju očekuje CreateFontPackage, običan niz Unicode kodnih tačaka koje treba zadržati u 16-bitnom obliku koji zahteva argument liste za zadržavanje ovog API-ja. Pošto je taj argument niz 16-bitnih reči, on dobro obuhvata Basic Multilingual Plane, koji bez komplikacija pokriva uobičajeni CJK, ćirilicu, grčki i arapski tekst; radni list koji koristi znakove iz dopunskog plana, određene emoji znakove ili retka istorijska pisma izlazi iz onoga što jedan unos liste za zadržavanje može direktno imenovati, što je granica koju vredi poznavati, a ne nedostatak, jer velika većina poslovnih tabela sa mnogo Unicode teksta nikada ne zalazi u taj plan

Šta se dešava kada fontsub.dll nedostaje?

HotXLS nikada ne pretpostavlja da je fontsub.dll prisutan i izvoz u PDF nikada ne otkazuje zato što ga nema. Biblioteka se dinamički učitava u trenutku kada je podskup potreban, pomoću SafeLoadLibrary i GetProcAddress, a ne statičkim uvozom, upravo zato što fontsub.dll nije dokumentovan javni API čije je prisustvo zagarantovano na način na koji je to kernel32.dll: to je priloženi alat za ugrađivanje fontova i Microsoftov ugovor ne obećava da će opstati na svakom SKU-u, u svakoj grani servisiranja ili na svakom sloju kompatibilnosti koji pokušava da oponaša Windows

var
  hFontSub: HMODULE;
  CreateFontPackage: TCreateFontPackage;
begin
  hFontSub := SafeLoadLibrary('FontSub.dll');
  if hFontSub = 0 then
    Exit; // no subsetting available - keep the full embedded font
  try
    @CreateFontPackage := GetProcAddress(hFontSub, 'CreateFontPackage');
    if not Assigned(CreateFontPackage) then
      Exit;
    // ... call CreateFontPackage, check its return code ...
  finally
    FreeLibrary(hFontSub);
  end;
end;

Svaki put greške vraća se na isti ishod. Ako DLL nedostaje, izvoz nedostaje, povratni kod nije nula ili tabela OS/2 fonta svojim bitovima dozvole za ugrađivanje zabranjuje pravljenje podskupa, HotXLS jednostavno zadržava ceo font koji je već ugradio i nastavlja dalje. Ništa ne izaziva izuzetak, izvoz se ne prekida, a pozivajući kod ne mora da obavija optimizaciju fonta sopstvenim rukovanjem izuzecima; izvezeni PDF je validan u oba slučaja, a jedina razlika je da li će biti mali ili nešto veći

Koliko PDF zaista postaje manji?

HotXLS pravljenje podskupa TrueType fonta obično smanjuje izvezeni PDF radnog lista sa mnogo Unicode teksta na između dvadesetine i osmine njegove veličine bez podskupa, što znači smanjenje od 8 do 20 puta u zavisnosti od toga koliki deo celog fonta dokument zaista koristi: narudžbenica zasnovana na nekoliko stotina različitih kineskih znakova zadržava samo tih nekoliko stotina glifova od desetina hiljada koje CJK font isporučuje, dok list sa širim skupom znakova zadržava proporcionalno više. HotXLS primenjuje dodatnu Flate kompresiju na bajtove fonta sa podskupom pre njihovog upisivanja u PDF tok /FontFile2, istu kompresiju kroz koju već prolaze tokovi sadržaja ostatka dokumenta, a pozivajući kod zbog toga ne mora ništa dodatno da radi: radni list koji nikada ne izlazi iz WinAnsi putanje ne dodiruje ovaj postupak i nastavlja izvoz preko običnog fonta Helvetica, dok radni list koji aktivira Unicode putanju automatski dobija podskup, bez podešavanja svojstva i bez posebnog poziva, a jedino povezano svojstvo, UnicodeFontFile, bira koji font će biti ugrađen i obrađen, ne da li će se podskup praviti

Pravljenje podskupa fonta samo je jedan detalj šire PDF izvozne površine HotXLS Delphi Excel komponente, uz numerisanje stranica, metapodatke za štampu radnog lista i izvozne putanje za CSV, HTML i RTF koje ona isporučuje