Artigo Técnico

HotXLS Delphi Component: ODS open, save, and round-trip in Delphi

Um backend de relatórios em Delphi que emite arquivos .xlsx há anos depara-se com um novo requisito: as regras de contratação pública de um cliente exigem outputs no formato OpenDocument Spreadsheet, e os analistas desse cliente enviam as suas edições de volta como arquivos .ods guardados a partir do LibreOffice. Assim, o mesmo código tem agora de escrever e ler ODS. O HotXLS, a biblioteca nativa de folhas de cálculo em Object Pascal da losLab para Delphi e C++Builder, lida com ambas as direções sem necessidade de ter o Excel ou o LibreOffice instalados. As duas direções são parecidas, mas não idênticas. Desde o HotXLS 2.88.229, o caminho de importação traz as fórmulas de volta como fórmulas vivas ao lado dos resultados em cache, e as versões atuais também leem estilos, formatos numéricos, intervalos mesclados, comentários, imagens, gráficos e formatos condicionais. O que ainda não sobrevive é a semântica completa do OpenFormula e uma série de detalhes no nível do pacote, de modo que uma equipe que assuma um espelhamento perfeito ainda vai esbarrar em casos de borda em algum ponto entre a revisão do cliente e o relatório seguinte, geralmente sem nenhum erro para apontar

O suporte a ODS reside na fachada XLSX, não na XLS

O HotXLS disponibiliza duas hierarquias de classes independentes num único pacote: TXLSWorkbook na unit lxHandle para arquivos binários BIFF8 .xls, e TXLSXWorkbook na unit lxHandleX para pacotes OOXML .xlsx. Todos os pontos de entrada OpenDocument - OpenODS, SaveAsODS, GetODSSheetNames - dependem de TXLSXWorkbook. Esta localização não é arbitrária. Um pacote ODS, conforme especificado no ODF 1.3 da OASIS, é um arquivo zip contendo um membro mimetype, um manifesto e um corpo content.xml, o que o torna um primo estrutural do zip OOXML; o BIFF8 é um fluxo de registros binários dos anos 90 com o qual nada tem em comum

Esta organização tem um aspeto prático: um livro legacy .xls não se pode tornar num .ods numa única chamada. Primeiro, faz a ponte do conteúdo BIFF para o modelo XLSX com SaveXLSWorkbookAsXLSX a partir da unit lxXlsxExport, reabre o resultado através de TXLSXWorkbook e depois exporta a partir daí. Esta transição não é isenta de perdas, e convém conhecer as lacunas antes de construir sobre ela. Copia valores, fórmulas, formatos numéricos, fontes, preenchimentos, bordas, intervalos mesclados, larguras de colunas e alturas de linhas. Descarta comentários, gráficos e formatação condicional. Uma origem .xls com formatação pesada chegará ao ODS com um aspeto mais simples do que o original, e isso é uma propriedade da ponte de conversão, não do escritor ODS

A deteção no lado da importação é automática. O método simples Open reconhece um pacote ODS pelo seu membro mimetype, recorrendo a uma verificação do content.xml de nível superior quando esse membro está ausente, pelo que um caminho de código genérico de 'abrir o que quer que o usuário tenha carregado' não necessita de fazer análises à extensão do arquivo. Após a abertura, a propriedade SourceFormat indica qual ramificação foi acionada

Diagrama do layout de classes do HotXLS em Delphi em que todo ponto de entrada ODS vive em TXLSXWorkbook e uma ponte SaveXLSWorkbookAsXLSX carrega conteúdo .xls BIFF8 através
Todo ponto de entrada OpenDocument pende de TXLSXWorkbook, e um .xls legado chega ao ODS apenas através da ponte com perdas de BIFF para XLSX

Exportar para ODS com TODSExportOptions

A chamada de exportação em si resume-se a uma linha; o objeto de opções em seu redor contém as decisões sobre as quais um revisor irá perguntar mais tarde:

O objeto de opções pertence ao chamador. O HotXLS não o irá libertar, razão pela qual o bloco try..finally interno é necessário e não opcional. As duas propriedades que alteram o output, em vez de apenas o etiquetar, merecem uma análise mais detalhada. Definir IncludeCharts := False faz mais do que ocultar gráficos: remove do pacote os subdocumentos de gráficos e as suas entradas de manifesto, o que é exatamente o pretendido quando o consumidor é um pipeline de dados que falharia ao encontrá-los. O Generator substitui a string ODF meta:generator, que de outra forma seria HotXLS/<versão>; altere-a quando as ferramentas a jusante identificam os produtores do arquivo para encaminhamento de suporte. Se nada disto se aplicar, ignore completamente o objeto de opções. Chamar SaveAs(FileName, xlsxOpenDocumentSpreadsheet) é o mesmo que chamar SaveAsODS com as definições padrão, e as sobrecargas de fluxo (stream) em ambos permitem escrever o pacote diretamente numa resposta HTTP sem criar arquivos temporários

