Технічна стаття

Палітра BIFF8 на 56 кольорів: відображення OKLab у HotXLS

HotXLS розкладає довільні RGB і кольори тем на 56-слотову кольорову палітру BIFF8 у два шари: NearestIndexedColor знаходить перцептивно найближчий наявний запис палітри в просторі OKLab, а BuildBiffPalettePlan разом із ApplyBiffPalettePlan переписує вільні слоти палітри, тож повнокольорова книга переживає збереження в класичний XLS. Тригер завжди один і той самий — тикет підтримки. Хтось збирає звіт у XLSX із фірмовими темно-синіми заголовками й м'яким бірюзовим акцентом, зберігає його як .xls для застарілого споживача, і заголовки повертаються чисто чорними, а бірюза перетворюється на кричущу туркуазу. Нічого не впало й жодного попередження не пролунало. Колірна модель старого формату просто не вміє тримати те, що описав новий, а бібліотека мусила щось обрати

Чому файл XLS здатен тримати лише 56 кольорів?

Бо клітинковий формат BIFF8 ніколи не зберігає значення RGB: шрифти, заливки й рамки несуть колірний індекс, а глобальний для книги запис Palette ($0092, [MS-XLS] §2.4.188) постачає рівно 56 непрозорих RGB-записів для індексів з 8 по 63. Індекси з 0 по 7 — фіксовані копії восьми базових кольорів, а значення понад 63 — це взагалі не кольори, а токени на кшталт системного кольору тексту, системного тла й тексту діаграм. HotXLS віддає палітру через публічний ColorIndex від 1 до 56 — це фізичний індекс мінус 7, — а ResolveIndexedColor тримає три схеми нумерації порізно через TXLSIndexedColorSpace: xicsPublicColorIndex для значень API 1..56, xicsBiffIcv для сирих дискових індексів, які звіряються з підмножиною IcvFont, IcvXF чи IcvChart залежно від ролі, яку ви передали, і xicsOoxmlIndexed, де 64 і 65 означають системний колір тексту й тла

HotXLS тримає три схеми індексованих кольорів порізно через TXLSIndexedColorSpace: сирі значення icv BIFF, від 0 до 7 фіксовані за вісьмома базовими кольорами, 56 слотів палітри від 8 до 63 запису Palette $0092, токени понад 63 на кшталт системного кольору тексту, публічний ColorIndex від 1 до 56 зі зсувом мінус 7 і xicsOoxmlIndexed, де 64 і 65 означають системні колір тексту й тло
Той самий колірний індекс означає різні числа в кожній схемі, тож HotXLS пропускає кожне значення через ResolveIndexedColor, а не дозволяє сирому токену BIFF видавати себе за публічний ColorIndex
var
  Res: TXLSIndexedColorResolution;
begin
  // $40 — це токен icv BIFF, а не слот палітри
  Workbook.ResolveIndexedColor($40, xicsBiffIcv, Res);
  case Res.Kind of
    xickPalette:   UseArgb(Res.ARGB);   // слот палітри, якщо розв'язано
    xickAutomatic,
    xickSystem:    UseSystemColor(Res.SystemColorRole);
    xickInvalid:   RejectToken(Res.RawIndex);
  end;
end;

Зверніть увагу: приклад гілкується за Res.Kind і ігнорує булеве значення, що повертається. ResolveIndexedColor повертає True лише тоді, коли дістав конкретний ARGB, а коротке перевантаження ніколи не читає робочий стіл Windows, тож автоматичний чи системний токен цілком легітимно повертає False, залишаючись класифікованим як xickSystem. HotXLS наступив на це у власному серіалізаторі книги: код, який трактує False як «кольору немає», мовчки викидає значення Automatic і System, закладені в токені. Якщо вам потрібні справжні значення RGB для цих токенів, викличте довге перевантаження й передайте callback TXLSTryResolveSystemColor, який застосовує вашу власну політику UI, експорту чи headless-режиму

Чому HotXLS підбирає кольори в OKLab, а не в RGB?

