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 означають системний колір тексту й тла
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 і не претендує бути ним, але в нього немає кускових корекцій відтінку, він коштує кількох множень на колір і достатньо стабільний, щоб вести кластеризувальний цикл — а це якраз те місце, де він справді заробляє своє місце
Що гарантує 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, тож пакетна робота може відмовитися зберігати, коли наближення стає надто грубим для брендбуку
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