reamkit 1.15.3 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/esm/core/converter/facade.d.ts +5 -0
  2. package/dist/esm/core/converter/ream.d.ts +6 -0
  3. package/dist/esm/core/converter/ream.js +34 -8
  4. package/dist/esm/core/document-model/index.d.ts +1 -1
  5. package/dist/esm/core/document-model/types.d.ts +187 -1
  6. package/dist/esm/core/drawingml/chart-geometry.d.ts +28 -0
  7. package/dist/esm/core/drawingml/chart-geometry.js +371 -88
  8. package/dist/esm/core/drawingml/chart-parser.js +262 -11
  9. package/dist/esm/core/drawingml/colors.d.ts +6 -6
  10. package/dist/esm/core/drawingml/colors.js +58 -18
  11. package/dist/esm/core/drawingml/preset-geometry.js +7 -6
  12. package/dist/esm/core/drawingml/sparkline-geometry.js +6 -4
  13. package/dist/esm/core/drawingml/theme-parser.d.ts +41 -0
  14. package/dist/esm/core/drawingml/theme-parser.js +74 -1
  15. package/dist/esm/core/font/ttf-subset.js +26 -1
  16. package/dist/esm/core/images.js +172 -43
  17. package/dist/esm/core/indexed-colors.d.ts +11 -0
  18. package/dist/esm/core/indexed-colors.js +81 -0
  19. package/dist/esm/core/ir/sheet.d.ts +67 -3
  20. package/dist/esm/{excel → core}/number-format.d.ts +37 -0
  21. package/dist/esm/core/number-format.js +800 -0
  22. package/dist/esm/core/opc/alternate-content.d.ts +13 -0
  23. package/dist/esm/core/opc/alternate-content.js +72 -0
  24. package/dist/esm/core/opc/core-properties.js +1 -0
  25. package/dist/esm/core/opc/package.d.ts +14 -1
  26. package/dist/esm/core/opc/package.js +43 -4
  27. package/dist/esm/core/opc/relationships.js +1 -0
  28. package/dist/esm/core/opc/xml-entities.d.ts +12 -0
  29. package/dist/esm/core/opc/xml-entities.js +91 -0
  30. package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
  31. package/dist/esm/core/spreadsheet-model/types.d.ts +137 -0
  32. package/dist/esm/core/vector.d.ts +13 -0
  33. package/dist/esm/excel/activex-parser.d.ts +9 -0
  34. package/dist/esm/excel/activex-parser.js +16 -1
  35. package/dist/esm/excel/column-bands.d.ts +26 -1
  36. package/dist/esm/excel/column-bands.js +156 -20
  37. package/dist/esm/excel/comments-parser.js +5 -3
  38. package/dist/esm/excel/conditional-format.d.ts +25 -2
  39. package/dist/esm/excel/conditional-format.js +63 -19
  40. package/dist/esm/excel/escaped-text.d.ts +9 -0
  41. package/dist/esm/excel/escaped-text.js +20 -0
  42. package/dist/esm/excel/form-control-parser.js +1 -0
  43. package/dist/esm/excel/formula/dates.js +1 -1
  44. package/dist/esm/excel/header-footer.d.ts +9 -1
  45. package/dist/esm/excel/header-footer.js +162 -14
  46. package/dist/esm/excel/index.d.ts +1 -1
  47. package/dist/esm/excel/index.js +2 -2
  48. package/dist/esm/excel/pivot-table-parser.js +1 -0
  49. package/dist/esm/excel/print-model.d.ts +75 -0
  50. package/dist/esm/excel/print-model.js +902 -77
  51. package/dist/esm/excel/printer-settings.d.ts +16 -0
  52. package/dist/esm/excel/printer-settings.js +36 -0
  53. package/dist/esm/excel/rich-value.d.ts +13 -0
  54. package/dist/esm/excel/rich-value.js +120 -0
  55. package/dist/esm/excel/shared-strings-parser.js +11 -7
  56. package/dist/esm/excel/sheet-drawing.d.ts +11 -0
  57. package/dist/esm/excel/sheet-drawing.js +26 -3
  58. package/dist/esm/excel/sheet-shape-parser.d.ts +53 -1
  59. package/dist/esm/excel/sheet-shape-parser.js +461 -32
  60. package/dist/esm/excel/sheet-to-flow.d.ts +23 -2
  61. package/dist/esm/excel/sheet-to-flow.js +551 -36
  62. package/dist/esm/excel/slicer-parser.js +1 -0
  63. package/dist/esm/excel/styles-parser.d.ts +30 -2
  64. package/dist/esm/excel/styles-parser.js +177 -40
  65. package/dist/esm/excel/table-parser.js +1 -0
  66. package/dist/esm/excel/vml-drawing.d.ts +68 -0
  67. package/dist/esm/excel/vml-drawing.js +158 -0
  68. package/dist/esm/excel/workbook-parser.d.ts +5 -0
  69. package/dist/esm/excel/workbook-parser.js +5 -1
  70. package/dist/esm/excel/worksheet-parser.d.ts +2 -1
  71. package/dist/esm/excel/worksheet-parser.js +149 -28
  72. package/dist/esm/excel/xlsx-reader.js +271 -33
  73. package/dist/esm/excel/xlsx-to-pdf.js +12 -1
  74. package/dist/esm/html/html-writer.js +11 -2
  75. package/dist/esm/layout/page-doc.d.ts +26 -0
  76. package/dist/esm/layout/styled-layout.d.ts +2 -0
  77. package/dist/esm/layout/styled-layout.js +493 -64
  78. package/dist/esm/pdf/cid-font.js +20 -9
  79. package/dist/esm/pdf/objects.d.ts +13 -0
  80. package/dist/esm/pdf/objects.js +18 -1
  81. package/dist/esm/pdf/styled-page-emitter.js +164 -32
  82. package/dist/esm/pdf/text-page-renderer.js +1 -0
  83. package/dist/esm/pdf/vector-graphics.d.ts +3 -1
  84. package/dist/esm/pdf/vector-graphics.js +19 -2
  85. package/dist/esm/pptx/pptx-reader.js +1 -0
  86. package/dist/esm/pptx/slide-parser.js +12 -1
  87. package/dist/esm/svg/svg-writer.js +23 -4
  88. package/dist/esm/word/document-parser.js +1 -0
  89. package/dist/esm/word/drawing-parser.d.ts +34 -1
  90. package/dist/esm/word/drawing-parser.js +52 -2
  91. package/dist/esm/word/numbering-parser.js +1 -0
  92. package/dist/esm/word/settings-parser.js +1 -0
  93. package/dist/esm/word/styles-parser.js +1 -0
  94. package/package.json +7 -3
  95. package/dist/esm/excel/number-format.js +0 -476
