Article technique

Aller-retour XLSX sans perte en Delphi : Thème, extLst, calcChain

HotXLS, la bibliothèque Excel native pour Delphi et C++Builder, est conçue pour des allers-retours XLSX sans perte : ouvrez un classeur, modifiez une cellule, enregistrez, et le thème personnalisé du client, les blocs d'extension extLst externes et la chaîne de calcul (calculation chain) sont tous préservés. Trois mécanismes permettent cela — la mise en cache textuelle de xl/theme/theme1.xml, la resérialisation basée sur les événements des blocs <ext> inconnus et la génération d'un fichier xl/calcChain.xml récent et conforme aux spécifications à chaque sauvegarde d'un classeur contenant des formules

Le scénario qui motive ces trois aspects n'est que trop fréquent. Un service de facturation charge un modèle conçu par le client sous Excel — thème couleur d'entreprise, graphiques sparklines dans une colonne d'indicateurs (KPI), règle de formatage conditionnel ajoutée par une version plus récente d'Excel — écrit le total d'une facture dans la cellule B3 et l'enregistre. Le client ouvre le résultat et s'aperçoit que les couleurs de sa marque sont revenues au bleu standard d'Office, que les sparklines ont disparu et qu'Excel lui propose de « réparer » le fichier. Pourtant, rien dans le code n'a touché à ces fonctionnalités. C'est la bibliothèque qui l'a fait, simplement en enregistrant

Pourquoi les fichiers Excel perdent-ils leur mise en forme après modification par une bibliothèque ?

Les fichiers Excel perdent leur mise en forme après édition par une bibliothèque car la plupart d'entre elles ne se contentent pas de modifier le fichier — elles le reconstruisent. Un package .xlsx est un ZIP composé d'éléments XML : xl/workbook.xml, un fichier xl/worksheets/sheetN.xml par feuille, xl/styles.xml, xl/theme/theme1.xml, xl/calcChain.xml, etc. Une bibliothèque classique analyse ces éléments dans un modèle d'objet à l'ouverture et régénère chaque composant à partir de ce modèle lors de l'enregistrement. Toute fonctionnalité non représentée dans le modèle — un thème jamais analysé, un bloc d'extension d'un Excel plus récent — n'a aucun espace en mémoire, de sorte que l'élément régénéré l'omet silencieusement

La norme ECMA-376 avait anticipé une partie de ce problème. SpreadsheetML définit extLst (ECMA-376 Partie 1, la zone de stockage des données pour les futures fonctionnalités, §18.2.10 pour l'élément au niveau du classeur) comme un point d'extension désigné : les producteurs plus récents y placent leurs fonctionnalités, chacune enveloppée dans un élément <ext> portant un attribut uri qui identifie la fonctionnalité, et les consommateurs plus anciens sont censés préserver ce qu'ils ne comprennent pas. C'est ainsi que transitent les sparklines, les segments (slicers) et les nouveaux types de formatage conditionnel. Une bibliothèque qui supprime les blocs <ext> inconnus n'est pas seulement incomplète — elle viole le contrat de compatibilité ascendante autour duquel le format a été conçu. La question à poser à toute bibliothèque de feuilles de calcul que vous évaluez est directe : si je modifie une cellule, qu'est-ce qui change d'autre ?

Comment HotXLS conserve-t-il un thème personnalisé à l'octet près ?

HotXLS préserve le thème d'un classeur en mettant en cache les octets d'origine de xl/theme/theme1.xml lors de l'ouverture et en les réécrivant textuellement lors de l'enregistrement. L'élément de thème (ECMA-376 Partie 1, §14.2.7) est du DrawingML, pas du SpreadsheetML — jeux de couleurs, jeux de polices, jeux de formats — et un moteur de feuille de calcul n'a aucune raison de le modéliser en détail. Les versions antérieures de HotXLS régénéraient un thème Office fixe à chaque sauvegarde, ce qui provoquait justement le retour aux couleurs standard d'Office décrit ci-dessus ; depuis la version v2.89.46, le thème du package ouvert est stocké sous sa forme brute et réémis tel quel, et le thème Office intégré n'est généré que pour les classeurs créés à partir de zéro. Les octets bruts constituent la meilleure garantie de fidélité possible : pas d'analyse, pas de resérialisation, aucun risque de dérive

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('branded-invoice.xlsx');
    Book.Sheets[0].Cells[3, 2].Value := 42750.00;  // the one edit
    Book.SaveAs('branded-invoice-out.xlsx');
    // theme1.xml in the output is byte-identical to the input
  finally
    Book.Free;
  end;
end;

Qu'advient-il des blocs extLst inconnus lors de l'enregistrement ?

HotXLS capture chaque bloc <ext> au niveau de la feuille de calcul qu'il ne modélise pas nativement et le restitue dans le bloc extLst de la feuille enregistrée, garantissant que les fonctionnalités écrites par des versions d'Excel plus récentes survivent à l'aller-retour. Depuis la version v2.131.0, les fragments capturés sont visibles via la propriété en lecture seule RawWorksheetExts, une structure TStringList propre à chaque feuille XLSX, ce qui permet de vérifier cette garantie par du code de test plutôt que de s'en remettre à la confiance :

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)); // peek at each uri
  finally
    Book.Free;
  end;
