מאמר טכני

פלטת 56 הצבעים של BIFF8: מיפוי צבעים ב-OKLab ב-HotXLS

‏HotXLS ממפה צבעי RGB ו-theme שרירותיים על פלטת הצבעים של BIFF8 בשתי שכבות: NearestIndexedColor מוצא את ערך הפלטה הקיים הקרוב ביותר תפיסתית במרחב OKLab, ו-BuildBiffPalettePlan יחד עם ApplyBiffPalettePlan כותבים מחדש את משבצות הפלטה הפנויות כדי שחוברת true color תשרוד שמירה ל-XLS הקלאסי. הטריגר הוא תמיד אותה פניית תמיכה. מישהו בונה דוח ב-XLSX עם כותרות בכחול קמע של המותג והדגשה טורקיז רכה, שומר אותו כ-.xls לצרכן ישן, והכותרות חוזרות שחורות לגמרי בזמן שהטורקיז הופך לטורקיז צורחני. שום דבר לא קרס ואף אזהרה לא נרשמה. מודל הצבעים של הפורמט הישן פשוט לא מסוגל להחזיק את מה שהחדש תיאר, ולספרייה לא הייתה ברירה אלא לבחור משהו

למה קובץ XLS יכול להחזיק רק 56 צבעים?

כי פורמט תא של BIFF8 לעולם לא מאחסן ערך RGB: גופנים, מילויים ומסגרות נושאים אינדקס צבע, ורשומת ה-Palette הגלובלית של החוברת ($0092, [MS-XLS] §2.4.188) מספקת בדיוק 56 ערכי RGB אטומים עבור האינדקסים 8 עד 63. האינדקסים 0 עד 7 הם עותקים קבועים של שמונת הצבעים הבסיסיים, והערכים מעל 63 בכלל לא צבעים אלא טוקנים כמו system foreground, system background וטקסט גרף. 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
אותו אינדקס צבע משמעותו מספרים שונים בכל סכמה, ולכן 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 קונקרטי, וה-overload הקצר לעולם לא קורא את שולחן העבודה של Windows, כך שטוקן automatic או system חוזר False לגיטימית בזמן שהוא עדיין מסווג כ-xickSystem. HotXLS נתקל בזה בסריאליזר החוברות של עצמו: קוד שמתייחס ל-False כ"אין צבע" משליך בשקט את המשמעות של Automatic ו-System של הטוקן. אם אתה צריך ערכי RGB אמיתיים לטוקנים האלה, קרא ל-overload הארוך וספק callback מסוג TXLSTryResolveSystemColor שמיישם את מדיניות ה-UI, הייצוא או ה-headless שלך

למה HotXLS מתאים צבעים ב-OKLab במקום ב-RGB?

כי ערכי הערוצים של sRGB מקודדים בגאמה, כך שמרחק אוקלידי ב-RGB לא עוקב אחרי מה שעין אנושית רואה, והשגיאה הכי גרועה בדיוק בגוונים הכהים והרוויים שפלטות המותג אוהבות. קח את הכחול הכהה $000033. ב-RGB המרחק לשחור הוא 51 והמרחק לערך ה-navy המוגדר כברירת מחדל $000080 הוא 77, כך שמתאים RGB מצייר לך את הכותרת בשחור בביטחון מלא. ב-OKLab המרחקים בריבוע הם כ-0.0312 לשחור וכ-0.0235 ל-navy, ו-HotXLS בוחר navy, ColorIndex 11 במשבצת הפיזית 18; המקרה המדויק הזה נעול בסוויטת הבדיקות גם עבור מנוע ה-Classic וגם עבור מנוע ה-XLSX. ההמרה בתוך ArgbToOklab מעבירה כל ערוץ sRGB לייצוג ליניארי, מפעילה את מטריצת ה-LMS של OKLab, מוציאה שורש קובי ומקרינה על L, a ו-b, ואז מרחק אוקלידי ריבועי פשוט הוא פרוקסי סביר להבדל הנתפס. OKLab הוא לא CIEDE2000 ולא מעמיד פנים שהוא כזה, אבל אין לו תיקוני גוון מקוטעים, הוא עולה כמה כפלים בודדים לכל צבע, והוא יציב מספיק כדי להניע לולאת clustering — ושם הוא באמת מרוויח את מקומו

