Technischer Artikel

BIFF8-56-Farbpalette: OKLab-Farbzuordnung in HotXLS

HotXLS mappt beliebige RGB- und Designfarben in zwei Schichten auf die BIFF8-Farbpalette mit 56 Plätzen: NearestIndexedColor findet den wahrnehmungsmäßig nächsten existierenden Paletteneintrag im OKLab-Raum, und BuildBiffPalettePlan zusammen mit ApplyBiffPalettePlan schreibt die freien Palettenplätze um, damit eine True-Color-Arbeitsmappe das Speichern als klassisches XLS überlebt. Der Auslöser ist immer dasselbe Support-Ticket. Jemand baut einen Report in XLSX mit firmenblauen Kopfzeilen und einem sanften Türkis-Akzent, speichert ihn als .xls für einen Legacy-Verbraucher, und die Kopfzeilen kommen rein schwarz zurück, während aus dem Türkis ein schrilles Cyan wird. Nichts ist abgestürzt, keine Warnung wurde ausgelöst. Das Farbmodell des alten Formats kann einfach nicht halten, was das neue beschrieben hat, und die Bibliothek musste sich für etwas entscheiden

Warum kann eine XLS-Datei nur 56 Farben halten?

Denn ein BIFF8-Zellformat speichert nie einen RGB-Wert: Schriftarten, Füllungen und Rahmen tragen einen Farbindex, und der arbeitsmappenglobale Palette-Record ($0092, [MS-XLS] §2.4.188) liefert exakt 56 opake RGB-Einträge für die Indizes 8 bis 63. Die Indizes 0 bis 7 sind feste Kopien der acht Grundfarben, und die Werte über 63 sind überhaupt keine Farben, sondern Tokens wie Systemvordergrund, Systemhintergrund und Charttext. HotXLS legt die Palette über einen öffentlichen ColorIndex von 1 bis 56 offen – dem physikalischen Index minus 7 –, und ResolveIndexedColor hält die drei Nummerierungsschemata über TXLSIndexedColorSpace auseinander: xicsPublicColorIndex für die API-Werte 1..56, xicsBiffIcv für die rohen On-Disk-Indizes, die je nach übergebener Rolle gegen die Teilmenge IcvFont, IcvXF oder IcvChart geprüft werden, und xicsOoxmlIndexed, wo 64 und 65 für Systemvordergrund und -hintergrund stehen

HotXLS hält die drei indizierten Farbschemata über TXLSIndexedColorSpace auseinander: rohe BIFF-icv-Werte, 0 bis 7 fest auf die acht Grundfarben gelegt, die 56 Palettenplätze 8 bis 63 des Palette-Records $0092, Tokens über 63 wie Systemvordergrund, öffentlicher ColorIndex 1 bis 56 mit Offset minus 7, und xicsOoxmlIndexed, wo 64 und 65 für Systemvordergrund und -hintergrund stehen
Derselbe Farbindex bedeutet in jedem Schema andere Zahlen, daher führt HotXLS jeden Wert durch ResolveIndexedColor, statt einen rohen BIFF-Token als öffentlichen ColorIndex ausgeben zu lassen
var
  Res: TXLSIndexedColorResolution;
begin
  // $40 ist ein BIFF-icv-Token, kein Palettenplatz
  Workbook.ResolveIndexedColor($40, xicsBiffIcv, Res);
  case Res.Kind of
    xickPalette:   UseArgb(Res.ARGB);   // Palettenplatz, falls aufgelöst
    xickAutomatic,
    xickSystem:    UseSystemColor(Res.SystemColorRole);
    xickInvalid:   RejectToken(Res.RawIndex);
  end;
end;

