@rowsncolumns/toolkit 24.4.2 → 24.4.4

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 (166) hide show
  1. package/dist/cjs/excel-parser/cfb-decryptor.d.ts +6 -0
  2. package/dist/cjs/excel-parser/cfb-decryptor.d.ts.map +1 -1
  3. package/dist/cjs/excel-parser/cfb-decryptor.js +6 -0
  4. package/dist/cjs/excel-parser/helpers.d.ts +9 -1
  5. package/dist/cjs/excel-parser/helpers.d.ts.map +1 -1
  6. package/dist/cjs/excel-parser/helpers.js +37 -0
  7. package/dist/cjs/excel-parser/interface.d.ts +15 -2
  8. package/dist/cjs/excel-parser/interface.d.ts.map +1 -1
  9. package/dist/cjs/excel-parser/plugins/base/index.d.ts +2 -0
  10. package/dist/cjs/excel-parser/plugins/base/index.d.ts.map +1 -1
  11. package/dist/cjs/excel-parser/plugins/base/index.js +3 -0
  12. package/dist/cjs/excel-parser/plugins/ods/index.d.ts +19 -0
  13. package/dist/cjs/excel-parser/plugins/ods/index.d.ts.map +1 -1
  14. package/dist/cjs/excel-parser/plugins/ods/index.js +676 -304
  15. package/dist/cjs/excel-parser/plugins/ods/styles.d.ts +43 -1
  16. package/dist/cjs/excel-parser/plugins/ods/styles.d.ts.map +1 -1
  17. package/dist/cjs/excel-parser/plugins/ods/styles.js +343 -45
  18. package/dist/cjs/excel-parser/plugins/xls/index.d.ts +55 -1
  19. package/dist/cjs/excel-parser/plugins/xls/index.d.ts.map +1 -1
  20. package/dist/cjs/excel-parser/plugins/xls/index.js +518 -6
  21. package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts +4 -0
  22. package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
  23. package/dist/cjs/excel-parser/plugins/xlsb/index.js +33 -14
  24. package/dist/cjs/excel-parser/plugins/xlsx/chartex.d.ts +37 -0
  25. package/dist/cjs/excel-parser/plugins/xlsx/chartex.d.ts.map +1 -0
  26. package/dist/cjs/excel-parser/plugins/xlsx/chartex.js +244 -0
  27. package/dist/cjs/excel-parser/plugins/xlsx/charts.d.ts +10 -0
  28. package/dist/cjs/excel-parser/plugins/xlsx/charts.d.ts.map +1 -1
  29. package/dist/cjs/excel-parser/plugins/xlsx/charts.js +68 -9
  30. package/dist/cjs/excel-parser/plugins/xlsx/drawing.d.ts +28 -1
  31. package/dist/cjs/excel-parser/plugins/xlsx/drawing.d.ts.map +1 -1
  32. package/dist/cjs/excel-parser/plugins/xlsx/drawing.js +155 -20
  33. package/dist/cjs/excel-parser/plugins/xlsx/form-controls.d.ts +70 -0
  34. package/dist/cjs/excel-parser/plugins/xlsx/form-controls.d.ts.map +1 -0
  35. package/dist/cjs/excel-parser/plugins/xlsx/form-controls.js +295 -0
  36. package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts +35 -2
  37. package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
  38. package/dist/cjs/excel-parser/plugins/xlsx/index.js +737 -257
  39. package/dist/cjs/excel-parser/plugins/xlsx/pivots.d.ts.map +1 -1
  40. package/dist/cjs/excel-parser/plugins/xlsx/pivots.js +125 -37
  41. package/dist/cjs/excel-parser/plugins/xlsx/sheet-views.d.ts +3 -0
  42. package/dist/cjs/excel-parser/plugins/xlsx/sheet-views.d.ts.map +1 -1
  43. package/dist/cjs/excel-parser/plugins/xlsx/sheet-views.js +4 -1
  44. package/dist/cjs/excel-parser/plugins/xlsx/styles.d.ts +15 -1
  45. package/dist/cjs/excel-parser/plugins/xlsx/styles.d.ts.map +1 -1
  46. package/dist/cjs/excel-parser/plugins/xlsx/styles.js +102 -6
  47. package/dist/cjs/excel-parser/registry.d.ts +7 -0
  48. package/dist/cjs/excel-parser/registry.d.ts.map +1 -1
  49. package/dist/cjs/excel-parser/registry.js +29 -0
  50. package/dist/cjs/excel-parser/types.d.ts +17 -2
  51. package/dist/cjs/excel-parser/types.d.ts.map +1 -1
  52. package/dist/cjs/excel-parser/workbook.d.ts +3 -0
  53. package/dist/cjs/excel-parser/workbook.d.ts.map +1 -1
  54. package/dist/cjs/excel-parser/workbook.js +24 -14
  55. package/dist/cjs/excel-parser/worker.js +131 -131
  56. package/dist/cjs/exporter/ods/ods-export.d.ts.map +1 -1
  57. package/dist/cjs/exporter/ods/ods-export.js +432 -175
  58. package/dist/cjs/exporter/xlsx/chart-builder.d.ts.map +1 -1
  59. package/dist/cjs/exporter/xlsx/chart-builder.js +215 -39
  60. package/dist/cjs/exporter/xlsx/chartex-builder.d.ts +36 -0
  61. package/dist/cjs/exporter/xlsx/chartex-builder.d.ts.map +1 -0
  62. package/dist/cjs/exporter/xlsx/chartex-builder.js +109 -0
  63. package/dist/cjs/exporter/xlsx/form-controls-builder.d.ts +61 -0
  64. package/dist/cjs/exporter/xlsx/form-controls-builder.d.ts.map +1 -0
  65. package/dist/cjs/exporter/xlsx/form-controls-builder.js +197 -0
  66. package/dist/cjs/exporter/xlsx/pivot-builder.d.ts +8 -0
  67. package/dist/cjs/exporter/xlsx/pivot-builder.d.ts.map +1 -1
  68. package/dist/cjs/exporter/xlsx/pivot-builder.js +141 -42
  69. package/dist/cjs/exporter/xlsx/style-builder.js +9 -3
  70. package/dist/cjs/exporter/xlsx/threaded-comments-builder.d.ts +7 -1
  71. package/dist/cjs/exporter/xlsx/threaded-comments-builder.d.ts.map +1 -1
  72. package/dist/cjs/exporter/xlsx/threaded-comments-builder.js +15 -3
  73. package/dist/cjs/exporter/xlsx/xlsx-export.d.ts +11 -2
  74. package/dist/cjs/exporter/xlsx/xlsx-export.d.ts.map +1 -1
  75. package/dist/cjs/exporter/xlsx/xlsx-export.js +991 -297
  76. package/dist/cjs/helpers/implicit-intersection.d.ts +16 -0
  77. package/dist/cjs/helpers/implicit-intersection.d.ts.map +1 -0
  78. package/dist/cjs/helpers/implicit-intersection.js +197 -0
  79. package/dist/cjs/print-settings.d.ts +105 -0
  80. package/dist/cjs/print-settings.d.ts.map +1 -0
  81. package/dist/cjs/print-settings.js +494 -0
  82. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  83. package/dist/esm/excel-parser/cfb-decryptor.d.ts +6 -0
  84. package/dist/esm/excel-parser/cfb-decryptor.d.ts.map +1 -1
  85. package/dist/esm/excel-parser/cfb-decryptor.js +6 -0
  86. package/dist/esm/excel-parser/helpers.d.ts +9 -1
  87. package/dist/esm/excel-parser/helpers.d.ts.map +1 -1
  88. package/dist/esm/excel-parser/helpers.js +34 -0
  89. package/dist/esm/excel-parser/interface.d.ts +15 -2
  90. package/dist/esm/excel-parser/interface.d.ts.map +1 -1
  91. package/dist/esm/excel-parser/plugins/base/index.d.ts +2 -0
  92. package/dist/esm/excel-parser/plugins/base/index.d.ts.map +1 -1
  93. package/dist/esm/excel-parser/plugins/base/index.js +3 -0
  94. package/dist/esm/excel-parser/plugins/ods/index.d.ts +19 -0
  95. package/dist/esm/excel-parser/plugins/ods/index.d.ts.map +1 -1
  96. package/dist/esm/excel-parser/plugins/ods/index.js +347 -33
  97. package/dist/esm/excel-parser/plugins/ods/styles.d.ts +43 -1
  98. package/dist/esm/excel-parser/plugins/ods/styles.d.ts.map +1 -1
  99. package/dist/esm/excel-parser/plugins/ods/styles.js +276 -2
  100. package/dist/esm/excel-parser/plugins/xls/index.d.ts +55 -1
  101. package/dist/esm/excel-parser/plugins/xls/index.d.ts.map +1 -1
  102. package/dist/esm/excel-parser/plugins/xls/index.js +441 -3
  103. package/dist/esm/excel-parser/plugins/xlsb/index.d.ts +4 -0
  104. package/dist/esm/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
  105. package/dist/esm/excel-parser/plugins/xlsb/index.js +30 -9
  106. package/dist/esm/excel-parser/plugins/xlsx/chartex.d.ts +37 -0
  107. package/dist/esm/excel-parser/plugins/xlsx/chartex.d.ts.map +1 -0
  108. package/dist/esm/excel-parser/plugins/xlsx/chartex.js +199 -0
  109. package/dist/esm/excel-parser/plugins/xlsx/charts.d.ts +10 -0
  110. package/dist/esm/excel-parser/plugins/xlsx/charts.d.ts.map +1 -1
  111. package/dist/esm/excel-parser/plugins/xlsx/charts.js +69 -6
  112. package/dist/esm/excel-parser/plugins/xlsx/drawing.d.ts +28 -1
  113. package/dist/esm/excel-parser/plugins/xlsx/drawing.d.ts.map +1 -1
  114. package/dist/esm/excel-parser/plugins/xlsx/drawing.js +99 -0
  115. package/dist/esm/excel-parser/plugins/xlsx/form-controls.d.ts +70 -0
  116. package/dist/esm/excel-parser/plugins/xlsx/form-controls.d.ts.map +1 -0
  117. package/dist/esm/excel-parser/plugins/xlsx/form-controls.js +246 -0
  118. package/dist/esm/excel-parser/plugins/xlsx/index.d.ts +35 -2
  119. package/dist/esm/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
  120. package/dist/esm/excel-parser/plugins/xlsx/index.js +454 -32
  121. package/dist/esm/excel-parser/plugins/xlsx/pivots.d.ts.map +1 -1
  122. package/dist/esm/excel-parser/plugins/xlsx/pivots.js +86 -6
  123. package/dist/esm/excel-parser/plugins/xlsx/sheet-views.d.ts +3 -0
  124. package/dist/esm/excel-parser/plugins/xlsx/sheet-views.d.ts.map +1 -1
  125. package/dist/esm/excel-parser/plugins/xlsx/sheet-views.js +2 -0
  126. package/dist/esm/excel-parser/plugins/xlsx/styles.d.ts +15 -1
  127. package/dist/esm/excel-parser/plugins/xlsx/styles.d.ts.map +1 -1
  128. package/dist/esm/excel-parser/plugins/xlsx/styles.js +92 -2
  129. package/dist/esm/excel-parser/registry.d.ts +7 -0
  130. package/dist/esm/excel-parser/registry.d.ts.map +1 -1
  131. package/dist/esm/excel-parser/registry.js +19 -0
  132. package/dist/esm/excel-parser/types.d.ts +17 -2
  133. package/dist/esm/excel-parser/types.d.ts.map +1 -1
  134. package/dist/esm/excel-parser/workbook.d.ts +3 -0
  135. package/dist/esm/excel-parser/workbook.d.ts.map +1 -1
  136. package/dist/esm/excel-parser/workbook.js +19 -10
  137. package/dist/esm/excel-parser/worker.js +12 -18
  138. package/dist/esm/exporter/ods/ods-export.d.ts.map +1 -1
  139. package/dist/esm/exporter/ods/ods-export.js +306 -35
  140. package/dist/esm/exporter/xlsx/chart-builder.d.ts.map +1 -1
  141. package/dist/esm/exporter/xlsx/chart-builder.js +161 -28
  142. package/dist/esm/exporter/xlsx/chartex-builder.d.ts +36 -0
  143. package/dist/esm/exporter/xlsx/chartex-builder.d.ts.map +1 -0
  144. package/dist/esm/exporter/xlsx/chartex-builder.js +97 -0
  145. package/dist/esm/exporter/xlsx/form-controls-builder.d.ts +61 -0
  146. package/dist/esm/exporter/xlsx/form-controls-builder.d.ts.map +1 -0
  147. package/dist/esm/exporter/xlsx/form-controls-builder.js +173 -0
  148. package/dist/esm/exporter/xlsx/pivot-builder.d.ts +8 -0
  149. package/dist/esm/exporter/xlsx/pivot-builder.d.ts.map +1 -1
  150. package/dist/esm/exporter/xlsx/pivot-builder.js +104 -19
  151. package/dist/esm/exporter/xlsx/style-builder.js +9 -3
  152. package/dist/esm/exporter/xlsx/threaded-comments-builder.d.ts +7 -1
  153. package/dist/esm/exporter/xlsx/threaded-comments-builder.d.ts.map +1 -1
  154. package/dist/esm/exporter/xlsx/threaded-comments-builder.js +15 -3
  155. package/dist/esm/exporter/xlsx/xlsx-export.d.ts +11 -2
  156. package/dist/esm/exporter/xlsx/xlsx-export.d.ts.map +1 -1
  157. package/dist/esm/exporter/xlsx/xlsx-export.js +675 -64
  158. package/dist/esm/helpers/implicit-intersection.d.ts +16 -0
  159. package/dist/esm/helpers/implicit-intersection.d.ts.map +1 -0
  160. package/dist/esm/helpers/implicit-intersection.js +184 -0
  161. package/dist/esm/print-settings.d.ts +105 -0
  162. package/dist/esm/print-settings.d.ts.map +1 -0
  163. package/dist/esm/print-settings.js +422 -0
  164. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  165. package/package.json +3 -3
  166. package/xlsx.d.ts +14 -0
