Het donkerblauwe $000033 zit 51 eenheden van zwart en 77 eenheden van de klassieke marineblauwe invoer $000080 als je afstand meet als kale Euclidische afstand over RGB-kanaalwaarden. Een converter die klassieke .xls-bestanden schrijft met nearest-RGB-matching verft een donkerblauwe kopregel dus zwart, en zwart is wat elke lezer die alleen naar de paletindex kijkt toont. Het formaat laat de schrijver geen ruimte: een celformaat slaat een paletindex op, en het palet bevat 56 invoeren. De kopregel goed krijgen vraagt twee dingen, een afstandsmaat die volgt wat mensen zien, en een manier om het palet zelf te herschrijven wanneer een werkmap meer verschillende kleuren gebruikt dan het palet aan kan
Waar het kleurlimiet van 56 vandaan komt
Een BIFF8-record voor lettertype, vulling of rand draagt nooit een RGB-waarde. Hij draagt een kleurindex, en het werkboekglobale Palette-record ($0092, [MS-XLS] §2.4.188) levert precies 56 ondoorzichtige RGB-invoeren voor de indexen 8 tot en met 63. Indexen 0 tot 7 zijn vaste kopieën van de acht basiskleuren. Waarden boven 63 zijn helemaal geen kleuren maar tokens: $40 is de systeemvoorgrond, $41 de systeemachtergrond, $4D tot $4F zijn charttekst, chartachtergrond en de neutrale chartkleur, en $7FFF betekent automatic. Excel 2007 voegde het XFExt-record toe ($087D, §2.4.355), dat een volledige RGB- of themakleur aan een celformaat kan hangen, maar Excel 97–2003 en elke parser die alleen het XF-record leest zien nog niets dan de index
HotXLS stelt het palet beschikbaar via een publieke ColorIndex van 1 tot 56, de fysieke index min 7. Drie nummeringsschema's zijn in het spel, en ResolveIndexedColor houdt ze uit elkaar via TXLSIndexedColorSpace: xicsPublicColorIndex voor de API-waarden 1..56, xicsBiffIcv voor rauwe indexen op schijf, en xicsOoxmlIndexed, waar 64 en 65 systeemvoorgrond en -achtergrond betekenen. Rauwe BIFF-indexen worden ook gecontroleerd tegen de rol die je als TXLSIndexedColorRole doorgeeft, want een lettertype, een vulling en een chart accepteren verschillende tokensets; $51 is legaal voor een lettertype en ongeldig voor een vulling
De Booleaanse uitkomst van ResolveIndexedColor is waar aanroepers struikelen. Hij geeft True alleen terug wanneer hij een concrete ARGB-waarde heeft verkregen, en de korte overload vraagt het besturingssysteem nooit om desktopkleuren. Een system- of automatic-token komt daarom terug als False met Res.Kind op xickSystem of xickAutomatic, en code die False leest als "geen kleur" gooit stilletjes de betekenis van het token weg. De enige uitzondering is $4F, de neutrale chartkleur, die zonder hulp naar zwart resolveert. Voor echte RGB-waarden bij de andere tokens geef je een TXLSTryResolveSystemColor-methode door aan de lange overload en pas je je eigen beleid toe voor de UI, een export of een headless service:
type
TExportColors = class
function ResolveSystemColor(AColor: TXLSSystemColorRole;
ARole: TXLSIndexedColorRole; out AArgb: LongWord): Boolean;
end;
function TExportColors.ResolveSystemColor(AColor: TXLSSystemColorRole;
ARole: TXLSIndexedColorRole; out AArgb: LongWord): Boolean;
begin
// headless export: vaste kleuren in plaats van het huidige desktopthema
Result := True;
case AColor of
xscrWindowText, xscrChartText, xscrToolTipText:
AArgb := $FF000000;
xscrWindowBackground, xscrChartBackground:
AArgb := $FFFFFFFF;
else
Result := False;
end;
end;
var
Colors: TExportColors; // éénmaal aangemaakt door je exportcode
Res: TXLSIndexedColorResolution;
begin
// $41 is het systeemachtergrond-token, geldig voor een vulling
if Workbook.ResolveIndexedColor($41, xicsBiffIcv, xicrFillBackground,
Colors.ResolveSystemColor, Res) then
PaintBackground(Res.ARGB) // je eigen tekenroutine
else if Res.Kind = xickInvalid then
RejectToken(Res.RawIndex); // je eigen foutafhandeling
end;
Waarom kiest RGB-afstand de verkeerde paletinvoer?
sRGB-kanaalwaarden zijn gamma-gecodeerd, dus gelijke stappen in de getallen zijn geen gelijke stappen in wat een mens ziet, en de fout is het grootst in precies de donkere, verzadigde tonen waar huisstijlsjablonen van houden. In RGB zit $000033 op 51 van zwart en 77 van marineblauw, dus zwart wint. Zodra elk kanaal is gelineariseerd en de kleur in OKLab is geprojecteerd, zijn de gekwadrateerde afstanden ongeveer 0.0312 tot zwart en 0.0235 tot marineblauw, en marineblauw wint. De match is ColorIndex 11 op fysiek slot 18, en de Classic- en XLSX-engines geven hetzelfde antwoord
Het standaardpalet voegt een complicatie toe. Marineblauw staat twee keer in het palet, op ColorIndex 11 en opnieuw op 25, dus $000033 zit exact even ver van beide. Een matcher die in een andere volgorde scant, of kleiner-of-gelijk gebruikt, kiest 25 in de ene run en 11 in een andere build, en twee gegenereerde bestanden diffen niet meer netjes. HotXLS scant vanaf slot 8 omhoog met een strikte kleiner-dan-vergelijking, dus een gelijkspel gaat altijd naar de lagere index
OKLab is een bewuste afweging. CIEDE2000 zou sommige hue-paren nauwkeuriger rangschikken, maar zijn stuksgewijze correcties maken hem traag en lastiger te doorgronden binnen een iteratieve lus. OKLab kost een linearisatie, twee kleine matrixproducten en drie derdemachtswortels per kleur, waarna een kale gekwadrateerde afstand een goed genoeg plaatsvervanger is voor waargenomen verschil. Voor een paletscan over 56 invoeren die bij elke kleurtoewijzing en binnen een clusteringspas draait, is de goedkopere, gladdere metriek de juiste keuze
Wat geeft NearestIndexedColor terug?
NearestIndexedColor neemt een ARGB-waarde aan en vult een TXLSNearestIndexedColorMatch-record zonder het werkboek te veranderen. Elke werkmap houdt de 56 paletkleuren bij met hun OKLab-coördinaten en een paletgeneratieteller, dus een query kost één invoerconversie en één vaste scan. Het palet resetten bouwt die cache opnieuw, het veranderen van één invoer actualiseert alleen dat slot, en een query tegen een cache die uit de pas loopt met de huidige generatie geeft False terug in plaats van te gokken
Alpha volgt een smalle regel. Een alpha-byte van nul geldt als dekkend, wat aansluit bij hoe de meeste Delphi-code kale RGB-waarden doorgeeft, en $FF is per definitie dekkend. Alles daartussenin wordt geweigerd: de functie geeft False terug met ColorIndex 0 en PaletteSlot -1, want een paletinvoer heeft nergens transparantie op te slaan
var
Match: TXLSNearestIndexedColorMatch;
begin
if Workbook.NearestIndexedColor($FF000033, Match) then
begin
// standaardpalet: Match.ColorIndex = 11, Match.PaletteSlot = 18,
// Match.ARGB = $FF000080
if not Match.ExactMatch then
LogApproximation(Match.InputARGB, Match.ARGB, // je eigen logger
Match.DistanceSquared);
end;
end;
Dezelfde matcher zit achter de RGB-setters. Het toekennen van Interior.Color, Font.Color of Border.Color op een Classic-werkmap houdt de exacte kleur voor het XFExt-record en kiest met deze scan de index die in het XF-record komt, en de vulling- en randwriters resolven RGB- en themakleuren op dezelfde manier naar een index. Wat NearestIndexedColor voor een kleur rapporteert, is het slot dat een index-only lezer toont
Een true-color-werkboek in 56 slots passen
Matchen tegen het standaardpalet is prima voor een handvol kleuren. Een huisstijlsjabloon met een dozijn bijna identieke blauwtinten vraagt meer: het palet zelf moet veranderen. BuildBiffPalettePlan berekent een compleet voorstel voor alle 56 slots en laat het werkboek onaangetast, zodat je het kunt loggen, tegen een drempel kunt afzetten of kunt weggooien
De planner begint met ScanIndexedColorUsage. Elk slot waarnaar een lettertype, vulling, rand, voorwaardelijke opmaak, shape, opmerking, chartlettertype of werkbladrasterlijn via een index verwijst, wordt vergrendeld, want het wijzigen van die invoer kleurt in één keer alle consumenten van de index opnieuw. De targets zijn de directe RGB- en opgeloste themakleuren uit lettertypen, vullingen, kleurverloopstops, randen, stijlen van voorwaardelijke opmaak, databalken en kleurschalen. Elke target wordt gewogen op het grotere van zijn referentieaantal en zijn definitieaantal, en een voorwaardelijke opmaak telt elke cel die zijn bereiken bestrijken, dus een vulling die een hele kolom afloopt weegt zwaarder dan een kleur in één opmerking. De plaatsing volgt daarna een vaste volgorde:
- Vergrendelde slots behouden hun huidige kleur onvoorwaardelijk
- Een target die al in het palet bestaat, houdt zijn laagste matchende slot, en dat slot wordt vastgelegd
- Passen de resterende unieke targets in de vrije slots, dan krijgt elk daarvan een exact slot, toegekend in oplopende ARGB-volgorde
- Anders wordt
Quantizedgezet, wordt elk vrij slot geseed met de target waarvan de afstand tot het dichtstbijzijnde bestaande middelpunt, vermenigvuldigd met zijn gewicht, het grootst is, en verschuiven maximaal 16 rondes gewogen k-means in OKLab alleen de vrije middelpunten tot de toewijzingen niet meer veranderen
Het overlooppad verdient een heldere lezing. K-means is hier een begrensde lokale optimalisatie, en een vrij slot houdt uiteindelijk een clustermiddelpunt vast dat met clamping terug naar sRGB is geconverteerd, wat een kleur kan zijn die geen enkele cel letterlijk gebruikte. Wat de planner wél levert is herhaalbaarheid: hetzelfde werkboek levert altijd hetzelfde plan op. Het plan rapporteert ook hoe ver hij moest buigen via WeightedError, MaxDistanceSquared, ExactTargetWeight en TotalTargetWeight, wat genoeg is voor een batchtaak om een opslag te weigeren zodra de benadering te grof wordt voor een huisstijlrichtlijn. De Targets-array toont per invoerkleur het slot waarnaar hij mapt en zijn gekwadrateerde afstand
const
MaxAcceptedError = 0.002; // jouw drempel, in gekwadrateerde OKLab-eenheden
var
Plan: TXLSBiffPalettePlan;
I: Integer;
begin
Plan := Workbook.BuildBiffPalettePlan; // alleen-lezen
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); // je eigen logger
if not Workbook.ApplyBiffPalettePlan(Plan) then
raise Exception.Create('The palette changed after planning');
end;
Wanneer weigert ApplyBiffPalettePlan een plan?
ApplyBiffPalettePlan controleert het volledige plan voordat hij één slot wegschrijft en geeft False terug, met het palet onaangeroerd, als er iets niet klopt met het huidige werkboek. Het plan draagt SourcePaletteGeneration en SourcePaletteHash, een 64-bit FNV-1a-hash over de 56 bronkleuren. De validatie controleert bovendien elke publieke en fysieke index, elke bronkleur, dat geen vergrendeld slot als gewijzigd staat gemarkeerd, dat de aantallen vergrendeld en gewijzigd optellen, en dat elke target dekkend is
Elke echte paletwijziging tussen plannen en toepassen maakt het plan verouderd, en dat geldt ook voor een eerdere geslaagde toepassing van hetzelfde plan. Plannen zijn in de praktijk eenmalig bruikbaar: bouwen, inspecteren, toepassen, en opnieuw bouwen als je er nog een nodig hebt. Een geldig plan zonder gewijzigde slots slaagt zonder de generatie op te schuiven. Een echte wijziging schuift hem één keer op en ververset de matcher één keer. Op de Classic-engine gaan de nieuwe kleuren rechtstreeks het palet in; op de XLSX-engine worden ze custom indexed colors, die HasCustomIndexedColor en CustomIndexedColorCount rapporteren en die de XLSX-writer als een indexedColors-blok uitzendt
Opt-in voor XLS-opslag en XLSX-naar-XLS-conversie
De eigenschap BiffPaletteSavePolicy staat standaard op xbpsPreserve, dus het palet van een werkmap verandert alleen als je erom vraagt. xbpsOptimizeTrueColors zetten laat een Classic-werkmap binnen SaveAs een vers plan bouwen en toepassen, en alleen wanneer het doelformaat xlExcel97 is; BIFF5, CSV, HTML, PDF, XLSX en de andere writers negeren de instelling. Na een geslaagde opslag blijft het geoptimaliseerde palet in de werkmap zitten, zodat latere queries en opslagen dezelfde mapping zien. Faalt de opslag, wordt die geannuleerd of geeft die een exception, dan worden de oorspronkelijke 56 kleuren en de oorspronkelijke generatie hersteld
Voor gegenereerde rapporten zet je het optimaliserende beleid aan. Die werkmappen zijn opgebouwd uit RGB- en themakleuren, niemand heeft hun palet met de hand afgestemd, en een index-only lezer toont ze er met dat beleid veel dichter bij het origineel. Houd xbpsPreserve voor werkmappen waarvan het palet deel van het contract uitmaakt, zoals een sjabloon die stroomafwaartse code via ColorIndex bewerkt, waar een slot dat stilletjes van kleur verandert die code zou breken
XLSX-bronnen nemen een andere route. SaveXLSXWorkbookAsXLS in lxXlsxExport bouwt één plan vanuit het geladen TXLSXWorkbook en schrijft dat in het bestemmingspalet voordat er ook maar één stijl wordt geconverteerd, zodat elke geconverteerde cel tegen het uiteindelijke palet wordt gematcht. De workbook audit- en conversie-workbenchdemo draait precies dit pad. Themakleuren gaan na het omrekenen van hun tint naar RGB door dezelfde planner; chartvullingen die levende themaverwijzingen moeten blijven zijn een apart geval, behandeld in het artikel over GelFrame-themakleur-chartvullingen, waar binair XLS een schema-index opslaat in plaats van een afgevlakte kleur
// klassiek werkboek: opt-in, geldt alleen voor BIFF8-opslag
Workbook.BiffPaletteSavePolicy := xbpsOptimizeTrueColors;
if Workbook.SaveAs('report.xls', xlExcel97) <> 1 then
HandleSaveFailure; // je eigen handler; palet al hersteld
// XLSX-model naar BIFF8 met één deterministisch paletplan
XWorkbook := TXLSXWorkbook.Create;
try
if XWorkbook.Open('report.xlsx') = 1 then
if SaveXLSXWorkbookAsXLS(XWorkbook, 'report.xls') <> 1 then
HandleSaveFailure;
finally
XWorkbook.Free;
end;
De paletaanroepen (ResolveIndexedColor, NearestIndexedColor, ScanIndexedColorUsage, BuildBiffPalettePlan en ApplyBiffPalettePlan) hebben dezelfde handtekeningen op IXLSWorkbook en TXLSXWorkbook, vanuit Delphi en C++Builder. De trial-build op de HotXLS Delphi Excel-componentpagina is genoeg om een plan tegen je kleurrijkste sjabloon te draaien en MaxDistanceSquared te lezen voordat er iets wordt weggeschreven