Articolo tecnico

RtLTextOut in HotPDF: testo PDF da destra a sinistra

Inviate la frase araba يوضح ملف PDF هذا al semplice TextOut e la pagina che ne esce è sbagliata in due modi contemporaneamente. Le parole scorrono da sinistra a destra invece che da destra a sinistra, e le lettere restano separate nelle loro forme isolate invece di unirsi in parole legate. Nulla segnala un errore. Il codice Delphi compila, il file si apre, e un revisore che legge arabo vi dice che il risultato è inutilizzabile. La correzione è una sola chiamata, non un cambio di libreria: HotPDF instrada il testo da destra a sinistra attraverso un metodo separato, RtLTextOut, che si occupa del riordino che il semplice TextOut non esegue. Questa pagina è il riferimento operativo di quel metodo: la firma e i suoi parametri, il parametro charset che seleziona la scrittura, l'effetto collaterale a livello di documento, la preparazione del font che deve venire prima e i malfunzionamenti che arrivano davvero al supporto, ciascuno con la sua soluzione

Firma e parametri

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 ancorano il tratto nel sistema di coordinate proprio della pagina, misurato dall'angolo in basso a sinistra con la Y che cresce verso l'alto, la stessa origine che usa ogni chiamata TextOut; RtLTextOut cambia l'ordine dei glifi, non il punto da cui la pagina misura. angle ruota la linea di base esattamente come in TextOut, quindi 0 disegna una riga orizzontale. Text è la stringa in ordine logico, l'ordine in cui la scrivereste, e il secondo overload accetta gli stessi dati UTF-16 come buffer PWORD grezzo con un conteggio esplicito di unità di codice, che è la forma da usare quando il testo arriva da una API anziché da una stringa Delphi. Sulle versioni di Delphi più vecchie, che precedono la risoluzione degli overload per questi tipi, la forma con stringa è esposta con il nome RtLTextOutStr e una lista di parametri identica

La divisione dei compiti fra le due chiamate di output è netta. TextOut disegna i codepoint nell'ordine in cui li passate, che è corretto per latino, cirillico e CJK e sbagliato per arabo ed ebraico. RtLTextOut riordina prima ogni riga in ordine visivo da destra a sinistra, poi disegna, mantenendo le parole latine e le cifre incorporate leggibili da sinistra a destra dentro la riga. HotPDF tiene i due metodi deliberatamente separati invece di indovinare la direzione dai caratteri, quindi la scelta di quale chiamare è la scelta del comportamento di scrittura che ottenete; usate RtLTextOut per i tratti da destra a sinistra, TextOut per tutto il resto, e non fate mai passare uno attraverso l'altro. Perché il riordino esista, che cosa facciano davvero l'algoritmo bidirezionale Unicode e la giunzione contestuale araba, e dove si ferma lo shaping di HotPDF sono il tema dell'articolo di accompagnamento sul rendering di arabo e testo RTL con HotPDF; tutto quello che segue è la preparazione pratica

Diagramma di come RtLTextOut riordina una riga mista di arabo e latino in ordine visivo da destra a sinistra prima di disegnarla in un PDF
RtLTextOut riordina ogni riga in ordine visivo prima di disegnare: i tratti da destra a sinistra mantengono la loro sequenza mentre le parole latine e le cifre incorporate si leggono da sinistra a destra dentro la riga

Il parametro charset decide la scrittura

Ciò che dice a RtLTextOut se sta impaginando arabo o ebraico non è il metodo, è il font. SetFont accetta un charset Windows come quarto argomento, e quel valore porta le regole della scrittura dentro la chiamata da destra a sinistra: 178 seleziona l'arabo, 177 seleziona l'ebraico. Impostate il charset, poi disegnate, e le due righe qui sotto escono nel corretto ordine di lettura senza altra configurazione

// Arabo: il charset 178 dice a RtLTextOut di applicare le regole arabe
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

// Ebraico: il charset 177 passa alle regole ebraiche
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 660, 0, 'קובץ PDF זה');

