Uma planilha de quatorze colunas com 45 linhas de dados, salva como ODS, normalmente termina com um elemento a mais do que você espera: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Essa linha única descreve todas as linhas da 46 até o fundo da grade. Expanda-a literalmente, como aquela linha escrita 1.048.530 vezes, e um content.xml pequeno vira um milhão de registros de linha, um milhão de entradas de altura e, se as colunas carregarem um estilo de célula padrão, quase quinze milhões de células vazias com estilo
Por que uma única linha repetida é cara de importar
O custo vem da expansão, e a expansão é o jeito óvio de escrever um importer. O estilo ro1 define style:row-height="0.452cm", que dá 12.81 pontos. Um finalizador de linhas que o aplica com uma chamada SetRowHeight(RowIndex + i, RowHeight) por linha repetida grava 1.048.530 entradas em qualquer tabela de altura por linha que o object model mantenha. Se essa tabela é uma lista de strings com chave em que cada gravação primeiro busca uma chave existente, a importação é quadrática na contagem de repetição, e uma contagem de repetição de um milhão é precisamente a entrada em que quadrático deixa de ser teórico
Células vazias pioram o quadro. Cada <table:table-column> num arquivo desses normalmente nomeia um table:default-cell-style-name, e toda célula vazia na linha repetida o herda. Um vazio com estilo ainda é uma célula para a maioria dos object models. Materialize uma por coluna por linha repetida e a conta de memória ofusca a tabela de alturas
O lado do save tem o problema espelhado. Uma vez que o modelo diz que a linha 1.048.575 tem altura, um writer que caminha da linha 1 até a última linha com qualquer coisa emite um milhão de elementos <table:table-row>. Uma importação barata seguida de um save caro só muda o custo de botão
Como o HotXLS armazena a altura de uma linha repetida
O HotXLS Delphi Excel Component armazena a altura de uma linha ODS repetida como um run: um registro TXLSXRowHeightRun guardando FirstRow, LastRow e uma Height. Quando o TXLSXWorkbook.OpenODS termina uma linha cuja contagem de repetição é 1, ele chama SetRowHeight como qualquer outro código faria. Para qualquer contagem maior ele registra o trecho inteiro como um run, então a cauda do exemplo vira as linhas 46 a 1.048.575 a 12.81 pt. Uma contagem de repetição que passe do fim da grade é cortada na linha 1.048.576 em vez de ser rejeitada
Uma regra do importer surpreende quem faz diff do resultado contra o arquivo de origem. Aplicativos de planilha que gravam ODS tendem a dar um style:row-height a todo estilo de linha, incluindo linhas que ninguém redimensionou, e marcam essas com style:use-optimal-row-height="true". Para uma linha que não armazena célula alguma, uma altura automática é simplesmente a altura padrão, então o HotXLS não registra altura para ela. Linhas que armazenam algo mantêm a altura mesmo quando marcada como ótima, porque uma linha de texto com quebra que cresceu para caber no conteúdo encolheria de volta quando o workbook fosse reaberto como XLSX. Esse trade-off é o certo: descartar uma altura real produz uma mudança de layout visível, enquanto manter uma redundante custa um único run
A leitura das alturas passa pela propriedade RowHeight comum. Ela consulta a lista por linha primeiro e só cai para os runs quando a linha não tem entrada explícita; o HasRowHeight segue a mesma ordem. Runs nunca se sobrepõem, então o fallback para no primeiro run que contém a linha
var
Workbook: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Spans: TXLSXAxisSpans;
I: Integer;
begin
Workbook := TXLSXWorkbook.Create;
try
// Uma planilha cujo último elemento de linha se repete 1.048.530 vezes
Workbook.OpenODS('tail-repeat.ods');
Sheet := Workbook.Sheets[1]; // Sheets é base 1
// Metadados de linha armazenados: entradas de linha única primeiro, depois runs
Spans := Sheet.GetStoredAxisSpans(True);
for I := 0 to High(Spans) do
Writeln(Spans[I].FirstIndex, '..', Spans[I].LastIndex);
// As duas leituras vêm do mesmo run
Writeln(Sheet.RowHeight[46]:0:2, ' pt');
Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
// Uma entrada por linha é consultada antes do run, então ela vence
Sheet.RowHeight[500000] := 36;
// Limpar uma linha dentro do run corta um buraco de uma linha nele
Sheet.ClearRowHeight(500001);
Writeln(Sheet.HasRowHeight(500001)); // FALSE
Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // ainda a altura do run
finally
Workbook.Free;
end;
end;
As duas edições naquele exemplo se comportam diferente de propósito. Sheet.RowHeight[500000] := 36 deixa o run em paz e acrescenta uma entrada à lista por linha, que ofusca o run porque é consultada primeiro. O ClearRowHeight não pode agir assim, já que uma linha limpa precisa reportar nenhuma altura mesmo que um run ainda a cubra. Então ele apaga qualquer entrada por linha e depois reconstrói a lista de runs: todo run fora da linha limpa é mantido como está, o run que a continha é dividido na parte antes e na parte depois, e nada é armazenado para a linha 500.001 em si. O GetStoredAxisSpans(True) agora lista 46..500000 e 500002..1048575 como dois runs separados, ao lado da entrada de linha única para 500.000. O ClearRowHeights esvazia a lista por linha e os runs numa chamada só
Para onde vão os estilos herdados das células vazias?
Estilos herdados de células vazias viram overlays de estilo: retângulos na planilha que carregam um estilo, sem objetos de célula por trás. Ao ler uma linha, o importer percorre as células repetidas em trechos de colunas adjacentes que resolvem para o mesmo estilo. Um trecho cujo estilo parece exatamente com Default não armazena nada. Um trecho com formatação real, digamos um padrão de coluna centralizado ou em negrito, vira um valor de linha cobrindo todas as colunas dele
Quando esse valor de linha é confirmado e a linha se repete, o HotXLS materializa uma única célula no canto superior esquerdo do trecho, aplica o estilo herdado a ela e registra os seis índices de estilo (fonte, fill, borda, formato de número, alinhamento, proteção) através de Sheet.StyleOverlays.Add para o retângulo inteiro: toda coluna do trecho, da primeira linha repetida à última. Nada mais é criado para as 1.048.529 linhas restantes. O Sheet.Cells.Count fica perto do número de células que de fato guardam dados
Uma célula dentro do retângulo pega o overlay no momento em que seu código a toca. Depois de abrir uma planilha cuja cauda é formatada por um padrão de coluna em negrito, Sheet.Cells[700000, 1].FontIndex resolve para a fonte em negrito mesmo que nenhuma célula existisse naquela coordenada uma linha antes. Arquivos XLSX com uma coluna formatada mas vazia passam pela mesma maquinaria, e o artigo sobre armazenamento de células por bloco de linhas e overlays de estilo por intervalo cobre como vários overlays se empilham e resolvem. O importer ODS só difere em de onde vêm os overlays: ele os deriva das contagens de repetição por conta própria
Como o SaveAsODS regrava a contagem de repetição
O TXLSXWorkbook.SaveAsODS grava cada trecho de linhas idênticas como um <table:table-row> com table:number-rows-repeated, e encerra um trecho só onde algo nas linhas muda. A tabela se estende até a linha mais baixa que carrega qualquer coisa: um valor, uma célula vazia só de estilo, uma altura de linha única, o fim de um run de altura, a borda de baixo de um overlay, uma entrada oculta ou de outline, ou uma área de validação ou de formatação condicional
Da linha 1, o writer olha adiante para a próxima fronteira. Os candidatos são a próxima célula materializada, o início ou fim de um run de altura, qualquer entrada de linha única (altura, flag oculta, nível de outline, estado colapsado, quebra de página manual), bordas de overlays, intervalos mesclados, hyperlinks, comentários, imagens, e as bordas de todo retângulo de validação e formatação condicional. Tudo até aquela fronteira é um elemento. Dentro de cada elemento a mesma lógica corre pelas colunas, então um trecho vazio vira um <table:table-cell> com table:number-columns-repeated e o nome de estilo para o qual o overlay resolve
Estilos de linha são compartilhados. A altura da linha é formatada como texto, 12.81pt no exemplo, e esse texto junto com a flag oculta e a flag de quebra de página forma a chave do estilo automático de linha. Toda linha com os mesmos três valores aponta para o mesmo estilo ro<N>, que guarda uma única propriedade style:row-height. Uma linha sem altura, visível e sem quebra não ganha estilo de linha algum
var
Workbook, Reopened: TXLSXWorkbook;
Saved: TMemoryStream;
begin
Workbook := TXLSXWorkbook.Create;
Reopened := TXLSXWorkbook.Create;
Saved := TMemoryStream.Create;
try
Workbook.OpenODS('tail-repeat.ods');
Workbook.Sheets[1].RowHeight[500000] := 36;
Workbook.Sheets[1].ClearRowHeight(500001);
// A cauda é gravada como alguns poucos elementos de linha repetidos
Workbook.SaveAsODS(Saved);
Writeln('ODS stream: ', Saved.Size, ' bytes');
Saved.Position := 0;
Reopened.OpenODS(Saved);
// Esperado: override, buraco e os dois pedaços do run sobrevivem ao round trip
Writeln(Reopened.Sheets[1].RowHeight[500000]:0:2); // 36.00
Writeln(Reopened.Sheets[1].HasRowHeight(500001)); // FALSE
Writeln(Reopened.Sheets[1].RowHeight[500002]:0:2); // a altura do run
finally
Saved.Free;
Reopened.Free;
Workbook.Free;
end;
end;
Para a planilha acima, a cauda se divide em quatro pedaços: linhas 46 a 499.999 na altura do run, linha 500.000 a 36 pt, linha 500.001 sem estilo de linha, e linhas 500.002 a 1.048.575 de novo na altura do run. Qualquer coluna cujo estilo padrão deixou um overlay pode acrescentar uma divisão onde esse overlay termina. As 45 linhas de dados acima da cauda são gravadas um elemento por linha porque cada uma delas guarda células
XLSX é outra história. SpreadsheetML não tem atributo de repetição, e um elemento <row> descreve exatamente uma linha, então salvar uma planilha respaldada por runs como .xlsx enumera as linhas que um run cobre e grava ht e customHeight="1" em cada uma delas. O modelo continua compacto na memória; o formato alvo decide quão grande o arquivo fica. Se um workbook chega como ODS e só precisa sair como ODS, ele nunca paga esse custo por linha
Edições que renumeram linhas
Uma segunda representação das alturas de linha só está correta se toda operação que move linhas mover ela também. A TXLSXWorksheet.InsertRows e a DeleteRows deslocam os runs junto com as listas por linha: a parte de um run antes do ponto de edição fica onde está, as linhas dentro de uma janela apagada são descartadas, e o resto se move pela contagem inserida ou apagada. Um run que atravessa uma inserção portanto vira dois runs com um vão de linhas sem altura entre eles, e um run que atravessa uma remoção fica mais curto
procedure ShiftAroundTheRun(Sheet: TXLSXWorksheet);
begin
Sheet.RowHeight[500000] := 36;
Sheet.ClearRowHeight(500001);
// Duas linhas novas na 500000: o override vai para 500002, o buraco para 500003
Sheet.InsertRows(500000, 2);
Writeln(Sheet.RowHeight[500002]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500003)); // FALSE
Writeln(Sheet.HasRowHeight(500000)); // FALSE: linhas inseridas não têm altura
// Remova-as de novo: o override e o buraco voltam aos números de linha antigos
Sheet.DeleteRows(500000, 2);
Writeln(Sheet.RowHeight[500000]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500001)); // FALSE
Writeln(Sheet.UsedRange.Row2); // inclui o fim do run deslocado
end;
O round trip não deixa a cauda exatamente onde estava. O insert empurra o run da cauda de 500.002..1.048.575 para 500.004..1.048.577, e a grade termina na linha 1.048.576, então a parte além dessa linha é cortada. O delete então move o que sobrou duas para cima, e o run termina na linha 1.048.574: a linha 1.048.575 não tem mais altura armazenada. Se as últimas linhas da grade importam para você, defina a altura delas de novo depois de um par insert e delete. Linhas inseridas no meio de um run também não herdam a altura dele: elas abrem um vão de linhas sem altura e não herdam a altura do run, e é por isso que o HasRowHeight(500000) imprimiu FALSE acima
As outras operações que movem linhas seguem o mesmo padrão. A TileRangeAxisMetadata limpa os runs por todo o trecho tileado e depois registra cada run de origem uma vez por cópia no offset dele. A TXLSXWorksheet.CopyFrom e a TXLSXSheets.AddCopy dão à planilha nova a própria cópia da lista de runs, então um ClearRowHeights num clone deixa as alturas do original no lugar. Do lado da leitura, o UsedRange se estende até a primeira e a última linha de todo run, e o caminhar de linhas do writer XLSX se estende pelos runs para que linhas só de altura continuem sendo gravadas
Linhas ocultas e níveis de outline ficam por linha
Alturas de linha e estilos herdados de células vazias são as duas coisas que colapsam em runs e overlays. Flags de oculto, níveis de outline e estado colapsado são registrados uma vez por linha. Uma linha repetida que está oculta, ou que fica dentro de um <table:table-row-group>, produz uma entrada por linha repetida para cada um desses atributos, e no save cada entrada dessas é uma fronteira própria, então o trecho oculto é gravado com um <table:table-row> por linha
Para templates que você controla, mantenha a parte oculta e agrupada de uma planilha tão enxuta quanto os dados precisam. Ocultar vinte linhas abaixo de um relatório é de graça. Ocultar tudo da linha 46 até o fundo da grade significa um milhão de entradas no load e um milhão de elementos de linha no save, e a única diferença visível é que ninguém consegue mais rolar além do relatório
O lado do formato no nível do workbook, incluindo quais recursos do ODS mapeiam para quais partes do object model, está no walkthrough de abrir e salvar arquivos ODS, e outras alavancas para arquivos grandes estão reunidas em desempenho com workbooks grandes. O elemento de linha em si, com seus atributos de repetição e estilo, é definido no capítulo de tabelas da ODF 1.3 Part 3
Uma repetição de um milhão de linhas é um detalhe do formato que um reader nativo precisa modelar bem, porque não há suíte de office ao fundo para absorvê-lo. A página do componente de planilhas Delphi HotXLS lista os recursos XLS, XLSX e ODS que ele lê e grava, junto com as versões suportadas de Delphi e C++Builder