@@ -1,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,13 +417,26 @@ 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;
432
+ const contentAt = /* @__PURE__ */ new Set();
163
433
  for (const c of worksheet.cells) if (c.rawValue !== "" || c.inlineText !== void 0) {
434
+ contentAt.add(key(c.row, c.column));
164
435
  if (c.row > usedRow) usedRow = c.row;
165
436
  if (c.column > usedCol) usedCol = c.column;
166
437
  }
167
438
  for (const m of worksheet.merges) {
439
+ if (!contentAt.has(key(m.startRow, m.startColumn))) continue;
168
440
  if (m.endRow > usedRow) usedRow = m.endRow;
169
441
  if (m.endColumn > usedCol) usedCol = m.endColumn;
170
442
  }
@@ -174,7 +446,61 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
174
446
  if (host.startRow > usedRow) usedRow = host.startRow;
175
447
  if (host.startColumn > usedCol) usedCol = host.startColumn;
176
448
  }
177
- 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);
178
504
  let rowStart = 0;
179
505
  let colStart = 0;
180
506
  let rowEnd = usedRow;
@@ -186,7 +512,8 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
186
512
  colEnd = Math.min(usedCol, print.printArea.endColumn);
187
513
  }
188
514
  if (rowEnd < rowStart || colEnd < colStart) return [];
189
- const MAX_GRID_COLS = 1024;
515
+ const MAX_GRID_COLS = 16384;
516
+ const SHEET_MAX_ROWS = 1048576;
190
517
  const MAX_GRID_ROWS = 5e4;
191
518
  const MAX_GRID_CELLS = 1e6;
192
519
  const wantRows = rowEnd - rowStart + 1;
