reamkit 1.15.3 → 1.16.0

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.
Files changed (95) hide show
  1. package/dist/esm/core/converter/facade.d.ts +5 -0
  2. package/dist/esm/core/converter/ream.d.ts +6 -0
  3. package/dist/esm/core/converter/ream.js +34 -8
  4. package/dist/esm/core/document-model/index.d.ts +1 -1
  5. package/dist/esm/core/document-model/types.d.ts +187 -1
  6. package/dist/esm/core/drawingml/chart-geometry.d.ts +28 -0
  7. package/dist/esm/core/drawingml/chart-geometry.js +371 -88
  8. package/dist/esm/core/drawingml/chart-parser.js +262 -11
  9. package/dist/esm/core/drawingml/colors.d.ts +6 -6
  10. package/dist/esm/core/drawingml/colors.js +58 -18
  11. package/dist/esm/core/drawingml/preset-geometry.js +7 -6
  12. package/dist/esm/core/drawingml/sparkline-geometry.js +6 -4
  13. package/dist/esm/core/drawingml/theme-parser.d.ts +41 -0
  14. package/dist/esm/core/drawingml/theme-parser.js +74 -1
  15. package/dist/esm/core/font/ttf-subset.js +26 -1
  16. package/dist/esm/core/images.js +172 -43
  17. package/dist/esm/core/indexed-colors.d.ts +11 -0
  18. package/dist/esm/core/indexed-colors.js +81 -0
  19. package/dist/esm/core/ir/sheet.d.ts +67 -3
  20. package/dist/esm/{excel → core}/number-format.d.ts +37 -0
  21. package/dist/esm/core/number-format.js +800 -0
  22. package/dist/esm/core/opc/alternate-content.d.ts +13 -0
  23. package/dist/esm/core/opc/alternate-content.js +72 -0
  24. package/dist/esm/core/opc/core-properties.js +1 -0
  25. package/dist/esm/core/opc/package.d.ts +14 -1
  26. package/dist/esm/core/opc/package.js +43 -4
  27. package/dist/esm/core/opc/relationships.js +1 -0
  28. package/dist/esm/core/opc/xml-entities.d.ts +12 -0
  29. package/dist/esm/core/opc/xml-entities.js +91 -0
  30. package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
  31. package/dist/esm/core/spreadsheet-model/types.d.ts +137 -0
  32. package/dist/esm/core/vector.d.ts +13 -0
  33. package/dist/esm/excel/activex-parser.d.ts +9 -0
  34. package/dist/esm/excel/activex-parser.js +16 -1
  35. package/dist/esm/excel/column-bands.d.ts +26 -1
  36. package/dist/esm/excel/column-bands.js +156 -20
  37. package/dist/esm/excel/comments-parser.js +5 -3
  38. package/dist/esm/excel/conditional-format.d.ts +25 -2
  39. package/dist/esm/excel/conditional-format.js +63 -19
  40. package/dist/esm/excel/escaped-text.d.ts +9 -0
  41. package/dist/esm/excel/escaped-text.js +20 -0
  42. package/dist/esm/excel/form-control-parser.js +1 -0
  43. package/dist/esm/excel/formula/dates.js +1 -1
  44. package/dist/esm/excel/header-footer.d.ts +9 -1
  45. package/dist/esm/excel/header-footer.js +162 -14
  46. package/dist/esm/excel/index.d.ts +1 -1
  47. package/dist/esm/excel/index.js +2 -2
  48. package/dist/esm/excel/pivot-table-parser.js +1 -0
  49. package/dist/esm/excel/print-model.d.ts +75 -0
  50. package/dist/esm/excel/print-model.js +902 -77
  51. package/dist/esm/excel/printer-settings.d.ts +16 -0
  52. package/dist/esm/excel/printer-settings.js +36 -0
  53. package/dist/esm/excel/rich-value.d.ts +13 -0
  54. package/dist/esm/excel/rich-value.js +120 -0
  55. package/dist/esm/excel/shared-strings-parser.js +11 -7
  56. package/dist/esm/excel/sheet-drawing.d.ts +11 -0
  57. package/dist/esm/excel/sheet-drawing.js +26 -3
  58. package/dist/esm/excel/sheet-shape-parser.d.ts +53 -1
  59. package/dist/esm/excel/sheet-shape-parser.js +461 -32
  60. package/dist/esm/excel/sheet-to-flow.d.ts +23 -2
  61. package/dist/esm/excel/sheet-to-flow.js +551 -36
  62. package/dist/esm/excel/slicer-parser.js +1 -0
  63. package/dist/esm/excel/styles-parser.d.ts +30 -2
  64. package/dist/esm/excel/styles-parser.js +177 -40
  65. package/dist/esm/excel/table-parser.js +1 -0
  66. package/dist/esm/excel/vml-drawing.d.ts +68 -0
  67. package/dist/esm/excel/vml-drawing.js +158 -0
  68. package/dist/esm/excel/workbook-parser.d.ts +5 -0
  69. package/dist/esm/excel/workbook-parser.js +5 -1
  70. package/dist/esm/excel/worksheet-parser.d.ts +2 -1
  71. package/dist/esm/excel/worksheet-parser.js +149 -28
  72. package/dist/esm/excel/xlsx-reader.js +271 -33
  73. package/dist/esm/excel/xlsx-to-pdf.js +12 -1
  74. package/dist/esm/html/html-writer.js +11 -2
  75. package/dist/esm/layout/page-doc.d.ts +26 -0
  76. package/dist/esm/layout/styled-layout.d.ts +2 -0
  77. package/dist/esm/layout/styled-layout.js +493 -64
  78. package/dist/esm/pdf/cid-font.js +20 -9
  79. package/dist/esm/pdf/objects.d.ts +13 -0
  80. package/dist/esm/pdf/objects.js +18 -1
  81. package/dist/esm/pdf/styled-page-emitter.js +164 -32
  82. package/dist/esm/pdf/text-page-renderer.js +1 -0
  83. package/dist/esm/pdf/vector-graphics.d.ts +3 -1
  84. package/dist/esm/pdf/vector-graphics.js +19 -2
  85. package/dist/esm/pptx/pptx-reader.js +1 -0
  86. package/dist/esm/pptx/slide-parser.js +12 -1
  87. package/dist/esm/svg/svg-writer.js +23 -4
  88. package/dist/esm/word/document-parser.js +1 -0
  89. package/dist/esm/word/drawing-parser.d.ts +34 -1
  90. package/dist/esm/word/drawing-parser.js +52 -2
  91. package/dist/esm/word/numbering-parser.js +1 -0
  92. package/dist/esm/word/settings-parser.js +1 -0
  93. package/dist/esm/word/styles-parser.js +1 -0
  94. package/package.json +7 -3
  95. package/dist/esm/excel/number-format.js +0 -476
