Artículo técnico

Renderizar glifos compuestos y acentos CFF seac en PDF

HotPDF renderiza letras con acento de fuentes incrustadas correctamente al manejar dos mecanismos de fuentes que construyen un glifo a partir de otros: los glifos compuestos TrueType, cuyos offsets de componente deben aplicarse en unidades de glifo antes de la matriz glifo-a-dispositivo, y la forma seac de endchar en CFF, que une una letra base y un acento mediante códigos StandardEncoding. Equivóquese con cualquiera de los dos y é, Å, ü o una comilla doble curva salen sin una parte, mientras las letras simples alrededor se ven perfectas

Ese último detalle es lo que hace caros estos bugs. Un reporte renderiza, el cuerpo del texto está bien, y solo "München" tiene un hueco, o una cita abre con comilla simple y un espacio. Nadie sospecha del renderizador. La primera teoría siempre es una fuente dañada o un productor malo, y el ticket da vueltas durante una semana. Este artículo recorre qué falla realmente en cada mecanismo, cómo lo maneja el componente HotPDF Delphi desde v2.768.10 y v2.768.11, y el arreglo relacionado de subsetting en v2.770.83 que mantiene intactos los glifos compuestos cuando HotPDF escribe fuentes en lugar de leerlas

¿Por qué solo el acento desaparece de un glifo PDF?

El acento desaparece porque es la única parte del glifo que carga un offset 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 hacia arriba y a la derecha". La letra base casi siempre se sienta en el offset (0, 0), así que cualquier error en cómo se aplican los offsets la deja intacta. El acento es el componente que se mueve, así que es el componente que aterriza en el lugar equivocado o que nunca llega a dibujarse

El patrón del síntoma es un diagnóstico por sí mismo. Si las letras base de los caracteres acentuados renderizan y los diacríticos no, o si la puntuación emparejada como “ y ” se muestra como una sola marca en lugar de dos, deje de mirar el archivo de la fuente. Una fuente rota usualmente rompe glifos enteros, y se los rompe a todos los visores. Partes faltantes de glifos compuestos en un renderizador, mientras Acrobat muestra la página bien, apuntan al código que ensambla los componentes

¿Cómo colocan sus componentes los glifos compuestos TrueType?

Un glifo compuesto TrueType coloca cada componente con su propia transform afín pequeña, expresada en unidades de fuente, y esa transform tiene que actuar sobre el contorno del componente antes de que lo haga la matriz glifo-a-dispositivo. En la tabla glyf de OpenType, un glifo compuesto tiene un conteo de contornos negativo, seguido de una lista de registros de componente. Cada registro carga 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 ARGS_ARE_XY_VALUES (0x0002) está activo, los dos argumentos son un offset dx/dy en unidades de fuente. MORE_COMPONENTS (0x0020) dice que sigue otro registro

Así que para cada componente usted arma una matriz de componente CM a partir de la parte de escala y el offset, y luego la combina con M, la matriz que mapea unidades de fuente a píxeles de dispositivo. El orden de esa combinación es todo el bug. La multiplicación de matrices interna de HotPDF usa la convención de vectores columna, así que el producto de A y B aplica B primero y luego A. La composición correcta aplica CM primero, en espacio de glifo, y M después. Escrito al revés, M corre primero y el offset de CM se suma al final, ya en píxeles de dispositivo y sin escalar nunca

// Solo ilustración: un record afín 2D con la convención de vector 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 chica, y volteada)
// CM : transform del componente; e/f guardan dx/dy en unidades de fuente
Sub := Mul(M, CM);   // correcto: el offset se escala hacia abajo con el contorno
// Sub := Mul(CM, M); // incorrecto: dx/dy aterrizan como píxeles de dispositivo crudos

Los números hacen obvia la falla en cuanto los ve. Arial usa un em de 2048 unidades, y el componente del acento de é carga su offset en esas unidades de fuente. Con el orden correcto ese offset se encoge junto con el contorno hasta unos pocos píxeles al zoom normal. Con el orden invertido el valor crudo en unidades de fuente se suma como píxeles de dispositivo, y en el render de prueba de HotPDF el acento del é de Arial terminó a 243 píxeles de su letra, muy fuera del glifo y usualmente 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 renderizaba como un ‘ solitario seguido de espacio vacío

