Uma planilha contém uma coluna de nomes de clientes. Alguns estão em chinês, outros em cirílico, e uns poucos trazem tremas alemães ou acentos franceses. Você exporta para CSV, abre o resultado e cada caractere está intacto. Você exporta a mesma pasta de trabalho para RTF visando um modelo de mala direta, abre-a em um processador de texto, e os nomes não ASCII entram em colapso se transformando em linhas de pontos de interrogação. Os dados nunca mudaram. O que mudou foi o contrato de codificação do formato em que você gravou, e cada via de exportação traz um contrato diferente
Esta é a armadilha que pega uma biblioteca que parece totalmente compatível com Unicode superficialmente. O texto da célula é retido internamente como um WideString, então o modelo nunca perde um caractere. A perda acontece na fronteira, no gravador que tem que serializar aquele texto para um formato com suas próprias regras a respeito de quais bytes são legais e de que forma qualquer coisa fora do intervalo permitido precisa ser codificada. Se você acertar um gravador, ainda pode entregar um outro que estraga o mesmo texto. A correção não é uma opção (switch) global. É uma decisão separada e correta em cada caminho
RTF é um formato seguro de 7 bits por design
O formato Rich Text (RTF) precede o Unicode e foi especificado para sobreviver a transportes que transmitem somente ASCII imprimíveis. Um documento RTF declara uma página de código em seu cabeçalho, e qualquer caractere que o gravador não puder representar naquela página de código tem que ser emitido como um caractere de escape, em vez de um byte bruto. O caractere de escape em questão é o \u, que transmite uma unidade de código (code unit) com sinal de 16 bits seguida por um caractere de fallback (alternativo) ASCII para os leitores antigos demais que não compreenderão o escape
O HotXLS grava o RTF dessa maneira. O cabeçalho do documento é aberto declarando a página de código na forma \ansi\ansicpg1252\uc1, e o gravador na unidade lxRTF percorre cada string emitindo qualquer caractere acima do ASCII simples como um caractere de escape \u para que o fluxo de bytes permaneça limpo com 7 bits, sem importar o que a página de código declarada conseguir deter. Um ponto de código, como por exemplo U+4E2D, torna-se a sequência literal \u20013?, não um byte bruto que um visualizador em seguida tentaria interpretar mediante qualquer página de código que acaso houvesse adotado. Sem aquela disciplina, tudo que fique fora da página de código declarada carece de representação em bytes legal, e um gravador que emita o valor bruto produz os pontos de interrogação com os quais demos início a este artigo
O detalhe para ter em conta é que a página de código declarada e os escapes formam as duas metades de um único contrato. Apenas declarar a página de código não ajuda um texto que se situa fora dela. A emissão de escapes, na falta de uma página de código declarada, torna ambíguos os caracteres de fallback. Ambos precisam estar simultaneamente corretos, eis o motivo de um gravador que trata de apenas um deles ainda falhar de encontro à primeira pasta de trabalho multilíngue
O escape no HTML é muito mais do que sinais de menor e maior
A exportação para HTML produz um documento com múltiplas planilhas cujos quadros de navegação carregam os nomes das planilhas como texto visível. Esses nomes são strings controladas pelo autor que podem conter qualquer caractere, incluindo aqueles significativos para marcação. Uma planilha literalmente chamada Q1 & Q2 <draft> tem que chegar à página como entidades escapadas (escaped entities), ou os sinais de menor e maior abrirão uma tag fantasma e o "e comercial" iniciará uma referência de entidade que nunca foi a intenção. Isso é o escape HTML comum, e ignorá-lo no rótulo de um quadro é o tipo de omissão que passa em todos os testes construídos a partir de nomes de planilhas apenas com ASCII
A questão de codificação fica uma camada abaixo disso. Quando caracteres não-ASCII caem em um contexto que não tem garantia de ser servido como UTF-8, a representação segura é uma referência de caractere numérica; portanto, U+00E9 é escrito como é em vez de um byte bruto cujo significado depende do conjunto de caracteres da resposta. A imagem refletida dessa regra se aplica na entrada. Uma pasta de trabalho lida de volta do XLSX carrega strings compartilhadas nas quais um caractere já pode estar armazenado como uma entidade XML numérica, e essa entidade tem que ser decodificada em um caractere completo antes de entrar no modelo de célula. Decodifique-o descuidadamente, dividindo um ponto de código em bytes separados, e um único caractere ressurge como duas peças de texto distorcido (mojibake) que nenhuma exportação posterior conseguirá reparar
O contêiner XLSX é um ZIP, e o ZIP tem sua própria codificação de nomes
Um arquivo XLSX é um arquivo ZIP, e o arquivo armazena um nome para cada membro que ele contém. O formato ZIP é antigo o suficiente para que sua especificação original não dissesse nada sobre a codificação desses nomes; portanto, um leitor que não encontrar sinal algum presumirá a página de código local do arquivo. Essa suposição está errada no momento em que o nome de um membro contém um caractere não ASCII, o que acontece com nomes de partes de planilhas localizadas e com mídias embutidas cujos nomes de arquivo carregam acentos ou scripts não latinos
A correção é um único bit. O bit de uso geral 11 em cada cabeçalho de arquivo local declara que o nome do membro está codificado em UTF-8. O HotXLS verifica exatamente esse bit ao ler um arquivo, testando as flags (sinalizadores) de uso geral contra a máscara $0800; um leitor ou gravador que ignora isso lerá incorretamente um nome que uma implementação correta armazenou como UTF-8. O bit é fácil de configurar e fácil de ser honrado, sendo toda a diferença entre um nome de membro capaz de sobreviver à viagem de ida e volta, e outro que chega já corrompido, e isso ainda antes de o conteúdo da planilha sofrer o processo de análise
A conversão de maiúsculas/minúsculas e a varredura de números ocultam o mesmo perigo
A avaliação de fórmulas é onde a segurança Unicode deixa de ser sobre serialização e passa a ser sobre comparação. A função SEARCH não faz distinção entre maiúsculas e minúsculas, o que significa que ela deve converter a caixa (case folding) antes de procurar por uma substring. A maneira errada de converter é através da página de código ANSI, porque transformar em maiúsculas um texto não-ASCII dessa maneira roteia os caracteres através de uma página de código estreita e corrompe qualquer coisa fora dela. A maneira certa é a conversão para maiúsculas em wide-string, que preserva o intervalo completo do UTF-16. O HotXLS faz a conversão com WideUpperCase exatamente por esse motivo, então uma pesquisa por texto acentuado ou não latino corresponde aos mesmos caracteres que foram dados em vez de uma aproximação distorcida pela página de código
O tokenizador de fórmula carrega uma obrigação relacionada que não tem nada a ver com letras e tudo a ver com o local onde um token termina. A notação científica como 1E3 ou 2.5E-3 é um único literal numérico, e o scanner tem que reconhecer o E, um sinal opcional e os dígitos a seguir como parte do número em vez de dividir a entrada em um nome seguido de um número separado. Um scanner que lida incorretamente com isso transforma uma constante perfeitamente válida em um erro de análise ou, pior, em uma expressão silenciosamente errada. Ela pertence à mesma discussão porque ambos os casos se referem a um leitor tomando uma decisão correta em nível de caractere: uma sobre como dobrar um caractere para comparação (case folding), a outra sobre se um caractere continua o token atual
Criando e exportando uma pasta de trabalho multilíngue
A API pública não pede que você pense sobre nada disso. Você cria a pasta de trabalho a partir de valores de célula em WideString e chama o ponto de entrada de exportação desejado. As decisões de codificação acontecem dentro de cada gravador. O exemplo abaixo preenche uma planilha com texto em vários scripts e, em seguida, grava um arquivo RTF e um arquivo HTML a partir da mesma pasta de trabalho, para que os dois caminhos sejam executados com uma entrada idêntica
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';
// O texto da célula é mantido como WideString, para que cada script sobreviva ao modelo.
Sheet.Cells[2, 1].Value := '王伟'; // Chinês
Sheet.Cells[2, 2].Value := '北京';
Sheet.Cells[3, 1].Value := 'Müller'; // Trema alemão
Sheet.Cells[3, 2].Value := 'Köln';
Sheet.Cells[4, 1].Value := 'Иванов'; // Cirílico
Sheet.Cells[4, 2].Value := 'Москва';
Sheet.Cells[5, 1].Value := 'Désirée'; // Acentos franceses
Sheet.Cells[5, 2].Value := 'Montréal';
// RTF: o gravador lxRTF declara a página de código e emite cada
// caractere não-ASCII como um escape \u, mantendo o arquivo limpo em 7 bits.
Book.SaveAsRTF('Customers.rtf');
// HTML: os nomes das planilhas são escapados com HTML e o texto não-ASCII é escrito
// para não depender de um charset (conjunto de caracteres) de resposta adivinhado.
Book.SaveAsHTML('Customers.html');
finally
Book := nil;
end;
end;
Ambas as chamadas retornam um status em Integer, e ambas consomem o mesmo texto na memória. Nada no código de chamada declara uma página de código ou escapa um caractere, porque a responsabilidade fica com o gravador que conhece seu próprio formato. O SaveAsCSV no nível da pasta de trabalho segue a mesma forma se você precisar de uma exportação delimitada a partir da fonte idêntica
// A mesma pasta de trabalho, um terceiro caminho de exportação com suas próprias regras de codificação.
Book.SaveAsCSV('Customers.csv');
A segurança Unicode é por caminho, não por biblioteca
A lição que vale a pena levar é que não há um lugar único para se estar seguro com o Unicode. O RTF precisa de uma página de código declarada, além de escapes \u. O HTML precisa de escape de entidade para os caracteres significativos de marcação e de referências numéricas onde o conjunto de caracteres não for garantido, além da decodificação correta das entidades que chegam em strings compartilhadas. O contêiner ZIP requer a ativação do bit de uso geral 11 a fim de que um nome de membro em UTF-8 seja lido como UTF-8. A avaliação da fórmula demanda a conversão de maiúsculas/minúsculas (case folding) em wide-string e um tokenizador que mantenha inteiriça a notação científica. Cada um desses é um contrato diferente, e uma biblioteca pode satisfazer um enquanto viola discretamente um outro. É por isso que uma ferramenta que acerta no CSV ainda pode te entregar um RTF cheio de pontos de interrogação
Se suas exportações se inclinam para os formatos delimitados, as compensações entre eles são abordadas em nosso guia prático de exportação para CSV, TSV e HTML, e quando a origem é um conjunto de resultados em vez de uma planilha construída à mão, os padrões na exportação de banco de dados para relatórios Delphi formam uma dupla perfeita e natural com as regras de codificação descritas aqui. Tudo isso é enviado como parte do Componente HotXLS para Delphi e C++Builder, juntamente com as APIs de leitura, fórmula e formatação abordadas em outros lugares deste blog