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 de Windows fontsub.dll para reconstruir una fuente TrueType incrustada alrededor de solo los puntos de código Unicode que realmente usó una hoja de cálculo, en lugar de enviar el archivo de tipografía completo. Un informe con doscientas filas de nombres de productos en chino podría necesitar solo unos pocos cientos de caracteres Han distintos, sin embargo, las fuentes CJK que incluye Windows suelen pesar entre 5 y 20 MB cada una. Incruste una completa, y la fuente por sí sola puede superar en peso a todos los demás objetos del PDF combinados

fontsub.dll no es una biblioteca de la que la mayoría de los desarrolladores Delphi hayan oído hablar, y hay una razón para eso: Microsoft la distribuye como una pequeña DLL de utilidad escasamente documentada en lugar de una API Win32 destacada. HotXLS la trata como una capacidad opcional, no como una dependencia estricta, así que cómo la carga, la llama, y recurre a un respaldo cuando falta dice tanto sobre programación defensiva de Windows como sobre formatos de fuente, y ambas mitades de esa historia vale la pena recorrerlas

¿Por qué el texto Unicode infla una exportación PDF de HotXLS?

El exportador PDF de HotXLS recurre a una fuente TrueType incrustada solo 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 ruta predeterminada que cubre en profundidad el recorrido de exportación de hoja de cálculo a PDF. WinAnsi cubre el texto de Europa Occidental lo suficientemente bien como para que muchos libros de trabajo nunca disparen una incrustación de fuente en absoluto: el PDF simplemente referencia Helvetica por nombre y el lector la provee 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 perdido en un comentario, el exportador tiene que incrustar un programa de fuente real, porque un lector de PDF no tiene ninguna fuente de glifo 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 incluye para el renderizado de chino y coreano, a menos que la propiedad UnicodeFontFile del exportador ya apunte a un archivo específico, y cualquiera que sea la fuente en la que aterrice se incrusta completa antes de que se ejecute cualquier creación de subconjunto. Ese requisito de incrustación es específico de PDF: las rutas de exportación RTF y HTML de HotXLS mantienen el texto Unicode intacto escapando los puntos de código dentro del flujo de bytes en lugar de enviar un programa de fuente, que es por qué 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 creador de subconjuntos desde cero?

fontsub.dll es una pequeña biblioteca del sistema de Windows, incluida desde Windows XP, que expone una única función relevante aquí: CreateFontPackage. Entréguele los bytes de una fuente TrueType de origen y una lista de puntos de código Unicode que conservar, y le devuelve una fuente mínima que sigue satisfaciendo cada restricción del formato de fuente: índices de glifo renumerados, glyf y loca reconstruidos alrededor de solo los contornos retenidos, 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 llamarla significaría implementar un creador de subconjuntos TrueType correcto: recorrer glifos compuestos para incorporar cada glifo componente que referencia un glifo retenido, reconstruir los desplazamientos de loca después de descartar contornos, respetar los bits de permiso de incrustación en la tabla OS/2 de una fuente, y acertar en todo eso a través de cualesquiera fuentes atípicas que la máquina de un cliente resulte tener instaladas. Microsoft ya resolvió ese problema y distribuye la solución como parte del propio Windows, así que llamar a una DLL del sistema que ella mantiene, prueba contra su propia pila de renderizado de fuentes, y distribuye a cada máquina de forma gratuita le cuesta a HotXLS una carga dinámica y un puntero a función; reimplementar la misma lógica significaría poseer un analizador para un formato binario con décadas de casos límite, para una característica que solo importa cuando una fuente resulta ser grande

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

HotXLS construye la lista de retención para la creación de subconjuntos a partir de un mapa que ya mantenía por otra razón, 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 impulsa el CMap ToUnicode del PDF para que copiar y pegar desde el documento terminado devuelva el texto original en lugar de IDs de glifo crudos. Para cuando los flujos de contenido de página están terminados, ese mapa ya lista exactamente el conjunto de puntos de código Unicode que usó el documento, ni más ni 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 retención que espera CreateFontPackage, un simple arreglo de los puntos de código Unicode a conservar en la forma de 16 bits que requiere el argumento de lista de retención de la API. Como ese argumento es un arreglo de palabras de 16 bits, direcciona el Plano Multilingüe Básico sin complicaciones, que cubre el texto CJK ordinario, cirílico, griego y árabe sin problema; una hoja de cálculo que se apoya en caracteres de planos suplementarios, ciertos emoji o escrituras históricas raras, queda fuera de lo que una sola entrada de lista de retención puede nombrar directamente, lo cual vale la pena conocer como un límite en lugar de un defecto, ya que la gran mayoría de las hojas de cálculo empresariales con mucho Unicode nunca se acercan a ese plano en primer lugar

¿Qué pasa cuando falta fontsub.dll?

HotXLS nunca asume que fontsub.dll está presente, y la exportación 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 de la forma en que 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, cada rama de mantenimiento, o cualquier 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 ruta de fallo se pliega de vuelta al mismo resultado. Una DLL faltante, una exportación faltante, un código de retorno distinto de cero, o una fuente cuya tabla OS/2 prohíbe la creación de subconjuntos mediante sus bits de permiso de incrustación, HotXLS simplemente conserva la fuente completa que ya había incrustado y continúa. 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 de cualquier manera, y la única variable es si termina pequeño o algo más grande

¿Cuánto más pequeño se vuelve realmente el PDF?

La creación de subconjuntos de fuentes TrueType de HotXLS típicamente reduce el PDF exportado de una hoja de cálculo con mucho Unicode a algo entre un veinteavo y un octavo de su tamaño sin subconjunto, una reducción de 8 a 20 veces cuya escala sigue cuánto de una fuente completa realmente toca un documento dado: una orden de compra construida alrededor de 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 aplica 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 pasa el resto de los flujos de contenido del documento, y nada de esto le pide algo extra al código que llama: una hoja de cálculo que nunca sale de WinAnsi nunca toca esta ruta y sigue exportando a través de Helvetica plano, mientras que una hoja de cálculo que sí dispara la ruta de fuente Unicode obtiene la creación de subconjuntos automáticamente, sin propiedad que establecer y sin llamada separada que hacer, y la única propiedad involucrada, UnicodeFontFile, solo elige qué fuente se incrusta y se reduce a subconjunto, no si la creación de subconjuntos ocurre

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