Artículo técnico

Subconjuntos de fuentes TrueType mediante fontsub.dll en Delphi

HotXLS, el componente Excel para Delphi y C++Builder, reduce el tamaño de las fuentes PDF incrustadas mediante subconjuntos de fuentes TrueType: en el momento de exportar a PDF llama a la función CreateFontPackage de la biblioteca del sistema fontsub.dll de Windows para reconstruir una fuente TrueType incrustada en torno únicamente a los puntos de código Unicode que una hoja de cálculo realmente usó, en lugar de distribuir el archivo de tipografía completo. Un informe con doscientas filas de nombres de producto en chino puede necesitar solo unos pocos cientos de caracteres Han distintos, y sin embargo las fuentes CJK que incluye Windows suelen pesar entre 5 y 20 MB cada una. Incrustad una entera y la fuente por sí sola puede superar en peso a todos los demás objetos del PDF juntos

fontsub.dll no es una biblioteca de la que la mayoría de los desarrolladores Delphi hayan oído hablar jamás, y hay una razón para ello: Microsoft la distribuye como una pequeña DLL de utilidad escasamente documentada en lugar de como una API Win32 de primera fila. HotXLS la trata como una capacidad opcional, no como una dependencia obligatoria, así que cómo la carga el exportador, cómo la llama y cómo recae en un respaldo cuando falta dice tanto sobre la programación defensiva en Windows como sobre los formatos de fuente, y ambas mitades de la historia merecen recorrerse

¿Por qué el texto Unicode dispara el tamaño de una exportación PDF de HotXLS?

El exportador PDF de HotXLS solo recurre a una fuente TrueType incrustada cuando el texto de la hoja de cálculo cae fuera de WinAnsi, y se mantiene en la familia Helvetica integrada el resto del tiempo, la vía por defecto que cubre en profundidad el recorrido por la exportación de hoja de cálculo a PDF. WinAnsi cubre el texto de Europa occidental lo bastante bien como para que muchos libros de Excel nunca lleguen a disparar una incrustación de fuente: el PDF simplemente referencia Helvetica por nombre y el lector la suministra localmente, así que el archivo se mantiene pequeño. En el momento en que una celda contiene algo que WinAnsi no puede representar, un nombre de producto en chino, una nota en coreano, un símbolo suelto en un comentario, el exportador tiene que incrustar un programa de fuente real, porque un lector de PDF no tiene ninguna fuente de glifos de respaldo para caracteres fuera de las 14 fuentes estándar

HotXLS localiza esa fuente automáticamente, escaneando la carpeta de fuentes de Windows en busca de una lista corta de candidatas instaladas, incluidas las tipografías con capacidad CJK que Windows distribuye para el renderizado de chino y coreano, a menos que la propiedad UnicodeFontFile del exportador ya apunte a un archivo concreto, y cualquiera que sea la fuente en la que recaiga se incrusta entera antes de que se ejecute el subconjunto. Ese requisito de incrustación es específico de PDF: las vías de exportación RTF y HTML de HotXLS mantienen el texto Unicode intacto escapando los puntos de código en el flujo de bytes en lugar de distribuir un programa de fuente, razón por la cual el problema de tamaño que cubre este artículo no tiene equivalente en esos dos formatos

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;

¿Qué es fontsub.dll, y por qué no escribir un generador de subconjuntos desde cero?

fontsub.dll es una pequeña biblioteca de sistema de Windows, distribuida desde Windows XP, que expone una única función relevante aquí: CreateFontPackage. Entregadle los bytes de una fuente TrueType de origen y una lista de puntos de código Unicode que conservar, y os devuelve una fuente mínima que sigue satisfaciendo cada restricción del formato de fuente: índices de glifo renumerados, glyf y loca reconstruidos en torno únicamente a los contornos conservados, hmtx y cmap reescritos para coincidir. HotXLS declara el tipo de puntero a función directamente contra ese contrato

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;

Escribir a mano el trabajo de CreateFontPackage en lugar de llamarlo implicaría implementar un generador de subconjuntos TrueType correcto: recorrer los glifos compuestos para incorporar cada glifo componente al que hace referencia un glifo conservado, reconstruir los desplazamientos de loca tras eliminar contornos, respetar los bits de permiso de incrustación de la tabla OS/2 de una fuente, y hacerlo todo bien con cualquier fuente peculiar que resulte tener instalada la máquina de un cliente. Microsoft ya ha resuelto ese problema y distribuye la solución como parte del propio Windows, así que llamar a una DLL de sistema que ella misma mantiene, prueba contra su propia pila de renderizado de fuentes y distribuye gratis a cada máquina le cuesta a HotXLS una carga dinámica y un puntero a función; reimplementar la misma lógica supondría hacerse cargo de un analizador para un formato binario con décadas de casos límite, para una funcionalidad que solo importa cuando una fuente resulta ser grande

