مقاله فنی

پالت 56 رنگی BIFF8: نگاشت رنگ OKLab در HotXLS

HotXLS رنگ‌های دلخواه RGB و theme را در دو لایه روی پالت 56 رنگی BIFF8 نگاشت می‌کند: NearestIndexedColor نزدیک‌ترین مدخل موجود پالت را از نظر ادراکی در فضای OKLab پیدا می‌کند، و BuildBiffPalettePlan همراه ApplyBiffPalettePlan اسلات‌های آزاد پالت را بازنویسی می‌کند تا یک ورک‌بوک true-color از ذخیره به XLS کلاسیک سالم بیرون بیاید. محرک همیشه همان تیکت پشتیبانی است. کسی گزارشی در XLSX با هدرهای سرمه‌ای سازمانی و یک اکسنت فیروزه‌ای ملایم می‌سازد، برای یک consumer قدیمی آن را .xls ذخیره می‌کند، و هدرها یکسره سیاه برمی‌گردند و فیروزه‌ای به یک فیروزه‌ای پرسروصدا تبدیل می‌شود. هیچ‌چیز کرش نکرد و هیچ هشداری روشن نشد. مدل رنگ فرمت قدیمی به‌سادگی نمی‌تواند چیزی را که فرمت جدید توصیف کرده حمل کند، و کتابخانه ناچار بود چیزی انتخاب کند

چرا یک فایل XLS فقط 56 رنگ جا می‌دهد؟

چون یک فرمت سلول BIFF8 هرگز مقدار RGB ذخیره نمی‌کند: فونت‌ها و fillها و borderها یک color index حمل می‌کنند، و رکورد Palette سراسریِ ورک‌بوک ($0092، [MS-XLS] §2.4.188) دقیقاً 56 مدخل RGB مات برای اندیس‌های 8 تا 63 می‌دهد. اندیس‌های 0 تا 7 نسخه‌های ثابت هشت رنگ پایه‌اند، و مقادیر بالای 63 اصلاً رنگ نیستند بلکه توکن‌هایی مثل system foreground و system background و متن chart هستند. HotXLS پالت را از طریق یک ColorIndex عمومی از 1 تا 56 عرضه می‌کند که همان اندیس فیزیکی منهای 7 است، و ResolveIndexedColor سه طرح شماره‌گذاری را با TXLSIndexedColorSpace از هم جدا نگه می‌دارد: xicsPublicColorIndex برای مقادیر API در بازهٔ 1..56، xicsBiffIcv برای اندیس‌های خام روی دیسک که برابر زیرمجموعهٔ IcvFont یا IcvXF یا IcvChart مربوط به نقشی که می‌دهید اعتبارسنجی می‌شوند، و xicsOoxmlIndexed که در آن 64 و 65 یعنی system foreground و system background

HotXLS سه طرح رنگ اندیسی را با TXLSIndexedColorSpace از هم جدا نگه می‌دارد: مقادیر icv خام BIFF، 0 تا 7 ثابت روی هشت رنگ پایه، 56 اسلات پالت 8 تا 63 مربوط به رکورد Palette با $0092، توکن‌های بالای 63 مثل system foreground، ColorIndex عمومی 1 تا 56 با آفست منفی 7، و xicsOoxmlIndexed که در آن 64 و 65 یعنی system foreground و system background
یک color index یکسان در هر طرح عدد متفاوتی معنی می‌دهد، پس HotXLS هر مقدار را از ResolveIndexedColor عبور می‌دهد و نمی‌گذارد یک توکن خام BIFF خودش را ColorIndex عمومی جا بزند
var
  Res: TXLSIndexedColorResolution;
begin
  // $40 یک توکن icv در BIFF است، نه اسلات پالت
  Workbook.ResolveIndexedColor($40, xicsBiffIcv, Res);
  case Res.Kind of
    xickPalette:   UseArgb(Res.ARGB);   // اسلات پالت، اگر resolve شده باشد
    xickAutomatic,
    xickSystem:    UseSystemColor(Res.SystemColorRole);
    xickInvalid:   RejectToken(Res.RawIndex);
  end;
end;

دقت کنید که مثال روی Res.Kind سوییچ می‌کند و مقدار بازگشتی Boolean را نادیده می‌گیرد. ResolveIndexedColor فقط وقتی True برمی‌گرداند که یک ARGB مشخص به دست آورده باشد، و overload کوتاه هرگز دسکتاپ ویندوز را نمی‌خواند، پس یک توکن automatic یا system کاملاً مشروع با False برمی‌گردد در حالی که هنوز xickSystem طبقه‌بندی می‌شود. HotXLS خودش در serializer ورک‌بوکش به این برخورد: کدی که False را «بدون رنگ» می‌گیرد معنای Automatic و System توکن را بی‌سروصدا دور می‌ریزد. اگر برای آن توکن‌ها مقدار RGB واقعی لازم دارید، overload بلند را صدا بزنید و یک callback از نوع TXLSTryResolveSystemColor بدهید که سیاست UI یا export یا headless خودتان را اعمال کند

