Technical Article

Modelação de Texto Árabe e RTL em PDFs Delphi com o HotPDF

Passe a frase em árabe يوضح ملف PDF para TextOut e abra o resultado. As letras surgem na direção errada e cada uma delas apresenta-se na sua forma isolada, com um espaço visível antes da seguinte, como se alguém tivesse escrito em inglês ao contrário e premido a barra de espaço entre cada caractere. Nenhuma exceção foi acionada. Nenhum aviso foi impresso. O resultado está simplesmente incorreto, e isto acontece porque duas transformações distintas de que o árabe depende nunca ocorreram. Saber quais são essas duas transformações e qual a chamada que as executa constitui a maior parte do trabalho na geração de PDFs com scripts complexos.

O HotPDF é um componente nativo de PDF para VCL em Delphi e C++Builder, e realiza o trabalho da direita para a esquerda (RTL) por si através de uma chamada específica. No entanto, também apresenta limitações em alguns pontos específicos que convém conhecer antes de se comprometer com um idioma (locale), pelo que essas restrições terão a sua própria secção perto do fim.

Porque é que uma cadeia de caracteres (string) correta ainda é impressa incorretamente

O Unicode mantém o texto em ordem lógica (a ordem em que é escrito e lido em voz alta). Um renderizador tem de colocar os glifos em ordem visual. Nos scripts da esquerda para a direita, estas ordens coincidem e ninguém pensa nisso. No árabe e no hebraico, isso não acontece, e quando uma única linha mistura direções (por exemplo, uma frase em árabe que contém o termo latino "PDF" ou um preço escrito em algarismos), o Algoritmo Bidirecional Unicode (UAX #9) decide exatamente como os fragmentos da esquerda para a direita se aninham dentro da linha da direita para a esquerda. Esta é a primeira transformação (a reordenação) e ignorá-la é o que inverte a linha.

A segunda transformação é a modelação contextual (contextual shaping). Uma letra árabe é desenhada de forma diferente dependendo da sua posição numa palavra: inicial, média, final ou isolada. O ponto de código (codepoint) permanece o mesmo; apenas o glifo se altera. Um fluxo de processamento que envie cada ponto de código diretamente para o seu glifo padrão produz exatamente o resultado desarticulado e de forma isolada descrito no parágrafo de abertura. O hebraico dispensa este passo, dado que as suas letras não se ligam, mas continua a necessitar da reordenação. O árabe precisa de ambos, e é por isso que o árabe, e não o hebraico, é a string ideal para os seus testes.

No ambiente de trabalho, nada disto é da sua responsabilidade. Quando um formulário VCL desenha árabe num TEdit, a pilha de texto do sistema operativo reordena e modela o texto silenciosamente, razão pela qual a string que parece perfeita no ecrã surge danificada num PDF gerado de forma ingénua. Um fluxo de conteúdo de PDF não armazena texto editável; armazena glifos posicionados. Por isso, quem gera o fluxo herda a tarefa de modelação que o sistema operativo costumava gerir. A função RtLTextOut é a chamada que assume essa tarefa.

O RtLTextOut realiza a reordenação e a ligação

O HotPDF mantém o fluxo de caracteres latinos e o fluxo de scripts complexos como dois métodos distintos. O TextOut imprime o conteúdo na ordem em que é fornecido. O RtLTextOut executa primeiro a reordenação e a análise contextual para depois imprimir. As regras de escrita aplicadas dependem do argumento de charset passado a SetFont: 178 para árabe, 177 para hebraico.

// Arabic: pass logical order; RtLTextOut reorders and joins
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF');

// Hebrew: reordering only, no contextual joining
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 660, 0, 'קובץ PDF זה');

Um erro consome aqui mais horas de depuração do que qualquer outro. O próprio RtLTextOut inverte a string, pelo que, se lhe fornecer texto que já tenha invertido manualmente (geralmente uma solução temporária de uma tentativa anterior com o simples TextOut), ele irá invertê-lo novamente, voltando ao ponto de partida. O aspeto mais problemático é que o texto duplamente invertido pode parecer correto num teste simples de apenas árabe, mas falhará assim que a linha contiver uma palavra latina ou um número, pois esses segmentos inseridos deixarão de seguir o algoritmo UAX #9. Passe sempre a ordem lógica e deixe a função tratar de tudo.

Este comportamento de direções mistas costuma baralhar mais os revisores do que o próprio código. Numa linha escrita da direita para a esquerda, os algarismos e as palavras latinas embutidas continuam a ler-se da esquerda para a direita. Alguém que não esteja familiarizado com esquemas bidirecionais olhará para uma fatura renderizada, verá o número de conta escrito no sentido "errado" em relação ao árabe circundante e reportará o caso como um bug. Trata-se do resultado correto de acordo com a especificação. Uma pequena nota nos seus critérios de aceitação, redigida antes da primeira revisão por um falante nativo, evita este tipo de mal-entendido.

A cobertura de glifos é definida antes do início da modelação

A modelação escolhe glifos a partir de uma fonte. Se a fonte não os possuir, não há nada para selecionar. Este é o tipo de falha de implementação que faz perder uma tarde: o relatório surge perfeito na máquina do programador, onde a fonte Arial Unicode MS está instalada, mas aparece como uma série de retângulos em branco no servidor do cliente, onde o Windows substituiu a fonte por uma que não suporta árabe. A solução consiste em deixar de confiar nas fontes que uma determinada máquina possui e registar uma fonte distribuída juntamente com a aplicação.

// Ship a known font instead of relying on installed system fonts
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansArabic.ttf');
Pdf.CurrentPage.SetFont('NotoSansArabic', [], 12);

// Audit coverage for the codepoints your data actually uses
GID := Pdf.GetUnicodeGlyphForCodepoint($0628);  // U+0628 ARABIC LETTER BEH
LogGlyphAudit($0628, GID);

