Um workbook ODS com uma tabela pivô passa por uma biblioteca, uma única célula de origem é editada, e o arquivo salvo abre com todos os valores de célula intactos e o pivô sumido: o content.xml continha um <table:data-pilot-table> na entrada e contém zero na saída. O reparo óvio é copiar o XML do pivô como string. Isso restaura a contagem de elementos, e aí um parser ciente de namespaces recusa a part salva de cara com um erro de unbound-prefix. As duas falhas têm uma raiz só. Um pedaço de XML só viaja com segurança quando as declarações de namespace de que ele depende viajam junto
Onde a definição de pivô ODS vive no content.xml
Uma tabela pivô OpenDocument, que o formato chama de data pilot, é uma definição armazenada depois das planilhas no content.xml, e um writer que reconstrói essa part do próprio object model dele só a mantém se o modelo tiver onde colocá-la. A ODF 1.3 Part 3 §9.6 descreve o contêiner: <table:data-pilot-tables> guarda um <table:data-pilot-table> por pivô, cada um com um table:source-cell-range, um conjunto de filhos table:data-pilot-field, um table:target-range-address e uma lista table:buttons. As células de resultado que o pivô exibe ficam dentro da worksheet como células comuns. Então um writer que só regenera células, estilos, validações, named expressions e database ranges produz uma planilha perfeitamente válida com os mesmos números na tela e nenhum pivô por trás deles
O HotXLS Delphi Excel Component lê a subárvore <table:data-pilot-tables> inteira do content.xml durante o OpenODS, antes de parsear uma única célula, e então tenta traduzir a coleção para o modelo de pivô dele. Uma definição na forma simples que o modelo representa, com intervalo de origem local, um campo de linha, medidas de agregação e resultados armazenados que batem com os dados de origem, vira uma TXLSPivotTable na lista PivotTables da worksheet, o mesmo objeto que você obtém de um pivô XLSX e pode estender com calculated fields e atualizar do Delphi. Qualquer coisa fora dessa forma mantém a coleção como XML capturado, e o SaveAsODS a grava de volta. Um atributo num namespace que o tradutor não conhece já basta: pivôs salvos pelo LibreOffice podem carregar atributos loext: e calcext:, e um atributo loext: manda a definição pelo caminho capturado
O fallback é tudo ou nada por workbook. Se um pivô numa coleção de três cai fora da forma traduzível, os três ficam como XML capturado. Separá-los deixaria o content.xml com duas fontes de verdade para um contêiner, metade reconstruída de objetos e metade repetida de texto, e qualquer discordância entre as metades cairia no arquivo do usuário. Manter a coleção inteira é o padrão mais seguro, e é a mesma política que o HotXLS aplica a blocos extLst desconhecidos no XLSX: o que o modelo não consegue carregar é mantido como foi escrito
Por que uma definição de pivô fatiada quebra o arquivo salvo?
Uma definição de pivô cortada do content.xml com busca de string perde as declarações de namespace que dão significado aos prefixos dela, e a spec Namespaces in XML trata um prefixo sem declaração como erro de boa formação. A abordagem por string é curta: decodifique a part para um WideString, ache <table:data-pilot-tables com Pos, ache a tag de fechamento correspondente depois dela e copie tudo no meio. O texto que sai parece completo. Ele contém loext:ignore-selected-page="true" num page field e calcext:repeat-item-labels="false" em todos os níveis, mas as declarações xmlns:loext e xmlns:calcext que ligam esses prefixos ficam na raiz do documento, <office:document-content>, milhares de caracteres antes
Namespaces in XML 1.0 §6.1 mantém uma declaração em escopo da start tag que a carrega até a end tag correspondente, e todo nome com prefixo dentro resolve contra ela. Corte uma subárvore do documento e você a corta desse escopo. Quando o fragmento é colado num documento novo, cada prefixo só resolve se a nova raiz por acaso o declarar. O HotXLS escreve o próprio <office:document-content> dele com os namespaces que o writer dele usa, entre eles office, table, text, style, fo, draw, calcext, tableooo e of. Na cópia fatiada table: resolve, calcext: resolve, e loext: não. Um prefixo sem ligação basta para um parser ciente de namespaces rejeitar a part inteira, então o pivô não degrada graciosamente; os dados de planilha em volta dele também ficam ilegíveis
Busca de string tem outros pontos cegos que uma captura baseada em reader nunca encontra. O Pos casa o texto da tag dentro de um comentário, dentro de uma seção CDATA ou dentro de um valor de atributo que o cite. Ele não distingue elementos aninhados de mesmo nome. E ele casa o prefixo literal table:, enquanto a ODF permite que um produtor ligue o namespace table a qualquer prefixo que preferir
Como o HotXLS leva o escopo de namespace para o fragmento
O HotXLS captura a subárvore do pivô com um reader XML de streaming que rastreia cada declaração de namespace junto com a profundidade em que apareceu, e copia as declarações ainda em vigor para a tag de abertura do fragmento no momento em que o elemento alvo chega. A pilha cresce a cada start tag que declara xmlns ou xmlns:prefix e encolhe quando o elemento que declarou uma ligação fecha. Elementos vazios precisam do mesmo tratamento: <x xmlns:p="..."/> nunca produz um evento separado de end-tag, então o pop precisa disparar na start tag vazia também, senão uma ligação vaza para os irmãos
No alvo, a captura percorre a pilha da ligação mais interna para fora e escreve cada prefixo uma vez. Percorrer de dentro para fora é o que implementa shadowing. Se a <office:spreadsheet> religa um prefixo que a raiz já declarou, o valor mais próximo é o que vale no pivô, exatamente como o §6.1 exige, e um conjunto de prefixos já escritos impede que o valor externo o siga. Um prefixo que o próprio elemento do pivô declara é pulado, porque escrever xmlns:table duas vezes na mesma tag é um outro erro de boa formação
O alvo é casado por namespace, então um produtor que escreve <t:data-pilot-tables> com t ligado ao URI de table da ODF continua sendo reconhecido, enquanto as tags capturadas mantêm a grafia do arquivo. Nós de texto com whitespace voltam exatamente como escritos. O quoting de atributos e a escolha entre <x/> e <x></x> são normalizados, já que o fragmento é re-serializado dos eventos do reader em vez de copiado byte a byte. Se o content.xml termina com a subárvore do pivô ainda aberta, como acontece num arquivo truncado, a abertura falha com exceção em vez de guardar meia definição que seria gravada de volta no próximo save
O conjunto levado é deliberadamente generoso. A captura copia toda ligação em escopo, use a subárvore ou não, então a raiz do fragmento repete xmlns:table e xmlns:calcext mesmo que a raiz do documento salvo também as declare. Redeclarar um prefixo num escopo aninhado é legal sob a spec Namespaces in XML, e um arquivo de um produtor que declara dezenas de namespaces na raiz ganha alguns kilobytes. Podar a lista significaria escanear a subárvore atrás de cada prefixo em nomes de elemento, nomes de atributo e valores de atributo que guardam QNames, e um único erro aí traz de volta a falha de unbound-prefix. Umas declarações redundantes custam muito menos que esse risco
Abrindo, editando e salvando um workbook que carrega um pivô
Do lado de quem chama não há nada para ligar: a definição do pivô vem junto com um OpenODS e um SaveAsODS comuns. As worksheets são numeradas a partir de 1, e os dois métodos devolvem 1 em caso de sucesso:
uses
System.SysUtils, lxHandleX;
procedure EditPivotSource(const InFile, OutFile: string);
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
if Book.OpenODS(InFile) <> 1 then
raise Exception.CreateFmt('Cannot open %s', [InFile]);
// A E2 está dentro de um intervalo de origem do pivô de Sheet1.A2:E30
Book.Sheets[1].Cells[2, 5].Value := 1250.0;
if Book.SaveAsODS(OutFile) <> 1 then
raise Exception.CreateFmt('Cannot save %s', [OutFile]);
finally
Book.Free;
end;
end;
No content.xml salvo, a coleção de pivôs fica dentro da <office:spreadsheet> logo depois de <table:named-expressions> que o writer gera e antes de <table:database-ranges>. Um workbook que carrega tanto um AutoFilter, que o HotXLS grava como database range, quanto um pivô põe os dois elementos lado a lado, e se o seu pipeline roda um validador de schema ODF estrito, esse par é o primeiro lugar a olhar
A edição naquele exemplo muda um valor de origem e deixa as células de resultado do pivô como estavam. Uma definição capturada é repetida como escrita: o HotXLS não a recalcula, e o aplicativo de planilha que abre o arquivo recalcula o pivô no próximo refresh dele. O mesmo vale para os endereços. Renomeie Sheet1 ou mova o bloco de origem, e a definição salva ainda aponta para Sheet1.A2:E30, que o aplicativo consumidor reporta como intervalo quebrado quando atualiza. Mantenha nomes de planilha e o bloco de origem estáveis em qualquer workbook cujos pivôs viajem desse jeito
Que caminho um dado pivô tomou?
A verificação mais rápida é a lista PivotTables da worksheet: uma definição traduzida aparece lá como TXLSPivotTable, e uma capturada não. O relatório de conversão responde à outra metade da pergunta, o que aconteceria com o pivô num formato de saída diferente. A GetConversionReport inspeciona o workbook sem salvá-lo ou recalculá-lo, e quem chama é dono do relatório devolvido:
uses
System.SysUtils, lxHandleX, lxDiagnostics;
function DescribeOdsPivots(Book: TXLSXWorkbook): string;
var
I, Modeled: Integer;
Report: TXLSConversionReport;
begin
Modeled := 0;
for I := 1 to Book.Sheets.Count do // Sheets é base 1
Inc(Modeled, Book.Sheets[I].PivotTables.Count);
Report := Book.GetConversionReport(xlsConversionOpenXmlWorkbook);
try
for I := 0 to Report.Count - 1 do
if (Report[I].Feature = 'OpenDocumentPivotTables') and
(Report[I].Fidelity = xlsConversionDropped) then
Exit('Captured ODF pivot XML: keep this workbook on .ods');
finally
Report.Free;
end;
if Modeled > 0 then
Result := Format('%d pivot table(s) in the pivot model', [Modeled])
else
Result := 'No pivot definitions';
end;
O caminho de saída importa porque o XML capturado é vocabulário ODF e só o writer ODS o emite. Salvo como .xlsx, a coleção capturada não tem contraparte em SpreadsheetML, e o relatório a lista sob OpenDocumentPivotTables com xlsConversionDropped. O writer binário .xlsb recusa um workbook que ainda a contenha. Um pivô traduzido é um objeto de modelo comum e segue as regras do formato alvo em vez disso. As diferenças entre os caminhos de importação e exportação ODS valem aqui como em todo lugar, e para arquivos com pivôs a rota segura é .ods entra, .ods sai
Mais uma regra decorre do fallback tudo-ou-nada. Um workbook ou repete uma coleção capturada ou escreve os pivôs dele do modelo; ele nunca mistura os dois num mesmo content.xml. Adicionar um pivô com AddPivotTable a um workbook que já contém XML de pivô capturado faz o save ODS parar com erro em vez de emendar XML gerado ao lado de XML repetido
Conferindo a part salva com um segundo parser
Uma contagem de elementos não prova nada sobre namespaces. Uma expressão regular que acha start tags data-pilot-table no content.xml salvo vê texto, e a cópia fatiada da segunda seção passa nesse teste. Um parser ciente de namespaces que não tem nada a ver com a biblioteca que escreveu o arquivo é um juiz muito mais estrito, e o Delphi já vem com um. A função abaixo é uma verificação para você acrescentar à sua própria suíte de teste; TZipFile e TXMLDocument vêm da RTL do Delphi, não do HotXLS:
uses
System.SysUtils, System.Classes, System.Zip,
Xml.xmldom, Xml.XMLIntf, Xml.XMLDoc;
// Seu próprio helper de teste: True quando o content.xml de um pacote .ods
// carrega num parser DOM ciente de namespaces
function OdsContentParses(const OdsFile: string; out Error: string): Boolean;
var
Zip: TZipFile;
Bytes: TBytes;
Stream: TBytesStream;
Doc: IXMLDocument;
begin
Error := '';
Zip := TZipFile.Create;
try
Zip.Open(OdsFile, zmRead);
Zip.Read('content.xml', Bytes);
finally
Zip.Free;
end;
Stream := TBytesStream.Create(Bytes);
try
Doc := TXMLDocument.Create(nil);
try
Doc.LoadFromStream(Stream); // um prefixo sem ligação falha aqui
Result := True;
except
on E: EDOMParseError do
begin
Error := E.Message;
Result := False;
end;
end;
finally
Stream.Free;
end;
end;
Rode em todo .ods que seu código salva, depois vá um passo além para arquivos com pivôs e compare a subárvore do pivô da entrada e da saída depois de parsear: tag, atributos ordenados, texto e filhos, recursivamente, com nomes expandidos para URI de namespace mais nome local. Essa comparação deixa passar um prefixo reescrito e pega um sem ligação, que é exatamente a linha entre inofensivo e quebrado. Alimente-a com arquivos de todo produtor que seus usuários realmente rodam. Um documento que liga o namespace table a um prefixo diferente, declara um prefixo no próprio elemento do pivô ou religa um na <office:spreadsheet> exercita as regras de pular e shadowing de um jeito que um único arquivo do LibreOffice não exercita
O reader e o writer ODS, o modelo de pivô para XLSX e ODS, e o relatório de conversão usado acima fazem parte do HotXLS Delphi Excel Component; a página do produto cobre o suporte completo de leitura e gravação XLSX, XLS e ODS para Delphi e C++Builder