HotPDF renderiza correctamente las letras con acento de fuentes incrustadas manejando dos mecanismos de fuentes que construyen un glifo a partir de otros: los composite glyphs de TrueType, cuyos desplazamientos de componente deben aplicarse en unidades de glifo antes de la matriz glifo-a-dispositivo, y la forma seac de endchar en CFF, que cose una letra base y un acento mediante códigos StandardEncoding. Equivócate con cualquiera de los dos y é, Å, ü o una comilla doble curva salen sin una parte, mientras las letras simples de alrededor se ven perfectas
Ese último detalle es lo que hace caros estos bugs. Un informe se renderiza, el cuerpo del texto está bien, y solo "München" tiene un agujero, o una cita abre con comilla simple y un hueco. Nadie sospecha del renderer. La primera teoría siempre es una fuente dañada o un productor defectuoso, y el ticket da vueltas durante una semana. Este artículo recorre qué falla de verdad en cada mecanismo, cómo lo maneja el componente Delphi de HotPDF desde v2.768.10 y v2.768.11, y el arreglo de subsetting relacionado de la v2.770.83 que mantiene íntegros los composite glyphs cuando HotPDF escribe fuentes en lugar de leerlas
¿Por qué de un glifo de PDF desaparece solo el acento?
El acento desaparece porque es la única parte del glifo que lleva un desplazamiento distinto de cero. Tanto las fuentes TrueType como las CFF ahorran espacio reutilizando contornos: é no se dibuja como una forma nueva sino como "el glifo e, más el glifo acute desplazado arriba y a la derecha". La letra base casi siempre se queda en el desplazamiento (0, 0), así que cualquier error en cómo se aplican los desplazamientos la deja intacta. El acento es el componente que se mueve, así que es el componente que cae en el sitio equivocado o que no llega a dibujarse nunca
El patrón del síntoma es un diagnóstico en sí mismo. Si las letras base de los caracteres acentuados se renderizan y los diacríticos no, o si la puntuación emparejada como “ y ” aparece como una sola marca en lugar de dos, deja de mirar el archivo de fuente. Una fuente rota suele romper glifos enteros, y se los rompe a todos los visores. Piezas ausentes de glifos compuestos en un renderer, mientras Acrobat muestra la página bien, apuntan al código que ensambla los componentes
¿Cómo colocan sus componentes los composite glyphs de TrueType?
Un composite glyph de TrueType coloca cada componente con su propia pequeña transformación afín, expresada en unidades de fuente, y esa transformación debe actuar sobre el contorno del componente antes de que lo haga la matriz glifo-a-dispositivo. En la tabla glyf de OpenType, un composite glyph lleva un contador de contornos negativo, seguido de una lista de registros de componente. Cada registro guarda una palabra de flags, el índice del glifo componente, dos argumentos que son bytes o words (ARG_1_AND_2_ARE_WORDS, 0x0001) y una escala opcional: un valor F2Dot14 para WE_HAVE_A_SCALE (0x0008), dos para WE_HAVE_AN_X_AND_Y_SCALE (0x0040), o una matriz 2x2 completa para WE_HAVE_A_TWO_BY_TWO (0x0080). Cuando está activo ARGS_ARE_XY_VALUES (0x0002), los dos argumentos son un desplazamiento dx/dy en unidades de fuente. MORE_COMPONENTS (0x0020) dice que sigue otro registro
Así que para cada componente construyes una matriz de componente CM a partir de la parte de escala y del desplazamiento, y la combinas con M, la matriz que mapea unidades de fuente a píxeles de dispositivo. El orden de esa combinación es todo el bug. El producto de matrices interno de HotPDF usa la convención de vectores columna, así que el producto de A y B aplica primero B y luego A. La composición correcta aplica CM primero, en espacio de glifo, y M después. Escrita al revés, M corre primero y el desplazamiento de CM se añade después, ya en píxeles de dispositivo y sin escalar nunca
// Solo ilustración: un record afín 2D con la convención de vectores columna,
// donde Mul(A, B) mapea un punto primero por B y luego por A
type
TAffine = record
a, b, c, d, e, f: Double;
end;
function Mul(const A, B: TAffine): TAffine;
begin
Result.a := A.a * B.a + A.c * B.b;
Result.b := A.b * B.a + A.d * B.b;
Result.c := A.a * B.c + A.c * B.d;
Result.d := A.b * B.c + A.d * B.d;
Result.e := A.a * B.e + A.c * B.f + A.e;
Result.f := A.b * B.e + A.d * B.f + A.f;
end;
// M : unidades de fuente -> píxeles de dispositivo (escala mínima, y invertida)
// CM : transformación del componente; e/f guardan dx/dy en unidades de fuente
Sub := Mul(M, CM); // correcto: el offset se escala con el contorno
// Sub := Mul(CM, M); // mal: dx/dy caen como píxeles de dispositivo en bruto
Los números hacen obvio el fallo en cuanto los ves. Arial usa un em de 2048 unidades, y el componente del acento de é lleva su desplazamiento en esas unidades de fuente. Con el orden correcto ese desplazamiento se encoge junto con el contorno hasta unos pocos píxeles al zoom normal. Con el orden invertido el valor en unidades de fuente se añade como píxeles de dispositivo, y en el render de pruebas de HotPDF el acento del é de Arial acabó a 243 píxeles de su letra, muy fuera del glifo y normalmente fuera de cualquier cosa visible. La comilla doble izquierda de Arial son dos copias de la forma de comilla simple, la segunda desplazada, así que se renderizaba como una ‘ solitaria seguida de espacio vacío
La parte lineal raramente expone el bug. Los componentes de acento normalmente no llevan escala ninguna, y una escala uniforme conmuta con M de todos modos, así que solo la traslación revela el producto invertido. Por eso también un test rápido con texto latino simple pasa: la inversión llevaba en el código muchas releases antes de que v2.768.10 la corrigiera, y solo los componentes con desplazamiento la mostraban nunca
Recursión y ciclos
Los composite glyphs pueden referenciar otros composite glyphs, así que un renderer los recorre recursivamente y debe defenderse. HotPDF limita la profundidad de anidamiento a 5 niveles y el total de componentes por glifo a 1024, se salta un componente que nombra su propio índice de glifo y trata el modo de emparejamiento por puntos apenas usado (argumentos que son números de punto en lugar de desplazamientos) como un desplazamiento cero. Una fuente hostil o corrupta puede por tanto perder un componente, pero no puede colgar el render de la página
¿Qué es la forma seac de endchar en CFF?
La forma seac de endchar es una forma obsoleta de que una charstring Type 2 de CFF diga "este glifo es una letra base más un acento": cuando endchar tiene cuatro argumentos, son adx ady bchar achar. La Adobe Technical Note #5177 lo describe en su apéndice sobre compatibilidad con Type 1. El nombre viene del operador seac de Type 1 (standard encoding accented character), y los subsets CFF convertidos de fuentes Type 1 aún lo usan intensivamente para letras latinas acentuadas
Tres detalles hacen tropezar a los implementadores. Primero, bchar y achar no son glyph IDs. Son códigos de Adobe StandardEncoding, así que el renderer mapea cada código a un nombre de glifo y luego busca ese nombre a través del charset de la fuente para encontrar el GID. Segundo, Type 2 no tiene argumento asb, al contrario que el seac de Type 1, que toma asb adx ady bchar achar. En Type 2 el origen del acento va directamente a (adx, ady) relativo al origen del glifo base, sin corrección de side bearing. Tercero, las fuentes CFF con claves CID no pueden usar seac en absoluto, porque no hay charset de nombres de glifo por el que resolver los códigos StandardEncoding
Luego está el ancho. Toda charstring Type 2 puede arrancar su primer operador de limpieza de pila con un argumento opcional de ancho de avance, y endchar es uno de esos operadores. Un endchar simple con un argumento lleva un ancho; un endchar seac con cinco argumentos lleva un ancho seguido de los cuatro valores seac. La regla es contar argumentos contra la cuenta fija esperada, que para seac son cuatro. Antes de v2.768.11, HotPDF trataba cualquier endchar como "toma el ancho y para", así que los glifos seac producían un contorno vacío: sin base, sin acento, solo un hueco del tamaño correcto en la línea
// Ilustración de la decisión de endchar en un intérprete Type 2.
// La pila guarda los operandos que preceden a endchar; Count es cuántos.
if (Count >= 4) and not IsCIDFont then
begin
// 5 operandos: el primero es el advance width, el resto son seac
if Count = 5 then
TakeWidthAndShift; // tira el ancho, conserva 4 operandos
if StackDepth = 4 then
begin
BaseGID := GIDForName(StdEncName(Round(Stack[2]))); // bchar
AccentGID := GIDForName(StdEncName(Round(Stack[3]))); // achar
DrawGlyph(BaseGID, Identity);
DrawGlyph(AccentGID, Translate(Stack[0], Stack[1])); // adx, ady
Exit;
end;
end;
// si no: endchar ordinario, ancho opcional, cierra el path
En el intérprete real ambos glifos componente se ejecutan con el mismo motor de charstrings, la matriz del acento es la matriz de glifo actual compuesta con una traslación a (adx, ady), y la matriz se restaura después para que los glifos posteriores de la línea no se vean afectados. Si cualquiera de los dos nombres no resuelve a un GID, ese componente se salta en lugar de hacer fallar el glifo entero, que es el comportamiento indulgente que quieres cuando un subset se ha recortado un poco demasiado agresivo
Comprobar el arreglo desde tu propio código Delphi
La forma más rápida de verificar el manejo de composite glyphs y seac es renderizar una página con texto acentuado de fuentes incrustadas por ambas rutas de salida públicas: THotPDF.RenderLoadedPageToBitmap para salida raster y THotPDF.BuildLoadedPageSVG con EmbedFonts a True para salida vectorial. Ambas comparten el mismo código de contornos de glifo, así que ambas recibieron los arreglos de v2.768.10 y v2.768.11. El pipeline como conjunto está cubierto en cómo HotPDF renderiza glifos incrustados TrueType, CFF y Type 3, y las opciones de bitmap en renderizar páginas PDF a bitmaps en Delphi
uses
System.Classes, Vcl.Graphics, HPDFDoc;
procedure RenderAccentProbe(const FileName: string);
var
Pdf: THotPDF;
Bmp: TBitmap;
Svg: AnsiString;
Lines: TStringList;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile(FileName) <= 0 then
Exit;
// Ruta raster: 144 DPI hace fácil ver los acentos mal colocados
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
if Bmp <> nil then
try
Bmp.SaveToFile('accent-probe.bmp');
finally
Bmp.Free;
end;
// Ruta vectorial: EmbedFonts = True escribe contornos de glifo, así que
// el ensamblado composite y seac se ve en los paths SVG mismos
Svg := Pdf.BuildLoadedPageSVG(0, True);
if Svg <> '' then
begin
Lines := TStringList.Create;
try
Lines.Text := string(Svg);
Lines.SaveToFile('accent-probe.svg');
finally
Lines.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Elige los documentos de prueba con intención. Quieres un PDF con un subset TrueType incrustado que contenga é, Å y comillas dobles curvas, y otro con un subset CFF incrustado (FontFile3, Type1C) producido desde una fuente Type 1, que es donde el seac todavía vive, típicamente en salida de composición antigua y algunos flujos de imprenta. El texto compuesto con fuentes que el productor no incrustó no te dice nada aquí, porque esos glifos vienen de fuentes Windows instaladas y no del programa de fuente incrustado que describe ISO 32000-1 §9.9
¿Por qué se rompían los composite glyphs durante el subsetting de fuentes?
Los composite glyphs se rompían durante el subsetting porque el subsetter recorría los registros de componente con el paso equivocado siempre que un componente llevaba escala o transformación 2x2, y luego leía mal el índice de glifo del siguiente componente. HotPDF lo arregló en la v2.770.83. El subsetting es el gemelo del lado escritura del bug de render: cuando HotPDF incrusta un subset de una fuente TrueType instalada, tiene que conservar cada glifo que un composite referencie y renumerar esas referencias a los nuevos IDs de glifo compactos
El antiguo recorrido solo miraba el flag de tamaño de argumentos y el flag de más-componentes, así que avanzaba 6 u 8 bytes por registro. Ignoraba los 2, 4 u 8 bytes de datos de transformación que añaden los flags de escala. Tras un componente escalado, el siguiente "índice de glifo" que leía eran de verdad bytes de transformación, así que el componente real podía quedarse fuera del subset, y el paso de renumeración podía escribir un nuevo ID de glifo dentro de la transformación. El resultado era una letra acentuada que se renderizaba en algunos visores con el acento equivocado, un acento distorsionado o ninguno, y solo en fuentes que escalan sus componentes, que es por lo que la mayoría de los tests nunca lo cazaron
Dos problemas más se sentaban junto al bug del paso. Renumerar un índice de glifo no es idempotente, y los composites anidados se estaban renumerando dos veces: una por la pasada sobre cada composite marcado y otra por un descenso recursivo dentro de él. HotPDF ahora renumera cada composite exactamente una vez y gobierna el paso de "qué glifos necesita este composite" desde una cola de trabajo, así que un ciclo de composites que se referencian entre sí termina, los índices de glifo más allá del recuento de glifos de maxp se rechazan y los glifos vacíos nunca se parsean como composites
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'accents-subset.pdf';
Pdf.FontEmbedding := True; // incrusta los glifos que se usan
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 14);
// é, Å, ü y las comillas curvas son todos composite glyphs en Arial
Pdf.CurrentPage.UnicodeTextOut(50, 720, 0,
'Café, Ångström, München, “quoted”');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Pasa ese archivo por el round-trip de la sonda de arriba y por un segundo visor. Desde la v2.770.83, cada componente de cada composite glyph sobrevive al subsetting, incluidos los componentes detrás de una escala o transformación 2x2, así que los acentos se renderizan igual en HotPDF, Acrobat y los visores de navegador
Lo que el manejo de composite glyphs no cubre
El arreglo de composite glyphs cubre la colocación por desplazamiento, que es la que usan casi todas las fuentes reales, y aproxima el resto de forma conservadora. Los composites TrueType de emparejamiento por puntos, donde los dos argumentos nombran puntos que alinear en lugar de dar un desplazamiento, se dibujan con desplazamiento cero. El seac en fuentes CFF con claves CID se ignora por diseño porque la especificación lo prohíbe ahí. Un seac cuyo código base o de acento no tiene glifo en un subset recortado dibuja el componente que pueda encontrar. Ninguno de estos casos revienta el renderer, pero si produces PDF para archivado o uso PDF/A, el movimiento más seguro es incrustar fuentes que usen composites por desplazamiento y charstrings Type 2 sin seac, para que cada consumidor, no solo un renderer, vea las mismas letras
FAQ
¿Por qué las letras acentuadas se renderizan sin sus acentos en mi renderer de PDF?
Las letras acentuadas suelen ser composite glyphs o glifos seac de CFF, y el acento es el único componente con desplazamiento. Un renderer que aplica el desplazamiento del componente después de la matriz de dispositivo, o que ignora el seac en endchar, pierde o coloca mal el acento mientras la letra base se renderiza normal
¿Soporta HotPDF la forma seac de endchar en CFF?
Sí. Desde la v2.768.11, HotPDF dibuja un endchar con cuatro argumentos, o cinco contando el ancho, como un glifo base más un acento desplazado a (adx, ady), resolviendo ambos por nombres StandardEncoding y el charset de la fuente, tanto en render bitmap como en exportación SVG con fuentes incrustadas
¿Se conservan los composite glyphs cuando HotPDF hace subsetting de fuentes TrueType?
Sí. Desde la v2.770.83, HotPDF avanza correctamente sobre transformaciones de componente de 2, 4 u 8 bytes, conserva cada componente referenciado en el subset, renumera los composites anidados una vez y se para con seguridad ante referencias de glifo cíclicas o fuera de rango
El render de composite glyphs, el soporte seac de CFF y el subsetting TrueType seguro vienen como parte del HotPDF Delphi PDF Component, una biblioteca VCL nativa para Delphi y C++Builder que crea, edita, extrae y renderiza documentos PDF sin DLL externas