@@ -2,17 +2,21 @@ import { cellRangeToAddress, cellToAddress, escapeCharacters, getCellEffectiveFo
2
2
  import { zipSync } from "fflate";
3
3
  import { omitInvalidDataValidationError } from "../invalid-data-validation";
4
4
  import { buildAutoFilterConditionXml } from "../../autofilter-condition";
5
+ import { buildControlElementXml, buildControlsXml, buildCtrlPropXml, buildFormControlVmlShape, buildVmlDrawingXml, CTRL_PROP_CONTENT_TYPE, CTRL_PROP_REL_TYPE, FORM_CONTROL_VML_SHAPETYPE, } from "./form-controls-builder";
6
+ import { buildChartExGraphicFrameXml, buildChartExXml, CHART_EX_CONTENT_TYPE, CHART_EX_REL_TYPE, isChartExType, } from "./chartex-builder";
5
7
  import { applyPowerQueryParts } from "./power-query-builder";
8
+ import { encodeImplicitIntersection } from "../../helpers/implicit-intersection";
6
9
  import { qualifyFutureFunctions } from "./future-functions";
7
10
  import { PERSON_CONTENT_TYPE, PERSON_REL_TYPE, PersonRegistry, THREADED_COMMENT_REL_TYPE, THREADED_COMMENTS_CONTENT_TYPE, ThreadedCommentsCollector, } from "./threaded-comments-builder";
8
11
  import { CFBEncryptor } from "../../excel-parser/cfb-encryptor";
9
12
  import { columnPixelToChars, imageUrlToBase64 } from "../../helpers";
13
+ import { buildPrintAreaFormula, buildPrintTitlesFormula, isBuiltinPrintName, isHeaderFooterEmpty, PRINT_AREA_NAME, PRINT_TITLES_NAME, } from "../../print-settings";
10
14
  import { generateThemeXml } from "../../helpers/theme-xml-generator";
11
15
  import theme1Xml from "../../theme1";
12
16
  import { tokenize as defaultTokenizer } from "@rowsncolumns/calculator";
13
17
  import { CellXfsRegistry, deriveEffectiveNumberFormatFromFormula, } from "@rowsncolumns/spreadsheet-state";
14
18
  import { buildChartXml } from "./chart-builder";
15
- import { buildPivotCacheDefinitionXml, buildPivotTableRelsXml, buildPivotTableXml, } from "./pivot-builder";
19
+ import { buildPivotCacheDefinitionXml, buildPivotTableRelsXml, buildPivotTableXml, calculatedFieldReferences, } from "./pivot-builder";
16
20
  import { buildSlicerCacheXml, buildSlicerXml } from "./slicer-builder";
17
21
  import { buildSparklineGroupsXml } from "./sparkline-builder";
18
22
  import { StyleBuilder } from "./style-builder";
@@ -21,7 +25,7 @@ const isStyleReference = (value) => !!value && typeof value === "object" && "sid
21
25
  const DEFAULT_COLUMN_WIDTH_PX = 100;
22
26
  const DEFAULT_ROW_HEIGHT_PX = 21;
23
27
  const pixelToPoints = (px) => Math.max(1, Number(((px * 72) / 96).toFixed(2)));
24
- export const createExcelFile = async ({ conditionalFormats: _conditionalFormats, dataValidations: _dataValidations, sheets, sheetData, sharedStrings: sourceSharedStrings, tables: _tables = [], cellXfs, theme, defaultFont, embeds = [], charts = [], slicers = [], pivotTables = [], externalLinks = [], namedRanges = [], namedCellStyles, customTableStyles, iterativeCalculationOptions, ignoreFormulaPatterns, citations, comments, password, locale, use1904 = false, properties, powerQuery, }) => {
28
+ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats, dataValidations: _dataValidations, sheets, sheetData, sharedStrings: sourceSharedStrings, tables: _tables = [], cellXfs, theme, defaultFont, embeds = [], charts = [], slicers = [], pivotTables = [], externalLinks = [], namedRanges = [], protectedRanges = [], namedCellStyles, customTableStyles, iterativeCalculationOptions, ignoreFormulaPatterns, citations, comments, password, locale, use1904 = false, properties, powerQuery, }) => {
25
29
  const sortedSheets = sortSheetsByIndex(sheets, true);
26
30
  const sharedStrings = new SharedStrings();
27
31
  // Workbook-scoped author registry shared across every sheet's threaded
@@ -51,6 +55,8 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
51
55
  const embedsBySheet = groupEmbedsBySheet(embeds ?? []);
52
56
  const textBoxesBySheet = groupTextBoxesBySheet(embeds ?? []);
53
57
  const shapesBySheet = groupShapesBySheet(embeds ?? []);
58
+ const formControlsBySheet = groupFormControlsBySheet(embeds ?? []);
59
+ const ctrlProps = { parts: [] };
54
60
  const chartsBySheet = groupChartsBySheet(charts ?? []);
55
61
  // Remap slicer tableIds to match prepared table IDs
56
62
  const remappedSlicers = slicers.map((slicer) => {
@@ -116,6 +122,7 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
116
122
  const embedImages = embedsBySheet.get(sheet.sheetId) ?? [];
117
123
  const textBoxes = textBoxesBySheet.get(sheet.sheetId) ?? [];
118
124
  const sheetShapes = shapesBySheet.get(sheet.sheetId) ?? [];
125
+ const sheetFormControls = formControlsBySheet.get(sheet.sheetId) ?? [];
119
126
  const sheetCharts = chartsBySheet.get(sheet.sheetId) ?? [];
120
127
  const sheetSlicers = slicersBySheet.get(sheet.sheetId) ?? [];
121
128
  const worksheetSlicerRelIds = [];
@@ -183,12 +190,26 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
183
190
  fieldIndex.set(name, fieldNameList.length);
184
191
  fieldNameList.push(name);
185
192
  };
193
+ const calculatedNames = new Set((pivot.calculatedFields ?? []).map((c) => c.name));
186
194
  for (const r of pivot.rows ?? [])
187
195
  registerField(r.field);
188
196
  for (const c of pivot.columns ?? [])
189
197
  registerField(c.field);
190
- for (const v of pivot.values ?? [])
191
- registerField(v.field);
198
+ for (const v of pivot.values ?? []) {
199
+ if (!calculatedNames.has(v.field))
200
+ registerField(v.field);
201
+ }
202
+ // Source fields a calculated field reads must exist in the cache
203
+ // for Excel to evaluate the formula; the calculated fields
204
+ // themselves come last, as Excel orders them.
205
+ for (const c of pivot.calculatedFields ?? []) {
206
+ for (const ref of calculatedFieldReferences(c.formula)) {
207
+ if (!calculatedNames.has(ref))
208
+ registerField(ref);
209
+ }
210
+ }
211
+ for (const c of pivot.calculatedFields ?? [])
212
+ registerField(c.name);
192
213
  const cacheXml = buildPivotCacheDefinitionXml(pivot, sourceSheetName, fieldNameList);
193
214
  const tableXml = buildPivotTableXml(pivot, cacheIndex, fieldIndex);
194
215
  if (!cacheXml || !tableXml)
@@ -233,6 +254,9 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
233
254
  sheetData,
234
255
  sheets: sortedSheets,
235
256
  locale,
257
+ protectedRanges,
258
+ formControls: sheetFormControls,
259
+ ctrlProps,
236
260
  });
