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
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
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
Quantizedgesetzt, 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
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