end;

Le détail d'implémentation important à connaître est que la capture s'apparente à une resérialisation au niveau des événements, et non à une copie d'octets bruts. Le lecteur XML en flux de HotXLS n'exposant aucun décalage source, le sous-arbre inconnu est reconstruit à partir des événements Element, Text et EndElement au fur et à mesure de leur lecture. Cette approche cache un piège classique : un élément à fermeture automatique comme <a/> ne déclenche qu'un événement Element marqué vide et jamais d'EndElement, de sorte que tout compteur de profondeur qui décrémenterait uniquement sur l'événement EndElement ne verrait jamais le sous-arbre se fermer. En gérant cela, le fragment reconstruit est sémantiquement équivalent à l'original — les guillemets des attributs et les formes auto-fermantes sont normalisés, la structure n'est donc pas identique à l'octet près, mais Excel lit la sémantique, non les octets. Deux propriétés de la sortie d'Excel rendent la restitution sûre : Excel déclare les attributs xmlns nécessaires sur l'élément <ext> ou à l'intérieur de celui-ci, de sorte que chaque fragment capturé est autonome au niveau des espaces de noms, et cette même autonomie explique pourquoi la duplication d'une feuille à l'intérieur ou entre des classeurs peut transporter ces blocs externes par une simple affectation de liste de chaînes

Écrire calcChain.xml pour qu'Excel fasse confiance à vos formules

HotXLS écrit xl/calcChain.xml (la section chaîne de calcul, ECMA-376 Partie 1, §12.3.1) chaque fois que le classeur enregistré contient des formules, et choisit entre deux ordonnancements. Si le graphe de dépendances des formules a déjà été construit et est à jour — c'est-à-dire si vous avez appelé Recalculate après votre dernière modification — la chaîne est émise dans un ordre topologique complet, les dépendances avant les dépendants, avec les membres de références circulaires ajoutés à la fin. Sinon, les cellules sont listées dans l'ordre du document. Les deux sont valides : les notes d'implémentation de Microsoft pour ce format, [MS-XLSX], traitent la chaîne de calcul comme une indication qu'Excel vérifie et réorganise lors du chargement, de sorte que toute liste complète est légale, et HotXLS refuse délibérément de forcer la construction du graphe à l'intérieur de SaveAs — la construction des arêtes étant quadratique par rapport au nombre de cellules, ce serait un coût masqué inacceptable sur une sauvegarde de millions de cellules

Book.Open('model.xlsx');
Book.Sheets[0].Cells[10, 4].Formula := '=SUM(D2:D9)';
// Saved now, calcChain.xml lists formula cells in document order.
// After Recalculate the dependency graph exists, so the same save
// emits a full topological order instead:
Book.Recalculate;
Book.SaveAs('model-out.xlsx');

Où s'arrête l'aller-retour sans perte

L'honnêteté importe plus ici qu'une case à cocher marketing, les limites méritent donc une attention égale. HotXLS ne copie pas l'intégralité du package à l'octet près : le XML des feuilles, les styles, les chaînes partagées et les éléments du classeur sont régénérés à partir du modèle analysé, la sortie est donc sémantiquement fidèle mais non binaire-identique — les en-têtes locaux du ZIP portent de nouveaux horodatages DOS. Les fragments <ext> capturés reviennent normalisés, comme décrit ci-dessus. Les surcharges programmatiques des polices de thème sont ignorées lorsqu'un thème textuel est présent. Et le filet de préservation a une maille définie : les fonctionnalités modélisées nativement par HotXLS (les sparklines, par exemple, sont analysées et réécrites plutôt que copiées aveuglément), les contenus extLst externes et les composants mis en cache textuellement. Un élément qui n'est ni modélisé ni placé dans un point d'extension — la section personnalisée d'un complément exotique, par exemple — échappe aux trois mécanismes présentés dans cet article ; testez donc vos propres modèles réels au lieu de supposer que tout sera préservé

D'autres travaux de préservation complètent le tableau. Les projets VBA et les références de classeurs externes survivent à la sauvegarde selon la même philosophie consistant à préserver ce que l'on ne modélise pas, présentée dans l'article connexe sur la préservation des projets VBA et des liens externes, et les propriétés de document dans docProps disposent de leur propre API de lecture-écriture au lieu d'être supprimées silencieusement. Lorsque vous évaluez une bibliothèque de feuilles de calcul, exécutez le test de la cellule unique : ouvrez un classeur de production riche en fonctionnalités, modifiez une seule valeur, enregistrez et comparez les éléments ZIP décompressés par rapport à l'original. Ce qui a changé en dehors de la feuille à laquelle vous avez touché vous en dira plus sur la bibliothèque que n'importe quelle grille de fonctionnalités

Les mécanismes d'aller-retour décrits ici — conservation du thème textuel depuis la version v2.89.46, capture des extLst externes et émission de calcChain.xml depuis la version v2.131.0 — sont intégrés dans le composant actuel HotXLS Delphi Excel Component, dont la page produit documente l'ensemble des fonctionnalités de lecture et d'écriture XLSX pour Delphi et C++Builder