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,10 +1,67 @@
1
1
  import { eighthPtToPt, halfPtToPt, pt, twipsToPt } from "../core/ir/units.js";
2
2
  import { FEATURES } from "../core/ir/features.js";
3
+ import { applyNumberFormat, generalToWidth, numberFormatColorHex } from "../core/number-format.js";
3
4
  import { parseAreaRef, parseTitleRowRange } from "./defined-name-ref.js";
4
- import { applyNumberFormat } from "./number-format.js";
5
5
  import "./index.js";
6
- import { bandedTables, computeColumnBands } from "./column-bands.js";
6
+ import { bandedTables, computeColumnBands, withHeadingBand } from "./column-bands.js";
7
7
  import { buildConditionalFormatter } from "./conditional-format.js";
8
+ /** The point size {@link TWIPS_PER_EXCEL_CHAR} was measured at (Excel's default). */
9
+ var DEFAULT_FONT_PT = 11;
10
+ /**
11
+ * How far past the content a value-less but PAINTED cell may push the used
12
+ * range. A legend or a colour key sits a handful of columns off the data; a
13
+ * whole-row style reaches column XFD. The distance is what tells them apart.
14
+ */
15
+ var PAINT_REACH_COLUMNS = 64;
16
+ var PAINT_REACH_ROWS = 512;
17
+ /** One screen pixel at 96 DPI, in twips — the unit Excel's width formula works in. */
18
+ var TWIPS_PER_PIXEL = 15;
19
+ /**
20
+ * §18.3.1.13 — a `<col width>` in twips.
21
+ *
22
+ * The padding above is the formula for a column at least one character wide.
23
+ * Excel documents a SEPARATE one below that: `px = Trunc(width × MDW + 0.5)`,
24
+ * with no padding at all. It matters because the padding is a constant: on a
25
+ * form drawn over a fine grid — tdf118668.xlsx rules 168 columns of 0.855
26
+ * characters — 3.75pt of padding nearly doubles a 4.5pt column, and the sheet
27
+ * came out 1384pt wide against the reference's 754, so it paginated across two
28
+ * pages where every reader prints one.
29
+ *
30
+ * @param chars The declared width in characters.
31
+ * @param charTwips The Maximum Digit Width, in twips.
32
+ * @returns The rendered column width in twips.
33
+ */
34
+ function columnTwips(chars, charTwips) {
35
+ return columnTwipsOf(chars, charTwips);
36
+ }
37
+ /**
38
+ * §18.3.1.81 — the DEFAULT column, in twips, from whichever of the two the
39
+ * sheet declares.
40
+ *
41
+ * The two are not the same number. `defaultColWidth` "includes margin padding
42
+ * and extra padding for gridlines"; `baseColWidth` is the bare character count
43
+ * and explicitly excludes them, so deriving a default from it adds the padding
44
+ * once to reach a defaultColWidth and once more to render it. 47668.xlsx says
45
+ * `baseColWidth="10"` and caches its picture's extent at 9753600 EMU = 768pt
46
+ * over 12 columns plus 48pt — 60pt, or 80px, a column. Padding it once gives
47
+ * 75px and squeezed that picture by 6%. LibreOffice measures ~79px here, so
48
+ * both references agree against us.
49
+ *
50
+ * @param worksheet The sheet.
51
+ * @param charTwips The Maximum Digit Width, in twips.
52
+ * @param fallbackChars The width to use when the sheet declares neither.
53
+ * @returns The default column width in twips.
54
+ */
55
+ function defaultColumnTwips(worksheet, charTwips, fallbackChars) {
56
+ if (worksheet.defaultColWidthChars !== void 0) return columnTwipsOf(worksheet.defaultColWidthChars, charTwips);
57
+ if (worksheet.baseColWidthChars !== void 0) return columnTwipsOf(worksheet.baseColWidthChars, charTwips) + 150;
58
+ return columnTwipsOf(fallbackChars, charTwips);
59
+ }
60
+ function columnTwipsOf(chars, charTwips) {
61
+ const mdwPx = charTwips / TWIPS_PER_PIXEL;
62
+ if (chars >= 1 && mdwPx > 0) return Math.trunc((256 * chars + Math.trunc(128 / mdwPx)) / 256 * mdwPx) * TWIPS_PER_PIXEL;
63
+ return Math.trunc(chars * charTwips / TWIPS_PER_PIXEL + .5) * TWIPS_PER_PIXEL;
64
+ }
8
65
  /**
9
66
  * Excel insets a cell's text by ~2 px each side (1.5 pt at 96 DPI). The layout
10
67
  * engine's default is a word processor's 108 twips (5.4 pt), which is nearly
@@ -12,12 +69,106 @@ import { buildConditionalFormatter } from "./conditional-format.js";
12
69
  */
13
70
  var EXCEL_CELL_INSET_PT = 1.5;
14
71
  /**
72
+ * The width behind {@link DEFAULT_COL_TWIPS}, for a non-Calibri unit.
73
+ *
74
+ * In the STORED unit, which is what {@link columnTwips} reads: Excel's default
75
+ * column is "8.43 characters" in its own interface and `width="9.140625"` in
76
+ * every file that writes it out, because the stored form carries the padding.
77
+ */
78
+ var DEFAULT_COL_CHARS = 9.140625;
79
+ /**
80
+ * The column-width unit in twips: the Maximum Digit Width of the font the
81
+ * document is rendered in (§18.3.1.13), or Excel's own 7 px when the caller has
82
+ * not measured one.
83
+ */
84
+ /**
85
+ * The digit advance, as a fraction of the em, of the faces a spreadsheet names
86
+ * for its normal style.
87
+ *
88
+ * Read off each face's `hmtx` for `0`–`9`; the metric-compatible substitutes
89
+ * are listed beside their originals because a file naming one is measured in
90
+ * the other's unit by every reader that has to substitute.
91
+ *
92
+ * Every entry here is measured. The CJK defaults are deliberately absent: the
93
+ * evidence says MS Pゴシック 11 is 8px — Japanese Excel's default column is
94
+ * 72px, and §18.3.1.13's forward formula turns that into exactly the
95
+ * `width="9"` 54524.xlsx writes for its default band — but I have no copy of
96
+ * the face to measure, and fitting 0.55 to it moved 50299.xlsx from 16 pages
97
+ * to 22 against LibreOffice's 17. A face we cannot measure keeps the old 7px.
98
+ */
99
+ var DIGIT_EM = [
100
+ [/^(calibri|carlito)$/i, 1063 / 2048],
101
+ [/^(arial|helvetica|liberation sans|arimo|arial unicode ms)$/i, 1139 / 2048],
102
+ [/^(times new roman|liberation serif|tinos)$/i, 1024 / 2048],
103
+ [/^(tahoma)$/i, 1118 / 2048],
104
+ [/^(verdana|dejavu sans)$/i, 1303 / 2048]
105
+ ];
106
+ /**
107
+ * §18.3.1.13 — the Maximum Digit Width, in twips, of the normal style's font.
108
+ *
109
+ * "The number of characters of the maximum digit width of the numbers 0, 1,
110
+ * 2, …, 9 **as rendered in the normal style's font**" — the unit is bound to
111
+ * that face at that size, and Excel takes it in whole screen pixels. This used
112
+ * to test only whether the file NAMED a font and hand every named one Excel's
113
+ * 7px: right for the Calibri 11 and Arial 10 the note below cites, wrong for
114
+ * anything else. 55850.xlsx and 55927.xlsx are Arial ELEVEN, whose digit is
115
+ * 8px, so their columns came out 14% narrow and both files lost the last
116
+ * character of every date. A face we do not know keeps the old 7px rather than
117
+ * a guess.
118
+ *
119
+ * @param name The normal style's font name.
120
+ * @param sizePt Its size in points; 11 when the file omits one.
121
+ * @returns The Maximum Digit Width in twips.
122
+ */
123
+ function maximumDigitTwips(name, sizePt) {
124
+ const size = sizePt !== void 0 && Number.isFinite(sizePt) && sizePt > 0 ? sizePt : 11;
125
+ const em = DIGIT_EM.find(([re]) => re.test(name.trim()))?.[1];
126
+ if (em === void 0) return 105;
127
+ const px = Math.trunc(em * size * (96 / 72));
128
+ return px > 0 ? px * TWIPS_PER_PIXEL : 105;
129
+ }
130
+ function digitTwips(digitWidthPt) {
131
+ if (digitWidthPt === void 0 || !Number.isFinite(digitWidthPt) || digitWidthPt <= 0) return 105;
132
+ return digitWidthPt * TWIPS_PER_POINT;
133
+ }
134
+ /**
15
135
  * ECMA-376 §18.3.1.81 — the row height Excel uses when a sheet declares no
16
136
  * `<sheetFormatPr defaultRowHeight>`: 15pt, the line height of its default
17
137
  * theme font (Calibri 11). Independent of whatever font we end up rendering
18
138
  * with — the height belongs to the document, not to the typesetter.
19
139
  */
20
140
  var EXCEL_DEFAULT_ROW_HEIGHT_PT = 15;
141
+ /**
142
+ * The same height for a workbook whose Normal style names another size —
143
+ * Excel's own table, which is not proportional because the height is a line
144
+ * height and a font's leading is not proportional to its body.
145
+ *
146
+ * A workbook written before Calibri says Arial 10 and no `defaultRowHeight`,
147
+ * and every one of its rows is 12.75pt: tdf100709.xlsx fits twenty of them on
148
+ * the page where our 15pt fitted fourteen and pushed the rest onto a second.
149
+ */
150
+ var EXCEL_ROW_HEIGHT_BY_FONT_PT = new Map([
151
+ [8, 11],
152
+ [9, 12],
153
+ [10, 12.75],
154
+ [11, 15],
155
+ [12, 15.75],
156
+ [14, 18]
157
+ ]);
158
+ /**
159
+ * The row height a sheet that declares none inherits from the workbook's
160
+ * Normal font. Sizes outside Excel's table scale from its 10pt entry and snap
161
+ * up to the 0.75pt grid row heights are stored on (one pixel at 96 dpi).
162
+ *
163
+ * @param fontPt The Normal style's font size, or undefined if unknown.
164
+ * @returns The default row height in points.
165
+ */
166
+ function defaultRowHeightPtFor(fontPt) {
167
+ if (fontPt === void 0 || !Number.isFinite(fontPt) || fontPt <= 0) return EXCEL_DEFAULT_ROW_HEIGHT_PT;
168
+ const known = EXCEL_ROW_HEIGHT_BY_FONT_PT.get(fontPt);
169
+ if (known !== void 0) return known;
170
+ return Math.ceil(fontPt * 12.75 / 10 / .75) * .75;
171
+ }
21
172
  var TWIPS_PER_POINT = 20;
