O substream PivotCache do BIFF guarda o dataset em cache de uma PivotTable separadamente da view que a exibe, e o HotXLS lê e escreve esse substream inspecionando os corpos dos records em vez de confiar nos números deles. Essa distinção é a história inteira: o mesmo número de record carrega dois layouts de corpo incompatíveis, dependendo de qual writer produziu o arquivo, então o leitor decide o enquadramento pelo primeiro corpo de record que vê
Você encontra essa camada no instante em que uma PivotTable precisa sobreviver a um round trip. Uma view de pivot sem o cache dela é uma casca vazia, e o Excel vai reconstruir o cache a partir do intervalo de origem quando abrir o arquivo, o que funciona bem até o ponto em que o intervalo de origem sumiu, os dados foram colados de uma query, ou a workbook é um fechamento arquivado que não pode mudar quando alguém abrir
Duas estruturas, dois lugares no arquivo
Os dados em cache e a definição do cache vivem em partes diferentes da workbook, e confundi-los é a primeira coisa a acertar. Os records em cache formam um substream próprio, dado em [MS-XLS] §2.1.7.12 como PIVOTCACHE = SXDB SXDBEx *SXFORMULA *FDB *DBB EOF. Note o que está ausente: não há BOF no começo dessa produção
A definição fica nos globals da workbook, como PIVOTCACHEDEFINITION = SXStreamID SXVS [SXSRC] [SXADDLCACHE] (§2.1.7.20.3), posicionada depois dos records de formatação e antes dos records BoundSheet e Country. Então um único cache é descrito em dois lugares separados por centenas de records, e a ligação entre eles é um identificador de stream que precisa bater em três locais ao mesmo tempo
Cada cache pertence a um stream sob _SX_DB_CUR cujo nome é a grafia hexadecimal maiúscula de quatro dígitos do seu identificador. O SXStreamID.idStm, o campo idstm repetido no cabeçalho do SXDB, e esse nome de stream precisam todos casar. Ao alocar um novo identificador, reserve primeiro todo número já lido do arquivo, ou um cache novo pode reivindicar um número que pertence a um cache mais antigo que o leitor ainda não alcançou
Mais um identificador pega as pessoas. O valor iCache numa view de pivot é a posição zero-based do SXStreamID correspondente na sequência global, e não um identificador de cache que você escolhe. Na escrita ele precisa ser mapeado do objeto de cache para a sua posição real de saída, e as views existentes precisam ser renumeradas junto, ou atualizar um cache aponta silenciosamente uma view para outro
var
Book: TXLSWorkbook;
Cache: TXLSPivotCache;
Field: TXLSPivotCacheField;
V: TXLSPivotCacheValue;
begin
Book := TXLSWorkbook.Create(nil);
try
Book.LoadFromFile('sales.xls');
Cache := Book.PivotCaches.Add;
Cache.SourceRangeSheet := 'Data';
Cache.SourceFirstRow := 1; Cache.SourceFirstCol := 1;
Cache.SourceLastRow := 500; Cache.SourceLastCol := 6;
Cache.SourceDataType := 1; // SXVS SHEET, MS-XLS 2.4.317
Cache.RefreshOnLoad := False; // confia nos records em cache
Cache.SaveData := True;
Field := Cache.AddField('Region', xlpcftString);
V.ValueType := xlpcftString;
V.StrValue := 'North';
Field.FindOrAddItem(V);
Cache.SetRecordCount(0); // limpa, e depois dimensiona o grid de records
Cache.SetRecordCount(500);
Book.StorePivotCaches;
finally
Book.Free;
end;
end;
O SetRecordCount duplo não é superstição. O RecordCount é uma escrita de propriedade comum que não aloca, e o caminho interno de crescimento só inicializa as linhas recém-adicionadas, então um cache cuja contagem foi definida pelo caminho do cabeçalho pode acabar com um grid de índices de comprimento zero. Escritas em RecordIndices são então descartadas sem erro. Definir a contagem para zero e de volta restabelece o grid, e isso tem de acontecer depois de todo campo ter sido adicionado, porque a largura da linha vem da contagem de campos
Por que um número de record não pode dizer o layout do corpo?
Porque os números de record e os layouts de corpo mudaram em momentos diferentes, então o mapeamento entre eles não é uma função. Um número do conjunto legado só aparece em arquivos de writers mais antigos, o que o torna um sinal confiável numa direção. Outro número é genuinamente ambíguo: aparece tanto em arquivos corretos quanto numa faixa de versões intermediárias que usavam o número novo com o layout de corpo velho
O enquadramento, portanto, tem de ser decidido pelo corpo, e uma vez por substream de cache em vez de por record. O HotXLS trava o dialeto pelo comprimento do primeiro record SXDBB de cada substream. No enquadramento da especificação, um SXDBB guarda exatamente um record de cache, então o comprimento dele é igual a uma largura de linha. No enquadramento empacotado mais antigo, o primeiro record guarda quantas linhas couberem, então para qualquer cache com mais de uma linha ele tem pelo menos duas larguras de linha. A comparação é decisiva sempre que as duas previsões diferem
Quando elas não diferem, o leitor fica com a leitura da especificação, pelo princípio de que arquivos escritos pelo Excel superam em número arquivos escritos por um build intermediário. Esse ponto cego é estreito por construção e, quando ocorre, o próprio arquivo continua a ser reproduzido byte a byte. Só os índices tipados expostos aos callers são afetados
A largura do índice vive em outro record
O SXDBB (§2.4.276) carrega um índice por campo de cache cujo flag de valores distintos está ligado, em ordem de campos, e a largura de cada índice é decidida em outro lugar: o record de campo SXFDB correspondente (§2.4.283) declara um flag de itens curtos, e esse flag diz se o índice ocupa dois bytes ou um. Dois records, um contrato implícito, e uma única frase na especificação conectando os dois
Esse acoplamento é exatamente onde uma codificação caseira erra. Um writer anterior do HotXLS empacotava cada campo no número mínimo de bits, completando até a fronteira de byte entre linhas, o que é defensável isoladamente e contradiz diretamente a largura que o mesmo writer tinha acabado de declarar no SXFDB. Um campo com três valores distintos era descrito com um byte de largura num record e ocupava dois bits no outro. O conserto não foi corrigir a aritmética, mas extrair a decisão de largura para uma função que os dois emissores chamam, para que os dois records não possam mais se separar. É a mesma classe de defeito descrita em drift de declaração de comprimento de records BIFF, em que um tamanho declarado e um corpo real se separam
A consequência de não ler esses records de forma alguma vale ser explicitada, porque é fácil subestimá-la. Quando o leitor pulava os índices de records, todo cache carregado de um arquivo reportava índice zero para todo campo de toda linha, o que significa que toda linha apontava para o primeiro valor de cada campo. Não é apenas introspecção reduzida: o caminho de avaliação de pivot e o caminho de preenchimento de cache para célula ambos consomem esse grid. E um teste de round-trip não detecta, porque um cache ainda em replay bruto é escrito de volta a partir dos bytes originais
// Flags de proveniência dizem o que você está segurando e o que pode ser reescrito
if Cache.FromRawBlobs then
begin
Writeln('stream id : ', IntToHex(Cache.StreamId, 4));
Writeln('legacy framing : ', Cache.RawFramingIsLegacy);
Writeln('own storage : ', Cache.RawHasStorageStream);
Writeln('model complete : ', Cache.RawModelIsComplete);
// Reemitir só é lossless quando todo record tem um modelo aqui
if Cache.CanUpgradeFraming then
Writeln('safe to rewrite with the current emitters');
end;
Quando reescrever um cache é lossless?
Só quando três condições valem juntas, e o CanUpgradeFraming é a única propriedade que responde à pergunta. O cache precisa estar ainda em replay bruto, o substream precisa estar num dos enquadramentos que esta biblioteca já escreveu incorretamente, e o leitor precisa ter construído um modelo tipado completo de todo record nele. Um cache que o Excel escreveu nunca se qualifica, porque o substream dele carrega records para os quais o HotXLS não tem modelo, e reemitir a partir do modelo os descartaria
O teste de completude é mais estrito do que parece à primeira vista. Um record que o leitor manteve só como bytes opacos marca o modelo como incompleto. Também marca uma contagem declarada de records de fórmula que o emissor não consegue reproduzir, porque reemitir reescreveria uma declaração de vários records de fórmula como uma declaração de nenhum, e um valor no arquivo que não pode ser reproduzido equivale a um record que não pode ser reproduzido
O conservadorismo deliberado atravessa o writer também. Os índices são clamped para a faixa legal em vez de codificados como um sentinela fora de banda, porque a especificação define um índice para a sequência de valores distintos e nada mais, e uma célula vazia é ela própria um valor nessa sequência. Um corpo de record de cache que exceda o teto de records do BIFF não é escrito de forma alguma, o que exigiria milhares de campos de cache e é inalcançável dentro do limite de colunas do BIFF8 de qualquer forma; o fallback é que o Excel atualiza a partir do intervalo de origem, que é comportamento definido, não um arquivo corrompido
Datas carregam a última dependência entre records. A conversão de serial para data depende do sistema de datas da workbook, e o emissor de records não consegue ver a workbook, então a escolha da data base é passada como um parâmetro que tem o sistema 1900 como padrão e é fornecida pelo caminho de save no nível da workbook. Sob o sistema 1900, o número serial é o valor diretamente; o sistema 1904 difere em 1462 dias. O tratamento mais amplo de seriais de data está em seriais de data, o sistema 1904 e formatos numéricos
Se você está trabalhando na camada de view e não na de cache, os records que descrevem o pivot visível estão cobertos em o conjunto de records PivotTable do BIFF8, e o comportamento do lado do cálculo em campos calculados, itens calculados e refresh. As três camadas vêm no componente de planilha HotXLS para Delphi, que é o que torna possível carregar uma workbook legada, inspecionar o que o cache dela realmente contém, e decidir se reescrevê-lo é seguro antes de fazer