Articolo tecnico

Round-trip XLSX senza perdite: theme, extLst, calcChain

HotXLS, la libreria Excel nativa per Delphi e C++Builder, è costruita per round-trip XLSX senza perdite: aprite una cartella di lavoro, cambiate una cella, salvate, e il tema personalizzato del cliente, i blocchi di estensione extLst estranei e la catena di calcolo sopravvivono tutti. Tre meccanismi lo rendono possibile — la cache letterale di xl/theme/theme1.xml, la ri-serializzazione basata su eventi dei blocchi <ext> sconosciuti e un xl/calcChain.xml nuovo e conforme alle specifiche a ogni salvataggio di una cartella con formule

I tre meccanismi dietro un round-trip XLSX senza perdite di HotXLS in Delphi: xl/theme/theme1.xml messo in cache come byte grezzi e riscritto identico, blocchi extLst estranei catturati dagli eventi XML e riprodotti, e un calcChain.xml nuovo e conforme alle specifiche emesso a ogni salvataggio con formule
Ogni parte conservata segue il proprio percorso attraverso il salvataggio — byte letterali del tema, riproduzione a livello di evento di extLst e catena di calcolo rigenerata — mentre l'XML dei fogli e gli stili vengono ricostruiti dal modello

Lo scenario che motiva tutti e tre è deprimentemente comune. Un servizio di fatturazione carica un modello progettato dal cliente in Excel — tema di colori aziendale, sparkline in una colonna KPI, una regola di formattazione condizionale aggiunta da una build più recente di Excel — scrive il totale di una fattura nella cella B3 e salva. Il cliente apre il risultato e i colori del marchio sono tornati al blu Office di serie, le sparkline sono sparite ed Excel propone di "riparare" il file. Nulla nel codice ha toccato quelle funzionalità. Lo ha fatto la libreria, semplicemente salvando

Perché i file Excel perdono la formattazione dopo le modifiche di una libreria?

I file Excel perdono la formattazione dopo le modifiche di una libreria perché la maggior parte delle librerie non modifica il file — lo ricostruisce. Un pacchetto .xlsx è uno ZIP di parti XML: xl/workbook.xml, un xl/worksheets/sheetN.xml per foglio, xl/styles.xml, xl/theme/theme1.xml, xl/calcChain.xml e altre ancora. Una libreria tipica analizza quelle parti in un modello a oggetti all'apertura e rigenera ogni parte da quel modello al salvataggio. Qualsiasi funzionalità che il modello non rappresenta — un tema che non ha mai analizzato, un blocco di estensione di un Excel più recente — non ha dove risiedere in memoria, quindi la parte rigenerata la omette in silenzio

ECMA-376 aveva previsto metà di questo problema. SpreadsheetML definisce extLst (ECMA-376 Parte 1, l'area di memorizzazione dei dati di funzionalità future, §18.2.10 per l'elemento a livello di cartella di lavoro) come punto di estensione dedicato: i produttori più recenti parcheggiano lì le funzionalità, ciascuna avvolta in un elemento <ext> che porta un attributo uri a identificarla, e ci si aspetta che i consumatori più vecchi conservino ciò che non comprendono. Sparkline, filtri dei dati e i tipi più recenti di formattazione condizionale viaggiano tutti così. Una libreria che scarta i blocchi <ext> sconosciuti non è quindi soltanto lacunosa — viola il contratto di compatibilità in avanti attorno a cui il formato è stato progettato. La domanda da porre a qualsiasi libreria per fogli di calcolo che state valutando è brutale: se cambio una cella, cos'altro cambia

Come fa HotXLS a mantenere un tema personalizzato byte per byte?

HotXLS conserva il tema di una cartella di lavoro mettendo in cache i byte originali di xl/theme/theme1.xml all'apertura e riscrivendoli letteralmente al salvataggio. La parte del tema (ECMA-376 Parte 1, §14.2.7) è DrawingML, non SpreadsheetML — schemi di colori, schemi di font, schemi di formato — e un motore per fogli di calcolo non ha motivo di modellarla in profondità. Le versioni precedenti di HotXLS rigeneravano un tema Office fisso a ogni salvataggio, che è esattamente il guasto dei "colori del marchio tornati indietro" descritto sopra; dalla v2.89.46 il tema del pacchetto aperto viene memorizzato grezzo e riemesso intatto, e il tema Office incorporato viene generato solo per le cartelle di lavoro create da zero. I byte grezzi sono la garanzia di fedeltà più forte possibile: nessuna analisi, nessuna ri-serializzazione, nessuna possibilità di deriva

