Teknisk artikel

TrueType-typsnittsdelmängder via fontsub.dll i Delphi

HotXLS, Excel-komponenten för Delphi och C++Builder, skär ner inbäddad PDF-typsnittsstorlek genom TrueType-typsnittsdelmängdsbildning: vid PDF-exporttillfället anropar den Windows systembibliotek fontsub.dll:s funktion CreateFontPackage för att bygga om ett inbäddat TrueType-typsnitt kring bara de Unicode-kodpunkter ett kalkylblad faktiskt använde, i stället för att skicka med hela typsnittsfilen. En rapport med tvåhundra rader kinesiska produktnamn kanske bara behöver några hundra distinkta Han-tecken, ändå kör CJK-typsnitten Windows levererar rutinmässigt 5 till 20 MB styck. Bädda in ett helt, och typsnittet ensamt kan väga tyngre än varje annat objekt i PDF:en tillsammans

fontsub.dll är inte ett bibliotek de flesta Delphi-utvecklare någonsin hört talas om, och det finns en anledning till det: Microsoft levererar det som ett litet, sparsamt dokumenterat verktygs-DLL snarare än en huvudrubriks-Win32-API. HotXLS behandlar det som en valfri förmåga, inte ett hårt beroende, så hur exportören laddar den, anropar den, och faller tillbaka när den saknas säger lika mycket om defensiv Windows-programmering som om typsnittsformat, och båda halvorna av den historien är värda att gå igenom

Varför sväller Unicode-text upp en HotXLS-PDF-export?

HotXLS:s PDF-exportör tar till ett inbäddat TrueType-typsnitt bara när kalkylbladstext faller utanför WinAnsi, och stannar på den inbyggda Helvetica-familjen resten av tiden, standardvägen som genomgången av kalkylblad-till-PDF-export täcker på djupet. WinAnsi täcker västeuropeisk text tillräckligt väl för att gott om arbetsböcker aldrig utlöser en typsnittsinbäddning alls: PDF:en refererar bara Helvetica vid namn och läsaren tillhandahåller det lokalt, så filen förblir liten. I samma stund en cell innehåller något WinAnsi inte kan representera, ett kinesiskt produktnamn, en koreansk anteckning, en vilsekommen symbol i en kommentar, måste exportören bädda in ett riktigt typsnittsprogram, eftersom en PDF-läsare inte har någon reservkälla för glyfer utanför de 14 standardtypsnitten

HotXLS hittar det typsnittet automatiskt, genom att skanna Windows Typsnitt-mappen efter en kort lista med installerade kandidater, inklusive de CJK-kapabla typsnitt Windows levererar för kinesisk och koreansk rendering, om inte exportörens UnicodeFontFile-egenskap redan pekar mot en specifik fil, och vilket typsnitt den än landar på bäddas in helt innan delmängdsbildning någonsin körs. Det inbäddningskravet är specifikt för PDF: HotXLS:s RTF- och HTML-exportvägar håller Unicode-text intakt genom att escapa kodpunkter in i bytströmmen i stället för att skicka med ett typsnittsprogram, vilket är varför storleksproblemet den här artikeln täcker inte har någon motsvarighet på de två formaten

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;

Vad är fontsub.dll, och varför inte skriva en delmängdsbildare från grunden?

fontsub.dll är ett litet Windows-systembibliotek, levererat sedan Windows XP, som exponerar en enda funktion relevant här: CreateFontPackage. Ge den bytesen från ett käll-TrueType-typsnitt och en lista med Unicode-kodpunkter att behålla, och den lämnar tillbaka ett minimalt typsnitt som fortfarande uppfyller varje typsnittsformatbegränsning: glyfindex omnumrerade, glyf och loca ombyggda kring bara de behållna konturerna, hmtx och cmap omskrivna för att matcha. HotXLS deklarerar funktionspekartypen direkt mot det kontraktet

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;

Att skriva CreateFontPackages jobb för hand i stället för att anropa det skulle innebära att implementera en korrekt TrueType-delmängdsbildare: gå igenom sammansatta glyfer för att dra in varje komponentglyf en behållen glyf refererar till, bygga om loca-offset efter att konturer tagits bort, respektera inbäddningsbehörighetsbitarna i ett typsnitts OS/2-tabell, och få allt det rätt över vilka udda typsnitt en kunds maskin än råkar ha installerade. Microsoft har redan löst det problemet och levererar lösningen som en del av Windows självt, så att anropa en systemDLL Microsoft underhåller, testar mot sin egen typsnittsrenderingsstack, och distribuerar till varje maskin gratis kostar HotXLS en dynamisk laddning och en funktionspekare; att implementera om samma logik skulle innebära att äga en parser för ett binärt format med årtionden av specialfall, för en funktion som bara spelar roll när ett typsnitt råkar vara stort

