Artículo técnico

Campos comb con árabe y hebreo en Delphi con HotPDF

HotPDF rellena campos comb AcroForm con texto árabe, hebreo, con marcas combinantes y de dirección mixta mediante THPDFHeadlessDocument.SetTextFieldValueWithFamilies. La rejilla 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 la línea entera pasa por el algoritmo bidireccional Unicode antes de asignar cualquier celda

Cada una de esas tres reglas sustituye un atajo que se ve bien en una demo latina y se desmorona en el primer formulario real. Imagina una solicitud gubernamental con un campo comb de diez casillas para un nombre escrito en árabe. Rellénalo con lógica por carácter y obtienes letras en formas aisladas marchando de izquierda a derecha, una ligadura lam-alef apretada en una casilla mientras su vecina se queda 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 rellenar un campo comb árabe con HotPDF en Delphi: el atajo por carácter aplasta la ligadura lam-alef, el relleno 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é pide de verdad la bandera Comb a un writer de PDF

La bandera Comb le pide al writer repartir el campo en exactamente MaxLen posiciones equiespaciadas 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 los 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 a cero. El bit en sí es $1000000 en el valor /Ff de 32 bits; si alguna vez has visto campos comb portarse mal tras un viaje de ida y vuelta por una variable de coma flotante, el artículo sobre banderas de campo AcroForm de 32 bits explica por qué ese bit es el primero en irse

La especificación dice «carácter» y ahí se queda. Se escribió pensando en casillas latinas de paso fijo y no dice qué es un carácter cuando un par de letras árabes se convierte en un glyph, o cuando una marca de vocal no tiene ancho propio. HotPDF tiene que elegir una unidad, y mientras lo hace hace cumplir las reglas estructurales con rigor: un campo comb que además sea multiline, o que no lleve un MaxLen positivo, se rechaza con un diagnóstico, y los campos password no pueden ser comb en absoluto

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

HotPDF cuenta las celdas comb en escalares Unicode porque es la misma unidad que usa para hacer cumplir MaxLen, y la rejilla y el check de longitud tienen que coincidir o un valor que pasa la validación no cabrá en sus casillas. Las tres alternativas tentadoras rompen ese acuerdo cada una a su manera

  • 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 recuentos de glyphs dependen de la fuente: la mayoría de las fuentes árabes convierten lam más alef en un glyph, así que el recuento de celdas cambiaría en el momento en que cambies de tipografía
  • Los recuentos de grafemas funden una letra base con sus marcas, lo que significa que la rejilla discreparía de un check MaxLen que cuenta cada escalar que tecleó el usuario

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 sustituto válido una vez y rechaza de plano un sustituto sin pareja. Si tu UI quiere avisar antes de la llamada en vez de cazar una excepción después, cuenta igual

Matriz que compara unidades UTF-16, glyphs y grafemas contra escalares Unicode para las celdas comb de HotPDF en Delphi, con lam-alef, beh más fatha, U+20BB7 y e acentuada
solo la columna de escalares Unicode coincide con el check de MaxLen — un par sustituto cuenta una vez y el recuento de celdas jamás depende de la fuente
// Cuenta escalares Unicode igual que la rejilla comb:
// un par sustituto 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;

Unos cuantos valores muestran hasta dónde se separan las unidades, y por qué solo la columna de escalares coincide con lo que un campo comb presupuesta

Valorunidades UTF-16Escalares (celdas)glyphs típicos
Lam + alef (لا)221 ligadura
Beh + fatha (بَ)222, marca unida a la base
U+20BB7 (𠮷)211
Decomposed e + U+0301221 o 2, según 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 unidad única a lo largo 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 se queda centrada a lo largo de ambas. Un beh con shadda y fatha son tres escalares, así que recibe tres casillas, con las marcas todavía unidas a la letra

El centrado es donde la implementación obvia se equivoca. Centrar cada glyph en su propia casilla arranca una marca de su base, porque la posición de la marca solo significa algo respecto a la letra que decora. HotPDF mantiene cada glyph 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 glyph posicionados y la tinta real del glyph. El cluster se mueve en bloque; nada dentro se reordena

