Articolo tecnico

RtLTextOut in HotPDF: testo PDF da destra a sinistra in Delphi

Invia la frase araba يوضح ملف PDF هذا al semplice TextOut e la pagina che ottieni è sbagliata in due modi contemporaneamente. Le parole scorrono da sinistra a destra invece che da destra a sinistra, e le lettere rimangono separate nelle loro forme isolate invece di unirsi in parole collegate. Non si verifica alcun errore. Il codice Delphi compila, il file si apre, e un revisore che legge l'arabo ti dice che l'output è inutilizzabile. La soluzione è una singola chiamata, non uno scambio di libreria: HotPDF instrada il testo da destra a sinistra attraverso un metodo separato, RtLTextOut, che gestisce il riordinamento che il semplice TextOut non fa. Quattro aspetti di quel metodo determinano se l'output è utilizzabile: cosa fa alla stringa, come il suo argomento charset seleziona la scrittura, la modifica a livello di documento che produce come effetto collaterale e il lavoro sul font che deve essere fatto prima.

Perché il testo da destra a sinistra richiede una chiamata propria

Un flusso di contenuto PDF non memorizza testo modificabile. Memorizza glifi a posizioni fisse, il che significa che chiunque emetta il flusso ha il compito di decidere in quale ordine vadano quei glifi. Sullo schermo il sistema operativo ha fatto quel lavoro per te: inserisci l'arabo in un TEdit e lo stack di testo del SO lo riordina e lo connette prima che tu veda un singolo pixel. È esattamente per questo che la stringa appare perfetta nel tuo form e si rompe nel PDF. Il desktop ha fatto il lavoro silenziosamente, e nel momento in cui scrivi il tuo flusso di contenuto, il lavoro torna dalla tua parte.

TextOut ti prende in parola. Disegna i codepoint nell'ordine in cui li passi, da sinistra a destra, il che è corretto per il latino, il cirillico e il CJK e sbagliato per l'arabo e l'ebraico. RtLTextOut è la chiamata che riordina prima la riga nell'ordine visivo da destra a sinistra, poi disegna. HotPDF mantiene i due metodi deliberatamente separati piuttosto che indovinare la direzione dai caratteri, quindi la scelta di quale chiamare è la scelta del comportamento della scrittura che si ottiene. La meccanica più profonda del riordinamento bidirezionale e della giunzione contestuale araba è un argomento a sé stante, trattato nell'articolo sulla composizione del testo arabo e RTL con HotPDF; qui il punto pratico è più circoscritto. Usa RtLTextOut per le sequenze da destra a sinistra, usa TextOut per tutto il resto, e non instradare mai l'uno attraverso l'altro.

Diagramma di come RtLTextOut riordina una riga mista arabo-latino nell'ordine visivo da destra a sinistra prima di disegnarla in un PDF
RtLTextOut riordina ogni riga in ordine visivo prima di disegnarla: le sequenze da destra a sinistra mantengono la loro sequenza mentre le parole latine e le cifre incorporate si leggono da sinistra a destra all'interno della riga.

L'argomento charset decide la scrittura

Ciò che dice a RtLTextOut se sta componendo arabo o ebraico non è il metodo, è il font. SetFont prende un charset Windows come quarto argomento, e quel valore porta le regole della scrittura nella chiamata da destra a sinistra: 178 seleziona l'arabo, 177 seleziona l'ebraico. Imposta il charset, poi disegna, e le due righe seguenti escono nell'ordine di lettura corretto senza alcuna ulteriore configurazione.

// Arabic: charset 178 tells RtLTextOut to apply Arabic rules
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

// Hebrew: charset 177 switches the rules to Hebrew
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 660, 0, 'קובץ PDF זה');

Due dettagli riguardanti quelle coordinate sono facili da trascurare. La posizione che passi è ancora l'inizio della sequenza nel sistema di coordinate proprio della pagina, misurato dall'angolo in basso a sinistra con Y che cresce verso l'alto, la stessa origine che usa ogni TextOut; RtLTextOut cambia l'ordine dei glifi, non il punto di misura della pagina. E come per qualsiasi chiamata di disegno, il SetFont deve venire prima e deve essere ripetuto dopo ogni AddPage, perché il font corrente non sopravvive a un'interruzione di pagina. Dimentica la ripetizione e la seconda pagina torna a qualunque font fosse attivo, che per l'arabo di solito significa caselle vuote.

Non inverte il testo che hai già invertito

Il singolo errore che consuma più tempo di debug qui è passare a RtLTextOut una stringa che hai già invertito a mano. Le persone raggiungono questo metodo dopo un primo tentativo con il semplice TextOut che è uscito al contrario, e un rimedio comune è invertire i caratteri nel codice prima di disegnare. RtLTextOut inverte internamente da solo, quindi una stringa pre-invertita viene invertita una seconda volta e torna esattamente da dove era partita. Passa il testo nell'ordine logico, l'ordine in cui lo digiteresti e lo leggeresti ad alta voce, e lascia che la chiamata faccia il riordinamento.

La trappola è più insidiosa di un semplice ribaltamento perché una stringa doppiamente invertita può sembrare corretta per una frase di test tutta in arabo e poi rompersi nel momento in cui una riga porta una parola latina o un numero. All'interno di una riga da destra a sinistra quelle sequenze incorporate devono essere lette da sinistra a destra, e l'inversione manuale distrugge quel nesting mentre il caso puro-arabo riesce per caso a sopravvivere. Così il bug supera il primo test rapido e si manifesta in seguito su una vera fattura con un numero di conto. Elimina ogni inversione manuale nel momento in cui passi a RtLTextOut.

