Technický článek

56barevná paleta BIFF8: mapování barev přes OKLab v HotXLS

HotXLS mapuje libovolné RGB a barvy motivů na 56slotovou barevnou paletu BIFF8 ve dvou vrstvách: NearestIndexedColor najde v prostoru OKLab smyslově nejbližší existující položku palety a BuildBiffPalettePlan spolu s ApplyBiffPalettePlan přepíše volné sloty palety, aby sešit s plnými barvami přežil uložení do klasického XLS. Spouštěčem je vždy stejný support ticket. Někdo postaví report v XLSX se záhlavími ve firemní tmavě modré a tlumeným teal akcentem, uloží ho jako .xls kvůli legacy konzumentovi a záhlaví se vrátí čistě černá, zatímco teal skončí jako křiklavě tyrkysový. Nic nespadlo a žádné varování nevyběhlo. Barevný model starého formátu prostě neunese, co popisoval ten nový, a knihovna si musela něco vybrat

Proč zvládne soubor XLS jen 56 barev?

Protože formát buňky BIFF8 nikdy neukládá hodnotu RGB: písma, výplně a ohraničení nesou index barvy a záznam Palette globální pro celý sešit ($0092, [MS-XLS] §2.4.188) dodává přesně 56 neprůhledných RGB položek pro indexy 8 až 63. Indexy 0 až 7 jsou pevné kopie osmi základních barev a hodnoty nad 63 nejsou barvy vůbec, ale tokeny jako system foreground, system background a text grafu. HotXLS vystavuje paletu přes veřejný ColorIndex 1 až 56, což je fyzický index mínus 7, a ResolveIndexedColor udržuje ta tři schémata číslování odděleně přes TXLSIndexedColorSpace: xicsPublicColorIndex pro API hodnoty 1..56, xicsBiffIcv pro syrové indexy na disku, které se validují proti podmnožině IcvFont, IcvXF nebo IcvChart podle role, kterou předáte, a xicsOoxmlIndexed, kde 64 a 65 znamenají system foreground a background

HotXLS drží tři schémata indexovaných barev odděleně přes TXLSIndexedColorSpace: syrové hodnoty BIFF icv, 0 až 7 pevně přiřazené osmi základním barvám, 56 slotů palety 8 až 63 ze záznamu Palette $0092, tokeny nad 63 jako system foreground, veřejný ColorIndex 1 až 56 posunutý o mínus 7 a xicsOoxmlIndexed, kde 64 a 65 znamenají system foreground a background
Stejný index barvy znamená v každém schématu jiné číslo, takže HotXLS pouští každou hodnotu přes ResolveIndexedColor, místo aby nechal syrový BIFF token vydávat se za veřejný ColorIndex
var
  Res: TXLSIndexedColorResolution;
begin
  // $40 je BIFF icv token, ne slot palety
  Workbook.ResolveIndexedColor($40, xicsBiffIcv, Res);
  case Res.Kind of
    xickPalette:   UseArgb(Res.ARGB);   // slot palety, pokud se rozliší
    xickAutomatic,
    xickSystem:    UseSystemColor(Res.SystemColorRole);
    xickInvalid:   RejectToken(Res.RawIndex);
  end;
end;

Všimněte si, že příklad switchuje podle Res.Kind a Boolean návratovou hodnotu ignoruje. ResolveIndexedColor vrací True jen tehdy, když získalo konkrétní ARGB, a krátká overload nikdy nečte plochu Windows, takže automatický nebo systémový token legitimně vrátí False, přestože zůstává klasifikovaný jako xickSystem. HotXLS na to narazilo ve vlastním serializátoru sešitů: kód, který bere False jako „žádnou barvu“, potichu zahodí význam tokenů Automatic a System. Pokud pro tyhle tokeny potřebujete skutečné RGB hodnoty, zavolejte dlouhou overload a dodáte callback TXLSTryResolveSystemColor, který uplatní vaši vlastní UI, exportní nebo headless politiku

Proč porovnává HotXLS barvy v OKLab místo v RGB?

