Une série de graphique remplie par un RGB littéral ne suit pas le thème du classeur. Changez le thème et la série conserve l’ancienne couleur. HotXLS gère cela dans le XLS binaire avec des remplissages de séries liés au thème : un enregistrement GelFrame, 4198 ou $1066, écrit juste après AreaFormat dans le bloc de série et portant un index de schéma OfficeArt ainsi qu’une teinte. Excel rend alors la série comme il rend un remplissage lié au thème qu’il a écrit lui-même
D’où vient le numéro d’enregistrement GelFrame ?
Le numéro d’enregistrement GelFrame est 4198 ($1066), et vous ne le trouverez pas dans la section de spécification propre à l’enregistrement. [MS-XLS] 2.4.131 décrit ce que contient un GelFrame, mais contrairement à la plupart des sections d’enregistrements, ne donne pas la valeur rt. L’ABNF du sous-flux de graphique n’aide pas davantage : elle ne fournit que la production GELFRAME = 1*2GelFrame *Continue, qui nomme l’enregistrement sans le numéroter. Le numéro se trouve dans la table d’énumération des numéros d’enregistrements, à plusieurs pages de la section qui documente la charge utile. Cette production mérite un second regard pour quiconque écrit un lecteur : elle autorise un ou deux enregistrements GelFrame, chacun pouvant être suivi d’enregistrements Continue ; un parseur qui suppose un seul enregistrement par production traitera donc mal un fichier qu’il n’a pas écrit. HotXLS émet exactement un GelFrame par série thématique, ce que produit Excel pour un remplissage thématique uni simple, et son décodeur traite l’enregistrement comme une charge autonome plutôt que de supposer un compte fixe
Dans la charge GelFrame : deux tables de propriétés OfficeArt
La charge GelFrame est constituée de deux tables de propriétés OfficeArt placées l’une à la suite de l’autre : un OfficeArtFOPT (appelé OPT1), suivi d’un OfficeArtTertiaryFOPT (OPT2). Chaque table commence par un nombre de propriétés sur deux octets, suivi du nombre correspondant d’entrées FOPTE de six octets ; chaque entrée est composée d’un opid de deux octets et d’un op de quatre octets. Le bit 15 de opid est fComplex : lorsqu’il est défini, la valeur op est une longueur en octets et une queue variable suit les entrées fixes. Un décodeur qui ignore ces queues se désynchronise et lit des opid incohérents pour tout ce qui suit la première propriété complexe
Le remplissage thématique s’exprime par trois propriétés réparties dans les deux tables, plus une qui déclare le type de remplissage. HotXLS écrit quatre propriétés en 28 octets, sans queue complexe :
fillType$0180 dans OPT1, défini à 1 (msofillSolid)fillColor$0181 dans OPT1, le RGB aplati qu’un consommateur ancien ou ignorant des thèmes dessinerafillColorExt$019E dans OPT2, la couleur de thème de basefillColorExtMod$01A0 dans OPT2, la teinte ou l’ombre appliquée à cette base
Cette séparation est délibérée dans le format, pas un accident de l’implémentation : [MS-ODRAW] 2.2.2 décrit le triplet thématique comme une couleur plate, une couleur de base et une modification ; un consommateur qui comprend les thèmes recalcule donc le remplissage, tandis qu’un autre le peint tout de même de manière raisonnable. Les opid environnants suivent le même modèle et portent une numérotation identique dans les éditions ancienne et actuelle de [MS-ODRAW], ce qui est pratique lorsque vous confrontez deux révisions : fillOpacity $0182, fillBackColor $0183, fillShadeType $019C, fillBackColorExt $01A2 et fillBackColorExtMod $01A4
Pourquoi l’index de schéma se trouve-t-il dans l’octet rouge ?
Parce qu’un OfficeArtCOLORREF est défini par offset d’octet, pas par valeur numérique : rouge à l’octet 0, vert à l’octet 1, bleu à l’octet 2 et flags à l’octet 3. Lisez cette structure comme un DWORD little-endian, ce qu’est chaque op FOPTE, et le rouge devient l’octet de poids faible. L’exemple développé de lineColor dans [MS-ODRAW] le confirme. Ainsi, fSchemeIndex, qui est le bit E des flags, a pour valeur numérique $08000000 et l’index de schéma lui-même va dans l’octet rouge, les octets vert et bleu devant être nuls. Accent1 est donc la valeur op $08000004, et non $00000004, encore moins $04000000
L’ordre des index de thème que la spécification refuse de définir
La spécification qualifie l’ordre des index de schéma de défini par l’hôte et ne fournit aucune table, ce qui signifie que la disposition des octets seule ne suffit pas pour interopérer avec Excel. HotXLS utilise l’ordre des thèmes du tableur, celui qui fonctionne en aller-retour avec les vrais fichiers Excel :
- 0 = lt1, 1 = dk1, 2 = lt2, 3 = dk2
- 4 à 9 = accent1 à accent6
- 10 = hlink, 11 = folHlink
Teinte et ombre : la charge MSOTINTSHADE
L’opération fillColorExtMod est une valeur MSOTINTSHADE qui encode la direction et la quantité dans un seul DWORD plutôt que dans une fraction signée. La valeur $20000000 signifie non modifié. Une teinte éclaircissante est $02F4 shl 16 or amount shl 8 or $10 (MSOTINT) ; une teinte assombrissante possède la même forme avec $01F4 dans le mot de poids fort (MSOSHADE). L’octet amount évolue à l’inverse de l’intuition : $FF signifie inchangé et $00 la modification complète. HotXLS normalise cela en un seul double de style DrawingML, où une valeur positive éclaircit et une valeur négative assombrit, avec plus ou moins (255 - amount) / 255. Le mappage est exact pour les valeurs réellement proposées par Excel dans son interface, raison pour laquelle l’aller-retour est sans perte plutôt qu’approximatif : le familier « Lighter 40% » correspond à amount 153, et (255 - 153) / 255 vaut 0,4 sans erreur d’arrondi dans un sens comme dans l’autre. Une ombre avec amount 191 revient comme -64/255. Voici l’encodeur, limité à la plage légale :
if Tint > 0 then // MSOTINT - plus clair
TintOp := LongWord($02F4) shl 16 or
(LongWord(Round(255 * (1 - Tint))) shl 8) or $10
else if Tint < 0 then // MSOSHADE - plus sombre
TintOp := LongWord($01F4) shl 16 or
(LongWord(Round(255 * (1 + Tint))) shl 8) or $10
else
TintOp := $20000000; // MSOCOLORMODUNDEFINED
Définir et lire un remplissage thématique dans Delphi
Du côté écriture, un remplissage thématique correspond à deux champs supplémentaires dans l’enregistrement de style par série. TXLSChartSeriesStyleInfo a reçu HasFillTheme, FillThemeColor et FillThemeTint, et le générateur n’émet le GelFrame que lorsque HasStyle et HasFillTheme sont tous deux définis. Si vous définissez aussi un FillRgb explicite, cette valeur est placée telle quelle dans le fillColor OPT1 ; sinon, HotXLS aplatit lui-même la couleur au moyen d’une table de thème Office par défaut intégrée, avec la teinte appliquée, de sorte qu’une série uniquement thématique conserve une couleur plate saine pour les consommateurs qui ignorent OPT2. Remarquez l’initialisation Default(), importante parce que TXLSChartSeriesInfo contient des champs gérés et que ses membres Boolean simples seraient sinon des déchets de pile :
var
Wb: TXLSWorkbook;
Series: array [0..1] of TXLSChartSeriesInfo;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Sheets.Add.Name := 'Data';
Series[0] := Default(TXLSChartSeriesInfo); // ne jamais appliquer FillChar à cet enregistrement
Series[0].Name := 'Explicit';
Series[0].Categories := 'Data!$A$1:$A$2';
Series[0].Values := 'Data!$B$1:$B$2';
Series[0].HasStyle := True;
Series[0].Style.HasFill := True;
Series[0].Style.FillRgb := $C47244; // accent1, rouge dans l’octet de poids faible
Series[0].Style.HasFillTheme := True;
Series[0].Style.FillThemeColor := 4; // accent1
Series[0].Style.FillThemeTint := 0.4; // Lighter 40%
Series[1] := Default(TXLSChartSeriesInfo);
Series[1].Name := 'ThemeOnly';
Series[1].Categories := 'Data!$A$1:$A$2';
Series[1].Values := 'Data!$C$1:$C$2';
Series[1].HasStyle := True;
Series[1].Style.HasFillTheme := True; // aucun RGB explicite : aplati
Series[1].Style.FillThemeColor := 8; // accent5
Wb.Sheets.AddChartSheet('Themed', xlsChartTypeColumn, '', '', '', Series);
Wb.SaveAs('themed.xls');
finally
Wb.Free;
end;
end;
La lecture repasse par le même modèle de graphique que celui utilisé par le reste de l’inspection des graphiques HotXLS. GetChartModel renvoie un TXLSChartModel dont vous êtes propriétaire et que vous devez libérer, et chaque TXLSChartSeries expose HasFillTheme, FillThemeColor et FillThemeTint à côté du FillRgb décodé à partir du fillColor OPT1, qui prime sur la couleur AreaFormat de cette série. Les mêmes trois valeurs atteignent aussi l’instantané sémantique canonique sous les noms SolidFillThemeSet, SolidFillThemeColor et SolidFillThemeTint, si bien qu’une comparaison de classeurs voit une modification de thème comme une modification de thème plutôt qu’une dérive RGB inexpliquée. Si vous venez du monde XLSX, il s’agit de l’équivalent au format binaire du style décrit dans le guide HotXLS des graphiques, images et dessins Excel dans Delphi :
Wb := TXLSWorkbook.Create;
try
Wb.Open('themed.xls');
Model := Wb.Sheets[2]._Chart.GetChartModel;
try
Ser := Model.GetSeries(0);
if Ser.HasFillTheme then
begin
WriteLn(Ser.FillThemeColor); // 4 = accent1
WriteLn(Ser.FillThemeTint:0:3); // 0.400
WriteLn(IntToHex(Ser.FillRgb, 6)); // C47244, le fillColor OPT1
end;
finally
Model.Free;
end;
finally
Wb.Free;
end;
Ce qu’un remplissage thématique dans un XLS binaire ne garantit pas
Trois limites honnêtes. Premièrement, et c’est le plus important pour quiconque audite ce code : aucun fichier d’exemple du corpus local ne contient d’enregistrement GelFrame. Les onze occurrences de la paire d’octets 66 10 dans l’exemple de mise en forme conditionnelle se trouvent à des limites qui ne sont pas celles d’enregistrements, et un dump complet des enregistrements du flux ne trouve aucune occurrence. La disposition des bits décrite ici a été déduite de la spécification puis verrouillée de trois façons : par symétrie de décodage sur la sortie du générateur, par des tests d’octets construits à la main qui injectent directement une charge $1066 synthétique dans le décodeur et par une assertion sur le RGB aplati exact. C’est une forme de preuve plus faible qu’un fichier Excel capturé, et il vaut mieux le dire que laisser entendre le contraire. Deuxièmement, l’aplatissement d’un remplissage uniquement thématique utilise une table de thème Office par défaut intégrée, et non une partie de thème lue dans le classeur, car le XLS binaire ne possède pas de partie de thème au sens d’un XLSX empaqueté ; si vous avez besoin que le thème propre au classeur pilote la couleur plate, fournissez vous-même FillRgb. Troisièmement, le décodeur n’accepte un GelFrame qu’à l’intérieur d’un bloc de série ; le même enregistrement peut apparaître dans la zone du graphique ou dans un cadre d’axe, et l’accepter là attribuerait silencieusement un remplissage d’arrière-plan à une série, ces occurrences sont donc ignorées. Un fillColorExt sans le flag $08000000 est de même traité comme une couleur étendue ordinaire et ne définit jamais HasFillTheme. Pour les classeurs dont le graphique est créé dans le monde XLSX puis ne fait que transiter, le chemin de préservation de l’édition de graphiques Excel sans perte du ChartML est plus sûr, et le conteneur où résident ces enregistrements est couvert dans la lecture des fichiers composés OLE2 dans Delphi sans COM IStorage
Les remplissages de graphiques liés au thème, l’encodeur et le décodeur GelFrame ainsi que le générateur complet de sous-flux de graphiques BIFF8 sont livrés dans le composant tableur HotXLS pour Delphi pour Delphi et C++Builder, qui lit et écrit XLS, XLSX et ODS sans Excel installé