Article technique

Palette BIFF8 de 56 couleurs : mapping OKLab dans HotXLS

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

HotXLS maintient les trois schémas de couleurs indexées séparés via TXLSIndexedColorSpace : valeurs BIFF icv brutes, 0 à 7 fixés aux huit couleurs de base, les 56 entrées de palette 8 à 63 de l'enregistrement Palette $0092, jetons au-dessus de 63 comme le premier plan système, ColorIndex public 1 à 56 décalé de moins 7, et xicsOoxmlIndexed où 64 et 65 signifient premier plan et arrière-plan système
Le même index de couleur désigne des nombres différents dans chaque schéma, donc HotXLS route chaque valeur via ResolveIndexedColor au lieu de laisser un jeton BIFF brut se faire passer pour un ColorIndex public
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

Comment HotXLS ramène le bleu foncé $000033 sur la palette : la distance euclidienne en RGB encodé gamma mesure 51 vers le noir et 77 vers le bleu marine et peindrait l'en-tête en noir, tandis que les distances au carré de ArgbToOklab, 0.0312 et 0.0235, permettent à NearestIndexedColor de choisir le bleu marine, ColorIndex 11 à l'emplacement physique 18
Les valeurs de canaux encodées gamma font de la distance RGB un mauvais proxy de ce qu'un œil perçoit, donc HotXLS convertit une seule fois vers OKLab et laisse une comparaison euclidienne au carré ordinaire piloter le balayage de la palette

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 Quantized est 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

Le pipeline de palette HotXLS pour un classeur en couleurs vraies : ScanIndexedColorUsage verrouille chaque emplacement qu'une police, un remplissage, une bordure, une mise en forme conditionnelle, une forme, un commentaire ou une ligne de grille référence, BuildBiffPalettePlan place les couleurs exactes en ordre ARGB croissant ou lance jusqu'à 16 rounds de k-means pondéré par fréquence en OKLab, et ApplyBiffPalettePlan valide la génération et le hash FNV-1a avant d'écrire
La planification est en lecture seule et répétable, le plan signale ses propres dégâts via WeightedError et MaxDistanceSquared, et un plan périmé est rejeté avec la palette intacte parce que les plans sont en pratique à usage unique
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