@@ -1,7 +1,7 @@
1
1
  import { pt } from "../core/ir/units.js";
2
2
  import { EMPTY_STYLE_SHEET, resolveBodyStyles } from "../core/style-cascade/resolver.js";
3
3
  import "../core/style-cascade/index.js";
4
- import { resolvePrintArea, resolvePrintTitleRows, sectionFromWorksheet, slicerTable, worksheetToBody } from "./print-model.js";
4
+ import { cellPaintsVisibly, printableHeightPt, printableWidthPt, resolvePrintArea, resolvePrintTitleRows, sectionFromWorksheet, slicerTable, worksheetToBody } from "./print-model.js";
5
5
  import { buildHeaderFooterContent } from "./header-footer.js";
6
6
  //#region src/excel/sheet-to-flow.ts
7
7
  var HEADER_REL = "_xlsxHeaderDefault";
@@ -9,8 +9,8 @@ var FOOTER_REL = "_xlsxFooterDefault";
9
9
  /**
10
10
  * Project a {@link SheetDoc} into a {@link FlowDoc} (E-SHEET SA2): each grid sheet
11
11
  * becomes flow blocks (a table + chart/picture/shape/slicer frames, then comment
12
- * / control listings), with sheets after the first starting on a new page. The
13
- * first sheet's page geometry + header/footer drive the document section.
12
+ * / control listings), with sheets after the first starting on a new page. Each
13
+ * sheet gets its own section — its page geometry and its own header/footer.
14
14
  *
15
15
  * @param sheet The SpreadsheetML IR tree.
16
16
  * @param options Projection knobs (the W9 reference date).
@@ -23,64 +23,117 @@ function projectSheetDoc(sheet, options = {}) {
23
23
  let firstSheetSection;
24
24
  const headersFooters = /* @__PURE__ */ new Map();
25
25
  const sheetGrids = new Map(sheet.sheets.map((s) => [s.name, s.grid]));
