Una serie de gráfico rellena con un RGB literal no sigue el tema del workbook. Cambie el tema y la serie conserva el color antiguo. HotXLS gestiona esto en XLS binario mediante rellenos de serie coloreados por tema: un registro GelFrame, 4198 o $1066, escrito justo después de AreaFormat dentro del bloque de la serie y con un índice de esquema OfficeArt más un tint. Excel renderiza entonces la serie como renderiza un relleno temático escrito por él mismo
¿De dónde sale el número de registro GelFrame?
El número de registro GelFrame es 4198 ($1066), y no lo encontrará en la propia sección de especificación del registro. [MS-XLS] 2.4.131 describe qué contiene un GelFrame, pero, a diferencia de la mayoría de las secciones de registros, no indica el valor rt. La ABNF del substream de gráficos tampoco ayuda: solo ofrece la producción GELFRAME = 1*2GelFrame *Continue, que nombra el registro sin numerarlo. El número vive en la tabla de enumeración de números de registro, varias páginas más allá de la sección que documenta el payload. Esa producción merece una segunda mirada para quien escriba un lector: permite uno o dos registros GelFrame, cada uno seguido opcionalmente de registros Continue, así que un parser que suponga un único registro por producción tratará mal un fichero que no haya escrito él. HotXLS emite exactamente un GelFrame por serie con tema, que es lo que produce Excel para un relleno temático sólido sencillo, y su decoder trata el registro como un payload autocontenido en lugar de suponer un recuento fijo
Dentro del payload GelFrame: dos tablas de propiedades OfficeArt
El payload GelFrame son dos tablas de propiedades OfficeArt una detrás de otra: una OfficeArtFOPT (llamada OPT1) seguida de una OfficeArtTertiaryFOPT (OPT2). Cada tabla es un recuento de propiedades de dos bytes seguido de ese número de entradas FOPTE de seis bytes, y cada entrada tiene un opid de dos bytes más un op de cuatro bytes. El bit 15 de opid es fComplex: cuando está activado, el valor op es una longitud en bytes y detrás de las entradas fijas aparece una cola variable. Un decoder que ignore esas colas se desincroniza y lee opids basura para todo lo que sigue a la primera propiedad compleja
El relleno de tema se expresa mediante tres propiedades distribuidas entre ambas tablas, más una que declara el tipo de relleno. HotXLS escribe cuatro propiedades en 28 bytes sin colas complejas:
fillType$0180 en OPT1, fijado en 1 (msofillSolid)fillColor$0181 en OPT1, el RGB aplanado que dibujará un consumidor antiguo o que no conozca los temasfillColorExt$019E en OPT2, el color base del temafillColorExtMod$01A0 en OPT2, el tint o shade aplicado a esa base
La separación es deliberada en el formato, no un accidente de la implementación: [MS-ODRAW] 2.2.2 describe el triple del tema como un color plano más un color base y una modificación, de modo que un consumidor que entiende los temas recalcula el relleno y uno que no los entiende sigue pintando algo razonable. Los opids que lo rodean siguen el mismo patrón y llevan numeración idéntica en las ediciones antigua y actual de [MS-ODRAW], algo cómodo al leer en paralelo dos revisiones: fillOpacity $0182, fillBackColor $0183, fillShadeType $019C, fillBackColorExt $01A2 y fillBackColorExtMod $01A4
¿Por qué el índice de esquema está en el byte rojo?
Porque un OfficeArtCOLORREF se define por offset de byte, no por valor numérico: el rojo está en el byte 0, el verde en el byte 1, el azul en el byte 2 y los flags en el byte 3. Lea esa estructura como un DWORD little-endian, que es lo que representa cada op de FOPTE, y el rojo se convierte en el byte menos significativo. El ejemplo trabajado de lineColor en [MS-ODRAW] lo confirma. Por eso fSchemeIndex, que es el bit E de los flags, tiene el valor numérico $08000000, y el índice de esquema va en el byte rojo, con verde y azul obligatoriamente a cero. Accent1 es por tanto el valor op $08000004, no $00000004 y desde luego no $04000000
El orden de índices de tema que la especificación se niega a definir
La especificación llama al orden de índices de esquema definido por el host y no ofrece ninguna tabla, lo que significa que el layout de bytes por sí solo no basta para interoperar con Excel. HotXLS utiliza el orden del tema de la hoja de cálculo, que es el que hace round-trip frente a ficheros Excel reales:
- 0 = lt1, 1 = dk1, 2 = lt2, 3 = dk2
- 4 a 9 = accent1 a accent6
- 10 = hlink, 11 = folHlink
Tint y shade: el payload MSOTINTSHADE
El op fillColorExtMod es un valor MSOTINTSHADE y codifica dirección y cantidad en un DWORD en lugar de una fracción con signo. El valor $20000000 significa sin modificar. Un tint de aclarado es $02F4 shl 16 or amount shl 8 or $10 (MSOTINT); un tint de oscurecimiento tiene la misma forma con $01F4 en la palabra alta (MSOSHADE). El byte amount avanza en dirección contraria a la intuición: $FF significa sin cambios y $00 significa la modificación completa. HotXLS lo normaliza a un único double al estilo DrawingML donde positivo aclara y negativo oscurece, utilizando más o menos (255 - amount) / 255. El mapeo es exacto para los valores que ofrece Excel en su UI, por eso el round-trip no tiene pérdida en lugar de ser aproximadamente sin pérdida: el conocido «Lighter 40%» es amount 153 y (255 - 153) / 255 es 0,4 sin error de redondeo en ninguna dirección. Un shade con amount 191 vuelve como -64/255. Este es el encoder, limitado al rango legal:
if Tint > 0 then // MSOTINT - más claro
TintOp := LongWord($02F4) shl 16 or
(LongWord(Round(255 * (1 - Tint))) shl 8) or $10
else if Tint < 0 then // MSOSHADE - más oscuro
TintOp := LongWord($01F4) shl 16 or
(LongWord(Round(255 * (1 + Tint))) shl 8) or $10
else
TintOp := $20000000; // MSOCOLORMODUNDEFINED
Establecer y leer un relleno de tema desde Delphi
En el lado de escritura, un relleno de tema son dos campos adicionales en el record de estilo por serie. TXLSChartSeriesStyleInfo ha ganado HasFillTheme, FillThemeColor y FillThemeTint, y el builder emite el GelFrame solo cuando están establecidos tanto HasStyle como HasFillTheme. Si también establece un FillRgb explícito, ese valor pasa literalmente al fillColor de OPT1; si no lo hace, HotXLS aplana el color por su cuenta mediante una tabla de temas Office predeterminada integrada y con el tint aplicado, de modo que una serie que solo tenga tema sigue llevando un color plano sensato para los consumidores que ignoren OPT2. Fíjese en la inicialización Default(), que importa porque TXLSChartSeriesInfo contiene campos gestionados y sus miembros Boolean normales serían basura de la pila en otro caso:
var
Wb: TXLSWorkbook;
Series: array [0..1] of TXLSChartSeriesInfo;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Sheets.Add.Name := 'Data';
Series[0] := Default(TXLSChartSeriesInfo); // nunca aplicar FillChar a este 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, rojo en el byte bajo
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; // sin RGB explícito: aplanado
Series[1].Style.FillThemeColor := 8; // accent5
Wb.Sheets.AddChartSheet('Themed', xlsChartTypeColumn, '', '', '', Series);
Wb.SaveAs('themed.xls');
finally
Wb.Free;
end;
end;
La lectura pasa por el mismo modelo de gráficos que utiliza el resto de la inspección de gráficos de HotXLS. GetChartModel devuelve un TXLSChartModel propiedad del caller que debe liberar, y cada TXLSChartSeries expone HasFillTheme, FillThemeColor y FillThemeTint junto a FillRgb, decodificado desde el fillColor de OPT1, que tiene precedencia sobre el color de AreaFormat de esa serie. Los mismos tres valores también llegan a la snapshot semántica canónica como SolidFillThemeSet, SolidFillThemeColor y SolidFillThemeTint, así que un diff de workbook ve un cambio de tema como cambio de tema y no como una deriva RGB inexplicable. Si llega desde el lado XLSX, este es el equivalente de formato binario del estilo descrito en la guía de HotXLS sobre gráficos, imágenes y dibujos Excel en 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, el fillColor de OPT1
end;
finally
Model.Free;
end;
finally
Wb.Free;
end;
¿Qué no promete un relleno de tema en XLS binario?
Tres límites honestos. Primero, y más importante para quien audite este código: ningún fichero de ejemplo del corpus local contiene un registro GelFrame. Las once apariciones de la pareja de bytes 66 10 en el ejemplo de formato condicional se encuentran en límites que no son de registro y un volcado completo del stream encuentra cero coincidencias. El layout de bits descrito aquí se derivó de la especificación y después se fijó de tres maneras: mediante simetría de decodificación sobre la salida del builder, mediante pruebas de bytes construidas a mano que introducen un payload $1066 sintético directamente en el decoder y mediante la aserción del RGB aplanado exacto. Es una forma de evidencia más débil que un fichero Excel capturado, y merece la pena decirlo en lugar de insinuar lo contrario. Segundo, el aplanado de un relleno que solo tenga tema utiliza una tabla de temas Office predeterminada integrada, no una parte de tema leída del workbook, porque XLS binario no tiene una parte de tema en el sentido empaquetado de XLSX: si necesita que el tema propio del workbook determine el color plano, suministre FillRgb usted mismo. Tercero, el decoder solo acepta un GelFrame dentro de un bloque de serie; el mismo registro puede aparecer en el área del gráfico o en un frame de eje, y aceptarlo allí atribuiría silenciosamente un relleno de fondo a una serie, así que se ignoran. Un fillColorExt sin el flag $08000000 también se trata como un color extendido normal y nunca establece HasFillTheme. Para workbooks cuyo gráfico se haya creado en el mundo XLSX y solo pase por el sistema, la ruta de conservación de editar gráficos Excel sin perder ChartML es más segura, y el contenedor donde viven estos registros se explica en la lectura de ficheros compuestos OLE2 en Delphi sin COM IStorage
Los rellenos de gráfico coloreados por tema, el encoder y decoder GelFrame y el builder completo del substream de gráficos BIFF8 se incluyen en el componente de hojas de cálculo Delphi HotXLS para Delphi y C++Builder, que lee y escribe XLS, XLSX y ODS sin tener Excel instalado