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