Artigo Técnico

HotPDF: Unicode text shaping for complex scripts in Delphi

Passe a frase árabe يوضح ملف PDF para TextOut e abra o resultado. As letras correm no sentido errado e cada uma assenta na sua forma isolada com um espaço visível antes da seguinte, como se alguém tivesse escrito inglês ao contrário e premido o espaço entre cada caráter. Não foi disparada qualquer exceção. Não foi impresso nenhum aviso. O resultado está simplesmente errado e está errado porque duas transformações distintas das quais o árabe depende nunca chegaram a acontecer. Saber quais são essas duas transformações e qual a chamada que as executa é a base da produção de PDFs com scripts complexos

O HotPDF é um componente PDF nativo VCL para Delphi e C++Builder, realizando o trabalho da direita para a esquerda por si através de uma chamada distinta. Tem igualmente algumas limitações pontuais de que desejará ter conhecimento antes de aplicar a um idioma (locale). É isso mesmo que este artigo se propõe a mapear sob a forma de conceitos e dos seus limites mais honestos; a configuração prática da própria chamada está patente no artigo de referência do RtLTextOut

Porque é que uma cadeia correta ainda é impressa incorretamente

O Unicode mantém o texto por ordem lógica, ou seja, pela mesma sequência com que o digita e lê em voz alta. O renderizador tem de colocar os glifos numa ordem visual. Em relação aos scripts da esquerda para a direita essas ordens coincidem, fato em que ninguém pensa. No árabe e hebraico a história não é bem assim e, num cenário de direções mistas perante uma única linha (pensemos numa frase em árabe com a denominação latina "PDF" ou num preço escrito em algarismos), entra em cena o Algoritmo Bidirecional do Unicode (UAX #9) para determinar, com a precisão exigida, como devem os fragmentos orientados da esquerda para a direita aninhar-se numa frase orientada da direita para a esquerda. Trata-se, desta forma, da primeira transformação, a reordenação, cuja omissão resulta na inversão da linha

A segunda é a formatação contextual. Uma letra árabe é desenhada de forma diferente consoante a sua posição numa palavra: inicial, intermédia, final ou isolada. O ponto de código (codepoint) permanece o mesmo ao longo de todo o processo; apenas o glifo se altera. Um pipeline que entregue cada ponto de código diretamente ao seu glifo padrão produzirá exatamente a saída desconectada e sob forma isolada do parágrafo inicial. O hebraico salta este passo, uma vez que as suas letras não se juntam, não prescindindo de passar na mesma pela reordenação. O árabe necessita de ambos, e é por essa razão que o árabe, e não o hebraico, é a cadeia com a qual se deve testar

No computador, nada disto é problema seu. Quando um formulário VCL pinta texto árabe num TEdit, a pilha de texto do sistema operativo reordena-o e molda-o de forma silenciosa, sendo essa a razão pela qual a cadeia que parece perfeita no tela sai partida num PDF ingénuo. O fluxo de conteúdo não guarda texto editável. Guarda glifos posicionados, pelo que quem emitir o fluxo herda a função de formatação com a qual o SO costumava lidar. RtLTextOut é a chamada que retoma esse trabalho

O que o RtLTextOut formata por si

O HotPDF conserva a via latina e a via de script complexo como dois métodos distintos. O TextOut imprime o que lhe entrega na ordem em que o entrega. O RtLTextOut efetua primeiro ambas as transformações — reordenação bidirecional em toda a linha, análise contextual para os scripts com união — e depois imprime. As regras do script a aplicar não fluem pela chamada em si, mas sim pelo conjunto de carateres do tipo de letra, pelo que a direção é uma escolha explícita em cada local de chamada e não uma conjetura a partir dos carateres. A configuração parâmetro a parâmetro, os valores do conjunto de carateres, os passos de registro do tipo de letra e um exemplo completo e compilável encontram-se no artigo de referência do RtLTextOut; esta peça limita-se ao significado das transformações, onde terminam e como provar que funcionaram

Uma regra de utilização tem importância mesmo a este nível: a entrada tem de estar por ordem lógica, porque o RtLTextOut efetua ele próprio a inversão, e uma cadeia que já tenha invertido à mão sai duplamente invertida — o artigo de referência aborda essa armadilha e a sua correção. O que justifica mencionar aqui esta armadilha é o motivo pelo qual sobrevive aos testes. Uma cadeia em árabe puro duplamente invertida pode parecer perfeitamente correta e apenas se desmorona quando uma linha contém uma palavra latina ou um número, porque essas sequências incorporadas já não se aninham da forma que o UAX #9 dita. O erro não está na renderização; está em fornecer ao algoritmo texto que já estava meio processado

Esse mesmo comportamento de direção mista confunde mais os revisores do que o próprio código. Numa linha da direita para a esquerda, os algarismos e as palavras latinas incorporadas continuam a ser lidos da esquerda para a direita. Alguém que não tenha trabalhado com disposições bidirecionais irá olhar para uma fatura renderizada, ver o número de conta ser lido da forma "errada" relativamente ao árabe à sua volta e reportá-lo como um erro. Trata-se, na verdade, do resultado correto segundo as especificações. Uma breve nota nos seus critérios de aceitação, escrita antes da primeira revisão por um falante nativo, poupa esse trabalho redobrado

Quando a reordenação e a ligação são suficientes, e quando não são

No caso dos textos corridos em árabe e hebraico — relatórios, faturas, contratos, cartas —, a reordenação em conjunto com a ligação contextual constituem a totalidade do trabalho, e o RtLTextOut assume isso sozinho. O limite surge quando a tipografia exige mais do que a união. A resposta do HotPDF no lado árabe é um formatador opcional do lado do produtor: defina AutoShapeArabic := True e o componente reescreve o percurso de ordem lógica para Formulários de Apresentação Unicode antes da passagem bidirecional. Dessa forma, as formas de junção são calculadas em relação a vizinhos lógicos e as reduções de ligaduras são incorporadas nos pontos de código que o PDF realmente comporta, em vez de serem deixadas para um visualizador resolver. O interruptor está predefinido como desligado e o resultado é estável em bytes quando permanece assim, pelo que a sua ativação constitui uma decisão deliberada por cada pipeline de documentos, e não uma atualização global. O mesmo modelo opcional estende-se aos restantes scripts da direita para a esquerda com ligações que o HotPDF formata: o siríaco, o n'ko, o adlam e o rohingya hanifi possuem, cada um, a sua própria bandeira de formatação automática que espelha a do árabe

Os recursos opcionais do OpenType constituem um mecanismo novamente diferente. As ligaduras discricionárias e funcionalidades de substituição única idênticas passam por GetSingleSubstituteGlyph(GID, 'liga'), que resolve uma substituição de cada vez — primeiro o ID do glifo de entrada e depois a etiqueta da funcionalidade — e devolve o glifo de entrada sem qualquer alteração quando a funcionalidade não é aplicável. É o suficiente para gerir uma lista de ligaduras finita e conhecida que o usuário mantém por si próprio. Não se trata de um motor GSUB completo, e a diferença reside precisamente no fato de ser aqui que falham os planos de idioma mais ambiciosos: um pipeline de formatação que lida com o árabe de modo irrepreensível apenas demonstrou reordenação e ligação, nada mais

Abrangência por entre scripts

O árabe exercita ambas as transformações, sendo por isso a cadeia indicada para efetuar testes e a razão pela qual uma passagem em árabe é a prova mais irrefutável de que o pipeline funciona. O hebraico necessita da reordenação mas não da união, uma vez que as suas letras se apresentam isoladas; caso o hebraico seja renderizado corretamente mas o árabe saia desligado, a parte bidirecional está correta e a parte contextual nunca foi executada. O persa e o urdu apoiam-se no script árabe e herdam o seu comportamento, se bem que a preferência do urdu pelo estilo Nastaliq seja uma decisão de tipo de letra com consequências na legibilidade que um leitor nativo deverá julgar

O tailandês situa-se inteiramente do outro lado da linha. Executa da esquerda para a direita, pelo que não necessita de trabalho bidirecional, e as suas letras não se juntam, não requerendo análise contextual; as cadeias em tailandês passam pela via normal TextOut como o latim. Aquilo que o tailandês tem são as marcas sobrepostas — vogais e marcas de tom acima e abaixo da consoante de base — e a sua correta colocação depende do fato de o tipo de letra construir as suas marcas de combinação de modo a que se sobreponham sem ajuda do motor de formatação. A maior parte dos tipos de letra dedicados ao tailandês faz isso. Teste com o tipo de letra exato que pretende incorporar, e não com um semelhante

O devanágari e as restantes famílias índicas representam o travão definitivo e mais honesto. Os seus sinais vocálicos são reordenados em torno de grupos de consoantes e as suas conjunções formam-se através de cadeias de substituições dependentes do contexto, o que é um território tipicamente GSUB, para lá da reordenação e junção. Se um idioma índico estiver no roteiro, execute um verdadeiro piloto em cadeias genuínas do cliente antes de o prometer — o fato de o árabe funcionar não é prova de que o devanágari também funcione. As cadeias de caracteres CJK (chinês, japonês e coreano), o vietnamita com os seus diacríticos sobrepostos e o texto misto europeu adotam todos a via comum, sem qualquer análise bidirecional, e compensa manter as duas vias fisicamente separadas no código de relatório: uma rotina para os trajetos em RTL e outra para tudo o resto. Deste modo, a lógica da linguagem fica visível no local da chamada, em vez de oculta por trás de um indicador que alguém se pode esquecer de ativar

A abrangência de glifos é decidida antes mesmo de a formatação ser executada

A formatação retira glifos de um tipo de letra. Se o tipo de letra não os possuir, não há nada que retirar, razão pela qual a clássica falha de implementação — impecável na máquina do programador, caixas em branco no servidor do cliente após uma substituição silenciosa do tipo de letra — é um problema de abrangência, não de formatação. A solução prática, registar um tipo de letra que o usuário distribui em vez de confiar no que estiver instalado numa máquina, é explicada passo a passo no artigo de referência. O ponto concetual é que a abrangência tem de ser estabelecida antes de qualquer questão de formatação ter sequer sentido, e que pode ser estabelecida de forma programática em vez de apenas se observar a saída a olho nu

O próprio registro acarreta dois constrangimentos — um limite base do PDF 1.5 para o processamento de Unicode incorporado e os bits de permissão de incorporação do tipo de letra —, ambos abordados em conjunto com os passos de configuração no artigo de referência do RtLTextOut. O que interessa aqui é o hábito de auditoria: o GetUnicodeGlyphForCodepoint é o seu sistema de aviso atempado. Percorra os intervalos de pontos de código que os seus dados efetivamente utilizam quando o serviço é iniciado e registe os IDs de glifos que são devolvidos. Uma lacuna de abrangência aparece então como uma linha num registro de arranque durante a implementação, em vez de se apresentar como caracteres em falta numa fatura que já chegou a um cliente

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

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

Obter todos os glifos corretamente não significa que a tarefa esteja terminada. A norma ISO 32000-1 §12.2 define uma preferência do visualizador designada por /Direction, que estipula a ordem de leitura global do documento. Esta não afeta quaisquer glifos. O que faz é informar o visualizador sobre a forma de organizar páginas duplas, a partir de que lado deve iniciar-se a disposição das páginas lado a lado, e a direção para onde deve pender a interface de leitura. Nada disso se manifesta numa única página, que é precisamente a razão pela qual é esquecido

Definir a Direction é todo o trabalho: o definidor da propriedade adiciona o vpDirection às ViewerPreferences do documento, pelo que uma única linha transfere a preferência para o arquivo. Se a saída do texto ocorrer através do RtLTextOut obtém isto de forma gratuita, dado que a chamada inverte a direção do documento de forma inerente — o artigo de referência refere a eventualidade de se exigir um recuo face a um documento de natureza mista. O cenário em que o tem de definir manualmente diz respeito a um documento da direita para a esquerda gerado por qualquer outra via, como, por exemplo, a partir de dados de entrada cujo formato foi predefinido a montante e que foram desenhados através da rota habitual. Se não o incluir, a prova de página única que está a visualizar parece idêntica de qualquer das formas; de seguida, alguém imprime um folheto duplex, os folhetos espelham a imagem de origem, e a causa é uma linha de código em falta desde umas semanas antes

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

Verificar a saída formatada

Efetue uma verificação de ponta a ponta, visto que uma página pode aparentar estar correta e ser inútil para tudo o que surgir a jusante. Três verificações detetam a maioria dos problemas. Copie novamente o texto do Acrobat e compare os pontos de código com a sua cadeia de origem. Execute a pesquisa no documento do visualizador em relação a uma palavra que consiga vislumbrar na página. De seguida, abra a saída numa máquina que não possua os seus tipos de letra de programação instalados, que tem a maior probabilidade de detetar a ocorrência de uma substituição. Nenhuma destas substitui o olhar de um leitor nativo perante um documento real, o qual deteta anomalias que um corpus sintetizado jamais consegue. Garanta essa revisão no calendário antes de efetuar o envio do formato

Selecione propositadamente as cadeias de teste, em vez de reaproveitar o que um tradutor tenha enviado no ano anterior. Um mínimo viável por idioma: uma frase de script puro, uma frase com nomes de marcas latinas incorporados, uma linha com algarismos e moedas e nomes com diacríticos ou marcas de combinação. Os nomes reais de clientes quebram pressupostos que o texto de preenchimento deixa intocados, pelo que deverá permitir que o conjunto de regressão cresça através de uma cadeia sempre que um caso de assistência detetar um padrão até então não visualizado

O registro de tipo de letra, os subconjuntos e a API comum para desenhar texto são abordados no artigo sobre saída de relatórios, tipos de letra e imagens com o HotPDF. Quando os mesmos documentos têm igualmente de satisfazer perfis de acessibilidade, as regras de marcação e estruturação do idioma no artigo de validação de PDF/A e PDF/UA assentam em cima do trabalho de formatação aqui exposto

As APIs dos tipos de letra da direita para a esquerda e Unicode descritas acima são fornecidas em conjunto com o Componente HotPDF para Delphi e C++Builder; a página do produto fornece hiperligações com toda a referência para a saída de texto