Бо значення каналів sRGB гамма-кодовані, тож евклідова відстань у RGB не відслідковує те, що бачить людина, і похибка найбільша саме в темних, насичених тонах, які так люблять фірмові палітри. Візьміть темно-синій $000033. У RGB відстань до чорного — 51, а до типового запису темно-синього $000080 — 77, тож RGB-підбирач упевнено фарбує ваш заголовок чорним. В OKLab квадратичні відстані — приблизно 0.0312 до чорного і 0.0235 до темно-синього, і HotXLS обирає темно-синій, ColorIndex 11 на фізичному слоті 18; цей точний випадок пришпилений в тестовому наборі для обох рушіїв, Classic і XLSX. Перетворення всередині ArgbToOklab лінеаризує кожен канал sRGB, застосовує матрицю LMS OKLab, бере кубічні корені й проєктує на L, a і b, після чого звичайна квадратична евклідова відстань — розумна заміна сприйманої різниці. OKLab — це не CIEDE2000 і не претендує бути ним, але в нього немає кускових корекцій відтінку, він коштує кількох множень на колір і достатньо стабільний, щоб вести кластеризувальний цикл — а це якраз те місце, де він справді заробляє своє місце

Як HotXLS відображає темно-синій $000033 на палітру: евклідова відстань у гамма-кодованому RGB дає 51 до чорного і 77 до темно-синього й залила б заголовок чорним, тоді як квадратичні відстані ArgbToOklab 0.0312 і 0.0235 дозволяють NearestIndexedColor обрати темно-синій, ColorIndex 11 на фізичному слоті 18
Гамма-кодовані значення каналів роблять відстань у RGB поганою заміною того, що бачить людина, тож HotXLS один раз перетворює колір на OKLab і дає звичайному квадратичному евклідовому порівнянню вести скан палітри

Що гарантує NearestIndexedColor?

NearestIndexedColor гарантує детерміновану відповідь без запису: одне вхідне перетворення, один фіксований скан по 56 кешованих записах і найменший публічний індекс щоразу, коли два записи однаково близькі. Кожна книга кешує нормалізований ARGB і координати OKLab усіх 56 фізичних слотів разом із лічильником покоління палітри. Скид палітри перебудовує кеш, зміна одного слота оновлює лише його, а запит проти застарілого покоління повертає False замість вгадування. Скан використовує строгу нерівність, починаючи зі слота 8, — саме тому палітра, що містить той самий колір двічі, завжди відповідає меншим індексом; це важливо, коли ви робите diff двох згенерованих файлів і очікуєте побайтово ідентичний вихід. Вхідна альфа підпорядковується вузькому контракту: нульовий байт альфи трактується як непрозорість, а частково прозоре значення відхиляється з ColorIndex 0 і PaletteSlot -1, бо записи палітри не мають альфи. Заливки й рамки рушія Classic конвертують RGB і кольори тем в індекс тією самою рутиною OKLab-підбору в момент збереження, тож API і збережений файл погоджуються, на який слот приземлиться колір

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;

Як BuildBiffPalettePlan влаштовує true color у 56 слотів?

BuildBiffPalettePlan обчислює повну пропозицію для всіх 56 слотів, не торкаючись книги, тож її можна роздивитися, занотувати чи викинути. Планувальник спершу викликає ScanIndexedColorUsage: кожен слот, який шрифт, заливка, рамка, умовне форматування, фігура, коментар чи сітка аркуша посилається за індексом, блокується, бо зміна запису палітри перекрашує всіх споживачів цього індексу разом. Цілями є прямі RGB і розв'язані кольори тем із шрифтів, заливок, рамок, диференціальних стилів, гістограм і колірних шкал. Кожну ціль зважено за більшим із кількості посилань у рендері та кількості визначень, а умовне форматування враховує кількість клітинок, які покривають його діапазони, тож колір, розмальований по цілої колонки, важить більше за колір однієї примітки. Розміщення далі йде у фіксованому порядку:

  • Заблоковані слоти безумовно тримають свій вихідний колір
  • Ціль, яка вже є в палітрі, залишається на найменшому відповідному їй слоті, і цей слот стає фіксованим
  • Якщо решта унікальних цілей вміщується у вільні слоти, кожна отримує точний слот, призначений у порядку зростання ARGB
  • Інакше виставляється Quantized, кожен вільний слот засівається ціллю, в якої відстань до найближчого наявного центру, помножена на вагу, найбільша, і до 16 раундів частотно-зваженого k-means в OKLab рухають лише вільні центри, доки призначення не перестануть змінюватися

