Read QExcell theme colors from XLSX styles

This commit is contained in:
Курнат Андрей
2026-06-01 12:07:13 +03:00
parent 1df1b1114e
commit c751fda372
3 changed files with 114 additions and 8 deletions
+32
View File
@@ -342,6 +342,38 @@ describe("excellOffice OOXML helpers", () => {
});
});
it("читает theme, tint и indexed цвета XLSX из styles.xml", () => {
const styleTable = xlsxStylesXmlToCellFormatTable({
styleSheet: {
fonts: {
font: [{ color: { theme: "1" } }, { color: { theme: "4", tint: "0.5" } }, { color: { indexed: "22" } }]
},
fills: {
fill: [{}, {}, { patternFill: { fgColor: { theme: "6" } } }]
},
borders: {
border: [{}, { left: { style: "thin", color: { indexed: "4" } } }]
},
cellXfs: {
xf: [
{ fontId: "0", fillId: "0", borderId: "0" },
{ fontId: "1", fillId: "2", borderId: "1" },
{ fontId: "2", fillId: "0", borderId: "0" }
]
}
}
});
expect(styleTable[0]).toBeUndefined();
expect(styleTable[1]).toEqual({
textColor: "A7C0DE",
fillColor: "9BBB59",
border: true,
borderColor: "0000FF"
});
expect(styleTable[2]).toEqual({ textColor: "C0C0C0" });
});
it("преобразует XML листа в модель QExcell", () => {
const cells = worksheetXmlToCells(
{
+80 -6
View File
@@ -105,6 +105,41 @@ const hyperlinkRelationshipType = "http://schemas.openxmlformats.org/officeDocum
const drawingRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing";
const chartRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart";
const customNumberFormatStartId = 164;
const defaultXlsxThemeColors = [
"000000",
"FFFFFF",
"1F497D",
"EEECE1",
"4F81BD",
"C0504D",
"9BBB59",
"8064A2",
"4BACC6",
"F79646",
"0000FF",
"800080"
];
const indexedXlsxColors: Record<number, string> = {
0: "000000",
1: "FFFFFF",
2: "FF0000",
3: "00FF00",
4: "0000FF",
5: "FFFF00",
6: "FF00FF",
7: "00FFFF",
8: "000000",
9: "FFFFFF",
10: "FF0000",
11: "00FF00",
12: "0000FF",
13: "FFFF00",
14: "FF00FF",
15: "00FFFF",
22: "C0C0C0",
23: "808080",
64: ""
};
const builtinNumberFormats: Record<number, string> = {
1: "0",
2: "0.00",
@@ -869,7 +904,7 @@ export function createCellStyleRegistry(cellFormatsBySheet: Array<Record<string,
export function xlsxStylesXmlToCellFormatTable(stylesXml?: unknown): Array<CellFormat | undefined> {
const styleSheet = childRecord(stylesXml, "styleSheet");
const numberFormats = xlsxNumberFormats(styleSheet);
const fonts = toArray(childRecord(styleSheet, "fonts").font).map(fontXmlToCellFormat);
const fonts = toArray(childRecord(styleSheet, "fonts").font).map((font, index) => fontXmlToCellFormat(font, index));
const fills = toArray(childRecord(styleSheet, "fills").fill).map(fillXmlToCellFormat);
const borders = toArray(childRecord(styleSheet, "borders").border).map(borderXmlToCellFormat);
@@ -1034,9 +1069,9 @@ function normalizedCellFormat(format: CellFormat | undefined): CellFormat | null
return Object.keys(normalized).length > 0 ? normalized : null;
}
function fontXmlToCellFormat(font: unknown): CellFormat {
function fontXmlToCellFormat(font: unknown, index = 0): CellFormat {
const record = asRecord(font);
const textColor = normalizeFillColor(String(childRecord(record, "color").rgb || ""));
const textColor = colorRecordToHex(childRecord(record, "color"), { ignoreThemeAndIndexed: index === 0 });
const fontSize = normalizedFontSize(numberAttribute(childRecord(record, "sz").val));
const fontFamily = normalizedFontFamily(String(childRecord(record, "name").val || ""));
return (
@@ -1054,7 +1089,7 @@ function fontXmlToCellFormat(font: unknown): CellFormat {
function fillXmlToCellFormat(fill: unknown): CellFormat {
const patternFill = childRecord(fill, "patternFill");
const fillColor = normalizeFillColor(String(childRecord(patternFill, "fgColor").rgb || ""));
const fillColor = colorRecordToHex(childRecord(patternFill, "fgColor"));
return fillColor ? { fillColor } : {};
}
@@ -1066,7 +1101,7 @@ function borderXmlToCellFormat(border: unknown): CellFormat {
});
const borderColor = borderedSides
.map((side) => normalizeFillColor(String(childRecord(childRecord(record, side), "color").rgb || "")))
.map((side) => colorRecordToHex(childRecord(childRecord(record, side), "color")))
.find(Boolean);
return normalizedCellFormat({
@@ -1081,7 +1116,7 @@ function baseFontXml() {
function formatFontXml(format: CellFormat) {
const flags = `${format.bold ? "<b/>" : ""}${format.italics ? "<i/>" : ""}${format.underline ? "<u/>" : ""}${format.strike ? "<strike/>" : ""}`;
const color = format.textColor ? `<color rgb="FF${format.textColor}"/>` : '<color theme="1"/>';
const color = format.textColor ? `<color rgb="FF${format.textColor}"/>` : "";
return `<font>${flags}<sz val="${format.fontSize ?? 11}"/>${color}<name val="${escapeXmlAttribute(format.fontFamily ?? "Calibri")}"/><family val="2"/></font>`;
}
@@ -1107,6 +1142,45 @@ function normalizeFillColor(value?: string) {
return /^[0-9A-F]{6}$/.test(raw) ? raw : "";
}
function colorRecordToHex(record: XmlRecord, options: { ignoreThemeAndIndexed?: boolean } = {}) {
const rgbColor = normalizeFillColor(String(record.rgb ?? ""));
if (rgbColor) {
return rgbColor;
}
if (options.ignoreThemeAndIndexed) {
return "";
}
if (record.theme !== undefined) {
const themeColor = themeColorToHex(numberAttribute(record.theme), Number.parseFloat(String(record.tint ?? "")));
if (themeColor) {
return themeColor;
}
}
return record.indexed !== undefined ? indexedXlsxColors[numberAttribute(record.indexed)] ?? "" : "";
}
function themeColorToHex(themeIndex: number, tint = 0) {
const baseColor = defaultXlsxThemeColors[themeIndex];
if (!baseColor) {
return "";
}
return Number.isFinite(tint) && tint !== 0 ? applyXlsxTint(baseColor, tint) : baseColor;
}
function applyXlsxTint(hexColor: string, tint: number) {
const channels = [0, 2, 4].map((offset) => Number.parseInt(hexColor.slice(offset, offset + 2), 16));
const tinted = channels.map((channel) => {
const value = tint < 0 ? channel * (1 + tint) : channel * (1 - tint) + 255 * tint;
return Math.min(255, Math.max(0, Math.round(value))).toString(16).padStart(2, "0").toUpperCase();
});
return tinted.join("");
}
function normalizedFontSize(value?: number) {
if (!Number.isFinite(value) || !value || value <= 0) {
return undefined;