Technical Article

Exportação de Folhas de Cálculo Segura para Unicode em Delphi: RTF e HTML

Uma folha de cálculo contém uma coluna de nomes de clientes. Alguns estão em chinês, outros em cirílico, uns quantos apresentam tremas alemães ou um acento francês. Exporta a folha para CSV e abre o resultado: todos os caracteres estão intactos. Exporta o mesmo livro de trabalho para RTF com o intuito de servir de modelo de correspondência, abre-o num processador de texto, e os nomes não ASCII converteram-se em sequências de pontos de interrogação. Os dados nunca se alteraram. O que mudou foi o contrato de codificação do formato que escreveu, e cada caminho de exportação possui um contrato diferente.

Esta é a armadilha que afeta uma biblioteca que parece totalmente compatível com Unicode à superfície. O texto das células é mantido internamente como WideString, pelo que o modelo nunca perde um carácter. A perda ocorre no limite do sistema, no escritor que tem de serializar esse texto num formato com as suas próprias regras sobre quais os bytes legais e como tudo o que estiver fora desse intervalo deve ser codificado. Ajustar corretamente um escritor e poder ainda assim disponibilizar outro que corrompe o mesmo texto é um cenário real. A solução não passa por um interruptor global. Consiste numa decisão individual e correta em cada caminho de processamento.

O RTF é um formato seguro para 7 bits por conceção

O formato RTF (Rich Text Format) é anterior ao Unicode e foi especificado para resistir a transmissões que apenas aceitam ASCII imprimível. Um documento RTF declara uma página de códigos no seu cabeçalho, e qualquer carácter que o escritor não consiga representar nessa página de códigos tem de ser emitido como uma sequência de escape, em vez de um byte bruto. O escape relevante é \u, que transporta uma unidade de código de 16 bits com sinal seguida de um carácter ASCII alternativo (fallback) para leitores demasiado antigos para compreenderem a sequência de escape.

HotXLS escreve RTF desta forma. O cabeçalho do documento abre declarando a página de códigos, no formato \ansi\ansicpg1252\uc1, e o escritor na unidade lxRTF percorre cada cadeia de texto emitindo qualquer carácter acima do ASCII simples como uma sequência de escape \u, para que o fluxo de bytes permaneça limpo em 7 bits, independentemente do que a página de códigos declarada consiga suportar. Um ponto de código como U+4E2D torna-se na sequência literal \u20013?, e não num byte bruto que um visualizador tentaria depois interpretar através de qualquer página de códigos que por acaso assumisse. Sem essa disciplina, tudo o que se situe fora da página de códigos declarada não tem representação de bytes legal, e um escritor que emita o valor bruto produzirá os pontos de interrogação mencionados no início deste artigo.

O detalhe a ter em mente é que a página de códigos declarada e as sequências de escape constituem duas metades de um único contrato. Declarar apenas a página de códigos não resolve o problema do texto que se situa fora dela. Emitir escapes sem uma página de códigos declarada deixa os caracteres alternativos ambíguos. Ambos têm de estar corretos em conjunto, razão pela qual um escritor que trate apenas um deles continuará a falhar no primeiro livro de trabalho multilingue.

O escape em HTML vai além dos parênteses angulares

A exportação para HTML produz um documento com múltiplas folhas cujas estruturas de navegação exibem os nomes das folhas como texto visível. Esses nomes são cadeias de texto controladas pelo autor que podem conter qualquer carácter, incluindo os que possuem significado para a marcação (markup). Uma folha literalmente chamada Q1 & Q2 <draft> tem de chegar à página sob a forma de entidades de escape, caso contrário os parênteses angulares abrirão uma etiqueta fantasma e o E comercial (&) iniciará uma referência de entidade que nunca foi pretendida. Isto corresponde ao escape normal de HTML, e omiti-lo numa etiqueta de estrutura é o tipo de falha que passa em qualquer teste baseado em nomes de folhas puramente ASCII.

A questão da codificação situa-se um nível abaixo disso. Quando caracteres não ASCII surgem num contexto que não se garante vir a ser servido como UTF-8, a representação segura consiste numa referência de carácter numérica, pelo que U+00E9 é escrito como &#233; e não como um byte bruto cujo significado dependeria do conjunto de caracteres da resposta. A imagem invertida desta regra aplica-se na importação. Um livro de trabalho lido a partir de um XLSX contém cadeias de texto partilhadas nas quais um carácter pode já estar guardado como uma entidade XML numérica, e essa entidade tem de ser descodificada num carácter completo antes de entrar no modelo de células. Deodifique-a sem cuidado, dividindo um ponto de código em bytes separados, e um único carácter reemergirá sob a forma de fragmentos de texto corrompido (mojibake) que nenhuma exportação posterior conseguirá corrigir.

O contentor XLSX é um ZIP, e o ZIP tem a sua própria codificação de nomes

Um arquivo XLSX é um arquivo ZIP, e este guarda um nome para cada membro que contém. O formato ZIP é suficientemente antigo para que a sua especificação original nada dissesse sobre a codificação desses nomes, pelo que um leitor que não encontre qualquer sinalização assume a página de códigos local do arquivo. Essa suposição revela-se errada no momento em que o nome de um membro contém um carácter não ASCII, o que sucede com nomes de secções de folhas de cálculo localizadas e com ficheiros multimédia incorporados cujos nomes contêm acentos ou caracteres não latinos.