Beachten Sie, dass das Beispiel über Res.Kind verzweigt und den booleschen Rückgabewert ignoriert. ResolveIndexedColor gibt True nur dann zurück, wenn es ein konkretes ARGB ermittelt hat, und die kurze Überladung liest nie den Windows-Desktop, sodass ein Automatic- oder System-Token zu Recht False liefert, während er weiterhin als xickSystem klassifiziert ist. HotXLS ist darüber in seinem eigenen Workbook-Serializer gestolpert: Code, der False als „keine Farbe“ behandelt, wirft die Automatic- und System-Bedeutung des Tokens stillschweigend weg. Wenn Sie echte RGB-Werte für diese Tokens brauchen, rufen Sie die lange Überladung auf und geben einen TXLSTryResolveSystemColor-Callback mit, der Ihre eigene UI-, Export- oder Headless-Policy anwendet

Warum matcht HotXLS Farben in OKLab statt in RGB?

Weil sRGB-Kanalwerte gamma-kodiert sind, folgt der euklidische Abstand in RGB nicht dem, was ein Mensch sieht, und der Fehler ist ausgerechnet in den dunklen, gesättigten Tönen am größten, die Firmenpaletten so gerne einsetzen. Nehmen wir das Dunkelblau $000033. In RGB misst der Abstand zu Schwarz 51 und zum Standard-Navy-Eintrag $000080 77, also färbt ein RGB-Matcher Ihre Kopfzeile getrost schwarz. In OKLab liegen die quadrierten Abstände bei etwa 0.0312 zu Schwarz und 0.0235 zu Navy, und HotXLS wählt Navy, ColorIndex 11 am physikalischen Platz 18; genau dieser Fall ist in der Testsuite für beide Engines, Classic wie XLSX, festgenagelt. Die Konvertierung in ArgbToOklab linearisiert jeden sRGB-Kanal, wendet die OKLab-LMS-Matrix an, zieht Kubikwurzeln und projiziert auf L, a und b, wonach eine schlicht quadrierte euklidische Distanz eine brauchbare Näherung für die wahrgenommene Differenz ist. OKLab ist nicht CIEDE2000 und gibt das auch nicht vor, aber es hat keine stückweisen Hue-Korrekturen, kostet eine Handvoll Multiplikationen pro Farbe und ist stabil genug, um eine Clustering-Schleife anzutreiben – und genau dort spielt es seine Stärke aus

Wie HotXLS das Dunkelblau $000033 auf die Palette matcht: Der euklidische Abstand im gamma-kodierten RGB misst 51 zu Schwarz und 77 zu Navy und würde die Kopfzeile schwarz färben, während die quadrierten ArgbToOklab-Abstände von 0.0312 und 0.0235 NearestIndexedColor Navy wählen lassen, ColorIndex 11 am physikalischen Platz 18
Gamma-kodierte Kanalwerte machen den RGB-Abstand zu einem schlechten Näherungswert für die menschliche Wahrnehmung, daher konvertiert HotXLS einmal nach OKLab und lässt eine schlicht quadrierte euklidische Vergleichsrechnung den Palettenscan steuern

Was garantiert NearestIndexedColor?

NearestIndexedColor garantiert eine deterministische, nur lesende Antwort: eine Eingabekonvertierung, ein fester Scan über 56 gecachte Einträge und der niedrigste öffentliche Index, wann immer zwei Einträge gleich nah liegen. Jede Arbeitsmappe cacht das normalisierte ARGB und die OKLab-Koordinaten aller 56 physikalischen Plätze zusammen mit einem Palette-Generationszähler. Ein Palette-Reset baut den Cache neu, eine Änderung an einem einzelnen Platz aktualisiert nur diesen Platz, und eine Abfrage gegen eine veraltete Generation gibt False zurück, statt zu raten. Der Scan arbeitet mit einem strikten Kleiner-Vergleich ab Platz 8, weshalb eine Palette, die dieselbe Farbe zweimal enthält, immer mit dem niedrigeren Index antwortet; das ist relevant, wenn Sie zwei erzeugte Dateien diffen und byteidentische Ausgabe erwarten. Das Eingabe-Alpha folgt einem engen Vertrag: Ein Alpha-Byte von null gilt als opak, ein teilweise transparenter Wert wird mit ColorIndex 0 und PaletteSlot -1 abgelehnt, denn Paletteneinträge haben kein Alpha. Die Fill- und Border-Writer der Classic-Engine wandeln RGB- und Designfarben zur Speicherzeit mit derselben OKLab-Matching-Routine in einen Index um, sodass API und gespeicherte Datei darüber einig sind, auf welchem Platz eine Farbe landet

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;