איך HotXLS ממפה את הכחול הכהה $000033 על הפלטה: מרחק אוקלידי ב-RGB מקודד גאמה נמדד 51 לשחור ו-77 ל-navy והיה צובע את הכותרת בשחור, בזמן שמרחקים בריבוע של ArgbToOklab, 0.0312 ו-0.0235, מאפשרים ל-NearestIndexedColor לבחור navy, ColorIndex 11 במשבצת הפיזית 18
ערכי ערוצים מקודדי גאמה הופכים את מרחק ה-RGB לפרוקסי גרוע למה שעין אנושית רואה, ולכן HotXLS ממיר פעם אחת ל-OKLab ונותן להשוואה אוקלידית ריבועית פשוטה להניע את סריקת הפלטה

מה NearestIndexedColor באמת מבטיח?

‏NearestIndexedColor מבטיח תשובה דטרמיניסטית ולקריאה בלבד: המרת קלט אחת, סריקה קבועה אחת על פני 56 ערכים במטמון, והאינדקס הציבורי הנמוך ביותר בכל פעם ששני ערכים קרובים במידה שווה. כל חוברת שומרת במטמון את ה-ARGB המנורמל ואת קואורדינטות ה-OKLab של כל 56 המשבצות הפיזיות יחד עם מונה דור של הפלטה. איפוס פלטה בונה את המטמון מחדש, שינוי במשבצת בודדת מעדכן רק אותה, ושאילתה מול דור מיושן מחזירה False במקום לנחש. הסריקה משתמשת בהשוואת קטן-ממש החל ממשבצת 8, ולכן פלטה שמכילה את אותו צבע פעמיים תמיד תענה באינדקס הנמוך; זה משנה כשאתה עושה diff בין שני קבצים שנוצרו ומצפה לפלט זהה בייט. האלפא בקלט כפוף לחוזה צר: בייט אלפא אפס מתייחס כאטום, וערך חצי-שקוף נפסל עם ColorIndex 0 ו-PaletteSlot -1, כי לערכי פלטה אין אלפא. כותבי המילוי והמסגרות של מנוע ה-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 colors ל-56 משבצות?

‏BuildBiffPalettePlan מחשב הצעה מלאה לכל 56 המשבצות בלי לגעת בחוברת, כך שאפשר לבחון אותה, לרשום אותה ליומן או להשליך אותה. המתכנן קורא קודם ל-ScanIndexedColorUsage: כל משבצת שגופן, מילוי, מסגרת, עיצוב מותנה, צורה, הערה או קווי רשת של גיליון מפנים אליה לפי אינדקס ננעלת, כי שינוי של ערך פלטה מצבע מחדש בבת אחת את כל הצרכנים של אותו אינדקס. היעדים הם צבעי RGB ישירים וצבעי theme שנפתרו מגופנים, מילויים, מסגרות, סגנונות דיפרנציאליים, data bars ו-color scales. כל יעד מקבל משקל לפי הגדול מבין מספר ההפניות שלו ברינדור ומספר ההגדרות שלו, ועיצוב מותנה סופר את התאים שהטווחים שלו מכסים, כך שצבע שנמשח על פני עמודה שלמה גובר על צבע שנוצל בהערה אחת. ההצבה עצמה מתקדמת בסדר קבוע:

  • משבצות נעולות שומרות על צבע המקור שלהן ללא תנאי
  • יעד שכבר קיים בפלטה נשאר במשבצת הנמוכה ביותר שמתאימה לו, והמשבצת הזו ננעלת
  • אם היעדים הייחודיים שנותרו נכנסים למשבצות הפנויות, כל אחד מקבל משבצת מדויקת, בהקצאה בסדר ARGB עולה
  • אחרת מוגדר Quantized, כל משבצת פנויה מאותחלת עם היעד שהמרחק שלו למרכז הקיים הקרוב אליו, כפול המשקל שלו, הוא הגדול ביותר, ועד 16 סבבים של k-means משוקלל תדירות ב-OKLab מזיזים רק את המרכזים הפנויים עד שההקצאות מפסיקות להשתנות

