Artigo Técnico

RtLTextOut no HotPDF: Texto PDF da Direita para a Esquerda no Delphi

Envie a frase em árabe يوضح ملف PDF هذا para o simples TextOut e a página que retornará estará errada de duas formas ao mesmo tempo. As palavras correm da esquerda para a direita em vez da direita para a esquerda, e as letras ficam separadas em suas formas isoladas em vez de se unirem em palavras conectadas. Nenhum erro ocorre. O Delphi compila, o arquivo abre, e um revisor que lê árabe lhe diz que o resultado é inutilizável. A correção é uma chamada, e não uma troca de biblioteca: o HotPDF roteia texto da direita para a esquerda por meio de um método separado, RtLTextOut, que gerencia o reordenamento que o TextOut não faz. Esta página é a referência prática para esse método: a assinatura e seus parâmetros, o argumento charset que seleciona a escrita, o efeito colateral no nível do documento, a configuração da fonte que precisa vir antes e as falhas que realmente chegam 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 o fluxo no próprio sistema de coordenadas da página, medido a partir do canto inferior esquerdo com o Y crescendo para cima, a mesma origem que todas as chamadas TextOut utilizam; o RtLTextOut altera a ordem dos glifos, não de onde a página faz as medições. O angle gira a linha de base exatamente como no TextOut, portanto, 0 desenha uma linha horizontal. Text é a string na ordem lógica, a ordem em que você a digitaria, e a segunda sobrecarga (overload) recebe os mesmos dados UTF-16 como um buffer bruto PWORD com uma contagem explícita da unidade de código, que é a forma de se usar quando o texto chega de uma API ao invés de uma string do Delphi. Em versões mais antigas do Delphi que antecedem a resolução de sobrecarga para esses tipos, a forma de string é exposta sob o nome RtLTextOutStr com a mesma lista de parâmetros

A divisão de trabalho entre as duas chamadas de saída é rigorosa. O TextOut desenha os codepoints na ordem em que você os passa, o que é correto para o latim, cirílico e CJK, mas errado para o árabe e o hebraico. O RtLTextOut primeiro reordena cada linha para a ordem visual da direita para a esquerda e, depois, desenha, mantendo as palavras e os dígitos em latim incorporados para serem lidos 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, para que a escolha de qual chamar seja a escolha de qual comportamento de escrita você obterá; utilize o RtLTextOut para fluxos da direita para a esquerda, o TextOut para todo o resto e nunca direcione um por meio do outro. O motivo pelo qual o reordenamento sequer existe, o que o Algoritmo Bidirecional do Unicode e a união contextual árabe realmente fazem, e onde termina a modelagem do HotPDF são os assuntos do artigo complementar Modelagem de texto árabe e RTL com HotPDF; tudo que se encontra abaixo é a configuração prática

Diagrama de como o RtLTextOut reordena uma linha mista de árabe e latim em uma ordem visual da direita para a esquerda antes de desenhá-la em um PDF
O RtLTextOut reordena cada linha para a ordem visual antes do desenho: as sequências da direita para a esquerda mantêm sua sequência, enquanto palavras em latim e dígitos incorporados são lidos da esquerda para a direita dentro da linha.

O argumento de charset decide a escrita (script)

O que diz a RtLTextOut se ela deve criar um layout em árabe ou hebraico não é o método, mas a fonte. SetFont aceita um charset do Windows como o seu quarto argumento, e esse valor leva as regras de escrita para dentro da chamada da direita para a esquerda: 178 seleciona o árabe, 177 seleciona o hebraico. Defina o charset e desenhe, e as duas linhas a seguir sairão na ordem de leitura correta sem nenhuma configuração adicional

// 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 זה');

É fácil deixar passar um detalhe de sequenciamento: o SetFont deve vir primeiro e precisa ser repetido após cada AddPage, porque a fonte atual, inclusive o charset, não sobrevive a uma quebra de página. Se você esquecer a repetição, a segunda página recairá na fonte que estava ativa, que para o árabe normalmente significa caixas vazias

Ele não inverte textos que você já tenha invertido

O único erro que engole a maior parte do tempo de depuração aqui é fornecer a RtLTextOut uma string que você já inverteu manualmente. As pessoas chegam a esse método após uma primeira tentativa com o simples TextOut que saiu de trás para frente, e uma medida provisória muito comum é inverter os caracteres no código antes de desenhar. A RtLTextOut inverte por conta própria internamente, e portanto, uma string pré-invertida será invertida uma segunda vez e cairá bem de volta onde começou. Passe o texto na ordem lógica, ou seja, na ordem em que você o digitaria e leria em voz alta, e deixe que a chamada efetue o reordenamento

A armadilha é mais grave do que uma simples inversão, pois uma string invertida duas vezes pode parecer correta para uma frase de teste toda em árabe e, em seguida, falhar no momento em que a linha trouxer uma palavra em latim ou um número. Dentro de uma linha da direita para a esquerda, os fluxos incorporados devem ser lidos da esquerda para a direita e a inversão manual acaba com esse aninhamento, embora o caso puramente árabe acabe sobrevivendo a ela. Assim, o erro passará tranquilamente em seu primeiro smoke test e aparecerá depois, em uma fatura verdadeira que tiver um número de conta dentro. Livre-se de toda inversão manual assim que você passar a utilizar a RtLTextOut

O efeito colateral de direção que vale a pena conhecer