La parte lineal rara vez expone el bug. Los componentes de acento usualmente no cargan escala alguna, y una escala uniforme conmuta con M de todos modos, así que solo la traslación revela el producto invertido. Por eso también una prueba rápida con texto latino simple pasa: la inversión había estado en el código a través de muchas releases antes de que v2.768.10 la corrigiera, y solo los componentes con offsets la mostraban

Recursión y ciclos

Los glifos compuestos pueden referenciar otros glifos compuestos, así que un renderizador los recorre recursivamente y debe defenderse. HotPDF limita la profundidad de anidamiento a 5 niveles y el conteo total de componentes por glifo a 1024, omite un componente que nombra su propio índice de glifo, y trata el modo de point-matching poco usado (argumentos que son números de punto en lugar de offsets) como un offset 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 deprecada de que una charstring CFF Type 2 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 la 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 todavía lo usan mucho para letras latinas acentuadas

Tres detalles hacen tropezar a los implementadores. Primero, bchar y achar no son glyph IDs. Son códigos en Adobe StandardEncoding, así que el renderizador mapea cada código a un nombre de glifo y luego busca ese nombre a través del charset de la fuente para hallar el GID. Segundo, Type 2 no tiene argumento asb, a diferencia de 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 de claves CID no pueden usar seac en absoluto, porque no hay charset de nombres de glifo por donde resolver los códigos StandardEncoding

Luego está el ancho. Cada 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 carga un ancho; un endchar seac con cinco argumentos carga un ancho seguido de los cuatro valores seac. La regla es contar los argumentos contra el conteo fijo esperado, que es cuatro para seac. Antes de v2.768.11, HotPDF trataba cualquier endchar como "tome el ancho y deténgase", así que los glifos seac producían un contorno vacío: ni base ni 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.
// Stack carga los operandos que preceden a endchar; Count es cuántos.
if (Count >= 4) and not IsCIDFont then
begin
  // 5 operandos: el primero es el ancho de avance, el resto son seac
  if Count = 5 then
    TakeWidthAndShift;                 // tire el ancho, conserve 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;
// de lo contrario: endchar ordinario, ancho opcional, cierre 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 omite en lugar de fallar todo el glifo, que es el comportamiento indulgente que usted quiere cuando un subset fue recortado un poco demasiado agresivamente

Verificar el arreglo desde su propio código Delphi

La forma más rápida de verificar el manejo de glifos compuestos y seac es renderizar una página con texto acentuado de fuentes incrustadas a través de ambas rutas de salida públicas: THotPDF.RenderLoadedPageToBitmap para salida raster y THotPDF.BuildLoadedPageSVG con EmbedFonts en True para salida vector. 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 un todo está cubierto en cómo HotPDF renderiza glifos TrueType, CFF y Type 3 incrustados, 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áciles de detectar los acentos mal puestos
    Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
    if Bmp <> nil then
    try
      Bmp.SaveToFile('accent-probe.bmp');
    finally
      Bmp.Free;
    end;

    // Ruta vector: EmbedFonts = True escribe contornos de glifos, así que el
    // ensamblado composite y seac es visible en los paths del 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;

Elija los documentos de prueba con intención. Usted quiere un PDF con un subset TrueType incrustado que contenga é, Å y comillas dobles curvas, y uno con un subset CFF incrustado (FontFile3, Type1C) producido desde una fuente Type 1, que es donde seac todavía vive, típicamente en salida de tipografía antigua y algunos flujos de impresión. El texto compuesto en fuentes que el productor no incrustó no le dice nada aquí, porque esos glifos vienen de las fuentes Windows instaladas y no del programa de fuente incrustado descrito en ISO 32000-1 §9.9

¿Por qué se rompían los glifos compuestos durante el subsetting de fuentes?