@@ -195,21 +522,22 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
195
522
  let rowCount = Math.max(1, Math.min(wantRows, MAX_GRID_ROWS, Math.floor(MAX_GRID_CELLS / colCount)));
196
523
  if (rowCount < wantRows) rowCount = Math.max(1, lastContentRow(worksheet, rowStart, rowCount, colStart, colCount) + 1);
197
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";
198
526
  if (rowCount < wantRows) print.losses?.push({
199
527
  severity: "dropped",
200
528
  feature: FEATURES.tables,
201
- 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))})`,
202
530
  ...print.sheetName ? { where: `sheet "${print.sheetName}"` } : {}
203
531
  });
204
532
  if (colCount < wantCols) print.losses?.push({
205
533
  severity: "dropped",
206
534
  feature: FEATURES.tables,
207
- 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")})`,
208
536
  ...print.sheetName ? { where: `sheet "${print.sheetName}"` } : {}
209
537
  });
210
538
  const rowWindowEnd = rowStart + rowCount - 1;
211
539
  const colWindowEnd = colStart + colCount - 1;
212
- 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);
213
541
  const rowHeightMap = /* @__PURE__ */ new Map();
214
542
  for (let r = 0; r < rowCount; r++) rowHeightMap.set(r, {
215
543
  heightTwips: defaultRowTwips,
@@ -221,7 +549,7 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
221
549
  const heightTwips = Math.round(h.heightPt * TWIPS_PER_POINT);
222
550
  rowHeightMap.set(local, {
223
551
  heightTwips,
224
- heightRule: "atLeast"
552
+ heightRule: h.customHeight ? "exact" : "atLeast"
225
553
  });
226
554
  }
227
555
  const cellMatrix = Array.from({ length: rowCount }, () => new Array(colCount));
@@ -240,22 +568,54 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
240
568
  const cEnd = Math.min(m.endColumn, colWindowEnd);
241
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));
242
570
  }
243
- const defaultColChars = worksheet.defaultColWidthChars ?? worksheet.baseColWidthChars;
244
- const defaultColTwips = defaultColChars !== void 0 ? Math.round(defaultColChars * 105 + 75) : 960;
571
+ const defaultColTwips = defaultColumnTwips(worksheet, charTwipsUnit, DEFAULT_COL_CHARS);
245
572
  const columnWidths = new Array(colCount).fill(defaultColTwips);
573
+ const hiddenCols = /* @__PURE__ */ new Set();
246
574
  for (const col of worksheet.columns) {
247
- const twips = Math.round(col.widthChars * 105 + 75);
575
+ const twips = columnTwips(col.widthChars, charTwipsUnit);
248
576
  for (let abs = col.min - 1; abs <= col.max - 1; abs++) {
249
577
  const i = abs - colStart;
250
- 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);
251
581
  }
252
582
  }
253
- 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);
254
589
  let totalGridHeightTwips = 0;
255
- for (let r = 0; r < rowCount; r++) totalGridHeightTwips += rowHeightMap.get(r)?.heightTwips ?? 300;
256
- 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
+ });
257
616
  const scaled = printScale < .999;
258
- const breakRows = new Set(worksheet.rowBreaks ?? []);
617
+ if (print.scaleSink) print.scaleSink.value = printScale;
618
+ const breakRows = new Set(honouredBreaks(worksheet, "rows"));
259
619
  let textBudget = MAX_SHEET_TEXT_CHARS;
260
620
  let textBudgetReported = false;
261
621
  const runPropsByXf = /* @__PURE__ */ new Map();
@@ -281,22 +641,57 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
281
641
  const sparklineByCell = buildSparklineLookup(worksheet, print);
282
642
  const tableFormatByCell = buildTableFormatLookup(worksheet);
283
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
+ }
284
657
  const rows = [];
658
+ const rowNumbers = [];
659
+ let titleRowIndex = -1;
285
660
  for (let r = 0; r < rowCount; r++) {
661
+ if (hiddenRows.has(r)) continue;
286
662
  const absR = r + rowStart;
287
663
  const cells = [];
288
664
  const overflowed = /* @__PURE__ */ new Set();
665
+ const cellColumns = [];
666
+ const leftOverflow = [];
289
667
  for (let c = 0; c < colCount; c++) {
290
- if (overflowed.has(c)) continue;
668
+ if (hiddenCols.has(c) || overflowed.has(c)) continue;
291
669
  const absC = c + colStart;
292
670
  const merge = mergeOrigins.get(key(absR, absC));
293
671
  if (insideMerge.has(key(absR, absC)) && !merge) {
294
672
  const verticalParent = verticalMergeParent(worksheet.merges, absR, absC);
295
- 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
+ }
296
683
  continue;
297
684
  }
298
685
  const ws = cellMatrix[r]?.[c];
299
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
+ }
300
695
  const cfText = text.length > 0 ? text : void 0;
