Articol tehnic

Subsetarea fonturilor TrueType prin fontsub.dll în Delphi

HotXLS, componenta Excel pentru Delphi și C++Builder, reduce dimensiunea fonturilor PDF încorporate prin subsetarea fonturilor TrueType: la momentul exportului PDF apelează funcția CreateFontPackage a bibliotecii de sistem Windows fontsub.dll pentru a reconstrui un font TrueType încorporat în jurul doar al punctelor de cod Unicode pe care le-a folosit efectiv o foaie de calcul, în loc să livreze întregul fișier de tipar. Un raport cu două sute de rânduri de nume de produse chinezești ar putea avea nevoie doar de câteva sute de caractere Han distincte, totuși fonturile CJK pe care le livrează Windows au de obicei 5 până la 20 MB fiecare. Încorporați-l întreg, iar fontul singur poate depăși ca greutate fiecare alt obiect din PDF combinat

fontsub.dll nu este o bibliotecă despre care majoritatea dezvoltatorilor Delphi au auzit vreodată, și există un motiv pentru asta: Microsoft o livrează ca o mică DLL utilitară puțin documentată, nu ca un API Win32 de prim rang. HotXLS o tratează ca o capabilitate opțională, nu ca o dependență dură, așa că modul în care exportatorul o încarcă, o apelează și revine când lipsește spune la fel de multe despre programarea defensivă pentru Windows ca și despre formatele de font, iar ambele jumătăți ale acelei povești merită parcurse

De ce umflă textul Unicode un export PDF HotXLS?

Exportatorul PDF al HotXLS apelează la un font TrueType încorporat doar atunci când textul foii de calcul depășește WinAnsi, și rămâne pe familia încorporată Helvetica restul timpului, calea implicită pe care ghidul de export foaie-de-calcul-la-PDF o acoperă în detaliu. WinAnsi acoperă textul vest-european suficient de bine încât multe registre de lucru nu declanșează niciodată o încorporare de font: PDF-ul doar referă Helvetica după nume, iar cititorul o furnizează local, așa că fișierul rămâne mic. În momentul în care o celulă conține ceva ce WinAnsi nu poate reprezenta, un nume de produs chinezesc, o notă coreeană, un simbol rătăcit într-un comentariu, exportatorul trebuie să încorporeze un program de font efectiv, pentru că un cititor PDF nu are nicio sursă de glife de rezervă pentru caractere din afara celor 14 fonturi standard

HotXLS localizează acel font automat, scanând folderul Fonts al Windows pentru o scurtă listă de candidați instalați, incluzând tipurile de literă capabile de CJK pe care Windows le livrează pentru randarea chineză și coreeană, decât dacă proprietatea UnicodeFontFile a exportatorului indică deja spre un fișier specific, iar orice font pe care ajunge este încorporat întreg înainte ca subsetarea să ruleze vreodată. Această cerință de încorporare este specifică PDF-ului: căile de export RTF și HTML ale HotXLS păstrează textul Unicode intact prin escaparea punctelor de cod în fluxul de octeți, în loc să livreze un program de font, motiv pentru care problema de dimensiune acoperită de acest articol nu are echivalent pe acele două formate

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;

Ce este fontsub.dll și de ce nu se scrie un subsetter de la zero?

fontsub.dll este o mică bibliotecă de sistem Windows, livrată încă de la Windows XP, care expune o singură funcție relevantă aici: CreateFontPackage. Predați-i octeții unui font TrueType sursă și o listă de puncte de cod Unicode de păstrat, iar ea returnează un font minimal care încă satisface fiecare constrângere de format de font: indexurile de glife renumerotate, glyf și loca reconstruite în jurul doar al contururilor păstrate, hmtx și cmap rescrise pentru a se potrivi. HotXLS declară tipul pointerului de funcție direct față de acel contract

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;

Scrierea manuală a sarcinii lui CreateFontPackage în loc să fie apelat ar însemna implementarea unui subsetter TrueType corect: parcurgerea glifelor compuse pentru a include fiecare glifă componentă la care se referă o glifă păstrată, reconstruirea offset-urilor loca după ce contururile sunt eliminate, respectarea biților de permisiune de încorporare din tabelul OS/2 al unui font, și obținerea corectă a tuturor acestora pe orice fonturi ciudate se întâmplă să aibă instalate un calculator de client. Microsoft a rezolvat deja acea problemă și livrează soluția ca parte a Windows-ului însuși, așa că apelarea unei DLL de sistem pe care ea o menține, o testează față de propriul stack de randare de fonturi și o distribuie gratuit pe fiecare mașină costă HotXLS o încărcare dinamică și un pointer de funcție; reimplementarea aceleiași logici ar însemna deținerea unui parser pentru un format binar cu decenii de cazuri limită, pentru o funcționalitate care contează doar atunci când un font se întâmplă să fie mare

