Uma planilha contém uma coluna com nomes de clientes. Alguns estão em chinês, outros em cirílico, alguns possuem tremas em alemão ou acentos em francês. Você a exporta para CSV e abre o resultado: cada caractere está intacto. Você exporta a mesma pasta de trabalho para RTF para um modelo de mala direta, abre em um processador de texto e os nomes não ASCII viraram sequências de pontos de interrogação. Os dados nunca mudaram. O que mudou foi o contrato de codificação do formato gravado, e cada caminho de exportação possui um contrato diferente.
Esta é a armadilha na qual cai uma biblioteca que parece totalmente compatível com Unicode na superfície. O texto da célula é mantido internamente como WideString, de modo que o modelo nunca perde um caractere. A perda ocorre no limite, no gravador que precisa serializar esse texto em um formato com suas próprias regras sobre quais bytes são legais e como qualquer coisa fora do intervalo permitido deve ser codificada. Acerte um gravador e você ainda poderá lançar outro que corrompa o mesmo texto. A correção não é uma chave global. É uma decisão separada e correta em cada caminho.
O RTF é um formato seguro para 7 bits por design
O Rich Text Format precede o Unicode e foi especificado para sobreviver a transportes que transmitem apenas ASCII imprimível. Um documento RTF declara uma página de código em seu cabeçalho, e qualquer caractere que o gravador não possa representar nessa página de código deve ser emitido como um escape, em vez de um byte bruto. O escape relevante é \u, que carrega uma unidade de código com sinal de 16 bits seguida por um caractere de escape ASCII para leitores muito antigos para entender o escape.
O HotXLS grava RTF dessa maneira. O cabeçalho do documento inicia 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 escape \u para que o fluxo de bytes permaneça limpo para 7 bits, independentemente do que a página de código declarada possa conter. Um ponto de código como U+4E2D torna-se a sequência literal \u20013?, não um byte bruto que um visualizador tentaria interpretar por meio de qualquer página de código que assumisse. Sem essa disciplina, qualquer coisa fora da página de código declarada não tem representação de byte legal, e um gravador que emite o valor bruto produz os pontos de interrogação mencionados no início deste artigo.
O detalhe a ser lembrado é que a página de código declarada e os escapes são duas metades de um único contrato. Declarar apenas a página de código não ajuda o texto que está fora dela. Emitir escapes sem uma página de código declarada deixa os caracteres de fallback ambíguos. Ambos devem estar corretos juntos, e é por isso que um gravador que trata apenas um deles falha na primeira pasta de trabalho multilíngue.
O escape de HTML vai além dos colchetes angulares
A exportação de HTML produz um documento com várias 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> deve chegar à página como entidades escapadas, caso contrário, os colchetes angulares abrem uma tag fantasma e o E comercial inicia uma referência de entidade que nunca foi pretendida. Esse é o escape de HTML comum, e omiti-lo em um rótulo de quadro é o tipo de falha que passa em qualquer teste baseado em nomes de planilhas apenas em ASCII.
A questão da codificação está uma camada abaixo disso. Quando caracteres não ASCII chegam a um contexto que não tem garantia de ser servido como UTF-8, a representação segura é uma referência de caractere numérico, portanto U+00E9 é escrito como é em vez de um byte bruto cujo significado depende do conjunto de caracteres da resposta. A imagem espelhada dessa regra se aplica na entrada. Uma pasta de trabalho lida de volta de um XLSX carrega strings compartilhadas nas quais um caractere já pode estar armazenado como uma entidade XML numérica, e essa entidade precisa ser decodificada em um caractere inteiro antes de entrar no modelo de célula. Decodifique-o sem cuidado, dividindo um ponto de código em bytes separados, e um único caractere reaparecerá como duas partes de mojibake que nenhuma exportação posterior poderá reparar.
O contêiner XLSX é um ZIP, e o ZIP possui sua própria codificação de nomes
Um arquivo XLSX é um arquivo compactado ZIP, e o arquivo armazena um nome para cada membro que 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 encontra nenhuma sinalização assume a página de código local do arquivo compactado. Essa suposição se torna incorreta no momento em que o nome de um membro contém um caractere não ASCII, o que ocorre com nomes de partes de planilhas localizados e com mídias incorporadas cujos nomes de arquivo carregam acentos ou caracteres não latinos.
A correção é um único bit. O bit 11 de propósito geral em cada cabeçalho de arquivo local declara que o nome do membro está codificado como UTF-8. O HotXLS verifica exatamente esse bit quando lê um arquivo compactado, testando as flags de propósito geral contra a máscara $0800, e un leitor ou gravador que o ignora lerá incorretamente um nome que uma implementação correta armazenou como UTF-8. O bit é simples de definir e de respeitar, e faz 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 planilha ser analisado.
O folding de caixa e a varredura de números ocultam o mesmo risco
A avaliação de fórmulas é onde a segurança do Unicode deixa de ser sobre serialização e passa a ser sobre comparação. A função SEARCH é insensível a maiúsculas e minúsculas, o que significa que ela precisa converter a caixa (case folding) antes de procurar uma substring. A maneira incorreta de converter é por meio da página de código ANSI, porque converter texto não ASCII para maiúsculas dessa forma direciona os caracteres por uma página de código estreita e corrompe qualquer coisa fora dela. A maneira correta é a conversão para maiúsculas de strings largas, que preserva todo o intervalo UTF-16. O HotXLS faz a transformação com WideUpperCase exatamente por esse motivo, de modo que uma pesquisa por texto acentuado ou não latino corresponda aos mesmos caracteres recebidos, em vez de uma aproximação corrompida 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 onde um token termina. A notação científica como 1E3 ou 2.5E-3 é um único literal numérico, e o scanner deve reconhecer o E, um sinal opcional e os dígitos seguintes como parte do número, em vez de dividir a entrada em um nome seguido por um número separado. Um scanner que trata isso incorretamente transforma uma constante perfeitamente válida em um erro de análise ou, pior, em uma expressão silenciosamente errada. Isso pertence à mesma discussão porque ambos os casos tratam de um leitor tomando uma decisão correta ao nível do caractere: um sobre como converter a caixa de um caractere para comparação, o outro sobre se um caractere continua o token atual.
Construindo e exportando uma pasta de trabalho multilíngue
A API pública não exige que você pense em nada disso. Você constrói a pasta de trabalho a partir de valores de células do tipo WideString e chama o ponto de entrada de exportação desejado. As decisões de codificação ocorrem dentro de cada gravador. O exemplo abaixo preenche uma planilha com texto em vários sistemas de escrita 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 a mesma entrada.
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 retornam um status do tipo Integer e ambas consomem o mesmo texto em memória. Nada no código de chamada declara uma página de código ou faz o escape de um caractere, porque a responsabilidade é do gravador que conhece seu próprio formato. O método SaveAsCSV no nível da pasta de trabalho segue a mesma estrutura caso você precise 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 que se deve levar é que não existe um único lugar para ser seguro em relação ao Unicode. O RTF precisa de uma página de código declarada e de escapes \u. O HTML precisa de escapes de entidades para caracteres significativos de marcação e de referências numéricas onde o conjunto de caracteres não é garantido, além de decodificação correta de entidades que chegam em strings compartilhadas. O contêiner ZIP precisa do bit 11 de propósito geral definido para que o nome de um membro em UTF-8 seja lido como UTF-8. A avaliação de fórmulas precisa de folding de caixa de strings largas e de um tokenizador que mantenha a notação científica intacta. Cada um desses é um contrato diferente, e uma biblioteca pode satisfazer um enquanto viola silenciosamente outro. Essa é a razão pela qual uma ferramenta que funciona corretamente para CSV ainda pode gerar um RTF cheio de pontos de interrogação.
If suas exportações dependem de formatos delimitados, as vantagens e desvantagens entre eles são abordadas em nosso passo a passo sobre exportação de CSV, TSV e HTML, e quando a origem é um conjunto de resultados em vez de uma planilha manual, os padrões em exportação de banco de dados para relatórios em Delphi se combinam naturalmente com as regras de codificação descritas aqui. Tudo isso é enviado como parte do HotXLS Component para Delphi e C++Builder, junto com as APIs de leitura, fórmula e formatação abordadas em outras partes deste blog.