Artigo Técnico

RtLTextOut no HotPDF: Texto PDF da Direita para a Esquerda

Envie a frase árabe يوضح ملف PDF هذا ao TextOut simples e a página que volta está errada de duas maneiras ao mesmo tempo. As palavras correm da esquerda para a direita em vez da direita para a esquerda, e as letras ficam separadas nas suas formas isoladas em vez de se ligarem em palavras. Nada dá erro. O Delphi compila, o ficheiro abre, e um revisor que lê árabe diz-lhe que o resultado é inutilizável. A correção é uma chamada, não uma troca de biblioteca: o HotPDF encaminha o texto da direita para a esquerda por um método separado, RtLTextOut, que trata da reordenação que o TextOut simples não faz. Esta página é a referência de trabalho desse método: a assinatura e os seus parâmetros, o argumento de charset que seleciona a escrita, o efeito secundário ao nível do documento, a preparação da fonte que tem de vir primeiro, e as falhas que chegam mesmo ao suporte, cada uma com a sua correção

Assinatura e 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 ancoram a sequência no sistema de coordenadas da própria página, medido a partir do canto inferior esquerdo com o Y a crescer para cima, a mesma origem que qualquer chamada a TextOut usa; o RtLTextOut muda a ordem dos glifos, não o ponto a partir do qual a página mede. O angle roda a linha de base exatamente como no TextOut, pelo que 0 desenha uma linha horizontal. O Text é a cadeia de caracteres em ordem lógica, a ordem pela qual a escreveria, e a segunda sobrecarga recebe os mesmos dados UTF-16 como um buffer PWORD em bruto com uma contagem explícita de unidades de código, que é a forma a usar quando o texto chega de uma API em vez de uma string Delphi. Em versões antigas do Delphi anteriores à resolução de sobrecargas para estes tipos, a forma com string é exposta sob o nome RtLTextOutStr, com a lista de parâmetros idêntica

A divisão de trabalho entre as duas chamadas de saída é estrita. O TextOut desenha os pontos de código pela ordem em que os passa, o que está correto para latino, cirílico e CJK e errado para árabe e hebraico. O RtLTextOut reordena primeiro cada linha para a ordem visual da direita para a esquerda, e só depois desenha, mantendo as palavras latinas e os algarismos incorporados a ler-se da esquerda para a direita dentro da linha. O HotPDF mantém os dois métodos deliberadamente separados em vez de adivinhar a direção a partir dos caracteres, pelo que a escolha de qual chamar é a escolha do comportamento de escrita que obtém; use RtLTextOut para sequências da direita para a esquerda, TextOut para tudo o resto, e nunca encaminhe um pelo outro. Porque é que a reordenação existe, o que o Algoritmo Bidirecional Unicode e a ligação contextual árabe fazem de facto, e onde a composição do HotPDF para são o assunto do artigo companheiro sobre composição de texto árabe e RTL com o HotPDF; tudo o que se segue é a preparação prática

Diagrama de como o RtLTextOut reordena uma linha mista de arabe e latino para a ordem visual da direita para a esquerda antes de a desenhar num PDF
O RtLTextOut reordena cada linha para a ordem visual antes de desenhar: as sequências da direita para a esquerda mantêm a sua ordem enquanto as palavras latinas e os algarismos incorporados se leem da esquerda para a direita dentro da linha

O argumento de charset decide a escrita

O que diz ao RtLTextOut se está a compor árabe ou hebraico não é o método, é a fonte. O SetFont recebe um charset do Windows como quarto argumento, e esse valor leva as regras da escrita até à chamada da direita para a esquerda: 178 seleciona árabe, 177 seleciona hebraico. Defina o charset, depois desenhe, e as duas linhas abaixo saem na ordem de leitura correta sem qualquer configuração adicional

// Árabe: o charset 178 diz ao RtLTextOut para aplicar regras árabes
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

// Hebraico: o charset 177 muda as regras para hebraico
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 660, 0, 'קובץ PDF זה');

