O substream PivotCache do BIFF guarda o conjunto de dados em cache de uma PivotTable separadamente da vista que o apresenta, e o HotXLS lê e escreve esse substream inspecionando corpos de registos em vez de confiar nos números de registo. Essa distinção é toda a história: o mesmo número de registo transporta dois layouts de corpo incompatíveis consoante o escritor que produziu o ficheiro, por isso o leitor decide a delimitação a partir do primeiro corpo de registo que vê
Encontra esta camada no momento em que uma PivotTable tem de sobreviver a uma viagem de ida e volta. Uma vista dinâmica sem a sua cache é uma casca, e o Excel vai reconstruir a cache a partir do intervalo de origem quando abrir o ficheiro, o que é ótimo até ao ponto em que o intervalo de origem desapareceu, os dados foram colados de uma query, ou o livro é um fecho arquivado que não pode mudar quando alguém o abre
Duas estruturas, dois sítios no ficheiro
Os dados em cache e a definição da cache vivem em partes diferentes do livro, e confundi-los é a primeira coisa a acertar. Os registos em cache formam o seu próprio substream, 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 à cabeça dessa produção
A definição senta-se nos globals do livro em vez disso, como PIVOTCACHEDEFINITION = SXStreamID SXVS [SXSRC] [SXADDLCACHE] (§2.1.7.20.3), posicionada depois dos registos de formatação e antes dos registos BoundSheet e Country. Por isso uma única cache é descrita em dois sítios separados por centenas de registos, e a ligação entre eles é um identificador de stream que tem de bater certo em três locais ao mesmo tempo
Cada cache pertence a um stream sob _SX_DB_CUR cujo nome é a ortografia hexadecimal maiúscula de quatro dígitos do seu identificador. SXStreamID.idStm, o campo idstm repetido no cabeçalho SXDB, e esse nome de stream têm todos de coincidir. Quando aloca um identificador novo, reserve primeiro todos os números já lidos do ficheiro, ou uma cache nova pode reclamar um número que pertence a uma cache mais antiga à qual o leitor ainda não chegou
Mais um identificador apanha as pessoas. O valor iCache numa vista dinâmica é a posição base zero do SXStreamID correspondente na sequência global, e não um identificador de cache que possa escolher. Na escrita tem de ser mapeado do objeto de cache para a sua posição real de output, e as vistas existentes têm de ser renumeradas juntamente, ou atualizar uma cache aponta silenciosamente uma vista para outra
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; // confie nos registos 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 a grelha de registos
Cache.SetRecordCount(500);
Book.StorePivotCaches;
finally
Book.Free;
end;
end;
O SetRecordCount duplo não é superstição. RecordCount é uma escrita de propriedade simples que não aloca, e o caminho interno de crescimento só inicializa as linhas recém-acrescentadas, por isso uma cache cuja contagem foi definida pelo caminho do cabeçalho pode acabar com uma grelha de índices de comprimento zero. As escritas em RecordIndices são então descartadas sem erro. Definir a contagem a zero e de volta restabelece a grelha, e isso tem de acontecer depois de todos os campos terem sido acrescentados, porque a largura da linha vem da contagem de campos
Porque é que um número de registo não lhe diz o layout do corpo?
Porque os números de registo e os layouts de corpo mudaram em momentos diferentes, por isso o mapeamento entre eles não é uma função. Um número do conjunto legado só aparece em ficheiros de escritores mais antigos, o que o torna um sinal fiável num sentido. Outro número é genuinamente ambíguo: aparece tanto em ficheiros corretos como numa gama de versões intermédias que usavam o número novo com o corpo antigo
A delimitação por isso tem de ser decidida pelo corpo, e uma vez por substream de cache em vez de por registo. O HotXLS fixa o dialecto a partir do comprimento do primeiro registo SXDBB de cada substream. Na delimitação da especificação, um SXDBB contém exatamente um registo de cache, por isso o seu comprimento iguala uma largura de linha. Na delimitação empacotada mais antiga, o primeiro registo contém quantas linhas couberem, por isso para qualquer cache com mais de uma linha tem pelo menos duas larguras de linha. A comparação é decisiva sempre que as duas previsões diferem
Quando não diferem, o leitor fica com a leitura da especificação, pelo princípio de que os ficheiros escritos pelo Excel superam em número os ficheiros escritos por uma build intermédia. Esse ponto cego é estreito por construção e, quando acontece, o ficheiro em si ainda é reproduzido byte a byte. Só os índices tipados expostos aos chamadores são afetados
A largura do índice vive num registo diferente
SXDBB (§2.4.276) transporta um índice por campo de cache cuja flag de valores distintos está posta, pela ordem dos campos, e a largura de cada índice é decidida noutro sítio: o registo de campo SXFDB correspondente (§2.4.283) declara uma flag de itens curtos, e essa flag diz se o índice ocupa dois bytes ou um. Dois registos, um contrato implícito, e uma única frase na especificação a ligá-los
Esse acoplamento é precisamente onde uma codificação caseira falha. Um escritor anterior do HotXLS empacotava cada campo no número mínimo de bits, preenchendo até a uma fronteira de byte entre linhas, o que é defensável isoladamente e contradiz diretamente a largura que o mesmo escritor acabava de declarar em SXFDB. Um campo com três valores distintos era descrito com um byte de largura num registo e ocupava dois bits no outro. A correção não foi corrigir a aritmética mas extrair a decisão de largura para uma função que ambos os emissores chamam, para que os dois registos não possam mais divergir. É a mesma classe de defeito descrita em divergência de declaração de comprimento de registos BIFF, em que um tamanho declarado e um corpo real se separam
A consequência de não ler de todo estes registos vale a pena explicitar, porque é fácil subestimá-la. Quando o leitor saltava os índices de registos, toda a cache carregada de um ficheiro reportava índice zero para cada campo de cada linha, o que significa que cada linha apontava para o primeiro valor de cada campo. Isso não é mero empobrecimento da introspeção: o caminho de avaliação da tabela dinâmica e o caminho de preenchimento cache-para-célula ambos consomem essa grelha. E um teste de ida e volta não o deteta, porque uma cache ainda em replay bruto é escrita de volta a partir dos seus bytes originais
// As flags de proveniência dizem-lhe o que tem nas mãos 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ó é sem perdas quando todos os registos têm modelo aqui
if Cache.CanUpgradeFraming then
Writeln('safe to rewrite with the current emitters');
end;
Quando é que reescrever uma cache é sem perdas?
Só quando três condições se verificam em conjunto, e CanUpgradeFraming é a única propriedade que responde à pergunta. A cache tem de estar ainda em replay bruto, o substream tem de estar numa das delimitações que esta biblioteca escreveu incorretamente no passado, e o leitor tem de ter construído um modelo tipado completo de todos os registos nela. Uma cache escrita pelo Excel nunca se qualifica, porque o seu substream transporta registos para os quais o HotXLS não tem modelo, e reemitir a partir do modelo deixá-los-ia cair
O teste de completeza é mais estrito do que à primeira vista parece. Um registo que o leitor guardou apenas como bytes opacos marca o modelo como incompleto. O mesmo acontece com uma contagem declarada de registos de fórmula que o emissor não consegue reproduzir, porque reemitir reescreveria uma declaração de vários registos de fórmula como uma declaração de nenhum, e um valor no ficheiro que não se consegue reproduzir é equivalente a um registo que não se consegue reproduzir
O conservadorismo deliberado percorre também o escritor. Os índices são limitados ao intervalo 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 registo de cache que exceda o teto de registos BIFF não é escrito de todo, 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 e não um ficheiro corrompido
As datas transportam a última dependência entre registos. A conversão de serial para data depende do sistema de datas do livro, e o emissor de registos não consegue ver o livro, por isso a escolha da data base é passada como um parâmetro que tem a predefinição do sistema 1900 e é fornecida pelo caminho de gravação ao nível do livro. Sob o sistema 1900 o número de série é o valor diretamente; o sistema 1904 difere por 1462 dias. O tratamento mais amplo dos seriais de data está em seriais de data, o sistema 1904 e formatos numéricos
Se está a trabalhar na camada da vista e não na camada da cache, os registos que descrevem a tabela dinâmica visível estão cobertos em o conjunto de registos PivotTable BIFF8, e o comportamento do lado do cálculo em campos calculados, itens calculados e atualização. As três camadas são distribuídas no componente de folhas de cálculo Delphi HotXLS, que é o que torna possível carregar um livro legado, inspecionar o que a sua cache realmente contém, e decidir se a reescrever é seguro antes de o fazer