22
173
  var TWIPS_PER_INCH = 1440;
23
174
  var PAPER_SIZES_TWIPS = new Map([
@@ -29,7 +180,7 @@ var PAPER_SIZES_TWIPS = new Map([
29
180
  [11, [8392, 11906]],
30
181
  [70, [5953, 8392]]
31
182
  ]);
32
- var DEFAULT_PAPER_TWIPS = [11906, 16838];
183
+ var DEFAULT_PAPER_TWIPS = [11900, 16840];
33
184
  /**
34
185
  * Build the page section (paper size + margins) from a worksheet's `<pageSetup>`
35
186
  * / `<pageMargins>`.
@@ -102,6 +253,57 @@ function resolvePrintTitleRows(definedNames, sheetIdx) {
102
253
  for (const dn of definedNames) if (dn.name === PRINT_TITLES_NAME && dn.localSheetId === sheetIdx) return parseTitleRowRange(dn.value);
103
254
  }
104
255
  var MIN_PRINT_SCALE = .1;
256
+ /**
257
+ * The width one page has for content: the sheet's paper (declared or the A4
258
+ * default) less its left and right margins, in points. The same measure the
259
+ * column bands are packed into — a drawing layer banded on any other width
260
+ * would not line up with the grid beside it.
261
+ */
262
+ function printableWidthPt(worksheet) {
263
+ return sheetContentWidthTwips(worksheet) / TWIPS_PER_POINT;
264
+ }
265
+ /**
266
+ * Whether a sheet paginates ACROSS its columns.
267
+ *
268
+ * Fit-to-ONE-page does not: the whole grid is squeezed onto a single page
269
+ * width, and Excel lets that override even a manual column break. An explicit
270
+ * `<pageSetup scale>` is a different thing entirely — §18.3.1.63 makes it a
271
+ * plain zoom, and Excel paginates AFTER applying it. Treating the two alike
272
+ * left 47737.xlsx (`scale="63"`, still 583pt wide) as one 583pt table on a
273
+ * 487pt page: its last column fell off the paper and its "Heading" with it.
274
+ *
275
+ * @param worksheet The sheet.
276
+ * @param fitWide `fitToWidth`, or 1 when the sheet is not fit-to-page.
277
+ * @returns True when the grid should be split into column bands.
278
+ */
279
+ function bandsAcross(worksheet, fitWide) {
280
+ return !worksheet.fitToPage || fitWide > 1;
281
+ }
282
+ /**
283
+ * The manual breaks (§18.3.1.74/§18.3.1.14) that still apply on one axis.
284
+ *
285
+ * Fit-to-page and a manual break are two answers to the same question, and
286
+ * Excel takes the scaling one: turn on "fit to N pages" and the breaks drawn
287
+ * into the sheet stop dividing it. Honouring both asks for the impossible —
288
+ * a break costs a page at EVERY scale, so `fitToHeight="1"` beside one of them
289
+ * can never be satisfied, and the fit search would shrink toward its floor
290
+ * chasing a page count it will never reach. sheet-fit-breaks.xlsx says fit on
291
+ * one page and breaks after row 20; the reference prints all forty rows on one
292
+ * page and we printed two.
293
+ *
294
+ * An explicit `0` on the axis means "as many pages as it takes" — nothing is
295
+ * being fitted there, so the breaks on it stand.
296
+ *
297
+ * @param worksheet The sheet, for its `fitToPage` flag and page setup.
298
+ * @param axis Which axis's breaks are wanted.
299
+ * @returns The break indices to honour; empty when fit-to-page overrides them.
300
+ */
301
+ function honouredBreaks(worksheet, axis) {
302
+ const breaks = (axis === "rows" ? worksheet.rowBreaks : worksheet.colBreaks) ?? [];
303
+ if (!worksheet.fitToPage) return breaks;
304
+ const setup = worksheet.pageSetup;
305
+ return ((axis === "rows" ? setup?.fitToHeight : setup?.fitToWidth) ?? 1) >= 1 ? [] : breaks;
306
+ }
105
307
  function sheetContentWidthTwips(worksheet) {
106
308
  const pageSize = pageSizeFromSetup(worksheet.pageSetup);
107
309
  const pageWidthTwips = pageSize ? Math.round(pageSize.width * 20) : DEFAULT_PAPER_TWIPS[0];
@@ -110,6 +312,17 @@ function sheetContentWidthTwips(worksheet) {
110
312
  const right = Math.round(margins.right * 20);
111
313
  return Math.max(TWIPS_PER_INCH / 2, pageWidthTwips - left - right);
112
314
  }
315
+ /**
316
+ * The printable HEIGHT of one page of this sheet, in points — its paper height
317
+ * less its own top and bottom margins. The vertical twin of
318
+ * {@link printableWidthPt}.
319
+ *
320
+ * @param worksheet The parsed worksheet (paper + margins).
321
+ * @returns The height a page of this sheet has for content, in points.
322
+ */
323
+ function printableHeightPt(worksheet) {
324
+ return sheetContentHeightTwips(worksheet) / TWIPS_PER_POINT;
325
+ }
113
326
  function sheetContentHeightTwips(worksheet) {
114
327
  const pageSize = pageSizeFromSetup(worksheet.pageSetup);
115
328
  const pageHeightTwips = pageSize ? Math.round(pageSize.height * 20) : DEFAULT_PAPER_TWIPS[1];
@@ -118,27 +331,73 @@ function sheetContentHeightTwips(worksheet) {
118
331
  const bottom = Math.round(margins.bottom * 20);
119
332
  return Math.max(TWIPS_PER_INCH / 2, pageHeightTwips - top - bottom);
120
333
  }
121
- function computePrintScale(worksheet, totalGridTwips, contentWidthTwips, totalGridHeightTwips, contentHeightTwips) {
334
+ /**
335
+ * How many pages the grid ACTUALLY paginates into at a given scale — the same
336
+ * greedy fill the layout performs, run over one axis.
337
+ *
338
+ * A row does not split, so the closed form `contentHeight × fitToHeight / total`
339
+ * is a lower bound and not an answer: whatever height the last row that will not
340
+ * fit leaves unused is a page's worth of slack the formula never accounts for,
341
+ * and print titles make it worse by consuming their height again on every page.
342
+ */
343
+ function pageCountAtScale(extents, forcedBreaks, repeatTwips, limitTwips, scale) {
344
+ let pages = 1;
345
+ let used = 0;
346
+ for (let i = 0; i < extents.length; i++) {
347
+ const extent = extents[i] * scale;
348
+ if (used > 0 && (forcedBreaks.has(i) || used + extent > limitTwips)) {
349
+ pages++;
350
+ used = repeatTwips * scale;
351
+ }
352
+ used += extent;
353
+ }
354
+ return pages;
355
+ }
356
+ /**
357
+ * The largest whole percentage at or below `closed` that paginates into at most
358
+ * `maxPages` pages. Whole percentages because that is the unit Excel searches in
359
+ * and stores the answer in (`<pageSetup scale="51">`), so the two agree exactly.
360
+ * Monotone — shrinking never adds a page — so a binary search is enough.
361
+ */
362
+ function searchFitScale(closed, maxPages, pageCount) {
363
+ const floor = Math.round(MIN_PRINT_SCALE * 100);
364
+ let hi = Math.min(100, Math.floor(closed * 100));
365
+ if (hi <= floor) return Math.max(MIN_PRINT_SCALE, closed);
366
+ if (pageCount(hi / 100) <= maxPages) return hi / 100;
367
+ let lo = floor;
368
+ if (pageCount(lo / 100) > maxPages) return closed;
369
+ while (lo < hi) {
370
+ const mid = Math.ceil((lo + hi) / 2);
371
+ if (pageCount(mid / 100) <= maxPages) lo = mid;
372
+ else hi = mid - 1;
373
+ }
374
+ return lo / 100;
375
+ }
376
+ function computePrintScale(worksheet, totalGridTwips, contentWidthTwips, totalGridHeightTwips, contentHeightTwips, geometry) {
122
377
  const setup = worksheet.pageSetup;
123
378
  let s = 1;
124
379
  if (worksheet.fitToPage) {
125
380
  const fitW = setup?.fitToWidth ?? 1;
381
+ const fitH = setup?.fitToHeight ?? 1;
126
382
  let sW = 1;
127
383
  let sH = 1;
128
384
  if (fitW >= 1 && totalGridTwips > 0) sW = contentWidthTwips * fitW / totalGridTwips;
129
- const fitH = setup?.fitToHeight;
130
- if (fitH !== void 0 && fitH >= 1 && totalGridHeightTwips > 0) sH = contentHeightTwips * fitH / totalGridHeightTwips;
385
+ if (fitH >= 1 && totalGridHeightTwips > 0) sH = contentHeightTwips * fitH / totalGridHeightTwips;
386
+ if (fitW >= 1 && geometry.colTwips.length > 0) sW = searchFitScale(sW, fitW, (scale) => pageCountAtScale(geometry.colTwips, geometry.colBreaks, 0, contentWidthTwips, scale));
387
+ if (fitH >= 1 && geometry.rowTwips.length > 0) sH = searchFitScale(sH, fitH, (scale) => pageCountAtScale(geometry.rowTwips, geometry.rowBreaks, geometry.titleTwips, contentHeightTwips, scale));
131
388
  s = Math.min(sW, sH);
132
389
  } else if (setup?.scale !== void 0 && setup.scale > 0) s = setup.scale / 100;
133
390
  return Math.min(1, Math.max(MIN_PRINT_SCALE, s));
134
391
  }
135
392
  function scaleRunFont(props, scale) {
136
- const hp = props.fontSizePt !== void 0 ? Math.round(props.fontSizePt * 2) : 22;
393
+ const base = props.fontSizePt ?? DEFAULT_FONT_PT;
137
394
  return {
138
395
  ...props,
139
- fontSizePt: halfPtToPt(Math.max(2, Math.round(hp * scale)))
396
+ fontSizePt: pt(Math.max(1, base * scale))
140
397
  };
141
398
  }
399
+ var PRINT_GRIDLINE_HEX = "C0C0C0";
400
+ var CF_NUMBER_FORMAT_ID = 1000001;
142
401
  var MAX_SHEET_TEXT_CHARS = 1e6;
143
402
  /**
144
403
  * Project one worksheet's grid into Flow body elements — a single {@link Table}
@@ -158,6 +417,16 @@ var MAX_SHEET_TEXT_CHARS = 1e6;
158
417
  */
159
418
  function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
160
419
  if (worksheet.maxRow < 0 || worksheet.maxColumn < 0) return [];
420
+ const charTwipsUnit = styles.fonts[0]?.name === void 0 ? digitTwips(print.digitWidthPt) : maximumDigitTwips(styles.fonts[0].name, styles.fonts[0].sizePt);
421
+ const textTwipsUnit = 105;
422
+ const rowStyleAt = /* @__PURE__ */ new Map();
423
+ for (const r of worksheet.rowStyles ?? []) rowStyleAt.set(r.row, r.styleIndex);
424
+ const columnStyles = worksheet.columnStyles ?? [];
425
+ const defaultStyleAt = (row, col, cellXfs) => {
426
+ const fromRow = rowStyleAt.get(row);
427
+ if (fromRow !== void 0) return cellXfs[fromRow];
428
+ for (const cs of columnStyles) if (col + 1 >= cs.min && col + 1 <= cs.max) return cellXfs[cs.styleIndex];
429
+ };
161
430
  let usedRow = -1;
162
431
  let usedCol = -1;
163
432
  const contentAt = /* @__PURE__ */ new Set();
@@ -177,7 +446,61 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
177
446
  if (host.startRow > usedRow) usedRow = host.startRow;
178
447
  if (host.startColumn > usedCol) usedCol = host.startColumn;
179
448
  }
180
- if (usedRow < 0 || usedCol < 0) return [];
449
+ if (usedRow >= 0 && usedCol >= 0 && print.drawingExtentPt) {
450
+ const wantTwips = Math.round(print.drawingExtentPt.widthPt * TWIPS_PER_POINT);
451
+ const defTwips = defaultColumnTwips(worksheet, charTwipsUnit, DEFAULT_COL_CHARS);
452
+ const widthAt = (abs) => {
453
+ for (const col of worksheet.columns) {
454
+ if (abs < col.min - 1 || abs > col.max - 1) continue;
455
+ return col.hidden ? 0 : columnTwips(col.widthChars, charTwipsUnit);
456
+ }
457
+ return defTwips;
458
+ };
459
+ let acc = 0;
460
+ for (let col = 0; col < 16384 && acc < wantTwips; col++) {
461
+ acc += widthAt(col);
462
+ if (col > usedCol) usedCol = col;
463
+ }
464
+ }
465
+ if (usedRow >= 0 && usedCol >= 0) {
466
+ const colReach = usedCol + PAINT_REACH_COLUMNS;
467
+ const rowReach = usedRow + PAINT_REACH_ROWS;
468
+ for (const c of worksheet.cells) {
469
+ if (c.column > colReach || c.row > rowReach) continue;
470
+ if (c.column <= usedCol && c.row <= usedRow) continue;
471
+ if (!cellPaintsSomething(c, styles)) continue;
472
+ if (c.row > usedRow) usedRow = c.row;
473
+ if (c.column > usedCol) usedCol = c.column;
474
+ }
475
+ }
476
+ if (usedRow < 0 && usedCol < 0) {
477
+ const pageTwips = sheetContentWidthTwips(worksheet);
478
+ const defTwips = defaultColumnTwips(worksheet, charTwipsUnit, DEFAULT_COL_CHARS);
479
+ let acc = 0;
480
+ let colLimit = 0;
481
+ while (colLimit < PAINT_REACH_COLUMNS && acc < pageTwips) {
482
+ const col = worksheet.columns.find((cc) => colLimit >= cc.min - 1 && colLimit <= cc.max - 1);
483
+ acc += col ? col.hidden ? 0 : columnTwips(col.widthChars, charTwipsUnit) : defTwips;
484
+ colLimit++;
485
+ }
486
+ for (const c of worksheet.cells) {
487
+ if (c.column >= colLimit || c.row >= PAINT_REACH_ROWS) continue;
488
+ if (!cellPaintsVisibly(c, styles)) continue;
489
+ if (c.row > usedRow) usedRow = c.row;
490
+ if (c.column > usedCol) usedCol = c.column;
491
+ }
492
+ }
493
+ if (usedRow < 0 || usedCol < 0) {
494
+ if (print.scaleSink) print.scaleSink.value = computePrintScale(worksheet, Math.round((print.drawingExtentPt?.widthPt ?? 0) * TWIPS_PER_POINT), sheetContentWidthTwips(worksheet), Math.round((print.drawingExtentPt?.heightPt ?? 0) * TWIPS_PER_POINT), sheetContentHeightTwips(worksheet), {
495
+ rowTwips: [],
496
+ rowBreaks: /* @__PURE__ */ new Set(),
497
+ titleTwips: 0,
498
+ colTwips: [],
499
+ colBreaks: /* @__PURE__ */ new Set()
500
+ });
501
+ return [];
502
+ }
503
+ usedCol += overflowColumnsPastUsedRange(worksheet, usedCol, sharedStrings, styles, date1904, charTwipsUnit);
181
504
  let rowStart = 0;
182
505
  let colStart = 0;
183
506
  let rowEnd = usedRow;
@@ -190,6 +513,7 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
190
513
  }
191
514
  if (rowEnd < rowStart || colEnd < colStart) return [];
192
515
  const MAX_GRID_COLS = 16384;
516
+ const SHEET_MAX_ROWS = 1048576;
193
517
  const MAX_GRID_ROWS = 5e4;
194
518
  const MAX_GRID_CELLS = 1e6;
195
519
  const wantRows = rowEnd - rowStart + 1;
@@ -198,21 +522,22 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
198
522
  let rowCount = Math.max(1, Math.min(wantRows, MAX_GRID_ROWS, Math.floor(MAX_GRID_CELLS / colCount)));
199
523
  if (rowCount < wantRows) rowCount = Math.max(1, lastContentRow(worksheet, rowStart, rowCount, colStart, colCount) + 1);
200
524
  if (colCount < wantCols) colCount = Math.max(1, lastContentColumn(worksheet, rowStart, rowCount, colStart, colCount) + 1);
525
+ const reason = (want, limit, unit, last) => want > limit ? `the sheet declares cells past ${unit} ${last}, the last SpreadsheetML defines` : "memory guard";
201
526
  if (rowCount < wantRows) print.losses?.push({
202
527
  severity: "dropped",
203
528
  feature: FEATURES.tables,
204
- detail: `grid clipped to the first ${rowCount} rows of ${wantRows} in the used range (memory guard)`,
529
+ detail: `grid clipped to the first ${rowCount} rows of ${wantRows} in the used range (${reason(rowStart + wantRows, SHEET_MAX_ROWS, "row", String(SHEET_MAX_ROWS))})`,
205
530
  ...print.sheetName ? { where: `sheet "${print.sheetName}"` } : {}
206
531
  });
207
532
  if (colCount < wantCols) print.losses?.push({
208
533
  severity: "dropped",
209
534
  feature: FEATURES.tables,
210
- detail: `grid clipped to the first ${colCount} columns of ${wantCols} in the used range (memory guard)`,
535
+ detail: `grid clipped to the first ${colCount} columns of ${wantCols} in the used range (${reason(colStart + wantCols, MAX_GRID_COLS, "column", "XFD")})`,
211
536
  ...print.sheetName ? { where: `sheet "${print.sheetName}"` } : {}
212
537
  });
213
538
  const rowWindowEnd = rowStart + rowCount - 1;
214
539
  const colWindowEnd = colStart + colCount - 1;
215
- const defaultRowTwips = Math.round((worksheet.defaultRowHeightPt ?? EXCEL_DEFAULT_ROW_HEIGHT_PT) * TWIPS_PER_POINT);
540
+ const defaultRowTwips = Math.round((worksheet.defaultRowHeightPt ?? defaultRowHeightPtFor(print.defaultFontPt)) * TWIPS_PER_POINT);
216
541
  const rowHeightMap = /* @__PURE__ */ new Map();
217
542
  for (let r = 0; r < rowCount; r++) rowHeightMap.set(r, {
218
543
  heightTwips: defaultRowTwips,
@@ -224,7 +549,7 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
224
549
  const heightTwips = Math.round(h.heightPt * TWIPS_PER_POINT);
225
550
  rowHeightMap.set(local, {
226
551
  heightTwips,
227
- heightRule: "atLeast"
552
+ heightRule: h.customHeight ? "exact" : "atLeast"
228
553
  });
229
554
  }
230
555
  const cellMatrix = Array.from({ length: rowCount }, () => new Array(colCount));
@@ -243,22 +568,54 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
243
568
  const cEnd = Math.min(m.endColumn, colWindowEnd);
244
569
  for (let r = Math.max(m.startRow, rowStart); r <= rEnd; r++) for (let c = Math.max(m.startColumn, colStart); c <= cEnd; c++) if (!(r === m.startRow && c === m.startColumn)) insideMerge.add(key(r, c));
245
570
  }
246
- const defaultColChars = worksheet.defaultColWidthChars ?? worksheet.baseColWidthChars;
247
- const defaultColTwips = defaultColChars !== void 0 ? Math.round(defaultColChars * 105 + 75) : 960;
571
+ const defaultColTwips = defaultColumnTwips(worksheet, charTwipsUnit, DEFAULT_COL_CHARS);
248
572
  const columnWidths = new Array(colCount).fill(defaultColTwips);
573
+ const hiddenCols = /* @__PURE__ */ new Set();
249
574
  for (const col of worksheet.columns) {
250
- const twips = Math.round(col.widthChars * 105 + 75);
575
+ const twips = columnTwips(col.widthChars, charTwipsUnit);
251
576
  for (let abs = col.min - 1; abs <= col.max - 1; abs++) {
252
577
  const i = abs - colStart;
253
- if (i >= 0 && i < colCount) columnWidths[i] = twips;
578
+ if (i < 0 || i >= colCount) continue;
579
+ columnWidths[i] = twips;
580
+ if (col.hidden) hiddenCols.add(i);
254
581
  }
255
582
  }
256
- const totalGridTwips = columnWidths.reduce((sum, w) => sum + w, 0);
583
+ const hiddenRows = /* @__PURE__ */ new Set();
584
+ for (const h of worksheet.rowHeights) if (h.hidden) hiddenRows.add(h.row - rowStart);
585
+ const visibleCols = [];
586
+ for (let c = 0; c < colCount; c++) if (!hiddenCols.has(c)) visibleCols.push(c);
587
+ const visibleWidths = visibleCols.map((c) => columnWidths[c]);
588
+ const totalGridTwips = visibleWidths.reduce((sum, w) => sum + w, 0);
257
589
  let totalGridHeightTwips = 0;
258
- for (let r = 0; r < rowCount; r++) totalGridHeightTwips += rowHeightMap.get(r)?.heightTwips ?? 300;
259
- const printScale = computePrintScale(worksheet, totalGridTwips, sheetContentWidthTwips(worksheet), totalGridHeightTwips, sheetContentHeightTwips(worksheet));
590
+ const fitRowTwips = [];
591
+ const fitRowBreaks = /* @__PURE__ */ new Set();
592
+ const manualRowBreaks = new Set(honouredBreaks(worksheet, "rows"));
593
+ let fitTitleTwips = 0;
594
+ for (let r = 0; r < rowCount; r++) {
595
+ if (hiddenRows.has(r)) continue;
596
+ const twips = rowHeightMap.get(r)?.heightTwips ?? 300;
597
+ totalGridHeightTwips += twips;
598
+ const abs = r + rowStart;
599
+ if (manualRowBreaks.has(abs)) fitRowBreaks.add(fitRowTwips.length);
600
+ if (print.titleRows && abs >= print.titleRows.startRow && abs <= print.titleRows.endRow) fitTitleTwips += twips;
601
+ fitRowTwips.push(twips);
602
+ }
603
+ const manualColBreaks = new Set(honouredBreaks(worksheet, "cols"));
604
+ const fitColBreaks = /* @__PURE__ */ new Set();
605
+ visibleCols.forEach((c, i) => {
606
+ if (manualColBreaks.has(c + colStart)) fitColBreaks.add(i);
607
+ });
608
+ const drawing = print.drawingExtentPt;
609
+ const printScale = computePrintScale(worksheet, Math.max(totalGridTwips, Math.round((drawing?.widthPt ?? 0) * TWIPS_PER_POINT)), sheetContentWidthTwips(worksheet), Math.max(totalGridHeightTwips, Math.round((drawing?.heightPt ?? 0) * TWIPS_PER_POINT)), sheetContentHeightTwips(worksheet), {
610
+ rowTwips: fitRowTwips,
611
+ rowBreaks: fitRowBreaks,
612
+ titleTwips: fitTitleTwips,
613
+ colTwips: visibleWidths,
614
+ colBreaks: fitColBreaks
615
+ });
260
616
  const scaled = printScale < .999;
261
- const breakRows = new Set(worksheet.rowBreaks ?? []);
617
+ if (print.scaleSink) print.scaleSink.value = printScale;
618
+ const breakRows = new Set(honouredBreaks(worksheet, "rows"));
262
619
  let textBudget = MAX_SHEET_TEXT_CHARS;
263
620
  let textBudgetReported = false;
264
621
  const runPropsByXf = /* @__PURE__ */ new Map();
@@ -284,22 +641,57 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
284
641
  const sparklineByCell = buildSparklineLookup(worksheet, print);
285
642
  const tableFormatByCell = buildTableFormatLookup(worksheet);
286
643
  const dropdownRanges = listDropdownRanges(worksheet.dataValidations);
644
+ const bandEndOfCol = /* @__PURE__ */ new Map();
645
+ {
646
+ const widthsForBands = scaledColumnWidths(visibleWidths, printScale, scaled);
647
+ const total = widthsForBands.reduce((sum, w) => sum + w, 0);
648
+ const breaks = /* @__PURE__ */ new Set();
649
+ for (const brk of worksheet.colBreaks ?? []) {
650
+ const local = brk - colStart;
651
+ if (local > 0 && local < colCount) breaks.add(local);
652
+ }
653
+ const wide = worksheet.fitToPage ? worksheet.pageSetup?.fitToWidth ?? 1 : 1;
654
+ const width = sheetContentWidthTwips(worksheet);
655
+ if (colCount > 1 && bandsAcross(worksheet, wide) && (total > width || breaks.size > 0)) for (const band of computeColumnBands(widthsForBands, width, breaks)) for (let i = band.start; i <= band.end; i++) bandEndOfCol.set(visibleCols[i] ?? i, visibleCols[band.end] ?? band.end);
656
+ }
287
657
  const rows = [];
658
+ const rowNumbers = [];
659
+ let titleRowIndex = -1;
288
660
  for (let r = 0; r < rowCount; r++) {
661
+ if (hiddenRows.has(r)) continue;
289
662
  const absR = r + rowStart;
290
663
  const cells = [];
291
664
  const overflowed = /* @__PURE__ */ new Set();
665
+ const cellColumns = [];
666
+ const leftOverflow = [];
292
667
  for (let c = 0; c < colCount; c++) {
293
- if (overflowed.has(c)) continue;
668
+ if (hiddenCols.has(c) || overflowed.has(c)) continue;
294
669
  const absC = c + colStart;
295
670
  const merge = mergeOrigins.get(key(absR, absC));
296
671
  if (insideMerge.has(key(absR, absC)) && !merge) {
297
672
  const verticalParent = verticalMergeParent(worksheet.merges, absR, absC);
298
- if (verticalParent && absC === verticalParent.startColumn) cells.push(makeVerticalContinuation(verticalParent, colWindowEnd, absR, rowWindowEnd));
673
+ if (verticalParent && absC === verticalParent.startColumn) {
674
+ const originCell = cellMatrix[verticalParent.startRow - rowStart]?.[verticalParent.startColumn - colStart];
675
+ const originXf = originCell ? styles.cellXfs[originCell.styleIndex ?? 0] : void 0;
676
+ const own = cellMatrix[r]?.[c];
677
+ const ownXf = own ? styles.cellXfs[own.styleIndex ?? 0] : void 0;
678
+ cells.push(makeVerticalContinuation(verticalParent, colWindowEnd, absR, rowWindowEnd, {
679
+ shading: originXf ? shadingFromXf(originXf, styles) : void 0,
680
+ borders: (ownXf ? bordersFromXf(ownXf, styles) : void 0) ?? (originXf ? bordersFromXf(originXf, styles) : void 0)
681
+ }));
682
+ }
299
683
  continue;
300
684
  }
301
685
  const ws = cellMatrix[r]?.[c];
302
686
  let text = ws ? resolveCellText(ws, sharedStrings, styles, date1904) : "";
687
+ if (ws?.type === "n" && ws.rawValue.length > 0 && (styles.cellXfs[ws.styleIndex ?? 0]?.numFmtId ?? 0) === 0) {
688
+ const unit = charTwips(styles.cellXfs[ws.styleIndex ?? 0], styles, textTwipsUnit);
689
+ const digit = charWidthUnits("0");
690
+ if (unit > 0 && digit > 0) {
691
+ const room = columnWidths[c] / unit;
692
+ text = generalToWidth(ws.rawValue, (candidate) => estimateChars(candidate) / digit <= room);
693
+ }
694
+ }
303
695
  const cfText = text.length > 0 ? text : void 0;
304
696
  if (text.length > textBudget) {
305
697
  text = text.slice(0, Math.max(0, textBudget));
@@ -314,8 +706,15 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
314
706
  }
315
707
  }
316
708
  textBudget -= text.length;
317
- const xf = ws && ws.styleIndex !== void 0 ? styles.cellXfs[ws.styleIndex] : void 0;
709
+ const xf = ws ? styles.cellXfs[ws.styleIndex ?? 0] : defaultStyleAt(absR, absC, styles.cellXfs);
318
710
  let runProps = cellRunProps(xf);
711
+ if (ws?.type === "n" && xf?.numFmtId) {
712
+ const fmtColor = numberFormatColorHex(ws.rawValue, xf.numFmtId, styles.numFmts);
713
+ if (fmtColor !== void 0 && fmtColor !== runProps.colorHex) runProps = {
714
+ ...runProps,
715
+ colorHex: fmtColor
716
+ };
717
+ }
319
718
  const alignment = alignmentFromXf(xf, ws?.type);
320
719
  let shading = xf ? shadingFromXf(xf, styles) : void 0;
321
720
  const tableFmt = tableFormatByCell.get(key(absR, absC));
@@ -324,46 +723,74 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
324
723
  ...runProps,
325
724
  colorHex: tableFmt.fontColorHex
326
725
  };
327
- const borders = xf ? bordersFromXf(xf, styles) : void 0;
726
+ let borders = xf ? scaleBorders(bordersFromXf(xf, styles), printScale) : void 0;
328
727
  let dataBar;
329
728
  let icon;
330
729
  const sparkline = sparklineByCell.get(key(absR, absC));
331
730
  if (cfFormatter && ws) {
332
- const over = cfFormatter(absR, absC, ws.type === "n" && Number.isFinite(Number(ws.rawValue)) ? Number(ws.rawValue) : void 0, cfText);
731
+ const blank = ws.rawValue.length === 0 && ws.inlineText === void 0;
732
+ const over = cfFormatter(absR, absC, ws.type === "n" && Number.isFinite(Number(ws.rawValue)) ? Number(ws.rawValue) : void 0, cfText, blank);
333
733
  if (over) {
334
734
  if (over.fillHex) shading = { colorHex: over.fillHex };
335
735
  if (over.dataBar) dataBar = over.dataBar;
736
+ if (over.numberFormat !== void 0 && ws.type === "n" && ws.rawValue.length > 0) text = applyNumberFormat(ws.rawValue, CF_NUMBER_FORMAT_ID, new Map([[CF_NUMBER_FORMAT_ID, over.numberFormat]]), date1904);
737
+ if (over.hideValue) text = "";
336
738
  if (over.icon) icon = over.icon;
739
+ if (over.border) borders = {
740
+ ...borders,
741
+ ...mapXlsxBorder(over.border)
742
+ };
337
743
  runProps = applyCfOverride(runProps, over);
338
744
  }
339
745
  }
340
746
  let overflowSpan = 1;
747
+ let paintColumns = 1;
748
+ let overflowRightRule;
341
749
  const wrapText = xf?.alignment?.wrapText === true;
342
750
  const rotation = xf?.alignment?.textRotation;
343
751
  const rotated = rotation !== void 0 && rotation !== 0 && text.length > 0;
344
752
  const shrinkToFit = xf?.alignment?.shrinkToFit === true && !wrapText && !rotated;
345
- if (text.length > 0 && !merge && !wrapText && !rotated && !shrinkToFit && ws && (ws.type === "s" || ws.type === "str" || ws.type === "inlineStr") && alignment === "left") {
753
+ if (text.length > 0 && !merge && !wrapText && !rotated && !shrinkToFit && ws && (ws.type === "s" || ws.type === "str" || ws.type === "inlineStr") && (alignment === "left" || alignment === "center") && estimateChars(text) * charTwips(xf, styles, textTwipsUnit) > columnWidths[c]) {
346
754
  let availTwips = columnWidths[c];
347
755
  let cc = c + 1;
348
- const neighbourIsFree = (col) => !cellHasContent(cellMatrix[r]?.[col]) && !cellPaintsSomething(cellMatrix[r]?.[col], styles) && !sparklineByCell.has(key(absR, col + colStart)) && !(dropdownRanges.length > 0 && rangesCover(dropdownRanges, absR, col + colStart));
349
- while (cc < colCount && neighbourIsFree(cc)) {
350
- availTwips += columnWidths[cc];
756
+ const neighbourIsFree = (col) => !cellHasContent(cellMatrix[r]?.[col]) && !insideMerge.has(key(absR, col + colStart)) && !mergeOrigins.has(key(absR, col + colStart)) && (!cellPaintsSomething(cellMatrix[r]?.[col], styles) || spanPreservesPaint(cellMatrix[r]?.[col], styles, shading, borders)) && !sparklineByCell.has(key(absR, col + colStart)) && !(dropdownRanges.length > 0 && rangesCover(dropdownRanges, absR, col + colStart));
757
+ const bandEnd = bandEndOfCol.get(c) ?? colCount - 1;
758
+ while (cc <= bandEnd && (hiddenCols.has(cc) || neighbourIsFree(cc))) {
759
+ if (!hiddenCols.has(cc)) availTwips += columnWidths[cc];
351
760
  cc++;
352
761
  }
353
- if (cc < colCount) {
354
- const charsFit = Math.max(1, Math.round(availTwips / 105));
355
- if (text.length > charsFit) text = text.slice(0, charsFit);
762
+ if (cc <= bandEnd && !cellHasContent(cellMatrix[r]?.[cc]) && !sparklineByCell.has(key(absR, cc + colStart)) && !(dropdownRanges.length > 0 && rangesCover(dropdownRanges, absR, cc + colStart))) {
763
+ const rule = absorbableRightRule(cellMatrix[r]?.[cc], styles, shading, borders);
764
+ if (rule) {
765
+ availTwips += columnWidths[cc];
766
+ overflowRightRule = rule;
767
+ cc++;
768
+ }
356
769
  }
770
+ text = clipToWidth(text, availTwips * 1.1, charTwips(xf, styles, textTwipsUnit));
357
771
  if (cc > c + 1) {
358
- overflowSpan = cc - c;
772
+ overflowSpan = 0;
773
+ for (let k = c; k < cc; k++) if (!hiddenCols.has(k)) overflowSpan++;
359
774
  for (let k = c + 1; k < cc; k++) overflowed.add(k);
775
+ paintColumns = 1;
776
+ for (let k = c + 1; k < cc; k++) {
777
+ if (hiddenCols.has(k)) continue;
778
+ if (!cellPaintsSomething(cellMatrix[r]?.[k], styles)) break;
779
+ paintColumns++;
780
+ }
360
781
  }
782
+ } else if (text.length > 0 && merge && !wrapText && !rotated && !shrinkToFit) {
783
+ const lastCol = Math.min(merge.endColumn - colStart, bandEndOfCol.get(c) ?? colCount - 1);
784
+ let availTwips = 0;
785
+ for (let k = c; k <= lastCol; k++) if (!hiddenCols.has(k)) availTwips += columnWidths[k] ?? 0;
786
+ text = clipToWidth(text, availTwips * 1.1, charTwips(xf, styles, textTwipsUnit));
361
787
  }
362
788
  if (shrinkToFit && ws && text.length > 0) {
363
- const charsFit = Math.max(1, columnWidths[c] / 105);
364
- if (text.length > charsFit) {
789
+ const drawnTwips = estimateChars(text) / charWidthUnits("0") * charTwips(xf, styles, textTwipsUnit);
790
+ const room = columnWidths[c];
791
+ if (drawnTwips > room) {
365
792
  const baseHalfPt = (styles.fonts[xf.fontId]?.sizePt ?? 11) * 2;
366
- const scaledHalfPt = Math.max(2, Math.round(baseHalfPt * charsFit / text.length));
793
+ const scaledHalfPt = Math.max(2, Math.floor(baseHalfPt * room / drawnTwips));
367
794
  runProps = {
368
795
  ...runProps,
369
796
  fontSizePt: halfPtToPt(scaledHalfPt)
@@ -373,15 +800,28 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
373
800
  const dropdown = dropdownRanges.length > 0 && rangesCover(dropdownRanges, absR, absC);
374
801
  const href = print.hyperlinks && print.hyperlinks.length > 0 ? hyperlinkUrlAt(print.hyperlinks, absR, absC) : void 0;
375
802
  const visibleEndCol = merge ? Math.min(merge.endColumn, colWindowEnd) : 0;
803
+ let mergeSpan = 0;
804
+ if (merge && visibleEndCol > merge.startColumn) {
805
+ for (let a = merge.startColumn; a <= visibleEndCol; a++) if (!hiddenCols.has(a - colStart)) mergeSpan++;
806
+ }
376
807
  const properties = {
377
- ...merge && visibleEndCol > merge.startColumn ? { colSpan: visibleEndCol - merge.startColumn + 1 } : overflowSpan > 1 ? { colSpan: overflowSpan } : {},
808
+ ...mergeSpan > 1 ? { colSpan: mergeSpan } : overflowSpan > 1 ? {
809
+ colSpan: overflowSpan,
810
+ paintColumns
811
+ } : {},
378
812
  ...merge && Math.min(merge.endRow, rowWindowEnd) > merge.startRow ? { merge: "start" } : {},
379
813
  ...shading ? { shading } : {},
380
814
  ...dataBar ? { dataBar } : {},
381
815
  ...icon ? { icon } : {},
382
816
  ...sparkline ? { sparkline } : {},
383
- ...borders ? { borders } : {},
384
- ...dropdown ? { dropdown: true } : {}
817
+ ...borders || overflowRightRule ? { borders: {
818
+ ...borders,
819
+ ...overflowRightRule ? { right: overflowRightRule } : {}
820
+ } } : {},
821
+ ...dropdown ? { dropdown: true } : {},
822
+ ...!wrapText && !rotated ? { noWrap: true } : {},
823
+ ...!wrapText && !rotated && !shrinkToFit && !merge && ws?.type === "n" && (xf?.numFmtId ?? 0) !== 0 && text.length > 0 && tooWideToShow(text, charTwips(xf, styles, textTwipsUnit), columnWidths[c]) ? { hashOnOverflow: true } : {},
824
+ verticalAlign: verticalAlignOf(xf)
385
825
  };
386
826
  const indentLevels = xf?.alignment?.indent ?? 0;
387
827
  const baseParaProps = cellParaProps(alignment);
@@ -399,29 +839,49 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
399
839
  properties: runProps,
400
840
  ...href ? { href } : {}
401
841
  }] : [];
402
- const content = rotated ? stackedVerticalContent(text, runProps, href) : cellRuns.length === 0 ? [] : [{
842
+ const content = rotated ? stackedVerticalContent(text, runProps, href) : cellRuns.length === 0 ? [] : splitCellLines(cellRuns, wrapText).map((runs) => ({
403
843
  kind: "paragraph",
404
844
  paragraph: {
405
845
  properties: paragraphProps,
406
- runs: cellRuns
846
+ runs
407
847
  }
408
- }];
848
+ }));
849
+ if (text.length > 0 && !merge && !wrapText && !rotated && !shrinkToFit && c > 0 && alignment === "right" && estimateChars(text) * charTwips(xf, styles, textTwipsUnit) > columnWidths[c]) leftOverflow.push({
850
+ index: cells.length,
851
+ column: c,
852
+ needTwips: estimateChars(text) * charTwips(xf, styles, textTwipsUnit) - columnWidths[c],
853
+ shading,
854
+ borders
855
+ });
856
+ cellColumns.push(c);
409
857
  cells.push({
410
858
  properties,
411
859
  content
412
860
  });
413
861
  }
862
+ absorbLeftwards(cells, cellColumns, leftOverflow, styles, (col) => {
863
+ if (insideMerge.has(key(absR, col)) || mergeOrigins.has(key(absR, col))) return void 0;
864
+ if (sparklineByCell.has(key(absR, col + colStart))) return void 0;
865
+ if (dropdownRanges.length > 0 && rangesCover(dropdownRanges, absR, col + colStart)) return;
866
+ if (cellHasContent(cellMatrix[r]?.[col])) return void 0;
867
+ return {
868
+ cell: cellMatrix[r]?.[col],
869
+ widthTwips: columnWidths[col]
870
+ };
871
+ });
414
872
  const baseRowProps = rowHeightMap.get(r);
415
873
  const rowHeightTwips = scaled && baseRowProps !== void 0 ? Math.max(1, Math.round(baseRowProps.heightTwips * printScale)) : baseRowProps?.heightTwips;
416
874
  const isTitleRow = print.titleRows !== void 0 && absR >= print.titleRows.startRow && absR <= print.titleRows.endRow;
417
875
  const rowProps = {
418
876
  ...rowHeightTwips !== void 0 ? {
419
877
  height: twipsToPt(rowHeightTwips),
420
- heightRule: "atLeast"
878
+ heightRule: baseRowProps?.heightRule ?? "atLeast"
421
879
  } : {},
422
880
  ...isTitleRow ? { isHeader: true } : {},
423
881
  ...breakRows.has(absR) ? { pageBreakBefore: true } : {}
424
882
  };
883
+ if (isTitleRow && titleRowIndex < 0) titleRowIndex = rows.length;
884
+ rowNumbers.push(absR + 1);
425
885
  rows.push({
426
886
  properties: rowProps,
427
887
  cells
@@ -429,13 +889,14 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
429
889
  }
430
890
  const thin = {
431
891
  style: "single",
432
- width: eighthPtToPt(4)
892
+ width: eighthPtToPt(2),
893
+ colorHex: PRINT_GRIDLINE_HEX
433
894
  };
434
895
  const centered = worksheet.printOptions?.horizontalCentered === true;
435
896
  const tableProperties = {
436
897
  defaultCellMargins: {
437
- left: pt(EXCEL_CELL_INSET_PT),
438
- right: pt(EXCEL_CELL_INSET_PT),
898
+ left: pt(EXCEL_CELL_INSET_PT * printScale),
899
+ right: pt(EXCEL_CELL_INSET_PT * printScale),
439
900
  top: pt(0),
440
901
  bottom: pt(0)
441
902
  },
@@ -457,16 +918,42 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
457
918
  if (local > 0 && local < colCount) colBreaksLocal.add(local);
458
919
  }
459
920
  const fitWide = worksheet.fitToPage ? worksheet.pageSetup?.fitToWidth ?? 1 : 1;
460
- const bandWidths = scaled ? columnWidths.map((w) => Math.max(1, Math.floor(w * printScale))) : columnWidths;
921
+ const bandWidths = scaledColumnWidths(visibleWidths, printScale, scaled);
461
922
  const bandTotal = bandWidths.reduce((sum, w) => sum + w, 0);
462
- if (colCount > 1 && (!scaled || fitWide > 1) && (bandTotal > contentWidthTwips || colBreaksLocal.size > 0)) {
923
+ if (colCount > 1 && bandsAcross(worksheet, fitWide) && (bandTotal > contentWidthTwips || colBreaksLocal.size > 0)) {
463
924
  const bands = computeColumnBands(bandWidths, contentWidthTwips, colBreaksLocal);
464
- if (bands.length > 1) return bandedTables(rows, bandWidths, bands, tableProperties);
925
+ if (bands.length > 1) {
926
+ if (print.bandSink) print.bandSink.lefts = bands.map((band) => twipsToPt(bandWidths.slice(0, band.start).reduce((sum, w) => sum + w, 0)));
927
+ return bandedTables(rows, bandWidths, bands, tableProperties, titleRowIndex, Math.round((print.drawingExtentPt?.widthPt ?? 0) * TWIPS_PER_POINT * printScale), worksheet.printOptions?.headings ? {
928
+ colStart,
929
+ rowNumbers
930
+ } : void 0);
931
+ }
465
932
  }
466
933
  const frozen = worksheet.pane && (worksheet.pane.frozenRows > 0 || worksheet.pane.frozenCols > 0) ? {
467
934
  rows: worksheet.pane.frozenRows,
468
935
  cols: worksheet.pane.frozenCols
469
936
  } : void 0;
937
+ if (titleRowIndex > 0 && titleRowIndex < rows.length) {
938
+ const titleStart = titleRowIndex;
939
+ const grid = bandWidths.map((w) => twipsToPt(w));
940
+ return [{
941
+ kind: "table",
942
+ table: {
943
+ properties: tableProperties,
944
+ grid,
945
+ rows: rows.slice(0, titleStart)
946
+ }
947
+ }, {
948
+ kind: "table",
949
+ table: {
950
+ properties: tableProperties,
951
+ grid,
952
+ rows: rows.slice(titleStart)
953
+ }
954
+ }];
955
+ }
956
+ const headed = worksheet.printOptions?.headings ? withHeadingBand(rows, bandWidths, colStart, rowNumbers) : void 0;
470
957
  return [{
471
958
  kind: "table",
472
959
  table: {
@@ -474,20 +961,22 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
474
961
  ...tableProperties,
475
962
  frozen
476
963
  } : tableProperties,
477
- grid: bandWidths.map((w) => twipsToPt(w)),
478
- rows
964
+ grid: (headed?.widths ?? bandWidths).map((w) => twipsToPt(w)),
965
+ rows: headed?.rows ?? rows
479
966
  }
480
967
  }];
481
968
  }
482
969
  function verticalMergeParent(merges, row, column) {
483
970
  for (const m of merges) if (column >= m.startColumn && column <= m.endColumn && row > m.startRow && row <= m.endRow && m.endRow > m.startRow) return m;
484
971
  }
485
- function makeVerticalContinuation(merge, colWindowEnd, absR, rowWindowEnd) {
972
+ function makeVerticalContinuation(merge, colWindowEnd, absR, rowWindowEnd, paint) {
486
973
  const span = Math.min(merge.endColumn, colWindowEnd) - merge.startColumn + 1;
487
974
  return {
488
975
  properties: {
489
976
  merge: absR < Math.min(merge.endRow, rowWindowEnd) ? "middle" : "end",
490
- ...span > 1 ? { colSpan: span } : {}
977
+ ...span > 1 ? { colSpan: span } : {},
978
+ ...paint.shading ? { shading: paint.shading } : {},
979
+ ...paint.borders ? { borders: paint.borders } : {}
491
980
  },
492
981
  content: [{
493
982
  kind: "paragraph",
@@ -518,16 +1007,17 @@ function resolveCellText(cell, sharedStrings, styles, date1904) {
518
1007
  }
519
1008
  if (cell.type === "b") return cell.rawValue === "1" ? "TRUE" : "FALSE";
520
1009
  if (cell.type === "str" || cell.type === "e" || cell.type === "d") return cell.rawValue;
521
- const numFmtId = (cell.styleIndex !== void 0 ? styles.cellXfs[cell.styleIndex] : void 0)?.numFmtId ?? 0;
1010
+ const numFmtId = styles.cellXfs[cell.styleIndex ?? 0]?.numFmtId ?? 0;
522
1011
  return applyNumberFormat(cell.rawValue, numFmtId, styles.numFmts, date1904);
523
1012
  }
524
1013
  function runPropsFromXf(xf, styles) {
525
- if (!xf.applyFont && xf.fontId === 0) return {};
526
1014
  const font = styles.fonts[xf.fontId];
527
1015
  if (!font) return {};
528
1016
  const props = {};
529
1017
  if (font.bold) props.bold = true;
530
1018
  if (font.italic) props.italic = true;
1019
+ if (font.underline) props.underline = "single";
1020
+ if (font.strike) props.strike = true;
531
1021
  if (font.sizePt !== void 0) props.fontSizePt = halfPtToPt(Math.round(font.sizePt * 2));
532
1022
  if (font.colorHex) props.colorHex = font.colorHex;
533
1023
  return props;
@@ -549,9 +1039,10 @@ function stackedVerticalContent(text, runProps, href) {
549
1039
  }
550
1040
  function richRunProps(base, rr) {
551
1041
  const out = { ...base };
552
- if (rr.bold) out.bold = true;
553
- if (rr.italic) out.italic = true;
1042
+ if (rr.bold !== void 0) out.bold = rr.bold;
1043
+ if (rr.italic !== void 0) out.italic = rr.italic;
554
1044
  if (rr.underline) out.underline = "single";
1045
+ if (rr.strike !== void 0) out.strike = rr.strike;
555
1046
  if (rr.colorHex) out.colorHex = rr.colorHex;
556
1047
  if (rr.sizePt !== void 0) out.fontSizePt = halfPtToPt(Math.round(rr.sizePt * 2));
557
1048
  if (rr.vertAlign) out.verticalAlign = rr.vertAlign;
@@ -562,7 +1053,8 @@ function applyCfOverride(base, o) {
562
1053
  ...base,
563
1054
  ...o.fontColorHex ? { colorHex: o.fontColorHex } : {},
564
1055
  ...o.bold !== void 0 ? { bold: o.bold } : {},
565
- ...o.italic !== void 0 ? { italic: o.italic } : {}
1056
+ ...o.italic !== void 0 ? { italic: o.italic } : {},
1057
+ ...o.strike !== void 0 ? { strike: o.strike } : {}
566
1058
  };
567
1059
  }
568
1060
  /**
@@ -668,6 +1160,42 @@ function bordersFromXf(xf, styles) {
668
1160
  }
669
1161
  return Object.keys(out).length > 0 ? out : void 0;
670
1162
  }
1163
+ /** An {@link XlsxBorder}'s four edges as {@link CellBorders} — a dxf's, or a cell's. */
1164
+ function mapXlsxBorder(border) {
1165
+ const out = {};
1166
+ const top = mapBorderEdge(border.top);
1167
+ const right = mapBorderEdge(border.right);
1168
+ const bottom = mapBorderEdge(border.bottom);
1169
+ const left = mapBorderEdge(border.left);
1170
+ if (top) out.top = top;
1171
+ if (right) out.right = right;
1172
+ if (bottom) out.bottom = bottom;
1173
+ if (left) out.left = left;
1174
+ return out;
1175
+ }
1176
+ /** Every edge of a cell's box, at the print scale. */
1177
+ function scaleBorders(borders, scale) {
1178
+ if (!borders || scale >= .999) return borders;
1179
+ const edge = (b) => b === void 0 || b.width === void 0 ? b : {
1180
+ ...b,
1181
+ width: pt(b.width * scale)
1182
+ };
1183
+ const out = {};
1184
+ for (const side of [
1185
+ "top",
1186
+ "right",
1187
+ "bottom",
1188
+ "left",
1189
+ "insideH",
1190
+ "insideV"
1191
+ ]) {
1192
+ const e = edge(borders[side]);
1193
+ if (e) out[side] = e;
1194
+ }
1195
+ if (borders.diagonalUp !== void 0) out.diagonalUp = borders.diagonalUp;
1196
+ if (borders.diagonalDown !== void 0) out.diagonalDown = borders.diagonalDown;
1197
+ return out;
1198
+ }
671
1199
  function mapBorderEdge(edge) {
672
1200
  if (!edge || !edge.style || edge.style === "none") return void 0;
673
1201
  const { style, sizeEighthPt } = mapBorderStyle(edge.style);
@@ -681,41 +1209,56 @@ function mapBorderStyle(style) {
681
1209
  switch (style) {
682
1210
  case "hair": return {
683
1211
  style: "single",
684
- sizeEighthPt: 2
1212
+ sizeEighthPt: 3
685
1213
  };
686
1214
  case "thin": return {
687
1215
  style: "single",
688
- sizeEighthPt: 4
1216
+ sizeEighthPt: 6
1217
+ };
1218
+ case "medium": return {
1219
+ style: "single",
1220
+ sizeEighthPt: 12
1221
+ };
1222
+ case "mediumDashed": return {
1223
+ style: "dashed",
1224
+ sizeEighthPt: 12
689
1225
  };
690
- case "medium":
691
- case "mediumDashed":
692
1226
  case "mediumDashDot":
1227
+ case "slantDashDot": return {
1228
+ style: "dashDot",
1229
+ sizeEighthPt: 12
1230
+ };
693
1231
  case "mediumDashDotDot": return {
694
- style: "single",
695
- sizeEighthPt: 8
1232
+ style: "dashDotDot",
1233
+ sizeEighthPt: 12
696
1234
  };
697
1235
  case "thick": return {
698
1236
  style: "thick",
699
- sizeEighthPt: 12
1237
+ sizeEighthPt: 18
700
1238
  };
701
- case "dashed":
702
- case "dashDot":
703
- case "dashDotDot":
704
- case "slantDashDot": return {
705
- style: "dashed",
706
- sizeEighthPt: 4
1239
+ case "dashed": return {
1240
+ style: "dashSmallGap",
1241
+ sizeEighthPt: 6
1242
+ };
1243
+ case "dashDot": return {
1244
+ style: "dashDot",
1245
+ sizeEighthPt: 6
1246
+ };
1247
+ case "dashDotDot": return {
1248
+ style: "dashDotDot",
1249
+ sizeEighthPt: 6
707
1250
  };
708
1251
  case "dotted": return {
709
1252
  style: "dotted",
710
- sizeEighthPt: 4
1253
+ sizeEighthPt: 6
711
1254
  };
712
1255
  case "double": return {
713
1256
  style: "double",
714
- sizeEighthPt: 4
1257
+ sizeEighthPt: 18
715
1258
  };
716
1259
  default: return {
717
1260
  style: "single",
718
- sizeEighthPt: 4
1261
+ sizeEighthPt: 6
719
1262
  };
720
1263
  }
721
1264
  }
@@ -890,12 +1433,290 @@ function lastContentColumn(worksheet, rowStart, rowCount, colStart, colCount) {
890
1433
  * Such a cell cannot be swallowed by a neighbour's overflowing text: the span
891
1434
  * that gives the text its width would take the paint with it.
892
1435
  */
1436
+ /**
1437
+ * Whether a cell puts anything VISIBLE on an otherwise-empty page.
1438
+ *
1439
+ * The difference from {@link cellPaintsSomething} is white: a solid fill in the
1440
+ * paper's own colour paints nothing a reader can see, and a sheet of them is a
1441
+ * blank sheet. bnc762542.xlsx is 176 such cells — `fgColor indexed="9"`, the
1442
+ * legacy palette's white — with every word it prints living in a callout, and
1443
+ * both references print the callout on a bare page. 48779.xlsx is one cell of
1444
+ * solid red, and both print the swatch.
1445
+ *
1446
+ * @param cell The cell, or undefined.
1447
+ * @param styles The workbook's style tables.
1448
+ * @returns True when the cell draws something the eye can find.
1449
+ */
1450
+ function cellPaintsVisibly(cell, styles) {
1451
+ if (!cell || cell.styleIndex === void 0) return false;
1452
+ const xf = styles.cellXfs[cell.styleIndex];
1453
+ if (!xf) return false;
1454
+ if (bordersFromXf(xf, styles) !== void 0) return true;
1455
+ const shading = shadingFromXf(xf, styles);
1456
+ return shading !== void 0 && shading.colorHex.toUpperCase() !== "FFFFFF";
1457
+ }
893
1458
  function cellPaintsSomething(cell, styles) {
894
1459
  if (!cell || cell.styleIndex === void 0) return false;
895
1460
  const xf = styles.cellXfs[cell.styleIndex];
896
1461
  if (!xf) return false;
897
1462
  return shadingFromXf(xf, styles) !== void 0 || bordersFromXf(xf, styles) !== void 0;
898
1463
  }
1464
+ /**
1465
+ * How many empty columns past the used range the last column's text needs to
1466
+ * run into, bounded by the printable width (Excel stops at the page edge too).
1467
+ *
1468
+ * Only the last used column can want them — anywhere else the grid already has
1469
+ * neighbours. Zero for the overwhelming majority of sheets, which keeps their
1470
+ * projection exactly as it was.
1471
+ */
1472
+ function overflowColumnsPastUsedRange(worksheet, usedCol, sharedStrings, styles, date1904, charTwipsUnit) {
1473
+ const defaultTwips = defaultColumnTwips(worksheet, charTwipsUnit, DEFAULT_COL_CHARS);
1474
+ const widthOf = (abs) => {
1475
+ for (const col of worksheet.columns) {
1476
+ if (abs < col.min - 1 || abs > col.max - 1) continue;
1477
+ return col.hidden ? 0 : columnTwips(col.widthChars, charTwipsUnit);
1478
+ }
1479
+ return defaultTwips;
1480
+ };
1481
+ const lastOfRow = /* @__PURE__ */ new Map();
1482
+ for (const cell of worksheet.cells) {
1483
+ if (!cellHasContent(cell)) continue;
1484
+ const prev = lastOfRow.get(cell.row);
1485
+ if (!prev || cell.column > prev.column) lastOfRow.set(cell.row, cell);
1486
+ }
1487
+ let needTwips = 0;
1488
+ for (const cell of lastOfRow.values()) {
1489
+ if (!(cell.type === "s" || cell.type === "str" || cell.type === "inlineStr")) continue;
1490
+ const xf = styles.cellXfs[cell.styleIndex ?? 0];
1491
+ const align = xf?.alignment;
1492
+ if (align?.wrapText || align?.shrinkToFit || align?.textRotation) continue;
1493
+ if (alignmentFromXf(xf, cell.type) !== "left") continue;
1494
+ let room = 0;
1495
+ for (let abs = cell.column; abs <= usedCol; abs++) room += widthOf(abs);
1496
+ const text = resolveCellText(cell, sharedStrings, styles, date1904);
1497
+ needTwips = Math.max(needTwips, estimateChars(text) * charTwips(xf, styles, 105) - room);
1498
+ }
1499
+ if (needTwips <= 0) return 0;
1500
+ let gridTwips = 0;
1501
+ for (let abs = 0; abs <= usedCol; abs++) gridTwips += widthOf(abs);
1502
+ const limit = sheetContentWidthTwips(worksheet);
1503
+ const MAX_OVERFLOW_COLS = 256;
1504
+ let extra = 0;
1505
+ while (needTwips > 0 && extra < MAX_OVERFLOW_COLS) {
1506
+ const w = widthOf(usedCol + extra + 1);
1507
+ if (w <= 0 || gridTwips + w > limit) break;
1508
+ gridTwips += w;
1509
+ needTwips -= w;
1510
+ extra++;
1511
+ }
1512
+ return extra;
1513
+ }
1514
+ /** Column widths as the bands see them: shrunk by the print scale, floor-rounded. */
1515
+ function scaledColumnWidths(widths, printScale, scaled) {
1516
+ return scaled ? widths.map((w) => Math.max(1, Math.floor(w * printScale))) : [...widths];
1517
+ }
1518
+ /**
1519
+ * Break a cell's runs at the hard line breaks inside them, or — for a cell that
1520
+ * does not wrap, and so shows one line — drop those breaks.
1521
+ *
1522
+ * Dropped, not turned into a space. A line feed is a control character with no
1523
+ * width of its own: switch wrapping off in Excel and the two lines run
1524
+ * together, which is the complaint behind every "my Alt+Enter disappeared"
1525
+ * question. preserve_space.xlsx holds a bold "123", a break and a plain "456",
1526
+ * and the reference prints "123456" where we printed "123 456" — a space the
1527
+ * document does not contain.
1528
+ */
1529
+ function splitCellLines(runs, wrapText) {
1530
+ if (!runs.some((r) => /[\r\n]/.test(r.text))) return [[...runs]];
1531
+ if (!wrapText) return [runs.map((r) => ({
1532
+ ...r,
1533
+ text: r.text.replace(/[\r\n]+/g, "")
1534
+ }))];
1535
+ const lines = [[]];
1536
+ for (const run of runs) {
1537
+ const parts = run.text.split(/\r\n|\r|\n/);
1538
+ for (let i = 0; i < parts.length; i++) {
1539
+ if (i > 0) lines.push([]);
1540
+ const text = parts[i];
1541
+ if (text.length > 0) lines[lines.length - 1].push({
1542
+ ...run,
1543
+ text
1544
+ });
1545
+ }
1546
+ }
1547
+ return lines.some((l) => l.length > 0) ? lines : [];
1548
+ }
1549
+ /** §18.8.1 `<alignment vertical>`, defaulting to Excel's bottom. */
1550
+ function verticalAlignOf(xf) {
1551
+ const v = xf?.alignment?.vertical;
1552
+ if (v === "top") return "top";
1553
+ if (v === "center") return "center";
1554
+ if (v === "justify" || v === "distributed") return "top";
1555
+ return "bottom";
1556
+ }
1557
+ /**
1558
+ * The width of one character of a cell's own font, in twips.
1559
+ *
1560
+ * {@link TWIPS_PER_EXCEL_CHAR} is the Maximum Digit Width of the DEFAULT 11pt
1561
+ * font, and column widths are measured in exactly those units — but the text
1562
+ * inside a cell is not necessarily written in it. Measuring a 7pt annotation by
1563
+ * the 11pt yardstick makes it read as half again as wide as it is, and the
1564
+ * overflow clip then cuts it in the middle of a word: tdf171828.xlsx lost the
1565
+ * "eff. Zins =" half of a label that fits its band perfectly well.
1566
+ */
1567
+ function charTwips(xf, styles, unit) {
1568
+ const sizePt = (xf ? styles.fonts[xf.fontId]?.sizePt : void 0) ?? DEFAULT_FONT_PT;
1569
+ if (!Number.isFinite(sizePt) || sizePt <= 0) return unit;
1570
+ return unit * sizePt / DEFAULT_FONT_PT;
1571
+ }
1572
+ var NARROW_CHARS = /* @__PURE__ */ new Set(" .,:;'\"!|il");
1573
+ var SEMI_NARROW_CHARS = /* @__PURE__ */ new Set("ftr()[]{}-/\\");
1574
+ var WIDE_CHARS = /* @__PURE__ */ new Set("MWmw@%");
1575
+ function isFullWidth(code) {
1576
+ return code >= 4352 && code <= 4447 || code >= 11904 && code <= 12350 || code >= 12353 && code <= 13311 || code >= 13312 && code <= 19903 || code >= 19968 && code <= 40959 || code >= 40960 && code <= 42191 || code >= 43360 && code <= 43391 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65040 && code <= 65135 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 131072 && code <= 262141;
1577
+ }
1578
+ function charWidthUnits(ch) {
1579
+ if (NARROW_CHARS.has(ch)) return .53;
1580
+ if (SEMI_NARROW_CHARS.has(ch)) return .71;
1581
+ if (WIDE_CHARS.has(ch)) return 1.77;
1582
+ if (ch >= "a" && ch <= "z") return 1.12;
1583
+ if (ch >= "A" && ch <= "Z") return 1.36;
1584
+ const code = ch.codePointAt(0);
1585
+ if (code !== void 0 && isFullWidth(code)) return 2;
1586
+ return 1.18;
1587
+ }
1588
+ /**
1589
+ * Whether a number is too wide for its column to show it at all — the test
1590
+ * behind {@link CellProperties.hashOnOverflow}.
1591
+ *
1592
+ * Two corrections separate this from the clipping estimate, and both are about
1593
+ * how destructive the answer is. Filling a cell with `#` erases the value, so
1594
+ * the test has to be sure.
1595
+ *
1596
+ * `charWidthUnits` measures in the font we DRAW in, expressed in Excel's unit —
1597
+ * right for deciding what fits on the page, wrong for deciding what the
1598
+ * document itself considers too wide, since §18.3.1.13 defines that unit AS the
1599
+ * digit's width. Dividing by our own digit puts the text back in the document's
1600
+ * terms: a face 18 % wider than the workbook's would otherwise hash a column of
1601
+ * dates that every other reader shows (forum-mso-de-104083.xlsx).
1602
+ *
1603
+ * And then a tenth of headroom, because the estimate is an estimate. A value
1604
+ * that overruns by one percent is a value we should draw and let the cell clip;
1605
+ * only one that plainly cannot fit earns the hashes.
1606
+ */
1607
+ function tooWideToShow(text, perCharTwips, columnTwips) {
1608
+ const digit = charWidthUnits("0");
1609
+ if (!(digit > 0) || !(perCharTwips > 0)) return false;
1610
+ return estimateChars(text) / digit * perCharTwips > columnTwips * 1.1;
1611
+ }
1612
+ /** Width of `text` in column-width units, estimated character by character. */
1613
+ function estimateChars(text) {
1614
+ let n = 0;
1615
+ for (const ch of text) n += charWidthUnits(ch);
1616
+ return n;
1617
+ }
1618
+ /** The longest prefix of `text` that fits `availTwips`, by the same estimate. */
1619
+ function clipToWidth(text, availTwips, perChar) {
1620
+ if (perChar <= 0) return text;
1621
+ const budget = availTwips / perChar;
1622
+ let used = 0;
1623
+ let i = 0;
1624
+ for (const ch of text) {
1625
+ const w = charWidthUnits(ch);
1626
+ if (used + w > budget) break;
1627
+ used += w;
1628
+ i += ch.length;
1629
+ }
1630
+ return i >= text.length ? text : text.slice(0, Math.max(1, i));
1631
+ }
1632
+ /**
1633
+ * Widen each right-aligned cell that overflows leftwards over the free columns
1634
+ * before it, deleting the cells those columns produced.
1635
+ *
1636
+ * The mirror of the rightward overflow the row loop does inline, but it cannot
1637
+ * be done inline: the space a right-aligned cell needs lies behind it, in cells
1638
+ * already emitted. Same freeness rule as rightwards — nothing in the column,
1639
+ * and nothing painted there that the span would erase.
1640
+ */
1641
+ function absorbLeftwards(cells, cellColumns, candidates, styles, neighbourAt) {
1642
+ for (let k = candidates.length - 1; k >= 0; k--) {
1643
+ const cand = candidates[k];
1644
+ let need = cand.needTwips;
1645
+ let first = cand.index;
1646
+ while (first > 0 && need > 0) {
1647
+ const prev = first - 1;
1648
+ const col = cellColumns[prev];
1649
+ if ((cells[prev].properties.colSpan ?? 1) !== 1) break;
1650
+ if (cellColumns[first] !== col + 1) break;
1651
+ const free = neighbourAt(col);
1652
+ if (!free) break;
1653
+ if (!spanPreservesPaint(free.cell, styles, cand.shading, cand.borders)) break;
1654
+ need -= free.widthTwips;
1655
+ first = prev;
1656
+ }
1657
+ if (first === cand.index) continue;
1658
+ const span = cand.index - first + 1;
1659
+ const cell = cells[cand.index];
1660
+ cells.splice(first, span, {
1661
+ ...cell,
1662
+ properties: {
1663
+ ...cell.properties,
1664
+ colSpan: (cell.properties.colSpan ?? 1) + span - 1
1665
+ }
1666
+ });
1667
+ cellColumns.splice(first, span, cellColumns[first]);
1668
+ }
1669
+ }
1670
+ /**
1671
+ * Whether spanning a cell's overflowing text across `neighbour` would leave the
1672
+ * page looking the same as drawing over it would.
1673
+ *
1674
+ * A span paints the ORIGIN's fill and the ORIGIN's outer borders across the
1675
+ * whole run, so it is lossless exactly when the neighbour's fill already
1676
+ * matches, the neighbour carries no vertical rule of its own to erase, and its
1677
+ * horizontal edges are the ones the origin will redraw anyway. A block of empty
1678
+ * cells continuing a filled, top-ruled band — which is what a decorated
1679
+ * neighbour almost always is — passes; a differently filled or boxed cell does
1680
+ * not, and the text is clipped short of it instead.
1681
+ */
1682
+ function spanPreservesPaint(neighbour, styles, shading, borders) {
1683
+ if (!neighbour || neighbour.styleIndex === void 0) return true;
1684
+ const xf = styles.cellXfs[neighbour.styleIndex];
1685
+ if (!xf) return true;
1686
+ if (shadingFromXf(xf, styles)?.colorHex !== shading?.colorHex) return false;
1687
+ const nb = bordersFromXf(xf, styles);
1688
+ if (!nb) return true;
1689
+ if (nb.left || nb.right || nb.insideV || nb.diagonalUp || nb.diagonalDown) return false;
1690
+ return sameBorder(nb.top, borders?.top) && sameBorder(nb.bottom, borders?.bottom);
1691
+ }
1692
+ function sameBorder(a, b) {
1693
+ if (a === void 0 || b === void 0) return a === b;
1694
+ return a.style === b.style && a.width === b.width && a.colorHex === b.colorHex;
1695
+ }
1696
+ /**
1697
+ * The right rule a neighbour carries when that rule is the ONLY thing standing
1698
+ * between it and the overflowing text — in which case the span can simply adopt
1699
+ * it as its own right border and nothing is lost.
1700
+ *
1701
+ * {@link spanPreservesPaint} refuses any vertical rule, which is right for a
1702
+ * rule INSIDE the run (a span erases it) but wrong for the one that closes it:
1703
+ * the last cell's right edge is the boundary of the band the origin sits in,
1704
+ * and the span's own right border lands in exactly the same place. Refusing it
1705
+ * cut five labels short on every page of tdf171828.xlsx — "ohne Sondertilgung"
1706
+ * as "ohne Sondertil" — where the reference runs each of them to that very
1707
+ * edge.
1708
+ */
1709
+ function absorbableRightRule(neighbour, styles, shading, borders) {
1710
+ if (!neighbour || neighbour.styleIndex === void 0) return void 0;
1711
+ const xf = styles.cellXfs[neighbour.styleIndex];
1712
+ if (!xf) return void 0;
1713
+ if (shadingFromXf(xf, styles)?.colorHex !== shading?.colorHex) return void 0;
1714
+ const nb = bordersFromXf(xf, styles);
1715
+ if (!nb?.right) return void 0;
1716
+ if (nb.left || nb.insideV || nb.diagonalUp || nb.diagonalDown) return void 0;
1717
+ if (!sameBorder(nb.top, borders?.top) || !sameBorder(nb.bottom, borders?.bottom)) return;
1718
+ return nb.right;
1719
+ }
899
1720
  var SLICER_WIDTH_PT = 108;
900
1721
  var SLICER_ROW_PT = 16;
901
1722
  var SLICER_UNSELECTED_HEX = "F2F2F2";
@@ -910,7 +1731,8 @@ function slicerTable(slicer) {
910
1731
  const colWidthPt = SLICER_WIDTH_PT / cols;
911
1732
  const thin = {
912
1733
  style: "single",
913
- width: eighthPtToPt(4)
1734
+ width: eighthPtToPt(2),
1735
+ colorHex: PRINT_GRIDLINE_HEX
914
1736
  };
915
1737
  const rowProps = {
916
1738
  height: pt(SLICER_ROW_PT),
@@ -979,4 +1801,4 @@ function slicerParagraph(text, runProps) {
979
1801
  };
980
1802
  }
981
1803
  //#endregion
982
- export { resolveCellText, resolvePrintArea, resolvePrintTitleRows, sectionFromWorksheet, slicerTable, worksheetToBody };
1804
+ export { cellPaintsVisibly, columnTwips, printableHeightPt, printableWidthPt, resolveCellText, resolvePrintArea, resolvePrintTitleRows, sectionFromWorksheet, slicerTable, worksheetToBody };