Техническа статия

BIFF8 палитра от 56 цвята: OKLab картографиране в HotXLS

HotXLS разпределя произволни RGB и theme цветове върху BIFF8 цветовата палитра с 56 слота на два слоя: NearestIndexedColor намира перцептивно най-близкия съществуващ запис в палитрата в OKLab пространството, а BuildBiffPalettePlan заедно с ApplyBiffPalettePlan пренаписва свободните слотове, така че true-color работна книга да оцелее при запис в класически XLS. Спусъкът е винаги един и същ support ticket. Някой прави отчет в XLSX с корпоративни тъмносини header-и и мек тюркоазен акцент, записва го като .xls за някоя стара система, а header-ите се връщат чисто черни, а тюркоазеното се превръща в крещящ бирюзов цвят. Нищо не се е сринало и никакво предупреждение не се е запалило. Цветовият модел на стария формат просто не може да побере онова, което новият описва, а библиотеката е трябвало да избере нещо

Защо XLS файл може да побере само 56 цвята?

Защото BIFF8 cell формат никога не съхранява RGB стойност: шрифтовете, fill-овете и рамките носят color index, а workbook-глобалният Palette record ($0092, [MS-XLS] §2.4.188) подава точно 56 непрозрачни RGB записа за индекси 8 до 63. Индекси 0 до 7 са фиксирани копия на осемте основни цвята, а стойностите над 63 изобщо не са цветове, а токени като system foreground, system background и chart text. HotXLS излага палитрата чрез публичен ColorIndex от 1 до 56, тоест физическият индекс минус 7, а ResolveIndexedColor държи трите схеми на номерация разделени чрез TXLSIndexedColorSpace: xicsPublicColorIndex за API стойностите 1..56, xicsBiffIcv за суровите on-disk индекси, валидирани спрямо подмножеството IcvFont, IcvXF или IcvChart според ролята, която подавате, и xicsOoxmlIndexed, където 64 и 65 значат system foreground и background

HotXLS държи трите индексни цветови схеми разделени чрез TXLSIndexedColorSpace: сурови BIFF icv стойности, 0 до 7 фиксирани към осемте основни цвята, 56-те слота на палитрата 8 до 63 от Palette record $0092, токени над 63 като system foreground, публичен ColorIndex 1 до 56 с отместване минус 7, и xicsOoxmlIndexed, където 64 и 65 значат system foreground и background
Един и същ color index значи различни числа във всяка схема, затова HotXLS прекарва всяка стойност през ResolveIndexedColor, вместо да позволи суров BIFF токен да се представя за публичен ColorIndex
var
  Res: TXLSIndexedColorResolution;
begin
  // $40 е BIFF icv токен, не слот в палитрата
  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 и игнорира Boolean връщаната стойност. ResolveIndexedColor връща True само когато е получил конкретен ARGB, а краткият overload изобщо не чете Windows десктопа, така че automatic или system токен законно се връща като False, докато продължава да е класифициран като xickSystem. HotXLS удари това в собствения си workbook serializer: код, който третира False като „няма цвят“, тихо изхвърля Automatic и System значението на токена. Ако ви трябват реални RGB стойности за тези токени, викайте дългия overload и подавайте TXLSTryResolveSystemColor callback, който прилага вашата собствена UI, export или headless политика

Защо HotXLS съпоставя цветове в OKLab вместо в RGB?

Защото sRGB каналните стойности са gamma-кодирани, така че евклидовото разстояние в RGB не следи това, което човек вижда, а грешката е най-голяма точно в тъмните, наситени тонове, които корпоративните палитри обичат. Вземете тъмносиния $000033. В RGB разстоянието до черното е 51, а разстоянието до navy записа по подразбиране $000080 е 77, така че RGB matcher уверено ви боядисва header-а в черно. В OKLab квадратичните разстояния са около 0.0312 до черното и 0.0235 до navy, и HotXLS избира navy, ColorIndex 11 на физически слот 18; точно този случай е закован в тестовия suite и за двата engine-а, Classic и XLSX. Конверсията вътре в ArgbToOklab линеаризира всеки sRGB канал, прилага OKLab LMS матрицата, вади кубични корени и проектира върху L, a и b, след което обикновено квадратично евклидово разстояние е разумен заместител на възприетата разлика. OKLab не е CIEDE2000 и не се преструва, че е, но няма piecewise hue корекции, струва шепа умножения на цвят и е достатъчно стабилен да задвижва clustering цикъл — точно там си изкарва мястото

