literal RGBでfillしたchart seriesはworkbook themeに従いません。themeを変更してもseriesは古いcolorを保持します。HotXLSはbinary XLSでtheme-colored chart series fillを扱えます。AreaFormatの直後、series blockの中へGelFrame record、4198または$1066を書き、OfficeArt scheme indexとtintを持たせます。Excelはtheme fillを自分で書いた場合と同じようにseriesをrenderします
GelFrame record numberの出所
GelFrame record numberは4198($1066)で、record自身のspecification sectionには見つかりません。[MS-XLS] 2.4.131はGelFrameの内容を説明しますが、ほとんどのrecord sectionと違いrt valueを記載していません。chart substream ABNFも役に立ちません。GELFRAME = 1*2GelFrame *Continueというproductionを示し、recordをnumberingせずにnameだけを挙げています。numberはpayloadをdocumentするsectionから数ページ離れたrecord-number enumeration tableにあります。このproductionはreaderを書く人がもう一度見る価値があります。1つまたは2つのGelFrame recordを許し、それぞれにContinue recordがoptionalで続くため、productionごとに1 recordだけと仮定するparserは自分が書いていないfileを誤処理します。HotXLSはsimple solid theme fillでExcelが生成する形に合わせ、theme seriesごとにGelFrameを正確に1つemitします。decoderはrecordをself-contained payloadとして扱い、fixed countを仮定しません
GelFrame payloadの中身:2つのOfficeArt property table
GelFrame payloadは2つのOfficeArt property tableが連続したものです。OfficeArtFOPT(OPT1と呼ぶ)に続いてOfficeArtTertiaryFOPT(OPT2)があります。各tableは2-byte property countと、その数の6-byte FOPTE entryからなります。各entryは2-byte opidと4-byte opです。opidのbit 15はfComplexで、setされるとop valueがbyte lengthになり、fixed entryの後ろにvariable tailが続きます。このtailを無視するdecoderはdesynchronizeし、最初のcomplex property以後のすべてでgarbage opidを読みます
theme fillは2つのtableに分かれた3つのpropertyと、fill kindを宣言する1つのpropertyで表します。HotXLSはcomplex tailなしで、28 byte内に4 propertyを書きます
- OPT1の
fillType$0180。1(msofillSolid)にsetします - OPT1の
fillColor$0181。olderまたはtheme-unaware consumerが描くflattened RGBです - OPT2の
fillColorExt$019E。base theme colorです - OPT2の
fillColorExtMod$01A0。baseへ適用するtintまたはshadeです
このsplitはimplementationの偶然ではなくformatの意図です。[MS-ODRAW] 2.2.2はtheme tripleをflat color、base color、modificationとして記述します。そのためthemeを理解するconsumerはfillを再計算でき、理解しないconsumerもreasonableなものを描けます。周囲のopidも同じpatternに従い、oldとcurrentの[MS-ODRAW] editionで同じnumberingを持ちます。2つのrevisionをcross-readするとき便利です。fillOpacity $0182、fillBackColor $0183、fillShadeType $019C、fillBackColorExt $01A2、fillBackColorExtMod $01A4です
scheme indexがred byteに入る理由
OfficeArtCOLORREFはnumeric valueではなくbyte offsetで定義されるためです。byte 0がred、byte 1がgreen、byte 2がblue、byte 3がflagsです。このstructureをlittle-endian DWORDとして読むと、すべてのFOPTE opがそうであるように、redがleast significant byteになります。[MS-ODRAW]のworked lineColor exampleもこれを確認します。そのためfSchemeIndex、flags bit Eのnumeric valueは$08000000で、scheme index自身はred byteに置き、greenとblueはzeroにしなければなりません。したがってAccent1のop valueは$08000004であり、$00000004ではなく、もちろん$04000000でもありません
specificationが定義を拒むtheme index order
specificationはscheme index orderをhost-definedと呼び、tableを与えません。そのためbyte layoutだけではExcelとのinteropに足りません。HotXLSはreal Excel fileとのround-tripに合うspreadsheet theme orderを使います
- 0 = lt1、1 = dk1、2 = lt2、3 = dk2
- 4から9 = accent1からaccent6
- 10 = hlink、11 = folHlink
tintとshade:MSOTINTSHADE payload
fillColorExtMod opはMSOTINTSHADE valueで、directionとamountをsigned fractionではなく1つのDWORDにencodeします。$20000000はunmodifiedを意味します。lightening tintは$02F4 shl 16 or amount shl 8 or $10(MSOTINT)、darkening tintはhigh wordに$01F4を置く同じ形(MSOSHADE)です。amount byteはintuitionと逆に動きます。$FFがunchanged、$00がfull modificationです。HotXLSはそれをDrawingML-style doubleへnormalizeし、positiveをlighten、negativeをdarkenとして、plusまたはminus (255 - amount) / 255を使います。Excel UIが実際に提供するvalueについてmappingはexactなので、round-tripはapproximately losslessではなくlosslessです。おなじみの「Lighter 40%」はamount 153で、(255 - 153) / 255はどちら向きにもrounding errorなしに0.4です。amount 191のshadeは-64/255として戻ります。次がlegal rangeへclampしたencoderです
if Tint > 0 then // MSOTINT - lighter
TintOp := LongWord($02F4) shl 16 or
(LongWord(Round(255 * (1 - Tint))) shl 8) or $10
else if Tint < 0 then // MSOSHADE - darker
TintOp := LongWord($01F4) shl 16 or
(LongWord(Round(255 * (1 + Tint))) shl 8) or $10
else
TintOp := $20000000; // MSOCOLORMODUNDEFINED
Delphiからtheme fillを設定して読む
write sideではtheme fillはper-series style recordへの2つのextra fieldです。TXLSChartSeriesStyleInfoにHasFillTheme、FillThemeColor、FillThemeTintが追加され、builderはHasStyleとHasFillThemeの両方がsetされたときだけGelFrameをemitします。explicitなFillRgbも設定すると、そのvalueはOPT1のfillColorへそのまま入ります。設定しなければHotXLSはbuilt-in default Office theme tableを通じてcolorを自分でflattenし、tintも適用します。theme-only seriesもOPT2を無視するconsumer向けにreasonableなflat colorを持つことになります。managed fieldを含むTXLSChartSeriesInfoのplain Boolean memberはstack garbageになり得るため、Default() initializationも重要です
var
Wb: TXLSWorkbook;
Series: array [0..1] of TXLSChartSeriesInfo;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Sheets.Add.Name := 'Data';
Series[0] := Default(TXLSChartSeriesInfo); // このrecordを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、low byteがred
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;
read backはHotXLSの残りのchart inspectionが使うのと同じchart modelを通ります。GetChartModelはownerであるTXLSChartModelを返し、callerがfreeします。各TXLSChartSeriesはFillRgbと並べてHasFillTheme、FillThemeColor、FillThemeTintを公開します。FillRgbはOPT1のfillColorからdecodeされ、seriesのAreaFormat colorより優先されます。同じ3つのvalueはcanonical semantic snapshotにもSolidFillThemeSet、SolidFillThemeColor、SolidFillThemeTintとして届きます。そのためworkbook diffはtheme changeを説明のないRGB driftではなくtheme changeとして見ます。XLSX sideから来たなら、これはDelphiでのHotXLS Excel chart、image、drawing guideが扱うstyleのbinary-format counterpartです
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;
binary XLSのtheme fillがpromiseしないもの
正直に3つあります。第一に、そしてこのcodeをauditする人にとって最重要なのは、local corpusのsample fileにGelFrame recordが1つもないことです。conditional-formatting sampleにあるbyte pair 66 10の11 occurrencesはrecord boundaryではなく、full-stream record dumpでhitは0件です。ここで説明するbit layoutはspecからderiveし、その後3方向で固定しました。builder outputとのdecode symmetry、synthetic $1066 payloadをdecoderへ直接feedするhand-built byte test、exact flattened RGBのassertです。captured Excel fileより弱いevidenceであり、そのことを別のように装うべきではありません。第二に、theme-only fillのflatteningはworkbookからreadしたtheme partではなく、built-in default Office theme tableを使います。packaged XLSXのような意味でbinary XLSにはtheme partがないためです。workbook自身のthemeでflat colorを決める必要があるなら、自分でFillRgbを供給してください。第三に、decoderはseries block内のGelFrameだけを受け入れます。同じrecordはchart areaやaxis frameにも現れます。そこで受け入れるとbackground fillをseriesへ黙って帰属するため、無視します。$08000000 flagなしのfillColorExtもplain extended colorとして扱い、HasFillThemeはsetしません。chartがXLSX worldでauthorされ、ただ通過するだけのworkbookでは、ChartMLを失わずにExcel chartを編集するpreservation pathのほうが安全です。これらのrecordが入るcontainerはCOM IStorageなしでDelphiからOLE2 compound fileを読む方法で扱っています
theme-colored chart fill、GelFrame encoderとdecoder、full BIFF8 chart substream builderは、DelphiとC++Builder向けHotXLS Delphi spreadsheet componentの一部です。ExcelをinstallせずにXLS、XLSX、ODSをreadとwriteできます