Nomes de cidades chinesas exportados para .dbf num PC ocidental podem voltar como pontos de interrogação de um export que reportou sucesso, porque arquivos dBASE, SYLK e DIF não têm onde registrar uma codificação. Um writer seguro usa a code page ANSI do host e confere cada nome e cada valor contra ela antes do primeiro byte; o HotXLS recusa o que essa página não puder guardar
A code page do host é a única codificação que esses arquivos têm
Os três formatos vêm dos primeiros anos do PC: dBASE da Ashton-Tate, SYLK da era Multiplan da Microsoft, DIF da Software Arts, a empresa do VisiCalc. Cada um guarda texto como bytes únicos e assume que o reader divide a code page do writer — aposta justa quando toda máquina do escritório rodava o mesmo setup DOS. No Windows de hoje essa página é o que o GetACP retornar: 1252 numa instalação da Europa Ocidental, 936 no chinês simplificado, 1251 no cirílico. dBASE IV e FoxPro depois puseram uma marca de language-driver no byte 29 do header da tabela, mas o layout dBASE III que o HotXLS grava deixa esse byte em zero, e SYLK, DIF e o texto .prn alinhado por espaços não têm campo de header para isso de forma alguma. Então a code page é acordada fora do arquivo. O HotXLS usa a code page ANSI do host na escrita e na leitura, e confere antes de gravar se todo caractere tem um byte nessa página
| Formato | Extensão no SaveAs / Open | Codificação declarada no arquivo | Texto que a página do host não pode guardar |
|---|---|---|---|
| Tabela dBASE III | .dbf, mais um arquivo de memo .dbt quando necessário | Não (byte 29 fica em zero) | EXlsDbf é levantada antes de gravar, e o .dbf / .dbt recém-criados são apagados |
| SYLK | .slk, .sylk | Não | O SaveAs retorna -1 e não cria arquivo |
| DIF | .dif | Não | O SaveAs retorna -1 e não cria arquivo |
| Texto formatado | .prn | Não | O SaveAs retorna -1 e não cria arquivo |
Dez bytes para um nome de field dBASE, contados na code page
Todo limite de tamanho numa tabela dBASE é uma contagem de bytes. É aí que code pages multibyte machucam. Um field descriptor guarda o nome em onze bytes — dez para o nome, um para o NUL terminador dele — e põe a letra de tipo logo atrás, no offset 11, a largura no 16 e a contagem de decimais no 17. Sob a code page 936 um caractere chinês são dois bytes, então um nome chinês de dez caracteres são vinte. Copie esses bytes para dentro sem medi-los e eles atravessam direto o NUL, a letra de tipo e a largura; nenhum reader recupera esse descriptor depois, mesmo que o nome tenha passado numa checagem de dez caracteres. Mantenha nomes de field dBASE em ASCII mesmo numa estação CJK. Ferramentas xBase, drivers ODBC e software de GIS que leem tabelas de atributos de shapefile todos se chaveiam nesses nomes, e um nome que só decodifica sob 936 vira bytes diferentes em cada máquina rodando outra página
| Bytes | Conteúdo dBASE III |
|---|---|
| 0 | Versão: 0x03, ou 0x83 quando um arquivo de memo .dbt acompanha a tabela |
| 1–3 | Última atualização: ano desde 1900, mês, dia |
| 4–7 | Contagem de records, little-endian de 32 bits |
| 8–11 | Comprimento do header e comprimento do record, palavras de 16 bits |
| 32 em diante | Um descriptor de 32 bytes por field, depois 0x0D; records seguem, cada um atrás de um deletion flag |
Larguras também são bytes. Um field de caracteres (C) tem teto de 254 bytes, valores N são texto alinhado à direita com os decimais declarados, valores D são oito caracteres na forma YYYYMMDD, e um valor largo demais para o field dele vira uma fila de asteriscos, a convenção que o próprio dBASE usa. Texto mais longo vai para um campo memo (M) que guarda um número de bloco no companheiro .dbt: um header de 512 bytes, depois blocos de 512 bytes, cada memo terminado por 0x1A 0x1A. O TXLSXWorkbook.SaveAs com um nome .dbf transforma a primeira linha usada da planilha ativa em nomes de field, infere C, N, L ou D por coluna, cai para C em colunas mistas e move texto com mais de 254 caracteres para M. Quando as larguras precisam ser controladas em bytes, monte a tabela você mesmo com o TXlsDbfTable e dimensione cada field C para o comprimento codificado do maior valor dele
uses
Windows, SysUtils, Variants, lxDbf;
procedure WriteCustomerTable(const FileName: string);
var
Table: TXlsDbfTable;
Row: Integer;
begin
Table := TXlsDbfTable.Create;
try
// Nomes ASCII de no máximo dez bytes; larguras são bytes na code page do host
Table.AddField('CUSTNO', 'N', 6, 0);
Table.AddField('CITY', 'C', 40, 0);
Table.AddField('BALANCE', 'N', 12, 2);
Table.AddField('OPENED', 'D', 8, 0);
Row := Table.AddRow;
Table.SetValue(Row, 0, 1001);
Table.SetValue(Row, 1, WideString(#$5317#$4EAC#$5E02)); // 北京市, 6 bytes na CP936
Table.SetValue(Row, 2, 1234.5);
Table.SetValue(Row, 3, EncodeDate(2026, 11, 12));
try
XlsWriteDbfFile(FileName, Table);
except
on E: EXlsDbf do
// Esperado num host 1252: a mensagem nomeia o field "CITY" e a linha 1,
// e nenhum .dbf fica no disco
raise Exception.CreateFmt('DBF export refused under code page %d: %s',
[GetACP, E.Message]);
end;
finally
Table.Free;
end;
end;
Esperado: num host 936 a tabela é gravada e reabre com os quatro fields. Num host 1252 o XlsWriteDbfFile levanta EXlsDbf nomeando o field e a linha de base um. O XlsWriteDbfStream converte cada nome de field e cada valor C ou M para a página do host e de volta, compara e mede cada nome contra os dez bytes dele antes de o header sair, então uma tabela recusada deixa o stream intocado. A forma arquivo vai um passo além e apaga o .dbf, e qualquer .dbt, que já tinha criado
Como testar se uma string sobrevive à code page do host?
Converta para AnsiString, converta de volta para WideString e compare com o original. Um caractere que a página não tem volta como um substituto, normalmente ?, então a comparação falha exatamente quando o export perderia dados. Os writers usam a mesma conversão, então a checagem e o writer não podem divergir sobre a code page. Guarde também o comprimento em bytes: é o número para confrontar com um slot de nome de dez bytes ou uma largura de field
uses
Variants;
// True quando todo caractere de V sobrevive à code page ANSI do host;
// ByteLen é o comprimento codificado a comparar com uma largura de field
function FitsHostCodePage(const V: Variant; out ByteLen: Integer): Boolean;
var
W: WideString;
A: AnsiString;
begin
W := VarToWideStr(V); // VarToStr pode converter antes de esta checagem ver o texto
A := AnsiString(W);
ByteLen := Length(A);
Result := WideString(A) = W;
end;
O comentário no VarToWideStr carrega a lição real. No Delphi 2007 e anteriores o string é AnsiString em toda parte, e sob Free Pascal a unit Variants é compilada com o tipo string próprio da RTL, AnsiString, não importa o modo da sua unit. Nesses compiladores o VarToStr já pôs um ? onde cada caractere chinês estava. Uma guarda construída sobre AnsiString(VarToStr(V)) então compara interrogações com interrogações e aceita tudo. O TStringList tem o mesmo formato de problema ali: atribua texto wide à propriedade Text dele para partir linhas e todo caractere passa pela code page do host a caminho de dentro. O XlsParseSlk, o XlsParseDifText e o XlsParsePrnText recebem WideString e mantêm todo caractere dele em qualquer host. Se você também compila com Lazarus, veja que tipo de string as units do HotXLS compilam sob Free Pascal
Records SYLK e DIF, linha por linha
SYLK é um record por linha, campos separados por ponto e vírgula. O HotXLS grava um header ID;PHotXLS, um record C;Ylinha;Xcoluna;Kvalor por célula e um terminador E. Texto vai entre aspas com aspas embutidas dobradas, o que também protege um ponto e vírgula dentro de um valor, e o reader aceita o escape ;; que alguns produtores usam em vez disso. Não existe Boolean. Os writers do próprio formato mandam 1 e 0, então é isso que o SaveAs grava; erros saem como texto como #N/A, datas como números de série. Aquelas duas letras no topo são como o Excel reconhece SYLK — e por que ele confunde um CSV cuja primeira célula lê ID com um arquivo SYLK. Construção e parse ambos trabalham sobre WideString, então a ida e volta em memória abaixo dá a mesma resposta em qualquer host
uses
SysUtils, lxSlk;
function SylkRoundTripsInMemory: Boolean;
var
Cells, Back: TXLSSlkCells;
Text: WideString;
I: Integer;
begin
SetLength(Cells, 3);
Cells[0].Row := 1; Cells[0].Col := 1; Cells[0].Kind := slkText;
Cells[0].Text := WideString(#$4E0A#$6D77'; Pudong'); // 上海; Pudong
Cells[1].Row := 1; Cells[1].Col := 2; Cells[1].Kind := slkText;
Cells[1].Text := 'Pipe 12" steel';
Cells[2].Row := 2; Cells[2].Col := 1; Cells[2].Kind := slkNumber;
Cells[2].Number := 1234.5;
Text := XlsBuildSlk(Cells); // K"上海; Pudong", K"Pipe 12"" steel", K1234.5
Result := XlsParseSlk(Text, Back) = Length(Cells);
for I := 0 to High(Cells) do
if Result then
Result := (Back[I].Row = Cells[I].Row) and (Back[I].Col = Cells[I].Col) and
(Back[I].Kind = Cells[I].Kind) and (Back[I].Text = Cells[I].Text) and
((Cells[I].Kind <> slkNumber) or (Back[I].Number = Cells[I].Number));
end;
DIF é mais falante. Quatro grupos de header, TABLE, VECTORS (contagem de colunas), TUPLES (contagem de linhas) e DATA, tomam três linhas cada; cada item de dado toma depois duas, uma linha tipo,número e uma linha guardando a string entre aspas ou um indicador de valor. Uma linha abre com -1,0 e BOT, e -1,0 com EOD fecha o dado. Um número é 0,valor mais V, um lógico 0,1 mais TRUE ou 0,0 mais FALSE, uma string 1,0 mais o texto dela entre aspas. Na leitura, o HotXLS mapeia os indicadores NA e ERROR para #N/A e #VALUE! e trata NULL como vazio. Uma planilha chamada Orders cuja única linha guarda Beijing, 1234.5 e True exporta como:
TABLE
0,1
"Orders"
VECTORS
0,3
""
TUPLES
0,1
""
DATA
0,0
""
-1,0
BOT
1,0
"Beijing"
0,1234.5
V
0,1
TRUE
-1,0
EOD
Uma vírgula decimal quebra os três formatos
A code page não é a única configuração de máquina que vaza para esses arquivos. Numa estação com configurações regionais alemãs ou francesas, o FloatToStr transforma 1234.5 em 1234,5. No DIF a linha de item vira 0,1234,5 e perde a forma tipo,valor. No SYLK, K1234,5 não é número. Num field N dBASE, readers xBase esperam um ponto. O HotXLS grava e lê os três com TFormatSettings.Invariant: o SYLK recebe o token mais curto que lê de volta para o mesmo double, o DIF um FloatToStr invariante com .0 anexado aos inteiros, os fields numéricos dBASE os decimais declarados deles, alinhados à direita. O parse ignora o locale também, então um arquivo de uma máquina de vírgula decimal lê igual em toda parte
Lendo tabelas que outros programas gravaram
Abrir a tabela de outra pessoa põe a pergunta da code page na máquina que lê. O XlsReadDbfFile, e o TXLSXWorkbook.Open num nome .dbf, aceitam bytes de versão 0x03, 0x83, 0x8B, 0x30, 0x31 e 0x32, cobrindo tabelas dBASE III e IV e Visual FoxPro, incluindo os tipos binários do FoxPro I (inteiro de 32 bits), B (double), Y (currency escalada por 10.000) e T (dia juliano mais milissegundos). Texto de memo vem de um .dbt ao lado da tabela, ou de um .fpt com o layout de blocos do FoxPro quando não existe .dbt; a extensão decide, então nenhum companheiro é lido como o outro. Dados de caracteres são decodificados na página do host. Uma tabela dBASE III gravada por um programa DOS sob a code page 866, com o byte 29 em zero, portanto mostra as letras erradas numa máquina 1252. Quando você conhece o produtor, rode o import onde a code page dele é a página do sistema. Arquivos SYLK, DIF e .prn seguem a mesma regra, porque o Open decodifica os bytes deles na página do host antes de o parser de texto wide vê-los
Que formato deve carregar texto não ASCII entre locales?
Nenhum desses quatro, havendo escolha. Use-os quando o sistema receptor insistir — uma tela de import de ERP, uma aplicação xBase, um pipeline de GIS — e quando o dado é ASCII ou os dois lados dividem uma code page. Todo o resto deve viajar num formato que declara a codificação própria: XLSX, SpreadsheetML 2003, ou CSV, que o HotXLS grava como UTF-8 com byte order mark (o export CSV e TSV tem armadilhas de transporte próprias). Como o SaveAs escolhe o formato pela extensão, o seu código precisa tratar os dois estilos de recusa desde a primeira tabela: exceção para .dbf, resultado -1 para os formatos de texto
uses
SysUtils, lxHandleX, lxDbf;
function SaveForLegacyImport(const BaseName: string): string;
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Orders'); // a primeira planilha adicionada vira a ativa
Sheet.Cells[1, 1].Value := 'CUSTNO';
Sheet.Cells[1, 2].Value := 'CITY';
Sheet.Cells[2, 1].Value := 1001;
Sheet.Cells[2, 2].Value := WideString(#$5317#$4EAC); // 北京
Result := BaseName + '.slk';
if Book.SaveAs(Result) <> 1 then
begin
// A code page ativa não pode guardar o texto: nenhum .slk foi criado
Result := BaseName + '.xlsx';
Book.SaveAs(Result);
end;
try
Book.SaveAs(BaseName + '.dbf');
except
on E: EXlsDbf do
Result := Result + ' (no .dbf: ' + E.Message + ')';
end;
finally
Book.Free;
end;
end;
Num host 936 esta função produz um .slk e um .dbf que ambos guardam 北京; num host 1252 ela produz um .xlsx e reporta por que o .dbf foi recusado. Nomes de entrada RTF, HTML e ZIP no componente de planilhas Delphi HotXLS seguem regras de codificação próprias