Construirea listei de păstrare din glifele efectiv randate

HotXLS construiește lista de păstrare pentru subsetare dintr-o hartă pe care o menținea deja dintr-un alt motiv, așa că evidența nu costă nimic în plus. De fiecare dată când codul de randare a paginii desenează un caracter care are nevoie de fontul Unicode încorporat, acesta caută indexul de glifă al acelui caracter și înregistrează perechea în FUnicodeGlyphMap, un tabel glifă-la-punct-de-cod care conduce și CMap-ul PDF ToUnicode, astfel încât copierea și lipirea din documentul finalizat returnează textul original, nu ID-uri brute de glifă. Până când fluxurile de conținut ale paginii sunt terminate, acea hartă listează deja exact setul de puncte de cod Unicode pe care documentul le-a folosit, nici mai mult, nici mai puțin

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;

La momentul finalizării, HotXLS parcurge aceeași hartă a doua oară pentru a construi lista de păstrare pe care o așteaptă CreateFontPackage, un simplu tablou al punctelor de cod Unicode de reținut, în forma pe 16 biți pe care o cere argumentul de listă de păstrare al API-ului. Pentru că acel argument este un tablou de cuvinte pe 16 biți, el adresează curat Planul Multilingv de Bază, ceea ce acoperă CJK obișnuit, chirilic, grecesc și arab fără complicații; o foaie de calcul care se bazează pe caractere din plane suplimentare, anumite emoji sau scripturi istorice rare, se află în afara a ceea ce o singură intrare din lista de păstrare poate numi direct, ceea ce este o limită care merită cunoscută, nu un defect, întrucât marea majoritate a foilor de calcul de business intensive în Unicode nu se apropie niciodată de acel plan în primul rând

Ce se întâmplă când fontsub.dll lipsește?

HotXLS nu presupune niciodată că fontsub.dll este prezent, iar exportul PDF nu eșuează niciodată din cauza asta. Biblioteca este încărcată dinamic în momentul în care este necesar un subset, cu SafeLoadLibrary și GetProcAddress, nu cu un import static, tocmai pentru că fontsub.dll nu este un API public documentat, garantat prezent, așa cum este kernel32.dll: este o unealtă de încorporare de fonturi grupată, iar nimic din contractul Microsoft nu promite că supraviețuiește pe fiecare SKU, fiecare ramură de servicing, sau fiecare strat de compatibilitate care încearcă să emuleze 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;

Fiecare cale de eșec se pliază înapoi la același rezultat. O DLL lipsă, un export lipsă, un cod de retur diferit de zero, sau un font al cărui tabel OS/2 interzice subsetarea prin biții săi de permisiune de încorporare, HotXLS pur și simplu păstrează fontul complet pe care îl încorporase deja și merge mai departe. Nimic nu ridică o excepție, nimic nu abandonează exportul, iar codul apelant nu trebuie niciodată să învelească o optimizare de font în propria gestionare de excepții; PDF-ul exportat este valid în orice caz, iar singura variabilă este dacă ajunge mic sau ceva mai mare

Cât de mult mai mic devine efectiv PDF-ul?

Subsetarea fonturilor TrueType a HotXLS de obicei reduce PDF-ul exportat al unei foi de calcul intensive în Unicode la undeva între o douăzecime și o optime din dimensiunea sa nesubsetată, o reducere de 8 până la 20 de ori, a cărei scară urmărește cât de mult dintr-un font complet atinge efectiv un document dat: un ordin de achiziție construit în jurul câtorva sute de caractere chinezești distincte păstrează doar acele câteva sute de glife din zecile de mii pe care le livrează un tip de literă CJK, în timp ce o foaie care se întinde pe un mix mai larg de caractere păstrează proporțional mai multe. HotXLS stratifică o trecere suplimentară de compresie Flate peste octeții fontului subsetat înainte de a-i scrie în fluxul /FontFile2 al PDF-ului, aceeași compresie prin care trec deja fluxurile de conținut ale restului documentului, iar nimic din toate acestea nu cere ceva în plus codului apelant: o foaie de calcul care nu părăsește niciodată WinAnsi nu atinge niciodată această cale și continuă să exporte prin simplul Helvetica, în timp ce o foaie de calcul care declanșează calea fontului Unicode primește subsetare automat, fără nicio proprietate de setat și niciun apel separat de făcut, iar singura proprietate implicată, UnicodeFontFile, doar alege ce font este încorporat și subsetat, nu dacă subsetarea are loc

Subsetarea fonturilor este un detaliu în interiorul suprafeței mai largi de export PDF a componentei Excel HotXLS pentru Delphi, alături de paginare, metadatele de tipărire a foilor de calcul și căile de export CSV, HTML și RTF cu care este livrată