Wie bringt BuildBiffPalettePlan True Colors in 56 Plätze unter?

BuildBiffPalettePlan berechnet einen vollständigen Vorschlag für alle 56 Plätze, ohne die Arbeitsmappe anzufassen, sodass Sie ihn inspizieren, loggen oder verwerfen können. Der Planner ruft zuerst ScanIndexedColorUsage auf: Jeder Platz, auf den eine Schriftart, Füllung, ein Rahmen, eine bedingte Formatierung, eine Shape, ein Kommentar oder eine Worksheet-Gitterlinie per Index verweist, wird gesperrt, denn eine geänderte Palettenadresse färbt jeden Konsumenten dieses Index auf einmal um. Ziele sind die direkten RGB- und die aufgelösten Designfarben aus Schriftarten, Füllungen, Rahmen, Differential-Styles, Data Bars und Color Scales. Jedes Ziel wird mit dem größeren Wert aus gerenderter Referenzanzahl und Definitionsanzahl gewichtet, und eine bedingte Formatierung zählt die Zellen, die ihre Bereiche abdecken, sodass eine Farbe über eine ganze Spalte schwerer wiegt als eine in einer einzelnen Notiz. Die Platzierung läuft dann in fester Reihenfolge ab:

  • Gesperrte Plätze behalten ihre Quellfarbe bedingungslos
  • Ein Ziel, das in der Palette bereits existiert, bleibt auf seinem niedrigsten passenden Platz liegen, und dieser Platz wird fest
  • Passen die verbleibenden eindeutigen Ziele in die freien Plätze, bekommt jedes einen exakten Platz, vergeben in aufsteigender ARGB-Reihenfolge
  • Andernfalls wird Quantized gesetzt, jeder freie Platz wird mit dem Ziel eingesät, dessen Abstand zum nächsten existierenden Zentrum, multipliziert mit seinem Gewicht, am größten ist, und bis zu 16 Runden frequenzgewichtetes k-Means in OKLab bewegen nur die freien Zentren, bis die Zuweisungen sich nicht mehr ändern

Seien Sie ehrlich zu sich selbst, was der Overflow-Pfad liefert. Das Clustering ist eine beschränkte lokale Optimierung, kein globales Optimum, und ein freier Platz hält am Ende ein Zentrum, das mit Clamping zurück nach sRGB konvertiert wurde – möglicherweise eine Farbe, die keine Zelle je wörtlich verwendet hat. Was Sie bekommen, ist Reproduzierbarkeit: Dieselbe Arbeitsmappe liefert immer denselben Plan, und der Plan meldet seinen eigenen Schaden über WeightedError, MaxDistanceSquared, ExactTargetWeight und TotalTargetWeight, sodass ein Batch-Job das Speichern verweigern kann, wenn die Approximation für eine Brand Guideline zu grob wird

Die HotXLS-Paletten-Pipeline für eine True-Color-Arbeitsmappe: ScanIndexedColorUsage sperrt jeden Platz, auf den eine Schriftart, Füllung, ein Rahmen, eine bedingte Formatierung, eine Shape, ein Kommentar oder eine Gitterlinie verweist, BuildBiffPalettePlan platziert exakte Farben in aufsteigender ARGB-Reihenfolge oder führt bis zu 16 Runden frequenzgewichtetes k-Means in OKLab aus, und ApplyBiffPalettePlan prüft Generation und FNV-1a-Hash, bevor geschrieben wird
Die Planung ist nur lesend und reproduzierbar, der Plan meldet seinen eigenen Schaden über WeightedError und MaxDistanceSquared, und ein veralteter Plan wird abgelehnt, ohne dass die Palette angefasst wird, denn Pläne sind faktisch Einweg
var
  Plan: TXLSBiffPalettePlan;
  I: Integer;