301
696
  if (text.length > textBudget) {
302
697
  text = text.slice(0, Math.max(0, textBudget));
@@ -311,8 +706,15 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
311
706
  }
312
707
  }
313
708
  textBudget -= text.length;
314
- 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);
315
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
+ }
316
718
  const alignment = alignmentFromXf(xf, ws?.type);
317
719
  let shading = xf ? shadingFromXf(xf, styles) : void 0;
318
720
  const tableFmt = tableFormatByCell.get(key(absR, absC));
@@ -321,46 +723,74 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
321
723
  ...runProps,
322
724
  colorHex: tableFmt.fontColorHex
323
725
  };
324
- const borders = xf ? bordersFromXf(xf, styles) : void 0;
726
+ let borders = xf ? scaleBorders(bordersFromXf(xf, styles), printScale) : void 0;
325
727
  let dataBar;
326
728
  let icon;
327
729
  const sparkline = sparklineByCell.get(key(absR, absC));
328
730
  if (cfFormatter && ws) {
329
- 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);
330
733
  if (over) {
331
734
  if (over.fillHex) shading = { colorHex: over.fillHex };
332
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 = "";
333
738
  if (over.icon) icon = over.icon;
739
+ if (over.border) borders = {
740
+ ...borders,
741
+ ...mapXlsxBorder(over.border)
742
+ };
334
743
  runProps = applyCfOverride(runProps, over);
335
744
  }
336
745
  }
337
746
  let overflowSpan = 1;
747
+ let paintColumns = 1;
748
+ let overflowRightRule;
338
749
  const wrapText = xf?.alignment?.wrapText === true;
339
750
  const rotation = xf?.alignment?.textRotation;
340
751
  const rotated = rotation !== void 0 && rotation !== 0 && text.length > 0;
341
752
  const shrinkToFit = xf?.alignment?.shrinkToFit === true && !wrapText && !rotated;
342
- 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]) {
343
754
  let availTwips = columnWidths[c];
344
755
  let cc = c + 1;
345
- 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));
346
- while (cc < colCount && neighbourIsFree(cc)) {
347
- 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];
348
760
  cc++;
349
761
  }
350
- if (cc < colCount) {
351
- const charsFit = Math.max(1, Math.round(availTwips / 105));
352
- 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
+ }
353
769
  }
770
+ text = clipToWidth(text, availTwips * 1.1, charTwips(xf, styles, textTwipsUnit));
354
771
  if (cc > c + 1) {
355
- overflowSpan = cc - c;
772
+ overflowSpan = 0;
773
+ for (let k = c; k < cc; k++) if (!hiddenCols.has(k)) overflowSpan++;
356
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
+ }
357
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));
358
787
  }
359
788
  if (shrinkToFit && ws && text.length > 0) {
360
- const charsFit = Math.max(1, columnWidths[c] / 105);
361
- if (text.length > charsFit) {
789
+ const drawnTwips = estimateChars(text) / charWidthUnits("0") * charTwips(xf, styles, textTwipsUnit);
790
+ const room = columnWidths[c];
791
+ if (drawnTwips > room) {
362
792
  const baseHalfPt = (styles.fonts[xf.fontId]?.sizePt ?? 11) * 2;
363
- const scaledHalfPt = Math.max(2, Math.round(baseHalfPt * charsFit / text.length));
793
+ const scaledHalfPt = Math.max(2, Math.floor(baseHalfPt * room / drawnTwips));
364
794
  runProps = {
365
795
  ...runProps,
366
796
  fontSizePt: halfPtToPt(scaledHalfPt)
@@ -370,15 +800,28 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
370
800
  const dropdown = dropdownRanges.length > 0 && rangesCover(dropdownRanges, absR, absC);
371
801
  const href = print.hyperlinks && print.hyperlinks.length > 0 ? hyperlinkUrlAt(print.hyperlinks, absR, absC) : void 0;
372
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
+ }
373
807
  const properties = {
374
- ...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
+ } : {},
375
812
  ...merge && Math.min(merge.endRow, rowWindowEnd) > merge.startRow ? { merge: "start" } : {},
376
813
  ...shading ? { shading } : {},
377
814
  ...dataBar ? { dataBar } : {},
378
815
  ...icon ? { icon } : {},
379
816
  ...sparkline ? { sparkline } : {},
380
- ...borders ? { borders } : {},
381
- ...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)
382
825
  };
383
826
  const indentLevels = xf?.alignment?.indent ?? 0;
384
827
  const baseParaProps = cellParaProps(alignment);
@@ -396,29 +839,49 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
396
839
  properties: runProps,
397
840
  ...href ? { href } : {}
398
841
  }] : [];
