Artículo técnico

Shaping de texto conectable: Uniscribe y HarfBuzz en Delphi

El shaping de texto en el componente PDFium pasa por un objeto instalable. ConfigureTextShaper instala el shaper por el que se enruta cada punto de entrada de shaping, reemplazando y liberando lo que hubiera; ActiveTextShaper devuelve el instalado y crea el predeterminado de la plataforma en el primer uso; ActiveTextShaperName reporta qué backend está vivo; ClearTextShaper suelta la instalación y deja que el predeterminado se vuelva a crear. En Windows el predeterminado es TPdfUniscribeTextShaper. Bajo Free Pascal existe TPdfHarfBuzzTextShaper, que enlaza libharfbuzz en tiempo de ejecución, de modo que una biblioteca ausente es una condición reportada y no un fallo de carga

Arquitectura de shaping de texto conectable en el componente PDFium para Delphi: ConfigureTextShaper, ActiveTextShaper y ClearTextShaper gestionan un único backend instalado, Uniscribe en Windows y un HarfBuzz enlazado en runtime bajo Free Pascal
Cada llamada de shaping se enruta por el único objeto shaper instalado, con un predeterminado de plataforma en cada destino

Una interfaz, dos backends que reparten el trabajo de manera completamente distinta. Entender esa asimetría es lo que evita que la vía portable produzca texto bien moldeado y mal posicionado

Por qué el backend de Windows es una clase y el portable tres piezas

Porque Uniscribe son cuatro APIs fingiendo ser una. ScriptItemize segmenta una cadena por escritura y resuelve niveles bidireccionales; ScriptShape mapea caracteres a glifos; ScriptPlace calcula avances y desplazamientos; ScriptLayout pone los runs resultantes en orden visual. Un backend construido sobre ella no tiene nada que añadir, por eso el shaper de Windows es una sola clase con un solo método

HarfBuzz cubre las dos del medio. Moldea y posiciona un run cuya dirección y escritura ya decidió quien llama, y no tiene opinión sobre cómo se divide un párrafo en runs ni en qué orden aparecen esos runs. Así que el backend portable aporta el resto: el algoritmo bidireccional resuelve los niveles de incrustación, las funciones Unicode de HarfBuzz segmentan el texto por escritura, y los runs se disponen en el orden visual que produce la regla L2 de UAX #9. La mitad bidireccional es lo bastante sustancial como para ser su propia unidad, descrita en el artículo de niveles de incrustación UAX #9

Comparación del pipeline de shaping para texto PDF: Uniscribe suministra ScriptItemize, ScriptShape, ScriptPlace y ScriptLayout dentro de una sola clase, mientras que HarfBuzz cubre solo moldeado y posicionamiento alrededor de las etapas UAX #9 propias del componente
Uniscribe cubre las cuatro etapas; la vía portable debe aportar la itemización y el orden visual por sí misma

El shaper no resuelve fuentes, y eso es deliberado

Uniscribe lee el binario de la fuente de un contexto de dispositivo GDI. No hay equivalente portable de eso, e inventarlo dentro de una unidad de shaping significaría decidir, en nombre de cada aplicación, si las fuentes vienen de fontconfig, de CoreText, de una carpeta de fuentes de la aplicación o de una base de datos. Así que el backend de HarfBuzz recibe un resolvedor: una callback que mapea un nombre de fuente a los bytes TrueType u OpenType. Devolver False hace fallar la solicitud de shaping igual que una fuente GDI ilegible la hace fallar en Windows

uses
  FPdfTextShaping
{$IFDEF FPC}
  , FPdfTextShapingHb
{$ENDIF}
  ;

function TFontCatalogue.Resolve(const FontName: WideString;
  out FontData: TBytes): Boolean;
var
  Path: string;
begin
  // Su política: fontconfig, CoreText, una carpeta de fuentes de la app, una base de datos
  Result := FLookup.TryGetValue(LowerCase(FontName), Path);
  if Result then
    FontData := TFile.ReadAllBytes(Path);
end;

procedure InstallShaper(Catalogue: TFontCatalogue);
begin
{$IFDEF FPC}
  // La propiedad pasa a la unidad; llamar una vez durante el arranque,
  // antes de que algo moldee texto
  ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
  // En Delphi el predeterminado de plataforma (Uniscribe) se crea a demanda,
  // así que no hace falta instalación alguna
  LogInfo('shaping backend: ' + ActiveTextShaperName);
end;

Mantener el descubrimiento de fuentes fuera del shaper tiene un segundo beneficio que aparece en servidores: el mismo proceso puede moldear con un conjunto de fuentes incrustadas que no tiene nada que ver con lo instalado en la máquina, que es lo que se quiere cuando la salida debe ser reproducible byte a byte entre hosts. El componente también expone un proveedor de fuentes del sistema anfitrión para los casos donde sí quieren fuentes instaladas, cubierto en el artículo del proveedor de fuentes del sistema

El record de resultado es neutral al backend, y los clusters son la razón

