Artículo técnico

Campos comb en árabe y hebreo en Delphi con HotPDF

HotPDF llena campos comb de AcroForm con texto árabe, hebreo, marcas combinantes y de dirección mixta mediante THPDFHeadlessDocument.SetTextFieldValueWithFamilies. La cuadrícula comb se cuenta en escalares Unicode, una ligadura o una letra base con sus marcas se centra como un cluster a lo largo de las k celdas que sus escalares poseen, y toda la línea pasa por el algoritmo bidireccional Unicode antes de asignar cualquier celda

Cada una de esas tres reglas reemplaza un atajo que se ve bien en una demo latina y se desmorona en el primer formulario real. Imagine una solicitud gubernamental con un campo comb de diez casillas para un nombre escrito en árabe. Llénelo con lógica por carácter y obtiene letras en formas aisladas marchando de izquierda a derecha, una ligadura lam-alef apretada en una casilla mientras su vecina está vacía, y un fatha flotando sobre la celda equivocada. Cada uno de esos defectos viene de contar la unidad equivocada u ordenar el texto en el momento equivocado

Dos formas de llenar un campo comb árabe con HotPDF en Delphi: el atajo por carácter aplasta la ligadura lam-alef, el llenado con shaping centra cada cluster
contar escalares Unicode y reordenar la línea antes de asignar celdas mantiene la ligadura centrada a lo largo de sus dos casillas y no deja ninguna celda vacía

¿Qué le pide en realidad la bandera Comb a un writer de PDF?

La bandera Comb le pide al writer dividir el campo en exactamente MaxLen posiciones igualmente espaciadas y poner un carácter en cada una. ISO 32000-1 §12.7.4.3 define Comb como la posición de bit 25 en la Tabla 228, las banderas de campo específicas de campos de texto, y le da sentido solo cuando la entrada MaxLen de la Tabla 229 está presente y las banderas Multiline, Password y FileSelect están limpias. El bit mismo es $1000000 en el valor /Ff de 32 bits; si alguna vez vio campos comb portarse mal tras un viaje de ida y vuelta por una variable de punto flotante, el artículo sobre banderas de campo AcroForm de 32 bits explica por qué ese bit es el primero en perderse

La especificación dice "carácter" y hasta ahí llega. Fue escrita pensando en casillas latinas de paso fijo y no dice qué es un carácter cuando un par de letras árabes se vuelve un glifo, o cuando una marca de vocal no tiene ancho propio. HotPDF tiene que elegir una unidad, y hace cumplir las reglas estructurales estrictamente mientras lo hace: un campo comb que además sea multiline, o que no lleve un MaxLen positivo, se rechaza con un diagnóstico, y los campos de contraseña no pueden ser campos comb en absoluto

¿Por qué contar escalares Unicode en vez de unidades UTF-16, glifos o graphemes?

HotPDF cuenta las celdas comb en escalares Unicode porque es la misma unidad que usa para hacer cumplir MaxLen, y la cuadrícula y la verificación de longitud deben concordar o un valor que pasa la validación no cabrá en sus casillas. Las tres alternativas tentadoras rompen ese acuerdo cada una de una forma distinta

  • Las unidades de código UTF-16 son lo que devuelve el Length de Delphi, así que un carácter suplementario como U+20BB7 se comería dos casillas y dos slots del presupuesto del campo
  • Los conteos de glifos dependen de la fuente: la mayoría de las fuentes árabes convierten lam más alef en un glifo, así que el conteo de celdas cambiaría en cuanto cambie de tipografía
  • Los conteos de graphemes fusionan una letra base con sus marcas, lo que significa que la cuadrícula discreparía de una verificación MaxLen que cuenta cada escalar que el usuario tecleó