Un dettaglio di sequenza è facile da mancare: il SetFont deve venire prima e va ripetuto dopo ogni AddPage, perché il font corrente, charset incluso, non sopravvive a un cambio di pagina. Dimenticate la ripetizione e la seconda pagina ripiega su qualunque font fosse attivo, il che per l'arabo di solito significa riquadri vuoti

Non inverte il testo che avete già invertito

L'errore singolo che qui divora più tempo di debug è passare a RtLTextOut una stringa che avete già capovolto a mano. Si arriva a questo metodo dopo che un primo tentativo con il semplice TextOut è uscito al contrario, e un ripiego comune è invertire i caratteri nel codice prima di disegnare. RtLTextOut inverte internamente per conto suo, quindi una stringa pre-invertita viene invertita una seconda volta e torna esattamente al punto di partenza. Passate il testo in ordine logico, l'ordine in cui lo scrivereste e lo leggereste ad alta voce, e lasciate che sia la chiamata a fare il riordino

La trappola è più insidiosa di un semplice capovolgimento, perché una stringa invertita due volte può sembrare corretta su una frase di prova tutta in arabo e poi rompersi nell'istante in cui una riga porta una parola latina o un numero. Dentro una riga da destra a sinistra quei tratti incorporati devono leggersi da sinistra a destra, e l'inversione manuale distrugge quell'annidamento mentre il caso di solo arabo per puro caso sopravvive. Così il difetto supera il primo smoke test e riemerge più tardi su una fattura reale che contiene un numero di conto. Eliminate ogni inversione manuale nel momento in cui passate a RtLTextOut

L'effetto collaterale su Direction che vale la pena conoscere

Chiamare RtLTextOut cambia più della riga che state disegnando. Ribalta anche la preferenza di direzione di lettura del documento verso destra-sinistra, la stessa cosa che altrimenti impostereste voi tramite la proprietà Direction. Quel setter aggiunge vpDirection alle ViewerPreferences del documento, che dicono a un visualizzatore come disporre le doppie pagine e da quale lato parte un layout a pagine affiancate. Quando l'intero documento è in arabo o in ebraico questo è esattamente ciò che volete, e lo ottenete gratis

Vale la pena saperlo proprio perché su una pagina sola è invisibile. Se il documento è per lo più da sinistra a destra con un unico blocco da destra a sinistra, la prima chiamata a RtLTextOut ribalterà comunque la preferenza dell'intero file, e nulla nella vostra prova a una pagina lo mostrerà. Il sintomo compare settimane dopo, quando qualcuno stampa un opuscolo fronte-retro e le doppie pagine escono speculari. Se non è quello che volete, riportate Direction in modo esplicito dopo il tratto da destra a sinistra:

// RtLTextOut ha già impostato la direzione del documento su RightToLeft;
// ripristinate da sinistra a destra se il documento è prevalentemente LTR
Pdf.Direction := LeftToRight;

Per un documento che davvero si legge da destra a sinistra, lasciatelo stare. Il punto è sapere che la chiamata ha un effetto su tutto il documento, così la sorpresa dell'opuscolo non capita mai

Registrate il font che distribuite, non quello che sperate sia installato

Nessun riordino conta se il font non ha glifi da disegnare. Il classico fallimento è un report che si rende in modo impeccabile sulla macchina dello sviluppatore, dove Arial Unicode MS è per caso presente, ed esce come file di riquadri vuoti sul server di un cliente dove Windows ha silenziosamente sostituito un font privo di qualsiasi copertura araba. La cura è smettere di fidarsi dei font di sistema installati e registrarne uno che distribuite insieme all'applicazione

