El text shaping en el componente PDFium pasa por un único objeto instalable. ConfigureTextShaper instala el shaper por el que se encamina todo 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 informa de qué backend está vivo; ClearTextShaper retira la instalación y deja que el predeterminado se cree de nuevo. En Windows el predeterminado es TPdfUniscribeTextShaper. Bajo Free Pascal está 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
Una interfaz, dos backends que reparten el trabajo de forma completamente distinta. Entender esa asimetría es lo que evita que la ruta portable produzca texto conformado correctamente y posicionado mal
Por qué el backend de Windows es una clase y el portable tres piezas?
Porque Uniscribe son cuatro APIs haciendo como que son una. ScriptItemize segmenta una cadena por escritura y resuelve niveles bidireccionales; ScriptShape mapea caracteres a glifos; ScriptPlace calcula avances y offsets; 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 única clase con un único método
HarfBuzz cubre las dos del medio. Conforma y posiciona un run cuya dirección y escritura ya decidió quien llama, y no opina 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 sustanciosa para ser su propia unidad, descrita en el artículo de niveles de incrustación UAX #9
El shaper no resuelve fuentes, y es deliberado
Uniscribe lee el binario de la fuente de un contexto de dispositivo GDI. No hay equivalente portable de eso, e inventar uno 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 toma un resolver: un callback que mapea un nombre de fuente a los bytes TrueType u OpenType. Devolver False hace fallar la petición 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
// Vuestra 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; llamad una vez durante el arranque,
// antes de que nada conforme texto
ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
// En Delphi el predeterminado de plataforma (Uniscribe) se crea bajo 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 conformar con un conjunto de fuentes incrustado 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 anfitriones. El componente también expone un proveedor de fuentes del sistema anfitrión para los casos en que sí queréis 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 rellenan el mismo TPdfShapedText: el texto fuente, nombre de fuente, tamaño, bytes de la fuente, un array de runs, la anchura total, el recuento de glifos y el recuento de caracteres lógicos. Cada TPdfShapedRun lleva su tramo en el texto fuente, su posición X visual, su anchura, su nivel bidireccional y una bandera de derecha a izquierda, más sus glifos. Cada TPdfShapedGlyph lleva un identificador de glifo, un avance, offsets X e Y, y el cluster al que pertenece como inicio y longitud en el texto fuente
Esos campos de cluster son lo que hace al record utilizable y no meramente informativo. El shaping no es un mapeo uno a uno: una sílaba devanágari se convierte en 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 podéis colocar un caret, hacer hit-test de un clic ni resaltar una selección, porque no podéis decir a qué caracteres pertenece un glifo. Con ellos, la aritmética es local y el mismo código sirve para ambos backends
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 rellenado
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 pertenecen al 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 rellena 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 ambos convierte una cadena modesta en una asignación grande, y un servicio que conforma texto de PDF no confiables necesita un límite que él eligió y no un límite que impone la máquina
Fijar la dirección explícitamente en lugar de dejarla en automático merece hacerlo siempre que ya la conozcáis. El automático aplica las reglas de dirección de párrafo para adivinar a partir del primer carácter fuerte, lo 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 tiempo de ejecución, no 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, informando de capacidad reducida en el segundo caso en lugar de fallar al arrancar. Para una biblioteca distribuida a otros desarrolladores ese es el único arreglo viable, porque no podéis exigir a cada consumidor de un componente PDF que adquiera y empareje 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 informa de eso como shaper no disponible en lugar de como 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 nada conforme
La instalación reemplaza y libera el shaper anterior, así que llamarla repetidamente es seguro pero inútil, y llamarla mientras otro hilo conforma no es seguro en absoluto. Hacedlo durante el arranque. Si necesitáis volver al predeterminado de la plataforma más tarde, pasad nil, que también es como se deshace un doble de prueba al final de un test
Una vez instalado un backend, la medición y el ajuste de línea se comportan igual en ambas plataformas, porque 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 de producto de PDFium Delphi component