چرا HotXLS به‌جای RGB رنگ‌ها را در OKLab تطبیق می‌دهد؟

چون مقادیر کانال 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 نیست و ادعایش را هم ندارد، اما هیچ تصحیح piecewise روی hue ندارد، برای هر رنگ یک مشت عمل ضرب هزینه دارد، و آن‌قدر پایدار است که بتواند یک حلقهٔ clustering را هدایت کند، و همین‌جاست که واقعاً حقش را می‌گیرد

HotXLS چطور آبی تیرهٔ $000033 را روی پالت می‌نشاند: فاصلهٔ اقلیدسی در RGB گاما-کدگذاری‌شده تا سیاه 51 و تا سرمه‌ای 77 اندازه می‌گیرد و هدر را سیاه می‌کند، اما فاصله‌های مربعی ArgbToOklab یعنی 0.0312 و 0.0235 می‌گذارند NearestIndexedColor سرمه‌ای را بردارد، ColorIndex 11 در اسلات فیزیکی 18
مقادیر کانال گاما-کدگذاری‌شده فاصلهٔ RGB را تقریب بدی برای آنچه آدم می‌بیند می‌کنند، پس HotXLS یک‌بار به OKLab تبدیل می‌کند و مقایسهٔ اقلیدسی مربعی ساده اسکن پالت را هدایت می‌کند

NearestIndexedColor چه تضمینی می‌دهد؟

NearestIndexedColor یک جواب قطعی و فقط‌خواندنی تضمین می‌کند: یک تبدیل ورودی، یک اسکن ثابت روی 56 مدخل کش‌شده، و همیشه کمترین اندیس عمومی وقتی دو مدخل به یک اندازه نزدیک‌اند. هر ورک‌بوک ARGB نرمال‌شده و مختصات OKLab هر 56 اسلات فیزیکی را همراه یک شمارندهٔ generation پالت کش می‌کند. یک reset پالت کش را از نو می‌سازد، تغییر یک اسلات تکی فقط همان اسلات را به‌روز می‌کند، و کوئری روی یک generation کهنه به‌جای حدس زدن False برمی‌گرداند. اسکن با مقایسهٔ اکید کمتر-از از اسلات 8 شروع می‌شود، و دلیلش این است که پالتی که یک رنگ را دوبار داشته باشد همیشه با اندیس پایین‌تر جواب می‌دهد؛ وقتی دو فایل تولیدشده را diff می‌کنید و خروجی byte-identical انتظار دارید این مهم می‌شود. آلفای ورودی قراردادی باریک دارد: بایت آلفای صفر مات فرض می‌شود، و مقدار نیمه‌شفاف با ColorIndex 0 و PaletteSlot -1 رد می‌شود، چون مدخل‌های پالت آلفا ندارند. نویسنده‌های fill و border موتور Classic موقع ذخیره، رنگ‌های 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 یا border یا قالب‌بندی شرطی یا شکل یا کامنت یا خطوط شبکهٔ کاربرگ با اندیس به آن ارجاع می‌دهد قفل می‌شود، چون عوض کردن یک مدخل پالت یک‌جا همهٔ مصرف‌کنندگان آن اندیس را رنگ می‌کند. targetها رنگ‌های مستقیم RGB و رنگ‌های resolve‌شدهٔ theme از فونت‌ها و fillها و borderها و differential styleها و data barها و color scaleها هستند. هر target با بزرگ‌ترِ تعداد ارجاع رندرشده و تعداد تعریفش وزن می‌گیرد، و یک قالب‌بندی شرطی سلول‌هایی را می‌شمارد که rangeهایش پوشش می‌دهند، پس رنگی که یک ستون کامل را گرفته از رنگی که در یک کامنت تکی استفاده شده سنگین‌تر است. بعد جابه‌جایی در ترتیب ثابتی پیش می‌رود:

  • اسلات‌های قفل‌شده رنگ مبدأشان را بدون قید و شرط نگه می‌دارند
  • targetی که از قبل در پالت وجود دارد در کمترین اسلات منطبقش نگه داشته می‌شود و آن اسلات ثابت می‌شود
  • اگر targetهای یکتای باقی‌مانده در اسلات‌های آزاد جا شوند، هر یک اسلات دقیقی می‌گیرد و به ترتیب صعودی ARGB تخصیص داده می‌شود
  • وگرنه Quantized ست می‌شود، هر اسلات آزاد با targetی seed می‌شود که فاصله‌اش تا نزدیک‌ترین مرکز موجود ضربدر وزنش بزرگ‌ترین است، و تا 16 دور k-means وزن‌دار با فرکانس در OKLab فقط مراکز آزاد را جابه‌جا می‌کند تا تخصیص‌ها دیگر عوض نشوند

با خودتان روراست باشید که مسیر overflow چه چیزی تحویل می‌دهد. clustering یک بهینه‌سازی محلی کران‌دار است، نه بهینهٔ سراسری، و یک اسلات آزاد سرانجام centroidی را نگه می‌دارد که با clamp به sRGB برگشته، و ممکن است رنگی باشد که هیچ سلولی عیناً استفاده نکرده. چیزی که واقعاً می‌گیرید تکرارپذیری است: همان ورک‌بوک همیشه همان plan را می‌دهد، و plan خرابی خودش را از طریق WeightedError و MaxDistanceSquared و ExactTargetWeight و TotalTargetWeight گزارش می‌کند، پس یک batch job می‌تواند وقتی تقریب برای یک brand guideline بیش از حد درشت شد از ذخیره سر باز زند