Как HotXLS съпоставя тъмносиния $000033 върху палитрата: евклидово разстояние в gamma-кодиран RGB мери 51 до черното и 77 до navy и щеше да боядиса header-а в черно, докато ArgbToOklab квадратичните разстояния 0.0312 и 0.0235 позволяват на NearestIndexedColor да избере navy, ColorIndex 11 на физически слот 18
Gamma-кодирани канални стойности правят RGB разстоянието лош заместител на това, което човек вижда, затова HotXLS конвертира веднъж в OKLab и дава на обикновено квадратично евклидово сравнение да задвижва сканирането на палитрата

Какво гарантира NearestIndexedColor?

NearestIndexedColor гарантира детерминистичен, read-only отговор: една входна конверсия, едно фиксирано сканиране на 56 кеширани записа и най-ниският публичен индекс, когато два записа са еднакво близки. Всяка работна книга кешира нормализирания ARGB и OKLab координатите на всичките 56 физически слота заедно с брояч на palette generation. Reset на палитрата преизгражда кеша, промяна на един слот обновява само него, а заявка срещу остаряло поколение връща False вместо да гадае. Сканирането ползва строго „по-малко от“ сравнение, започвайки от слот 8, затова палитра, съдържаща един и същ цвят два пъти, винаги отговаря с по-ниския индекс; това има значение, когато diff-вате два генерирани файла и очаквате byte-identical изход. Входящият alpha следва тесен договор: нулев alpha байт се третира като непрозрачен, а частично прозрачна стойност се отхвърля с ColorIndex 0 и PaletteSlot -1, защото записите в палитрата нямат alpha. Fill и border writer-ите на Classic engine-а конвертират RGB и theme цветове в индекс със същата 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 цветовете в 56 слота?

BuildBiffPalettePlan изчислява пълно предложение за всичките 56 слота, без да пипа работната книга, така че можете да го инспектирате, да го логнете или да го изхвърлите. Planner-ът първо вика ScanIndexedColorUsage: всеки слот, който шрифт, fill, рамка, conditional format, shape, коментар или gridline на лист реферира по индекс, се заключва, защото промяната на запис в палитрата преоцветява наведнъж всеки консуматор на този индекс. Целите са директните RGB и разрешените theme цветове от шрифтове, fills, рамки, differential styles, data bars и color scales. Всяка цел тежи според по-голямото от броя си на рендерирани справки и броя си на дефиниции, а conditional format брои клетките, които диапазоните му покриват, така че цвят, размазан по цяла колона, тежи повече от ползван в една бележка. Разпределянето после върви в фиксиран ред:

  • Заключените слотове пазят своя source цвят безусловно
  • Цел, която вече съществува в палитрата, се запазва на най-ниския си съвпадащ слот, а този слот става фиксиран
  • Ако останалите уникални цели се побират в свободните слотове, всяка получава точен слот, разпределен във възходящ ARGB ред
  • Иначе Quantized се вдига, всеки свободен слот се сее с целта, чие разстояние до най-близкия ѝ съществуващ център, умножено по тежестта ѝ, е най-голямо, и до 16 рунда frequency-weighted k-means в OKLab местят само свободните центрове, докато разпределенията спрат да се менят

