Artículo técnico

RtLTextOut de HotPDF: texto de derecha a izquierda en Delphi

Envíe la frase árabe يوضح ملف PDF هذا a un TextOut normal y la página que obtiene está mal de dos maneras a la vez. Las palabras van de izquierda a derecha en lugar de derecha a izquierda, y las letras quedan separadas en sus formas aisladas en lugar de unirse en palabras conectadas. Nada da error. El Delphi compila, el fichero se abre, y un revisor que lea árabe le dice que la salida es inservible. La solución es una llamada, no cambiar de librería: HotPDF encamina el texto de derecha a izquierda por un método aparte, RtLTextOut, que se ocupa de la reordenación que el TextOut normal no hace. Esta página es la referencia práctica de ese método: la firma y sus parámetros, el argumento charset que selecciona la escritura, el efecto secundario a nivel de documento, la configuración de fuente que debe ir primero y los fallos que llegan de verdad a soporte, cada uno con su solución

Firma y parámetros

procedure RtLTextOut(X, Y: Single; angle: Extended;
  Text: WideString); overload;
procedure RtLTextOut(X, Y: Single; angle: Extended;
  Text: PWORD; TextLength: Integer); overload;

X e Y anclan el fragmento en el sistema de coordenadas propio de la página, medido desde la esquina inferior izquierda con Y creciendo hacia arriba, el mismo origen que usa cada llamada a TextOut; RtLTextOut cambia el orden de los glifos, no desde dónde mide la página. angle rota la línea base exactamente igual que en TextOut, así que 0 dibuja una línea horizontal. Text es la cadena en orden lógico, el orden en que la escribiría, y la segunda sobrecarga toma los mismos datos UTF-16 como un búfer PWORD en bruto con un recuento explícito de unidades de código, que es la forma que conviene cuando el texto llega desde una API y no desde una cadena Delphi. En versiones antiguas de Delphi anteriores a la resolución de sobrecargas para estos tipos, la forma de cadena se expone con el nombre RtLTextOutStr y la lista de parámetros idéntica

El reparto de tareas entre las dos llamadas de salida es estricto. TextOut dibuja los puntos de código en el orden en que se los pasa, lo cual es correcto para latino, cirílico y CJK e incorrecto para árabe y hebreo. RtLTextOut reordena primero cada línea en orden visual de derecha a izquierda y después dibuja, manteniendo las palabras latinas y los dígitos incrustados leyéndose de izquierda a derecha dentro de la línea. HotPDF mantiene los dos métodos deliberadamente separados en lugar de adivinar la dirección a partir de los caracteres, así que elegir cuál llamar es elegir qué comportamiento de escritura obtiene; use RtLTextOut para los fragmentos de derecha a izquierda, TextOut para todo lo demás, y nunca encamine uno a través del otro. Por qué existe la reordenación, qué hacen realmente el algoritmo bidireccional de Unicode y la unión contextual del árabe, y dónde se detiene el shaping de HotPDF son el tema del artículo complementario sobre shaping de texto árabe y RTL con HotPDF; todo lo que sigue es la configuración práctica

Diagrama de cómo RtLTextOut reordena una línea mixta de árabe y latino en orden visual de derecha a izquierda antes de dibujarla en un PDF
RtLTextOut reordena cada línea en orden visual antes de dibujar: los fragmentos de derecha a izquierda conservan su secuencia mientras las palabras latinas y los dígitos incrustados se leen de izquierda a derecha dentro de la línea

El argumento charset decide la escritura

Lo que le dice a RtLTextOut si está componiendo árabe o hebreo no es el método, es la fuente. SetFont toma un charset de Windows como cuarto argumento, y ese valor transporta las reglas de escritura a la llamada de derecha a izquierda: 178 selecciona árabe, 177 selecciona hebreo. Fije el charset, luego dibuje, y las dos líneas siguientes salen en el orden de lectura correcto sin ninguna configuración adicional

// Árabe: el charset 178 indica a RtLTextOut que aplique las reglas del árabe
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

// Hebreo: el charset 177 cambia las reglas a hebreo
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 660, 0, 'קובץ PDF זה');

