סדרת תרשים שמולאה ב-RGB literal אינה עוקבת אחרי ערכת הנושא של חוברת העבודה. משנים את ערכת הנושא והסדרה שומרת את הצבע הישן. HotXLS מטפלת בכך ב-XLS בינארי באמצעות מילויי סדרות בצבעי ערכת נושא: רשומת GelFrame, מספר 4198 או $1066, שנכתבת מיד אחרי AreaFormat בתוך בלוק הסדרה ונושאת אינדקס scheme של OfficeArt יחד עם tint. Excel מציגה אז את הסדרה כפי שהיא מציגה מילוי מונחה-ערכת נושא שהיא עצמה כתבה
מאיפה מגיע מספר רשומת GelFrame
מספר רשומת GelFrame הוא 4198 ($1066), ולא תמצאו אותו בסעיף המפרט של הרשומה עצמה. [MS-XLS] 2.4.131 מתאר מה GelFrame מכילה, אך בניגוד לרוב סעיפי הרשומות אינו מציין את ערך ה-rt. גם ABNF של תת-הזרם של התרשים אינו עוזר: הוא נותן רק את ההפקה GELFRAME = 1*2GelFrame *Continue, שמכנה את הרשומה בלי למספור אותה. המספר נמצא בטבלת enumeration של מספרי הרשומות, כמה עמודים מהסעיף שמתעד את ה-payload. כדאי להתעכב על ההפקה הזו גם עבור מי שכותב reader: היא מתירה רשומת GelFrame אחת או שתיים, כשאחריהן יכולים לבוא Continue records, לכן parser שמניח רשומה יחידה לכל production יטפל לא נכון בקובץ שלא הוא כתב. HotXLS מפיקה בדיוק GelFrame אחת לכל סדרה עם ערכת נושא, וזה מה ש-Excel מפיקה עבור מילוי theme פשוט ומוצק, וה-decoder שלה מתייחס לרשומה כ-payload עצמאי ולא מניח count קבוע
בתוך payload של GelFrame: שתי טבלאות properties של OfficeArt
ה-payload של GelFrame הוא שתי טבלאות properties של OfficeArt גב אל גב: OfficeArtFOPT (שנקראת OPT1) ואחריה OfficeArtTertiaryFOPT (OPT2). כל טבלה היא count של properties בן שני בתים ואחריו מספר זה של איברי FOPTE בני שישה בתים, וכל איבר הוא opid בן שני בתים ו-op בן ארבעה בתים. ביט 15 של opid הוא fComplex: כשהוא מוגדר, ערך op הוא אורך בבתים וזנב משתנה מגיע אחריו. decoder שמתעלם מהזנבות האלה יוצא מסנכרון וקורא opids זבל לכל מה שאחרי ה-property המורכב הראשון
מילוי ערכת הנושא מבוטא באמצעות שלוש properties הפרושות על שתי הטבלאות, ועוד אחת שמצהירה על סוג המילוי. HotXLS כותבת ארבע properties ב-28 בתים בלי זנבות מורכבים:
fillType$0180 ב-OPT1, מוגדר ל-1 (msofillSolid)fillColor$0181 ב-OPT1, ה-RGB השטוח שצרכן ישן או לא מודע לערכת נושא יציירfillColorExt$019E ב-OPT2, צבע הבסיס של ערכת הנושאfillColorExtMod$01A0 ב-OPT2, ה-tint או shade שמוחל על הבסיס
הפיצול הזה מכוון בפורמט ולא תאונה של המימוש: [MS-ODRAW] 2.2.2 מתאר את השלישייה של ערכת הנושא כצבע שטוח ועוד צבע בסיס ועוד modification, כדי שצרכן שמבין ערכות נושא יחשב מחדש את המילוי בעוד צרכן שאינו מבין עדיין יצייר משהו סביר. ה-opids שסביבם פועלים באותה תבנית ונושאים מספור זהה במהדורות [MS-ODRAW] הישנה והנוכחית, מה שנוח כשקוראים שתי גרסאות זו לצד זו: fillOpacity $0182, fillBackColor $0183, fillShadeType $019C, fillBackColorExt $01A2 ו-fillBackColorExtMod $01A4
מדוע אינדקס ה-scheme יושב ב-byte האדום
מפני ש-OfficeArtCOLORREF מוגדר לפי offset של byte ולא לפי ערך מספרי: אדום ב-byte 0, ירוק ב-byte 1, כחול ב-byte 2 ודגלים ב-byte 3. קוראים את המבנה הזה כ-DWORD ב-little-endian, כפי שכל op של FOPTE הוא, והאדום הופך ל-byte הכי פחות משמעותי. הדוגמה המלאה של lineColor ב-[MS-ODRAW] מאשרת זאת. לכן fSchemeIndex, שהוא flags bit E, מקבל את הערך המספרי $08000000, וה-scheme index עצמו נכנס ל-byte האדום כאשר הירוק והכחול חייבים להיות אפס. Accent1 הוא לכן ערך op של $08000004, לא $00000004 ובוודאי לא $04000000
סדר אינדקס ערכת הנושא שהמפרט מסרב להגדיר
המפרט קורא לסדר אינדקס ה-scheme host-defined ואינו נותן טבלה, כלומר layout הבתים לבדו אינו מספיק ל-interoperability עם Excel. HotXLS משתמשת בסדר ערכת הנושא של הגיליון, וזה שעובר round-trip מול קובצי Excel אמיתיים:
- 0 = lt1, 1 = dk1, 2 = lt2, 3 = dk2
- 4 עד 9 = accent1 עד accent6
- 10 = hlink, 11 = folHlink
Tint ו-shade: מטען MSOTINTSHADE
ה-fillColorExtMod op הוא ערך MSOTINTSHADE, והוא מקודד כיוון וכמות ב-DWORD אחד במקום כשבר חתום. הערך $20000000 פירושו ללא שינוי. tint שמבהיר הוא $02F4 shl 16 or amount shl 8 or $10 (MSOTINT); tint שמכהה הוא אותה צורה עם $01F4 ב-high word (MSOSHADE). ה-byte של amount מתקדם בכיוון ההפוך לאינטואיציה: $FF פירושו ללא שינוי ו-$00 פירושו modification מלאה. HotXLS מנרמלת זאת ל-double יחיד בסגנון DrawingML שבו חיובי מבהיר ושלילי מכהה, באמצעות פלוס או מינוס (255 - amount) / 255. המיפוי מדויק עבור הערכים ש-Excel מציעה בפועל ב-UI שלה, ולכן ה-round-trip הוא lossless ולא בקירוב lossless: "Lighter 40%" המוכר הוא amount 153, ו-(255 - 153) / 255 הוא 0.4 בלי שגיאת עיגול בשום כיוון. shade עם amount 191 חוזר כ--64/255. הנה ה-encoder, כשהוא מוגבל לטווח החוקי:
if Tint > 0 then // MSOTINT - בהיר יותר
TintOp := LongWord($02F4) shl 16 or
(LongWord(Round(255 * (1 - Tint))) shl 8) or $10
else if Tint < 0 then // MSOSHADE - כהה יותר
TintOp := LongWord($01F4) shl 16 or
(LongWord(Round(255 * (1 + Tint))) shl 8) or $10
else
TintOp := $20000000; // MSOCOLORMODUNDEFINED
הגדרה וקריאה של מילוי ערכת נושא מ-Delphi
בצד הכתיבה, מילוי theme הוא שני שדות נוספים ברשומת הסגנון לכל סדרה. TXLSChartSeriesStyleInfo קיבלה את HasFillTheme, FillThemeColor ו-FillThemeTint, והמחולל מפיק את GelFrame רק כאשר גם HasStyle וגם HasFillTheme מוגדרים. אם אתם מגדירים גם FillRgb מפורש, הערך נכנס ל-fillColor של OPT1 ללא שינוי; אם לא, HotXLS משטיחה בעצמה את הצבע דרך טבלת Office theme ברירת מחדל מובנית עם ה-tint מוחל, כך שלסדרה שמכילה רק theme יש עדיין צבע שטוח סביר לצרכנים שמתעלמים מ-OPT2. שימו לב לאתחול Default(), שחשוב מפני ש-TXLSChartSeriesInfo מכילה שדות מנוהלים ואיברי Boolean פשוטים הם אחרת זבל מה-stack:
var
Wb: TXLSWorkbook;
Series: array [0..1] of TXLSChartSeriesInfo;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Sheets.Add.Name := 'Data';
Series[0] := Default(TXLSChartSeriesInfo); // לעולם אל תעשה FillChar לרשומה הזו
Series[0].Name := 'Explicit';
Series[0].Categories := 'Data!$A$1:$A$2';
Series[0].Values := 'Data!$B$1:$B$2';
Series[0].HasStyle := True;
Series[0].Style.HasFill := True;
Series[0].Style.FillRgb := $C47244; // accent1, אדום ב-byte הנמוך
Series[0].Style.HasFillTheme := True;
Series[0].Style.FillThemeColor := 4; // accent1
Series[0].Style.FillThemeTint := 0.4; // Lighter 40%
Series[1] := Default(TXLSChartSeriesInfo);
Series[1].Name := 'ThemeOnly';
Series[1].Categories := 'Data!$A$1:$A$2';
Series[1].Values := 'Data!$C$1:$C$2';
Series[1].HasStyle := True;
Series[1].Style.HasFillTheme := True; // ללא RGB מפורש: עובר flatten
Series[1].Style.FillThemeColor := 8; // accent5
Wb.Sheets.AddChartSheet('Themed', xlsChartTypeColumn, '', '', '', Series);
Wb.SaveAs('themed.xls');
finally
Wb.Free;
end;
end;
הקריאה חזרה עוברת דרך אותו chart model ששאר inspection התרשימים של HotXLS משתמש בו. GetChartModel מחזירה TXLSChartModel בבעלות הקורא, שאותו משחררים, וכל TXLSChartSeries חושפת HasFillTheme, FillThemeColor ו-FillThemeTint לצד FillRgb שמפוענח מ-fillColor של OPT1, שמקבל קדימות על צבע AreaFormat של אותה סדרה. אותם שלושה ערכים מגיעים גם ל-semantic snapshot הקנוני כ-SolidFillThemeSet, SolidFillThemeColor ו-SolidFillThemeTint, לכן diff של חוברת עבודה רואה שינוי theme כשינוי theme ולא כנדידת RGB בלתי מוסברת. אם אתם מגיעים מצד XLSX, זהו המקביל של הפורמט הבינארי לעיצוב שמתואר במדריך HotXLS לתרשימי Excel, תמונות וציורים ב-Delphi:
Wb := TXLSWorkbook.Create;
try
Wb.Open('themed.xls');
Model := Wb.Sheets[2]._Chart.GetChartModel;
try
Ser := Model.GetSeries(0);
if Ser.HasFillTheme then
begin
WriteLn(Ser.FillThemeColor); // 4 = accent1
WriteLn(Ser.FillThemeTint:0:3); // 0.400
WriteLn(IntToHex(Ser.FillRgb, 6)); // C47244, ה-fillColor של OPT1
end;
finally
Model.Free;
end;
finally
Wb.Free;
end;
מה מילוי theme ב-XLS בינארי אינו מבטיח
שלוש מגבלות כנות. הראשונה, והחשובה ביותר למי שמבקר את הקוד הזה: אף קובץ דוגמה בקורפוס המקומי אינו מכיל כלל רשומת GelFrame. 11 ההופעות של צמד הבתים 66 10 בדוגמת ה-conditional-formatting יושבות בגבולות שאינם גבולות רשומה, ו-dump מלא של הזרם מוצא אפס התאמות. ה-layout של הביטים שמתואר כאן נגזר מהמפרט ואז ננעל בשלוש דרכים, באמצעות decode symmetry על פלט המחולל, בדיקות bytes שנבנו ביד ומזינות payload מסונתז של $1066 ישירות ל-decoder ובאמצעות assert על ה-RGB השטוח המדויק. זו ראיה חלשה יותר מקובץ Excel שנלכד, וכדאי לומר זאת במקום לרמוז אחרת. שנית, ה-flattening למילוי theme-only משתמש בטבלת Office theme ברירת מחדל מובנית ולא בחלק theme שנקרא מחוברת העבודה, מפני של-XLS בינארי אין theme part במובן שיש ל-XLSX ארוז — אם אתם צריכים שערכת הנושא של חוברת העבודה תניע את הצבע השטוח, ספקו FillRgb בעצמכם. שלישית, ה-decoder מקבל GelFrame רק בתוך בלוק סדרה; אותה רשומה יכולה להופיע באזור התרשים או במסגרת ציר, וקבלתה שם תייחס בשקט fill של רקע לסדרה, לכן אלה מתעלמים. fillColorExt בלי הדגל $08000000 מטופל גם הוא כצבע extended רגיל ולעולם אינו מגדיר HasFillTheme. עבור חוברות עבודה שהתרשים בהן נכתב בעולם XLSX ורק עובר דרככם, נתיב השימור בעריכת תרשימי Excel בלי לאבד ChartML בטוח יותר, והמיכל שבו הרשומות האלה יושבות מכוסה בקריאת קובצי OLE2 מורכבים ב-Delphi ללא COM IStorage
מילויי תרשים בצבעי ערכת נושא, ה-encoder וה-decoder של GelFrame ומחולל תת-הזרם המלא של תרשימי BIFF8 נשלחים ברכיב HotXLS Delphi spreadsheet component עבור Delphi ו-C++Builder, שקורא וכותב XLS, XLSX ו-ODS ללא Excel מותקן