chart series ที่ fill ด้วย RGB literal จะไม่ตาม workbook theme เปลี่ยน theme แล้ว series ยังคงสีเก่า HotXLS จัดการเรื่องนี้ใน binary XLS ด้วย theme-colored chart series fill ผ่าน record GelFrame, 4198 หรือ $1066 ซึ่งเขียนต่อจาก AreaFormat ภายใน series block และเก็บ OfficeArt scheme index กับ tint ไว้ Excel จึง render series แบบเดียวกับ themed fill ที่ตัวเองเขียน
หมายเลข record GelFrame มาจากไหน
หมายเลข record ของ GelFrame คือ 4198 ($1066) และคุณจะไม่พบมันใน section ที่อธิบาย record เอง [MS-XLS] 2.4.131 บอกว่า GelFrame มีอะไรบ้าง แต่ต่างจาก record section ส่วนใหญ่ มันไม่ได้ระบุค่า rt ส่วน chart substream ABNF ก็ไม่ช่วย เพราะให้เพียง production GELFRAME = 1*2GelFrame *Continue ซึ่งเรียกชื่อ record โดยไม่บอกหมายเลข หมายเลขอยู่ใน record-number enumeration table ที่ห่างออกไปหลายหน้า production นี้ควรอ่านซ้ำสำหรับคนเขียน reader เพราะมันอนุญาตให้มี GelFrame หนึ่งหรือสอง record และแต่ละ record อาจตามด้วย Continue record ได้ parser ที่ assume ว่า production หนึ่งมี record เดียวจะจัดการไฟล์ที่ตัวเองไม่ได้เขียนผิด HotXLS emit GelFrame หนึ่งตัวต่อ themed series ซึ่งตรงกับสิ่งที่ Excel ผลิตสำหรับ simple solid theme fill และ decoder ของมันมอง record เป็น payload ที่จบในตัวเองแทนการ assume count ตายตัว
ภายใน GelFrame payload: OfficeArt property table สองชุด
GelFrame payload คือ OfficeArt property table สองชุดต่อกัน ได้แก่ OfficeArtFOPT (เรียก OPT1) แล้วตามด้วย OfficeArtTertiaryFOPT (OPT2) แต่ละ table เริ่มด้วย property count สอง byte แล้วตามด้วย FOPTE entry ขนาดหก byte ตามจำนวนที่ระบุ แต่ละ entry มี opid สอง byte กับ op สี่ byte bit 15 ของ opid คือ fComplex เมื่อ set ค่า op จะเป็น byte length และมี variable tail ตามหลัง fixed entry decoder ที่ละเลย tail จะ desynchronize แล้วอ่าน opid ของทุก property หลัง complex property ตัวแรกเป็น garbage
theme fill แสดงด้วย property สามตัวที่กระจายอยู่ในทั้งสอง table และอีกหนึ่งตัวที่ประกาศชนิด fill HotXLS เขียน property สี่ตัวใน 28 byte โดยไม่มี complex tail
fillType$0180 ใน OPT1 ตั้งเป็น 1 (msofillSolid)fillColor$0181 ใน OPT1 คือ flattened RGB ที่ consumer รุ่นเก่าหรือไม่รู้จัก theme จะใช้วาดfillColorExt$019E ใน OPT2 คือ base theme colorfillColorExtMod$01A0 ใน OPT2 คือ tint หรือ shade ที่ใช้กับ base
การแยกนี้เป็นสิ่งที่ format ตั้งใจ ไม่ใช่ผลข้างเคียงของ implementation [MS-ODRAW] 2.2.2 อธิบาย theme triple ว่าเป็น flat color บวก base color บวก modification ดังนั้น consumer ที่เข้าใจ theme จะคำนวณ fill ใหม่ ส่วน consumer ที่ไม่เข้าใจก็ยังวาดสิ่งที่สมเหตุสมผลได้ opid รอบข้างใช้ pattern เดียวกันและมีเลขเหมือนกันใน [MS-ODRAW] edition เก่าและปัจจุบัน ซึ่งสะดวกเวลาต้องอ่านสอง revision เทียบกัน ได้แก่ fillOpacity $0182, fillBackColor $0183, fillShadeType $019C, fillBackColorExt $01A2 และ fillBackColorExtMod $01A4
ทำไม scheme index จึงอยู่ใน red byte
เพราะ OfficeArtCOLORREF ถูกกำหนดด้วย byte offset ไม่ใช่ numeric value โดย red อยู่ byte 0, green อยู่ byte 1, blue อยู่ byte 2 และ flags อยู่ byte 3 อ่าน structure นี้เป็น little-endian DWORD ซึ่งเป็นสิ่งที่ FOPTE op ทุกตัวใช้ แล้ว red จะกลายเป็น least significant byte ตัวอย่าง lineColor ที่ทำเป็น worked example ใน [MS-ODRAW] ยืนยันเรื่องนี้ ดังนั้น fSchemeIndex ซึ่งเป็น flags bit E จึงมี numeric value $08000000 และ scheme index เองต้องอยู่ใน red byte โดย green กับ blue ต้องเป็นศูนย์ Accent1 จึงมี op value เป็น $08000004 ไม่ใช่ $00000004 และแน่นอนว่าไม่ใช่ $04000000
ลำดับ theme index ที่ specification ไม่ยอมกำหนด
specification เรียกลำดับ scheme index ว่า host-defined และไม่ให้ table มา นั่นหมายความว่า byte layout อย่างเดียวไม่พอสำหรับ interop กับ Excel HotXLS ใช้ลำดับของ spreadsheet theme ซึ่ง round-trip กับไฟล์ Excel จริงได้
- 0 = lt1, 1 = dk1, 2 = lt2, 3 = dk2
- 4 ถึง 9 = accent1 ถึง accent6
- 10 = hlink, 11 = folHlink
Tint และ shade: payload MSOTINTSHADE
ค่า fillColorExtMod เป็น MSOTINTSHADE value และ encode direction กับ amount ไว้ใน DWORD เดียว ไม่ใช่ signed fraction ค่า $20000000 หมายถึงไม่แก้ไข tint ที่ทำให้สว่างขึ้นคือ $02F4 shl 16 or amount shl 8 or $10 (MSOTINT) ส่วน tint ที่ทำให้มืดลงใช้รูปแบบเดียวกันแต่มี $01F4 ใน high word (MSOSHADE) byte amount เดินสวนกับ intuition $FF หมายถึงไม่เปลี่ยนและ $00 หมายถึง modification เต็มรูปแบบ HotXLS normalize ค่านี้เป็น double แบบ DrawingML-style ค่าเดียว โดยค่าบวกทำให้สว่างและค่าลบทำให้มืด ใช้บวกหรือลบ (255 - amount) / 255 mapping นี้ตรงกับค่าที่ Excel เปิดให้เลือกใน UI จึง round-trip ได้ lossless ไม่ใช่แค่ใกล้เคียง "Lighter 40%" ที่คุ้นเคยคือ amount 153 และ (255 - 153) / 255 เท่ากับ 0.4 โดยไม่มี rounding error ไม่ว่าทิศทางใด shade ที่ amount 191 จะกลับมาเป็น -64/255 ต่อไปนี้คือ encoder ที่ clamp อยู่ใน legal range
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
ตั้งและอ่าน theme fill จาก Delphi
ฝั่งเขียน theme fill คือ field เพิ่มสองสามตัวใน per-series style record TXLSChartSeriesStyleInfo เพิ่ม HasFillTheme, FillThemeColor และ FillThemeTint และ builder จะ emit GelFrame ก็ต่อเมื่อทั้ง HasStyle และ HasFillTheme ถูก set หากคุณ set FillRgb ชัดเจนด้วย ค่านั้นจะเข้า OPT1 fillColor แบบตรง ๆ หากไม่ set HotXLS จะ flatten color เองผ่าน built-in default Office theme table พร้อมใช้ tint ดังนั้น series ที่มีเพียง theme ก็ยังมี flat color ที่สมเหตุสมผลสำหรับ consumer ที่ ignore OPT2 โปรดสังเกตการ initialize ด้วย Default() เพราะ TXLSChartSeriesInfo มี managed field และ Boolean ธรรมดาของมันจะกลายเป็น stack garbage หากไม่ initialize
var
Wb: TXLSWorkbook;
Series: array [0..1] of TXLSChartSeriesInfo;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Sheets.Add.Name := 'Data';
Series[0] := Default(TXLSChartSeriesInfo); // ห้าม FillChar record นี้
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, red อยู่ใน low 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; // ไม่มี explicit 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 เดียวกับที่ HotXLS chart inspection ส่วนอื่นใช้ GetChartModel คืน TXLSChartModel ที่มี ownership และคุณต้อง free เอง ส่วน TXLSChartSeries แต่ละตัว expose HasFillTheme, FillThemeColor และ FillThemeTint ควบคู่กับ FillRgb ที่ decode จาก OPT1 fillColor ซึ่งมี precedence เหนือ AreaFormat color ของ series นั้น ค่าเดียวกันสามตัวจะไปถึง canonical semantic snapshot ในชื่อ SolidFillThemeSet, SolidFillThemeColor และ SolidFillThemeTint ด้วย ดังนั้น workbook diff จะเห็น theme change เป็น theme change ไม่ใช่ RGB drift ที่อธิบายไม่ได้ หากคุณมาจากฝั่ง XLSX นี่คือ binary-format counterpart ของ styling ที่อธิบายใน คู่มือ HotXLS สำหรับ Excel chart, image และ drawing ใน 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, OPT1 fillColor
end;
finally
Model.Free;
end;
finally
Wb.Free;
end;
theme fill ใน binary XLS ไม่ได้สัญญาอะไร
มีข้อจำกัดที่ควรพูดอย่างตรงไปตรงมาสามอย่าง ข้อแรกและสำคัญที่สุดสำหรับคน audit code นี้คือ sample file ใน local corpus ไม่มี GelFrame record เลย byte pair 66 10 ที่พบสิบเอ็ดครั้งใน conditional-formatting sample อยู่บนตำแหน่งที่ไม่ใช่ record boundary และ full-stream record dump พบ hit ศูนย์ bit layout ที่อธิบายมาจาก specification แล้ว pin ไว้สามทาง ได้แก่ decode symmetry บน builder output, byte test ที่สร้างเองและป้อน synthetic $1066 payload ตรงเข้า decoder และการ assert flattened RGB นี่เป็นหลักฐานที่อ่อนกว่า Excel file ที่ capture มา และควรพูดเช่นนั้นแทนการทำให้เหมือนมีหลักฐานมากกว่าจริง ข้อสอง flattening สำหรับ theme-only fill ใช้ built-in default Office theme table ไม่ใช่ theme part ที่อ่านจาก workbook เพราะ binary XLS ไม่มี theme part ในความหมายเดียวกับ packaged XLSX หากต้องการให้ theme ของ workbook เป็นคนกำหนด flat color ให้ส่ง FillRgb เอง ข้อสาม decoder รับ GelFrame เฉพาะภายใน series block record เดียวกันอาจอยู่บน chart area หรือ axis frame ได้ และการรับตรงนั้นจะทำให้ background fill ถูก assign ให้ series อย่างเงียบ ๆ จึง ignore ไว้ fillColorExt ที่ไม่มี flag $08000000 ก็ถูกถือเป็น plain extended color และไม่เคย set HasFillTheme สำหรับ workbook ที่ chart ถูกสร้างในโลก XLSX แล้วเพียงผ่านเข้ามา preservation path ใน การแก้ Excel chart โดยไม่ทำ ChartML หาย ปลอดภัยกว่า และ container ที่เก็บ record เหล่านี้อธิบายใน การอ่าน OLE2 compound file ใน Delphi โดยไม่ใช้ COM IStorage
theme-colored chart fill, GelFrame encoder และ decoder รวมถึง BIFF8 chart substream builder ฉบับเต็มอยู่ใน HotXLS Delphi spreadsheet component สำหรับ Delphi และ C++Builder ซึ่งอ่านและเขียน XLS, XLSX และ ODS ได้โดยไม่ต้องติดตั้ง Excel