Τεχνικό Άρθρο

Excel chart theme colors στο Delphi: HotXLS GelFrame

Chart series με literal RGB fill δεν ακολουθεί το workbook theme. Άλλαξε το theme και η series κρατά το παλιό color. Το HotXLS το χειρίζεται σε binary XLS με theme-colored chart series fills: ένα GelFrame record, 4198 ή $1066, γραμμένο αμέσως μετά το AreaFormat μέσα στο series block και με OfficeArt scheme index συν tint. Το Excel τότε κάνει render τη series όπως κάνει render ένα themed fill που έγραψε το ίδιο

Από πού προκύπτει ο αριθμός του GelFrame record

Ο αριθμός του GelFrame record είναι 4198 ($1066), και δεν θα τον βρεις στο δικό του specification section. Το [MS-XLS] 2.4.131 περιγράφει τι περιέχει ένα GelFrame, αλλά σε αντίθεση με τα περισσότερα record sections δεν δηλώνει το rt value. Ο chart substream ABNF επίσης δεν βοηθά: δίνει μόνο την production GELFRAME = 1*2GelFrame *Continue, η οποία ονομάζει το record χωρίς να το αριθμεί. Ο αριθμός βρίσκεται στον record-number enumeration table, αρκετές σελίδες μακριά από το section που τεκμηριώνει το payload. Η production αξίζει δεύτερη ματιά για όποιον γράφει reader: επιτρέπει ένα ή δύο GelFrame records, καθένα προαιρετικά ακολουθούμενο από Continue records, οπότε parser που υποθέτει ένα record ανά production θα κακοχειριστεί file που δεν έγραψε ο ίδιος. Το HotXLS εκπέμπει ακριβώς ένα GelFrame ανά themed series, όπως παράγει το Excel για ένα απλό solid theme fill, και ο decoder του αντιμετωπίζει το record ως self-contained payload αντί να υποθέτει fixed count

Μέσα στο GelFrame payload: δύο OfficeArt property tables

Το GelFrame payload είναι δύο OfficeArt property tables στη σειρά: ένα OfficeArtFOPT (που λέγεται OPT1) και έπειτα ένα OfficeArtTertiaryFOPT (OPT2). Κάθε table είναι two-byte property count ακολουθούμενο από τόσα six-byte FOPTE entries, και κάθε entry είναι two-byte opid συν four-byte op. Το bit 15 του opid είναι fComplex: όταν είναι ενεργό, η τιμή op είναι byte length και ακολουθεί variable tail. Decoder που αγνοεί αυτά τα tails αποσυγχρονίζεται και διαβάζει garbage opids για όλα τα επόμενα complex properties

Το theme fill εκφράζεται από τρία properties μοιρασμένα στα δύο tables, συν ένα που δηλώνει το fill kind. Το HotXLS γράφει τέσσερα properties σε 28 bytes χωρίς complex tails:

  • fillType $0180 στο OPT1, με τιμή 1 (msofillSolid)
  • fillColor $0181 στο OPT1, το flattened RGB που θα σχεδιάσει παλιότερος ή theme-unaware consumer
  • fillColorExt $019E στο OPT2, το base theme color
  • fillColorExtMod $01A0 στο OPT2, το tint ή shade που εφαρμόζεται σε εκείνο το base

Αυτό το split είναι σκόπιμο στο format και όχι ατύχημα της υλοποίησης: το [MS-ODRAW] 2.2.2 περιγράφει το theme triple ως flat color συν base color συν modification, οπότε consumer που καταλαβαίνει themes επανυπολογίζει το fill, ενώ όποιος δεν καταλαβαίνει εξακολουθεί να σχεδιάζει κάτι λογικό. Τα surrounding opids ακολουθούν το ίδιο μοτίβο και έχουν ίδιο numbering στις παλιές και τις τρέχουσες εκδόσεις του [MS-ODRAW], κάτι βολικό όταν διαβάζεις δύο revisions παράλληλα: 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. Το worked lineColor example στο [MS-ODRAW] το επιβεβαιώνει. Άρα το fSchemeIndex, που είναι flags bit E, έχει numeric value $08000000 και το scheme index μπαίνει στο red byte, με green και blue υποχρεωτικά zero. Το Accent1 είναι επομένως op value $08000004 και όχι $00000004, πόσο μάλλον $04000000

Η σειρά των theme indexes που το specification αρνείται να ορίσει