היה כן עם עצמך לגבי מה שנתיב ההצפה באמת נותן. ה-clustering הוא אופטימיזציה מקומית חסומה, לא אופטימום גלובלי, ומשבצת פנויה נגמרת עם צנטרואיד שהומר חזרה ל-sRGB עם clamping, שיכול להיות צבע שאף תא לא השתמש בו ממילא. מה שכן מקבלים זה חזרתיות: אותה חוברת תמיד מניבה את אותה תוכנית, והתוכנית מדווחת על הנזק של עצמה דרך WeightedError, MaxDistanceSquared, ExactTargetWeight ו-TotalTargetWeight, כך שעבודת אצווה יכולה לסרב לשמור כשהקירוב נעשה גס מדי לפי קווים מנחים של מותג

צינור הפלטה של HotXLS לחוברת true color: ScanIndexedColorUsage נועל כל משבצת שגופן, מילוי, מסגרת, עיצוב מותנה, צורה, הערה או קו רשת מפנה אליה, BuildBiffPalettePlan מציב צבעים מדויקים בסדר ARGB עולה או מריץ עד 16 סבבים של k-means משוקלל תדירות ב-OKLab, ו-ApplyBiffPalettePlan מאמת את הדור ואת ה-hash של 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, hash של FNV-1a בן 64 סיביות על 56 צבעי המקור; האימות בודק מחדש גם כל אינדקס ציבורי ופיזי, כל צבע מקור, שאף משבצת נעולה לא מסומנת כמשתנה, את מוני הנעולות והמשתנות, ושכל יעד הוא אטום. כל שינוי פלטה אפקטיבי בין לבין, כולל החלה מוצלחת קודמת של אותה תוכנית עצמה, מיישן את התוכנית, כך שתוכניות הן למעשה חד-פעמיות. תוכנית תקפה בלי משבצות משתנות מצליחה בלי לקדם את הדור, ושינוי אמיתי מקפיץ את הדור פעם אחת ובונה מחדש את המתאים של OKLab פעם אחת — במנוע ה-Classic על ידי כתיבה מחדש של מערך הפלטה הקבוע, ובמנוע ה-XLSX על ידי החלפה ברשימת דריסת צבעים אינדקסיים מוכנה

הפעלה לשמירות BIFF8 ולהמרה מ-XLSX ל-XLS

המאפיין BiffPaletteSavePolicy ברירת המחדל שלו היא xbpsPreserve, כך ששדרוג של HotXLS לעולם לא כותב מחדש את הפלטה של אף אחד מאחורי גבו. הגדרה ל-xbpsOptimizeTrueColors גורמת לחוברת Classic לבנות ולהחיל תוכנית טרייה בתוך SaveAs, אבל רק כשפורמט היעד הוא xlExcel97; כותבי BIFF5, CSV, HTML, PDF, XLSX והשאר מתעלמים מההגדרה. אחרי שמירה מוצלחת הפלטה הממוטבת נשארת במודל החוברת, כך ששאילתות ושמירות מאוחרות רואות את אותו מיפוי. אם השמירה נכשלת או מבוטלת, 56 הצבעים המקוריים והדור המקורי משוחזרים. למקורות XLSX, SaveXLSXWorkbookAsXLS ב-lxXlsxExport בונה תוכנית אחת מהחוברת הטעונה וכותב אותה לפלטת היעד לפני שסגנון כלשהו מומר — זהו הגשר הדטרמיניסטי שהדגמת ה-workbench לביקורת והמרת חוברות מפעיל. צבעי theme עוברים דרך אותו מתכנן אחרי שה-tint שלהם נפתר ל-RGB; אם אתה מעדיף להשאיר themes חיים במילויי גרפים, המאמר על מילויי גרף בצבעי theme של 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. הורידו את גרסת הניסיון וכוונו אותה לגיליון הצבעוני ביותר שלכם דרך עמוד הרכיב HotXLS Delphi Excel