// Distribuite un font arabo noto e registratelo prima di disegnare
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansArabic.ttf');
Pdf.CurrentPage.SetFont('NotoSansArabic', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

Con la registrazione viaggiano due limiti. Un font introdotto tramite RegisterUnicodeTTF viene incorporato, e la gestione Unicode incorporata di HotPDF richiede il documento in PDF 1.5 o successivo; questo morde solo se qualcosa a valle insiste su PDF 1.4, ma quando succede il fallimento è silenzioso. L'altro è di natura legale più che tecnica: i file TrueType portano bit di permesso per l'incorporamento, e un carattere che a schermo sembra a posto può essere licenziato in modo da vietarne la distribuzione dentro i documenti dei clienti. Verificate la licenza prima di incorporare, non dopo un reclamo

Un esempio console completo

Mettendo insieme i pezzi, ecco un programma autonomo che scrive una pagina con una riga in arabo, una in ebraico e una riga mista che porta un nome di prodotto latino. Ogni blocco imposta il proprio charset, poi disegna in ordine logico

program RtLTextOutDemo;

{$APPTYPE CONSOLE}

uses
  HPDFDoc;   // unità principale di HotPDF

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

    // Un titolo latino passa per il normale percorso TextOut
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(40, 780, 0, 'Right-to-left text with HotPDF');

    // Arabo: charset 178, ordine logico, RtLTextOut fa il riordino
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 720, 0,
      'يوضح ملف PDF هذا كيفية التعامل مع النص العربي.');

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

    // Riga mista: la parola latina incorporata si legge ancora da sinistra a destra
    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.

Eseguitelo e aprite il risultato. Le righe in arabo e in ebraico si leggono da destra a sinistra, le lettere si uniscono dove la scrittura le unisce, e nell'ultima riga il token HotPDF sta da sinistra a destra dentro il tratto arabo. Quell'annidamento è il risultato bidirezionale corretto, non un difetto, anche se i revisori alle prime armi lo segnalano regolarmente come tale; l'articolo sullo shaping collegato sopra spiega perché le regole Unicode lo impongono e come formulare i criteri di accettazione in modo che la segnalazione non venga mai aperta

Errori comuni e relative soluzioni

Ogni malfunzionamento qui sotto è comparso in un vero thread di supporto, e ciascuno risale a una delle sezioni precedenti

  • Il risultato si legge al contrario o si scompiglia sulle righe miste — la stringa è stata invertita a mano prima della chiamata, di solito un rimasuglio di un tentativo con TextOut. Cancellate ogni inversione manuale e passate l'ordine logico; RtLTextOut inverte internamente
  • Le lettere si stampano staccate in forme isolate — il testo è passato per il semplice TextOut, oppure SetFont è stato chiamato senza un charset da destra a sinistra. Disegnate con RtLTextOut e passate 178 per l'arabo o 177 per l'ebraico come quarto argomento di SetFont
  • Riquadri vuoti sulla macchina del cliente — Windows ha sostituito un font privo di copertura araba o ebraica. Smettete di nominare font installati; registrate un carattere che distribuite tramite RegisterUnicodeTTF e richiamatelo con SetFont usando quel nome
  • La seconda pagina si rende con il font sbagliato — il font corrente non sopravvive ad AddPage. Ripetete la chiamata a SetFont, charset incluso, dopo ogni cambio di pagina
  • Le doppie pagine fronte-retro si stampano speculari su un documento prevalentemente LTR — la prima chiamata a RtLTextOut ha ribaltato Direction del documento come effetto collaterale. Impostate Pdf.Direction := LeftToRight dopo il tratto da destra a sinistra
  • Il testo Unicode incorporato degrada silenziosamente a valle — qualcosa nella pipeline forza PDF 1.4, e la gestione Unicode incorporata di HotPDF richiede 1.5 o successivo. Alzate la versione del documento o rimuovete il vincolo a valle

Prima che il formato entri in produzione, verificate oltre il colpo d'occhio: ricopiate il testo fuori dal visualizzatore, eseguite la ricerca interna al documento, aprite il file su una macchina priva dei vostri font di sviluppo e mettete un documento autentico davanti a un lettore madrelingua. La lista di verifica completa, la mappa di copertura per scrittura e il corpus di stringhe di prova che vale la pena costruire stanno tutti nell'articolo di accompagnamento sul rendering di arabo e testo RTL con HotPDF

Le chiamate RtLTextOut, SetFont e RegisterUnicodeTTF mostrate qui fanno parte del HotPDF Delphi Component per Delphi e C++Builder