begin
  Plan := Workbook.BuildBiffPalettePlan;   // nur lesend
  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;

Wie verwirft ApplyBiffPalettePlan einen veralteten Plan?

ApplyBiffPalettePlan validiert den gesamten Plan, bevor auch nur ein Platz geschrieben wird, und gibt False mit unangetaster Palette zurück, wenn etwas nicht zur aktuellen Arbeitsmappe passt. Der Plan trägt SourcePaletteGeneration und SourcePaletteHash, einen 64-Bit-FNV-1a-Hash über die 56 Quellfarben; die Validierung prüft außerdem jeden öffentlichen und physikalischen Index nach, jede Quellfarbe, dass kein gesperrter Platz als geändert markiert ist, die Zähler für gesperrte und geänderte Plätze und dass jedes Ziel opak ist. Jede wirksame Palettenänderung dazwischen, inklusive einer erfolgreichen früheren Anwendung desselben Plans, macht den Plan veraltet – Pläne sind also faktisch Einweg. Ein valider Plan ohne geänderte Plätze läuft durch, ohne die Generation voranzustellen, und eine echte Änderung stellt die Generation einmal vor und baut den OKLab-Matcher einmal neu – in der Classic-Engine durch Neuschreiben des festen Paletten-Arrays, in der XLSX-Engine durch Eintauschen einer vorbereiteten Indexed-Color-Override-Liste

Einschalten für BIFF8-Saves und die XLSX-nach-XLS-Konvertierung

Die Property BiffPaletteSavePolicy steht standardmäßig auf xbpsPreserve, ein HotXLS-Upgrade schreibt also niemandem die Palette hinter dem Rücken um. Stellt man sie auf xbpsOptimizeTrueColors, baut eine Classic-Arbeitsmappe innerhalb von SaveAs einen frischen Plan und wendet ihn an – aber nur, wenn das Zielformat xlExcel97 ist; BIFF5, CSV, HTML, PDF, XLSX und die anderen Writer ignorieren die Einstellung. Nach einem erfolgreichen Speichern bleibt die optimierte Palette im Workbook-Modell, spätere Abfragen und Saves sehen also dieselbe Zuordnung. Schlägt das Speichern fehl oder wird es abgebrochen, werden die ursprünglichen 56 Farben und die ursprüngliche Generation wiederhergestellt. Für XLSX-Quellen baut SaveXLSXWorkbookAsXLS in lxXlsxExport einen einzigen Plan aus der geladenen Arbeitsmappe und schreibt ihn in die Zielpalette, noch bevor ein Style konvertiert wird – die deterministische Brücke, die die Workbook-Audit- und Konvertierungs-Workbench-Demo fährt. Designfarben laufen nach Auflösung ihres Tint nach RGB durch denselben Planner; wenn Sie Designs lieber in Chart-Füllungen lebendig halten wollen, erklärt der Artikel zu GelFrame-Designfarben in Chart-Füllungen, wie binäres XLS einen Schema-Index statt einer abgeflachten Farbe speichert

// Klassische Arbeitsmappe: Opt-in, nur BIFF8
Workbook.BiffPaletteSavePolicy := xbpsOptimizeTrueColors;
if Workbook.SaveAs('report.xls', xlExcel97) <> 1 then
  HandleSaveFailure;   // Palette bereits wiederhergestellt

// XLSX-Modell nach BIFF8 mit einem deterministischen Palettenplan
XWorkbook := TXLSXWorkbook.Create;
try
  if XWorkbook.Open('report.xlsx') = 1 then
    SaveXLSXWorkbookAsXLS(XWorkbook, 'report.xls');
finally
  XWorkbook.Free;
end;

Die HotXLS-Paletten-APIs arbeiten auf IXLSWorkbook und TXLSXWorkbook gleichermaßen, aus Delphi wie aus C++Builder. Laden Sie die Trial herunter und richten Sie sie auf Ihre farbigste Tabelle – auf der Seite zur HotXLS-Delphi-Excel-Komponente gibt es den Download