Há um detalhe de sequência fácil de esquecer: o SetFont tem de vir primeiro e tem de ser repetido depois de cada AddPage, porque a fonte atual, charset incluído, não sobrevive a uma mudança de página. Esqueça a repetição e a segunda página recai na fonte que estivesse ativa, o que para árabe costuma significar caixas vazias

Não inverte texto que já inverteu

O único erro que aqui consome mais tempo de depuração é entregar ao RtLTextOut uma cadeia de caracteres que já inverteu à mão. As pessoas chegam a este método depois de uma primeira tentativa com o TextOut simples ter saído ao contrário, e um remendo comum é inverter os caracteres em código antes de desenhar. O RtLTextOut inverte internamente por si próprio, pelo que uma cadeia previamente invertida é invertida uma segunda vez e volta ao ponto de partida. Passe o texto em ordem lógica, a ordem pela qual o escreveria e o leria em voz alta, e deixe a chamada fazer a reordenação

A armadilha é mais traiçoeira do que uma simples inversão, porque uma cadeia duplamente invertida pode parecer correta numa frase de teste toda em árabe e depois partir-se no instante em que uma linha traz uma palavra latina ou um número. Dentro de uma linha da direita para a esquerda, essas sequências incorporadas devem ler-se da esquerda para a direita, e a inversão manual estraga esse aninhamento enquanto o caso do árabe puro por acaso lhe sobrevive. Assim, o erro passa incólume pelo primeiro teste rápido e aparece mais tarde numa fatura real com um número de conta. Elimine todas as inversões manuais no momento em que passar para o RtLTextOut

O efeito secundário no Direction que vale a pena conhecer

Chamar o RtLTextOut muda mais do que a linha que está a desenhar. Muda também a preferência de direção de leitura do documento para a direita para a esquerda, o mesmo que de outro modo definiria através da propriedade Direction. Esse setter acrescenta vpDirection às ViewerPreferences do documento, o que diz a um visualizador como dispor páginas duas a duas e de que lado começa um layout de páginas ao lado. Quando o documento inteiro é árabe ou hebraico, é exatamente isto que quer, e sai de graça

Vale a pena conhecê-lo precisamente porque é invisível numa página única. Se o documento for maioritariamente da esquerda para a direita com um bloco da direita para a esquerda, a primeira chamada a RtLTextOut vira na mesma a preferência de todo o ficheiro, e nada na sua prova de uma página o mostrará. O sintoma aparece semanas depois, quando alguém imprime um caderno frente e verso e as páginas ao lado saem espelhadas. Se não é isso que quer, reponha o Direction explicitamente depois da sequência da direita para a esquerda:

// O RtLTextOut já definiu a direção do documento como RightToLeft;
// reponha da esquerda para a direita se o documento for sobretudo LTR
Pdf.Direction := LeftToRight;

Para um documento que se lê genuinamente da direita para a esquerda, deixe estar. A ideia é saber que a chamada tem um efeito ao nível de todo o documento para que a surpresa do caderno nunca aconteça

Registe a fonte que distribui, não a que espera estar instalada

Nada da reordenação importa se a fonte não tiver glifos para desenhar. A falha clássica é um relatório que se representa na perfeição na máquina do programador, onde o Arial Unicode MS por acaso está presente, e sai como filas de caixas vazias no servidor de um cliente onde o Windows substituiu em silêncio por uma fonte sem qualquer cobertura de árabe. A cura é deixar de confiar nas fontes instaladas no sistema e registar uma que distribua com a aplicação

