Ir al contenido

Texto: costura de shaping, CJK y manejo de runs

El módulo de texto es la costura de shaping: una interfaz pequeña que convierte un run UTF-8 en glifos posicionados, un backend OpenType real cuando está disponible, un mecanismo de reserva determinista cuando no lo está y un registro para shapers específicos de cada script.

Ventana de terminal
composer require nextpdf/core:^3

ShaperInterface es la costura entre la canalización de maquetación de texto y un motor de shaping OpenType. Es deliberadamente mínima: un único método shape() que consume un ShaperInput y devuelve un ShapingResult. El tipo de retorno es la única salida visible para el consumidor: las implementaciones no deben filtrar detalles internos del motor de shaping, y el retorno tipado lo impone de manera estructural. ShapingResult transporta la lista de registros GlyphRun, el texto de origen reproducido, el script y la dirección, además de una etiqueta shaperImpl que indica qué backend produjo el resultado.

La selección del backend es explícita y clara sobre su capacidad. ShaperFactory ejecuta una prueba de capacidad una sola vez: si el host tiene un binding de HarfBuzz operativo, create() devuelve el shaper respaldado por HarfBuzz. De lo contrario, devuelve NullShaper. NullShaper es un mecanismo de reserva de paso directo. Emite un glifo sintético por cada punto de código Unicode, con avances cero y desplazamientos cero. Etiqueta el resultado para que la observabilidad detecte el mecanismo de reserva. Deja la resolución de avances al módulo de métricas de fuentes. Esta es una ruta degradada documentada, no shaping completo: la sustitución, las ligaduras, el posicionamiento de marcas y las formas contextuales requieren el backend real. wouldUseRealShaper() es un predicado de diagnóstico. En producción, la ramificación debería basarse en la etiqueta shaperImpl del resultado.

El shaping específico de cada script es un SPI, no una implementación incluida. ScriptShaperRegistry es un registro al estilo PSR-11 que resuelve un MongolianShaperInterface o un TibetanShaperInterface por etiqueta de script ISO 15924. El registro almacena las claves sin distinguir mayúsculas de minúsculas y delega la admisibilidad del código de script en una única fuente de verdad. El registro y las interfaces de shaper por script son un contrato congelado, de modo que una extensión puede registrar un proveedor de la Fase 12 sin tocar los puntos de llamada. El motor proporciona la costura; el consumidor aporta los proveedores para scripts complejos.

El manejo de runs CJK se apoya en la costura de codificación de tipografía. Una tipografía TrueType CJK incrustada se emite como una fuente de Tipo 0 con un CMap Identity-H y un descendiente CIDFontType2: ISO 32000-2 §9.7.4 (digest del RAG truncado por el límite de licencia; registrado en _downgraded-claims-o3.md). Cuando el programa TrueType está incrustado, el CIDFont de Tipo 2 asigna los identificadores de carácter a índices de glifo a través de la entrada CIDToGIDMap: ISO 32000-2 §9 (digest fijado por la página de contrato B1). El subsetter conserva con precisión la numeración de glifos original, de modo que un /CIDToGIDMap /Identity sigue siendo válido para el subconjunto. CjkFontValidator es el diagnóstico que comprueba si una fuente candidata cubre los bloques Unicode que un script necesita antes de elegirla.

TipoClaseMiembros claveEstabilidadDesde
ShaperInterfaceinterfazshape(ShaperInput): ShapingResultestable3.2.0
ShaperFactoryclase finaldefault(), create(), wouldUseRealShaper()estable3.2.0
NullShaperclase final readonlyshaper de reserva de paso directoestable3.2.0
ShapingResultclase final readonly$glyphRuns, $originalText, $script, $direction, $shaperImplestable3.2.0
ScriptShaperRegistryclase finalregisterMongolian(), getMongolian(), hasMongolian(), y los equivalentes para tibetanoestable3.1.0
CjkFontValidatorclase finalvalidateCoverage(), detectScript(), isCjkCodepoint()estable1.0.0

La forma register*, get* y has* de ScriptShaperRegistry, junto con las interfaces de shaper por script, constituye un contrato congelado. ShapingResult es, por diseño, la única salida del shaper visible para el consumidor.

examples/text/shaper-factory.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Font\Shaper\ShaperFactory;
use NextPDF\Font\Shaper\ShaperImpl;
$factory = ShaperFactory::default();
$shaper = $factory->create();
// Branch on the result tag, not on the concrete class.
$wouldShape = $factory->wouldUseRealShaper()
? 'HarfBuzz backend available'
: 'NullShaper fallback (degraded — no substitution or positioning)';
echo $wouldShape, "\n";

ShaperFactory::default() conecta la prueba de capacidad de producción. create() memoiza el backend seleccionado durante toda la vida de la factory. La indicación fiable de capacidad proviene de wouldUseRealShaper() y de la etiqueta shaperImpl de cada resultado.