Protože hodnoty kanálů sRGB jsou gamma kódované, takže euklidovská vzdálenost v RGB nenásleduje, co vidí člověk, a chyba je nejhorší přesně v těch tmavých, sytých tónech, které firemní palety milují. Vezměte si tmavě modrou $000033. V RGB je vzdálenost k černé 51 a vzdálenost k výchozí položce navy $000080 je 77, takže porovnávání přes RGB vám sebejistě natře záhlaví černě. V OKLab jsou kvadratické vzdálenosti zhruba 0.0312 k černé a 0.0235 k navy a HotXLS vybere navy, ColorIndex 11 na fyzickém slotu 18; tenhle přesný případ je v test sadě připíchnutý pro Classic i XLSX engine. Konverze uvnitř ArgbToOklab linearizuje každý sRGB kanál, aplikuje LMS matici OKLab, vezme krychlové odmocniny a projektuje na L, a a b, po čemž je obyčejná kvadratická euklidovská vzdálenost rozumným proxy vnímaného rozdílu. OKLab není CIEDE2000 a nechce za sebe vydávat, ale nemá žádné po úsecích rozdělené korekce odstínu, stojí pár násobení na barvu a je dostatečně stabilní, aby poháněla smyčku clusteringu — a tam je vlastně jeho skutečná síla

Jak HotXLS přiřadí tmavě modrou $000033 na paletu: euklidovská vzdálenost v gamma kódovaném RGB měří 51 k černé a 77 k navy a natřela by záhlaví černě, zatímco kvadratické vzdálenosti ArgbToOklab 0.0312 a 0.0235 nechají NearestIndexedColor vybrat navy, ColorIndex 11 na fyzickém slotu 18
Gamma kódované hodnoty kanálů dělají z RGB vzdálenosti špatný proxy toho, co vidí člověk, takže HotXLS zkonvertuje vstup jednou do OKLab a nechá obyčejné kvadratické euklidovské porovnání řídit sken palety

Co garantuje NearestIndexedColor?

NearestIndexedColor garantuje deterministickou odpověď bez zápisu: jednu konverzi vstupu, jeden pevný sken přes 56 cachovaných položek a nejnižší veřejný index vždy, když jsou dvě položky stejně blízko. Každý sešit cachuje normalizované ARGB a OKLab souřadnice všech 56 fyzických slotů spolu s počítadlem generace palety. Reset palety cache přestaví, změna jediného slotu aktualizuje jen ten slot a dotaz proti zastaralé generaci vrátí False místo hádání. Sken používá ostré méně než počínaje slotem 8, proto paleta obsahující tutéž barvu dvakrát vždy odpoví nižším indexem; na tom záleží, když diffujete dva vygenerované soubory a očekáváte bajtově identický výstup. Vstupní alpha se drží úzkého kontraktu: nulový alpha byte se bere jako neprůhledný a částečně průhledná hodnota se odmítne s ColorIndex 0 a PaletteSlot -1, protože položky palety nemají alpha. Writery výplní a ohraničení Classic engine převádějí RGB a barvy motivů na index stejnou OKLab rutinou při ukládání, takže API a uložený soubor se shodnou na tom, do kterého slotu barva dopadne

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;

Jak vměstká BuildBiffPalettePlan plné barvy do 56 slotů?

BuildBiffPalettePlan spočítá kompletní návrh pro všech 56 slotů, aniž by do sešitu sahal, takže ho můžete prohlédnout, zalogovat nebo zahodit. Planner nejdřív zavolá ScanIndexedColorUsage: každý slot, na který písmo, výplň, ohraničení, podmíněný formát, shape, komentář nebo mřížka listu odkazuje indexem, je uzamčený, protože změna položky palety přebarví všechny konzumenty toho indexu najednou. Targety jsou přímé RGB a rozlišené barvy motivů z písem, výplní, ohraničení, differential stylů, datových pruhů a barevných škál. Každý target se váží větší hodnotou z počtu vykreslených odkazů a počtu definic a podmíněný formát počítá buňky, které pokrývají jeho range, takže barva přetřená přes celý sloupec přebsite barvu použitou v jediné poznámce. Umístění pak probíhá v pevném pořadí:

  • Uzamčené sloty si ponechávají svou zdrojovou barvu bezpodmínečně
  • Target, který už v paletě existuje, se zachová na nejnižším odpovídajícím slotu a ten slot se zafixuje
  • Pokud se zbývající unikátní targety vejdou do volných slotů, každý dostane exaktní slot, přidělovaný ve vzestupném pořadí ARGB
  • Jinak se nastaví Quantized, každý volný slot se osadí targetem, jehož vzdálenost k nejbližšímu existujícímu centru krát jeho váha je největší, a až 16 kol frequency-weighted k-means v OKLab posouvá jen volná centra, dokud se přiřazení přestanou měnit

