documonster 0.18.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README_zh.md +1 -1
- package/dist/esm/modules/excel/core/workbook-model.js +1 -0
- package/dist/esm/modules/excel/core/workbook.browser.js +1 -0
- package/dist/esm/modules/excel/xlsb/conditional-format.js +54 -10
- package/dist/esm/modules/excel/xlsb/dxf.js +928 -0
- package/dist/esm/modules/excel/xlsb/fill.js +12 -2
- package/dist/esm/modules/excel/xlsb/filter-criteria.js +15 -8
- package/dist/esm/modules/excel/xlsb/font.js +5 -4
- package/dist/esm/modules/excel/xlsb/read/package.js +6 -0
- package/dist/esm/modules/excel/xlsb/styles.js +30 -359
- package/dist/esm/modules/excel/xlsb/write/package.js +13 -1
- package/dist/esm/modules/excel/xlsb/write/worksheet.js +16 -1
- package/dist/esm/modules/excel/xlsx/xform/simple/boolean-xform.js +12 -1
- package/dist/esm/modules/excel/xlsx/xform/style/alignment-xform.js +52 -16
- package/dist/esm/modules/excel/xlsx/xform/style/border-xform.js +24 -2
- package/dist/esm/modules/excel/xlsx/xform/style/dxf-extensions.js +55 -0
- package/dist/esm/modules/excel/xlsx/xform/style/dxf-xform.js +121 -6
- package/dist/esm/modules/excel/xlsx/xform/style/fill-xform.js +17 -10
- package/dist/esm/modules/excel/xlsx/xform/style/font-xform.js +32 -8
- package/dist/esm/modules/excel/xlsx/xform/style/styles-xform.js +83 -8
- package/dist/esm/modules/excel/xlsx/xlsx.browser.js +10 -0
- package/dist/esm/utils/text-measure.js +1 -1
- package/dist/iife/documonster.archive.iife.min.js +1 -1
- package/dist/iife/documonster.csv.iife.min.js +1 -1
- package/dist/iife/documonster.draw.iife.min.js +2 -2
- package/dist/iife/documonster.excel.iife.min.js +53 -53
- package/dist/iife/documonster.formula.iife.min.js +1 -1
- package/dist/iife/documonster.markdown.iife.min.js +1 -1
- package/dist/iife/documonster.mermaid.iife.min.js +2 -2
- package/dist/iife/documonster.pdf.iife.min.js +2 -2
- package/dist/iife/documonster.stream.iife.min.js +1 -1
- package/dist/iife/documonster.word.iife.min.js +60 -60
- package/dist/iife/documonster.xml.iife.min.js +1 -1
- package/dist/types/modules/excel/core/workbook-core.d.ts +10 -1
- package/dist/types/modules/excel/core/workbook-model.d.ts +3 -1
- package/dist/types/modules/excel/types.d.ts +33 -4
- package/dist/types/modules/excel/xlsb/conditional-format.d.ts +20 -2
- package/dist/types/modules/excel/xlsb/dxf.d.ts +56 -0
- package/dist/types/modules/excel/xlsb/fill.d.ts +5 -1
- package/dist/types/modules/excel/xlsb/filter-criteria.d.ts +9 -1
- package/dist/types/modules/excel/xlsb/font.d.ts +22 -0
- package/dist/types/modules/excel/xlsb/styles.d.ts +2 -13
- package/dist/types/modules/excel/xlsb/write/worksheet.d.ts +2 -0
- package/dist/types/modules/excel/xlsx/xform/simple/boolean-xform.d.ts +15 -0
- package/dist/types/modules/excel/xlsx/xform/style/alignment-xform.d.ts +13 -1
- package/dist/types/modules/excel/xlsx/xform/style/border-xform.d.ts +11 -0
- package/dist/types/modules/excel/xlsx/xform/style/dxf-extensions.d.ts +29 -0
- package/dist/types/modules/excel/xlsx/xform/style/dxf-xform.d.ts +34 -1
- package/dist/types/modules/excel/xlsx/xform/style/fill-xform.d.ts +15 -3
- package/dist/types/modules/excel/xlsx/xform/style/font-xform.d.ts +6 -0
- package/dist/types/modules/excel/xlsx/xform/style/styles-xform.d.ts +23 -0
- package/dist/types/utils/text-measure.d.ts +2 -1
- package/package.json +17 -18
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
import { borderStyleName, borderStyleValue } from "./border.js";
|
|
2
|
+
import { fillPatternName, fillPatternValue } from "./fill.js";
|
|
3
|
+
import { SCHEME, UNDERLINE, VERTICAL_ALIGN, nameOf, underlineValue } from "./font.js";
|
|
4
|
+
import { dxfExtensionsOf } from "../xlsx/xform/style/dxf-extensions.js";
|
|
5
|
+
import { BinaryWriter, concatUint8Arrays } from "../../../utils/binary.js";
|
|
6
|
+
/** `xfPropType` values — MS-XLSB 2.5.159. 0x27 and 0x28 are not defined. */
|
|
7
|
+
const PROPERTY = {
|
|
8
|
+
fillPattern: 0x00,
|
|
9
|
+
fillForeground: 0x01,
|
|
10
|
+
fillBackground: 0x02,
|
|
11
|
+
gradient: 0x03,
|
|
12
|
+
gradientStop: 0x04,
|
|
13
|
+
fontColor: 0x05,
|
|
14
|
+
borderTop: 0x06,
|
|
15
|
+
borderBottom: 0x07,
|
|
16
|
+
borderLeft: 0x08,
|
|
17
|
+
borderRight: 0x09,
|
|
18
|
+
borderDiagonal: 0x0a,
|
|
19
|
+
borderVertical: 0x0b,
|
|
20
|
+
borderHorizontal: 0x0c,
|
|
21
|
+
diagonalUp: 0x0d,
|
|
22
|
+
diagonalDown: 0x0e,
|
|
23
|
+
horizontalAlignment: 0x0f,
|
|
24
|
+
verticalAlignment: 0x10,
|
|
25
|
+
textRotation: 0x11,
|
|
26
|
+
indent: 0x12,
|
|
27
|
+
readingOrder: 0x13,
|
|
28
|
+
wrapText: 0x14,
|
|
29
|
+
justifyLastLine: 0x15,
|
|
30
|
+
shrinkToFit: 0x16,
|
|
31
|
+
mergeCell: 0x17,
|
|
32
|
+
fontName: 0x18,
|
|
33
|
+
fontWeight: 0x19,
|
|
34
|
+
underline: 0x1a,
|
|
35
|
+
script: 0x1b,
|
|
36
|
+
italic: 0x1c,
|
|
37
|
+
strike: 0x1d,
|
|
38
|
+
outline: 0x1e,
|
|
39
|
+
shadow: 0x1f,
|
|
40
|
+
condense: 0x20,
|
|
41
|
+
extend: 0x21,
|
|
42
|
+
charset: 0x22,
|
|
43
|
+
fontFamily: 0x23,
|
|
44
|
+
fontSize: 0x24,
|
|
45
|
+
fontScheme: 0x25,
|
|
46
|
+
numberFormatString: 0x26,
|
|
47
|
+
numberFormatId: 0x29,
|
|
48
|
+
relativeIndent: 0x2a,
|
|
49
|
+
locked: 0x2b,
|
|
50
|
+
hidden: 0x2c
|
|
51
|
+
};
|
|
52
|
+
/** The border edge each `XFPropBorder` type formats, in the model's names. */
|
|
53
|
+
const BORDER_EDGE = [
|
|
54
|
+
[PROPERTY.borderTop, "top"],
|
|
55
|
+
[PROPERTY.borderBottom, "bottom"],
|
|
56
|
+
[PROPERTY.borderLeft, "left"],
|
|
57
|
+
[PROPERTY.borderRight, "right"],
|
|
58
|
+
[PROPERTY.borderDiagonal, "diagonal"],
|
|
59
|
+
[PROPERTY.borderVertical, "vertical"],
|
|
60
|
+
[PROPERTY.borderHorizontal, "horizontal"]
|
|
61
|
+
];
|
|
62
|
+
/** `HorizAlign` — MS-XLSB 2.5.74, by value. A stated `general` resets alignment, so it is a value, not an absence. */
|
|
63
|
+
const HORIZONTAL = [
|
|
64
|
+
"general",
|
|
65
|
+
"left",
|
|
66
|
+
"center",
|
|
67
|
+
"right",
|
|
68
|
+
"fill",
|
|
69
|
+
"justify",
|
|
70
|
+
"centerContinuous",
|
|
71
|
+
"distributed"
|
|
72
|
+
];
|
|
73
|
+
/** `VertAlign` — MS-XLSB 2.5.158. Unlike a cell format's table, `bottom` is a stated value here, not a default. */
|
|
74
|
+
const VERTICAL = [
|
|
75
|
+
"top",
|
|
76
|
+
"middle",
|
|
77
|
+
"bottom",
|
|
78
|
+
"justify",
|
|
79
|
+
"distributed"
|
|
80
|
+
];
|
|
81
|
+
/** `ReadingOrder` — MS-XLSB 2.5.114. A stated `context` resets the direction, so it too is a value. */
|
|
82
|
+
const READING_ORDER = ["context", "ltr", "rtl"];
|
|
83
|
+
/** `relativeIndent` meaning "no relative indentation" — MS-XLSB 2.5.159, type 0x2A. */
|
|
84
|
+
const NO_RELATIVE_INDENT = 0xff;
|
|
85
|
+
/** `Bold` — MS-XLSB 2.5.6. An enumeration of two weights, not a flag. */
|
|
86
|
+
const WEIGHT_BOLD = 0x02bc;
|
|
87
|
+
const WEIGHT_NORMAL = 0x0190;
|
|
88
|
+
/** `XFPropTextRotation` for vertically stacked text — MS-XLSB 2.5.165. */
|
|
89
|
+
const ROTATION_VERTICAL = 0xff;
|
|
90
|
+
/** `fNewBorder`, bit 15 of `BrtDXF`'s flag word: the inner-border types 0x0B and 0x0C are permitted. */
|
|
91
|
+
const DXF_NEW_BORDER = 0x8000;
|
|
92
|
+
/** Twips per point, for a font size. */
|
|
93
|
+
const TWIPS_PER_POINT = 20;
|
|
94
|
+
/**
|
|
95
|
+
* The fixed size of each property's data, in bytes — MS-XLSB 2.5.159 and the structures it names. `fontName` is a
|
|
96
|
+
* minimum (its `LPWideString` count; the characters follow) and `numberFormatString` is left out, since it is
|
|
97
|
+
* never decoded. A property whose `cb` leaves less than this is reported, not read: reading it anyway took bytes
|
|
98
|
+
* from the *next* property, or threw a `RangeError` out of the whole styles part.
|
|
99
|
+
*/
|
|
100
|
+
const DATA_SIZE = {
|
|
101
|
+
[PROPERTY.fillPattern]: 1,
|
|
102
|
+
[PROPERTY.fillForeground]: 8,
|
|
103
|
+
[PROPERTY.fillBackground]: 8,
|
|
104
|
+
[PROPERTY.gradient]: 44,
|
|
105
|
+
[PROPERTY.gradientStop]: 18,
|
|
106
|
+
[PROPERTY.fontColor]: 8,
|
|
107
|
+
[PROPERTY.borderTop]: 10,
|
|
108
|
+
[PROPERTY.borderBottom]: 10,
|
|
109
|
+
[PROPERTY.borderLeft]: 10,
|
|
110
|
+
[PROPERTY.borderRight]: 10,
|
|
111
|
+
[PROPERTY.borderDiagonal]: 10,
|
|
112
|
+
[PROPERTY.borderVertical]: 10,
|
|
113
|
+
[PROPERTY.borderHorizontal]: 10,
|
|
114
|
+
[PROPERTY.diagonalUp]: 1,
|
|
115
|
+
[PROPERTY.diagonalDown]: 1,
|
|
116
|
+
[PROPERTY.horizontalAlignment]: 1,
|
|
117
|
+
[PROPERTY.verticalAlignment]: 1,
|
|
118
|
+
[PROPERTY.textRotation]: 1,
|
|
119
|
+
[PROPERTY.indent]: 2,
|
|
120
|
+
[PROPERTY.readingOrder]: 1,
|
|
121
|
+
[PROPERTY.wrapText]: 1,
|
|
122
|
+
[PROPERTY.justifyLastLine]: 1,
|
|
123
|
+
[PROPERTY.shrinkToFit]: 1,
|
|
124
|
+
[PROPERTY.mergeCell]: 1,
|
|
125
|
+
[PROPERTY.fontName]: 2,
|
|
126
|
+
[PROPERTY.fontWeight]: 2,
|
|
127
|
+
[PROPERTY.underline]: 2,
|
|
128
|
+
[PROPERTY.script]: 2,
|
|
129
|
+
[PROPERTY.italic]: 1,
|
|
130
|
+
[PROPERTY.strike]: 1,
|
|
131
|
+
[PROPERTY.outline]: 1,
|
|
132
|
+
[PROPERTY.shadow]: 1,
|
|
133
|
+
[PROPERTY.condense]: 1,
|
|
134
|
+
[PROPERTY.extend]: 1,
|
|
135
|
+
[PROPERTY.charset]: 1,
|
|
136
|
+
[PROPERTY.fontFamily]: 1,
|
|
137
|
+
[PROPERTY.fontSize]: 4,
|
|
138
|
+
[PROPERTY.fontScheme]: 1,
|
|
139
|
+
[PROPERTY.numberFormatId]: 2,
|
|
140
|
+
[PROPERTY.relativeIndent]: 2,
|
|
141
|
+
[PROPERTY.locked]: 1,
|
|
142
|
+
[PROPERTY.hidden]: 1
|
|
143
|
+
};
|
|
144
|
+
/** `XFPropColor.xclrType` — MS-XLSB 2.5.161. */
|
|
145
|
+
const COLOR_TYPE = {
|
|
146
|
+
automatic: 0x00,
|
|
147
|
+
indexed: 0x01,
|
|
148
|
+
rgb: 0x02,
|
|
149
|
+
theme: 0x03,
|
|
150
|
+
notSet: 0x04
|
|
151
|
+
};
|
|
152
|
+
// =============================================================================
|
|
153
|
+
// Writing
|
|
154
|
+
// =============================================================================
|
|
155
|
+
/**
|
|
156
|
+
* Encode a differential format.
|
|
157
|
+
*
|
|
158
|
+
* @param formatId - The `ifmt` a number-format code is written under. The format has to be declared by a `BrtFmt`
|
|
159
|
+
* in the same styles part (or be built in), so the caller that owns that table decides the number.
|
|
160
|
+
*/
|
|
161
|
+
export function encodeDxf(style, formatId) {
|
|
162
|
+
const props = [];
|
|
163
|
+
const dropped = [];
|
|
164
|
+
const add = (type, blob) => {
|
|
165
|
+
props.push({ type, bytes: xfProp(type, blob) });
|
|
166
|
+
};
|
|
167
|
+
const flag = (type, value) => {
|
|
168
|
+
if (value !== undefined) {
|
|
169
|
+
add(type, u8(value ? 1 : 0));
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const unknownKeys = (group, value, known) => {
|
|
173
|
+
for (const key of Object.keys(value ?? {})) {
|
|
174
|
+
if (!known.includes(key) && value[key] !== undefined) {
|
|
175
|
+
dropped.push(`${group}.${key}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
encodeFill(style.fill, add, dropped);
|
|
180
|
+
encodeFont(style.font, add, flag, dropped);
|
|
181
|
+
unknownKeys("font", style.font, FONT_KEYS);
|
|
182
|
+
encodeBorders(style.border, add, flag, dropped);
|
|
183
|
+
unknownKeys("border", style.border, BORDER_KEYS);
|
|
184
|
+
encodeAlignment(style.alignment, add, flag, dropped);
|
|
185
|
+
unknownKeys("alignment", style.alignment, ALIGNMENT_KEYS);
|
|
186
|
+
if (style.numFmt !== undefined) {
|
|
187
|
+
const code = typeof style.numFmt === "string" ? style.numFmt : style.numFmt.formatCode;
|
|
188
|
+
add(PROPERTY.numberFormatId, u16(formatId(code)));
|
|
189
|
+
}
|
|
190
|
+
flag(PROPERTY.locked, style.protection?.locked);
|
|
191
|
+
flag(PROPERTY.hidden, style.protection?.hidden);
|
|
192
|
+
unknownKeys("protection", style.protection, ["locked", "hidden"]);
|
|
193
|
+
// An XLSX `<extLst>` the reader preserved: `BrtDXF` has no extension point to carry it in.
|
|
194
|
+
if (dxfExtensionsOf(style) !== undefined) {
|
|
195
|
+
dropped.push("extLst");
|
|
196
|
+
}
|
|
197
|
+
// Anything at the top level beyond the six facets a differential format has. `numFmtId` is the XLSX
|
|
198
|
+
// writer's own bookkeeping and `styleName` names a cell style, which a differential format cannot apply.
|
|
199
|
+
for (const key of Object.keys(style)) {
|
|
200
|
+
if (!TOP_LEVEL_KEYS.includes(key) && style[key] !== undefined) {
|
|
201
|
+
dropped.push(key);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Ascending by type. The specification constrains which types may coexist, not their order, but Excel writes
|
|
205
|
+
// them sorted; the one ordering rule that exists — a gradient (3) followed by its stops (4) — survives a sort
|
|
206
|
+
// because the sort is stable and the stops are pushed after the gradient.
|
|
207
|
+
props.sort((left, right) => left.type - right.type);
|
|
208
|
+
if (props.length === 0) {
|
|
209
|
+
// A differential format that changes nothing is not writable: Excel discards the whole `DXFs` collection when
|
|
210
|
+
// it meets one, and with it every other rule's formatting. The caller gives the style no `dxfId`.
|
|
211
|
+
return { payload: undefined, dropped };
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
payload: concatUint8Arrays([
|
|
215
|
+
// `fNewBorder` is a *capability* — the inner-border types are permitted — not a claim that any is present.
|
|
216
|
+
// Excel sets it on every `BrtDXF` it writes.
|
|
217
|
+
u16(DXF_NEW_BORDER),
|
|
218
|
+
new BinaryWriter().writeUint16(0).writeUint16(props.length).toUint8Array(),
|
|
219
|
+
...props.map(property => property.bytes)
|
|
220
|
+
]),
|
|
221
|
+
dropped
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const TOP_LEVEL_KEYS = ["font", "fill", "border", "alignment", "protection", "numFmt", "numFmtId"];
|
|
225
|
+
const FONT_KEYS = [
|
|
226
|
+
"color",
|
|
227
|
+
"name",
|
|
228
|
+
"bold",
|
|
229
|
+
"underline",
|
|
230
|
+
"vertAlign",
|
|
231
|
+
"italic",
|
|
232
|
+
"strike",
|
|
233
|
+
"outline",
|
|
234
|
+
"shadow",
|
|
235
|
+
"condense",
|
|
236
|
+
"extend",
|
|
237
|
+
"charset",
|
|
238
|
+
"family",
|
|
239
|
+
"size",
|
|
240
|
+
"scheme"
|
|
241
|
+
];
|
|
242
|
+
const BORDER_KEYS = [
|
|
243
|
+
"top",
|
|
244
|
+
"bottom",
|
|
245
|
+
"left",
|
|
246
|
+
"right",
|
|
247
|
+
"diagonal",
|
|
248
|
+
"vertical",
|
|
249
|
+
"horizontal",
|
|
250
|
+
"color"
|
|
251
|
+
];
|
|
252
|
+
const ALIGNMENT_KEYS = [
|
|
253
|
+
"horizontal",
|
|
254
|
+
"vertical",
|
|
255
|
+
"textRotation",
|
|
256
|
+
"indent",
|
|
257
|
+
"readingOrder",
|
|
258
|
+
"wrapText",
|
|
259
|
+
"shrinkToFit",
|
|
260
|
+
"justifyLastLine",
|
|
261
|
+
"relativeIndent"
|
|
262
|
+
];
|
|
263
|
+
function encodeFill(fill, add, dropped) {
|
|
264
|
+
if (fill === undefined) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (fill.type === "pattern") {
|
|
268
|
+
// An absent pattern is written as no pattern property — the differential "keep the cell's pattern", with the
|
|
269
|
+
// colour painted solid — not as `FLSNULL`, which removes the fill.
|
|
270
|
+
if (fill.pattern !== undefined) {
|
|
271
|
+
const value = fillPatternValue(fill.pattern);
|
|
272
|
+
if (value === undefined) {
|
|
273
|
+
dropped.push("fill.pattern");
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
add(PROPERTY.fillPattern, u8(value));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (fill.fgColor !== undefined) {
|
|
280
|
+
add(PROPERTY.fillForeground, xfPropColor(fill.fgColor));
|
|
281
|
+
}
|
|
282
|
+
if (fill.bgColor !== undefined) {
|
|
283
|
+
add(PROPERTY.fillBackground, xfPropColor(fill.bgColor));
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// `XFPropGradient` — MS-XLSB 2.5.162: a type, then five `Xnum`s. The four rectangle coordinates are the
|
|
288
|
+
// XLSX `left`/`right`/`top`/`bottom`, which the model folds into `center` and leaves `right`/`bottom` out of
|
|
289
|
+
// when they repeat `left`/`top`.
|
|
290
|
+
const center = fill.center;
|
|
291
|
+
const path = fill.gradient === "path";
|
|
292
|
+
const left = path ? (center?.left ?? 0) : 0;
|
|
293
|
+
const top = path ? (center?.top ?? 0) : 0;
|
|
294
|
+
add(PROPERTY.gradient, new BinaryWriter()
|
|
295
|
+
.writeInt32(path ? 1 : 0)
|
|
296
|
+
.writeFloat64(path ? 0 : (fill.degree ?? 0))
|
|
297
|
+
.writeFloat64(left)
|
|
298
|
+
.writeFloat64(path ? (center?.right ?? left) : 0)
|
|
299
|
+
.writeFloat64(top)
|
|
300
|
+
.writeFloat64(path ? (center?.bottom ?? top) : 0)
|
|
301
|
+
.toUint8Array());
|
|
302
|
+
for (const stop of fill.stops ?? []) {
|
|
303
|
+
add(PROPERTY.gradientStop, concatUint8Arrays([
|
|
304
|
+
new BinaryWriter().writeUint16(0).writeFloat64(stop.position).toUint8Array(),
|
|
305
|
+
xfPropColor(stop.color)
|
|
306
|
+
]));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function encodeFont(font, add, flag, dropped) {
|
|
310
|
+
if (font === undefined) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (font.color !== undefined) {
|
|
314
|
+
add(PROPERTY.fontColor, xfPropColor(font.color));
|
|
315
|
+
}
|
|
316
|
+
if (font.name !== undefined) {
|
|
317
|
+
// An `LPWideString` here "MUST be less than or equal to 32 characters". A longer name cannot be written
|
|
318
|
+
// truncated — that names a different font — so it is not written at all.
|
|
319
|
+
if ([...font.name].length > 32) {
|
|
320
|
+
dropped.push("font.name");
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
add(PROPERTY.fontName, lpWideString(font.name));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (font.bold !== undefined) {
|
|
327
|
+
add(PROPERTY.fontWeight, u16(font.bold ? WEIGHT_BOLD : WEIGHT_NORMAL));
|
|
328
|
+
}
|
|
329
|
+
if (font.underline !== undefined) {
|
|
330
|
+
// `false` and `"none"` are written as `ULSNONE`: in a differential format they remove an underline.
|
|
331
|
+
add(PROPERTY.underline, u16(underlineValue(font.underline)));
|
|
332
|
+
}
|
|
333
|
+
if (font.vertAlign !== undefined) {
|
|
334
|
+
const value = font.vertAlign === "baseline"
|
|
335
|
+
? VERTICAL_ALIGN.none
|
|
336
|
+
: VERTICAL_ALIGN[font.vertAlign];
|
|
337
|
+
if (value === undefined) {
|
|
338
|
+
dropped.push("font.vertAlign");
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
add(PROPERTY.script, u16(value));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
flag(PROPERTY.italic, font.italic);
|
|
345
|
+
flag(PROPERTY.strike, font.strike);
|
|
346
|
+
flag(PROPERTY.outline, font.outline);
|
|
347
|
+
flag(PROPERTY.shadow, font.shadow);
|
|
348
|
+
flag(PROPERTY.condense, font.condense);
|
|
349
|
+
flag(PROPERTY.extend, font.extend);
|
|
350
|
+
if (font.charset !== undefined) {
|
|
351
|
+
if (Number.isInteger(font.charset) && font.charset >= 0 && font.charset <= 0xff) {
|
|
352
|
+
add(PROPERTY.charset, u8(font.charset));
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
dropped.push("font.charset");
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (font.family !== undefined) {
|
|
359
|
+
// "MUST be greater than or equal to 0 and less than or equal to 5."
|
|
360
|
+
if (Number.isInteger(font.family) && font.family >= 0 && font.family <= 5) {
|
|
361
|
+
add(PROPERTY.fontFamily, u8(font.family));
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
dropped.push("font.family");
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (font.size !== undefined) {
|
|
368
|
+
// Twips, and "MUST be greater than or equal to 20 and less than or equal to 8191".
|
|
369
|
+
const twips = Math.round(font.size * TWIPS_PER_POINT);
|
|
370
|
+
if (twips >= 20 && twips <= 8191) {
|
|
371
|
+
add(PROPERTY.fontSize, u32(twips));
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
dropped.push("font.size");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (font.scheme !== undefined) {
|
|
378
|
+
const value = SCHEME[font.scheme];
|
|
379
|
+
if (value === undefined) {
|
|
380
|
+
dropped.push("font.scheme");
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
add(PROPERTY.fontScheme, u8(value));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function encodeBorders(border, add, flag, dropped) {
|
|
388
|
+
if (border === undefined) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
// A border-wide `color` is the XLSX writer's shorthand for "every edge without its own"; applied the same way.
|
|
392
|
+
const fallback = border.color;
|
|
393
|
+
for (const [type, edge] of BORDER_EDGE) {
|
|
394
|
+
const value = border[edge];
|
|
395
|
+
if (value === undefined) {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (value.style !== undefined && borderStyleValue(value.style) === 0) {
|
|
399
|
+
dropped.push(`border.${edge}.style`);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
add(type, concatUint8Arrays([
|
|
403
|
+
xfPropColor(value.color ?? fallback),
|
|
404
|
+
u16(borderStyleValue(value.style))
|
|
405
|
+
]));
|
|
406
|
+
}
|
|
407
|
+
flag(PROPERTY.diagonalUp, border.diagonal?.up);
|
|
408
|
+
flag(PROPERTY.diagonalDown, border.diagonal?.down);
|
|
409
|
+
}
|
|
410
|
+
function encodeAlignment(alignment, add, flag, dropped) {
|
|
411
|
+
if (alignment === undefined) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const enumerated = (type, table, value, name) => {
|
|
415
|
+
if (value === undefined) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const index = table.indexOf(value);
|
|
419
|
+
if (index < 0) {
|
|
420
|
+
dropped.push(`alignment.${name}`);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
add(type, u8(index));
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
enumerated(PROPERTY.horizontalAlignment, HORIZONTAL, alignment.horizontal, "horizontal");
|
|
427
|
+
enumerated(PROPERTY.verticalAlignment, VERTICAL, alignment.vertical, "vertical");
|
|
428
|
+
if (alignment.textRotation !== undefined) {
|
|
429
|
+
const rotation = textRotationValue(alignment.textRotation);
|
|
430
|
+
if (rotation === undefined) {
|
|
431
|
+
dropped.push("alignment.textRotation");
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
add(PROPERTY.textRotation, u8(rotation));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (alignment.indent !== undefined) {
|
|
438
|
+
// "MUST be less than or equal to 15."
|
|
439
|
+
if (Number.isInteger(alignment.indent) && alignment.indent >= 0 && alignment.indent <= 15) {
|
|
440
|
+
add(PROPERTY.indent, u16(alignment.indent));
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
dropped.push("alignment.indent");
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
enumerated(PROPERTY.readingOrder, READING_ORDER, alignment.readingOrder, "readingOrder");
|
|
447
|
+
flag(PROPERTY.wrapText, alignment.wrapText);
|
|
448
|
+
flag(PROPERTY.shrinkToFit, alignment.shrinkToFit);
|
|
449
|
+
if (alignment.justifyLastLine !== undefined) {
|
|
450
|
+
// "If this value is 1, then an XFProp with xfPropType equal to 0x000F MUST exist … and MUST equal 0x07" —
|
|
451
|
+
// justify-distributed only means something for distributed text, and the record may not say it otherwise.
|
|
452
|
+
if (alignment.justifyLastLine && alignment.horizontal !== "distributed") {
|
|
453
|
+
dropped.push("alignment.justifyLastLine");
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
flag(PROPERTY.justifyLastLine, alignment.justifyLastLine);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (alignment.relativeIndent !== undefined) {
|
|
460
|
+
// A 2-byte signed integer, -15…15 ("or 255", the absence of one — which the model spells by omission).
|
|
461
|
+
const value = alignment.relativeIndent;
|
|
462
|
+
if (Number.isInteger(value) && value >= -15 && value <= 15) {
|
|
463
|
+
add(PROPERTY.relativeIndent, i16(value));
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
dropped.push("alignment.relativeIndent");
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* `trot` for the model's rotation: 0–90 counter-clockwise as itself, and the model's -1…-90 (clockwise) as
|
|
472
|
+
* 91–180 — the same mapping the XLSX `textRotation` attribute uses.
|
|
473
|
+
*/
|
|
474
|
+
function textRotationValue(rotation) {
|
|
475
|
+
if (rotation === "vertical") {
|
|
476
|
+
return ROTATION_VERTICAL;
|
|
477
|
+
}
|
|
478
|
+
if (!Number.isInteger(rotation) || rotation < -90 || rotation > 90) {
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
return rotation >= 0 ? rotation : 90 - rotation;
|
|
482
|
+
}
|
|
483
|
+
/** One `XFProp`: a type, the size of the *whole* structure, then the blob. */
|
|
484
|
+
function xfProp(type, blob) {
|
|
485
|
+
return concatUint8Arrays([
|
|
486
|
+
new BinaryWriter()
|
|
487
|
+
.writeUint16(type)
|
|
488
|
+
.writeUint16(blob.length + 4)
|
|
489
|
+
.toUint8Array(),
|
|
490
|
+
blob
|
|
491
|
+
]);
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* An `XFPropColor` — MS-XLSB 2.5.161. Eight bytes: `fValidRGBA` and `xclrType` sharing a byte, `icv`, the tint,
|
|
495
|
+
* then `LongRGBA`.
|
|
496
|
+
*/
|
|
497
|
+
function xfPropColor(color) {
|
|
498
|
+
const writer = new BinaryWriter();
|
|
499
|
+
if (color?.theme !== undefined) {
|
|
500
|
+
writer.writeUint8(COLOR_TYPE.theme << 1).writeUint8(color.theme & 0xff);
|
|
501
|
+
}
|
|
502
|
+
else if (typeof color?.argb === "string") {
|
|
503
|
+
// Bit 0 is `fValidRGBA`: `dwRgba` holds the colour itself.
|
|
504
|
+
writer.writeUint8(0x01 | (COLOR_TYPE.rgb << 1)).writeUint8(0);
|
|
505
|
+
}
|
|
506
|
+
else if (color?.indexed !== undefined) {
|
|
507
|
+
// A palette colour. It used to be written "not set", so an indexed fill came back with no colour.
|
|
508
|
+
writer.writeUint8(COLOR_TYPE.indexed << 1).writeUint8(color.indexed & 0xff);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
writer.writeUint8(COLOR_TYPE.notSet << 1).writeUint8(0);
|
|
512
|
+
}
|
|
513
|
+
// `nTintShade` maps to -1.0…1.0 and MUST NOT be -32768; written unsigned, two's complement done here.
|
|
514
|
+
const tint = Math.max(-32767, Math.min(32767, Math.round((color?.tint ?? 0) * 32767)));
|
|
515
|
+
writer.writeUint16(tint < 0 ? tint + 0x10000 : tint);
|
|
516
|
+
const argb = typeof color?.argb === "string" ? color.argb : "00000000";
|
|
517
|
+
// `LongRGBA` is red, green, blue, alpha — *not* the ARGB order the string spells.
|
|
518
|
+
const bytes = argb.padStart(8, "0").slice(-8);
|
|
519
|
+
const at = (index) => Number.parseInt(bytes.slice(index, index + 2), 16) || 0;
|
|
520
|
+
writer.writeUint8(at(2)).writeUint8(at(4)).writeUint8(at(6)).writeUint8(at(0));
|
|
521
|
+
return writer.toUint8Array();
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* An `LPWideString`: a **two-byte** character count, then UTF-16 — MS-XLSB 2.5.92. Writing the count as one byte
|
|
525
|
+
* put the name a byte early, and Excel discarded the whole `DXFs` collection over it.
|
|
526
|
+
*/
|
|
527
|
+
function lpWideString(value) {
|
|
528
|
+
const writer = new BinaryWriter().writeUint16(value.length);
|
|
529
|
+
for (let index = 0; index < value.length; index++) {
|
|
530
|
+
writer.writeUint16(value.charCodeAt(index));
|
|
531
|
+
}
|
|
532
|
+
return writer.toUint8Array();
|
|
533
|
+
}
|
|
534
|
+
function u8(value) {
|
|
535
|
+
return new BinaryWriter().writeUint8(value).toUint8Array();
|
|
536
|
+
}
|
|
537
|
+
function i16(value) {
|
|
538
|
+
return u16(value < 0 ? value + 0x10000 : value);
|
|
539
|
+
}
|
|
540
|
+
function u16(value) {
|
|
541
|
+
return new BinaryWriter().writeUint16(value).toUint8Array();
|
|
542
|
+
}
|
|
543
|
+
function u32(value) {
|
|
544
|
+
return new BinaryWriter().writeUint32(value).toUint8Array();
|
|
545
|
+
}
|
|
546
|
+
// =============================================================================
|
|
547
|
+
// Reading
|
|
548
|
+
// =============================================================================
|
|
549
|
+
/**
|
|
550
|
+
* Decode a differential format.
|
|
551
|
+
*
|
|
552
|
+
* Walked by `cb` rather than by a fixed field order: the specification constrains which types may coexist, not
|
|
553
|
+
* their sequence, and `cb` covering the whole `XFProp` is what makes the walk possible at all.
|
|
554
|
+
*
|
|
555
|
+
* @param formatById - The format code an `ifmt` names: the styles part's own `BrtFmt`, else the built-in code.
|
|
556
|
+
*/
|
|
557
|
+
export function readDxf(payload, formatById) {
|
|
558
|
+
const unread = [];
|
|
559
|
+
if (payload.length < 6) {
|
|
560
|
+
return { style: undefined, unread };
|
|
561
|
+
}
|
|
562
|
+
const view = new DataView(payload.buffer, payload.byteOffset, payload.length);
|
|
563
|
+
const font = {};
|
|
564
|
+
const fill = {};
|
|
565
|
+
const stops = [];
|
|
566
|
+
const border = {};
|
|
567
|
+
const alignment = {};
|
|
568
|
+
const protection = {};
|
|
569
|
+
let numFmt;
|
|
570
|
+
const edge = (name) => (border[name] ?? (border[name] = {}));
|
|
571
|
+
// Two flag bytes, then `XFProps`: two reserved and the property count, which "MUST match the number of XFProp
|
|
572
|
+
// structures" — so a disagreement is reported, and the walk still reads what is actually there.
|
|
573
|
+
const declared = view.getUint16(4, true);
|
|
574
|
+
let walked = 0;
|
|
575
|
+
let offset = 6;
|
|
576
|
+
while (offset + 4 <= payload.length) {
|
|
577
|
+
const type = view.getUint16(offset, true);
|
|
578
|
+
const size = view.getUint16(offset + 2, true);
|
|
579
|
+
if (size < 4 || offset + size > payload.length) {
|
|
580
|
+
unread.push("truncated property");
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
walked++;
|
|
584
|
+
const at = offset + 4;
|
|
585
|
+
const minimum = DATA_SIZE[type];
|
|
586
|
+
if (minimum !== undefined && size - 4 < minimum) {
|
|
587
|
+
// Too short for its own type. Skipped by its `cb`, which is still trustworthy — it was bounds-checked above
|
|
588
|
+
// — so every property after it is read normally.
|
|
589
|
+
unread.push(`truncated property 0x${type.toString(16).padStart(2, "0")}`);
|
|
590
|
+
offset += size;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const byte = () => view.getUint8(at);
|
|
594
|
+
const bool = () => view.getUint8(at) === 1;
|
|
595
|
+
switch (type) {
|
|
596
|
+
case PROPERTY.fillPattern: {
|
|
597
|
+
const name = fillPatternName(byte());
|
|
598
|
+
if (name === undefined) {
|
|
599
|
+
unread.push("fill pattern");
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
fill.pattern = name;
|
|
603
|
+
}
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
case PROPERTY.fillForeground:
|
|
607
|
+
fill.fgColor = readColor(view, at);
|
|
608
|
+
break;
|
|
609
|
+
case PROPERTY.fillBackground:
|
|
610
|
+
fill.bgColor = readColor(view, at);
|
|
611
|
+
break;
|
|
612
|
+
case PROPERTY.gradient: {
|
|
613
|
+
const path = view.getInt32(at, true) === 1;
|
|
614
|
+
fill.gradient = path ? "path" : "angle";
|
|
615
|
+
if (path) {
|
|
616
|
+
const left = view.getFloat64(at + 12, true);
|
|
617
|
+
const right = view.getFloat64(at + 20, true);
|
|
618
|
+
const top = view.getFloat64(at + 28, true);
|
|
619
|
+
const bottom = view.getFloat64(at + 36, true);
|
|
620
|
+
fill.center = {
|
|
621
|
+
left,
|
|
622
|
+
top,
|
|
623
|
+
...(right === left ? {} : { right }),
|
|
624
|
+
...(bottom === top ? {} : { bottom })
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
fill.degree = view.getFloat64(at + 4, true);
|
|
629
|
+
}
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
case PROPERTY.gradientStop:
|
|
633
|
+
stops.push({ position: view.getFloat64(at + 2, true), color: readColor(view, at + 10) });
|
|
634
|
+
break;
|
|
635
|
+
case PROPERTY.fontColor:
|
|
636
|
+
font.color = readColor(view, at);
|
|
637
|
+
break;
|
|
638
|
+
case PROPERTY.borderTop:
|
|
639
|
+
case PROPERTY.borderBottom:
|
|
640
|
+
case PROPERTY.borderLeft:
|
|
641
|
+
case PROPERTY.borderRight:
|
|
642
|
+
case PROPERTY.borderDiagonal:
|
|
643
|
+
case PROPERTY.borderVertical:
|
|
644
|
+
case PROPERTY.borderHorizontal: {
|
|
645
|
+
const name = BORDER_EDGE.find(([candidate]) => candidate === type)[1];
|
|
646
|
+
const target = edge(name);
|
|
647
|
+
const color = readColor(view, at);
|
|
648
|
+
const style = borderStyleName(view.getUint16(at + 8, true));
|
|
649
|
+
if (style !== undefined) {
|
|
650
|
+
target.style = style;
|
|
651
|
+
}
|
|
652
|
+
if (Object.keys(color).length > 0) {
|
|
653
|
+
target.color = color;
|
|
654
|
+
}
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
case PROPERTY.diagonalUp:
|
|
658
|
+
edge("diagonal").up = bool();
|
|
659
|
+
break;
|
|
660
|
+
case PROPERTY.diagonalDown:
|
|
661
|
+
edge("diagonal").down = bool();
|
|
662
|
+
break;
|
|
663
|
+
case PROPERTY.horizontalAlignment: {
|
|
664
|
+
const value = HORIZONTAL[byte()];
|
|
665
|
+
if (value === undefined) {
|
|
666
|
+
unread.push("horizontal alignment");
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
alignment.horizontal = value;
|
|
670
|
+
}
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
case PROPERTY.verticalAlignment: {
|
|
674
|
+
const value = VERTICAL[byte()];
|
|
675
|
+
if (value === undefined) {
|
|
676
|
+
unread.push("vertical alignment");
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
alignment.vertical = value;
|
|
680
|
+
}
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
case PROPERTY.textRotation: {
|
|
684
|
+
const trot = byte();
|
|
685
|
+
if (trot === ROTATION_VERTICAL) {
|
|
686
|
+
alignment.textRotation = "vertical";
|
|
687
|
+
}
|
|
688
|
+
else if (trot <= 90) {
|
|
689
|
+
alignment.textRotation = trot;
|
|
690
|
+
}
|
|
691
|
+
else if (trot <= 180) {
|
|
692
|
+
alignment.textRotation = 90 - trot;
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
// 254 is "context dependent", which the model has no word for.
|
|
696
|
+
unread.push("text rotation");
|
|
697
|
+
}
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
case PROPERTY.indent:
|
|
701
|
+
alignment.indent = view.getUint16(at, true);
|
|
702
|
+
break;
|
|
703
|
+
case PROPERTY.readingOrder: {
|
|
704
|
+
const value = READING_ORDER[byte()];
|
|
705
|
+
if (value === undefined) {
|
|
706
|
+
unread.push("reading order");
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
alignment.readingOrder = value;
|
|
710
|
+
}
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
case PROPERTY.wrapText:
|
|
714
|
+
alignment.wrapText = bool();
|
|
715
|
+
break;
|
|
716
|
+
case PROPERTY.shrinkToFit:
|
|
717
|
+
alignment.shrinkToFit = bool();
|
|
718
|
+
break;
|
|
719
|
+
case PROPERTY.justifyLastLine:
|
|
720
|
+
alignment.justifyLastLine = bool();
|
|
721
|
+
break;
|
|
722
|
+
case PROPERTY.mergeCell:
|
|
723
|
+
// Merging is a property of a *range* in the model (`Worksheet.mergeCells`), not of a format — and a
|
|
724
|
+
// differential format cannot merge anything: a conditional format or table style that "merges" is not
|
|
725
|
+
// a thing Excel offers. Excel never writes it here; a producer that does is reported, not obeyed.
|
|
726
|
+
unread.push("merge cell");
|
|
727
|
+
break;
|
|
728
|
+
case PROPERTY.fontName:
|
|
729
|
+
{
|
|
730
|
+
const name = readLpWideString(view, at, offset + size);
|
|
731
|
+
if (name === undefined) {
|
|
732
|
+
unread.push("truncated property 0x18");
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
font.name = name;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
break;
|
|
739
|
+
case PROPERTY.fontWeight: {
|
|
740
|
+
// Two values only. Anything else is neither bold nor normal, so it is not reported as either.
|
|
741
|
+
const weight = view.getUint16(at, true);
|
|
742
|
+
if (weight === WEIGHT_BOLD || weight === WEIGHT_NORMAL) {
|
|
743
|
+
font.bold = weight === WEIGHT_BOLD;
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
unread.push("font weight");
|
|
747
|
+
}
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
case PROPERTY.underline: {
|
|
751
|
+
const name = nameOf(UNDERLINE, view.getUint16(at, true));
|
|
752
|
+
if (name === undefined) {
|
|
753
|
+
unread.push("underline");
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
// `true` for a single underline and `false` for none: what the XLSX reader produces for `<u/>` and
|
|
757
|
+
// `<u val="none"/>`, so the two containers agree about the same format.
|
|
758
|
+
font.underline = name === "single" ? true : name === "none" ? false : name;
|
|
759
|
+
}
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
case PROPERTY.script: {
|
|
763
|
+
const name = nameOf(VERTICAL_ALIGN, view.getUint16(at, true));
|
|
764
|
+
if (name === undefined) {
|
|
765
|
+
unread.push("font script");
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
// `SSSNONE` is a stated baseline: it resets a super- or subscript.
|
|
769
|
+
font.vertAlign = name === "none" ? "baseline" : name;
|
|
770
|
+
}
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
case PROPERTY.italic:
|
|
774
|
+
font.italic = bool();
|
|
775
|
+
break;
|
|
776
|
+
case PROPERTY.strike:
|
|
777
|
+
font.strike = bool();
|
|
778
|
+
break;
|
|
779
|
+
case PROPERTY.outline:
|
|
780
|
+
font.outline = bool();
|
|
781
|
+
break;
|
|
782
|
+
case PROPERTY.shadow:
|
|
783
|
+
font.shadow = bool();
|
|
784
|
+
break;
|
|
785
|
+
case PROPERTY.condense:
|
|
786
|
+
font.condense = bool();
|
|
787
|
+
break;
|
|
788
|
+
case PROPERTY.extend:
|
|
789
|
+
font.extend = bool();
|
|
790
|
+
break;
|
|
791
|
+
case PROPERTY.charset:
|
|
792
|
+
font.charset = byte();
|
|
793
|
+
break;
|
|
794
|
+
case PROPERTY.fontFamily:
|
|
795
|
+
font.family = byte();
|
|
796
|
+
break;
|
|
797
|
+
case PROPERTY.fontSize:
|
|
798
|
+
font.size = view.getUint32(at, true) / TWIPS_PER_POINT;
|
|
799
|
+
break;
|
|
800
|
+
case PROPERTY.fontScheme: {
|
|
801
|
+
const name = nameOf(SCHEME, byte());
|
|
802
|
+
if (name === undefined) {
|
|
803
|
+
unread.push("font scheme");
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
font.scheme = name;
|
|
807
|
+
}
|
|
808
|
+
break;
|
|
809
|
+
}
|
|
810
|
+
case PROPERTY.numberFormatId: {
|
|
811
|
+
const code = formatById(view.getUint16(at, true));
|
|
812
|
+
if (code === undefined) {
|
|
813
|
+
unread.push("number format");
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
numFmt = code;
|
|
817
|
+
}
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
case PROPERTY.numberFormatString:
|
|
821
|
+
// The format code itself, in a structure MS-XLSB defers to MS-XLS for. Excel pairs it with an `ifmt` (0x29),
|
|
822
|
+
// which is read above, so it is only reported when there is no identifier to read instead — see below.
|
|
823
|
+
break;
|
|
824
|
+
case PROPERTY.relativeIndent: {
|
|
825
|
+
const value = view.getInt16(at, true);
|
|
826
|
+
if (value !== NO_RELATIVE_INDENT) {
|
|
827
|
+
alignment.relativeIndent = value;
|
|
828
|
+
}
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
case PROPERTY.locked:
|
|
832
|
+
protection.locked = bool();
|
|
833
|
+
break;
|
|
834
|
+
case PROPERTY.hidden:
|
|
835
|
+
protection.hidden = bool();
|
|
836
|
+
break;
|
|
837
|
+
default:
|
|
838
|
+
unread.push(`property 0x${type.toString(16).padStart(2, "0")}`);
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
offset += size;
|
|
842
|
+
}
|
|
843
|
+
if (walked !== declared) {
|
|
844
|
+
unread.push(`property count: ${declared} declared, ${walked} present`);
|
|
845
|
+
}
|
|
846
|
+
if (numFmt === undefined && hasProperty(view, payload.length, PROPERTY.numberFormatString)) {
|
|
847
|
+
unread.push("number format");
|
|
848
|
+
}
|
|
849
|
+
const style = {};
|
|
850
|
+
if (Object.keys(font).length > 0) {
|
|
851
|
+
style.font = font;
|
|
852
|
+
}
|
|
853
|
+
if (fill.gradient !== undefined) {
|
|
854
|
+
style.fill = { type: "gradient", ...fill, stops };
|
|
855
|
+
}
|
|
856
|
+
else if (Object.keys(fill).length > 0) {
|
|
857
|
+
// No pattern stays no pattern: the differential "keep the cell's pattern", as the XLSX reader models it.
|
|
858
|
+
style.fill = { type: "pattern", ...fill };
|
|
859
|
+
}
|
|
860
|
+
if (Object.keys(border).length > 0) {
|
|
861
|
+
style.border = border;
|
|
862
|
+
}
|
|
863
|
+
if (Object.keys(alignment).length > 0) {
|
|
864
|
+
style.alignment = alignment;
|
|
865
|
+
}
|
|
866
|
+
if (Object.keys(protection).length > 0) {
|
|
867
|
+
style.protection = protection;
|
|
868
|
+
}
|
|
869
|
+
if (numFmt !== undefined) {
|
|
870
|
+
style.numFmt = numFmt;
|
|
871
|
+
}
|
|
872
|
+
return { style: Object.keys(style).length === 0 ? undefined : style, unread };
|
|
873
|
+
}
|
|
874
|
+
function hasProperty(view, length, wanted) {
|
|
875
|
+
for (let offset = 6; offset + 4 <= length;) {
|
|
876
|
+
const size = view.getUint16(offset + 2, true);
|
|
877
|
+
if (view.getUint16(offset, true) === wanted) {
|
|
878
|
+
return true;
|
|
879
|
+
}
|
|
880
|
+
if (size < 4) {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
offset += size;
|
|
884
|
+
}
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
/** An `XFPropColor`, in the model's shape. Automatic and "not set" are both the absence of a colour. */
|
|
888
|
+
function readColor(view, offset) {
|
|
889
|
+
const kind = view.getUint8(offset) >> 1;
|
|
890
|
+
const icv = view.getUint8(offset + 1);
|
|
891
|
+
const tint = view.getInt16(offset + 2, true);
|
|
892
|
+
const color = {};
|
|
893
|
+
if (kind === COLOR_TYPE.theme) {
|
|
894
|
+
color.theme = icv;
|
|
895
|
+
}
|
|
896
|
+
else if (kind === COLOR_TYPE.rgb) {
|
|
897
|
+
const hex = (value) => value.toString(16).padStart(2, "0").toUpperCase();
|
|
898
|
+
color.argb =
|
|
899
|
+
hex(view.getUint8(offset + 7)) +
|
|
900
|
+
hex(view.getUint8(offset + 4)) +
|
|
901
|
+
hex(view.getUint8(offset + 5)) +
|
|
902
|
+
hex(view.getUint8(offset + 6));
|
|
903
|
+
}
|
|
904
|
+
else if (kind === COLOR_TYPE.indexed) {
|
|
905
|
+
color.indexed = icv;
|
|
906
|
+
}
|
|
907
|
+
if (tint !== 0) {
|
|
908
|
+
color.tint = tint / 32767;
|
|
909
|
+
}
|
|
910
|
+
return color;
|
|
911
|
+
}
|
|
912
|
+
/** An `LPWideString`: a two-byte character count, then UTF-16 code units. */
|
|
913
|
+
/**
|
|
914
|
+
* An `LPWideString`: a two-byte character count, then UTF-16 code units — bounded by `end`, the end of the property
|
|
915
|
+
* it sits in, not of the record. `undefined` when the count claims more characters than the property holds: reading
|
|
916
|
+
* on took the name's tail from the next property.
|
|
917
|
+
*/
|
|
918
|
+
function readLpWideString(view, offset, end) {
|
|
919
|
+
const length = view.getUint16(offset, true);
|
|
920
|
+
if (offset + 2 + length * 2 > end) {
|
|
921
|
+
return undefined;
|
|
922
|
+
}
|
|
923
|
+
let text = "";
|
|
924
|
+
for (let index = 0; index < length; index++) {
|
|
925
|
+
text += String.fromCharCode(view.getUint16(offset + 2 + index * 2, true));
|
|
926
|
+
}
|
|
927
|
+
return text;
|
|
928
|
+
}
|