La copia letterale vince deliberatamente sull'accesso programmatico al tema. TXLSXWorkbook espone ThemeMajorFont e ThemeMinorFont perché possiate scegliere i caratteri di titolo e di corpo per le cartelle nuove, ma quando all'apertura è stato catturato un tema letterale quei setter non hanno effetto sul file salvato — il round-trip ha la precedenza. Se avete davvero bisogno di alterare il tema di una cartella esistente, è un segnale che conviene modificare il modello in Excel stesso anziché attraverso una API orientata ai dati. Il caso quotidiano non richiede alcuna API:

HotXLS mette in cache i byte grezzi di xl/theme/theme1.xml all'apertura e li riscrive identici al salvataggio, mentre una libreria che ricostruisce dal modello rigenera un tema Office di serie e riporta indietro i colori del marchio del cliente
Mettere in cache theme1.xml letteralmente non richiede alcun modello del tema, e ThemeMajorFont insieme a ThemeMinorFont danno stile solo alle cartelle che non portano un tema catturato
var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('branded-invoice.xlsx');
    Book.Sheets[0].Cells[3, 2].Value := 42750.00;  // l'unica modifica
    Book.SaveAs('branded-invoice-out.xlsx');
    // theme1.xml nel risultato è identico byte per byte a quello di partenza
  finally
    Book.Free;
  end;
end;

Cosa succede ai blocchi extLst sconosciuti al salvataggio?

HotXLS cattura ogni blocco <ext> a livello di foglio che non modella nativamente e lo riproduce dentro l'extLst del foglio salvato, così che le funzionalità scritte da build più recenti di Excel sopravvivano intatte al round-trip. Dalla v2.131.0 i frammenti catturati sono visibili attraverso la proprietà in sola lettura RawWorksheetExts, una TStringList su ogni foglio XLSX, che rende la garanzia verificabile dal codice di test anziché essere un atto di fede:

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)); // sbirciate ogni uri
  finally
    Book.Free;
  end;
end;

Il dettaglio di implementazione che vale la pena conoscere è che la cattura è una ri-serializzazione a livello di evento, non una copia grezza di byte. Il lettore XML a flusso di HotXLS non espone offset sulla sorgente, quindi il sottoalbero sconosciuto viene ricostruito dagli eventi Element, Text ed EndElement mentre scorrono. Quell'approccio nasconde una trappola classica: un elemento auto-chiudente come <a/> genera solo un evento Element contrassegnato come vuoto e mai un EndElement, quindi qualunque contatore di profondità che decrementi solo su EndElement non vedrà mai chiudersi il sottoalbero. Gestitela, e il frammento ricostruito è semanticamente equivalente all'originale — la quotatura degli attributi e le forme auto-chiudenti vengono normalizzate, quindi non è identico byte per byte, ma Excel legge il significato, non i byte. Due proprietà dell'output di Excel stesso rendono sicura la riproduzione: Excel dichiara gli attributi xmlns necessari sull'elemento <ext> o al suo interno, quindi ogni frammento catturato è autosufficiente sui namespace, ed è quella stessa autosufficienza che permette alla duplicazione di un foglio dentro o fra cartelle di lavoro di portarsi dietro i blocchi estranei con una semplice assegnazione di lista di stringhe

Scrivere calcChain.xml perché Excel si fidi delle vostre formule

HotXLS scrive xl/calcChain.xml (la parte Calculation Chain, ECMA-376 Parte 1, §12.3.1) ogni volta che la cartella salvata contiene formule, e sceglie fra due ordinamenti. Se il grafo delle dipendenze fra formule è già stato costruito ed è aggiornato — avete chiamato Recalculate dopo l'ultima modifica — la catena viene emessa in ordine topologico completo, prima le dipendenze e poi i dipendenti, con gli eventuali membri di riferimenti circolari accodati alla fine. Altrimenti le celle sono elencate in ordine di documento. Entrambi sono corretti: le note di implementazione Microsoft per il formato, [MS-XLSX], trattano la catena di calcolo come un suggerimento che Excel verifica e riordina durante il caricamento, quindi qualsiasi elenco completo è lecito, e HotXLS si rifiuta deliberatamente di forzare la costruzione del grafo dentro SaveAs — la costruzione degli archi è quadratica nel numero di celle, un costo nascosto inaccettabile su un salvataggio da un milione di celle