پایپ‌لاین پالت HotXLS برای یک ورک‌بوک true-color: ScanIndexedColorUsage هر اسلاتی را که فونت یا fill یا border یا قالب‌بندی شرطی یا شکل یا کامنت یا خط شبکه ارجاع می‌دهد قفل می‌کند، BuildBiffPalettePlan رنگ‌های دقیق را به ترتیب صعودی ARGB می‌نشاند یا تا 16 دور k-means وزن‌دار با فرکانس در OKLab اجرا می‌کند، و ApplyBiffPalettePlan قبل از نوشتن generation و هش FNV-1a را اعتبارسنجی می‌کند
plan کردن فقط‌خواندنی و تکرارپذیر است، plan خرابی خودش را از طریق WeightedError و MaxDistanceSquared گزارش می‌کند، و plan کهنه با پالت دست‌نخورده رد می‌شود چون planها عملاً یک‌بارمصرف‌اند
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 چطور یک plan کهنه را رد می‌کند؟

ApplyBiffPalettePlan قبل از نوشتن حتی یک اسلات کل plan را اعتبارسنجی می‌کند، و اگر هر چیزی با ورک‌بوک فعلی ناسازگار باشد False برمی‌گرداند و پالت دست‌نخورده می‌ماند. plan با خودش SourcePaletteGeneration و SourcePaletteHash را حمل می‌کند، یک هش 64 بیتی FNV-1a روی 56 رنگ مبدأ؛ اعتبارسنجی همچنین هر اندیس عمومی و فیزیکی، هر رنگ مبدأ، اینکه هیچ اسلات قفل‌شده‌ای Changed علامت نخورده، شمارنده‌های locked و changed، و اینکه هر target مات است را دوباره چک می‌کند. هر تغییر مؤثر پالت در فاصله، از جمله یک اعمال موفق قبلیِ همان plan، plan را کهنه می‌کند، پس planها عملاً یک‌بارمصرف‌اند. یک plan معتبر بدون اسلات تغییرکرده بدون جلو بردن generation موفق می‌شود، و یک تغییر واقعی یک‌بار generation را بالا می‌برد و matcher مربوط به OKLab را یک‌بار از نو می‌سازد؛ در موتور Classic با بازنویسی آرایهٔ ثابت پالت و در موتور XLSX با جا زدن یک لیست override رنگ اندیسی آماده

روشن کردنش برای ذخیره‌های BIFF8 و تبدیل XLSX به XLS

ویژگی BiffPaletteSavePolicy به‌صورت پیش‌فرض xbpsPreserve است، پس آپگرید کردن HotXLS هرگز پالت هیچ‌کس را پشت سرش بازنویسی نمی‌کند. ست کردنش روی xbpsOptimizeTrueColors باعث می‌شود یک ورک‌بوک Classic داخل SaveAs یک plan تازه بسازد و اعمال کند، اما فقط وقتی فرمت مقصد xlExcel97 باشد؛ نویسنده‌های BIFF5 و CSV و HTML و PDF و XLSX و بقیه این تنظیم را نادیده می‌گیرند. بعد از یک ذخیرهٔ موفق پالت بهینه‌شده در مدل ورک‌بوک می‌ماند، پس کوئری‌ها و ذخیره‌های بعدی همان نگاشت را می‌بینند. اگر ذخیره شکست بخورد یا لغو شود، 56 رنگ اصلی و generation اصلی برمی‌گردند. برای مبدأهای XLSX، SaveXLSXWorkbookAsXLS در lxXlsxExport از ورک‌بوک لودشده یک plan می‌سازد و قبل از تبدیل هر style آن را در پالت مقصد می‌نویسد، که همان پل قطعی است که دموی workbench ممیزی و تبدیل ورک‌بوک تمرینش می‌کند. رنگ‌های theme بعد از اینکه tintشان به RGB resolve شد از همان planner عبور می‌کنند؛ اگر ترجیح می‌دهید themeها در fillهای chart زنده بمانند، مقالهٔ GelFrame theme color chart fills توضیح می‌دهد XLS باینری به‌جای رنگ تخت‌شده یک اندیس scheme ذخیره می‌کند

// ورک‌بوک Classic: opt-in کنید، فقط BIFF8
Workbook.BiffPaletteSavePolicy := xbpsOptimizeTrueColors;
if Workbook.SaveAs('report.xls', xlExcel97) <> 1 then
  HandleSaveFailure;   // پالت از قبل برگشته

// مدل XLSX به BIFF8 با یک plan پالت قطعی
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 را دانلود کنید و آن را روی رنگی‌ترین spreadsheet زندگی‌تان امتحان کنید، از طریق صفحهٔ کامپوننت Excel Delphi HotXLS