Будьте чесні з собою щодо того, що дає шлях переповнення. Кластеризація — обмежена локальна оптимізація, а не глобальний оптимум, і вільний слот у підсумку тримає центроїд, конвертований назад у sRGB із обтисненням до меж, тобто колір, який жодна клітинка могла й не використовувати дослівно. Але ви отримуєте повторюваність: та сама книга завжди дає той самий план, а план сам звітує про свою шкоду через WeightedError, MaxDistanceSquared, ExactTargetWeight і TotalTargetWeight, тож пакетна робота може відмовитися зберігати, коли наближення стає надто грубим для брендбуку

Конвеєр палітри HotXLS для повнокольорової книги: ScanIndexedColorUsage блокує кожен слот, на який посилається шрифт, заливка, рамка, умовне форматування, фігура, коментар чи сітка, BuildBiffPalettePlan розставляє точні кольори в порядку зростання ARGB або запускає до 16 раундів частотно-зваженого k-means в OKLab, а ApplyBiffPalettePlan звіряє покоління й хеш FNV-1a перед записом
Планування лише читає і повторюване, план сам звітує про свою шкоду через WeightedError і MaxDistanceSquared, а застарілий план відхиляється з недоторканою палітрою, бо плани фактично одноразові
var
  Plan: TXLSBiffPalettePlan;
  I: Integer;
begin
  Plan := Workbook.BuildBiffPalettePlan;   // лише читання
  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;

Як ApplyBiffPalettePlan відхиляє застарілий план?

ApplyBiffPalettePlan звіряє весь план, перш ніж записати хоч один слот, і повертає False з недоторканою палітрою, якщо хоч щось розходиться з поточною книгою. План несе SourcePaletteGeneration і SourcePaletteHash — 64-бітний хеш FNV-1a по 56 вихідних кольорах; валідація також перевіряє кожен публічний і фізичний індекс, кожен вихідний колір, те, що жоден заблокований слот не позначений зміненим, лічильники заблокованих і змінених слотів і те, що кожна ціль непрозора. Будь-яка фактична зміна палітри між цими кроками, включно з успішним ранішим застосуванням того самого плану, робить план застарілим, тож плани фактично одноразові. Валідний план без змінених слотів завершується успіхом без просування покоління, а справжня зміна один раз піднімає покоління й один раз перебудовує OKLab-збігач — на рушії Classic переписуванням фіксованого масиву палітри, а на рушії XLSX підстановкою заготовленого списку перевизначень індексованих кольорів

Увімкнення для збережень BIFF8 і конвертації XLSX у XLS

Властивість BiffPaletteSavePolicy типово стоїть у xbpsPreserve, тож оновлення HotXLS ніколи не переписує чиєюсь палітру за спиною. Перемикання на xbpsOptimizeTrueColors змушує книгу Classic будувати й застосовувати свіжий план усередині SaveAs, але лише коли цільовий формат — xlExcel97; письменники BIFF5, CSV, HTML, PDF, XLSX та інші це налаштування ігнорують. Після успішного збереження оптимізована палітра лишається в моделі книги, тож пізніші запити й збереження бачать те саме відображення. Якщо збереження зазнає невдачі чи скасоване, початкові 56 кольорів і початкове покоління відновлюються. Для джерел XLSX SaveXLSXWorkbookAsXLS у lxXlsxExport будує один план із завантаженої книги і записує його в палітру призначення до конвертації будь-якого стилю — це той детермінований місток, який вправляє демо робочого місця аудиту й конвертації книг. Кольори тем проходять крізь той самий планувальник після того, як їхній тінт розв'язано в RGB; якщо хочете лишити теми живими у заливках діаграм, стаття про theme color у заливках GelFrame пояснює, як бінарний XLS зберігає індекс схеми замість сплющеного кольору

// Книга Classic: увімкнути вручну, лише BIFF8
Workbook.BiffPaletteSavePolicy := xbpsOptimizeTrueColors;
if Workbook.SaveAs('report.xls', xlExcel97) <> 1 then
  HandleSaveFailure;   // палітру вже відновлено

// Модель XLSX у BIFF8 з одним детермінованим планом палітри
XWorkbook := TXLSXWorkbook.Create;
try
  if XWorkbook.Open('report.xlsx') = 1 then
    SaveXLSXWorkbookAsXLS(XWorkbook, 'report.xls');
finally
  XWorkbook.Free;
end;

API палітри HotXLS працюють однаково на IXLSWorkbook і TXLSXWorkbook, що з Delphi, що з C++Builder. Завантажте trial і наведіть його на найбарвистішу вашу таблицю зі сторінки компонента HotXLS Delphi Excel