Hay un detalle de secuencia fácil de pasar por alto: el SetFont tiene que ir primero y tiene que repetirse después de cada AddPage, porque la fuente actual, charset incluido, no sobrevive a un salto de página. Olvide la repetición y la segunda página recurre a la fuente que estuviera activa, lo que para el árabe suele significar cuadros vacíos

No invierte un texto que usted ya invirtió

El único error que más tiempo de depuración se traga aquí es pasar a RtLTextOut una cadena que ya invirtió a mano. La gente llega a este método después de que un primer intento con TextOut normal saliera al revés, y un parche habitual es invertir los caracteres en el código antes de dibujar. RtLTextOut invierte internamente por su cuenta, así que una cadena ya invertida se invierte una segunda vez y acaba justo donde empezó. Pase el texto en orden lógico, el orden en que lo escribiría y lo leería en voz alta, y deje que la llamada haga la reordenación

La trampa es más dañina que una simple inversión porque una cadena doblemente invertida puede parecer correcta con una frase de prueba enteramente en árabe y romperse en cuanto una línea lleva una palabra latina o un número. Dentro de una línea de derecha a izquierda esos fragmentos incrustados deben leerse de izquierda a derecha, y la inversión manual destroza ese anidamiento mientras el caso puramente árabe resulta sobrevivir. Así que el fallo pasa su primera prueba de humo y aflora más tarde en una factura real con un número de cuenta. Elimine toda inversión manual en el momento en que cambie a RtLTextOut

El efecto secundario sobre Direction que conviene conocer

Llamar a RtLTextOut cambia más que la línea que está dibujando. También pone la preferencia de dirección de lectura del documento en derecha a izquierda, lo mismo que en otro caso fijaría usted mediante la propiedad Direction. Ese setter añade vpDirection a las ViewerPreferences del documento, lo que indica al visor cómo disponer las dobles páginas y por qué lado empieza un diseño de páginas enfrentadas. Cuando todo el documento está en árabe o hebreo eso es exactamente lo que quiere, y lo obtiene gratis

Conviene conocerlo precisamente porque es invisible en una sola página. Si el documento es mayoritariamente de izquierda a derecha con un solo bloque de derecha a izquierda, la primera llamada a RtLTextOut inclinará igualmente la preferencia de todo el fichero, y nada en su prueba de una página lo mostrará. El síntoma aparece semanas después, cuando alguien imprime un folleto a doble cara y las dobles páginas salen reflejadas. Si no es eso lo que quiere, restablezca Direction explícitamente después del fragmento de derecha a izquierda:

// RtLTextOut ya ha puesto la dirección del documento en RightToLeft;
// restaurar izquierda a derecha si el documento es predominantemente LTR
Pdf.Direction := LeftToRight;

Para un documento que realmente se lee de derecha a izquierda, déjelo como está. La cuestión es saber que la llamada tiene un efecto sobre todo el documento para que la sorpresa del folleto nunca ocurra

Registre la fuente que distribuye, no la que espera que esté instalada

Nada de la reordenación importa si la fuente no tiene glifos que dibujar. El fallo clásico es un informe que se renderiza sin tacha en la máquina del desarrollador, donde Arial Unicode MS casualmente está presente, y sale como filas de cuadros vacíos en el servidor de un cliente donde Windows sustituyó en silencio una fuente sin ninguna cobertura de árabe. El remedio es dejar de confiar en las fuentes instaladas del sistema y registrar una que distribuya con la aplicación