237
261
  return {
238
262
  index,
@@ -305,6 +329,10 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
305
329
  const drawingSheets = [];
306
330
  const imageExtensions = new Set();
307
331
  const chartEntries = [];
332
+ // Excel 2016+ chartex parts (treemap / sunburst) — numbered apart from
333
+ // `chart{N}.xml`, as Excel does.
334
+ const chartExEntries = [];
335
+ let globalChartExIndex = 1;
308
336
  for (const entry of worksheetEntries) {
309
337
  const placements = [...entry.cellImages, ...entry.embedImages];
310
338
  const textBoxesForSheet = entry.textBoxes ?? [];
@@ -324,6 +352,7 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
324
352
  shapes: shapesForSheet,
325
353
  startImageIndex: globalImageIndex,
326
354
  startChartIndex: globalChartIndex,
355
+ startChartExIndex: globalChartExIndex,
327
356
  charts: chartsForSheet,
328
357
  slicers: slicersForSheet,
329
358
  sheetNameById,
@@ -341,13 +370,20 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
341
370
  for (const chartEntry of drawingParts.charts) {
342
371
  chartEntries.push(chartEntry);
343
372
  }
373
+ for (const chartExEntry of drawingParts.chartExs) {
374
+ chartExEntries.push(chartExEntry);
375
+ }
344
376
  globalImageIndex = drawingParts.nextImageIndex;
345
377
  globalChartIndex = drawingParts.nextChartIndex;
378
+ globalChartExIndex = drawingParts.nextChartExIndex;
346
379
  drawingSheets.push(entry.index + 1);
347
380
  }
348
381
  for (const chartEntry of chartEntries) {
349
382
  files[chartEntry.path] = encodeXml(chartEntry.xml);
350
383
  }
384
+ for (const chartExEntry of chartExEntries) {
385
+ files[chartExEntry.path] = encodeXml(chartExEntry.xml);
386
+ }
351
387
  for (const slicerCacheEntry of slicerCacheEntries) {
352
388
  files[slicerCacheEntry.path] = encodeXml(slicerCacheEntry.xml);
353
389
  }
@@ -381,6 +417,10 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
381
417
  const commentsSheets = worksheetEntries
382
418
  .filter((entry) => entry.commentsXml)
383
419
  .map((entry) => entry.sheet.sheetId ?? entry.index + 1);
420
+ const hasVmlDrawings = worksheetEntries.some((entry) => entry.vmlDrawingXml);
421
+ for (const part of ctrlProps.parts) {
422
+ files[part.path] = encodeXml(part.xml);
423
+ }
384
424
  const threadedCommentsSheets = worksheetEntries
385
425
  .filter((entry) => entry.threadedCommentsXml)
386
426
  .map((entry) => entry.sheet.sheetId ?? entry.index + 1);
@@ -390,6 +430,7 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
390
430
  drawingSheets,
391
431
  mediaExtensions: Array.from(imageExtensions),
392
432
  chartCount: chartEntries.length,
433
+ chartExCount: chartExEntries.length,
393
434
  slicerCount: slicerEntries.length,
394
435
  slicerCacheCount: slicerCacheEntries.length,
395
436
  pivotTableCount: pivotTableEntries.length,
@@ -398,6 +439,8 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
398
439
  threadedCommentsSheets,
399
440
  hasPersons: !personRegistry.isEmpty(),
400
441
  externalLinkCount: externalLinks.length,
442
+ hasVmlDrawings,
443
+ ctrlPropCount: ctrlProps.parts.length,
401
444
  });
402
445
  files["[Content_Types].xml"] = encodeXml(contentTypesXml);
403
446
  // Preserved Power Query machinery (DataMashup customXml, connections.xml, queryTable parts) is
@@ -546,6 +589,8 @@ class DataValidationCollector {
546
589
  return `<dataValidations count="${this.entries.size}">${items.join("")}</dataValidations>`;
547
590
  }
548
591
  }