Existem duas limitações associadas a este processo. Uma fonte registada via RegisterUnicodeTTF é incorporada no ficheiro, e o suporte de Unicode incorporado do HotPDF requer que o documento esteja no formato PDF 1.5 ou posterior. Isto só constitui problema se algum sistema a jusante exigir o PDF 1.4, mas, quando isso acontece, o sintoma é silencioso. A outra limitação é de caráter legal: os ficheiros TrueType contêm bits de permissão de incorporação, e uma tipografia que se desenha perfeitamente no ecrã pode ter uma licença que proíba a sua distribuição dentro de documentos de clientes. Verifique as licenças antes de efetuar o envio, e não após receber reclamações.

A segunda chamada, GetUnicodeGlyphForCodepoint, constitui o seu sistema de aviso prévio. Percorra as gamas de pontos de código que os seus dados utilizam ao iniciar o serviço e registe os IDs de glifo retornados. Qualquer falha de cobertura surgirá como uma linha no registo de inicialização durante a implementação, em vez de se traduzir em caracteres em falta numa fatura que já chegou ao cliente.

O texto que é Unicode mas não é escrito da direita para a esquerda (como strings CJK, vietnamita com os seus diacríticos empilhados ou texto europeu misto) segue o caminho normal. O método TextOut recebe uma WideString e desenha-a através da fonte registada sem qualquer análise bidirecional. É recomendável manter os dois caminhos fisicamente separados no código de relatório (uma rotina para os segmentos RTL e outra para tudo o resto), para que a lógica de localização seja visível no ponto de chamada, em vez de ficar oculta por trás de uma flag que alguém acabará por se esquecer de configurar.

A ordem de leitura pertence ao documento, não aos glifos

Garantir a exatidão de cada glifo ainda deixa uma tarefa por concluir. A norma ISO 32000-1 §12.2 define uma preferência do visualizador denominada /Direction, que indica a ordem geral de leitura do documento. Esta opção não afeta os glifos. O seu propósito é indicar ao visualizador como organizar páginas lado a lado, por que lado deve começar um esquema de páginas opostas e para que lado se deve inclinar a interface de leitura. Nada disto é visível numa única página, e é exatamente por isso que costuma ser esquecido.

// Declare right-to-left reading order at the document level
Pdf.Direction := RightToLeft;  // adds vpDirection to ViewerPreferences

Configurar o Direction é tudo o que precisa de fazer: o setter da propriedade adiciona vpDirection às ViewerPreferences do documento, pelo que uma única linha transfere essa preferência para o ficheiro. O erro mais comum é omitir esta configuração, o que parece inofensivo porque a prova de página única que está a analisar parece idêntica de qualquer das formas. Contudo, quando alguém imprime um folheto frente e verso, as páginas espelhadas surgem invertidas, e a causa reside na falta daquela linha simples de código omitida semanas antes.

Onde termina a modelação do HotPDF

Uma listagem honesta das limitações vale uma semana de testes. O RtLTextOut resolve a reordenação bidirecional e a ligação contextual do árabe por si só. O que não faz automaticamente é a aplicação geral de funcionalidades OpenType. Ligaduras opcionais e características tipográficas semelhantes requerem o método GetSingleSubstituteGlyph(GID, 'liga'), que resolve uma única substituição de cada vez (primeiro o ID do glifo, depois a tag da funcionalidade), devolvendo o glifo original inalterado caso a funcionalidade não se aplique. Isto é suficiente para gerir uma lista de ligaduras conhecida e finita mantida pelo programador, mas não constitui um motor GSUB completo. Para scripts que exijam mais do que reordenação e ligação (sendo as escritas índicas, com os seus sinais vocálicos de reordenação, o exemplo clássico), execute um projeto-piloto real com strings reais de clientes antes de prometer o suporte ao idioma. O bom funcionamento em árabe não é garantia de que o mesmo aconteça em Devanágari.

Efetue a validação de ponta a ponta, dado que uma página pode parecer correta e ser inútil para os processos subsequentes. Três testes identificam a maioria dos problemas: copie o texto do Acrobat e compare os pontos de código com os da string original; utilize a pesquisa do visualizador para procurar uma palavra visível na página; e abra o ficheiro gerado numa máquina que não possua as suas fontes de desenvolvimento, sendo esta a situação mais propensa a expor substituições indesejadas. Nada disto substitui a revisão de um documento real por um falante nativo, capaz de detetar pormenores que nenhum conjunto de testes sintéticos revelará. Agende essa revisão antes da distribuição final do formato.

Escolha as strings de teste criteriosamente, em vez de reutilizar traduções antigas. Um mínimo aceitável por idioma: uma frase apenas com o script nativo, uma frase com marcas latinas embutidas, uma linha com algarismos e moeda, e nomes com diacríticos ou marcas de combinação. Nomes reais de clientes invalidam pressupostos que o texto de preenchimento padrão (filler) não afeta, pelo que deve expandir o seu conjunto de testes de regressão sempre que um caso de suporte revelar um padrão que ainda não tinha observado.

O registo de fontes, a criação de subconjuntos (subsetting) e a API de desenho de texto comum são abordados no artigo sobre geração de relatórios, fontes e imagens com o HotPDF. Se os mesmos documentos necessitarem de cumprir perfis de acessibilidade, as regras de marcação de idioma e estruturação detalhadas no artigo sobre validação PDF/A e PDF/UA aplicam-se complementarmente ao trabalho de modelação aqui descrito.

As APIs de fontes Unicode e da direita para a esquerda aqui descritas são fornecidas com o HotPDF Component para Delphi e C++Builder; a página do produto disponibiliza a referência completa de escrita de texto.