مقاله فنی

رنگ theme در fill نمودار Excel با Delphi: GelFrame در HotXLS

series نمودار که با RGB literal پر شده باشد از theme مربوط به workbook پیروی نمی‌کند. theme را تغییر دهید و series همان رنگ قبلی را نگه می‌دارد. HotXLS این موضوع را در XLS باینری با fillهای theme-colored برای chart series مدیریت می‌کند: رکوردی از نوع GelFrame با مقدار 4198 یا $1066 که درست بعد از AreaFormat داخل block مربوط به series نوشته می‌شود و یک OfficeArt scheme index به‌اضافه tint حمل می‌کند. سپس Excel series را همان‌طور render می‌کند که fill دارای theme را که خودش نوشته render می‌کند

شماره record مربوط به GelFrame از کجا می‌آید؟

شماره record مربوط به GelFrame برابر 4198 یعنی $1066 است و آن را در بخش specification خود record پیدا نمی‌کنید. [MS-XLS] 2.4.131 توضیح می‌دهد GelFrame چه چیزی دارد، اما برخلاف بیشتر sectionهای record، مقدار rt را نمی‌گوید. ABNF مربوط به chart substream هم کمکی نمی‌کند: فقط productionی با نام GELFRAME = 1*2GelFrame *Continue می‌دهد که record را نام‌گذاری می‌کند اما شماره نمی‌دهد. این number در table مربوط به enumeration شماره record قرار دارد، چند page دورتر از sectionی که payload را document می‌کند. این production برای هرکسی که reader می‌نویسد ارزش نگاه دوم دارد: یک یا دو record از نوع GelFrame را اجازه می‌دهد و هرکدام می‌توانند اختیاری با Continue record دنبال شوند؛ پس parserی که برای هر production یک record ثابت فرض کند، fileی را که خودش ننوشته اشتباه handle می‌کند. HotXLS دقیقاً یک GelFrame به‌ازای هر series دارای theme emit می‌کند؛ همان چیزی که Excel برای یک fill ساده و solid از نوع theme تولید می‌کند و decoder آن record را به‌صورت payload خودکفا می‌بیند، نه با فرض count ثابت

داخل payload مربوط به GelFrame: دو table از propertyهای OfficeArt

payload مربوط به GelFrame دو table از propertyهای OfficeArt را پشت سر هم دارد: یک OfficeArtFOPT که OPT1 نامیده می‌شود و سپس یک OfficeArtTertiaryFOPT یعنی OPT2. هر table با count دو بایتی property شروع می‌شود و به همان تعداد entry شش‌بایتی FOPTE دارد؛ هر entry نیز یک opid دو بایتی و یک op چهار بایتی است. bit 15 از opid همان fComplex است: وقتی set باشد، value مربوط به op طول byte است و tail متغیری بعد از entryهای ثابت می‌آید. decoderی که این tailها را نادیده بگیرد، از sync خارج می‌شود و برای هر property بعد از نخستین property پیچیده، opidهای garbage می‌خواند

theme fill با سه property که در هر دو table پخش شده‌اند به‌اضافه یک property برای اعلام نوع fill بیان می‌شود. HotXLS چهار property را در 28 byte و بدون complex tail می‌نویسد:

  • fillType با مقدار $0180 در OPT1 و value برابر 1 یعنی msofillSolid
  • fillColor با مقدار $0181 در OPT1؛ RGB تخت‌شده‌ای که consumer قدیمی یا ناآگاه از theme آن را render می‌کند
  • fillColorExt با مقدار $019E در OPT2؛ رنگ پایه theme
  • fillColorExtMod با مقدار $01A0 در OPT2؛ tint یا shade اعمال‌شده روی آن base

این split در خود format عمدی است و تصادف implementation نیست: [MS-ODRAW] 2.2.2، سه‌گانه theme را به‌صورت color تخت به‌اضافه base color و modification توصیف می‌کند؛ بنابراین consumerی که theme را می‌فهمد fill را دوباره محاسبه می‌کند و consumerی که نمی‌فهمد همچنان چیزی معقول paint می‌کند. opidهای اطراف همین الگو را دنبال می‌کنند و در edition قدیمی و فعلی [MS-ODRAW] شماره‌گذاری یکسانی دارند؛ هنگام cross-reading دو revision این موضوع راحت است: fillOpacity با مقدار $0182، fillBackColor با مقدار $0183، fillShadeType با مقدار $019C، fillBackColorExt با مقدار $01A2 و fillBackColorExtMod با مقدار $01A4

چرا scheme index در byte قرمز قرار می‌گیرد؟

چون یک OfficeArtCOLORREF بر اساس byte offset تعریف می‌شود نه numeric value: red در byte 0، green در byte 1، blue در byte 2 و flagها در byte 3. این structure را مانند یک DWORD little-endian بخوانید که هر op مربوط به FOPTE هم همین است و red به کم‌ارزش‌ترین byte تبدیل می‌شود. مثال عملی lineColor در [MS-ODRAW] آن را تأیید می‌کند. پس fSchemeIndex که bit E از flagهاست، value عددی $08000000 دارد و خود scheme index در red byte می‌رود و green و blue باید صفر باشند. بنابراین Accent1 مقدار op برابر $08000004 دارد، نه $00000004 و قطعاً نه $04000000

ترتیب theme index که specification از تعریف آن سر باز می‌زند

specification ترتیب scheme index را host-defined می‌نامد و هیچ tableای ارائه نمی‌کند؛ یعنی layout مربوط به byte به‌تنهایی برای interop با Excel کافی نیست. HotXLS از ترتیب theme در spreadsheet استفاده می‌کند که با fileهای واقعی Excel round-trip می‌شود:

  • 0 = lt1، 1 = dk1، 2 = lt2، 3 = dk2
  • 4 تا 9 = accent1 تا accent6
  • 10 = hlink، 11 = folHlink