Το specification αποκαλεί τη σειρά των scheme indexes host-defined και δεν δίνει table, πράγμα που σημαίνει ότι το byte layout μόνο του δεν αρκεί για interop με το Excel. Το HotXLS χρησιμοποιεί τη spreadsheet theme order, η οποία κάνει round-trip με πραγματικά Excel files:

  • 0 = lt1, 1 = dk1, 2 = lt2, 3 = dk2
  • 4 έως 9 = accent1 έως accent6
  • 10 = hlink, 11 = folHlink

Tint και shade: το MSOTINTSHADE payload

Το fillColorExtMod op είναι τιμή MSOTINTSHADE και κωδικοποιεί direction και amount σε ένα DWORD αντί για signed fraction. Η τιμή $20000000 σημαίνει unmodified. Lightening tint είναι $02F4 shl 16 or amount shl 8 or $10 (MSOTINT), ενώ darkening tint έχει το ίδιο σχήμα με $01F4 στο high word (MSOSHADE). Το byte amount πηγαίνει αντίθετα από τη διαίσθηση: $FF σημαίνει unchanged και $00 full modification. Το HotXLS το κανονικοποιεί σε ένα DrawingML-style double όπου positive lightens και negative darkens, χρησιμοποιώντας plus ή minus (255 - amount) / 255. Η mapping είναι exact για τις τιμές που προσφέρει πραγματικά το Excel UI, γι’ αυτό το round-trip είναι lossless και όχι approximately 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 - 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

Ορισμός και ανάγνωση theme fill από Delphi

Στην πλευρά της εγγραφής, ένα theme fill είναι δύο extra fields στο per-series style record. Το TXLSChartSeriesStyleInfo απέκτησε HasFillTheme, FillThemeColor και FillThemeTint, και ο builder εκπέμπει GelFrame μόνο όταν είναι ενεργά και τα HasStyle και HasFillTheme. Αν ορίσεις και explicit FillRgb, αυτή η τιμή μπαίνει αυτούσια στο OPT1 fillColor· αν δεν ορίσεις, το HotXLS κάνει flatten το color μόνο του μέσω built-in default Office theme table με το tint εφαρμοσμένο, οπότε series μόνο με theme έχει ακόμη sane flat color για consumers που αγνοούν το OPT2. Σημείωσε το Default() initialization, που μετρά επειδή το TXLSChartSeriesInfo περιέχει managed fields και τα plain Boolean members του είναι διαφορετικά stack garbage:

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: flattened
    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 επιστρέφει owned TXLSChartModel που ελευθερώνεις εσύ, και κάθε TXLSChartSeries εκθέτει HasFillTheme, FillThemeColor και FillThemeTint μαζί με το FillRgb που αποκωδικοποιήθηκε από το OPT1 fillColor, το οποίο παίρνει precedence απέναντι στο AreaFormat color για τη series. Οι ίδιες τρεις τιμές φτάνουν και στο canonical semantic snapshot ως SolidFillThemeSet, SolidFillThemeColor και SolidFillThemeTint, οπότε ένα workbook diff βλέπει theme change ως theme change και όχι ως unexplained RGB drift. Αν έρχεσαι από την πλευρά XLSX, αυτό είναι το binary-format counterpart του styling που περιγράφεται στον οδηγό του HotXLS για Excel charts, images και drawings στο 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 βρίσκονται σε non-record boundaries και ένα full-stream record dump βρίσκει zero hits. Το bit layout που περιγράφεται εδώ προέκυψε από το specification και μετά καρφώθηκε με τρεις τρόπους: decode symmetry στο builder output, hand-built byte tests που ταΐζουν synthetic $1066 payload κατευθείαν στον decoder και assertion του exact flattened RGB. Είναι ασθενέστερη μορφή evidence από captured Excel file και αξίζει να ειπωθεί αντί να υπονοηθεί το αντίθετο. Δεύτερο, το 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 σε series, οπότε αγνοούνται. fillColorExt χωρίς το flag $08000000 αντιμετωπίζεται επίσης ως plain extended color και δεν ενεργοποιεί ποτέ το HasFillTheme. Για workbooks όπου το chart γράφτηκε στον κόσμο XLSX και απλώς περνά από εδώ, το preservation path στο editing Excel charts χωρίς απώλεια ChartML είναι ασφαλέστερη διαδρομή, ενώ το container στο οποίο κάθονται αυτά τα records καλύπτεται στο reading OLE2 compound files στο Delphi χωρίς COM IStorage

Τα theme-colored chart fills, ο GelFrame encoder και decoder και ο πλήρης BIFF8 chart substream builder διατίθενται στο HotXLS Delphi spreadsheet component για Delphi και C++Builder, το οποίο διαβάζει και γράφει XLS, XLSX και ODS χωρίς εγκατεστημένο Excel