Una consecuencia se pasa por alto con facilidad. 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 glyph. Cuentan contra MaxLen y ocupan una celda que se queda en blanco. Si tus datos fuente vienen de un sistema que espolvorea esas marcas en los nombres, límpialas antes de rellenar, o el campo mostrará un hueco y rechazará valores un carácter antes de lo que el usuario espera

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

HotPDF pasa la línea completa por el algoritmo bidireccional Unicode (UAX #9) primero, y solo entonces 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 en cuanto la línea contiene dígitos o un token latino, porque esos tramos tienen que 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 la línea entera 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
ejecuta el algoritmo bidireccional sobre la línea entera primero, y luego dale a cada escalar la celda en su posición visual para que los dígitos incrustados conserven su orden

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

Rellenar un campo comb de fuentes mixtas desde Delphi

El texto comb de fuentes mixtas usa una rejilla de celdas, un tamaño de fuente y una línea base para cada fuente suministrada. Con FallbackFontFamilies a True, HotPDF elige una fuente para cada grafema completo de las familias suministradas en su orden configurado, así que una cara árabe puede llevar las letras mientras una cara latina lleva ASCII incrustado. Cada tramo de fuente se compone con la línea entera como contexto, una marca jamás se separa de su base entre fuentes, y no se inventa ligadura alguna a través de una frontera de fuente. Maquetar cada tramo con su propia rejilla local o escalarlo a su propio bounding box sería más sencillo, y rompería tanto las posiciones de celda como los campos rotados en el momento en que 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;                   // autoajuste, con 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;

Fíjate en lo que el ejemplo no pone. Durante el relleno del documento, Width, Height, Rotation y CombCells se derivan de cada rectángulo de widget, su entrada /MK y el MaxLen del campo, así que los valores que pongas en esos miembros se sobreescriben. Un FontSize de cero pide dimensionado automático, que comprueba la extensión de cada cluster contra el ancho de sus k celdas y la altura métrica y de tinta compartidas de todas las fuentes participantes; un tamaño fijo sencillamente se recorta al widget. CharacterSpacing y WordSpacing jamás mueven posiciones comb, ya que la rejilla sola decide dónde aterriza un cluster. HotPDF carga sus runtimes HarfBuzz y FriBidi solo cuando un valor realmente exige shaping, y los valores que no necesitan shaping conservan el layout comb directo existente sin cambios. Para el flujo general de relleno de un PDF cargado, incluidos permisos y guardado, véase 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 de celdas correcta, clusters intactos y un valor lógico preservado; no garantiza uniones cursivas continuas ni una extracción de texto perfecta en todas las herramientas. Ambos límites se derivan de lo que es un campo comb, y conviene conocerlos antes de prometer nada a un cliente

Las uniones cursivas entre celdas son la primera. Las letras árabes siguen componiéndose con contexto de línea completa, así que cada una recibe su forma inicial, medial o final correcta, pero el paso fijo de celda mete espacio entre clusters separados y los trazos de unión 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 glyph y pueden devolver otro orden o duplicar el texto del cluster, porque un cluster multi-glyph no tiene un único valor Unicode obvio por glyph. Si un sistema aguas abajo necesita el valor, lee 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 glyph ausente, una fuente inválida, un exceso de presupuesto o una cancelación dejan el valor y las apariencias anteriores intactos, así que un relleno rechazado jamás deja un campo a medio dibujar

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 una pipeline de formularios

El soporte comb con shaping importa más para formularios que ya existen: declaraciones de la renta, solicitudes de visado, packs de alta bancaria y registros de identidad diseñados con campos en casillas y ahora rellenados en árabe, hebreo, persa o urdu. Cuenta en escalares, conserva el valor original en /V, deja que una sola rejilla gobierne todas las fuentes, y prueba con un valor que mezcle letras de derecha a izquierda, dígitos y una marca combinante, porque esa única cadena ejercita todas las reglas de este artículo. La API de documentos headless, el shaping comb y el fallback de fuentes descritos aquí forman parte del componente PDF HotPDF para Delphi