592
+ /** First VML shape id the note shapes take (`_x0000_s1026`, `_x0000_s1027`, …). */
593
+ const NOTE_VML_SHAPE_ID_BASE = 1026;
549
594
  class CommentsCollector {
550
595
  entries = [];
551
596
  add(address, text, author = "System") {
@@ -570,13 +615,18 @@ class CommentsCollector {
570
615
  .join("");
571
616
  return `${XML_DECLARATION}<comments xmlns="${WORKSHEET_NS}"><authors>${authorsXml}</authors><commentList>${commentsXml}</commentList></comments>`;
572
617
  }
573
- toVmlXml() {
574
- if (!this.entries.length) {
575
- return undefined;
576
- }
577
- const shapes = this.entries
578
- .map((entry, index) => {
579
- const shapeId = `_x0000_s${1026 + index}`;
618
+ /**
619
+ * VML shape ids the note shapes occupy. Excel allocates notes and form
620
+ * controls from one per-sheet counter, so the form-control allocator must
621
+ * never hand these out again.
622
+ */
623
+ vmlShapeIds() {
624
+ return this.entries.map((_, index) => NOTE_VML_SHAPE_ID_BASE + index);
625
+ }
626
+ /** One `<v:shape>` per note (Excel's hidden `Note` object). */
627
+ toVmlShapes() {
628
+ return this.entries.map((entry, index) => {
629
+ const shapeId = `_x0000_s${NOTE_VML_SHAPE_ID_BASE + index}`;
580
630
  // Parse cell address to get row and column
581
631
  const match = entry.address.match(/^([A-Z]+)(\d+)$/);
582
632
  if (!match)
@@ -587,11 +637,11 @@ class CommentsCollector {
587
637
  .split("")
588
638
  .reduce((acc, char) => acc * 26 + (char.charCodeAt(0) - 64), 0) - 1;
589
639
  return `<v:shape id="${shapeId}" type="#_x0000_t202" style='position:absolute;margin-left:59.25pt;margin-top:1.5pt;width:108pt;height:59.25pt;z-index:${index + 1};visibility:hidden' fillcolor="#ffffe1" o:insetmode="auto"><v:fill color2="#ffffe1"/><v:shadow on="t" color="black" obscured="t"/><v:path o:connecttype="none"/><v:textbox style='mso-direction-alt:auto'><div style='text-align:left'></div></v:textbox><x:ClientData ObjectType="Note"><x:MoveWithCells/><x:SizeWithCells/><x:Anchor>1, 15, 0, 2, 3, 15, 3, 16</x:Anchor><x:AutoFill>False</x:AutoFill><x:Row>${row - 1}</x:Row><x:Column>${colIndex}</x:Column></x:ClientData></v:shape>`;
590
- })
591
- .join("");
592
- return `<xml xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel"><o:shapelayout v:ext="edit"><o:idmap v:ext="edit" data="1"/></o:shapelayout><v:shapetype id="_x0000_t202" coordsize="21600,21600" o:spt="202" path="m,l,21600r21600,l21600,xe"><v:stroke joinstyle="miter"/><v:path gradientshapeok="t" o:connecttype="rect"/></v:shapetype>${shapes}</xml>`;
640
+ }).filter(Boolean);
593
641
  }
594
642
  }
643
+ /** VML `<v:shapetype>` for legacy notes (`#_x0000_t202`). */
644
+ const NOTE_VML_SHAPETYPE = `<v:shapetype id="_x0000_t202" coordsize="21600,21600" o:spt="202" path="m,l,21600r21600,l21600,xe"><v:stroke joinstyle="miter"/><v:path gradientshapeok="t" o:connecttype="rect"/></v:shapetype>`;
595
645
  const INVALID_XML_CHAR_REGEX = /[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g;
596
646
  const sanitizeXmlText = (value) => {
597
647
  if (!value)
@@ -643,7 +693,7 @@ const getRowsForSheet = (sheetData, sheetId) => {
643
693
  sheetData[Number(sheetId)] ??
644
694
  []);
645
695
  };
646
- const buildWorksheetXml = ({ sheet, rows, sharedStrings, sourceSharedStrings, cellXfs, styleBuilder, tables, relationships, drawingContext, conditionalFormats, dataValidationsById, dataValidations, ignoreFormulaPatterns, citations, comments, personRegistry, slicerRelIds, sheetData, sheets, locale, }) => {
696
+ const buildWorksheetXml = ({ sheet, rows, sharedStrings, sourceSharedStrings, cellXfs, styleBuilder, tables, relationships, drawingContext, conditionalFormats, dataValidationsById, dataValidations, ignoreFormulaPatterns, citations, comments, personRegistry, slicerRelIds, sheetData, sheets, locale, protectedRanges, formControls = [], ctrlProps = { parts: [] }, }) => {
647
697
  const tableNameMap = buildTableNameMap(tables);
648
698
  const bounds = {
649
699
  minRow: Number.POSITIVE_INFINITY,
@@ -806,14 +856,36 @@ const buildWorksheetXml = ({ sheet, rows, sharedStrings, sourceSharedStrings, ce
806
856
  }
807
857
  // Generate comments XML (notes) and add relationships if there are comments
808
858
  const commentsXml = commentsCollector.toXml();
809
- let legacyDrawingXml = "";
810
859
  if (commentsXml) {
811
860
  relationships.add(COMMENTS_REL_TYPE, `../comments${sheet.sheetId}.xml`);
861
+ }
862
+ // Notes and form controls share the sheet's one legacy VML part
863
+ // (`<legacyDrawing>`); a control without its VML shape is dropped by Excel.
864
+ const noteVmlShapes = commentsCollector.toVmlShapes();
865
+ const formControlBuild = buildSheetFormControls({
866
+ sheet,
867
+ controls: formControls,
868
+ relationships,
869
+ ctrlProps,
870
+ zIndexStart: noteVmlShapes.length,
871
+ reservedShapeIds: commentsCollector.vmlShapeIds(),
872
+ });
873
+ const vmlDrawingXml = buildVmlDrawingXml({
874
+ shapeTypes: [
875
+ ...(noteVmlShapes.length ? [NOTE_VML_SHAPETYPE] : []),
876
+ ...(formControlBuild.vmlShapes.length ? [FORM_CONTROL_VML_SHAPETYPE] : []),
877
+ ],
878
+ shapes: [...noteVmlShapes, ...formControlBuild.vmlShapes],
879
+ });
880
+ let legacyDrawingXml = "";
881
+ if (vmlDrawingXml) {
812
882
  const legacyDrawingRelId = relationships.add("http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing", `../drawings/vmlDrawing${sheet.sheetId}.vml`);
813
883
  legacyDrawingXml = `<legacyDrawing r:id="${legacyDrawingRelId}"/>`;
814
884
  }
885
+ const controlsXml = formControlBuild.controlsXml;
815
886
  const sheetDataXml = `<sheetData>${rowXmlChunks.join("")}</sheetData>`;
816
- const pageMargins = DEFAULT_PAGE_MARGINS;
887
+ const protectionXml = buildSheetProtectionXml(sheet, protectedRanges);
888
+ const printSettingsXml = buildPrintSettingsXml(sheet.printSettings);
817
889
  // Build row data map for sparkline detection
818
890
  const rowDataMap = {};
819
891
  for (let rowIndex = 1; rowIndex < maxRows; rowIndex++) {
@@ -847,13 +919,13 @@ const buildWorksheetXml = ({ sheet, rows, sharedStrings, sourceSharedStrings, ce
847
919
  explicitDefaultColumnWidthPx > 0) {
848
920
  sheetFormatPrAttrs.push(`defaultColWidth="${columnPixelToChars(explicitDefaultColumnWidthPx).toFixed(2)}"`);
849
921
  }
850
- const worksheetXml = `${XML_DECLARATION}<worksheet xmlns="${WORKSHEET_NS}" xmlns:r="${REL_NS}">${sheetPrXml}<dimension ref="${dimension}"/>${sheetViews}<sheetFormatPr ${sheetFormatPrAttrs.join(" ")}/>${columnXml}${sheetDataXml}${basicFilterXml}${mergeXml}${conditionalFormattingXml}${dataValidationsXml}${hyperlinksXml}${pageMargins}${drawingXml}${legacyDrawingXml}${tablePartsXml}${extLstXml}</worksheet>`;
922
+ const worksheetXml = `${XML_DECLARATION}<worksheet xmlns="${WORKSHEET_NS}" xmlns:r="${REL_NS}">${sheetPrXml}<dimension ref="${dimension}"/>${sheetViews}<sheetFormatPr ${sheetFormatPrAttrs.join(" ")}/>${columnXml}${sheetDataXml}${protectionXml}${basicFilterXml}${mergeXml}${conditionalFormattingXml}${dataValidationsXml}${hyperlinksXml}${printSettingsXml}${drawingXml}${legacyDrawingXml}${controlsXml}${tablePartsXml}${extLstXml}</worksheet>`;
851
923
  return {
852
924
  xml: worksheetXml,
853
925
  relationshipsXml: relationships.toXml(),
854
926
  images: collectedImages,
855
927
  commentsXml,
856
- vmlDrawingXml: commentsCollector.toVmlXml(),
928
+ vmlDrawingXml,
857
929
  threadedCommentsXml,
858
930
  };
859
931
  };
@@ -1814,7 +1886,9 @@ function rewriteStructuredReferenceFormula(formula, tableNameMap, tables, rowNum
1814
1886
  }
1815
1887
  function sanitizeFormulaText(formula) {
1816
1888
  const normalized = sanitizeXmlText(formula);
1817
- return qualifyFutureFunctions(normalized.replace(/\r\n|\r|\n/g, " "));
1889
+ // `@x` is persisted as `_xlfn.SINGLE(x)`; encoding first lets the
1890
+ // future-function pass add the prefix.
1891
+ return qualifyFutureFunctions(encodeImplicitIntersection(normalized.replace(/\r\n|\r|\n/g, " ")));
1818
1892
  }
1819
1893
  function escapeXmlBase(value) {
1820
1894
  return sanitizeXmlText(value)
@@ -1976,6 +2050,11 @@ const buildSheetViewsXml = (sheet) => {
1976
2050
  zoomScale !== 100) {
1977
2051
  attrs.push(`zoomScale="${zoomScale}"`);
1978
2052
  }
2053
+ // `rightToLeft` — only an explicit true is written (Excel's default is
2054
+ // LTR and the attribute is omitted), mirroring the Rust exporter.
2055
+ if (sheet.rightToLeft) {
2056
+ attrs.push(`rightToLeft="1"`);
2057
+ }
1979
2058
  const panes = [];
1980
2059
  const frozenRows = sheet.frozenRowCount ?? 0;
1981
2060
  const frozenColumns = sheet.frozenColumnCount ?? 0;
@@ -2230,6 +2309,57 @@ const sanitizeTableName = (name) => {
2230
2309
  }
2231
2310
  return cleaned;
2232
2311
  };
2312
+ /**
2313
+ * `SUBTOTAL()` function numbers (the 101+ "ignore hidden rows" family Excel
2314
+ * writes into totals-row cells) → `totalsRowFunction`.
2315
+ */
2316
+ const SUBTOTAL_TOTALS_ROW_FUNCTIONS = {
2317
+ "101": "average",
2318
+ "102": "countNums",
2319
+ "103": "count",
2320
+ "104": "max",
2321
+ "105": "min",
2322
+ "107": "stdDev",
2323
+ "109": "sum",
2324
+ "110": "var",
2325
+ };
2326
+ /**
2327
+ * Totals-row metadata for one `<tableColumn>`. Explicit column metadata
2328
+ * (imported from Excel, or set by the host) wins — an explicit `none` means
2329
+ * "no totals function" and is not second-guessed from the cell (the Rust
2330
+ * exporter's `totals_for_column` agrees). Otherwise it is derived from the
2331
+ * totals-row cell so tables whose totals row was added in-app (a "Total"
2332
+ * label + `=SUBTOTAL(109,[Col])` per numeric column) open in Excel with the
2333
+ * matching function selected in the totals dropdown. A formula that is not
2334
+ * a plain `SUBTOTAL(n, ref)` is left to the cell.
2335
+ */
2336
+ const resolveTableColumnTotals = (column, totalsCell) => {
2337
+ if (column?.totalsRowLabel) {
2338
+ return { totalsRowLabel: column.totalsRowLabel };
2339
+ }
2340
+ if (column?.totalsRowFunction) {
2341
+ if (column.totalsRowFunction === "none") {
2342
+ return undefined;
2343
+ }
2344
+ if (column.totalsRowFunction === "custom" && !column.totalsRowFormula) {
2345
+ return undefined;
2346
+ }
2347
+ return {
2348
+ totalsRowFunction: column.totalsRowFunction,
2349
+ totalsRowFormula: column.totalsRowFormula ?? undefined,
2350
+ };
2351
+ }
2352
+ if (!totalsCell)
2353
+ return undefined;
2354
+ const formula = getExtendedValueFormula(getCellUserEnteredValue(totalsCell));
2355
+ if (formula) {
2356
+ const match = /^=?\s*SUBTOTAL\(\s*(\d{3})\s*,[^,]*\)\s*$/i.exec(formula);
2357
+ const fn = match ? SUBTOTAL_TOTALS_ROW_FUNCTIONS[match[1]] : undefined;
2358
+ return fn ? { totalsRowFunction: fn } : undefined;
2359
+ }
2360
+ const label = getExtendedValueString(getCellUserEnteredValue(totalsCell));
2361
+ return label && label.trim() !== "" ? { totalsRowLabel: label } : undefined;
2362
+ };
2233
2363
  const buildTableXml = (table, rows, sourceSharedStrings, styleBuilder) => {
2234
2364
  const rangeRef = table.ref;
2235
2365
  const columnCount = Math.max(table.columns?.length ?? 0, (table.range.endColumnIndex ?? table.range.startColumnIndex) -
@@ -2275,6 +2405,7 @@ const buildTableXml = (table, rows, sourceSharedStrings, styleBuilder) => {
2275
2405
  // Fallback to table.columns name or generic name
2276
2406
  return table.columns?.[index]?.name ?? `Column${index + 1}`;
2277
2407
  });
2408
+ const totalsRowIndex = table.range.endRowIndex;
2278
2409
  const tableColumns = `<tableColumns count="${columnCount}">${columnNames
2279
2410
  .map((name, idx) => {
2280
2411
  const columnFormula = table.columns?.[idx]?.formula;
@@ -2284,7 +2415,22 @@ const buildTableXml = (table, rows, sourceSharedStrings, styleBuilder) => {
2284
2415
  const formulaXml = normalizedFormula
2285
2416
  ? `<calculatedColumnFormula>${escapeXmlText(stripFormula(normalizedFormula))}</calculatedColumnFormula>`
2286
2417
  : "";
2287
- return `<tableColumn id="${idx + 1}" name="${escapeXmlAttr(name)}">${formulaXml}</tableColumn>`;
2418
+ // Explicit per-column totals metadata exports whether or not the totals
2419
+ // row is shown: Excel keeps it on a table whose totals row is toggled
2420
+ // off so re-enabling restores each column's choice. Deriving it from
2421
+ // the last row only applies when that row IS the totals row.
2422
+ const totals = resolveTableColumnTotals(table.columns?.[idx], totalsRowCount > 0
2423
+ ? rows[totalsRowIndex]?.values?.[table.range.startColumnIndex + idx]
2424
+ : undefined);
2425
+ const totalsAttrs = totals?.totalsRowLabel
2426
+ ? ` totalsRowLabel="${escapeXmlAttr(totals.totalsRowLabel)}"`
2427
+ : totals?.totalsRowFunction
2428
+ ? ` totalsRowFunction="${totals.totalsRowFunction}"`
2429
+ : "";
2430
+ const totalsFormulaXml = totals?.totalsRowFunction === "custom" && totals.totalsRowFormula
2431
+ ? `<totalsRowFormula>${escapeXmlText(stripFormula(normalizeCalculatedColumnFormula(totals.totalsRowFormula, table)))}</totalsRowFormula>`
2432
+ : "";
2433
+ return `<tableColumn id="${idx + 1}" name="${escapeXmlAttr(name)}"${totalsAttrs}>${formulaXml}${totalsFormulaXml}</tableColumn>`;
2288
2434
  })
2289
2435
  .join("")}</tableColumns>`;
2290
2436
  const isBuiltInStyle = table.theme && /^TableStyle(Light|Medium|Dark)\d+$/.test(table.theme);
@@ -2332,6 +2478,106 @@ const buildWorksheetAutoFilterXml = (sheet, rows, styleBuilder) => {
2332
2478
  }
2333
2479
  return `<autoFilter ref="${ref}"/>`;
2334
2480
  };
2481
+ /** `CT_SheetProtection` boolean attributes in Excel's attribute order. */
2482
+ const SHEET_PROTECTION_FLAG_ATTRS = [
2483
+ "sheet",
2484
+ "objects",
2485
+ "scenarios",
2486
+ "formatCells",
2487
+ "formatColumns",
2488
+ "formatRows",
2489
+ "insertColumns",
2490
+ "insertRows",
2491
+ "insertHyperlinks",
2492
+ "deleteColumns",
2493
+ "deleteRows",
2494
+ "selectLockedCells",
2495
+ "sort",
2496
+ "autoFilter",
2497
+ "pivotTables",
2498
+ "selectUnlockedCells",
2499
+ ];
2500
+ /**
2501
+ * `<sheetProtection …/>` from `Sheet.sheetProtection`: the password hash first
2502
+ * (legacy `password` or the agile quartet, verbatim), then only the action
2503
+ * flags the model carries (`true` → `"1"`, `false` → `"0"`, absent → omitted so
2504
+ * Excel's schema default applies). Mirrors `rnc-export`'s `sheet_protection_xml`.
2505
+ */
2506
+ const buildSheetProtectionElementXml = (protection) => {
2507
+ const attrs = [];
2508
+ if (protection.algorithmName) {
2509
+ attrs.push(`algorithmName="${escapeXmlAttr(protection.algorithmName)}"`);
2510
+ }
2511
+ if (protection.hashValue) {
2512
+ attrs.push(`hashValue="${escapeXmlAttr(protection.hashValue)}"`);
2513
+ }
2514
+ if (protection.saltValue) {
2515
+ attrs.push(`saltValue="${escapeXmlAttr(protection.saltValue)}"`);
2516
+ }
2517
+ if (protection.spinCount != null) {
2518
+ attrs.push(`spinCount="${protection.spinCount}"`);
2519
+ }
2520
+ if (protection.password) {
2521
+ attrs.push(`password="${escapeXmlAttr(protection.password)}"`);
2522
+ }
2523
+ for (const flag of SHEET_PROTECTION_FLAG_ATTRS) {
2524
+ const value = protection[flag];
2525
+ if (typeof value === "boolean") {
2526
+ attrs.push(`${flag}="${value ? 1 : 0}"`);
2527
+ }
2528
+ }
2529
+ return attrs.length ? `<sheetProtection ${attrs.join(" ")}/>` : "";
2530
+ };
2531
+ /**
2532
+ * The sheet's protection block — `<sheetProtection>` followed by
2533
+ * `<protectedRanges>` — or `""` when the sheet is unprotected and has no
2534
+ * allow-edit ranges. Ranges missing a bound (a sheet-wide `protect-sheet`
2535
+ * entry only carries `sheetId`) have no `sqref` and are skipped; the `name`
2536
+ * is the range's id, synthesized as `Range{n}` when absent (schema requires it).
2537
+ */
2538
+ const buildSheetProtectionXml = (sheet, protectedRanges) => {
2539
+ const protectionXml = sheet.sheetProtection
2540
+ ? buildSheetProtectionElementXml(sheet.sheetProtection)
2541
+ : "";
2542
+ const rangeItems = [];
2543
+ for (const protectedRange of protectedRanges ?? []) {
2544
+ const range = protectedRange.range;
2545
+ if (range.sheetId !== sheet.sheetId)
2546
+ continue;
2547
+ if (range.startRowIndex == null ||
2548
+ range.endRowIndex == null ||
2549
+ range.startColumnIndex == null ||
2550
+ range.endColumnIndex == null) {
2551
+ continue;
2552
+ }
2553
+ // Excel writes a single cell as `D10`, a block as `A2:C5`.
2554
+ const isSingleCell = range.startRowIndex === range.endRowIndex &&
2555
+ range.startColumnIndex === range.endColumnIndex;
2556
+ const sqref = isSingleCell
2557
+ ? cellToAddress({
2558
+ rowIndex: range.startRowIndex,
2559
+ columnIndex: range.startColumnIndex,
2560
+ })
2561
+ : cellRangeToAddress(undefined, {
2562
+ startRowIndex: range.startRowIndex,
2563
+ endRowIndex: range.endRowIndex,
2564
+ startColumnIndex: range.startColumnIndex,
2565
+ endColumnIndex: range.endColumnIndex,
2566
+ });
2567
+ if (!sqref)
2568
+ continue;
2569
+ const id = protectedRange.protectedRangeId;
2570
+ const name = id != null && String(id) !== "" ? String(id) : `Range${rangeItems.length + 1}`;
2571
+ const securityDescriptor = protectedRange.securityDescriptor
2572
+ ? ` securityDescriptor="${escapeXmlAttr(protectedRange.securityDescriptor)}"`
2573
+ : "";
2574
+ rangeItems.push(`<protectedRange sqref="${escapeXmlAttr(sqref)}" name="${escapeXmlAttr(name)}"${securityDescriptor}/>`);
2575
+ }
2576
+ const rangesXml = rangeItems.length
2577
+ ? `<protectedRanges>${rangeItems.join("")}</protectedRanges>`
2578
+ : "";
2579
+ return `${protectionXml}${rangesXml}`;
2580
+ };
2335
2581
  const buildSheetPropertiesXml = (sheet) => {
2336
2582
  const tabColor = serializeColorXml(sheet.tabColor ?? undefined);
2337
2583
  // <outlinePr> mirrors the sheet's `outlinePr` setting; both attrs
@@ -2348,11 +2594,105 @@ const buildSheetPropertiesXml = (sheet) => {
2348
2594
  const outlineXml = outlineAttrs.length
2349
2595
  ? `<outlinePr ${outlineAttrs.join(" ")}/>`
2350
2596
  : "";
2351
- if (!tabColor && !outlineXml)
2597
+ // `<pageSetUpPr fitToPage="1">` is what makes Excel honour
2598
+ // `<pageSetup fitToWidth/fitToHeight>` instead of `scale`.
2599
+ const pageSetUpPrXml = sheet.printSettings?.pageSetup?.fitToPage
2600
+ ? `<pageSetUpPr fitToPage="1"/>`
2601
+ : "";
2602
+ if (!tabColor && !outlineXml && !pageSetUpPrXml)
2352
2603
  return "";
2353
- const inner = `${tabColor ? `<tabColor ${tabColor}/>` : ""}${outlineXml}`;
2604
+ const inner = `${tabColor ? `<tabColor ${tabColor}/>` : ""}${outlineXml}${pageSetUpPrXml}`;
2354
2605
  return `<sheetPr>${inner}</sheetPr>`;
2355
2606
  };
2607
+ const xmlBoolAttr = (name, value) => value == null ? "" : ` ${name}="${value ? "1" : "0"}"`;
2608
+ const xmlNumAttr = (name, value) => typeof value === "number" && Number.isFinite(value)
2609
+ ? ` ${name}="${value}"`
2610
+ : "";
2611
+ /**
2612
+ * The `CT_Worksheet` print tail — `<printOptions>`, `<pageMargins>`,
2613
+ * `<pageSetup>`, `<headerFooter>`, `<rowBreaks>`, `<colBreaks>` — in schema
2614
+ * order (after `<hyperlinks>`, before `<drawing>`). `<pageMargins>` is
2615
+ * mandatory in practice (Excel always writes it), so the default margins go
2616
+ * out when the sheet has none of its own.
2617
+ */
2618
+ const buildPrintSettingsXml = (print) => {
2619
+ const parts = [];
2620
+ const options = print?.printOptions;
2621
+ const optionAttrs = xmlBoolAttr("horizontalCentered", options?.horizontalCentered) +
2622
+ xmlBoolAttr("verticalCentered", options?.verticalCentered) +
2623
+ xmlBoolAttr("headings", options?.headings) +
2624
+ xmlBoolAttr("gridLines", options?.gridLines);
2625
+ if (optionAttrs)
2626
+ parts.push(`<printOptions${optionAttrs}/>`);
2627
+ const margins = print?.pageMargins;
2628
+ parts.push(margins &&
2629
+ Object.values(margins).some((v) => typeof v === "number")
2630
+ ? `<pageMargins left="${margins.left ?? 0.7}" right="${margins.right ?? 0.7}" top="${margins.top ?? 0.75}" bottom="${margins.bottom ?? 0.75}" header="${margins.header ?? 0.3}" footer="${margins.footer ?? 0.3}"/>`
2631
+ : DEFAULT_PAGE_MARGINS);
2632
+ const setup = print?.pageSetup;
2633
+ if (setup) {
2634
+ // Excel keeps `firstPageNumber` as a dormant value until the user picks
2635
+ // a first page number (`useFirstPageNumber="1"`); a bare number means
2636
+ // "Auto", so the flag is written only when the model has it on.
2637
+ const useFirst = setup.useFirstPageNumber === true ? true : undefined;
2638
+ const setupAttrs = xmlNumAttr("paperSize", setup.paperSize) +
2639
+ xmlNumAttr("scale", setup.scale) +
2640
+ xmlNumAttr("firstPageNumber", setup.firstPageNumber) +
2641
+ xmlNumAttr("fitToWidth", setup.fitToWidth) +
2642
+ xmlNumAttr("fitToHeight", setup.fitToHeight) +
2643
+ (setup.pageOrder ? ` pageOrder="${setup.pageOrder}"` : "") +
2644
+ (setup.orientation ? ` orientation="${setup.orientation}"` : "") +
2645
+ xmlBoolAttr("blackAndWhite", setup.blackAndWhite) +
2646
+ xmlBoolAttr("draft", setup.draft) +
2647
+ xmlBoolAttr("useFirstPageNumber", useFirst);
2648
+ if (setupAttrs)
2649
+ parts.push(`<pageSetup${setupAttrs}/>`);
2650
+ }
2651
+ const hf = print?.headerFooter;
2652
+ if (hf && !isHeaderFooterEmpty(hf)) {
2653
+ const hfAttrs = xmlBoolAttr("differentOddEven", hf.differentOddEven) +
2654
+ xmlBoolAttr("differentFirst", hf.differentFirst) +
2655
+ xmlBoolAttr("scaleWithDoc", hf.scaleWithDoc) +
2656
+ xmlBoolAttr("alignWithMargins", hf.alignWithMargins);
2657
+ const children = [
2658
+ "oddHeader",
2659
+ "oddFooter",
2660
+ "evenHeader",
2661
+ "evenFooter",
2662
+ "firstHeader",
2663
+ "firstFooter",
2664
+ ]
2665
+ .map((key) => {
2666
+ const text = hf[key];
2667
+ return typeof text === "string" && text
2668
+ ? `<${key}>${escapeXmlText(text)}</${key}>`
2669
+ : "";
2670
+ })
2671
+ .join("");
2672
+ parts.push(children
2673
+ ? `<headerFooter${hfAttrs}>${children}</headerFooter>`
2674
+ : `<headerFooter${hfAttrs}/>`);
2675
+ }
2676
+ const breaks = (ids, max) => {
2677
+ const valid = Array.from(new Set((ids ?? []).filter((id) => Number.isInteger(id) && id > 0))).sort((a, b) => a - b);
2678
+ if (!valid.length)
2679
+ return "";
2680
+ return valid
2681
+ .map((id) => `<brk id="${id}" max="${max}" man="1"/>`)
2682
+ .join("");
2683
+ };
2684
+ const rowBrks = breaks(print?.rowBreaks, 16383);
2685
+ if (rowBrks) {
2686
+ const count = rowBrks.split("<brk ").length - 1;
2687
+ parts.push(`<rowBreaks count="${count}" manualBreakCount="${count}">${rowBrks}</rowBreaks>`);
2688
+ }
2689
+ const colBrks = breaks(print?.columnBreaks, 1048575);
2690
+ if (colBrks) {
2691
+ const count = colBrks.split("<brk ").length - 1;
2692
+ parts.push(`<colBreaks count="${count}" manualBreakCount="${count}">${colBrks}</colBreaks>`);
2693
+ }
2694
+ return parts.join("");
2695
+ };
2356
2696
  const buildFilterColumnsXml = (filterSpecs, range, rows, headerRow = true, totalRow = false, styleBuilder) => {
2357
2697
  if (!filterSpecs?.length)
2358
2698
  return "";
@@ -2420,15 +2760,26 @@ const gridRangeToRef = (range) => {
2420
2760
  };
2421
2761
  const groupEmbedsBySheet = (embeds) => {
2422
2762
  const result = new Map();
2423
- for (const embed of embeds ?? []) {
2763
+ (embeds ?? []).forEach((embed, order) => {
2424
2764
  if (!embed.position?.overlayPosition)
2425
- continue;
2765
+ return;
2426
2766
  if (embed.spec?.type !== "image")
2427
- continue;
2767
+ return;
2428
2768
  const overlay = embed.position.overlayPosition;
2769
+ const imageSpec = embed.spec;
2770
+ const adjustments = {
2771
+ crop: imageSpec.crop ?? undefined,
2772
+ brightness: imageSpec.brightness ?? undefined,
2773
+ contrast: imageSpec.contrast ?? undefined,
2774
+ transparency: imageSpec.transparency ?? undefined,
2775
+ };
2429
2776
  const placement = {
2430
2777
  kind: "embed",
2431
- imageUrl: embed.spec.imageUrl,
2778
+ imageUrl: imageSpec.imageUrl,
2779
+ adjustments: Object.values(adjustments).some((v) => v !== undefined)
2780
+ ? adjustments
2781
+ : undefined,
2782
+ order,
2432
2783
  rowIndex: overlay.anchorCell.rowIndex,
2433
2784
  columnIndex: overlay.anchorCell.columnIndex,
2434
2785
  offsetX: overlay.offsetXPixels ?? 0,
@@ -2439,19 +2790,20 @@ const groupEmbedsBySheet = (embeds) => {
2439
2790
  const list = result.get(embed.position.sheetId) ?? [];
2440
2791
  list.push(placement);
2441
2792
  result.set(embed.position.sheetId, list);
2442
- }
2793
+ });
2443
2794
  return result;
2444
2795
  };
2445
2796
  const groupTextBoxesBySheet = (embeds) => {
2446
2797
  const result = new Map();
2447
- for (const embed of embeds ?? []) {
2798
+ (embeds ?? []).forEach((embed, order) => {
2448
2799
  if (!embed.position?.overlayPosition)
2449
- continue;
2800
+ return;
2450
2801
  if (embed.spec?.type !== "textbox")
2451
- continue;
2802
+ return;
2452
2803
  const overlay = embed.position.overlayPosition;
2453
2804
  const placement = {
2454
2805
  textBox: embed,
2806
+ order,
2455
2807
  rowIndex: overlay.anchorCell.rowIndex,
2456
2808
  columnIndex: overlay.anchorCell.columnIndex,
2457
2809
  offsetX: overlay.offsetXPixels ?? 0,
@@ -2462,19 +2814,144 @@ const groupTextBoxesBySheet = (embeds) => {
2462
2814
  const list = result.get(embed.position.sheetId) ?? [];
2463
2815
  list.push(placement);
2464
2816
  result.set(embed.position.sheetId, list);
2465
- }
2817
+ });
2466
2818
  return result;
2467
2819
  };
2820
+ /** Form-control embeds (`spec.type === "formControl"`) by sheet, in host order. */
2821
+ const groupFormControlsBySheet = (embeds) => {
2822
+ const result = new Map();
2823
+ (embeds ?? []).forEach((embed, order) => {
2824
+ if (!embed.position?.overlayPosition)
2825
+ return;
2826
+ if (embed.spec?.type !== "formControl")
2827
+ return;
2828
+ const overlay = embed.position.overlayPosition;
2829
+ const placement = {
2830
+ shape: embed,
2831
+ order,
2832
+ rowIndex: overlay.anchorCell.rowIndex,
2833
+ columnIndex: overlay.anchorCell.columnIndex,
2834
+ offsetX: overlay.offsetXPixels ?? 0,
2835
+ offsetY: overlay.offsetYPixels ?? 0,
2836
+ width: overlay.widthPixels ?? 120,
2837
+ height: overlay.heightPixels ?? 20,
2838
+ };
2839
+ const list = result.get(embed.position.sheetId) ?? [];
2840
+ list.push(placement);
2841
+ result.set(embed.position.sheetId, list);
2842
+ });
2843
+ return result;
2844
+ };
2845
+ /**
2846
+ * Write a sheet's form controls: one ctrlProp part + sheet relationship per
2847
+ * control, the `<controls>` worksheet element, and the VML shapes Excel
2848
+ * draws them with (merged into the sheet's `vmlDrawing` alongside notes).
2849
+ */
2850
+ const buildSheetFormControls = ({ sheet, controls, relationships, ctrlProps, zIndexStart, reservedShapeIds, }) => {
2851
+ if (!controls.length)
2852
+ return { controlsXml: "", vmlShapes: [] };
2853
+ const controlElements = [];
2854
+ const vmlShapes = [];
2855
+ // Excel numbers notes and form-control shapes from one per-sheet counter
2856
+ // (1025 upward); a control that arrived with its own id keeps it unless it
2857
+ // collides with a note shape or another control.
2858
+ const usedShapeIds = new Set(reservedShapeIds);
2859
+ let nextShapeId = 1025;
2860
+ const allocateShapeId = (wanted) => {
2861
+ if (wanted && Number.isInteger(wanted) && !usedShapeIds.has(wanted)) {
2862
+ usedShapeIds.add(wanted);
2863
+ return wanted;
2864
+ }
2865
+ while (usedShapeIds.has(nextShapeId))
2866
+ nextShapeId += 1;
2867
+ usedShapeIds.add(nextShapeId);
2868
+ return nextShapeId++;
2869
+ };
2870
+ const sumSizes = (count, getSize) => {
2871
+ let total = 0;
2872
+ for (let index = 0; index < count; index++)
2873
+ total += getSize(index);
2874
+ return total;
2875
+ };
2876
+ controls.forEach((placement, position) => {
2877
+ const spec = placement.shape.spec;
2878
+ const fromRow = Math.max(0, placement.rowIndex - 1);
2879
+ const fromColumn = Math.max(0, placement.columnIndex - 1);
2880
+ const offsetX = Math.max(0, placement.offsetX ?? 0);
2881
+ const offsetY = Math.max(0, placement.offsetY ?? 0);
2882
+ const widthPx = Math.max(5, placement.width ?? 120);
2883
+ const heightPx = Math.max(5, placement.height ?? 20);
2884
+ const columnSize = (index) => getColumnWidthPx(sheet, index, true);
2885
+ const rowSize = (index) => getRowHeightPx(sheet, index, true);
2886
+ const toColumn = resolveAnchorEndpoint({
2887
+ startIndex: fromColumn,
2888
+ startOffset: offsetX,
2889
+ span: widthPx,
2890
+ getSize: columnSize,
2891
+ });
2892
+ const toRow = resolveAnchorEndpoint({
2893
+ startIndex: fromRow,
2894
+ startOffset: offsetY,
2895
+ span: heightPx,
2896
+ getSize: rowSize,
2897
+ });
2898
+ const partIndex = ctrlProps.parts.length + 1;
2899
+ const path = `xl/ctrlProps/ctrlProp${partIndex}.xml`;
2900
+ ctrlProps.parts.push({ path, xml: buildCtrlPropXml(spec) });
2901
+ const relId = relationships.add(CTRL_PROP_REL_TYPE, `../ctrlProps/ctrlProp${partIndex}.xml`);
2902
+ const shapeId = allocateShapeId(spec.shapeId);
2903
+ controlElements.push(buildControlElementXml({
2904
+ spec,
2905
+ relId,
2906
+ shapeId,
2907
+ from: {
2908
+ col: fromColumn,
2909
+ colOff: offsetX * EMUS_PER_PIXEL,
2910
+ row: fromRow,
2911
+ rowOff: offsetY * EMUS_PER_PIXEL,
2912
+ },
2913
+ to: {
2914
+ col: toColumn.index,
2915
+ colOff: toColumn.offset * EMUS_PER_PIXEL,
2916
+ row: toRow.index,
2917
+ rowOff: toRow.offset * EMUS_PER_PIXEL,
2918
+ },
2919
+ }));
2920
+ vmlShapes.push(buildFormControlVmlShape({
2921
+ spec,
2922
+ shapeId,
2923
+ zIndex: zIndexStart + position + 1,
2924
+ geometry: {
2925
+ leftPx: sumSizes(fromColumn, columnSize) + offsetX,
2926
+ topPx: sumSizes(fromRow, rowSize) + offsetY,
2927
+ widthPx,
2928
+ heightPx,
2929
+ anchor: [
2930
+ fromColumn,
2931
+ Math.round(offsetX),
2932
+ fromRow,
2933
+ Math.round(offsetY),
2934
+ toColumn.index,
2935
+ Math.round(toColumn.offset),
2936
+ toRow.index,
2937
+ Math.round(toRow.offset),
2938
+ ],
2939
+ },
2940
+ }));
2941
+ });
2942
+ return { controlsXml: buildControlsXml(controlElements), vmlShapes };
2943
+ };
2468
2944
  const groupShapesBySheet = (embeds) => {
2469
2945
  const result = new Map();
2470
- for (const embed of embeds ?? []) {
2946
+ (embeds ?? []).forEach((embed, order) => {
2471
2947
  if (!embed.position?.overlayPosition)
2472
- continue;
2948
+ return;
2473
2949
  if (embed.spec?.type !== "shape")
2474
- continue;
2950
+ return;
2475
2951
  const overlay = embed.position.overlayPosition;
2476
2952
  const placement = {
2477
2953
  shape: embed,
2954
+ order,
2478
2955
  rowIndex: overlay.anchorCell.rowIndex,
2479
2956
  columnIndex: overlay.anchorCell.columnIndex,
2480
2957
  offsetX: overlay.offsetXPixels ?? 0,
@@ -2485,7 +2962,7 @@ const groupShapesBySheet = (embeds) => {
2485
2962
  const list = result.get(embed.position.sheetId) ?? [];
2486
2963
  list.push(placement);
2487
2964
  result.set(embed.position.sheetId, list);
2488
- }
2965
+ });
2489
2966
  return result;
2490
2967
  };
2491
2968
  const groupChartsBySheet = (charts) => {
@@ -3077,7 +3554,7 @@ const buildConditionalFormattingRuleXml = ({ rule, styleBuilder, priority, baseC
3077
3554
  rules.push(dataBarXml);
3078
3555
  return rules.length ? rules : undefined;
3079
3556
  };
3080
- const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImageIndex, startChartIndex, charts, slicers, sheet, sheetNameById, }) => {
3557
+ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImageIndex, startChartIndex, startChartExIndex = 1, charts, slicers, sheet, sheetNameById, }) => {
3081
3558
  if (!placements.length &&
3082
3559
  !textBoxes.length &&
3083
3560
  !shapes.length &&
@@ -3089,10 +3566,27 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
3089
3566
  const media = [];
3090
3567
  let imageIndex = startImageIndex;
3091
3568
  let chartIndex = startChartIndex;
3569
+ let chartExIndex = startChartExIndex;
3092
3570
  let pictureId = 1;
3093
3571
  let frameId = 1;
3094
3572
  const chartEntries = [];
3573
+ const chartExEntries = [];
3095
3574
  for (const placement of charts) {
3575
+ if (isChartExType(placement.chart.spec.chartType)) {
3576
+ // Treemap / sunburst live in a chartex part with an mc:AlternateContent anchor.
3577
+ const chartExXml = buildChartExXml(placement.chart, sheetNameById);
3578
+ if (!chartExXml)
3579
+ continue;
3580
+ const relId = relationships.add(CHART_EX_REL_TYPE, `../charts/chartEx${chartExIndex}.xml`);
3581
+ anchors.push(createChartAnchor({ sheet, placement, relId, frameId, chartEx: true }));
3582
+ chartExEntries.push({
3583
+ path: `xl/charts/chartEx${chartExIndex}.xml`,
3584
+ xml: chartExXml,
3585
+ });
3586
+ chartExIndex += 1;
3587
+ frameId += 1;
3588
+ continue;
3589
+ }
3096
3590
  const chartXml = buildChartXml(placement.chart, sheetNameById);
3097
3591
  if (!chartXml)
3098
3592
  continue;
@@ -3118,20 +3612,32 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
3118
3612
  }));
3119
3613
  frameId += 1;
3120
3614
  }
3615
+ // Embeds (text boxes, shapes, images) keep the host's `embeds` order in the
3616
+ // drawing part — anchor order IS Excel's z-order, so an image drawn over a
3617
+ // shape must be emitted after it regardless of kind. Placements without an
3618
+ // order (in-cell images) go last, in their own order.
3619
+ const embedAnchors = [];
3620
+ const orderOf = (order) => order ?? Number.MAX_SAFE_INTEGER;
3121
3621
  for (const placement of textBoxes) {
3122
- anchors.push(createTextBoxAnchor({
3123
- sheet,
3124
- placement,
3125
- frameId,
3126
- }));
3622
+ embedAnchors.push({
3623
+ order: orderOf(placement.order),
3624
+ xml: createTextBoxAnchor({
3625
+ sheet,
3626
+ placement,
3627
+ frameId,
3628
+ }),
3629
+ });
3127
3630
  frameId += 1;
3128
3631
  }
3129
3632
  for (const placement of shapes) {
3130
- anchors.push(createShapeAnchor({
3131
- sheet,
3132
- placement,
3133
- frameId,
3134
- }));
3633
+ embedAnchors.push({
3634
+ order: orderOf(placement.order),
3635
+ xml: createShapeAnchor({
3636
+ sheet,
3637
+ placement,
3638
+ frameId,
3639
+ }),
3640
+ });
3135
3641
  frameId += 1;
3136
3642
  }
3137
3643
  for (const placement of placements) {
@@ -3148,23 +3654,31 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
3148
3654
  extension: buffer.extension,
3149
3655
  });
3150
3656
  const relId = relationships.add(IMAGE_REL_TYPE, `../media/image${imageIndex}.${buffer.extension}`);
3151
- anchors.push(createTwoCellAnchor({
3152
- sheet,
3153
- placement,
3154
- relId,
3155
- pictureId,
3156
- }));
3657
+ embedAnchors.push({
3658
+ order: orderOf(placement.order),
3659
+ xml: createTwoCellAnchor({
3660
+ sheet,
3661
+ placement,
3662
+ relId,
3663
+ pictureId,
3664
+ }),
3665
+ });
3157
3666
  imageIndex += 1;
3158
3667
  pictureId += 1;
3159
3668
  }
3669
+ embedAnchors.sort((a, b) => a.order - b.order);
3670
+ for (const entry of embedAnchors)
3671
+ anchors.push(entry.xml);
3160
3672
  const drawingXml = `${XML_DECLARATION}<xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="${REL_NS}">${anchors.join("")}</xdr:wsDr>`;
3161
3673
  return {
3162
3674
  xml: drawingXml,
3163
3675
  relsXml: relationships.toXml(),
3164
3676
  media,
3165
3677
  charts: chartEntries,
3678
+ chartExs: chartExEntries,
3166
3679
  nextImageIndex: imageIndex,
3167
3680
  nextChartIndex: chartIndex,
3681
+ nextChartExIndex: chartExIndex,
3168
3682
  };
3169
3683
  };
3170
3684
  const createTwoCellAnchor = ({ placement, relId, pictureId, sheet, }) => {
@@ -3190,9 +3704,55 @@ const createTwoCellAnchor = ({ placement, relId, pictureId, sheet, }) => {
3190
3704
  });
3191
3705
  const fromXml = `<xdr:from><xdr:col>${fromColumn}</xdr:col><xdr:colOff>${Math.round(offsetX * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${fromRow}</xdr:row><xdr:rowOff>${Math.round(offsetY * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:from>`;
3192
3706
  const toXml = `<xdr:to><xdr:col>${toColumn.index}</xdr:col><xdr:colOff>${Math.round(toColumn.offset * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${toRow.index}</xdr:row><xdr:rowOff>${Math.round(toRow.offset * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:to>`;
3193
- return `<xdr:twoCellAnchor editAs="oneCell">${fromXml}${toXml}<xdr:pic><xdr:nvPicPr><xdr:cNvPr id="${pictureId}" name="Picture ${pictureId}"/><xdr:cNvPicPr/></xdr:nvPicPr><xdr:blipFill><a:blip r:embed="${relId}"/><a:stretch><a:fillRect/></a:stretch></xdr:blipFill><xdr:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${Math.round(widthPx * EMUS_PER_PIXEL)}" cy="${Math.round(heightPx * EMUS_PER_PIXEL)}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></xdr:spPr></xdr:pic><xdr:clientData/></xdr:twoCellAnchor>`;
3707
+ const { blipChildren, srcRectXml } = buildPictureAdjustmentsXml(placement.adjustments);
3708
+ const blipXml = blipChildren
3709
+ ? `<a:blip r:embed="${relId}">${blipChildren}</a:blip>`
3710
+ : `<a:blip r:embed="${relId}"/>`;
3711
+ return `<xdr:twoCellAnchor editAs="oneCell">${fromXml}${toXml}<xdr:pic><xdr:nvPicPr><xdr:cNvPr id="${pictureId}" name="Picture ${pictureId}"/><xdr:cNvPicPr/></xdr:nvPicPr><xdr:blipFill>${blipXml}${srcRectXml}<a:stretch><a:fillRect/></a:stretch></xdr:blipFill><xdr:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${Math.round(widthPx * EMUS_PER_PIXEL)}" cy="${Math.round(heightPx * EMUS_PER_PIXEL)}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></xdr:spPr></xdr:pic><xdr:clientData/></xdr:twoCellAnchor>`;
3194
3712
  };
3195
- const createChartAnchor = ({ placement, relId, frameId, sheet, }) => {
3713
+ /** Fraction (0…1) → DrawingML per-mille percent (100000 = 100%). */
3714
+ const fractionToPct = (value) => Math.round(Math.max(-1, Math.min(1, value)) * 100000);
3715
+ /**
3716
+ * `<a:blip>` children (`<a:alphaModFix>`, `<a:lum>`) and the `<a:srcRect>`
3717
+ * sibling for an image's picture-format adjustments; both empty when the
3718
+ * spec carries none.
3719
+ */
3720
+ const buildPictureAdjustmentsXml = (adjustments) => {
3721
+ if (!adjustments)
3722
+ return { blipChildren: "", srcRectXml: "" };
3723
+ let blipChildren = "";
3724
+ const { transparency, brightness, contrast, crop } = adjustments;
3725
+ if (typeof transparency === "number" && transparency > 0) {
3726
+ blipChildren += `<a:alphaModFix amt="${fractionToPct(1 - Math.min(1, transparency))}"/>`;
3727
+ }
3728
+ const hasBrightness = typeof brightness === "number" && brightness !== 0;
3729
+ const hasContrast = typeof contrast === "number" && contrast !== 0;
3730
+ if (hasBrightness || hasContrast) {
3731
+ blipChildren += `<a:lum${hasBrightness ? ` bright="${fractionToPct(brightness)}"` : ""}${hasContrast ? ` contrast="${fractionToPct(contrast)}"` : ""}/>`;
3732
+ }
3733
+ let srcRectXml = "";
3734
+ if (crop) {
3735
+ const edges = ["l", "t", "r", "b"]
3736
+ .map((edge) => {
3737
+ const value = {
3738
+ l: crop.left,
3739
+ t: crop.top,
3740
+ r: crop.right,
3741
+ b: crop.bottom,
3742
+ }[edge];
3743
+ // Negative edges are Excel's "out-crop" (padding around the
3744
+ // bitmap) and round-trip like positive ones.
3745
+ return typeof value === "number" && Number.isFinite(value) && value !== 0
3746
+ ? ` ${edge}="${fractionToPct(value)}"`
3747
+ : "";
3748
+ })
3749
+ .join("");
3750
+ if (edges)
3751
+ srcRectXml = `<a:srcRect${edges}/>`;
3752
+ }
3753
+ return { blipChildren, srcRectXml };
3754
+ };
3755
+ const createChartAnchor = ({ placement, relId, frameId, sheet, chartEx = false, }) => {
3196
3756
  const fromRow = Math.max(0, placement.rowIndex - 1);
3197
3757
  const fromColumn = Math.max(0, placement.columnIndex - 1);
3198
3758
  const offsetX = Math.max(0, placement.offsetX ?? 0);
@@ -3213,6 +3773,14 @@ const createChartAnchor = ({ placement, relId, frameId, sheet, }) => {
3213
3773
  });
3214
3774
  const fromXml = `<xdr:from><xdr:col>${fromColumn}</xdr:col><xdr:colOff>${Math.round(offsetX * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${fromRow}</xdr:row><xdr:rowOff>${Math.round(offsetY * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:from>`;
3215
3775
  const toXml = `<xdr:to><xdr:col>${toColumn.index}</xdr:col><xdr:colOff>${Math.round(toColumn.offset * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${toRow.index}</xdr:row><xdr:rowOff>${Math.round(toRow.offset * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:to>`;
3776
+ if (chartEx) {
3777
+ return `<xdr:twoCellAnchor>${fromXml}${toXml}${buildChartExGraphicFrameXml({
3778
+ relId,
3779
+ frameId,
3780
+ widthEmu: Math.round(widthPx * EMUS_PER_PIXEL),
3781
+ heightEmu: Math.round(heightPx * EMUS_PER_PIXEL),
3782
+ })}<xdr:clientData/></xdr:twoCellAnchor>`;
3783
+ }
3216
3784
  return `<xdr:twoCellAnchor>${fromXml}${toXml}<xdr:graphicFrame><xdr:nvGraphicFramePr><xdr:cNvPr id="${frameId}" name="Chart ${frameId}"/><xdr:cNvGraphicFramePr/></xdr:nvGraphicFramePr><xdr:xfrm><a:off x="0" y="0"/><a:ext cx="${Math.round(widthPx * EMUS_PER_PIXEL)}" cy="${Math.round(heightPx * EMUS_PER_PIXEL)}"/></xdr:xfrm><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:r="${REL_NS}" r:id="${relId}"/></a:graphicData></a:graphic></xdr:graphicFrame><xdr:clientData/></xdr:twoCellAnchor>`;
3217
3785
  };
3218
3786
  const createSlicerAnchor = ({ placement, frameId, sheet, }) => {
@@ -3857,7 +4425,10 @@ const buildWorkbookXml = (sheets, sheetRelIds, sheetNames, namedRanges, iterativ
3857
4425
  sheets.forEach((sheet, index) => {
3858
4426
  sheetNameById.set(sheet.sheetId ?? index + 1, sheetNames[index]);
3859
4427
  });
3860
- const definedNamesXml = buildDefinedNamesXml(namedRanges, sheetNameById);
4428
+ const definedNamesXml = buildDefinedNamesXml(namedRanges, sheetNameById, sheets.map((sheet, index) => ({
4429
+ sheetName: sheetNames[index] ?? `Sheet${index + 1}`,
4430
+ printSettings: sheet.printSettings,
4431
+ })));
3861
4432
  const calcPrXml = buildCalcPrXml(iterativeCalculationOptions);
3862
4433
  const slicerCachesXml = buildWorkbookSlicerCachesXml(slicerCacheRelIds);
3863
4434
  // Phase D2 — <pivotCaches> element. Each <pivotCache> pairs a
@@ -3886,12 +4457,48 @@ const buildWorkbookXml = (sheets, sheetRelIds, sheetNames, namedRanges, iterativ
3886
4457
  : `<workbookPr/>`;
3887
4458
  return `${XML_DECLARATION}<workbook xmlns="${WORKSHEET_NS}" xmlns:r="${REL_NS}">${workbookPrXml}${calcPrXml}<bookViews><workbookView activeTab="0" firstSheet="0"/></bookViews><sheets>${sheetEntries}</sheets>${definedNamesXml}${externalReferencesXml}${pivotCachesXml}${slicerCachesXml}</workbook>`;
3888
4459
  };
3889
- const buildDefinedNamesXml = (namedRanges, sheetNameById) => {
3890
- if (!namedRanges?.length)
3891
- return "";
4460
+ const buildDefinedNamesXml = (namedRanges, sheetNameById, printScopes = []) => {
3892
4461
  const entries = [];
3893
- for (const namedRange of namedRanges) {
3894
- if (!namedRange?.name)
4462
+ // The builtin print names are sheet-scoped (`localSheetId` = position in
4463
+ // `<sheets>`); they come from `sheet.printSettings`, never from
4464
+ // `namedRanges` (a global `_xlnm.Print_Area` makes Excel repair the file).
4465
+ // Documents imported before `printSettings` existed still carry the print
4466
+ // area as a `_xlnm.Print_Area` named range on the sheet; it is converted
4467
+ // to the sheet-scoped name here unless the sheet already has a print area.
4468
+ const sheetIndexByName = new Map(printScopes.map(({ sheetName }, index) => [sheetName, index]));
4469
+ const legacyPrintAreas = new Map();
4470
+ for (const namedRange of namedRanges ?? []) {
4471
+ if (namedRange?.name !== PRINT_AREA_NAME || !namedRange.range?.sheetId) {
4472
+ continue;
4473
+ }
4474
+ const sheetName = sheetNameById.get(namedRange.range.sheetId);
4475
+ const index = sheetName ? sheetIndexByName.get(sheetName) : undefined;
4476
+ if (index == null)
4477
+ continue;
4478
+ legacyPrintAreas.set(index, [
4479
+ ...(legacyPrintAreas.get(index) ?? []),
4480
+ namedRange.range,
4481
+ ]);
4482
+ }
4483
+ printScopes.forEach(({ sheetName, printSettings }, index) => {
4484
+ const printArea = printSettings?.printArea?.length
4485
+ ? printSettings.printArea
4486
+ : legacyPrintAreas.get(index);
4487
+ const area = printArea?.length
4488
+ ? buildPrintAreaFormula(sheetName, printArea)
4489
+ : undefined;
4490
+ if (area) {
4491
+ entries.push(`<definedName name="${PRINT_AREA_NAME}" localSheetId="${index}">${escapeXmlText(area)}</definedName>`);
4492
+ }
4493
+ const titles = printSettings?.printTitles
4494
+ ? buildPrintTitlesFormula(sheetName, printSettings.printTitles)
4495
+ : undefined;
4496
+ if (titles) {
4497
+ entries.push(`<definedName name="${PRINT_TITLES_NAME}" localSheetId="${index}">${escapeXmlText(titles)}</definedName>`);
4498
+ }
4499
+ });
4500
+ for (const namedRange of namedRanges ?? []) {
4501
+ if (!namedRange?.name || isBuiltinPrintName(namedRange.name))
3895
4502
  continue;
3896
4503
  const hiddenAttr = namedRange.hidden ? ` hidden="1"` : "";
3897
4504
  if (namedRange.range?.sheetId) {
@@ -3993,7 +4600,7 @@ const buildWorkbookRelationships = (sheetCount, slicerCacheCount = 0, externalLi
3993
4600
  slicerCacheRelationshipIds: slicerCacheRelIds,
3994
4601
  };
3995
4602
  };
3996
- const buildContentTypesXml = ({ sheetCount, tableCount, drawingSheets, mediaExtensions, chartCount, slicerCount = 0, slicerCacheCount = 0, pivotTableCount = 0, pivotCacheCount = 0, commentsSheets = [], threadedCommentsSheets = [], hasPersons = false, externalLinkCount = 0, }) => {
4603
+ const buildContentTypesXml = ({ sheetCount, tableCount, drawingSheets, mediaExtensions, chartCount, chartExCount = 0, slicerCount = 0, slicerCacheCount = 0, pivotTableCount = 0, pivotCacheCount = 0, commentsSheets = [], threadedCommentsSheets = [], hasPersons = false, externalLinkCount = 0, hasVmlDrawings = false, ctrlPropCount = 0, }) => {
3997
4604
  const sheetOverrides = Array.from({ length: sheetCount }, (_, index) => {
3998
4605
  return `<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
3999
4606
  }).join("");
@@ -4006,6 +4613,9 @@ const buildContentTypesXml = ({ sheetCount, tableCount, drawingSheets, mediaExte
4006
4613
  const chartOverrides = Array.from({ length: chartCount }, (_, index) => {
4007
4614
  return `<Override PartName="/xl/charts/chart${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.drawingml.chart+xml"/>`;
4008
4615
  }).join("");
4616
+ const chartExOverrides = Array.from({ length: chartExCount }, (_, index) => {
4617
+ return `<Override PartName="/xl/charts/chartEx${index + 1}.xml" ContentType="${CHART_EX_CONTENT_TYPE}"/>`;
4618
+ }).join("");
4009
4619
  const slicerOverrides = Array.from({ length: slicerCount }, (_, index) => {
4010
4620
  return `<Override PartName="/xl/slicers/slicer${index + 1}.xml" ContentType="application/vnd.ms-excel.slicer+xml"/>`;
4011
4621
  }).join("");
@@ -4030,10 +4640,11 @@ const buildContentTypesXml = ({ sheetCount, tableCount, drawingSheets, mediaExte
4030
4640
  const mediaDefaults = mediaExtensions
4031
4641
  .map((extension) => `<Default Extension="${extension}" ContentType="image/${extension}"/>`)
4032
4642
  .join("");
4033
- const vmlDefault = commentsSheets.length
4643
+ const vmlDefault = hasVmlDrawings || commentsSheets.length
4034
4644
  ? `<Default Extension="vml" ContentType="application/vnd.openxmlformats-officedocument.vmlDrawing"/>`
4035
4645
  : "";
4036
- return `${XML_DECLARATION}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>${vmlDefault}${mediaDefaults}<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>${sheetOverrides}${tableOverrides}${drawingOverrides}${chartOverrides}${slicerOverrides}${slicerCacheOverrides}${pivotTableOverrides}${pivotCacheOverrides}${externalLinkOverrides}${commentsOverrides}${threadedCommentsOverrides}${personOverride}<Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>`;
4646
+ const ctrlPropOverrides = Array.from({ length: ctrlPropCount }, (_, index) => `<Override PartName="/xl/ctrlProps/ctrlProp${index + 1}.xml" ContentType="${CTRL_PROP_CONTENT_TYPE}"/>`).join("");
4647
+ return `${XML_DECLARATION}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>${vmlDefault}${mediaDefaults}<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>${sheetOverrides}${tableOverrides}${drawingOverrides}${chartOverrides}${chartExOverrides}${slicerOverrides}${slicerCacheOverrides}${pivotTableOverrides}${pivotCacheOverrides}${externalLinkOverrides}${ctrlPropOverrides}${commentsOverrides}${threadedCommentsOverrides}${personOverride}<Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>`;
4037
4648
  };
4038
4649
  const buildRootRelationshipsXml = () => {
4039
4650
  return `${XML_DECLARATION}<Relationships xmlns="${PACKAGE_REL_NS}"><Relationship Id="rId1" Type="${REL_NS}/officeDocument" Target="xl/workbook.xml"/><Relationship Id="rId2" Type="${REL_NS}/extended-properties" Target="docProps/app.xml"/><Relationship Id="rId3" Type="${REL_NS}/core-properties" Target="docProps/core.xml"/></Relationships>`;