HotXLS emette xl/calcChain.xml in ordine topologico quando Recalculate ha costruito il grafo delle dipendenze, e in ordine di documento altrimenti; Excel tratta come suggerimento qualsiasi elenco completo e lo riordina durante il caricamento
Entrambi gli ordinamenti restano leciti perché Excel riverifica la catena al caricamento, e HotXLS non forza mai dentro SaveAs la costruzione del grafo dal costo quadratico
Book.Open('model.xlsx');
Book.Sheets[0].Cells[10, 4].Formula := '=SUM(D2:D9)';
// Salvando ora, calcChain.xml elenca le celle di formula in ordine di documento.
// Dopo Recalculate il grafo delle dipendenze esiste, quindi lo stesso salvataggio
// emette invece un ordine topologico completo:
Book.Recalculate;
Book.SaveAs('model-out.xlsx');

Perché preoccuparsi di una parte che Excel tratta come consultiva? Perché la sua assenza è un segnale. Alcuni consumatori — euristiche di riparazione, visualizzatori di terze parti, strumenti di confronto — si aspettano che una cartella con formule porti una catena di calcolo, e una libreria che scarta in silenzio quella parte al salvataggio produce file sottilmente diversi da qualunque cosa Excel scriva. Emettere una catena valida mantiene l'output dentro il perimetro su cui il resto dell'ecosistema è stato collaudato, che è il nucleo silenzioso e poco appariscente dell'ingegneria del round-trip

Dove finisce il round-trip senza perdite

Qui l'onestà conta più di una casella di marketing, quindi i limiti meritano pari spazio. HotXLS non copia l'intero pacchetto byte per byte: l'XML dei fogli, gli stili, le stringhe condivise e le parti della cartella di lavoro vengono rigenerati dal modello analizzato, quindi il risultato è semanticamente fedele ma non identico a livello binario — le sole intestazioni locali dello ZIP portano marche temporali DOS nuove. I frammenti <ext> catturati tornano normalizzati, come descritto sopra. Le sovrascritture programmatiche dei font del tema vengono ignorate quando è presente un tema letterale. E la rete di conservazione ha una maglia definita: le funzionalità che HotXLS modella nativamente (le sparkline, per esempio, vengono analizzate e riscritte anziché copiate alla cieca) più il contenuto extLst estraneo più le parti messe in cache letteralmente. Una parte che non è né modellata né dentro un punto di estensione — la parte personalizzata di un componente aggiuntivo esotico, poniamo — resta fuori dai tre meccanismi trattati in questo articolo, quindi collaudate i vostri modelli reali invece di dare per scontato

Il lavoro di conservazione adiacente completa il quadro. I progetti VBA e i riferimenti a cartelle di lavoro esterne attraversano il salvataggio con la stessa filosofia del conservare ciò che non si modella, trattata nell'articolo di accompagnamento sulla conservazione di VBA e collegamenti esterni, e le proprietà del documento in docProps hanno una loro API di lettura e scrittura invece di essere scartate in silenzio. Quando valutate una qualsiasi libreria per fogli di calcolo, fate la prova della cella singola: aprite una cartella di produzione ricca di funzionalità, cambiate un solo valore, salvate e confrontate le parti decompresse con l'originale. Ciò che è cambiato oltre al foglio che avete toccato vi dice sulla libreria più di qualunque matrice di funzionalità

I meccanismi di round-trip descritti qui — conservazione letterale del tema dalla v2.89.46, cattura di extLst estranei ed emissione di calcChain.xml dalla v2.131.0 — sono distribuiti nell'attuale HotXLS Delphi Excel Component, la cui pagina di prodotto documenta l'insieme completo di funzionalità di lettura e scrittura XLSX per Delphi e C++Builder