var
  Book: TXLSXWorkbook;
  Opts: TODSExportOptions;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-report.xlsx');
    Opts := TODSExportOptions.Create;        // a posse é do chamador, que também libera
    try
      Opts.Generator := 'ReportService 4.2'; // substitui meta:generator
      Opts.IncludeCharts := True;
      Opts.IncludeImages := True;
      Book.SaveAsODS('quarterly-report.ods', Opts);
    finally
      Opts.Free;
    end;
  finally
    Book.Free;
  end;
end;

O que o caminho de importação lê, e o que ele ainda ignora

Leia esta parte com atenção antes de prometer fidelidade de round-trip a alguém. A importação de ODS no HotXLS começou como um caminho leve que guardava apenas valores escalares e resultados de fórmulas em cache, e cresceu bem além disso. As versões atuais leem o content.xml junto com o styles.xml e o settings.xml: valores escalares e resultados de fórmulas em cache, linhas e colunas repetidas expandidas na grade, intervalos mesclados, rich text, hyperlinks, comentários, imagens e gráficos embutidos, estilos de célula herdados e default, formatos numéricos e de data, alturas de linha e larguras de coluna, intervalos nomeados, freeze panes, intervalos de AutoFilter, validações de dados e formatos condicionais. O que o OpenODS ainda não converte é a semântica completa do OpenFormula, chaves de proteção ODS, geometria de comentários, formatação avançada de comentários e recursos de pacote fora do conteúdo de planilha

As fórmulas são a parte com maior probabilidade de morder. Uma célula ODF armazena duas coisas lado a lado: a expressão da fórmula, escrita no dialeto OpenFormula definido no ODF 1.3 Parte 4, e o último valor que a aplicação geradora calculou para ela. As versões antigas liam apenas o valor em cache. Desde o HotXLS 2.88.229, o OpenODS também importa a expressão: uma célula escrita com o prefixo of:= ou oooc:= mantém o valor em cache e recebe uma fórmula Excel normalizada, com referências entre colchetes como [.A1:.B1] e separadores de argumento por ponto e vírgula convertidos para a sintaxe do Excel. Desde a 2.384.32, a importação também converte linhas de arrays inline ({1;2|3;4} vira {1,2;3,4}) e o operador de união de referências ~, e desde a 2.384.55 tal união é lida de volta sem um segundo par de parênteses. Traduzir OpenFormula para a sintaxe do Excel continua sendo um problema de conversão de dialeto, com casos de borda reais em torno de vocabulários de funções, sintaxe de referências e modelos de erro, e é por isso que o valor em cache vem junto: os números que você importa são exatamente os números que o remetente viu pela última vez

O modo de falha para se projetar em torno diminuiu proporcionalmente. Antes da 2.88.229, uma planilha cujos totais estavam corretos quando o LibreOffice a salvou pela última vez era importada com os números corretos, que agora eram constantes: editar uma célula de entrada e recalcular não movia nada. Nas versões atuais, as fórmulas voltam vivas, então o risco restante é um construto que o tradutor não modela, o que pode deixar uma fórmula que recalcula de forma diferente da aplicação geradora enquanto o valor em cache ainda mostra o resultado do remetente. Compare os resultados recalculados com os valores em cache importados para as fórmulas de que o seu pipeline depende, e restabeleça, a partir das suas próprias regras de negócio e por meio do Cell.Formula, qualquer fórmula que discorde — na fachada XLSX ele aceita a expressão sem o sinal de igual no início

Projetando em torno das lacunas restantes do round-trip