// Distribuir una fuente árabe conocida y registrarla antes de dibujar
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansArabic.ttf');
Pdf.CurrentPage.SetFont('NotoSansArabic', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

Dos límites acompañan al registro. Una fuente incorporada mediante RegisterUnicodeTTF se incrusta, y el manejo de Unicode incrustado de HotPDF necesita el documento en PDF 1.5 o posterior; eso solo muerde si algo aguas abajo insiste en PDF 1.4, pero cuando lo hace el fallo es silencioso. El otro es legal más que técnico: los ficheros TrueType llevan bits de permiso de incrustación, y una fuente que se ve bien en pantalla puede tener una licencia que prohíba distribuirla dentro de documentos de clientes. Confirme la licencia antes de incrustar, no después de una reclamación

Un ejemplo de consola completo

Juntando las piezas, aquí tiene un programa autocontenido que escribe una página con una línea en árabe, una línea en hebreo y una línea mixta que lleva un nombre de producto latino. Cada bloque fija su charset y después dibuja en orden lógico

program RtLTextOutDemo;

{$APPTYPE CONSOLE}

uses
  HPDFDoc;   // unidad principal de HotPDF

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'RtLTextOut.pdf';
    Pdf.BeginDoc;

    // Un encabezado latino pasa por la ruta ordinaria de TextOut
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(40, 780, 0, 'Right-to-left text with HotPDF');

    // Árabe: charset 178, orden lógico, RtLTextOut hace la reordenación
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 720, 0,
      'يوضح ملف PDF هذا كيفية التعامل مع النص العربي.');

    // Hebreo: charset 177
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
    Pdf.CurrentPage.RtLTextOut(400, 680, 0,
      'קובץ PDF זה מדגים טקסט עברי הזורם מימין לשמאל.');

    // Línea mixta: la palabra latina incrustada sigue leyéndose de izquierda a derecha
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 640, 0,
      'مرحبا بالعالم! تم إنشاؤه بواسطة HotPDF');

    Pdf.EndDoc;
    Writeln('Wrote RtLTextOut.pdf');
  finally
    Pdf.Free;
  end;
end.

Ejecútelo y abra el resultado. Las líneas en árabe y hebreo se leen de derecha a izquierda, las letras se unen donde la escritura las une, y en la última línea el token HotPDF queda de izquierda a derecha dentro del fragmento árabe. Ese anidamiento es el resultado bidireccional correcto, no un fallo, aunque los revisores primerizos lo reportan como tal de forma rutinaria; el artículo sobre shaping enlazado arriba explica por qué las reglas de Unicode lo exigen y cómo redactar sus criterios de aceptación para que ese informe nunca se presente

Errores comunes y sus soluciones

Cada fallo de la lista ha aparecido en un hilo de soporte real, y cada uno se remonta a una de las secciones anteriores

  • La salida se lee al revés o se desordena en líneas mixtas: la cadena se invirtió a mano antes de la llamada, normalmente un resto de un apaño con TextOut. Elimine toda inversión manual y pase el orden lógico; RtLTextOut invierte internamente
  • Las letras salen desconectadas en formas aisladas: el texto pasó por un TextOut normal, o se llamó a SetFont sin un charset de derecha a izquierda. Dibuje con RtLTextOut y pase 178 para árabe o 177 para hebreo como cuarto argumento de SetFont
  • Cuadros vacíos en la máquina del cliente: Windows sustituyó una fuente sin cobertura de árabe ni hebreo. Deje de nombrar fuentes instaladas; registre una fuente que distribuya mediante RegisterUnicodeTTF y selecciónela con SetFont por ese nombre
  • La segunda página se renderiza con la fuente equivocada: la fuente actual no sobrevive a AddPage. Repita la llamada a SetFont, charset incluido, después de cada salto de página
  • Las dobles páginas a doble cara salen reflejadas en un documento mayoritariamente LTR: la primera llamada a RtLTextOut cambió la Direction del documento como efecto secundario. Fije Pdf.Direction := LeftToRight después del fragmento de derecha a izquierda
  • El texto Unicode incrustado se degrada en silencio aguas abajo: algo en la canalización fuerza PDF 1.4, y el manejo de Unicode incrustado de HotPDF necesita 1.5 o posterior. Suba la versión del documento o elimine la restricción aguas abajo

Antes de que el formato salga a producción, verifique más allá de mirar: copie el texto de vuelta desde el visor, ejecute la búsqueda dentro del documento, abra el fichero en una máquina sin sus fuentes de desarrollo y ponga un documento real delante de un lector nativo. La lista completa de verificación, el mapa de cobertura por escritura y el corpus de cadenas de prueba que merece la pena construir están en el artículo complementario sobre shaping de texto árabe y RTL con HotPDF

Las llamadas RtLTextOut, SetFont y RegisterUnicodeTTF mostradas aquí forman parte del componente HotPDF para Delphi para Delphi y C++Builder