Att bygga behåll-listan från glyfer som faktiskt renderades

HotXLS bygger delmängdsbildningens behåll-lista från en karta den redan underhöll av en annan anledning, så bokföringen kostar ingenting extra. Varje gång sidrenderingskoden ritar ett tecken som behöver det inbäddade Unicode-typsnittet, slår den upp det tecknets glyfindex och registrerar paret i FUnicodeGlyphMap, en glyf-till-kodpunkt-tabell som också driver PDF-ToUnicode-CMappen så att kopiera-och-klistra ur det färdiga dokumentet returnerar den ursprungliga texten i stället för råa glyf-ID:n. Vid den tidpunkt då sidinnehållsströmmarna är klara listar den kartan redan exakt den uppsättning Unicode-kodpunkter dokumentet använde, varken mer eller mindre

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;

Vid finaliseringstillfället går HotXLS igenom samma karta en andra gång för att bygga den behåll-lista CreateFontPackage förväntar sig, en vanlig array av Unicode-kodpunkterna att behålla i den 16-bitars form API:ets behåll-lista-argument kräver. Eftersom det argumentet är en array av 16-bitars ord adresserar det Basic Multilingual Plane rent, vilket täcker vanlig CJK-, kyrillisk-, grekisk- och arabisk text utan komplikation; ett kalkylblad som lutar sig mot tecken i tilläggsplan, vissa emoji eller sällsynta historiska skriftsystem, sitter utanför vad en enda behåll-lista-post kan namnge direkt, vilket är en gräns värd att känna till snarare än en defekt, eftersom den stora majoriteten av Unicode-tunga företagskalkylblad aldrig går i närheten av det planet från början

Vad händer när fontsub.dll saknas?

HotXLS antar aldrig att fontsub.dll finns, och PDF-exporten misslyckas aldrig för att den inte gör det. Biblioteket laddas dynamiskt i samma stund en delmängd behövs, med SafeLoadLibrary och GetProcAddress snarare än en statisk import, precis eftersom fontsub.dll inte är ett dokumenterat, garanterat närvarande publikt API på det sätt kernel32.dll är: det är medskickat typsnittsinbäddningsverktyg, och inget i Microsofts kontrakt lovar att det överlever på varje SKU, varje underhållsgren, eller varje kompatibilitetslager som försöker emulera 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;

Varje felväg viker tillbaka till samma utfall. Ett saknat DLL, en saknad export, en icke-noll returkod, eller ett typsnitt vars OS/2-tabell förbjuder delmängdsbildning via dess inbäddningsbehörighetsbitar, HotXLS behåller bara det fullständiga typsnittet den redan bäddat in och fortsätter. Inget kastar, inget avbryter exporten, och den anropande koden behöver aldrig omsluta en typsnittsoptimering i sin egen undantagshantering; den exporterade PDF:en är giltig oavsett, och den enda variabeln är om den slutar liten eller något större

Hur mycket mindre blir PDF:en faktiskt?

HotXLS:s TrueType-typsnittsdelmängdsbildning krymper typiskt ett Unicode-tungt kalkylblads exporterade PDF till någonstans mellan en tjugondel och en åttondel av dess odelmängdsbildade storlek, en 8-till-20-faldig minskning vars skala följer hur mycket av ett fullständigt typsnitt ett givet dokument faktiskt rör: en inköpsorder byggd kring några hundra distinkta kinesiska tecken behåller bara de få hundra glyferna av de tiotusentals ett CJK-typsnitt levererar, medan ett blad som spänner över en bredare blandning av tecken behåller proportionellt fler. HotXLS lägger ett ytterligare Flate-komprimeringspass ovanpå delmängdstypsnittsbytesen innan de skrivs in i PDF:ens /FontFile2-ström, samma komprimering resten av dokumentets innehållsströmmar redan går igenom, och inget av det kräver något extra av den anropande koden: ett kalkylblad som aldrig lämnar WinAnsi rör aldrig den här vägen och fortsätter exportera genom ren Helvetica, medan ett kalkylblad som väl utlöser Unicode-typsnittsvägen får delmängdsbildning automatiskt, utan någon egenskap att sätta och inget separat anrop att göra, och den enda egenskapen inblandad, UnicodeFontFile, väljer bara vilket typsnitt som bäddas in och delmängdsbildas, inte om delmängdsbildning sker

Typsnittsdelmängdsbildning är en detalj inuti den bredare PDF-exportytan av HotXLS Delphi Excel-komponenten, tillsammans med paginering, kalkylbladsutskriftsmetadata, och CSV-, HTML- och RTF-exportvägarna den levereras med