La validación MaxLen en THPDFHeadlessDocument solo recorre la cadena cuando su longitud UTF-16 excede el límite, y cuando lo hace, cuenta un par surrogate válido una vez y rechaza un surrogate sin pareja sin más. Si su UI quiere advertir antes de la llamada en vez de capturar una excepción después, cuente de la misma forma

Matriz que compara unidades UTF-16, glifos y graphemes contra escalares Unicode para las celdas comb de HotPDF en Delphi, con lam-alef, beh más fatha, U+20BB7 y la e acentuada
solo la columna de escalares Unicode concuerda con la verificación MaxLen — un par surrogate cuenta una vez y el conteo de celdas nunca depende de la fuente
// Cuenta escalares Unicode como lo hace la cuadrícula comb:
// un par surrogate válido es un escalar
function CountScalars(const S: UnicodeString): Integer;
var
  I: Integer;
begin
  Result := 0;
  I := 1;
  while I <= Length(S) do
  begin
    if (Ord(S[I]) >= $D800) and (Ord(S[I]) <= $DBFF) and
       (I < Length(S)) and
       (Ord(S[I + 1]) >= $DC00) and (Ord(S[I + 1]) <= $DFFF) then
      Inc(I, 2)
    else
      Inc(I);
    Inc(Result);
  end;
end;

Algunos valores muestran qué tanto se separan las unidades, y por qué solo la columna de escalares coincide con lo que un campo comb presupuesta

ValorUnidades UTF-16Escalares (celdas)Glifos típicos
Lam + alef (لا)221 ligadura
Beh + fatha (بَ)222, marca unida a la base
U+20BB7 (𠮷)211
e + U+0301 descompuesto221 o 2, depende de la fuente

¿Cómo comparten sus celdas las ligaduras y las marcas combinantes?

Un cluster con shaping que cubre k escalares ocupa k celdas contiguas y se centra como una sola unidad a través de ellas. Tras el shaping, cada cluster se mapea de vuelta al tramo exacto de texto fuente del que vino, así que HotPDF sabe qué escalares, y por tanto qué celdas, posee. La ligadura lam-alef de arriba recibe dos casillas y queda centrada a lo largo de ambas. Una beh que lleva una shadda y un fatha son tres escalares, así que recibe tres casillas, con las marcas todavía unidas a la letra

Centrar es donde la implementación obvia se equivoca. Centrar cada glifo en su propia casilla separa una marca de su base, porque la posición de la marca solo significa algo relativo a la letra que decora. HotPDF mantiene cada glifo dentro de un cluster en su avance con shaping y sus offsets X e Y, y luego centra el cluster usando la extensión horizontal combinada del avance natural, los orígenes de glifo posicionados y la tinta real del glifo. El cluster se mueve como bloque; nada dentro de él se rearma

Una consecuencia es fácil de pasar por alto. Los controles direccionales como U+200F RIGHT-TO-LEFT MARK, U+061C ARABIC LETTER MARK, y los controles de embedding e isolate son escalares Unicode sin glifo. Cuentan contra MaxLen y sostienen una celda que queda en blanco. Si sus datos de origen vienen de un sistema que espolvorea estas marcas en los nombres, quítelas antes de llenar, o el campo mostrará un hueco y rechazará valores un carácter antes de lo que el usuario espera

¿Por qué el reordenamiento bidi debe ocurrir antes de asignar celdas?