A resolução consiste num único bit. O bit 11 de objetivo geral em cada cabeçalho de ficheiro local declara que o nome do membro está codificado em UTF-8. HotXLS verifica precisamente esse bit quando lê um arquivo, testando as sinalizações de objetivo geral contra a máscara $0800, e um leitor ou escritor que o ignore lerá incorretamente um nome que uma implementação correta guardou como UTF-8. O bit é simples de definir e fácil de respeitar, representando toda a diferença entre um nome de membro que sobrevive à viagem de ida e volta e um que chega corrompido antes mesmo de o conteúdo da folha de cálculo ser analisado.

A conversão de maiúsculas/minúsculas e a análise de números ocultam o mesmo risco

A avaliação de fórmulas é a etapa em que a segurança do Unicode deixa de se focar na serialização e passa a focar-se na comparação. A função SEARCH é insensível a maiúsculas e minúsculas, o que significa que tem de converter a caixa de texto antes de procurar uma subcadeia. A forma incorreta de efetuar esta conversão é através da página de códigos ANSI, porque converter texto não ASCII dessa forma canaliza os caracteres por uma página de códigos estreita e corrompe tudo o que estiver fora dela. O método correto é a conversão em maiúsculas de cadeias largas (wide-string), que preserva todo o intervalo UTF-16. HotXLS efetua a conversão com WideUpperCase precisamente por esta razão, para que uma pesquisa por texto acentuado ou não latino coincida com os mesmos caracteres fornecidos, em vez de uma aproximação distorcida pela página de códigos.

O tokenizador de fórmulas possui uma obrigação relacionada que nada tem a ver com letras, mas sim com o limite de fim de um token. A notação científica como 1E3 ou 2.5E-3 é um literal numérico único, e o digitalizador (scanner) tem de reconhecer o E, um sinal opcional e os dígitos seguintes como parte do número, em vez de dividir a entrada num nome seguido de um número separado. Um analisador que processe isto de forma incorreta transforma uma constante perfeitamente válida num erro de análise ou, pior, numa expressão silenciosamente incorreta. Enquadra-se na mesma discussão porque ambos os casos se prendem com a tomada de decisões corretas ao nível dos caracteres por parte do leitor: uma sobre como converter um carácter para comparação, e outra sobre se um carácter dá continuidade ao token atual.

Criar e exportar um livro de trabalho multilingue

uses
  lxHandle;

procedure ExportMultilingualWorkbook;
var
  Book: IXLSWorkbook;
  Sheet: IXLSWorksheet;
begin
  Book := TXLSWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Customers');

    Sheet.Cells[1, 1].Value := 'Name';
    Sheet.Cells[1, 2].Value := 'City';

    // Cell text is held as WideString, so every script survives the model.
    Sheet.Cells[2, 1].Value := '王伟';          // Chinese
    Sheet.Cells[2, 2].Value := '北京';
    Sheet.Cells[3, 1].Value := 'Müller';        // German umlaut
    Sheet.Cells[3, 2].Value := 'Köln';
    Sheet.Cells[4, 1].Value := 'Иванов';        // Cyrillic
    Sheet.Cells[4, 2].Value := 'Москва';
    Sheet.Cells[5, 1].Value := 'Désirée';       // French accents
    Sheet.Cells[5, 2].Value := 'Montréal';

    // RTF: the lxRTF writer declares the code page and emits every
    // non-ASCII character as a \u escape, keeping the file 7-bit clean.
    Book.SaveAsRTF('Customers.rtf');

    // HTML: sheet names are HTML-escaped and non-ASCII text is written
    // so it does not depend on a guessed response charset.
    Book.SaveAsHTML('Customers.html');
  finally
    Book := nil;
  end;
end;

Ambas as chamadas devolvem um estado Integer e ambas consomem o mesmo texto em memória. Nada no código que efetua a chamada declara uma página de códigos ou aplica escapes a caracteres, porque essa responsabilidade cabe ao escritor que conhece o seu próprio formato. O método SaveAsCSV ao nível do livro segue o mesmo formato se necessitar de uma exportação delimitada a partir da mesma origem.

// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');

A segurança do Unicode é por caminho, não por biblioteca

A lição a reter é que não existe um ponto único para garantir a segurança do Unicode. O RTF necessita de uma página de códigos declarada e de escapes \u. O HTML precisa de escapes de entidades para caracteres significativos na marcação e referências numéricas onde o conjunto de caracteres não é garantido, além de uma descodificação correta de entidades que chegam em cadeias de texto partilhadas. O contentor ZIP necessita do bit 11 de objetivo geral ativo para que um nome de membro UTF-8 seja lido como UTF-8. A avaliação de fórmulas precisa de conversão de maiúsculas/minúsculas de cadeias largas e de um tokenizador que mantenha a notação científica numa única peça. Cada um destes constitui um contrato diferente, e uma biblioteca pode satisfazer um enquanto viola silenciosamente outro. É essa a razão pela qual uma ferramenta que processa corretamente o CSV pode ainda assim produzir um RTF cheio de pontos de interrogação.

Se estiver a efetuar a exportação para formatos delimitados, as vantagens e desvantagens entre eles são abordadas no nosso guia de exportação para CSV, TSV e HTML, e quando a origem é um conjunto de resultados e não uma folha criada manualmente, os padrões descritos em exportação de base de dados para relatórios em Delphi articulam-se naturalmente com as regras de codificação descritas aqui. Tudo isto é fornecido como parte do HotXLS Component para Delphi e C++Builder, a par das APIs de leitura, fórmulas e formatação tratadas noutros locais deste blogue.