HotXLS, Delphi- og C++Builder-Excel-komponenten, skærer indlejret PDF-skrifttypestørrelse gennem TrueType-skrifttype-subsetting: ved PDF-eksporttidspunktet kalder den Windows-systembiblioteket fontsub.dll's CreateFontPackage-funktion for at genopbygge en indlejret TrueType-skrifttype omkring kun de Unicode-kodepunkter, et regneark rent faktisk brugte, i stedet for at sende hele skrifttypefilen med. En rapport med to hundrede rækker af kinesiske produktnavne har måske kun brug for et par hundrede distinkte Han-tegn, men de CJK-skrifttyper Windows leverer, kører rutinemæssigt på 5 til 20 MB stykket. Indlejr én hel, og skrifttypen alene kan opveje hvert andet objekt i PDF'en tilsammen
fontsub.dll er ikke et bibliotek, de fleste Delphi-udviklere nogensinde har hørt om, og der er en grund til det: Microsoft leverer det som en lille, sparsomt dokumenteret hjælpe-DLL frem for en topbillet-Win32-API. HotXLS behandler det som en valgfri egenskab, ikke en hård afhængighed, så hvordan eksportøren indlæser den, kalder den, og falder tilbage, når den mangler, siger lige så meget om defensiv Windows-programmering, som det gør om skrifttypeformater, og begge halvdele af den historie er værd at gennemgå
Hvorfor får Unicode-tekst en HotXLS-PDF-eksport til at ballonere?
HotXLSs PDF-eksportør griber kun til en indlejret TrueType-skrifttype, når regnearktekst falder uden for WinAnsi, og forbliver på den indbyggede Helvetica-familie resten af tiden, standardvejen dækket dybdegående i gennemgangen af regneark-til-PDF-eksport. WinAnsi dækker vesteuropæisk tekst godt nok til, at masser af projektmapper aldrig udløser en skrifttype-indlejring overhovedet: PDF'en refererer bare Helvetica ved navn, og læseren leverer den lokalt, så filen forbliver lille. I det øjeblik en celle indeholder noget, WinAnsi ikke kan repræsentere, et kinesisk produktnavn, en koreansk note, et vildfarent symbol i en kommentar, må eksportøren indlejre et rigtigt skrifttypeprogram, fordi en PDF-læser ikke har nogen fallback-glyf-kilde til tegn uden for de 14 standardskrifttyper
HotXLS finder den skrifttype automatisk ved at skanne Windows Fonts-mappen for en kort liste af installerede kandidater, inklusive de CJK-kapable skrifttyper Windows leverer til kinesisk og koreansk gengivelse, medmindre eksportørens UnicodeFontFile-egenskab allerede peger på en specifik fil, og hvilken skrifttype den end lander på, bliver indlejret helt, før subsetting overhovedet kører. Det indlejringskrav er specifikt for PDF: HotXLSs RTF- og HTML-eksportveje holder Unicode-tekst intakt ved at escape kodepunkter ind i byte-strømmen frem for at sende et skrifttypeprogram, hvilket er grunden til, at størrelsesproblemet denne artikel dækker, ikke har nogen tilsvarende på de to formater
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;
Hvad er fontsub.dll, og hvorfor ikke skrive en subsetter fra bunden?
fontsub.dll er et lille Windows-systembibliotek, leveret siden Windows XP, der eksponerer én funktion relevant her: CreateFontPackage. Giv den bytene fra en kilde-TrueType-skrifttype og en liste af Unicode-kodepunkter at beholde, og den giver en minimal skrifttype tilbage, der stadig opfylder hver skrifttypeformat-begrænsning: glyf-indekser omnummereret, glyf og loca genopbygget omkring kun de bevarede omrids, hmtx og cmap omskrevet til at matche. HotXLS erklærer funktionspointer-typen direkte mod den kontrakt
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;
At skrive CreateFontPackage's opgave i hånden i stedet for at kalde den ville betyde at implementere en korrekt TrueType-subsetter: at gennemgå sammensatte glyffer for at trække hver komponentglyf ind, en bevaret glyf refererer, genopbygge loca-forskydninger, efter omrids er droppet, respektere indlejrings-tilladelsesbittene i en skrifttypes OS/2-tabel, og få det hele rigtigt på tværs af hvilke som helst mærkelige skrifttyper, en kundes maskine tilfældigvis har installeret. Microsoft har allerede løst det problem og leverer løsningen som en del af selve Windows, så at kalde en system-DLL, den vedligeholder, tester mod sin egen skrifttype-gengivelses-stak og distribuerer til hver maskine gratis, koster HotXLS en dynamisk indlæsning og en funktionspointer; at genimplementere den samme logik ville betyde at eje en parser til et binært format med årtiers randtilfælde, til en funktion der kun betyder noget, når en skrifttype tilfældigvis er stor
At bygge keep-listen fra glyffer rent faktisk gengivet
HotXLS bygger subsetting-keep-listen fra et map, det allerede vedligeholdt af en anden grund, så regnskabet koster intet ekstra. Hver gang side-gengivelseskoden tegner et tegn, der har brug for den indlejrede Unicode-skrifttype, slår den det tegns glyf-indeks op og registrerer parringen i FUnicodeGlyphMap, en glyf-til-kodepunkt-tabel der også driver PDF ToUnicode-CMap'en, så kopiér-og-indsæt fra det færdige dokument returnerer den oprindelige tekst frem for rå glyf-ID'er. Når side-content-strømmene er færdige, lister det map allerede nøjagtig det sæt Unicode-kodepunkter, dokumentet brugte, hverken flere eller færre
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;
Ved finaliseringstidspunktet gennemgår HotXLS det samme map en anden gang for at bygge keep-listen, CreateFontPackage forventer, et almindeligt array af de Unicode-kodepunkter, der skal bevares, i den 16-bit form API'ets keep-liste-argument kræver. Fordi det argument er et array af 16-bit ord, adresserer det Basic Multilingual Plane rent, hvilket dækker almindelig CJK-, kyrillisk-, græsk- og arabisk tekst uden komplikationer; et regneark, der læner sig på supplerende-plan-tegn, visse emoji eller sjældne historiske skrifter, sidder uden for, hvad en enkelt keep-liste-post kan navngive direkte, hvilket er en grænse værd at kende til frem for en defekt, da langt de fleste Unicode-tunge forretningsregneark aldrig kommer i nærheden af det plan i første omgang
Hvad sker der, når fontsub.dll mangler?
HotXLS antager aldrig, at fontsub.dll er til stede, og PDF-eksporten fejler aldrig, fordi den ikke er. Biblioteket indlæses dynamisk, i det øjeblik en subset er nødvendig, med SafeLoadLibrary og GetProcAddress frem for en statisk import, netop fordi fontsub.dll ikke er en dokumenteret, garanteret-tilstedeværende offentlig API på den måde kernel32.dll er: det er medfølgende skrifttype-indlejrings-værktøj, og intet i Microsofts kontrakt lover, at det overlever på hver SKU, hver servicing-gren, eller hvert kompatibilitetslag, der forsøger at emulere 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;
Hver fejlvej folder tilbage til det samme resultat. En manglende DLL, en manglende eksport, en ikke-nul returkode, eller en skrifttype hvis OS/2-tabel forbyder subsetting gennem sine indlejrings-tilladelsesbits, HotXLS beholder bare den fulde skrifttype, den allerede havde indlejret, og fortsætter. Intet kaster en undtagelse, intet afbryder eksporten, og den kaldende kode skal aldrig pakke en skrifttypeoptimering ind i sin egen undtagelseshåndtering; den eksporterede PDF er gyldig enten som, og den eneste variabel er, om den ender lille eller noget større
Hvor meget mindre bliver PDF'en rent faktisk?
HotXLSs TrueType-skrifttype-subsetting krymper typisk et Unicode-tungt regnearks eksporterede PDF til et sted mellem en tyvendedel og en ottendedel af sin ikke-subsettede størrelse, en 8-til-20-gange-reduktion, hvis skala følger, hvor meget af en fuld skrifttype et givet dokument rent faktisk rører: en indkøbsordre bygget omkring et par hundrede distinkte kinesiske tegn beholder kun de få hundrede glyffer ud af de titusinder, en CJK-skrifttype leverer, mens et ark der spænder over en bredere blanding af tegn, beholder proportionelt flere. HotXLS lægger et yderligere Flate-komprimeringspas oven på de subsettede skrifttype-bytes, før den skriver dem ind i PDF'ens /FontFile2-stream, den samme komprimering resten af dokumentets content-streams allerede går igennem, og intet af det beder om noget ekstra fra den kaldende kode: et regneark, der aldrig forlader WinAnsi, rører aldrig denne vej og bliver ved med at eksportere gennem almindelig Helvetica, mens et regneark, der udløser Unicode-skrifttype-vejen, får subsetting automatisk, uden nogen egenskab at sætte og intet separat kald at foretage, og den ene egenskab involveret, UnicodeFontFile, vælger kun hvilken skrifttype der indlejres og subsettes, ikke om subsetting sker
Skrifttype-subsetting er én detalje inden i den bredere PDF-eksport-flade af HotXLS Delphi Excel-komponenten, sammen med paginering, regneark-udskrifts-metadata og de CSV-, HTML- og RTF-eksportveje, den leveres med