Artigo Técnico

Conversão sem Perdas de XLSX no Delphi: Tema, extLst, calcChain

O HotXLS, a biblioteca Excel nativa para Delphi e C++Builder, foi concebido para conversões sem perdas (lossless round-trips) em XLSX: abra um livro de trabalho, altere uma célula, grave, e o tema personalizado do cliente, os blocos de extensão extLst desconhecidos e a cadeia de cálculo (calculation chain) sobreviverão. Três mecanismos viabilizam esta operação — o armazenamento em cache literal de xl/theme/theme1.xml, a reserialização baseada em eventos de blocos <ext> desconhecidos e um xl/calcChain.xml novo e válido segundo as especificações em cada gravação de um livro de trabalho com fórmulas

Três mecanismos por trás de um round-trip XLSX sem perdas do HotXLS em Delphi: xl/theme/theme1.xml em cache como bytes brutos e regravado byte a byte idêntico, blocos extLst estrangeiros capturados de eventos XML e reproduzidos, e um calcChain.xml novo e válido segundo a especificação emitido em cada gravação de fórmulas
Cada parte preservada segue o seu próprio caminho na gravação — bytes de tema verbatim, repetição de extLst ao nível de evento e uma cadeia de cálculo regenerada — enquanto o XML da folha e os estilos são reconstruídos a partir do modelo

O cenário que motiva estes três mecanismos é deprimentemente comum. Um serviço de faturação carrega um modelo que o cliente desenhou no Excel — tema de cores corporativo, minigráficos (sparklines) numa coluna de KPIs, uma regra de formatação condicional adicionada por uma versão mais recente do Excel — escreve o total de uma fatura na célula B3 e grava. O cliente abre o resultado e as cores da marca voltaram ao azul padrão do Office, os minigráficos desapareceram e o Excel propõe "reparar" o ficheiro. Nada no código alterou essas funcionalidades. A biblioteca alterou-as, simplesmente ao gravar

Por que razão os ficheiros Excel perdem a formatação após edições com bibliotecas?

Os ficheiros Excel perdem a formatação após edições efetuadas por bibliotecas porque a maioria das bibliotecas não edita o ficheiro — reconstrói-o. Um pacote .xlsx é um ZIP de componentes XML: xl/workbook.xml, um xl/worksheets/sheetN.xml por folha, xl/styles.xml, xl/theme/theme1.xml, xl/calcChain.xml, entre outros. Uma biblioteca típica analisa esses componentes num modelo de objetos ao abrir e regenera cada componente a partir desse modelo ao gravar. Qualquer funcionalidade que o modelo não represente — um tema que nunca tenha analisado, um bloco de extensão de um Excel mais recente — não tem onde residir na memória, pelo que o componente regenerado simplesmente o omite

A norma ECMA-376 antecipou metade deste problema. O SpreadsheetML define extLst (ECMA-376 Parte 1, a "Área de Armazenamento de Dados de Funcionalidades Futuras", §18.2.10 para o elemento ao nível do livro de trabalho) como um ponto de extensão designado: os geradores mais recentes colocam aí funcionalidades, cada uma encapsulada num elemento <ext> que contém um atributo uri que identifica a funcionalidade, e os consumidores mais antigos devem preservar aquilo que não compreendem. Os minigráficos, segmentações de dados (slicers) e novos tipos de formatação condicional são todos transmitidos desta forma. Uma biblioteca que ignore blocos <ext> desconhecidos não é apenas incompleta — viola o contrato de compatibilidade futura em torno do qual o formato foi concebido. A pergunta a fazer a qualquer biblioteca de folhas de cálculo que esteja a avaliar é direta: se eu alterar uma célula, o que mais se altera?

Como é que o HotXLS mantém um tema personalizado byte a byte?

O HotXLS preserva o tema de um livro de trabalho armazenando em cache os bytes originais de xl/theme/theme1.xml no momento de abertura e reescrevendo-os textualmente no momento de gravação. O componente do tema (norma ECMA-376 Parte 1, §14.2.7) é DrawingML, e não SpreadsheetML — esquemas de cores, esquemas de tipos de letra, esquemas de formatação — e um motor de folha de cálculo não necessita de o modelar detalhadamente. As versões anteriores do HotXLS regeneravam um tema Office fixo a cada gravação, o que provocava exatamente a falha referida acima sobre o retorno às cores padrão da marca; desde a versão v2.89.46, o tema do pacote aberto é armazenado no seu estado bruto e reemitido inalterado, e o tema Office integrado é gerado apenas para livros de trabalho criados do zero. Bytes brutos constituem a melhor garantia de fidelidade possível: sem análise, sem reserialização, sem possibilidade de desvios