Los glifos compuestos se rompían durante el subsetting porque el subsetter recorría los registros de componente con el stride equivocado cada vez que un componente cargaba una escala o una transform 2x2, y luego leía mal el índice de glifo del siguiente componente. HotPDF lo corrigió en 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 compuesto referencia y renumerar esas referencias a los nuevos, compactos glyph IDs

El viejo walker 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 transform que los flags de escala agregan. Después de un componente escalado, el siguiente "índice de glifo" que leía eran realmente bytes de transform, así que el componente real podía quedarse fuera del subset, y el paso de renumeración podía escribir un glyph ID nuevo directo sobre la transform. El resultado era una letra acentuada que renderizaba en algunos visores con el acento equivocado, un acento distorsionado o ninguno, y solo para fuentes que escalan sus componentes, que es por lo que la mayoría de las pruebas nunca lo atraparon

Dos problemas más se sentaban junto al bug del stride. Renumerar un índice de glifo no es idempotente, y los compuestos anidados se estaban renumerando dos veces: una por la pasada sobre cada compuesto marcado y otra por un descenso recursivo dentro de él. HotPDF ahora renumera cada compuesto exactamente una vez y maneja el paso de "qué glifos necesita este compuesto" desde una cola de trabajo, así que un ciclo de compuestos que se referencian entre sí termina, los índices de glifo más allá del conteo de glifos de maxp se rechazan, y los glifos vacíos nunca se parsean como compuestos

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 comillas curvas son todos glifos compuestos en Arial
    Pdf.CurrentPage.UnicodeTextOut(50, 720, 0,
      'Café, Ångström, München, “quoted”');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Haga un round-trip de ese archivo a través de la sonda de arriba y de un segundo visor. Desde v2.770.83 cada componente de cada glifo compuesto sobrevive al subsetting, incluidos los componentes detrás de una escala o transform 2x2, así que los acentos renderizan igual en HotPDF, Acrobat y los visores de navegador

Lo que el manejo de glifos compuestos no cubre

El arreglo de glifos compuestos cubre la colocación por offsets, que es lo que usan casi todas las fuentes reales, y aproxima el resto de forma conservadora. Los compuestos TrueType de point-matching, donde los dos argumentos nombran puntos a alinear en lugar de dar un offset, se dibujan con offset cero. El seac en fuentes CFF de claves CID se ignora a propósito porque la especificación lo prohíbe ahí. Un seac cuyo código de base o de acento no tiene glifo en un subset recortado dibuja el componente que pueda encontrar. Ninguno de estos casos crashea el renderizador, pero si usted produce PDFs para uso de archivo o PDF/A, el movimiento más seguro es incrustar fuentes que usen compuestos por offset y charstrings Type 2 sin seac, para que cada consumidor, no solo un renderizador, vea las mismas letras

Preguntas frecuentes

¿Por qué las letras acentuadas renderizan sin sus acentos en mi renderizador PDF?

Las letras acentuadas suelen ser glifos compuestos o glifos CFF seac, y el acento es el único componente con offset. Un renderizador que aplica el offset del componente después de la matriz de dispositivo, o que ignora seac en endchar, tira o malubica el acento mientras la letra base renderiza normal

¿HotPDF soporta la forma seac de endchar en CFF?

Sí. Desde v2.768.11 HotPDF dibuja endchar con cuatro argumentos, o cinco incluido el ancho, como un glifo base más un acento desplazado a (adx, ady), resolviendo ambos a través de nombres StandardEncoding y el charset de la fuente, tanto para render bitmap como para export SVG con fuentes incrustadas

¿Se preservan los glifos compuestos cuando HotPDF hace subsetting de fuentes TrueType?

Sí. Desde v2.770.83 HotPDF avanza sobre las transforms de componente de 2, 4 u 8 bytes correctamente, conserva cada componente referenciado en el subset, renumera los compuestos anidados una vez y se detiene de forma segura ante referencias de glifo cíclicas o fuera de rango

El render de glifos compuestos, el soporte CFF seac 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