Бъдете честни със себе си какво доставя overflow пътят. Clustering-ът е ограничена локална оптимизация, не глобален оптимум, а свободен слот най-накрая държи центроид, конвертиран обратно в sRGB с clamping, което може да е цвят, който нито една клетка не е ползвала буквално. Това, което реално получавате, е повторяемост: същата работна книга винаги дава същия план, а планът докладва собствения си ущърб чрез WeightedError, MaxDistanceSquared, ExactTargetWeight и TotalTargetWeight, така че batch job може да откаже запис, когато апроксимацията стане твърде груба за brand guideline

Palette pipeline-ът на HotXLS за true-color работна книга: ScanIndexedColorUsage заключва всеки слот, който шрифт, fill, рамка, conditional format, shape, коментар или gridline реферира, BuildBiffPalettePlan разпределя точните цветове във възходящ ARGB ред или пуска до 16 рунда frequency-weighted k-means в OKLab, а ApplyBiffPalettePlan валидира поколението и FNV-1a hash преди запис
Планирането е read-only и повторяемо, планът докладва собствения си ущърб чрез WeightedError и MaxDistanceSquared, а остарял план се отхвърля с недокосната палитра, защото плановете са де факто за еднократна употреба
var
  Plan: TXLSBiffPalettePlan;
  I: Integer;
begin
  Plan := Workbook.BuildBiffPalettePlan;   // read-only
  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 hash върху 56-те source цвята; валидацията преизпитва също всеки публичен и физически индекс, всеки source цвят, че нито един заключен слот не е маркиран като променен, заключените и променените бройки, и че всяка цел е непрозрачна. Всяка ефективна промяна на палитрата междувременно, включително успешно приложението на същия план по-рано, прави плана остарял, затова плановете са де факто еднократни. Валиден план без променени слотове успява без да вдига поколението, а истинска промяна вдига поколението веднъж и преизгражда OKLab matcher-а веднъж — на Classic engine-а чрез пренаписване на фиксирания palette array, а на XLSX engine-а чрез сменяне с подготвен indexed-color override списък

Включване за BIFF8 записи и XLSX-to-XLS конверсия

Свойството BiffPaletteSavePolicy по подразбиране е xbpsPreserve, така че ъпгрейд на HotXLS никога не пренапва ничия палитра зад гърба ви. Задаването на xbpsOptimizeTrueColors кара Classic работна книга да изгради и приложи свеж план вътре в SaveAs, но само когато целевият формат е xlExcel97; BIFF5, CSV, HTML, PDF, XLSX и останалите writer-и игнорират настройката. След успешен запис оптимизираната палитра остава в модела на работната книга, така че по-късните заявки и записи виждат същото съответствие. Ако записът се провали или бъде отменен, оригиналните 56 цвята и оригиналното поколение се възстановяват. За XLSX източници SaveXLSXWorkbookAsXLS в lxXlsxExport строи един план от заредената работна книга и го записва в целевата палитра, преди да е конвертиран какъвто и да е style, което е детерминистичният мост, който демото на workbook audit и conversion workbench упражнява. Theme цветовете минават през същия planner, след като tint-ът им се разреши до RGB; ако предпочитате да пазите theme-овете живи в chart fills, статията за GelFrame theme color chart fills обяснява как binary XLS съхранява scheme индекс вместо изгладен цвят

// Classic работна книга: opt in, само BIFF8
Workbook.BiffPaletteSavePolicy := xbpsOptimizeTrueColors;
if Workbook.SaveAs('report.xls', xlExcel97) <> 1 then
  HandleSaveFailure;   // палитрата вече е възстановена

// XLSX модел към BIFF8 с един детерминистичен palette план
XWorkbook := TXLSXWorkbook.Create;
try
  if XWorkbook.Open('report.xlsx') = 1 then
    SaveXLSXWorkbookAsXLS(XWorkbook, 'report.xls');
finally
  XWorkbook.Free;
end;

Palette API-ите на HotXLS работят по същия начин на IXLSWorkbook и TXLSXWorkbook, както от Delphi, така и от C++Builder. Свалете trial-а и го насочете към най-цветната ви електронна таблица от страницата на HotXLS Delphi Excel компонента