L'effetto collaterale Direction da conoscere

Chiamare RtLTextOut cambia più della riga che stai disegnando. Cambia anche la preferenza di direzione di lettura del documento a destra a sinistra, la stessa cosa che altrimenti imposteresti tu stesso attraverso la proprietà Direction. Quel setter aggiunge vpDirection alle ViewerPreferences del documento, che dice a un visualizzatore come disporre i fogli doppi e da quale lato inizia un layout a pagine affiancate. Quando l'intero documento è in arabo o ebraico questo è esattamente ciò che vuoi, e lo ottieni gratuitamente.

Vale la pena saperlo precisamente perché è invisibile su una singola pagina. Se il documento è prevalentemente da sinistra a destra con un solo blocco da destra a sinistra, la prima chiamata RtLTextOut inclinerà comunque la preferenza dell'intero file, e nulla nella tua prova a pagina singola lo mostrerà. Il sintomo appare settimane dopo quando qualcuno stampa un libretto fronte-retro e i fogli escono specchiati. Se non è quello che vuoi, reimpostare Direction esplicitamente dopo la sequenza da destra a sinistra:

// RtLTextOut already set the document direction to RightToLeft;
// restore left-to-right if the document is predominantly LTR
Pdf.Direction := LeftToRight;

Per un documento che si legge genuinamente da destra a sinistra, lascialo stare. Il punto è sapere che la chiamata ha un effetto a livello di documento in modo che la sorpresa del libretto non accada mai.

Registra il font che distribuisci, non quello che speri sia installato

Nessuno del riordinamento conta se il font non ha glifi da disegnare. Il classico fallimento è un report che viene renderizzato perfettamente sulla macchina dello sviluppatore, dove Arial Unicode MS è presente, e che esce come righe di caselle vuote sul server di un cliente dove Windows ha silenziosamente sostituito un font senza alcuna copertura araba. La cura è smettere di fidarsi dei font di sistema installati e registrarne uno che distribuisci con l'applicazione.

// Ship a known Arabic font and register it before drawing
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansArabic.ttf');
Pdf.CurrentPage.SetFont('NotoSansArabic', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

Due vincoli si accompagnano alla registrazione. Un font inserito tramite RegisterUnicodeTTF viene incorporato, e la gestione Unicode incorporata di HotPDF richiede che il documento sia in PDF 1.5 o successivo; questo morde solo se qualcosa a valle insiste su PDF 1.4, ma quando lo fa il fallimento è silenzioso. L'altro è legale anziché tecnico: i file TrueType portano bit di permesso di incorporamento, e un typeface che sembra a posto sullo schermo può essere concesso in licenza in un modo che vieta di distribuirlo all'interno dei documenti dei clienti. Verifica la licenza prima di incorporare, non dopo una segnalazione.

Un esempio completo da console

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

program RtLTextOutDemo;

{$APPTYPE CONSOLE}

uses
  HPDFDoc;   // HotPDF main unit

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

    // A Latin heading goes through the ordinary TextOut path
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(40, 780, 0, 'Right-to-left text with HotPDF');

    // Arabic: charset 178, logical order, RtLTextOut does the reordering
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 720, 0,
      'يوضح ملف PDF هذا كيفية التعامل مع النص العربي.');

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

    // Mixed line: the embedded Latin word still reads left to right
    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.

Eseguilo e apri il risultato. Le righe araba ed ebraica si leggono da destra a sinistra, le lettere si uniscono dove la scrittura le unisce, e nell'ultima riga il token HotPDF si trova da sinistra a destra all'interno della sequenza araba, che è il risultato corretto secondo la specifica anche se sorprende chiunque veda il layout bidirezionale per la prima volta. Quest'ultimo punto vale la pena inserirlo nei tuoi criteri di accettazione prima che un lettore madrelingua riveda l'output, perché la sequenza incorporata che si legge nella direzione "sbagliata" rispetto alla scrittura circostante è la cosa che più spesso viene segnalata come bug quando non lo è.

Verificare l'output

Una pagina che sembra giusta non è la stessa cosa di una pagina che è giusta, quindi verificala nel modo in cui lo farà un sistema a valle. Copia il testo dal visualizzatore e confronta i codepoint con la stringa sorgente; l'ordine visivo corretto con l'ordine logico confuso è una reale modalità di errore. Esegui la ricerca nel documento del visualizzatore per una parola che puoi vedere sulla pagina. Poi apri il file su una macchina che non ha i tuoi font di sviluppo, quella più adatta a esporre una sostituzione silenziosa. Nulla di tutto ciò sostituisce un lettore madrelingua che legge un documento genuino, il che intercetta problemi che nessuna stringa di test sintetica troverà, quindi metti quella revisione in calendario prima che il formato vada in distribuzione.

RtLTextOut gestisce il riordinamento bidirezionale e la giunzione contestuale araba, il che copre la grande maggioranza del lavoro con report e documenti da destra a sinistra. Dove si ferma, le scritture che richiedono più del riordinamento e della giunzione come le famiglie indiche e le feature OpenType opzionali che passano attraverso la sostituzione di singoli glifi, è trattato insieme ai dettagli di copertura dei glifi e di composizione nell'articolo di accompagnamento sulla composizione del testo arabo e RTL con HotPDF.

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