A cópia literal prevalece deliberadamente sobre o acesso programático ao tema. O TXLSXWorkbook expõe a ThemeMajorFont e a ThemeMinorFont para que possa escolher tipos de letra de cabeçalho e corpo para novos livros de trabalho, mas quando um tema literal foi recolhido no momento de abertura, essas definições não têm qualquer efeito no ficheiro gravado — a integridade da conversão (round-trip) tem prioridade. Se necessitar realmente de alterar o tema de um livro de trabalho existente, isso indica que deve editar o modelo no próprio Excel, e não através de uma API focada em dados. O cenário quotidiano não necessita de qualquer API:

O HotXLS coloca em cache os bytes brutos de xl/theme/theme1.xml ao abrir e regrava-os byte a byte idênticos ao gravar, enquanto uma biblioteca de reconstrução a partir do modelo regenera um tema Office predefinido e desfaz as cores da marca do cliente
Colocar theme1.xml em cache verbatim não precisa de qualquer modelo de tema, e ThemeMajorFont com ThemeMinorFont só estilizam livros que não transportam tema capturado
var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('branded-invoice.xlsx');
    Book.Sheets[0].Cells[3, 2].Value := 42750.00;  // a única alteração
    Book.SaveAs('branded-invoice-out.xlsx');
    // o theme1.xml na saída é byte-idêntico à entrada
  finally
    Book.Free;
  end;
end;

O que acontece aos blocos extLst desconhecidos ao gravar?

O HotXLS captura cada bloco <ext> ao nível da folha de trabalho que não modela nativamente e reproduz-o no extLst da folha de trabalho gravada, garantindo que as funcionalidades escritas por versões mais recentes do Excel sobrevivam intactas à conversão. Desde a v2.131.0, os fragmentos capturados são visíveis através da propriedade apenas de leitura RawWorksheetExts, uma TStringList em cada folha de trabalho XLSX, o que torna a garantia verificável a partir do código de testes, em vez de ser um ato de fé:

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  i: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('from-newer-excel.xlsx');
    Sheet := Book.Sheets[0];
    WriteLn(Format('%d foreign ext block(s) captured',
      [Sheet.RawWorksheetExts.Count]));
    for i := 0 to Sheet.RawWorksheetExts.Count - 1 do
      WriteLn(Copy(Sheet.RawWorksheetExts[i], 1, 100)); // veja cada uri
  finally
    Book.Free;
  end;
end;

O detalhe de implementação que vale a pena conhecer é que a captura consiste numa reserialização ao nível dos eventos, e não numa cópia de bytes brutos. O leitor XML de streaming do HotXLS não expõe desvios de origem, pelo que a subárvore desconhecida é reconstruída a partir de eventos Element, Text e EndElement à medida que passam. Esta abordagem esconde uma armadilha clássica: um elemento de autofechamento como <a/> aciona apenas um evento Element marcado como vazio e nunca um EndElement, pelo que qualquer contador de profundidade que decremente exclusivamente no EndElement nunca verá a subárvore fechar-se. Tratando este caso, o fragmento reconstruído é semanticamente equivalente ao original — a delimitação de atributos e as formas de autofechamento são normalizadas, pelo que não é idêntico em bytes, mas o Excel lê significado, e não bytes. Duas propriedades da própria saída do Excel tornam a reprodução segura: o Excel declara os atributos xmlns necessários no elemento <ext> ou dentro dele, pelo que cada fragmento capturado é autónomo em termos de namespace, e essa mesma autonomia é o motivo pelo qual duplicar uma folha de trabalho dentro ou entre livros de trabalho pode transportar os blocos externos juntamente com uma atribuição simples de string-list

Gravar calcChain.xml para o Excel validar as suas fórmulas

