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
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
Lengthde 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
MaxLenque 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
// 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
| Valor | unidades UTF-16 | Escalares (celdas) | glyphs típicos |
|---|---|---|---|
| Lam + alef (لا) | 2 | 2 | 1 ligadura |
| Beh + fatha (بَ) | 2 | 2 | 2, marca unida a la base |
| U+20BB7 (𠮷) | 2 | 1 | 1 |
| Decomposed e + U+0301 | 2 | 2 | 1 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
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