A exportação renderiza a partir de todo o modelo do livro em memória: valores, fórmulas, estilos e, se solicitado, gráficos e imagens. A exportação de fórmulas foi sendo apertada release a release. Desde a 2.384.55, arrays inline, uniões de referências e referências absolutas mantêm o significado em OpenFormula; desde a 2.384.56, o HotXLS declara os namespaces dos seus prefixos de fórmula, de modo que o LibreOffice avalia as fórmulas em vez de mostrar #VALUE! em cada célula de fórmula; e desde a 2.384.65, referências de coluna inteira e de linha inteira são gravadas na forma OpenFormula [.A:.A], de modo que o Excel não as abre mais como #NAME?. Os formatos condicionais seguiram: desde a 2.384.66, regras baseadas em fórmula são gravadas como formula-is(...) com a célula base delas, forma que o LibreOffice reconhece, e desde a 2.384.69 regras de valor e de fórmula também são gravadas como condições style:map padrão, a única forma que o Excel lê, enquanto o lado da importação lê as duas formas. Portanto, um ciclo completo de .xlsx para .ods para .xlsx em uma versão atual mantém valores, fórmulas, estilos e formatos condicionais; o que ele ainda pode perder fica nas lacunas listadas na seção anterior

O padrão de arquitetura que daqui resulta: trate os arquivos .ods de entrada como feeds de dados, não como documentos para editar no local. Mantenha o livro de cálculo canónico em .xlsx, leia os valores a partir das revisões dos clientes e emita novos ODS sob procura a partir da cópia canónica. A verificação cabe a ambos os lados – abra os arquivos exportados no LibreOffice Calc, o leitor ODF de referência, e no Excel, que lê ODS há anos mas diverge do LibreOffice nos limites do suporte a gráficos e estilos. A contagem de folhas, um punhado de células-chave e a presença de gráficos constituem um teste de validação suficiente por perfil de exportação

Diagrama do round-trip ODS do HotXLS a partir de Delphi: exportação a partir do modelo de pasta de trabalho em memória e uma importação que as versões antigas limitavam a valores, deixando fórmulas como constantes antes do HotXLS 2.88.229
As versões antigas importavam apenas valores e resultados em cache, então um ciclo completo de .xlsx para .ods para .xlsx descartava estilos e fórmulas vivas; as versões atuais leem os dois de volta
Book := TXLSXWorkbook.Create;
try
  Book.Open('vendor-revision.ods');          // formato detectado automaticamente
  if Book.SourceFormat = xlsxOpenDocumentSpreadsheet then
  begin
    // Desde o HotXLS 2.88.229, as fórmulas voltam vivas ao lado dos
    // resultados em cache; restabeleça pelo Cell.Formula qualquer fórmula
    // de que o pipeline seguinte não possa confiar como traduzida antes de salvar.
    Book.Sheets[0].Cells[2, 5].Formula := 'SUM(B2:D2)';
    Book.SaveAs('vendor-revision.xlsx');
  end;
finally
  Book.Free;
end;

Sondar um arquivo ODS antes de avançar para a importação

Quando um endpoint aceita uploads, listar os nomes das folhas é muito mais barato do que uma análise completa e deteta surpresas estruturais logo no início:

A convenção de retorno costuma confundir os programadores: as chamadas do HotXLS devolvem geralmente uma contagem positiva ou 1 em caso de sucesso e -1 em caso de falha, limpando a lista ao falhar, pelo que deve testar <= 0 em vez de comparar com um valor positivo específico. O GetODSSheetNames não reinicia nem preenche a instância do livro de cálculo, pelo que um único objeto de sondagem pode examinar um diretório inteiro de arquivos recebidos. Verificações estruturais como esta detetam a falha mais comum no mundo real – um analista mudar o nome ou apagar uma folha antes de enviar a revisão de volta – logo no início, onde a mensagem de erro ainda pode identificar o arquivo e a folha em falta, em vez de se manifestar como uma referência nula três níveis abaixo

Diagrama do portão de triagem de uploads do HotXLS em Delphi em que GetODSSheetNames rejeita pacotes ODS ilegíveis e planilhas ausentes antes de uma importação completa rodar
Uma sonda GetODSSheetNames custa muito menos que uma interpretação completa e captura a falha de planilha renomeada enquanto o erro ainda pode nomear o arquivo

Se estiver a construir um pipeline de conversão mais amplo em torno disto, o artigo sobre auditoria de livros e bancada de conversão mostra como inventariar as funcionalidades de um arquivo antes de escolher um formato de destino, e o guia de desempenho para grandes livros de cálculo mantém as exportações em lote dentro de limites de memória razoáveis

O HotXLS é um biblioteca nativa de folhas de cálculo para Delphi e C++Builder com código fonte completo; a lista completa de funcionalidades e os detalhes de licenciamento encontram-se na página do Componente HotXLS

Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
  if Book.GetODSSheetNames('incoming.ods', Names) <= 0 then
    raise Exception.Create('not a readable ODS package');
  if Names.IndexOf('Data') < 0 then
    raise Exception.Create('revision is missing the Data sheet');
finally
  Book.Free;
  Names.Free;
end;