Buďte k sobě upřímní v tom, co overflow cesta skutečně dodá. Clustering je ohraničená lokální optimalizace, ne globální optimum, a volný slot nakonec drží centroid převedený zpět do sRGB s clampingem, což může být barva, kterou žádná buňka doslova nepoužila. Co ale dostanete, je opakovatelnost: tentýž sešit vždy dá tentýž plán a plán hlásí vlastní škody přes WeightedError, MaxDistanceSquared, ExactTargetWeight a TotalTargetWeight, takže batch job může uložení odmítnout, když je aproximace pro brand guidelines příliš hrubá

Pipeline palety HotXLS pro sešit s plnými barvami: ScanIndexedColorUsage uzamkne každý slot, na který odkazuje písmo, výplň, ohraničení, podmíněný formát, shape, komentář nebo mřížka, BuildBiffPalettePlan umístí exaktní barvy ve vzestupném pořadí ARGB nebo spustí až 16 kol frequency-weighted k-means v OKLab a ApplyBiffPalettePlan před zápisem zvaliduje generaci a FNV-1a hash
Plánování je jen pro čtení a opakovatelné, plán hlásí vlastní škody přes WeightedError a MaxDistanceSquared a zastaralý plán se odmítne s paletou nedotčenou, protože plány jsou v praxi na jedno použití
var
  Plan: TXLSBiffPalettePlan;
  I: Integer;
begin
  Plan := Workbook.BuildBiffPalettePlan;   // jen pro čtení
  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;

Jak odmítne ApplyBiffPalettePlan zastaralý plán?

ApplyBiffPalettePlan zvaliduje celý plán, než zapíše jediný slot, a vrátí False s paletou nedotčenou, pokud cokoli nesouhlasí s aktuálním sešitem. Plán nese SourcePaletteGeneration a SourcePaletteHash, 64bitový FNV-1a hash nad 56 zdrojovými barvami; validace navíc znovu zkontroluje každý veřejný i fyzický index, každou zdrojovou barvu, že žádný uzamčený slot není označený jako změněný, počty uzamčených a změněných slotů a to, že každý target je neprůhledný. Jakákoli efektivní změna palety mezitím, včetně dřívějšího úspěšného použití téhož plánu, plán zestaruje, takže plány jsou v praxi na jedno použití. Validní plán bez změněných slotů uspěje bez posunu generace a skutečná změna jednou povýší generaci a jednou přestaví OKLab matcher — na Classic engine přepsáním pevného pole palety a na XLSX engine vložením připraveného override seznamu indexovaných barev

Zapnutí pro ukládání BIFF8 a konverzi XLSX na XLS

Vlastnost BiffPaletteSavePolicy defaultně stojí na xbpsPreserve, takže upgrade HotXLS nikdy nikomu nepřepíše paletu za zády. Nastavení na xbpsOptimizeTrueColors donutí Classic sešit postavit a aplikovat čerstvý plán uvnitř SaveAs, ale jen když je cílový formát xlExcel97; BIFF5, CSV, HTML, PDF, XLSX a ostatní writery nastavení ignorují. Po úspěšném uložení zůstane optimalizovaná paleta v modelu sešitu, takže pozdější dotazy a ukládání vidí stejné mapování. Když uložení selže nebo se zruší, obnoví se původních 56 barev a původní generace. U XLSX zdrojů postaví SaveXLSXWorkbookAsXLS v lxXlsxExport jediný plán z načteného sešitu a zapíše ho do cílové palety, než se zkonvertuje jakýkoli styl — to je deterministický most, který procvičuje demo workbenche pro audit a konverzi sešitů. Barvy motivů projdou stejným plannerem, jakmile se jejich tint rozliší na RGB; pokud byste radši drželi motivy živé ve výplních grafů, článek o výplních grafů s barvami motivů GelFrame rozebírá, jak binární XLS ukládá index schématu místo zploštělé barvy

// Classic sešit: opt in, jen BIFF8
Workbook.BiffPaletteSavePolicy := xbpsOptimizeTrueColors;
if Workbook.SaveAs('report.xls', xlExcel97) <> 1 then
  HandleSaveFailure;   // paleta už je obnovená

// Model XLSX do BIFF8 s jedním deterministickým plánem palety
XWorkbook := TXLSXWorkbook.Create;
try
  if XWorkbook.Open('report.xlsx') = 1 then
    SaveXLSXWorkbookAsXLS(XWorkbook, 'report.xls');
finally
  XWorkbook.Free;
end;

Palette API HotXLS fungují stejně na IXLSWorkbook i TXLSXWorkbook, z Delphi i C++Builderu. Stáhněte si trial a namiřte ho na svou nejbarevnější tabulku ze stránky komponenty HotXLS pro Excel v Delphi