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. O que não faz é tornar as duas direções simétricas. A exportação transporta muito mais informação do que a importação recupera, e uma equipa que assuma o contrário verá fórmulas e formatações evaporarem-se algures entre a revisão do cliente e o relatório seguinte, sem qualquer erro que aponte o problema

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, tipos de letra, preenchimentos e larguras de colunas. Descarta margens, intervalos unidos, 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 ignora deliberadamente

Leia esta parte com atenção antes de prometer fidelidade de round-trip a alguém. A importação de ODS no HotXLS é deliberadamente um caminho leve. Preserva os valores de células escalares e o resultado em cache que cada fórmula continha no momento da gravação, e expande linhas e colunas repetidas ao longo da grelha. Não importa estilos, expressões de fórmulas ODS ou desenhos

A escolha relativa às fórmulas é a que tem maior probabilidade de causar problemas, e foi tomada de forma deliberada. 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 a mesma. Traduzir o OpenFormula para a sintaxe de fórmulas do Excel é um problema de conversão de dialeto próprio, com casos limite reais em torno de vocabulários de funções, sintaxe de referências e modelos de erro. Ler o valor em cache contorna toda essa classe de traduções incorretas e silenciosas, pelo que os números importados são exatamente os números que o remetente viu pela última vez. O custo é que chegam como números, não como as fórmulas ativas que os geraram

O modo de falha a contornar decorre diretamente daqui: uma folha de cálculo cujos totais estavam corretos quando o LibreOffice a guardou pela última vez é importada com os números corretos, mas esses números são agora constantes. Se editar uma célula de input e recalcular, nada muda – a fórmula desapareceu, restando apenas o seu resultado final. Se o fluxo de trabalho exigir fórmulas ativas após a importação, restabeleça-as programaticamente a partir das suas próprias regras de negócio através de Cell.Formula, que na fachada XLSX aceita a expressão sem o sinal de igual no início

Desenhar em torno do round-trip assimétrico

A exportação gera a partir de todo o modelo do livro em memória: valores, estilos e, se solicitado, gráficos e imagens. A importação devolve apenas valores. Portanto, a etapa de .xlsx para .ods é de alta fidelidade, mas a etapa de .ods para .xlsx traz de volta valores e resultados em cache, mas sem estilo e sem fórmulas ativas. Ao encadear as duas, a assimetria acentua-se. Um ciclo completo de .xlsx para .ods para .xlsx escreve tudo fielmente na saída e perde os estilos e fórmulas no regresso, muito embora nada tenha falhado em qualquer um dos passos

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 assimétrico do HotXLS a partir de Delphi: exportação com fidelidade total do modelo de pasta de trabalho em memória e uma importação apenas de valores que deixa fórmulas como constantes
A exportação renderiza o modelo completo em memória, enquanto a importação retorna valores e resultados em cache, então um ciclo completo de .xlsx para .ods para .xlsx descarta silenciosamente estilos e fórmulas vivas
Book := TXLSXWorkbook.Create;
try
  Book.Open('vendor-revision.ods');          // formato detectado automaticamente
  if Book.SourceFormat = xlsxOpenDocumentSpreadsheet then
  begin
    // Valores e resultados de fórmulas em cache estão presentes após uma
    // importação ODS; estilos e fórmulas vivas não estão. Reconstrua o que
    // for necessário para o pipeline seguinte 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;