tint و shade: payload از نوع MSOTINTSHADE

fillColorExtMod یک value از نوع MSOTINTSHADE است و direction و amount را در یک DWORD encode می‌کند، نه به‌شکل signed fraction. مقدار $20000000 یعنی unmodified. tint روشن‌کننده عبارت است از $02F4 shl 16 or amount shl 8 or $10 یعنی MSOTINT و tint تیره‌کننده همین شکل را با $01F4 در high word دارد یعنی MSOSHADE. byte مربوط به amount برخلاف intuition در جهت مخالف عمل می‌کند: $FF یعنی unchanged و $00 یعنی full modification. HotXLS آن را به یک double واحد به سبک DrawingML normalize می‌کند که positive روشن می‌کند و negative تیره، با استفاده از plus یا minus (255 - amount) / 255. این mapping برای valueهایی که Excel واقعاً در UI ارائه می‌دهد exact است و به همین دلیل round-trip تقریباً lossless نیست، بلکه lossless است: «Lighter 40%» آشنا amount برابر 153 دارد و (255 - 153) / 255 در هر دو جهت برابر 0.4 و بدون rounding error است. shade با amount برابر 191 به -64/255 برمی‌گردد. encoder زیر به بازه قانونی clamp شده است:

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

در سمت write، theme fill دو field اضافی روی record مربوط به style هر series است. TXLSChartSeriesStyleInfo، fieldهای HasFillTheme، FillThemeColor و FillThemeTint را گرفته است و builder فقط وقتی GelFrame را emit می‌کند که HasStyle و HasFillTheme هر دو set باشند. اگر FillRgb صریح را هم set کنید، آن value بدون تغییر وارد fillColor در OPT1 می‌شود؛ اگر set نکنید، HotXLS خودش color را با table داخلی پیش‌فرض Office theme و tint اعمال‌شده flatten می‌کند، بنابراین series فقط‌دارای-theme هنوز برای consumerهایی که OPT2 را نادیده می‌گیرند flat color معقولی دارد. به initialization از نوع Default() توجه کنید که مهم است چون TXLSChartSeriesInfo fieldهای managed دارد و memberهای Boolean ساده آن در غیر این صورت garbage روی stack هستند:

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، 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;        // بدون 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ی عبور می‌کند که بقیه chart inspection مربوط به HotXLS از آن استفاده می‌کند. GetChartModel یک TXLSChartModel تحت مالکیت caller برمی‌گرداند که باید آن را free کنید و هر TXLSChartSeries fieldهای HasFillTheme، FillThemeColor و FillThemeTint را در کنار FillRgb decode‌شده از fillColor در OPT1 expose می‌کند؛ این مقدار برای آن series بر color مربوط به AreaFormat تقدم دارد. همان سه value به snapshot semantic canonical نیز با نام‌های SolidFillThemeSet، SolidFillThemeColor و SolidFillThemeTint می‌رسند، بنابراین workbook diff تغییر theme را به‌عنوان تغییر theme می‌بیند نه RGB drift بی‌توضیح. اگر از سمت XLSX می‌آیید، این counterpart مربوط به format باینری همان style است که در راهنمای HotXLS برای chart، image و drawing در 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 fill در binary XLS چه چیزی را تضمین نمی‌کند؟

سه محدودیت صادقانه وجود دارد. اول و مهم‌تر از همه برای هرکسی که این code را audit می‌کند: هیچ sample fileی در corpus محلی اصلاً recordی از نوع GelFrame ندارد. یازده occurrence از byte pair یعنی 66 10 در sample مربوط به conditional-formatting، روی مرزهای غیر-record قرار دارند و dump کل stream صفر hit پیدا می‌کند. bit layoutی که اینجا توضیح داده شد از specification به دست آمد و سپس به سه روش pinned شد: decode symmetry روی output builder، testهای byte که payload مصنوعی $1066 را مستقیم وارد decoder می‌کنند و assert کردن RGB تخت‌شده دقیق. این evidence از file capture‌شده Excel ضعیف‌تر است و بهتر است همین گفته شود، نه اینکه خلاف آن القا شود. دوم، flatten کردن fill فقط‌دارای-theme از table داخلی پیش‌فرض Office theme استفاده می‌کند نه theme part خوانده‌شده از workbook، چون binary XLS به معنایی که XLSX package‌شده theme part دارد theme part ندارد؛ اگر لازم است theme خود workbook رنگ تخت را تعیین کند، FillRgb را خودتان فراهم کنید. سوم، decoder فقط GelFrameی را می‌پذیرد که داخل block مربوط به series باشد؛ همان record می‌تواند در chart area یا axis frame ظاهر شود و پذیرفتن آن در آنجا background fill را بی‌سروصدا به series نسبت می‌دهد، پس آن موارد ignore می‌شوند. fillColorExt بدون flag مقدار $08000000 نیز plain extended color در نظر گرفته می‌شود و هرگز HasFillTheme را set نمی‌کند. برای workbookهایی که chart در دنیای XLSX authored شده و فقط از این مسیر عبور می‌کند، مسیر preservation در ویرایش chartهای Excel بدون از دست دادن ChartML مسیر امن‌تری است و containerی که این recordها در آن قرار دارند در خواندن compound fileهای OLE2 در Delphi بدون COM IStorage پوشش داده شده است

fillهای theme-colored نمودار، encoder و decoder مربوط به GelFrame و builder کامل chart substream در BIFF8، در HotXLS Delphi spreadsheet component برای Delphi و C++Builder عرضه می‌شوند که XLS، XLSX و ODS را بدون نصب Excel می‌خواند و می‌نویسد