// Distribua uma fonte árabe conhecida e registe-a antes de desenhar
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansArabic.ttf');
Pdf.CurrentPage.SetFont('NotoSansArabic', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

Há duas fronteiras que andam a par do registo. Uma fonte trazida por RegisterUnicodeTTF é incorporada, e o tratamento de Unicode incorporado do HotPDF exige o documento em PDF 1.5 ou posterior; isso só morde se algo a jusante insistir em PDF 1.4, mas quando morde a falha é silenciosa. A outra é jurídica e não técnica: os ficheiros TrueType transportam bits de permissão de incorporação, e um tipo de letra que fica bem no ecrã pode estar licenciado de forma que proíbe distribuí-lo dentro de documentos de clientes. Confirme a licença antes de incorporar, não depois de uma queixa

Um exemplo de consola completo

Juntando as peças, eis um programa autónomo que escreve uma página com uma linha em árabe, uma linha em hebraico, e uma linha mista com um nome de produto latino. Cada bloco define o seu charset e depois desenha em ordem lógica

program RtLTextOutDemo;

{$APPTYPE CONSOLE}

uses
  HPDFDoc;   // unit principal do HotPDF

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

    // Um título latino segue pelo caminho normal do TextOut
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(40, 780, 0, 'Right-to-left text with HotPDF');

    // Árabe: charset 178, ordem lógica, o RtLTextOut faz a reordenação
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 720, 0,
      'يوضح ملف PDF هذا كيفية التعامل مع النص العربي.');

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

    // Linha mista: a palavra latina incorporada continua a ler-se da esquerda para a direita
    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.

Execute-o e abra o resultado. As linhas em árabe e em hebraico leem-se da direita para a esquerda, as letras ligam-se onde a escrita as liga, e na última linha o token HotPDF fica da esquerda para a direita dentro da sequência árabe. Esse aninhamento é o resultado bidirecional correto, não um erro, ainda que os revisores de primeira viagem o reportem rotineiramente como tal; o artigo sobre composição ligado acima explica porque é que as regras Unicode o exigem e como redigir os seus critérios de aceitação para que o relatório nunca chegue a ser aberto

Erros comuns e as suas correções

Todas as falhas abaixo já apareceram num pedido de suporte real, e cada uma remonta a uma das secções anteriores

  • O resultado lê-se ao contrário ou baralha-se em linhas mistas — a cadeia de caracteres foi invertida à mão antes da chamada, normalmente um remendo que sobrou de uma tentativa com TextOut. Apague todas as inversões manuais e passe a ordem lógica; o RtLTextOut inverte internamente
  • As letras saem soltas nas formas isoladas — o texto passou pelo TextOut simples, ou o SetFont foi chamado sem um charset da direita para a esquerda. Desenhe com RtLTextOut e passe 178 para árabe ou 177 para hebraico como quarto argumento do SetFont
  • Caixas vazias na máquina do cliente — o Windows substituiu por uma fonte sem cobertura de árabe ou hebraico. Deixe de nomear fontes instaladas; registe um tipo de letra que distribua através de RegisterUnicodeTTF e selecione-o por esse nome com SetFont
  • A segunda página representa com a fonte errada — a fonte atual não sobrevive ao AddPage. Repita a chamada a SetFont, charset incluído, depois de cada mudança de página
  • Páginas frente e verso saem espelhadas num documento sobretudo LTR — a primeira chamada a RtLTextOut virou o Direction do documento como efeito secundário. Defina Pdf.Direction := LeftToRight depois da sequência da direita para a esquerda
  • O texto Unicode incorporado degrada-se em silêncio a jusante — algo no pipeline força PDF 1.4, e o tratamento de Unicode incorporado do HotPDF precisa de 1.5 ou posterior. Suba a versão do documento ou remova a restrição a jusante

Antes de o formato seguir para produção, verifique para lá do olhómetro: copie o texto de volta a partir do visualizador, corra a pesquisa dentro do documento, abra o ficheiro numa máquina sem as suas fontes de desenvolvimento, e ponha um documento genuíno à frente de um leitor nativo. A lista de verificação completa, o mapa de cobertura por escrita e o corpus de cadeias de teste que vale a pena construir estão todos no artigo companheiro sobre composição de texto árabe e RTL com o HotPDF

As chamadas RtLTextOut, SetFont e RegisterUnicodeTTF aqui mostradas fazem parte do HotPDF Delphi Component para Delphi e C++Builder