Construir la lista de conservación a partir de los glifos realmente renderizados

HotXLS construye la lista de conservación del subconjunto a partir de un mapa que ya mantenía por otro motivo, así que la contabilidad no cuesta nada extra. Cada vez que el código de renderizado de página dibuja un carácter que necesita la fuente Unicode incrustada, busca el índice de glifo de ese carácter y registra el emparejamiento en FUnicodeGlyphMap, una tabla de glifo a punto de código que también gobierna el CMap ToUnicode del PDF, de modo que copiar y pegar desde el documento terminado devuelve el texto original en lugar de identificadores de glifo en bruto. Para cuando los flujos de contenido de página están terminados, ese mapa ya enumera exactamente el conjunto de puntos de código Unicode que usó el documento, ni uno más ni uno menos

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;

En el momento de finalizar, HotXLS recorre ese mismo mapa una segunda vez para construir la lista de conservación que espera CreateFontPackage, un simple array de los puntos de código Unicode que hay que conservar en la forma de 16 bits que exige el argumento de lista de conservación de la API. Como ese argumento es un array de palabras de 16 bits, direcciona el Plano Multilingüe Básico sin problemas, que cubre sin complicaciones el texto CJK ordinario, cirílico, griego y árabe; una hoja de cálculo que se apoye en caracteres de un plano suplementario, ciertos emoji o escrituras históricas poco frecuentes, queda fuera de lo que una única entrada de lista de conservación puede nombrar directamente, algo que merece la pena conocer como límite y no como defecto, ya que la inmensa mayoría de las hojas de cálculo empresariales con mucho Unicode nunca se acercan a ese plano en primer lugar

¿Qué ocurre cuando falta fontsub.dll?

HotXLS nunca asume que fontsub.dll está presente, y la exportación a PDF nunca falla porque no lo esté. La biblioteca se carga dinámicamente en el momento en que se necesita un subconjunto, con SafeLoadLibrary y GetProcAddress en lugar de una importación estática, precisamente porque fontsub.dll no es una API pública documentada y de presencia garantizada como sí lo es kernel32.dll: es herramienta de incrustación de fuentes empaquetada, y nada en el contrato de Microsoft promete que sobreviva en cada SKU, en cada rama de mantenimiento ni en cada capa de compatibilidad que intente emular 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;

Cada vía de fallo se repliega hacia el mismo resultado. Una DLL ausente, una exportación ausente, un código de retorno distinto de cero, o una fuente cuya tabla OS/2 prohíbe la generación de subconjuntos mediante sus bits de permiso de incrustación, HotXLS simplemente conserva la fuente completa que ya había incrustado y sigue adelante. Nada lanza una excepción, nada aborta la exportación, y el código que llama nunca tiene que envolver una optimización de fuente en su propio manejo de excepciones; el PDF exportado es válido en cualquier caso, y la única variable es si acaba siendo pequeño o algo más grande

¿Cuánto se reduce realmente el PDF?

La generación de subconjuntos de fuentes TrueType de HotXLS normalmente reduce el PDF exportado de una hoja de cálculo con mucho Unicode a algo entre una veinteava y una octava parte de su tamaño sin subconjunto, una reducción de 8 a 20 veces cuya magnitud sigue de cerca cuánto de una fuente completa toca realmente un documento dado: un pedido de compra construido en torno a unos pocos cientos de caracteres chinos distintos conserva solo esos pocos cientos de glifos de las decenas de miles que incluye una tipografía CJK, mientras que una hoja que abarca una mezcla más amplia de caracteres conserva proporcionalmente más. HotXLS añade una pasada adicional de compresión Flate encima de los bytes de fuente del subconjunto antes de escribirlos en el flujo /FontFile2 del PDF, la misma compresión por la que ya pasan los demás flujos de contenido del documento, y nada de esto pide nada extra al código que llama: una hoja de cálculo que nunca sale de WinAnsi nunca toca esta vía y sigue exportando con la Helvetica plana, mientras que una hoja de cálculo que sí dispara la vía de fuente Unicode obtiene la generación de subconjuntos automáticamente, sin ninguna propiedad que establecer ni ninguna llamada aparte que hacer, y la única propiedad implicada, UnicodeFontFile, solo elige qué fuente se incrusta y se reduce a subconjunto, no si la generación de subconjuntos ocurre

La generación de subconjuntos de fuentes es un detalle más dentro de la superficie de exportación PDF más amplia del componente Excel HotXLS para Delphi, junto con la paginación, los metadatos de impresión de hoja de cálculo y las vías de exportación a CSV, HTML y RTF con las que se distribuye