26
+ let printed = 0;
27
+ let printableSheets = sheet.sheets.filter((s) => !s.hidden).length;
28
+ const visible = sheet.sheets.filter((s) => !s.hidden);
29
+ const fallbackSheet = visible.some((s) => sheetPrintsAnything(s, sheet.styles)) ? void 0 : visible.find((s) => hasHeaderOrFooter(s)) ?? visible[0];
26
30
  for (let sheetIdx = 0; sheetIdx < sheet.sheets.length; sheetIdx++) {
27
31
  const ws = sheet.sheets[sheetIdx];
28
- const sheetSection = sheetIdx === 0 ? withHeaderFooter(sectionFromWorksheet(ws.grid), ws, headersFooters) : sectionFromWorksheet(ws.grid);
29
- if (sheetIdx === 0) firstSheetSection = sheetSection;
30
- sheetSections.push(sheetSection);
31
- if (sheetIdx > 0) body.push({
32
- kind: "paragraph",
33
- paragraph: {
34
- properties: { pageBreakBefore: true },
35
- runs: []
36
- }
37
- });
32
+ if (ws.hidden) continue;
33
+ if (!sheetPrintsAnything(ws, sheet.styles) && ws !== fallbackSheet) {
34
+ printableSheets--;
35
+ continue;
36
+ }
37
+ const scaleSink = { value: 1 };
38
+ const bandSink = { lefts: [0] };
39
+ const drawingExtentPt = drawingReachPt(ws);
38
40
  const printArea = resolvePrintArea(sheet.definedNames, sheetIdx);
39
41
  const titleRows = resolvePrintTitleRows(sheet.definedNames, sheetIdx);
40
- const gridLines = ws.grid.printOptions?.gridLines === true;
41
- body.push(...worksheetToBody(ws.grid, sheet.sharedStrings, sheet.styles, sheet.date1904, {
42
+ const gridBody = worksheetToBody(ws.grid, sheet.sharedStrings, sheet.styles, sheet.date1904, {
42
43
  ...printArea ? { printArea } : {},
43
44
  ...titleRows ? { titleRows } : {},
44
- gridLines,
45
+ gridLines: ws.grid.printOptions?.gridLines === true,
45
46
  sheetGrids,
46
47
  sheetName: ws.name,
47
48
  definedNames: sheet.definedNames,
48
49
  ...ws.hyperlinks ? { hyperlinks: ws.hyperlinks } : {},
49
50
  ...sheet.sharedStringRuns ? { sharedStringRuns: sheet.sharedStringRuns } : {},
50
51
  ...options.now ? { now: options.now } : {},
51
- ...options.losses ? { losses: options.losses } : {}
52
- }));
53
- for (const ref of ws.charts ?? []) body.push({
52
+ ...options.digitWidthPt !== void 0 ? { digitWidthPt: options.digitWidthPt } : {},
53
+ ...sheet.styles.fonts[0]?.sizePt !== void 0 ? { defaultFontPt: sheet.styles.fonts[0].sizePt } : {},
54
+ ...options.losses ? { losses: options.losses } : {},
55
+ scaleSink,
56
+ bandSink,
57
+ ...drawingExtentPt ? { drawingExtentPt } : {}
58
+ });
59
+ const sheetSection = withHeaderFooter(sectionFromWorksheet(ws.grid), ws, headersFooters, scaleSink.value, sheet.styles.fonts[0]?.sizePt, printed, options.fileName, sheet.themePalette, options.now);
60
+ if (printed === 0) firstSheetSection = sheetSection;
61
+ sheetSections.push(sheetSection);
62
+ if (printed > 0) body.push({
63
+ kind: "paragraph",
64
+ paragraph: {
65
+ properties: { pageBreakBefore: true },
66
+ runs: []
67
+ }
68
+ });
69
+ const drawings = [];
70
+ for (const ref of ws.charts ?? []) drawings.push({
54
71
  kind: "chart",
55
72
  chart: {
73
+ ...anchorFloat(ref.xPt, ref.yPt, scaleSink.value),
56
74
  chartRelId: ref.chartPartPath,
57
- width: pt(ref.widthPt),
58
- height: pt(ref.heightPt),
75
+ width: pt(ref.widthPt * scaleSink.value),
76
+ height: pt(ref.heightPt * scaleSink.value),
59
77
  paragraphProperties: {}
60
78
  }
61
79
  });
62
- for (const img of ws.images ?? []) body.push({
80
+ for (const img of ws.images ?? []) drawings.push({
63
81
  kind: "image",
64
82
  image: {
83
+ ...anchorFloat(img.xPt, img.yPt, scaleSink.value),
65
84
  resource: img.resourceId,
66
- width: pt(img.widthPt),
67
- height: pt(img.heightPt),
85
+ width: pt(img.widthPt * scaleSink.value),
86
+ height: pt(img.heightPt * scaleSink.value),
68
87
  paragraphProperties: {}
69
88
  }
70
89
  });
71
- for (const shape of ws.shapes ?? []) body.push({
90
+ for (const shape of ws.shapes ?? []) drawings.push({
72
91
  kind: "shape",
73
- shape
92
+ shape: centreShape(scaleShape(shape, scaleSink.value), ws.grid, drawingExtentPt, scaleSink.value)
74
93
  });
94
+ const controlBlocks = controlShapeBlocks(ws.formControls, ws.activeXControls);
95
+ const drawingBands = gridBody.length === 0 ? bandDrawings(controlBlocks, printableHeightPt(ws.grid), scaleSink.value, DOWN).flatMap((row) => bandDrawings(row, printableWidthPt(ws.grid), scaleSink.value)) : [controlBlocks];
96
+ for (let band = 0; band < drawingBands.length; band++) {
97
+ if (band > 0) drawings.push({
98
+ kind: "paragraph",
99
+ paragraph: {
100
+ properties: { pageBreakBefore: true },
101
+ runs: []
102
+ }
103
+ });
104
+ for (const shape of drawingBands[band]) drawings.push({
105
+ kind: "shape",
106
+ shape: centreShape(scaleShape(shape, scaleSink.value), ws.grid, drawingExtentPt, scaleSink.value)
107
+ });
108
+ }
109
+ body.push(...withDrawingsByBand(drawings, gridBody, bandSink.lefts));
75
110
  for (const slicer of ws.slicers ?? []) body.push({
76
111
  kind: "table",
77
112
  table: slicerTable(slicer)
78
113
  });
79
- if (ws.comments && ws.comments.length > 0) body.push(...commentBlocks(ws.comments));
114
+ const printComments = ws.grid.pageSetup?.cellComments === "atEnd" || ws.grid.pageSetup?.cellComments === "asDisplayed";
115
+ if (ws.comments && ws.comments.length > 0) if (printComments) body.push(...commentBlocks(ws.comments));
116
+ else options.losses?.push({
117
+ severity: "dropped",
118
+ feature: "comments",
119
+ detail: `${ws.comments.length} cell note(s) not printed — <pageSetup cellComments> is "${ws.grid.pageSetup?.cellComments ?? "none"}"`,
120
+ where: `sheet "${ws.name}"`
121
+ });
80
122
  if (ws.formControls && ws.formControls.length > 0) body.push(...formControlBlocks(ws.formControls));
81
123
  if (ws.activeXControls && ws.activeXControls.length > 0) body.push(...activeXBlocks(ws.activeXControls));
82
124
  sheetEnds.push(body.length);
125
+ printed++;
83
126
  }
127
+ if (sheet.embeddedObjects) options.losses?.push({
128
+ severity: "dropped",
129
+ feature: "shapes",
130
+ detail: `${String(sheet.embeddedObjects)} embedded object(s) not rendered — an OLE embedding draws as an icon from a metafile preview`
131
+ });
132
+ if (sheet.metafilePictures) options.losses?.push({
133
+ severity: "dropped",
134
+ feature: "images",
135
+ detail: `${String(sheet.metafilePictures)} picture(s) not rendered — a WMF/EMF/PICT metafile is replayed, not embedded; the anchor keeps its space`
136
+ });
84
137
  const sections = sheetSections.length > 1 ? sheetSections.map((properties, i) => ({
85
138
  properties,
86
139
  endIndex: sheetEnds[i]
@@ -127,7 +180,364 @@ function commentBlocks(comments) {
127
180
  }
128
181
  return out;
129
182
  }
183
+ var RADIO_DIAMETER_PT = 8.4;
184
+ var RADIO_RING_PT = .6;
185
+ var RADIO_DOT_INSET_PT = 1.2;
186
+ var CHECKBOX_SIDE_PT = 8.4;
187
+ var CAPTION_GUTTER_PT = 11.5;
188
+ var CONTROL_LINE_PT = .75;
189
+ var GROUP_BOX_GREY = "808080";
190
+ var DEFAULT_FORM_CONTROL_PT = 8;
191
+ var DEFAULT_ACTIVEX_CONTROL_PT = 10;
192
+ function formControlGlyph(objectType) {
193
+ switch ((objectType ?? "").toLowerCase()) {
194
+ case "radio": return "radio";
195
+ case "checkbox": return "checkbox";
196
+ case "gbox": return "group";
197
+ case "button":
198
+ case "buttons": return "button";
199
+ case "label": return "label";
200
+ default: return "frame";
201
+ }
202
+ }
203
+ function activeXGlyph(type) {
204
+ switch (type) {
205
+ case "option": return "radio";
206
+ case "checkbox": return "checkbox";
207
+ case "button":
208
+ case "toggle": return "button";
209
+ case "label": return "label";
210
+ default: return "frame";
211
+ }
212
+ }
213
+ /** A floating shape at `box`, sized and positioned in sheet points. */
214
+ function controlShape(box, dx, dy, widthPt, heightPt, rest) {
215
+ return {
216
+ float: {
217
+ wrap: "none",
218
+ posH: {
219
+ relativeFrom: "margin",
220
+ offsetPt: pt(box.xPt + dx)
221
+ },
222
+ posV: {
223
+ relativeFrom: "margin",
224
+ offsetPt: pt(box.yPt + dy)
225
+ }
226
+ },
227
+ width: pt(widthPt),
228
+ height: pt(heightPt),
229
+ ...rest,
230
+ paragraphProperties: {}
231
+ };
232
+ }
233
+ /**
234
+ * A control drawn where the document puts it (E-SHEET W8/W10), as the widget it
235
+ * is: an option button's ring and dot, a check box, a group box's frame, a
236
+ * push button's outline — each with its caption beside or inside it.
237
+ *
238
+ * Listing the captions after the grid instead was a stand-in for having no
239
+ * geometry to place them with. It read as a paragraph of ASCII where
240
+ * tdf111980_radioButtons.xlsx has eleven widgets spread across the sheet, the
241
+ * furthest of them 18cm from where we drew it.
242
+ */
243
+ function controlShapes(box, glyph, caption, checked, fontSizePt) {
244
+ const out = [];
245
+ const noFill = { kind: "none" };
246
+ const rect = {
247
+ kind: "preset",
248
+ preset: "rect"
249
+ };
250
+ const ellipse = {
251
+ kind: "preset",
252
+ preset: "ellipse"
253
+ };
254
+ const grey = {
255
+ colorHex: GROUP_BOX_GREY,
256
+ width: pt(CONTROL_LINE_PT)
257
+ };
258
+ const black = {
259
+ colorHex: "000000",
260
+ width: pt(CONTROL_LINE_PT)
261
+ };
262
+ if (glyph === "radio" || glyph === "checkbox") {
263
+ const side = glyph === "radio" ? RADIO_DIAMETER_PT : CHECKBOX_SIDE_PT;
264
+ const top = Math.max(0, (box.heightPt - side) / 2);
265
+ out.push(controlShape(box, 0, top, side, side, {
266
+ geometry: glyph === "radio" ? ellipse : rect,
267
+ fill: {
268
+ kind: "solid",
269
+ colorHex: "FFFFFF"
270
+ },
271
+ line: {
272
+ colorHex: "000000",
273
+ width: pt(RADIO_RING_PT)
274
+ }
275
+ }));
276
+ if (checked) {
277
+ const mark = side - 2 * RADIO_DOT_INSET_PT;
278
+ if (glyph === "radio") out.push(controlShape(box, RADIO_DOT_INSET_PT, top + RADIO_DOT_INSET_PT, mark, mark, {
279
+ geometry: ellipse,
280
+ fill: {
281
+ kind: "solid",
282
+ colorHex: "000000"
283
+ }
284
+ }));
285
+ else for (const flipV of [false, true]) out.push(controlShape(box, RADIO_DOT_INSET_PT, top + RADIO_DOT_INSET_PT, mark, mark, {
286
+ geometry: {
287
+ kind: "preset",
288
+ preset: "line"
289
+ },
290
+ fill: noFill,
291
+ line: {
292
+ colorHex: "000000",
293
+ width: pt(RADIO_RING_PT)
294
+ },
295
+ ...flipV ? { transform: { flipV: true } } : {}
296
+ }));
297
+ }
298
+ } else if (glyph !== "label") out.push(controlShape(box, 0, 0, box.widthPt, box.heightPt, {
299
+ geometry: rect,
300
+ fill: noFill,
301
+ line: glyph === "group" ? grey : black
302
+ }));
303
+ if (caption !== void 0 && caption.length > 0) {
304
+ const inset = glyph === "radio" || glyph === "checkbox" ? CAPTION_GUTTER_PT : 3;
305
+ const runs = [{
306
+ text: caption,
307
+ properties: { fontSizePt: pt(fontSizePt) }
308
+ }];
309
+ out.push(controlShape(box, 0, 0, box.widthPt, box.heightPt, {
310
+ geometry: rect,
311
+ fill: noFill,
312
+ text: {
313
+ content: [{
314
+ kind: "paragraph",
315
+ paragraph: {
316
+ properties: glyph === "button" ? { alignment: "center" } : {},
317
+ runs
318
+ }
319
+ }],
320
+ insetLeft: pt(inset),
321
+ insetTop: pt(1),
322
+ insetRight: pt(1),
323
+ insetBottom: pt(1),
324
+ anchor: glyph === "group" ? "t" : "ctr"
325
+ }
326
+ }));
327
+ }
328
+ return out;
329
+ }
330
+ /**
331
+ * Split floating drawings into page-wide bands, left to right — the "down, then
332
+ * over" order a wide sheet prints in (§18.3.1.63 `pageOrder`), applied to the
333
+ * drawing layer when there is no grid whose column bands it could follow.
334
+ *
335
+ * Each band is shifted back to the page's left margin, so band 2 starts where
336
+ * the printable width ran out. A drawing straddling the boundary belongs to the
337
+ * band its left edge is in, which is where the reference draws it too — the
338
+ * part past the edge is clipped on that page and continues on the next.
339
+ */
340
+ /**
341
+ * Whether a sheet has anything to put on a page.
342
+ *
343
+ * A cell that carries a value or inline text, any drawing, control or slicer,
344
+ * or a printed note. A tab nobody touched has none of those, and Excel and
345
+ * LibreOffice both leave it out of the print — a header or footer does not
346
+ * rescue it.
347
+ *
348
+ * @param ws The sheet.
349
+ * @returns True when the sheet would print something.
350
+ */
351
+ /**
352
+ * Whether a sheet carries a header or footer with anything in it.
353
+ *
354
+ * @param ws The sheet.
355
+ * @returns True when either band has a non-empty format string.
356
+ */
357
+ function hasHeaderOrFooter(ws) {
358
+ const hf = ws.grid.headerFooter;
359
+ return Boolean(hf && ((hf.oddHeader?.length ?? 0) > 0 || (hf.oddFooter?.length ?? 0) > 0));
360
+ }
361
+ function sheetPrintsAnything(ws, styles) {
362
+ if (ws.grid.cells.some((c) => c.rawValue !== "" || c.inlineText !== void 0)) return true;
363
+ if (ws.grid.cells.some((c) => cellPaintsVisibly(c, styles))) return true;
364
+ if ((ws.charts?.length ?? 0) > 0) return true;
365
+ if ((ws.images?.length ?? 0) > 0) return true;
366
+ if ((ws.shapes?.length ?? 0) > 0) return true;
367
+ if ((ws.slicers?.length ?? 0) > 0) return true;
368
+ if ((ws.comments?.length ?? 0) > 0) return true;
369
+ if ((ws.formControls?.length ?? 0) > 0) return true;
370
+ if ((ws.activeXControls?.length ?? 0) > 0) return true;
371
+ return false;
372
+ }
373
+ /**
374
+ * Put each drawing beside the column band it is anchored in.
375
+ *
376
+ * A wide sheet's grid prints as one table per band, each on its own page, and a
377
+ * float lands on whatever page the layout has reached when it meets the block.
378
+ * Emitted ahead of the whole grid, a drawing anchored in the second band was
379
+ * printed on the FIRST page at an offset a page and a half wide — off the paper
380
+ * entirely. shape-macro-ext-ref.xlsx anchors its chart and its macro button at
381
+ * x=838pt and x=891pt of a 480pt page and lost both.
382
+ *
383
+ * Each drawing goes in front of its band's table with its horizontal anchor
384
+ * measured from that band's left edge instead of the sheet's.
385
+ *
386
+ * @param drawings The sheet's floats, in the order they were projected.
387
+ * @param gridBody The grid's body elements — one table per band.
388
+ * @param lefts Each band's left edge in points (from the projection's sink).
389
+ * @returns The two merged; drawings first when the sheet does not band.
390
+ */
391
+ function withDrawingsByBand(drawings, gridBody, lefts) {
392
+ const tables = gridBody.filter((el) => el.kind === "table").length;
393
+ if (drawings.length === 0 || lefts.length < 2 || tables !== lefts.length) return [...drawings, ...gridBody];
394
+ const bandOf = (x) => {
395
+ let band = 0;
396
+ while (band + 1 < lefts.length && x >= lefts[band + 1]) band++;
397
+ return band;
398
+ };
399
+ const byBand = lefts.map(() => []);
400
+ for (const el of drawings) {
401
+ const x = floatLeftPt(el);
402
+ const band = x === void 0 ? 0 : bandOf(x);
403
+ byBand[band].push(x === void 0 ? el : shiftFloatLeft(el, lefts[band]));
404
+ }
405
+ const out = [];
406
+ let seen = 0;
407
+ for (const el of gridBody) {
408
+ out.push(el);
409
+ if (el.kind === "table") out.push(...byBand[seen++]);
410
+ }
411
+ return out;
412
+ }
413
+ /** A float's horizontal anchor, for the block kinds a sheet anchors. */
414
+ function floatLeftPt(el) {
415
+ return (el.kind === "chart" ? el.chart.float : el.kind === "image" ? el.image.float : el.kind === "shape" ? el.shape.float : void 0)?.posH?.offsetPt;
416
+ }
417
+ /** The same block with its horizontal anchor measured from `by` points later. */
418
+ function shiftFloatLeft(el, by) {
419
+ if (by === 0) return el;
420
+ const moved = (block) => ({
421
+ ...block,
422
+ float: {
423
+ ...block.float,
424
+ posH: {
425
+ ...block.float.posH,
426
+ offsetPt: pt((block.float.posH.offsetPt ?? 0) - by)
427
+ }
428
+ }
429
+ });
430
+ if (el.kind === "chart") return {
431
+ ...el,
432
+ chart: moved(el.chart)
433
+ };
434
+ if (el.kind === "image") return {
435
+ ...el,
436
+ image: moved(el.image)
437
+ };
438
+ if (el.kind === "shape") return {
439
+ ...el,
440
+ shape: moved(el.shape)
441
+ };
442
+ return el;
443
+ }
444
+ var ACROSS = {
445
+ anchorOf: (s) => s.float?.posH?.offsetPt,
446
+ sizeOf: (s) => s.width,
447
+ shifted: (s, by) => ({
448
+ ...s,
449
+ float: {
450
+ ...s.float,
451
+ posH: {
452
+ ...s.float.posH,
453
+ offsetPt: pt((s.float.posH.offsetPt ?? 0) - by)
454
+ }
455
+ }
456
+ })
457
+ };
458
+ var DOWN = {
459
+ anchorOf: (s) => s.float?.posV?.offsetPt,
460
+ sizeOf: (s) => s.height,
461
+ shifted: (s, by) => ({
462
+ ...s,
463
+ float: {
464
+ ...s.float,
465
+ posV: {
466
+ ...s.float.posV,
467
+ offsetPt: pt((s.float.posV.offsetPt ?? 0) - by)
468
+ }
469
+ }
470
+ })
471
+ };
472
+ function bandDrawings(shapes, printable, scale, axis = ACROSS) {
473
+ if (shapes.length === 0 || !(printable > 0)) return [shapes];
474
+ const rightOf = (s) => (axis.anchorOf(s) ?? 0) * scale + axis.sizeOf(s);
475
+ const extent = Math.max(0, ...shapes.map(rightOf));
476
+ const bandCount = Math.ceil(extent / printable);
477
+ if (bandCount <= 1) return [shapes];
478
+ const bands = Array.from({ length: bandCount }, () => []);
479
+ for (const shape of shapes) {
480
+ const anchor = axis.anchorOf(shape);
481
+ const left = (anchor ?? 0) * scale;
482
+ const right = left + axis.sizeOf(shape);
483
+ const first = Math.max(0, Math.min(bandCount - 1, Math.floor(left / printable)));
484
+ const last = Math.max(first, Math.min(bandCount - 1, Math.ceil(right / printable) - 1));
485
+ for (let band = first; band <= last; band++) {
486
+ if (!shape.float || anchor === void 0) {
487
+ bands[band].push(shape);
488
+ continue;
489
+ }
490
+ const { text: _text, ...frame } = shape;
491
+ if (band !== first && !frame.line && frame.fill.kind === "none") continue;
492
+ bands[band].push(axis.shifted(band === first ? shape : frame, band * printable / scale));
493
+ }
494
+ }
495
+ return bands.filter((b) => b.length > 0);
496
+ }
497
+ /**
498
+ * §20.5.2.35 — the float a drawing's anchor asks for: pinned at the point the
499
+ * anchor names, measured from the page margin, wrapping nothing.
500
+ *
501
+ * A chart or a picture placed in the FLOW instead lands at the left margin,
502
+ * below every block before it, and on whatever page that turns out to be:
503
+ * simple-monthly-budget.xlsx anchors its chart beside the summary on page one
504
+ * and we printed it alone on page two, a third of the way in from the left.
505
+ * Shapes have floated at their anchors since W2; these two never did.
506
+ *
507
+ * No anchor (an `absoluteAnchor`, or a reader that gave none) ⇒ no float, and
508
+ * the block keeps the flow placement it has always had.
509
+ */
510
+ function anchorFloat(xPt, yPt, scale) {
511
+ if (xPt === void 0 || yPt === void 0) return {};
512
+ return { float: {
513
+ wrap: "none",
514
+ posH: {
515
+ relativeFrom: "margin",
516
+ offsetPt: pt(xPt * scale)
517
+ },
518
+ posV: {
519
+ relativeFrom: "margin",
520
+ offsetPt: pt(yPt * scale)
521
+ }
522
+ } };
523
+ }
524
+ /** Every control that carries geometry, as the shapes that draw it. */
525
+ function controlShapeBlocks(formControls, activeXControls) {
526
+ const out = [];
527
+ for (const c of formControls ?? []) {
528
+ if (!c.box) continue;
529
+ out.push(...controlShapes(c.box, formControlGlyph(c.objectType), c.caption, c.checked === true, c.fontSizePt ?? DEFAULT_FORM_CONTROL_PT));
530
+ }
531
+ for (const c of activeXControls ?? []) {
532
+ if (!c.box) continue;
533
+ const on = c.value === "1" || c.value?.toLowerCase() === "true";
534
+ out.push(...controlShapes(c.box, activeXGlyph(c.type), c.caption, on, DEFAULT_ACTIVEX_CONTROL_PT));
535
+ }
536
+ return out;
537
+ }
130
538
  function formControlBlocks(controls) {
539
+ const listed = controls.filter((c) => c.box === void 0);
540
+ if (listed.length === 0) return [];
131
541
  const out = [{
132
542
  kind: "paragraph",
133
543
  paragraph: {
@@ -138,7 +548,7 @@ function formControlBlocks(controls) {
138
548
  }]
139
549
  }
140
550
  }];
141
- for (const c of controls) out.push({
551
+ for (const c of listed) out.push({
142
552
  kind: "paragraph",
143
553
  paragraph: {
144
554
  properties: {},
@@ -164,6 +574,8 @@ function formControlLabel(c) {
164
574
  }
165
575
  }
166
576
  function activeXBlocks(controls) {
577
+ const listed = controls.filter((c) => c.box === void 0);
578
+ if (listed.length === 0) return [];
167
579
  const out = [{
168
580
  kind: "paragraph",
169
581
  paragraph: {
@@ -174,7 +586,7 @@ function activeXBlocks(controls) {
174
586
  }]
175
587
  }
176
588
  }];
177
- for (const c of controls) out.push({
589
+ for (const c of listed) out.push({
178
590
  kind: "paragraph",
179
591
  paragraph: {
180
592
  properties: {},
@@ -203,28 +615,131 @@ function activeXLabel(c) {
203
615
  default: return label;
204
616
  }
205
617
  }
206
- function withHeaderFooter(section, ws, headersFooters) {
618
+ /**
619
+ * How far the sheet's drawings reach from its origin, or undefined when it has
620
+ * none. Fit-to-page has to fit them as well as the cells: a drawing anchored
621
+ * past the last value is still printed, and on a sheet whose values sit in a
622
+ * handful of cells it is the drawing that decides the page.
623
+ */
624
+ function drawingReachPt(ws) {
625
+ let widthPt = 0;
626
+ let heightPt = 0;
627
+ for (const shape of ws.shapes ?? []) {
628
+ widthPt = Math.max(widthPt, (shape.float?.posH?.offsetPt ?? 0) + shape.width);
629
+ heightPt = Math.max(heightPt, (shape.float?.posV?.offsetPt ?? 0) + shape.height);
630
+ }
631
+ for (const c of ws.charts ?? []) {
632
+ widthPt = Math.max(widthPt, (c.xPt ?? 0) + c.widthPt);
633
+ heightPt = Math.max(heightPt, (c.yPt ?? 0) + c.heightPt);
634
+ }
635
+ for (const p of ws.images ?? []) {
636
+ widthPt = Math.max(widthPt, (p.xPt ?? 0) + p.widthPt);
637
+ heightPt = Math.max(heightPt, (p.yPt ?? 0) + p.heightPt);
638
+ }
639
+ return widthPt > 0 || heightPt > 0 ? {
640
+ widthPt,
641
+ heightPt
642
+ } : void 0;
643
+ }
644
+ /**
645
+ * `<printOptions horizontalCentered/verticalCentered>` centres what is PRINTED,
646
+ * and a drawing is printed. We centred the table only, which leaves a drawing
647
+ * at its raw offset — and on a sheet whose cells are all empty there is no
648
+ * table to centre at all: bnc762542.xlsx asks for both axes and its callout sat
649
+ * some 280pt left of where the reference puts it, matching the file's own
650
+ * numbers exactly but ignoring the request that moves them.
651
+ */
652
+ function centreShape(shape, worksheet, extent, scale) {
653
+ const options = worksheet.printOptions;
654
+ if (!extent || !options?.horizontalCentered && !options?.verticalCentered) return shape;
655
+ const section = sectionFromWorksheet(worksheet);
656
+ const size = section.pageSize;
657
+ const margins = section.margins;
658
+ if (!size || !margins) return shape;
659
+ const dx = options.horizontalCentered ? (size.width - margins.left - margins.right - extent.widthPt * scale) / 2 : 0;
660
+ const dy = options.verticalCentered ? (size.height - margins.top - margins.bottom - extent.heightPt * scale) / 2 : 0;
661
+ if (dx <= 0 && dy <= 0) return shape;
662
+ const posH = shape.float?.posH;
663
+ const posV = shape.float?.posV;
664
+ if (!shape.float) return shape;
665
+ return {
666
+ ...shape,
667
+ float: {
668
+ ...shape.float,
669
+ ...posH && dx > 0 ? { posH: {
670
+ ...posH,
671
+ offsetPt: pt((posH.offsetPt ?? 0) + dx)
672
+ } } : {},
673
+ ...posV && dy > 0 ? { posV: {
674
+ ...posV,
675
+ offsetPt: pt((posV.offsetPt ?? 0) + dy)
676
+ } } : {}
677
+ }
678
+ };
679
+ }
680
+ /**
681
+ * A drawing at the sheet's print scale: its box and the point it floats at
682
+ * shrink together, exactly as the grid beneath them does. Returns the shape
683
+ * untouched at 1.0 so an unscaled sheet stays byte-identical.
684
+ */
685
+ function scaleShape(shape, scale) {
686
+ if (!(scale > 0) || scale >= .999) return shape;
687
+ const offset = (o) => o?.offsetPt === void 0 ? void 0 : o.offsetPt * scale;
688
+ const posH = shape.float?.posH;
689
+ const posV = shape.float?.posV;
690
+ return {
691
+ ...shape,
692
+ width: pt(shape.width * scale),
693
+ height: pt(shape.height * scale),
694
+ ...shape.float ? { float: {
695
+ ...shape.float,
696
+ ...posH ? { posH: {
697
+ ...posH,
698
+ ...withOffset(offset(posH))
699
+ } } : {},
700
+ ...posV ? { posV: {
701
+ ...posV,
702
+ ...withOffset(offset(posV))
703
+ } } : {}
704
+ } } : {}
705
+ };
706
+ }
707
+ function withOffset(value) {
708
+ return value === void 0 ? {} : { offsetPt: pt(value) };
709
+ }
710
+ /**
711
+ * The sheet's header/footer bands, registered under ids of this sheet's own.
712
+ *
713
+ * The map is document-level and the ids used to be fixed, so a second sheet
714
+ * overwrote the first's bands and every section pointed at the last one — in
715
+ * practice only the first sheet's header survived, because only its section
716
+ * carried the references. formats.xlsx puts the sheet name and page number on
717
+ * all five of its pages and we printed them on one.
718
+ */
719
+ function withHeaderFooter(section, ws, headersFooters, scale, basePt, sheetIdx, fileName, themePalette, now) {
720
+ const headerRel = `${HEADER_REL}${sheetIdx}`;
721
+ const footerRel = `${FOOTER_REL}${sheetIdx}`;
207
722
  const hf = ws.grid.headerFooter;
208
723
  if (!hf || !hf.oddHeader && !hf.oddFooter) return section;
209
724
  const headers = [];
210
725
  const footers = [];
211
726
  if (hf.oddHeader) {
212
- const content = buildHeaderFooterContent(hf.oddHeader, ws.name);
727
+ const content = buildHeaderFooterContent(hf.oddHeader, ws.name, scale, basePt, fileName, themePalette, now);
213
728
  if (content.length > 0) {
214
- headersFooters.set(HEADER_REL, resolveBodyStyles(content, EMPTY_STYLE_SHEET));
729
+ headersFooters.set(headerRel, resolveBodyStyles(content, EMPTY_STYLE_SHEET));
215
730
  headers.push({
216
731
  type: "default",
217
- relationshipId: HEADER_REL
732
+ relationshipId: headerRel
218
733
  });
219
734
  }
220
735
  }
221
736
  if (hf.oddFooter) {
222
- const content = buildHeaderFooterContent(hf.oddFooter, ws.name);
737
+ const content = buildHeaderFooterContent(hf.oddFooter, ws.name, scale, basePt, fileName, themePalette, now);
223
738
  if (content.length > 0) {
224
- headersFooters.set(FOOTER_REL, resolveBodyStyles(content, EMPTY_STYLE_SHEET));
739
+ headersFooters.set(footerRel, resolveBodyStyles(content, EMPTY_STYLE_SHEET));
225
740
  footers.push({
226
741
  type: "default",
227
- relationshipId: FOOTER_REL
742
+ relationshipId: footerRel
228
743
  });
229
744
  }
230
745
  }
@@ -2,6 +2,7 @@ import { XMLParser } from "fast-xml-parser";
2
2
  //#region src/excel/slicer-parser.ts
3
3
  var decoder = new TextDecoder("utf-8");
4
4
  var parser = new XMLParser({
5
+ htmlEntities: true,
5
6
  ignoreAttributes: false,
6
7
  attributeNamePrefix: "@_",
7
8
  parseAttributeValue: false,