HotPDF pasa la línea completa por el algoritmo bidireccional Unicode (UAX #9) primero, y solo entonces le da a cada escalar su celda según su posición visual. Asignar celdas en orden lógico y voltear el resultado después funciona para una cadena puramente árabe y falla tan pronto la línea contiene dígitos o un token latino, porque esos tramos deben seguir leyéndose de izquierda a derecha dentro de la línea de derecha a izquierda. Un valor como un código de referencia con prefijo árabe y un número mantiene sus dígitos en el orden correcto solo si el reordenamiento ve toda la línea de una vez. La visión general del shaping de texto complex-script cubre el problema de orden lógico contra visual con más profundidad

Asignación de celdas comb para un valor árabe con dígitos en HotPDF para Delphi: el orden lógico invierte el número, mientras que UAX 9 primero mantiene los dígitos intactos
ejecute el algoritmo bidireccional sobre toda la línea primero, y luego dé a cada escalar la celda de su posición visual para que los dígitos incrustados conserven su orden

La alineación luego posiciona toda la cuadrícula de celdas usadas dentro del campo. La entrada /Q heredada del campo gana cuando está presente; Options.Alignment aplica solo cuando el formulario no tiene ninguna. Eso importa para formularios de derecha a izquierda: un valor árabe corto en un campo comb alineado a la izquierda llena las casillas de la izquierda, lo que es correcto según la especificación y casi nunca lo que el diseñador del formulario pretendía. Arregle el /Q del formulario, o ponga htaRight cuando el campo no tenga quadding propio. La rotación del widget viene de la entrada /R de /MK, y a 90 o 270 grados el viewport lógico del texto intercambia ancho y alto mientras la lógica de celdas queda igual

Llenar un campo comb de fuentes mixtas desde Delphi

El texto comb de fuentes mixtas usa una sola cuadrícula de celdas, un solo tamaño de fuente y una sola baseline para cada fuente provista. Con FallbackFontFamilies en True, HotPDF elige una fuente para cada grapheme completo de las familias provistas en su orden configurado, así que una fuente árabe puede llevar las letras mientras una latina lleva ASCII incrustado. Cada tramo de fuente se moldea con toda la línea como contexto, una marca nunca se separa de su base entre fuentes, y no se inventa ninguna ligadura a través de una frontera de fuente. Maquetar cada tramo con su propia cuadrícula local o escalarlo a su propio bounding box sería más simple, y rompería tanto las posiciones de celdas como los campos rotados en cuanto el fallback cambiara de fuente a mitad de línea

uses
  System.SysUtils, System.IOUtils,
  HPDFHeadlessDocument, HPDFHeadlessAppearance;

procedure FillArabicComb(const SourceFile, TargetFile, Value: string);
var
  Doc: THPDFHeadlessDocument;
  Field: THPDFHeadlessFormField;
  Families: THPDFHeadlessRichTextFontFamilies;
  Options: THPDFHeadlessTextAppearanceOptions;
  FieldIndex: Integer;
begin
  Doc := THPDFHeadlessDocument.Create;
  try
    Doc.LoadFromFile(SourceFile);
    FieldIndex := Doc.FindFormField('applicant.name_ar');
    if FieldIndex < 0 then
      raise Exception.Create('Field not found');
    Field := Doc.GetFormField(FieldIndex);
    if (Field.Flags and $1000000) = 0 then   // bit 25: Comb
      raise Exception.Create('Not a comb field');

    SetLength(Families, 2);                  // de una a dieciséis familias
    Families[0].FamilyName := 'Noto Naskh Arabic';
    Families[0].Fonts[hrfsRegular] :=
      TFile.ReadAllBytes('NotoNaskhArabic-Regular.ttf');
    Families[1].FamilyName := 'Noto Sans';
    Families[1].Fonts[hrfsRegular] :=
      TFile.ReadAllBytes('NotoSans-Regular.ttf');

    Options := THPDFHeadlessTextAppearanceOptions.Default;
    Options.ShapeComplexText := True;        // ya es el valor por defecto
    Options.FallbackFontFamilies := True;    // el valor por defecto es False
    Options.FontSize := 0;                   // ajuste automático, tope en 12
    Options.Alignment := htaRight;           // se usa solo cuando /Q está ausente

    Doc.SetTextFieldValueWithFamilies(FieldIndex, Value, Families, Options);
    Doc.SaveToFile(TargetFile);              // debe diferir de SourceFile
  finally
    Doc.Free;
  end;
end;

Note lo que el ejemplo no pone. Durante el llenado del documento, Width, Height, Rotation y CombCells se derivan del rectángulo de cada widget, su entrada /MK y el MaxLen del campo, así que los valores que ponga en esos miembros se sobrescriben. Un FontSize cero pide dimensionado automático, que verifica la extensión de cada cluster contra el ancho de sus k celdas y la altura métrica y de tinta compartida de todas las fuentes participantes; un tamaño fijo simplemente se recorta al widget. CharacterSpacing y WordSpacing nunca mueven posiciones comb, ya que la cuadrícula sola decide dónde aterriza un cluster. HotPDF carga sus runtimes HarfBuzz y FriBidi solo cuando un valor realmente requiere shaping, y los valores que no lo requieren conservan el layout comb directo existente sin cambios. Para el flujo general de llenado de un PDF cargado, incluidos permisos y guardado, vea poner valores de campos de formulario en un PDF cargado

¿Qué no garantiza una apariencia comb con shaping?

Una apariencia comb con shaping garantiza asignación correcta de celdas, clusters intactos y un valor lógico preservado; no garantiza uniones cursivas sin costuras ni extracción de texto perfecta en toda herramienta. Ambos límites se derivan de lo que es un campo comb, y debería conocerlos antes de prometerle nada a un cliente

Las uniones cursivas entre celdas son la primera. Las letras árabes se siguen moldeando con el contexto de toda la línea, así que cada una recibe su forma inicial, medial o final correcta, pero el paso fijo de celdas pone espacio entre clusters separados y los trazos conectores no van a encontrarse como lo hacen en texto corrido. Eso es inherente a dibujar una escritura de unión en casillas, y ningún centrado lo esconde

La extracción de texto es la segunda. El /V del campo conserva el valor lógico plano, y el Form XObject padre de la apariencia lleva un tramo /ActualText (ISO 32000-1 §14.9.4) con la entrada completa; los Forms hijos no llevan ninguno. Los extractores que respetan ActualText recuperan la cadena original. Los que la ignoran reconstruyen el texto desde posiciones de glifo y pueden devolver otro orden o texto de cluster duplicado, porque un cluster de varios glifos no tiene un único valor Unicode obvio por glifo. Si un sistema aguas abajo necesita el valor, lea el campo, no el texto de la página

Las fronteras restantes son mecánicas. El texto comb debe ser una sola línea sin tabs, y un valor con más escalares que celdas falla en vez de truncarse. Los fallos son transaccionales: un glifo faltante, una fuente inválida, un exceso de presupuesto o una cancelación dejan el valor anterior y las apariencias intactas, así que un llenado rechazado nunca deja un campo medio dibujado

try
  Doc.SetTextFieldValueWithFamilies(FieldIndex, Value, Families, Options);
except
  on E: EHPDFHeadlessDocumentError do
    if E.Code = hdLimitExceeded then
      ShowFieldError('The value has more characters than the field allows')
    else
      raise;
  on E: EHPDFHeadlessAppearanceError do
    if E.Failure = haMissingGlyph then
      ShowFieldError('None of the supplied fonts covers this text')
    else
      raise;
end;
// Ante cualquiera de los dos errores el campo conserva su valor y apariencia anteriores

Dónde encajan los campos comb con shaping en un pipeline de formularios

El soporte comb con shaping importa más para formularios que ya existen: declaraciones de impuestos, solicitudes de visa, paquetes de onboarding bancario y registros de identidad diseñados con campos en casillas y ahora llenados en árabe, hebreo, persa o urdu. Cuente en escalares, conserve el valor original en /V, deje que una sola cuadrícula gobierne cada fuente, y pruebe con un valor que mezcle letras de derecha a izquierda, dígitos y una marca combinante, porque esa única cadena ejercita cada regla de este artículo. La API de documentos headless, el shaping comb y el fallback de fuentes descritos aquí son parte del componente PDF HotPDF para Delphi