Chamar a RtLTextOut muda mais coisas do que apenas a linha que você está desenhando. Ele também vira a preferência de direção de leitura do documento da direita para a esquerda, da mesma forma que você configuraria por conta própria por meio da propriedade Direction. Esse setter adiciona a vpDirection a ViewerPreferences do documento, que diz a um visualizador como as páginas devem se organizar lado a lado e de qual lado um layout de páginas espelhadas é iniciado. Quando o documento inteiro for em árabe ou hebraico, isto é exatamente o que você irá desejar, e você obtém isso de graça

Isso vale a pena ser conhecido justamente porque é invisível em uma página única. Se a maior parte do documento é da esquerda para a direita com um bloco da direita para a esquerda, a primeira chamada de RtLTextOut inclinará ainda assim a preferência do arquivo inteiro, e nada em seu modelo provisório de uma página mostrará isso. O sintoma aparecerá semanas depois, quando alguém imprimir um folheto frente e verso (duplex) e as páginas espelhadas saírem invertidas (refletidas). Se isto não for o que você quiser, devolva a propriedade Direction explicitamente de volta para o normal depois do fluxo da direita para a esquerda:

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

Para um documento que é genuinamente lido da direita para a esquerda, deixe a propriedade em paz. O ponto é saber que a chamada possui um efeito que abrange o documento inteiro, de forma que a surpresa no folheto nunca chegue a acontecer

Registre a fonte que você envia, não aquela que você espera estar instalada

O reordenamento de nada adiantará se a fonte não tiver os glifos necessários a se desenhar. A falha clássica é um relatório que é perfeitamente renderizado na máquina do desenvolvedor em que a Arial Unicode MS encontra-se presente, mas que sairá no servidor do cliente como fileiras de caixas vazias, onde o Windows substituiu silenciosamente por uma fonte que não abrange nada em árabe. A solução é parar de confiar em fontes de sistemas instaladas e registrar uma que você envie junto ao aplicativo

// 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 هذا');

Dois limites caminham de mãos dadas com o registro. Uma fonte trazida através de RegisterUnicodeTTF fica incorporada e a manipulação da Unicode incorporada ao HotPDF exige que o documento esteja num PDF a partir da versão 1.5; isto só virará um problema se algo downstream insistir no PDF 1.4, mas caso isso venha a ocorrer, a falha acontecerá de forma silenciosa. O outro problema é um problema jurídico e não técnico: os arquivos TrueType são arquivos que trazem consigo os bits de permissão de incorporação, e um formato que na tela parece correto, muitas vezes pode estar licenciado de tal maneira que não permite que você os entregue dentro de um documento a seus clientes. Certifique-se da licença antes de incorporar e não apenas após ter recebido alguma reclamação

Um exemplo completo de console

Unindo as peças, temos aqui um programa contido e autossuficiente que escreve em uma página, uma linha em árabe, uma linha em hebraico e uma linha mesclada que traz um nome de produto em latim. Cada bloco define seu charset e os desenha em ordem lógica

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.

Execute e abra o resultado. As linhas em árabe e em hebraico são lidas da direita para a esquerda, e a união entre letras acontece exatamente onde a escrita requer a sua união; e na última linha o token HotPDF senta-se da esquerda para a direita dentro do fluxo em árabe. Esse aninhamento é o resultado bidirecional correto, não um bug, muito embora os revisores de primeira viagem costumeiramente o classifiquem como tal; o artigo sobre modelagem mencionado acima explica por que as regras do Unicode o exigem e como redigir seus critérios de aceitação para que esse relato de erro nunca seja feito

Erros comuns e suas soluções

Todas as falhas descritas abaixo já apareceram em tópicos de suporte de verdade e, para cada uma delas, é possível remontar os passos para as seções acima

  • A saída é lida de trás para a frente ou fica embaralhada em linhas mistas — a string foi invertida manualmente antes da chamada, normalmente uma solução paliativa remanescente de uma tentativa com TextOut. Exclua todas as inversões manuais e passe a string na ordem lógica; a RtLTextOut inverte internamente
  • As letras são impressas desconectadas e em formas isoladas — o texto passou por um TextOut comum, ou SetFont foi chamado sem um charset da direita para a esquerda. Desenhe com RtLTextOut e passe 178 para o árabe ou 177 para o hebraico como quarto argumento de SetFont
  • Caixas vazias na máquina do cliente — o Windows substituiu por uma fonte sem cobertura para árabe ou hebraico. Pare de nomear fontes instaladas; registre uma fonte que você envia junto com o programa através de RegisterUnicodeTTF e faça o SetFont por esse nome
  • A segunda página é renderizada na fonte errada — a fonte atual não sobrevive ao AddPage. Repita a chamada SetFont, incluindo o charset, após cada quebra de página
  • Folhetos frente e verso (duplex) imprimem espelhados em um documento que é majoritariamente LTR (da esquerda para a direita) — a primeira chamada de RtLTextOut inverteu a propriedade Direction do documento como um efeito colateral. Defina Pdf.Direction := LeftToRight após a execução do trecho da direita para a esquerda
  • Texto Unicode incorporado é degradado silenciosamente no processo a jusante (downstream) — algo no fluxo de trabalho (pipeline) força o formato PDF 1.4, e a manipulação de Unicode incorporado pelo HotPDF precisa da versão 1.5 ou posterior. Eleve a versão do documento ou remova a restrição (constraint) no processo a jusante

Antes que o formato chegue aos clientes, verifique muito além de uma simples olhada: copie o texto de volta do visualizador, faça uma busca dentro do documento, abra o arquivo em uma máquina sem as suas fontes de desenvolvimento e coloque um documento verdadeiro em frente a um leitor nativo da língua. A lista de verificação completa, o mapa de cobertura por escrita (script) e o corpus de strings de teste que valem a pena criar encontram-se todos no artigo complementar sobre Modelagem de texto árabe e RTL com HotPDF

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