examples/text/script-shaper-registry.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Text\Shaping\MongolianShaperInterface;
use NextPDF\Text\Shaping\ScriptShaperRegistry;
final readonly class ComplexScriptBootstrap
{
public function __construct(private ScriptShaperRegistry $registry) {}
/**
* Register a consumer-supplied Mongolian shaper provider at boot so
* the layout pipeline can resolve it by ISO 15924 script tag.
*/
public function register(MongolianShaperInterface $mongolian): void
{
$this->registry->registerMongolian($mongolian);
}
public function hasMongolian(): bool
{
return $this->registry->hasMongolian();
}
}

El registro es el punto de integración para los proveedores de scripts complejos. El motor entrega la costura y la forma congelada del accesor. El consumidor suministra las implementaciones de mongol y tibetano.

  • Un resultado de NullShaper tiene avances cero y desplazamientos cero. No se debe alimentar una maquetación de texto directamente con esas posiciones: resolver los avances desde el módulo de métricas de fuentes y detectar el mecanismo de reserva mediante la etiqueta shaperImpl.
  • Una entrada vacía produce una lista glyphRuns vacía, no un run vacío. El código de iteración del consumidor no necesita un caso especial para runs de longitud cero.
  • ScriptShaperRegistry no implementa Psr\Container\ContainerInterface directamente, para que los accesores tipados conserven su tipo de retorno estrechado bajo análisis estático. Usar getMongolian() y getTibetan(), no un get() genérico.
  • Las etiquetas de script se cotejan por su valor canónico alfa-4 de ISO 15924, almacenado sin distinguir mayúsculas de minúsculas. Pasar Mong o Tibt. La capitalización no afecta a la búsqueda.
  • Los caracteres de la Extensión B de CJK se encuentran en el plano 2 de Unicode y obligan a una subtabla cmap de Formato 12 en el subconjunto. La ruta de codificación se encarga de esto. No se debe asumir que el plano multilingüe básico cubre todo el escenario CJK.

La prueba de capacidad se ejecuta una sola vez por instancia de ShaperFactory y el backend se memoiza, de modo que las llamadas repetidas a create() no tienen costo adicional. NullShaper es lineal respecto al número de puntos de código del run de entrada, sin E/S. La resolución de ScriptShaperRegistry es una búsqueda por clave en tiempo constante. CjkFontValidator muestrea los puntos de código a un paso fijo en lugar de probar cada uno, lo que mantiene de bajo costo las comprobaciones de cobertura incluso frente a una fuente CJK de 20,000 glifos. El performance_budget de 1500 ms de tiempo transcurrido y 64 MB de pico cubre un run típico. El costo dominante en el shaping real es el propio backend OpenType, que queda fuera del alcance del proceso cuando el mecanismo de reserva está activo.

La costura del shaper consume una cadena UTF-8. NullShaper tolera UTF-8 malformado dividiendo de mejor esfuerzo en lugar de lanzar una excepción, porque el contrato documentado del mecanismo de reserva ya es «sin shaping real». El llamador está preparado para una salida de baja calidad. El contrato de clúster por desplazamiento de bytes usa una longitud orientada a bytes, que es correcta para entradas multibyte y evita un defecto de mapeo de clústeres desfasado por un punto de código. El backend real, cuando está presente, es una biblioteca nativa de terceros. Tratar su entrada como no confiable y limitar la longitud del run aguas arriba. El registro de shapers por script almacena proveedores suministrados por el consumidor: el límite de confianza de esas implementaciones es el del consumidor, no el del motor.

AfirmaciónEstándarCláusulaEvidencia
Una tipografía TrueType CJK incrustada se emite como una fuente de Tipo 0 con un CMap Identity-H y un descendiente CIDFontType2.ISO 32000-2§9.7.4Digest del RAG truncado por el límite de licencia; prefijo 7a5258772f508e3b, ver _downgraded-claims-o3.md
Un CIDFont de Tipo 2 incrustado asigna los identificadores de carácter a índices de glifo a través de CIDToGIDMap.ISO 32000-2§9

Ambas cláusulas se presentan parafraseadas. La segunda está fijada por digest (reutilizada de la página de contrato B1) y la primera está corroborada por ADR-013 y el resumen para desarrolladores del codificador cmap. NextPDF no reproduce texto normativo. El backend del shaper es independiente de la conformidad PDF. Las afirmaciones de conformidad aquí se refieren a la emisión del diccionario de fuente CJK que produce la costura de codificación, documentada con más detalle en ADR-013 y el resumen para desarrolladores del codificador cmap.

Una canalización avanzada de preprocesamiento de texto y los servicios de extracción se construyen sobre la costura del shaper del núcleo y los tipos de valor para el manejo de runs. El módulo de texto del núcleo ofrece la costura, el mecanismo de reserva y el registro de shapers por script sin licencia. La omisión de un enlace de conversión es intencional.