399
- 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) => ({
400
843
  kind: "paragraph",
401
844
  paragraph: {
402
845
  properties: paragraphProps,
403
- runs: cellRuns
846
+ runs
404
847
  }
405
- }];
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);
406
857
  cells.push({
407
858
  properties,
408
859
  content
409
860
  });
410
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
+ });
411
872
  const baseRowProps = rowHeightMap.get(r);
412
873
  const rowHeightTwips = scaled && baseRowProps !== void 0 ? Math.max(1, Math.round(baseRowProps.heightTwips * printScale)) : baseRowProps?.heightTwips;
413
874
  const isTitleRow = print.titleRows !== void 0 && absR >= print.titleRows.startRow && absR <= print.titleRows.endRow;
414
875
  const rowProps = {
415
876
  ...rowHeightTwips !== void 0 ? {
416
877
  height: twipsToPt(rowHeightTwips),
417
- heightRule: "atLeast"
878
+ heightRule: baseRowProps?.heightRule ?? "atLeast"
418
879
  } : {},
419
880
  ...isTitleRow ? { isHeader: true } : {},
420
881
  ...breakRows.has(absR) ? { pageBreakBefore: true } : {}
421
882
  };
883
+ if (isTitleRow && titleRowIndex < 0) titleRowIndex = rows.length;
884
+ rowNumbers.push(absR + 1);
422
885
  rows.push({
423
886
  properties: rowProps,
424
887
  cells
@@ -426,13 +889,14 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
426
889
  }
427
890
  const thin = {
428
891
  style: "single",
429
- width: eighthPtToPt(4)
892
+ width: eighthPtToPt(2),
893
+ colorHex: PRINT_GRIDLINE_HEX
430
894
  };
431
895
  const centered = worksheet.printOptions?.horizontalCentered === true;
432
896
  const tableProperties = {
433
897
  defaultCellMargins: {
434
- left: pt(EXCEL_CELL_INSET_PT),
435
- right: pt(EXCEL_CELL_INSET_PT),
898
+ left: pt(EXCEL_CELL_INSET_PT * printScale),
899
+ right: pt(EXCEL_CELL_INSET_PT * printScale),
436
900
  top: pt(0),
437
901
  bottom: pt(0)
438
902
  },
@@ -454,16 +918,42 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
454
918
  if (local > 0 && local < colCount) colBreaksLocal.add(local);
455
919
  }
456
920
  const fitWide = worksheet.fitToPage ? worksheet.pageSetup?.fitToWidth ?? 1 : 1;
457
- const bandWidths = scaled ? columnWidths.map((w) => Math.max(1, Math.floor(w * printScale))) : columnWidths;
921
+ const bandWidths = scaledColumnWidths(visibleWidths, printScale, scaled);
458
922
  const bandTotal = bandWidths.reduce((sum, w) => sum + w, 0);
459
- if (colCount > 1 && (!scaled || fitWide > 1) && (bandTotal > contentWidthTwips || colBreaksLocal.size > 0)) {
923
+ if (colCount > 1 && bandsAcross(worksheet, fitWide) && (bandTotal > contentWidthTwips || colBreaksLocal.size > 0)) {
460
924
  const bands = computeColumnBands(bandWidths, contentWidthTwips, colBreaksLocal);
461
- 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
+ }
462
932
  }
463
933
  const frozen = worksheet.pane && (worksheet.pane.frozenRows > 0 || worksheet.pane.frozenCols > 0) ? {
464
934
  rows: worksheet.pane.frozenRows,
465
935
  cols: worksheet.pane.frozenCols
466
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;
467
957
  return [{
468
958
  kind: "table",
469
959
  table: {
@@ -471,20 +961,22 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
471
961
  ...tableProperties,
472
962
  frozen
473
963
  } : tableProperties,
474
- grid: bandWidths.map((w) => twipsToPt(w)),
475
- rows
964
+ grid: (headed?.widths ?? bandWidths).map((w) => twipsToPt(w)),
965
+ rows: headed?.rows ?? rows
476
966
  }
477
967
  }];
478
968
  }
