HotXLS projette des couleurs RGB et de theme arbitraires sur la palette BIFF8 de 56 entrées en deux couches : NearestIndexedColor trouve l'entrée existante perceptuellement la plus proche dans l'espace OKLab, et BuildBiffPalettePlan avec ApplyBiffPalettePlan réécrit les entrées libres de la palette pour qu'un classeur en couleurs vraies survive à une sauvegarde en XLS classique. Le déclencheur est toujours le même ticket de support. Quelqu'un construit un rapport en XLSX avec des en-têtes bleu marine institutionnels et un accent turquoise doux, l'enregistre en .xls pour un consommateur historique, et les en-têtes reviennent noir pur tandis que le turquoise se change en un turquoise criard. Rien n'a planté et aucun avertissement ne s'est déclenché. Le modèle de couleurs de l'ancien format ne peut tout simplement pas contenir ce que le nouveau décrivait, et la bibliothèque devait bien choisir quelque chose
Pourquoi un fichier XLS ne peut-il tenir que 56 couleurs ?
Parce qu'un format de cellule BIFF8 ne stocke jamais de valeur RGB : les polices, les remplissages et les bordures transportent un index de couleur, et l'enregistrement Palette global au classeur ($0092, [MS-XLS] §2.4.188) fournit exactement 56 entrées RGB opaques pour les index 8 à 63. Les index 0 à 7 sont des copies fixes des huit couleurs de base, et les valeurs au-dessus de 63 ne sont pas des couleurs du tout mais des jetons tels que premier plan système, arrière-plan système et texte de graphique. HotXLS expose la palette via un ColorIndex public de 1 à 56, à savoir l'index physique moins 7, et ResolveIndexedColor maintient les trois schémas de numérotation séparés grâce à TXLSIndexedColorSpace : xicsPublicColorIndex pour les valeurs d'API 1..56, xicsBiffIcv pour les index bruts sur disque, validés contre le sous-ensemble IcvFont, IcvXF ou IcvChart selon le rôle que vous passez, et xicsOoxmlIndexed, où 64 et 65 signifient premier plan et arrière-plan système
var
Res: TXLSIndexedColorResolution;
begin
// $40 est un jeton BIFF icv, pas une entrée de palette
Workbook.ResolveIndexedColor($40, xicsBiffIcv, Res);
case Res.Kind of
xickPalette: UseArgb(Res.ARGB); // entrée de palette, si résolue
xickAutomatic,
xickSystem: UseSystemColor(Res.SystemColorRole);
xickInvalid: RejectToken(Res.RawIndex);
end;
end;
Notez que l'exemple commute sur Res.Kind et ignore la valeur de retour booléenne. ResolveIndexedColor ne renvoie True que lorsqu'il a obtenu un ARGB concret, et la surcharge courte ne lit jamais le bureau Windows, donc un jeton automatic ou system revient légitimement à False tout en restant classé xickSystem. HotXLS l'a constaté dans son propre sérialiseur de classeur : du code qui traite False comme une absence de couleur jette silencieusement la signification Automatic et System du jeton. Si vous avez besoin de vraies valeurs RGB pour ces jetons, appelez la surcharge longue et fournissez un callback TXLSTryResolveSystemColor qui applique votre propre politique d'interface, d'export ou headless
Pourquoi HotXLS apparie-t-il les couleurs en OKLab plutôt qu'en RGB ?
Parce que les valeurs de canaux sRGB sont encodées en gamma, la distance euclidienne en RGB ne suit pas ce qu'un œil perçoit, et l'erreur est pire précisément dans les tons sombres et saturés qu'affectionnent les palettes institutionnelles. Prenez le bleu foncé $000033. En RGB, la distance au noir est de 51 et celle à l'entrée bleu marine par défaut $000080 vaut 77, donc un apparieur RGB peint vos en-têtes en noir avec aplomb. En OKLab, les distances au carré valent environ 0.0312 vers le noir et 0.0235 vers le bleu marine, et HotXLS choisit le bleu marine, ColorIndex 11 à l'emplacement physique 18 ; ce cas exact est verrouillé dans la suite de tests pour le moteur Classic comme pour le moteur XLSX. La conversion à l'intérieur d'ArgbToOklab linéarise chaque canal sRGB, applique la matrice LMS d'OKLab, prend des racines cubiques et projette sur L, a et b, après quoi une distance euclidienne au carré ordinaire est un proxy raisonnable de la différence perçue. OKLab n'est pas CIEDE2000 et ne prétend pas l'être, mais il n'a aucune correction de teinte par morceaux, coûte une poignée de multiplications par couleur, et est assez stable pour piloter une boucle de clustering, et c'est là qu'il gagne vraiment sa place
Que garantit NearestIndexedColor ?
NearestIndexedColor garantit une réponse déterministe et en lecture seule : une conversion de l'entrée, un balayage fixe des 56 entrées en cache, et l'index public le plus bas dès que deux entrées sont à égalité. Chaque classeur met en cache l'ARGB normalisé et les coordonnées OKLab des 56 emplacements physiques avec un compteur de génération de palette. Une réinitialisation de la palette reconstruit le cache, un changement d'un seul emplacement ne met à jour que celui-ci, et une requête contre une génération périmée renvoie False au lieu de deviner. Le balayage utilise une comparaison strictement inférieure en partant de l'emplacement 8, c'est pourquoi une palette contenant deux fois la même couleur répond toujours avec l'index le plus bas ; cela compte quand vous faites un diff de deux fichiers générés et attendez une sortie identique à l'octet près. L'alpha d'entrée suit un contrat étroit : un octet alpha nul est traité comme opaque, et une valeur partiellement transparente est rejetée avec ColorIndex 0 et PaletteSlot -1, puisque les entrées de palette n'ont pas d'alpha. Les écrivains de remplissage et de bordure du moteur Classic convertissent RGB et couleurs de theme en index avec la même routine d'appariement OKLab au moment de la sauvegarde, si bien que l'API et le fichier stocké s'accordent sur l'emplacement où une couleur atterrit
var
Match: TXLSNearestIndexedColorMatch;
begin
if Workbook.NearestIndexedColor($FF000033, Match) then
begin
// Match.ColorIndex = 11, Match.PaletteSlot = 18, Match.ARGB = $FF000080
if not Match.ExactMatch then
LogApproximation(Match.InputARGB, Match.ARGB, Match.DistanceSquared);
end;
end;
Comment BuildBiffPalettePlan fait-il tenir les couleurs vraies dans 56 emplacements ?
BuildBiffPalettePlan calcule une proposition complète pour les 56 emplacements sans toucher au classeur, si bien que vous pouvez l'inspecter, la journaliser ou la rejeter. Le planificateur appelle d'abord ScanIndexedColorUsage : tout emplacement qu'une police, un remplissage, une bordure, une mise en forme conditionnelle, une forme, un commentaire ou une ligne de grille de feuille référence par index est verrouillé, parce que changer une entrée de palette recolore d'un coup tous les consommateurs de cet index. Les cibles sont les couleurs RGB directes et les couleurs de theme résolues provenant des polices, remplissages, bordures, styles différentiels, barres de données et échelles de couleurs. Chaque cible est pondérée par le plus grand de son nombre de références rendues et de son nombre de définitions, et une mise en forme conditionnelle compte les cellules que ses plages couvrent, si bien qu'une couleur peinte sur toute une colonne l'emporte sur celle d'une seule note. Le placement suit ensuite un ordre fixe :
- Les emplacements verrouillés conservent leur couleur source inconditionnellement
- Une cible déjà présente dans la palette est conservée à son emplacement correspondant le plus bas et cet emplacement devient fixe
- Si les cibles uniques restantes tiennent dans les emplacements libres, chacune obtient un emplacement exact, attribué en ordre ARGB croissant
- Sinon
Quantizedest positionné, chaque emplacement libre est amorcé avec la cible dont la distance au centre existant le plus proche, multipliée par son poids, est la plus grande, et jusqu'à 16 rounds de k-means pondéré par fréquence en OKLab ne déplacent que les centres libres jusqu'à ce que les affectations cessent de changer
Soyez honnête avec vous-même sur ce que délivre le chemin de débordement. Le clustering est une optimisation locale bornée, pas un optimum global, et un emplacement libre finit par contenir un centroïde reconverti en sRGB avec bornage, qui peut être une couleur qu'aucune cellule n'a utilisée telle quelle. Ce que vous obtenez, c'est la répétabilité : le même classeur produit toujours le même plan, et le plan signale ses propres dégâts via WeightedError, MaxDistanceSquared, ExactTargetWeight et TotalTargetWeight, si bien qu'un traitement par lots peut refuser de sauvegarder quand l'approximation devient trop grossière pour une charte graphique
var
Plan: TXLSBiffPalettePlan;
I: Integer;
begin
Plan := Workbook.BuildBiffPalettePlan; // lecture seule
if Plan.Quantized and (Plan.MaxDistanceSquared > MaxAcceptedError) then
raise Exception.Create('Too many distinct colors for a BIFF8 palette');
for I := 0 to High(Plan.Slots) do
if Plan.Slots[I].Changed then
LogSlot(Plan.Slots[I].ColorIndex, Plan.Slots[I].SourceARGB,
Plan.Slots[I].TargetARGB);
if not Workbook.ApplyBiffPalettePlan(Plan) then
raise Exception.Create('The palette changed after planning');
end;
Comment ApplyBiffPalettePlan rejette-t-il un plan périmé ?
ApplyBiffPalettePlan valide le plan entier avant d'écrire un seul emplacement, et renvoie False avec la palette intacte si quoi que ce soit diverge du classeur courant. Le plan transporte SourcePaletteGeneration et SourcePaletteHash, un hash FNV-1a 64 bits sur les 56 couleurs sources ; la validation revérifie aussi chaque index public et physique, chaque couleur source, qu'aucun emplacement verrouillé n'est marqué comme changé, les compteurs de verrouillés et de changés, et que chaque cible est opaque. N'importe quel changement effectif de palette entre-temps, y compris une application antérieure réussie du même plan, rend le plan périmé, si bien que les plans sont en pratique à usage unique. Un plan valide sans emplacement changé réussit sans faire avancer la génération, et un vrai changement incrémente la génération une fois et reconstruit l'apparieur OKLab une fois, sur le moteur Classic en réécrivant le tableau fixe de la palette et sur le moteur XLSX en échangeant une liste de remplacement de couleurs indexées préparée à l'avance
Activation pour les sauvegardes BIFF8 et la conversion XLSX vers XLS
La propriété BiffPaletteSavePolicy vaut xbpsPreserve par défaut, si bien que mettre HotXLS à jour ne réécrit jamais la palette de personne dans son dos. La passer à xbpsOptimizeTrueColors fait construire et appliquer un plan frais à l'intérieur de SaveAs pour un classeur Classic, mais seulement quand le format cible est xlExcel97 ; BIFF5, CSV, HTML, PDF, XLSX et les autres écrivains ignorent ce réglage. Après une sauvegarde réussie, la palette optimisée reste dans le modèle du classeur, donc les requêtes et sauvegardes suivantes voient le même mapping. Si la sauvegarde échoue ou est annulée, les 56 couleurs d'origine et la génération d'origine sont restaurées. Pour les sources XLSX, SaveXLSXWorkbookAsXLS dans lxXlsxExport construit un seul plan depuis le classeur chargé et l'écrit dans la palette de destination avant toute conversion de style, ce qui est le pont déterministe qu'exerce la démonstration d'atelier d'audit et de conversion de classeurs. Les couleurs de theme passent par le même planificateur après résolution de leur teinte en RGB ; si vous préférez garder les themes vivants dans les remplissages de graphiques, l'article sur les remplissages de graphiques aux couleurs de theme GelFrame explique comment le XLS binaire stocke un index de schéma au lieu d'une couleur aplatie
// Classeur Classic : à activer, BIFF8 uniquement
Workbook.BiffPaletteSavePolicy := xbpsOptimizeTrueColors;
if Workbook.SaveAs('report.xls', xlExcel97) <> 1 then
HandleSaveFailure; // palette déjà restaurée
// Modèle XLSX vers BIFF8 avec un seul plan de palette déterministe
XWorkbook := TXLSXWorkbook.Create;
try
if XWorkbook.Open('report.xlsx') = 1 then
SaveXLSXWorkbookAsXLS(XWorkbook, 'report.xls');
finally
XWorkbook.Free;
end;
Les API de palette de HotXLS fonctionnent de la même manière sur IXLSWorkbook et TXLSXWorkbook, depuis Delphi comme depuis C++Builder. Téléchargez la version d'essai et pointez-la vers votre tableur le plus coloré depuis la page du composant Excel HotXLS pour Delphi