O HotXLS grava o xl/calcChain.xml (o componente Cadeia de Cálculo, norma ECMA-376 Parte 1, §12.3.1) sempre que o livro de trabalho gravado contém fórmulas, e escolhe entre duas ordenações. Se o gráfico de dependências de fórmulas já tiver sido construído e estiver atualizado — chamou o Recalculate após a sua última edição — a cadeia é emitida em ordem topológica completa, as dependências antes dos dependentes, com quaisquer elementos de referência circular anexados no final. Caso contrário, as células são listadas na ordem do documento. Ambas estão corretas: as notas de implementação da Microsoft para o formato, [MS-XLSX], tratam a cadeia de cálculo como uma pista que o Excel valida e reordena durante o carregamento, pelo que qualquer listagem completa é legal, e o HotXLS recusa-se deliberadamente a forçar a construção do gráfico dentro de SaveAs — a construção de arestas é quadrática em relação à contagem de células, um custo oculto inaceitável numa gravação de um milhão de células

O HotXLS emite xl/calcChain.xml por ordem topológica quando o Recalculate construiu o grafo de dependências e por ordem de documento caso contrário; o Excel trata qualquer listagem completa como uma dica e reordena-a durante a carga
Ambas as ordenações permanecem legais porque o Excel reverifica a cadeia ao carregar, e o HotXLS nunca força a construção de grafo de custo quadrático dentro de SaveAs
Book.Open('model.xlsx');
Book.Sheets[0].Cells[10, 4].Formula := '=SUM(D2:D9)';
// Agora gravado, o calcChain.xml lista as células de fórmula por ordem do documento.
// Depois de Recalculate, o grafo de dependências existe, pelo que a mesma gravação
// emite uma ordem topológica completa:
Book.Recalculate;
Book.SaveAs('model-out.xlsx');

Porquê preocupar-se com um componente que o Excel trata como meramente indicativo? Porque a sua ausência é um sinal. Alguns consumidores — heurísticas de reparação, visualizadores de terceiros, ferramentas de diff — esperam que um livro de trabalho com fórmulas traga uma cadeia de cálculo, e uma biblioteca que descarta o componente silenciosamente ao gravar produz ficheiros subtilmente diferentes de tudo o que o Excel escreve. Emitir uma cadeia válida mantém o resultado dentro do envelope contra o qual o resto do ecossistema foi testado, que é o núcleo discreto e pouco glamoroso da engenharia de conversão sem perdas

Onde termina a conversão sem perdas

A honestidade é mais importante aqui do que um visto de marketing, pelo que os limites merecem igual destaque. O HotXLS não copia todo o pacote byte a byte: os ficheiros XML das folhas de trabalho, estilos, strings partilhadas e componentes do livro de trabalho são regenerados a partir do modelo analisado, pelo que a saída é semanticamente fiel mas não binária-idêntica — os cabeçalhos locais do ZIP contêm carimbos de data/hora DOS atualizados. Os fragmentos <ext> capturados regressam normalizados, conforme descrito acima. As sobreposições de tipos de letra do tema programáticas são ignoradas quando existe um tema literal. E a rede de preservação tem uma malha definida: funcionalidades que o HotXLS modela nativamente (minigráficos, por exemplo, são analisados e reescritos em vez de copiados cegamente) mais conteúdo extLst externo mais os componentes mantidos literalmente em cache. Um componente que não seja modelado nem se encontre num ponto de extensão — como o componente personalizado de um suplemento (add-in) exótico — fica fora dos três mecanismos que este artigo aborda, pelo que deve testar os seus modelos reais em vez de assumir

O trabalho de preservação adjacente completa o cenário. Os projetos VBA e as referências de livros de trabalho externos mantêm-se na gravação sob a mesma filosofia de preservar o que não é modelado, descrita no artigo complementar sobre a preservação de VBA e ligações externas, e as propriedades do documento em docProps possuem a sua própria API de leitura e escrita, em vez de serem eliminadas silenciosamente. Ao avaliar qualquer biblioteca de folhas de cálculo, execute o teste de uma única célula: abra um livro de trabalho de produção rico em funcionalidades, altere um único valor, grave e compare (diff) os componentes descompactados com os originais. O que mudou além da folha em que tocou indica-lhe mais sobre a biblioteca do que qualquer matriz de funcionalidades

Os mecanismos de conversão descritos neste artigo — retenção literal de temas desde a v2.89.46, captura de extLst externos e emissão de calcChain.xml desde a v2.131.0 — são fornecidos no atual HotXLS Delphi Excel Component, cuja página de produto documenta o conjunto completo de funcionalidades de leitura e escrita XLSX para Delphi e C++Builder