479
969
  function verticalMergeParent(merges, row, column) {
480
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;
481
971
  }
482
- function makeVerticalContinuation(merge, colWindowEnd, absR, rowWindowEnd) {
972
+ function makeVerticalContinuation(merge, colWindowEnd, absR, rowWindowEnd, paint) {
483
973
  const span = Math.min(merge.endColumn, colWindowEnd) - merge.startColumn + 1;
484
974
  return {
485
975
  properties: {
486
976
  merge: absR < Math.min(merge.endRow, rowWindowEnd) ? "middle" : "end",
487
- ...span > 1 ? { colSpan: span } : {}
977
+ ...span > 1 ? { colSpan: span } : {},
978
+ ...paint.shading ? { shading: paint.shading } : {},
979
+ ...paint.borders ? { borders: paint.borders } : {}
488
980
  },
489
981
  content: [{
490
982
  kind: "paragraph",
@@ -515,16 +1007,17 @@ function resolveCellText(cell, sharedStrings, styles, date1904) {
515
1007
  }
516
1008
  if (cell.type === "b") return cell.rawValue === "1" ? "TRUE" : "FALSE";
517
1009
  if (cell.type === "str" || cell.type === "e" || cell.type === "d") return cell.rawValue;
518
- const numFmtId = (cell.styleIndex !== void 0 ? styles.cellXfs[cell.styleIndex] : void 0)?.numFmtId ?? 0;
1010
+ const numFmtId = styles.cellXfs[cell.styleIndex ?? 0]?.numFmtId ?? 0;
519
1011
  return applyNumberFormat(cell.rawValue, numFmtId, styles.numFmts, date1904);
520
1012
  }
521
1013
  function runPropsFromXf(xf, styles) {
522
- if (!xf.applyFont && xf.fontId === 0) return {};
523
1014
  const font = styles.fonts[xf.fontId];
524
1015
  if (!font) return {};
525
1016
  const props = {};
526
1017
  if (font.bold) props.bold = true;
527
1018
  if (font.italic) props.italic = true;
1019
+ if (font.underline) props.underline = "single";
1020
+ if (font.strike) props.strike = true;
528
1021
  if (font.sizePt !== void 0) props.fontSizePt = halfPtToPt(Math.round(font.sizePt * 2));
529
1022
  if (font.colorHex) props.colorHex = font.colorHex;
530
1023
  return props;
@@ -546,9 +1039,10 @@ function stackedVerticalContent(text, runProps, href) {
546
1039
  }
547
1040
  function richRunProps(base, rr) {
548
1041
  const out = { ...base };
549
- if (rr.bold) out.bold = true;
550
- 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;
551
1044
  if (rr.underline) out.underline = "single";
1045
+ if (rr.strike !== void 0) out.strike = rr.strike;
552
1046
  if (rr.colorHex) out.colorHex = rr.colorHex;
553
1047
  if (rr.sizePt !== void 0) out.fontSizePt = halfPtToPt(Math.round(rr.sizePt * 2));
554
1048
  if (rr.vertAlign) out.verticalAlign = rr.vertAlign;
@@ -559,7 +1053,8 @@ function applyCfOverride(base, o) {
559
1053
  ...base,
560
1054
  ...o.fontColorHex ? { colorHex: o.fontColorHex } : {},
561
1055
  ...o.bold !== void 0 ? { bold: o.bold } : {},
562
- ...o.italic !== void 0 ? { italic: o.italic } : {}
1056
+ ...o.italic !== void 0 ? { italic: o.italic } : {},
1057
+ ...o.strike !== void 0 ? { strike: o.strike } : {}
563
1058
  };
564
1059
  }
565
1060
  /**
@@ -665,6 +1160,42 @@ function bordersFromXf(xf, styles) {
665
1160
  }
666
1161
  return Object.keys(out).length > 0 ? out : void 0;
667
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
+ }
668
1199
  function mapBorderEdge(edge) {
669
1200
  if (!edge || !edge.style || edge.style === "none") return void 0;
670
1201
  const { style, sizeEighthPt } = mapBorderStyle(edge.style);
@@ -678,41 +1209,56 @@ function mapBorderStyle(style) {
678
1209
  switch (style) {
679
1210
  case "hair": return {
680
1211
  style: "single",
681
- sizeEighthPt: 2
1212
+ sizeEighthPt: 3
682
1213
  };
683
1214
  case "thin": return {
684
1215
  style: "single",
685
- 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
686
1225
  };
687
- case "medium":
688
- case "mediumDashed":
689
1226
  case "mediumDashDot":
1227
+ case "slantDashDot": return {
1228
+ style: "dashDot",
1229
+ sizeEighthPt: 12
1230
+ };
690
1231
  case "mediumDashDotDot": return {
691
- style: "single",
692
- sizeEighthPt: 8
1232
+ style: "dashDotDot",
1233
+ sizeEighthPt: 12
693
1234
  };
694
1235
  case "thick": return {
695
1236
  style: "thick",
696
- sizeEighthPt: 12
1237
+ sizeEighthPt: 18
697
1238
  };
698
- case "dashed":
699
- case "dashDot":
700
- case "dashDotDot":
701
- case "slantDashDot": return {
1239
+ case "dashed": return {
702
1240
  style: "dashed",
703
- sizeEighthPt: 4
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
704
1250
  };
705
1251
  case "dotted": return {
706
1252
  style: "dotted",
707
- sizeEighthPt: 4
1253
+ sizeEighthPt: 6
708
1254
  };
709
1255
  case "double": return {
710
1256
  style: "double",
711
- sizeEighthPt: 4
1257
+ sizeEighthPt: 18
712
1258
  };
713
1259
  default: return {
714
1260
  style: "single",
715
- sizeEighthPt: 4
1261
+ sizeEighthPt: 6
716
1262
  };
717
1263
  }
718
1264
  }
@@ -887,12 +1433,290 @@ function lastContentColumn(worksheet, rowStart, rowCount, colStart, colCount) {
887
1433
  * Such a cell cannot be swallowed by a neighbour's overflowing text: the span
888
1434
  * that gives the text its width would take the paint with it.
889
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
+ }
890
1458
  function cellPaintsSomething(cell, styles) {
891
1459
  if (!cell || cell.styleIndex === void 0) return false;
892
1460
  const xf = styles.cellXfs[cell.styleIndex];
893
1461
  if (!xf) return false;
894
1462
  return shadingFromXf(xf, styles) !== void 0 || bordersFromXf(xf, styles) !== void 0;
895
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
+ }
896
1720
  var SLICER_WIDTH_PT = 108;
897
1721
  var SLICER_ROW_PT = 16;
898
1722
  var SLICER_UNSELECTED_HEX = "F2F2F2";
@@ -907,7 +1731,8 @@ function slicerTable(slicer) {
907
1731
  const colWidthPt = SLICER_WIDTH_PT / cols;
908
1732
  const thin = {
909
1733
  style: "single",
910
- width: eighthPtToPt(4)
1734
+ width: eighthPtToPt(2),
1735
+ colorHex: PRINT_GRIDLINE_HEX
911
1736
  };
912
1737
  const rowProps = {
913
1738
  height: pt(SLICER_ROW_PT),
@@ -976,4 +1801,4 @@ function slicerParagraph(text, runProps) {
976
1801
  };
977
1802
  }
978
1803
  //#endregion
979
- export { resolveCellText, resolvePrintArea, resolvePrintTitleRows, sectionFromWorksheet, slicerTable, worksheetToBody };
1804
+ export { cellPaintsVisibly, columnTwips, printableHeightPt, printableWidthPt, resolveCellText, resolvePrintArea, resolvePrintTitleRows, sectionFromWorksheet, slicerTable, worksheetToBody };