Cuando un PDF no embebe una fuente, el componente HotPDF renderiza ese texto con una fuente de Windows instalada elegida por HPDFMapBaseFontToSystem: decodifica el nombre de /BaseFont, recorta la parte de estilo, prueba varias grafías hasta que GDI confirma que la familia está instalada, mide los widths faltantes del standard 14 desde fuentes métricamente compatibles, y convierte los códigos de un byte a Unicode antes de dibujar. Cada uno de esos pasos existe porque la versión ingenua fallaba con archivos reales. El renderer de páginas RenderLoadedPageToBitmap maneja bien los programas embebidos; esta es la historia de las fuentes que no están en el archivo para nada
¿Por qué GDI dibuja en silencio el typeface equivocado para una fuente no embebida?
GDI jamás reporta una fuente faltante: entréguele a mano a CreateFontIndirect un nombre de cara que no conoce y elige en silencio un sustituto, a menudo una serif distinta sin peso bold. El renderer temprano pasaba el nombre de PDF casi verbatim, así que TimesNewRoman,Bold, TimesNewRomanPS-BoldMT y SegoeUI-Semibold no matcheaban nada y salían en lo que GDI quisiera agarrar. Los nombres pueden ser peores. ISO 32000-1 §7.3.5 deja que un nombre escriba cualquier byte como #xx, y los productores CJK deletrean nombres de fuentes como bytes UTF-8 escapados o de code page legado de costumbre; antes de la v2.766.69 los escapes en sí se volvían el nombre de cara. HPDFMapBaseFontToSystem ahora decodifica primero los escapes, devuelve una secuencia de bytes UTF-8 válida como sus caracteres, y lee cualquier otro byte alto en la code page del sistema
Recortar el estilo es donde las heurísticas muerden. Una coma siempre termina la familia (Arial,Bold da Arial), pero un guion solo lo hace cuando la palabra que sigue es un estilo: Bold, Italic, Oblique, Regular, Roman, Medium, Light, Black, Heavy, Semi, Demi, Thin, Extra, Ultra o Condensed. Esa regla deja a MS-Mincho entero mientras convierte Calibri-Light en Calibri. HotPDF después prueba grafías de la familia, seguidas de grafías de la familia con un sufijo PSMT, MT o PS removido. Desde la v2.768.18 esas grafías cubren toda elección de espacios en los lugares donde puede arrancar una palabra: antes de una mayúscula que sigue a una minúscula (MyriadPro se vuelve Myriad Pro), en la última mayúscula de una tanda a la que sigue una minúscula (UIGothic), y después de un MS inicial (MSPGothic), desde la forma totalmente espaciada hasta el nombre tal como está escrito; pasado el cuarto de esos lugares solo se prueban la forma espaciada completa y el nombre unido. El espaciado no se puede aplicar a ciegas, porque Windows mantiene algunas palabras unidas: SimSun está instalado bajo exactamente esa grafía, mientras que MicrosoftYaHei, MicrosoftJhengHei y MSPGothic pertenecen a Microsoft YaHei, Microsoft JhengHei y MS PGothic. Antes de la v2.768.18 el mapper ponía un espacio antes de cada mayúscula interna, así que MicrosoftYaHei se buscaba como Microsoft Ya Hei y nunca se encontraba. Un candidato cuenta como instalado cuando CreateFontIndirect seguido de GetTextFace devuelve el nombre pedido o, desde la v2.768.18, cuando la tabla name de la fuente seleccionada lo lista como familia, familia completa o familia tipográfica en cualquier idioma; la respuesta se cachea por nombre, así que los documentos con muchas fuentes no instaladas ya no le preguntan a Windows por cada nombre en cada página
Como la función de mapeo es pública en la unidad HPDFRenderFontMetrics, un reporte de preflight puede mostrar con qué familia instalada va a renderizar cada fuente no embebida, usando la enumeración de fuentes que THotPDF ya expone para documentos cargados:
uses
HPDFDoc, HPDFRenderFontMetrics;
procedure ListSystemFontMappings(Pdf: THotPDF; Log: TStrings);
var
Page, I: Integer;
Info: THPDFLoadedFontInfo;
begin
for Page := 0 to Pdf.LoadedPageCount - 1 do
for I := 0 to Pdf.GetLoadedFontCount(Page) - 1 do
if Pdf.GetLoadedFontInfo(Page, I, Info) and not Info.IsEmbedded then
Log.Add(Format('page %d /%s %s -> %s',
[Page + 1, string(Info.ResourceName), string(Info.FontName),
HPDFMapBaseFontToSystem(Info.FontName)]));
end;
¿Cómo mide HotPDF las fuentes standard 14 que no tienen /Widths?
HotPDF mide los advances faltantes sobre la fuente instalada con las mismas métricas, porque ISO 32000-1 §9.6.2.2 permite a las fuentes standard 14 omitir /Widths y la librería no trae tablas AFM. Arial carga las métricas de Helvetica, Times New Roman carga las de Times y Courier New las de Courier, así que HPDFMeasureBaseFontWidths crea la cara que corresponde a lfHeight = -1000 y llama a GetCharWidth32W; a esa altura el resultado ya está en las unidades 1/1000 em que usan los widths de PDF. El renderer primero convierte cada código a Unicode vía /Encoding, /BaseEncoding y /Differences, con StandardEncoding por defecto. El export SVG y la extracción de texto pisaban una trampa más: una fuente Type 1 estándar sin /Encoding alguno producía un decoder sin información de encoding, el export SVG jamás la registraba, y todos los widths medidos iban sin uso. Suministrar la StandardEncoding implícita lo arregló, con la condición de marcarla como encoding predefinido; rutearla por el camino de nombre de CMap decodifica cada código como 0 y cada width la sigue
Bold, italic y un off-by-one en los flags del descriptor de fuente
La entrada /Flags de un descriptor de fuente numera sus bits desde 1, no desde 0, así que ForceBold es el bit 19 ($40000) e Italic es el bit 7 ($40), según la Tabla 123 de ISO 32000-1. El código viejo testeaba $20000, que es el bit 18, SmallCap. El error sobrevivió de la v2.345.0 a la v2.766.53 porque /FontDescriptor es casi siempre una referencia indirecta y el constructor de fuentes solo leía objetos directos, así que la rama entera de flags nunca corría, y la misma ceguera ignoraba /Widths 12 0 R y componía el texto con un advance de reserva de 500 unidades. Cuando la v2.766.53 empezó a resolver referencias indirectas a través del renderer, el bit tuvo que corregirse en el mismo cambio, si no toda cara small-caps se habría puesto bold de repente:
const
// ISO 32000-1 Tabla 123 cuenta las posiciones de bit desde 1
FD_ITALIC = $00040; // bit 7
FD_SMALLCAP = $20000; // bit 18, no es un peso
FD_FORCEBOLD = $40000; // bit 19
procedure ApplyDescriptorFlags(Flags: Integer; var LF: TLogFont);
begin
if (Flags and FD_FORCEBOLD) <> 0 then
LF.lfWeight := FW_BOLD;
if (Flags and FD_ITALIC) <> 0 then
LF.lfItalic := 1;
end;
¿Por qué las fuentes CJK con CMap UCS2 reciben widths equivocados?
Texto CJK con una CMap UCS2 predefinida dibuja los glyphs correctos pero el espaciado equivocado cuando un renderer trata el código como el CID, porque /W se indexa por CID, no por código de carácter. Con STSong-Light y UniGB-UCS2-H, el código casualmente iguala al valor Unicode, así que GDI dibuja los caracteres correctos y el bug se esconde en los advances: las letras minúsculas llegan como códigos 97 para arriba, quedan fuera de una entrada de /W como [1 95 500], y todas reciben el width default /DW de 1000. Desde la v2.766.56 el renderer de HotPDF lee los códigos a través de los rangos codespace de la CMap (ISO 32000-1 §9.7.6.2) y los mapea a CIDs antes de buscar widths. Solo se usan las tablas UCS2 y UTF16 integradas y los streams de CMap embebidos; una aproximación identidad para algo como GBK-EUC-H solo aparentaría soporte mientras produce salida equivocada, así que el renderer no finge
¿Por qué los caracteres acentuados se vuelven signos de interrogación en Windows chino?
Los códigos de un byte jamás deben llegar a las funciones GDI ANSI («A»), porque GetGlyphOutlineA y GetGlyphIndicesA interpretan los bytes en la code page del sistema mientras que TextOutA usa el character set de la fuente seleccionada. En un sistema chino, el byte $A9 de Arial (el signo de copyright en Windows-1252) se volvía un byte líder de GBK y se renderizaba como «?», una trampa en la que se metió de cabeza el camino de outlines sin hinting agregado en la v2.766.83. La v2.767.3 le pregunta a la fuente realizada por su character set con GetTextCharset, lo convierte a code page vía TranslateCharsetInfo, pasa el byte por MultiByteToWideChar y llama a las funciones W; las fuentes symbol usan U+F000 más el código en su lugar. Los encodings que discrepan de Windows-1252 — /Differences, StandardEncoding, MacRomanEncoding — se mapean a Unicode antes de que ninguna fuente del sistema los vea
¿Cuáles son los límites de dibujar con fuentes del sistema?
El renderizado con fuentes del sistema es una aproximación, y el componente HotPDF es honesto sobre dónde se detiene. Antes de la v2.768.18 el chequeo de instalación comparaba solo el nombre que devuelve GetTextFace, y en un Windows localizado esa función reporta el nombre de familia en el idioma del sistema, así que Microsoft YaHei en Windows chino o Yu Mincho en Windows japonés se juzgaban ausentes y se dibujaban con un sustituto de GDI; desde la v2.768.18 una cara que vuelve bajo otro nombre también se busca en la tabla name de la fuente, y esas fuentes se encuentran. La compatibilidad métrica está garantizada solo para las familias Helvetica, Times y Courier; Symbol se mapea a Symbol y ZapfDingbats a Wingdings, que es un parche de emergencia antes que un match. El preflight de arriba además solo ve las fuentes del diccionario /Resources de cada página, no las referenciadas desde dentro de form XObjects. Cuando un código todavía no se puede dibujar, el rastreo de glyphs sin resolver al momento de dibujar lo reporta, que es una señal mejor que mirar miniaturas a ojo
El fix durable queda del lado de la autoría. HotPDF mismo escribe con FontEmbedding en True por defecto, sustituyendo un Arial embebido incluso cuando el código llama a SetFont con Helvetica, y el texto embebido pasa por el renderer de glyphs de fuentes embebidas en lugar de cualquiera de las adivinanzas de arriba. Un guard barato para archivos entrantes es avisar antes de renderizar cuando una familia mapeada no está en la lista de fuentes de pantalla:
// VCL: Screen.Fonts lista los nombres de familia instalados (unidad Forms)
function MissingSystemFamily(const BaseFont: AnsiString): Boolean;
begin
Result := Screen.Fonts.IndexOf(HPDFMapBaseFontToSystem(BaseFont)) < 0;
end;
Para el componente completo, incluidos renderizado de páginas, extracción de texto y font subsetting del lado de escritura, vea la página de producto de HotPDF Delphi PDF component