Ambos backends llenan el mismo TPdfShapedText: el texto de origen, el nombre de la fuente, el tamaño, los bytes de la fuente, un arreglo de runs, el ancho total, el recuento de glifos y el recuento de caracteres lógicos. Cada TPdfShapedRun lleva su tramo en el texto de origen, su posición X visual, su ancho, su nivel bidireccional y una bandera de derecha a izquierda, más sus glifos. Cada TPdfShapedGlyph lleva un identificador de glifo, un avance, desplazamientos X e Y, y el cluster al que pertenece como un inicio y una longitud en el texto de origen

Esos campos de cluster son lo que vuelven el record utilizable y no meramente informativo. El shaping no es un mapeo uno a uno: una sílaba devanagari se vuelve un glifo a partir de cuatro caracteres, una ligadura árabe fusiona dos, y un solo carácter puede producir varias marcas. Sin tramos de cluster no pueden colocar un caret, hacer hit-test de un clic ni resaltar una selección, porque no pueden decir a qué caracteres pertenece un glifo. Con ellos, la aritmética es local y el mismo código funciona en ambos backends

Tramos de cluster de glifos en TPdfShapedText: un glifo de sílaba devanagari a partir de cuatro caracteres, una ligadura árabe de dos, y una base más marca de un carácter, cada uno mapeado de vuelta por ClusterStart y ClusterLength
Los tramos de cluster mapean cada glifo de vuelta a sus caracteres de origen para que carets, hit tests y selecciones funcionen
var
  Shaped: TPdfShapedText;
  R, G: Integer;
begin
  if ShapePdfText(Line, 'Noto Sans Arabic', 14, ptdAuto, Shaped) then
    for R := 0 to High(Shaped.Runs) do
    begin
      // Los runs ya llegan en orden visual con VisualX lleno
      X := Shaped.Runs[R].VisualX;
      for G := 0 to High(Shaped.Runs[R].Glyphs) do
      begin
        EmitGlyph(Shaped.Runs[R].Glyphs[G].GlyphID,
          X + Shaped.Runs[R].Glyphs[G].OffsetX,
          Shaped.Runs[R].Glyphs[G].OffsetY);
        X := X + Shaped.Runs[R].Glyphs[G].Advance;
      end;
    end;
end;

Los presupuestos van en el record de opciones

TPdfTextShapingOptions lleva una dirección más tres topes: máximo de caracteres, máximo de glifos y máximo de runs, con una función de clase Default que llena valores sensatos. Los topes no son paranoia ante entrada malformada; son aritmética. El shaping expande: una fuente con sustitución contextual agresiva puede emitir más glifos que caracteres de entrada, y un párrafo que alterna escrituras cada pocos caracteres produce un run por cambio. Un documento armado para maximizar ambas cosas convierte una cadena modesta en una asignación grande, y un servicio que moldea texto de PDFs no confiables necesita un límite que eligió y no un límite que impone la máquina

Fijar la dirección explícitamente en lugar de dejarla en automático vale la pena siempre que ya la conozcan. El automático aplica las reglas de dirección de párrafo para adivinar a partir del primer carácter fuerte, que es correcto para texto libre y erróneo para un campo de formulario cuya dirección es una propiedad del campo y no del valor que alguien tecleó en él

Enlace en runtime, no una dependencia de compilación

El backend de HarfBuzz carga la biblioteca dinámicamente. Es una decisión de despliegue con consecuencias reales: un binario corre en una máquina con HarfBuzz y en una sin él, reportando capacidad reducida en el segundo caso en lugar de fallar al arrancar. Para una biblioteca que se distribuye a otros desarrolladores es el único arreglo viable, porque no pueden exigirle a cada consumidor de un componente PDF que adquiera y haga coincidir versiones de una biblioteca de shaping que quizá no necesite

La regla correspondiente para quienes llaman es comprobar. ActiveTextShaper devuelve nil cuando la plataforma no tiene predeterminado y no se configuró ninguno, y el punto de entrada de shaping reporta eso como un shaper no disponible y no como un fallo de shaping. Son problemas distintos y merecen mensajes distintos: uno es un hueco de despliegue, el otro es un problema de fuente o de texto

Instalar una vez, antes de que algo se moldee

La instalación reemplaza y libera el shaper anterior, así que llamarla repetidamente es seguro pero inútil, y llamarla mientras otro hilo está moldeando no es seguro en absoluto. Háganlo durante el arranque. Si necesitan replegarse al predeterminado de plataforma más tarde, pasen nil, que también es como se deshace un doble de prueba al final de una prueba

Una vez instalado un backend, la medición y el ajuste de línea se comportan igual en ambas plataformas, ya que consumen las métricas de runs y glifos en lugar de llamar a la plataforma directamente; el modelo de ajuste de línea se describe en el artículo de medición de texto y ajuste de línea. Las plataformas y toolchains soportadas por el componente están en la página del producto PDFium Delphi component