@cj-tech-master/excelts 9.6.1 → 10.0.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 (207) hide show
  1. package/README.md +18 -3
  2. package/README_zh.md +18 -3
  3. package/dist/browser/modules/excel/cell.d.ts +4 -0
  4. package/dist/browser/modules/excel/note.js +5 -1
  5. package/dist/browser/modules/excel/row.js +35 -2
  6. package/dist/browser/modules/excel/stream/workbook-writer.browser.d.ts +8 -1
  7. package/dist/browser/modules/excel/stream/workbook-writer.browser.js +22 -2
  8. package/dist/browser/modules/excel/types.d.ts +81 -0
  9. package/dist/browser/modules/excel/utils/drawing-utils.d.ts +8 -0
  10. package/dist/browser/modules/excel/utils/drawing-utils.js +19 -2
  11. package/dist/browser/modules/excel/workbook.browser.d.ts +16 -0
  12. package/dist/browser/modules/excel/workbook.browser.js +32 -2
  13. package/dist/browser/modules/excel/worksheet.d.ts +31 -1
  14. package/dist/browser/modules/excel/worksheet.js +83 -0
  15. package/dist/browser/modules/excel/xlsx/xform/comment/vml-shape-xform.d.ts +7 -0
  16. package/dist/browser/modules/excel/xlsx/xform/comment/vml-shape-xform.js +42 -8
  17. package/dist/browser/modules/excel/xlsx/xform/core/content-types-xform.js +3 -1
  18. package/dist/browser/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +5 -0
  19. package/dist/browser/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +18 -1
  20. package/dist/browser/modules/excel/xlsx/xform/drawing/blip-xform.d.ts +6 -0
  21. package/dist/browser/modules/excel/xlsx/xform/drawing/blip-xform.js +38 -11
  22. package/dist/browser/modules/excel/xlsx/xform/drawing/one-cell-anchor-xform.d.ts +1 -0
  23. package/dist/browser/modules/excel/xlsx/xform/drawing/one-cell-anchor-xform.js +5 -0
  24. package/dist/browser/modules/excel/xlsx/xform/drawing/pic-xform.d.ts +2 -0
  25. package/dist/browser/modules/excel/xlsx/xform/drawing/pic-xform.js +2 -1
  26. package/dist/browser/modules/excel/xlsx/xform/drawing/shape-xform.d.ts +47 -0
  27. package/dist/browser/modules/excel/xlsx/xform/drawing/shape-xform.js +109 -0
  28. package/dist/browser/modules/excel/xlsx/xform/drawing/two-cell-anchor-xform.js +10 -1
  29. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +64 -1
  30. package/dist/browser/modules/pdf/builder/document-builder.js +22 -49
  31. package/dist/browser/modules/pdf/builder/pdf-editor.js +1 -1
  32. package/dist/browser/modules/pdf/core/pdf-stream.d.ts +28 -1
  33. package/dist/browser/modules/pdf/core/pdf-stream.js +38 -2
  34. package/dist/browser/modules/pdf/font/font-manager.d.ts +26 -0
  35. package/dist/browser/modules/pdf/font/font-manager.js +35 -18
  36. package/dist/browser/modules/pdf/render/page-renderer.d.ts +51 -3
  37. package/dist/browser/modules/pdf/render/page-renderer.js +111 -18
  38. package/dist/browser/modules/word/advanced/field-engine.js +45 -20
  39. package/dist/browser/modules/word/advanced/glossary.d.ts +10 -36
  40. package/dist/browser/modules/word/advanced/glossary.js +8 -9
  41. package/dist/browser/modules/word/advanced/math-convert.js +94 -12
  42. package/dist/browser/modules/word/advanced/ole-objects.d.ts +28 -0
  43. package/dist/browser/modules/word/advanced/ole-objects.js +122 -19
  44. package/dist/browser/modules/word/advanced/style-map.js +31 -10
  45. package/dist/browser/modules/word/builder/run-builders.d.ts +7 -1
  46. package/dist/browser/modules/word/builder/run-builders.js +7 -1
  47. package/dist/browser/modules/word/constants.d.ts +4 -0
  48. package/dist/browser/modules/word/constants.js +5 -1
  49. package/dist/browser/modules/word/convert/docx-to-semantic.d.ts +2 -1
  50. package/dist/browser/modules/word/convert/docx-to-semantic.js +135 -1
  51. package/dist/browser/modules/word/convert/html/html-import.d.ts +32 -1
  52. package/dist/browser/modules/word/convert/html/html-import.js +167 -14
  53. package/dist/browser/modules/word/convert/html/html.d.ts +2 -2
  54. package/dist/browser/modules/word/convert/html/html.js +1 -1
  55. package/dist/browser/modules/word/convert/markdown/markdown-import.d.ts +48 -18
  56. package/dist/browser/modules/word/convert/markdown/markdown-import.js +279 -69
  57. package/dist/browser/modules/word/convert/markdown/markdown.d.ts +1 -1
  58. package/dist/browser/modules/word/convert/odt/odt.js +407 -56
  59. package/dist/browser/modules/word/html.d.ts +2 -2
  60. package/dist/browser/modules/word/html.js +1 -1
  61. package/dist/browser/modules/word/index.base.d.ts +3 -3
  62. package/dist/browser/modules/word/index.base.js +1 -1
  63. package/dist/browser/modules/word/layout/layout-full.js +326 -19
  64. package/dist/browser/modules/word/layout/render-page.js +35 -8
  65. package/dist/browser/modules/word/markdown.d.ts +1 -1
  66. package/dist/browser/modules/word/query/compat.d.ts +10 -2
  67. package/dist/browser/modules/word/query/compat.js +29 -21
  68. package/dist/browser/modules/word/reader/docx-reader.js +105 -2
  69. package/dist/browser/modules/word/reader/math-parser.js +8 -2
  70. package/dist/browser/modules/word/security/cfb-reader.js +5 -5
  71. package/dist/browser/modules/word/types.d.ts +96 -1
  72. package/dist/browser/modules/word/writer/docx-packager.js +108 -2
  73. package/dist/browser/modules/word/writer/glossary-writer.d.ts +28 -0
  74. package/dist/browser/modules/word/writer/glossary-writer.js +121 -0
  75. package/dist/browser/modules/word/writer/header-footer-writer.js +105 -20
  76. package/dist/browser/modules/word/writer/math-writer.js +7 -2
  77. package/dist/browser/utils/font-metrics.d.ts +8 -0
  78. package/dist/browser/utils/font-metrics.js +43 -0
  79. package/dist/browser/utils/theme-colors.js +4 -1
  80. package/dist/cjs/modules/excel/note.js +5 -1
  81. package/dist/cjs/modules/excel/row.js +35 -2
  82. package/dist/cjs/modules/excel/stream/workbook-writer.browser.js +22 -2
  83. package/dist/cjs/modules/excel/utils/drawing-utils.js +19 -2
  84. package/dist/cjs/modules/excel/workbook.browser.js +31 -1
  85. package/dist/cjs/modules/excel/worksheet.js +83 -0
  86. package/dist/cjs/modules/excel/xlsx/xform/comment/vml-shape-xform.js +42 -8
  87. package/dist/cjs/modules/excel/xlsx/xform/core/content-types-xform.js +3 -1
  88. package/dist/cjs/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +5 -0
  89. package/dist/cjs/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +18 -1
  90. package/dist/cjs/modules/excel/xlsx/xform/drawing/blip-xform.js +38 -11
  91. package/dist/cjs/modules/excel/xlsx/xform/drawing/one-cell-anchor-xform.js +5 -0
  92. package/dist/cjs/modules/excel/xlsx/xform/drawing/pic-xform.js +2 -1
  93. package/dist/cjs/modules/excel/xlsx/xform/drawing/shape-xform.js +112 -0
  94. package/dist/cjs/modules/excel/xlsx/xform/drawing/two-cell-anchor-xform.js +10 -1
  95. package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +64 -1
  96. package/dist/cjs/modules/pdf/builder/document-builder.js +21 -48
  97. package/dist/cjs/modules/pdf/builder/pdf-editor.js +1 -1
  98. package/dist/cjs/modules/pdf/core/pdf-stream.js +38 -2
  99. package/dist/cjs/modules/pdf/font/font-manager.js +35 -18
  100. package/dist/cjs/modules/pdf/render/page-renderer.js +112 -18
  101. package/dist/cjs/modules/word/advanced/field-engine.js +45 -20
  102. package/dist/cjs/modules/word/advanced/glossary.js +8 -9
  103. package/dist/cjs/modules/word/advanced/math-convert.js +94 -12
  104. package/dist/cjs/modules/word/advanced/ole-objects.js +123 -19
  105. package/dist/cjs/modules/word/advanced/style-map.js +31 -10
  106. package/dist/cjs/modules/word/builder/run-builders.js +7 -1
  107. package/dist/cjs/modules/word/constants.js +5 -1
  108. package/dist/cjs/modules/word/convert/docx-to-semantic.js +135 -1
  109. package/dist/cjs/modules/word/convert/html/html-import.js +168 -14
  110. package/dist/cjs/modules/word/convert/html/html.js +2 -1
  111. package/dist/cjs/modules/word/convert/markdown/markdown-import.js +279 -69
  112. package/dist/cjs/modules/word/convert/odt/odt.js +407 -56
  113. package/dist/cjs/modules/word/html.js +2 -1
  114. package/dist/cjs/modules/word/index.base.js +4 -3
  115. package/dist/cjs/modules/word/layout/layout-full.js +325 -18
  116. package/dist/cjs/modules/word/layout/render-page.js +35 -8
  117. package/dist/cjs/modules/word/query/compat.js +29 -21
  118. package/dist/cjs/modules/word/reader/docx-reader.js +104 -1
  119. package/dist/cjs/modules/word/reader/math-parser.js +8 -2
  120. package/dist/cjs/modules/word/security/cfb-reader.js +5 -5
  121. package/dist/cjs/modules/word/writer/docx-packager.js +108 -2
  122. package/dist/cjs/modules/word/writer/glossary-writer.js +124 -0
  123. package/dist/cjs/modules/word/writer/header-footer-writer.js +105 -20
  124. package/dist/cjs/modules/word/writer/math-writer.js +7 -2
  125. package/dist/cjs/utils/font-metrics.js +44 -0
  126. package/dist/cjs/utils/theme-colors.js +4 -1
  127. package/dist/esm/modules/excel/note.js +5 -1
  128. package/dist/esm/modules/excel/row.js +35 -2
  129. package/dist/esm/modules/excel/stream/workbook-writer.browser.js +22 -2
  130. package/dist/esm/modules/excel/utils/drawing-utils.js +19 -2
  131. package/dist/esm/modules/excel/workbook.browser.js +32 -2
  132. package/dist/esm/modules/excel/worksheet.js +83 -0
  133. package/dist/esm/modules/excel/xlsx/xform/comment/vml-shape-xform.js +42 -8
  134. package/dist/esm/modules/excel/xlsx/xform/core/content-types-xform.js +3 -1
  135. package/dist/esm/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +5 -0
  136. package/dist/esm/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +18 -1
  137. package/dist/esm/modules/excel/xlsx/xform/drawing/blip-xform.js +38 -11
  138. package/dist/esm/modules/excel/xlsx/xform/drawing/one-cell-anchor-xform.js +5 -0
  139. package/dist/esm/modules/excel/xlsx/xform/drawing/pic-xform.js +2 -1
  140. package/dist/esm/modules/excel/xlsx/xform/drawing/shape-xform.js +109 -0
  141. package/dist/esm/modules/excel/xlsx/xform/drawing/two-cell-anchor-xform.js +10 -1
  142. package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +64 -1
  143. package/dist/esm/modules/pdf/builder/document-builder.js +22 -49
  144. package/dist/esm/modules/pdf/builder/pdf-editor.js +1 -1
  145. package/dist/esm/modules/pdf/core/pdf-stream.js +38 -2
  146. package/dist/esm/modules/pdf/font/font-manager.js +35 -18
  147. package/dist/esm/modules/pdf/render/page-renderer.js +111 -18
  148. package/dist/esm/modules/word/advanced/field-engine.js +45 -20
  149. package/dist/esm/modules/word/advanced/glossary.js +8 -9
  150. package/dist/esm/modules/word/advanced/math-convert.js +94 -12
  151. package/dist/esm/modules/word/advanced/ole-objects.js +122 -19
  152. package/dist/esm/modules/word/advanced/style-map.js +31 -10
  153. package/dist/esm/modules/word/builder/run-builders.js +7 -1
  154. package/dist/esm/modules/word/constants.js +5 -1
  155. package/dist/esm/modules/word/convert/docx-to-semantic.js +135 -1
  156. package/dist/esm/modules/word/convert/html/html-import.js +167 -14
  157. package/dist/esm/modules/word/convert/html/html.js +1 -1
  158. package/dist/esm/modules/word/convert/markdown/markdown-import.js +279 -69
  159. package/dist/esm/modules/word/convert/odt/odt.js +407 -56
  160. package/dist/esm/modules/word/html.js +1 -1
  161. package/dist/esm/modules/word/index.base.js +1 -1
  162. package/dist/esm/modules/word/layout/layout-full.js +326 -19
  163. package/dist/esm/modules/word/layout/render-page.js +35 -8
  164. package/dist/esm/modules/word/query/compat.js +29 -21
  165. package/dist/esm/modules/word/reader/docx-reader.js +105 -2
  166. package/dist/esm/modules/word/reader/math-parser.js +8 -2
  167. package/dist/esm/modules/word/security/cfb-reader.js +5 -5
  168. package/dist/esm/modules/word/writer/docx-packager.js +108 -2
  169. package/dist/esm/modules/word/writer/glossary-writer.js +121 -0
  170. package/dist/esm/modules/word/writer/header-footer-writer.js +105 -20
  171. package/dist/esm/modules/word/writer/math-writer.js +7 -2
  172. package/dist/esm/utils/font-metrics.js +43 -0
  173. package/dist/esm/utils/theme-colors.js +4 -1
  174. package/dist/iife/excelts.iife.js +496 -59
  175. package/dist/iife/excelts.iife.js.map +1 -1
  176. package/dist/iife/excelts.iife.min.js +39 -39
  177. package/dist/types/modules/excel/cell.d.ts +4 -0
  178. package/dist/types/modules/excel/stream/workbook-writer.browser.d.ts +8 -1
  179. package/dist/types/modules/excel/types.d.ts +81 -0
  180. package/dist/types/modules/excel/utils/drawing-utils.d.ts +8 -0
  181. package/dist/types/modules/excel/workbook.browser.d.ts +16 -0
  182. package/dist/types/modules/excel/worksheet.d.ts +31 -1
  183. package/dist/types/modules/excel/xlsx/xform/comment/vml-shape-xform.d.ts +7 -0
  184. package/dist/types/modules/excel/xlsx/xform/drawing/blip-xform.d.ts +6 -0
  185. package/dist/types/modules/excel/xlsx/xform/drawing/one-cell-anchor-xform.d.ts +1 -0
  186. package/dist/types/modules/excel/xlsx/xform/drawing/pic-xform.d.ts +2 -0
  187. package/dist/types/modules/excel/xlsx/xform/drawing/shape-xform.d.ts +47 -0
  188. package/dist/types/modules/pdf/core/pdf-stream.d.ts +28 -1
  189. package/dist/types/modules/pdf/font/font-manager.d.ts +26 -0
  190. package/dist/types/modules/pdf/render/page-renderer.d.ts +51 -3
  191. package/dist/types/modules/word/advanced/glossary.d.ts +10 -36
  192. package/dist/types/modules/word/advanced/ole-objects.d.ts +28 -0
  193. package/dist/types/modules/word/builder/run-builders.d.ts +7 -1
  194. package/dist/types/modules/word/constants.d.ts +4 -0
  195. package/dist/types/modules/word/convert/docx-to-semantic.d.ts +2 -1
  196. package/dist/types/modules/word/convert/html/html-import.d.ts +32 -1
  197. package/dist/types/modules/word/convert/html/html.d.ts +2 -2
  198. package/dist/types/modules/word/convert/markdown/markdown-import.d.ts +48 -18
  199. package/dist/types/modules/word/convert/markdown/markdown.d.ts +1 -1
  200. package/dist/types/modules/word/html.d.ts +2 -2
  201. package/dist/types/modules/word/index.base.d.ts +3 -3
  202. package/dist/types/modules/word/markdown.d.ts +1 -1
  203. package/dist/types/modules/word/query/compat.d.ts +10 -2
  204. package/dist/types/modules/word/types.d.ts +96 -1
  205. package/dist/types/modules/word/writer/glossary-writer.d.ts +28 -0
  206. package/dist/types/utils/font-metrics.d.ts +8 -0
  207. package/package.json +3 -1
@@ -21,6 +21,7 @@ exports.getLineHeight = getLineHeight;
21
21
  exports.isStandardFont = isStandardFont;
22
22
  exports.getStandardFontNames = getStandardFontNames;
23
23
  exports.mapToStandardFont = mapToStandardFont;
24
+ exports.styledFontVariant = styledFontVariant;
24
25
  // =============================================================================
25
26
  // Helvetica Metrics (afm data: Helvetica)
26
27
  // =============================================================================
@@ -301,3 +302,46 @@ function mapToStandardFont(fontName) {
301
302
  const lower = fontName.toLowerCase().trim();
302
303
  return FONT_FAMILY_MAP[lower] ?? "Helvetica";
303
304
  }
305
+ /**
306
+ * Given a standard PDF base font and bold/italic flags, return the matching
307
+ * metric variant name (e.g. "Helvetica" + bold → "Helvetica-Bold"). This keeps
308
+ * width measurement consistent with the glyphs that are actually drawn, so
309
+ * bold/italic runs are measured with their true (wider) metrics rather than
310
+ * the regular ones. Falls back to the base name when a variant is unknown.
311
+ */
312
+ function styledFontVariant(baseFont, bold, italic) {
313
+ const std = mapToStandardFont(baseFont);
314
+ if (!bold && !italic) {
315
+ return std;
316
+ }
317
+ // Determine the family from the resolved standard name.
318
+ const isTimes = std.startsWith("Times");
319
+ const isCourier = std.startsWith("Courier");
320
+ let candidate;
321
+ if (isTimes) {
322
+ // Times family uses -Roman / -Bold / -Italic / -BoldItalic.
323
+ if (bold && italic) {
324
+ candidate = "Times-BoldItalic";
325
+ }
326
+ else if (bold) {
327
+ candidate = "Times-Bold";
328
+ }
329
+ else {
330
+ candidate = "Times-Italic";
331
+ }
332
+ }
333
+ else {
334
+ // Helvetica / Courier families use -Bold / -Oblique / -BoldOblique.
335
+ const family = isCourier ? "Courier" : "Helvetica";
336
+ if (bold && italic) {
337
+ candidate = `${family}-BoldOblique`;
338
+ }
339
+ else if (bold) {
340
+ candidate = `${family}-Bold`;
341
+ }
342
+ else {
343
+ candidate = `${family}-Oblique`;
344
+ }
345
+ }
346
+ return candidate in FONT_DESCRIPTORS ? candidate : std;
347
+ }
@@ -121,6 +121,9 @@ function resolveOoxmlThemeColor(themeColorName, colors, tint, shade) {
121
121
  // Helpers
122
122
  // =============================================================================
123
123
  function toHex2(n) {
124
- const h = Math.max(0, Math.min(255, n)).toString(16);
124
+ // Emit uppercase hex to match the OOXML ST_HexColorRGB convention and the
125
+ // casing of theme scheme colors (which are stored uppercase), so a tinted /
126
+ // shaded result is consistent with an un-transformed passthrough.
127
+ const h = Math.max(0, Math.min(255, n)).toString(16).toUpperCase();
125
128
  return h.length < 2 ? "0" + h : h;
126
129
  }
@@ -36,7 +36,11 @@ class Note {
36
36
  set model(value) {
37
37
  const { note } = value;
38
38
  const { texts } = note;
39
- if (texts && texts.length === 1 && Object.keys(texts[0]).length === 1) {
39
+ // A single, plain text run with no extra box geometry can be flattened
40
+ // back to a simple string. Custom width/height must keep the full config
41
+ // so the sizing survives the model round-trip.
42
+ const hasCustomSize = note.width !== undefined || note.height !== undefined;
43
+ if (texts && texts.length === 1 && Object.keys(texts[0]).length === 1 && !hasCustomSize) {
40
44
  this.note = texts[0].text;
41
45
  }
42
46
  else {
@@ -213,12 +213,13 @@ class Row {
213
213
  // (e.g. DB entities). Cell.value setter handles unknown values via Value.getType
214
214
  // fallback to JSON type, so this cast is safe at runtime.
215
215
  this._worksheet.eachColumnKey((column, key) => {
216
- if (value[key] !== undefined) {
216
+ const resolved = resolveColumnKeyValue(value, key);
217
+ if (resolved !== undefined) {
217
218
  this.getCellEx({
218
219
  address: colCache.encodeAddress(this._number, column.number),
219
220
  row: this._number,
220
221
  col: column.number
221
- }).value = value[key];
222
+ }).value = resolved;
222
223
  }
223
224
  });
224
225
  }
@@ -459,4 +460,36 @@ class Row {
459
460
  this.style = value.style ? structuredClone(value.style) : {};
460
461
  }
461
462
  }
463
+ /**
464
+ * Resolve a column key against a row object, supporting dotted nested paths.
465
+ *
466
+ * A key without a `.` takes the original fast path (`obj[key]`), preserving
467
+ * exact backward compatibility — including keys that legitimately contain a
468
+ * dot only as a flat property name, which still resolve via the fast path
469
+ * first. A dotted key (e.g. `"address.city"`) is resolved by walking each
470
+ * segment; if any segment is missing or not an object, the result is
471
+ * `undefined` (the same signal the caller already uses to skip a cell).
472
+ *
473
+ * @param obj - The row object supplied to `row.values = {...}` / `addRow({...})`.
474
+ * @param key - The column key, optionally a dotted path.
475
+ * @returns The resolved value, or `undefined` when the path cannot be followed.
476
+ */
477
+ function resolveColumnKeyValue(obj, key) {
478
+ // Fast path: a flat key (no dot) or an exact flat property match. This keeps
479
+ // existing behaviour identical and also lets a literal "a.b" property win
480
+ // over nested traversal when it is actually present on the object.
481
+ const direct = obj[key];
482
+ if (direct !== undefined || !key.includes(".")) {
483
+ return direct;
484
+ }
485
+ // Dotted path: walk segments, bailing out to undefined on any gap.
486
+ let current = obj;
487
+ for (const segment of key.split(".")) {
488
+ if (current === null || typeof current !== "object") {
489
+ return undefined;
490
+ }
491
+ current = current[segment];
492
+ }
493
+ return current;
494
+ }
462
495
  export { Row };
@@ -387,13 +387,33 @@ export class WorkbookWriterBase {
387
387
  * ```
388
388
  */
389
389
  addImage(image) {
390
+ const { svg, ...raster } = image;
391
+ if (svg &&
392
+ raster.link &&
393
+ raster.buffer == null &&
394
+ raster.base64 == null &&
395
+ raster.filename == null) {
396
+ throw new ImageError("An SVG image requires an embedded raster fallback (buffer/base64/filename); it cannot be combined with an external link.");
397
+ }
390
398
  const id = this.media.length;
391
399
  const medium = {
392
- ...image,
400
+ ...raster,
393
401
  type: "image",
394
- name: `image${id}.${image.extension}`
402
+ name: `image${id}.${raster.extension}`
395
403
  };
396
404
  this.media.push(medium);
405
+ if (svg) {
406
+ // Register the SVG companion as a second image medium and link it back to
407
+ // the raster blip so the drawing serializer emits the svgBlip extension.
408
+ const svgId = this.media.length;
409
+ this.media.push({
410
+ ...svg,
411
+ type: "image",
412
+ extension: "svg",
413
+ name: `image${svgId}.svg`
414
+ });
415
+ medium.svgMediaId = svgId;
416
+ }
397
417
  return id;
398
418
  }
399
419
  getImage(id) {
@@ -122,6 +122,23 @@ export function buildDrawingAnchorsAndRels(media, existingRels, options) {
122
122
  },
123
123
  range: medium.range
124
124
  };
125
+ // SVG companion: allocate (and dedupe) a rel for the vector media, then
126
+ // record its rId so the blip serializer emits the asvg:svgBlip extension.
127
+ if (bookImage.svgMediaId !== undefined) {
128
+ const svgKey = `svg:${bookImage.svgMediaId}`;
129
+ let rIdSvg = imageRIdMap[svgKey];
130
+ if (!rIdSvg) {
131
+ const svgImage = options.getBookImage(bookImage.svgMediaId);
132
+ if (svgImage) {
133
+ rIdSvg = options.nextRId(rels);
134
+ imageRIdMap[svgKey] = rIdSvg;
135
+ rels.push(buildImageRel(rIdSvg, svgImage));
136
+ }
137
+ }
138
+ if (rIdSvg) {
139
+ anchor.picture.svgRId = rIdSvg;
140
+ }
141
+ }
125
142
  // Pass through watermark opacity as alphaModFix
126
143
  if (medium.opacity !== undefined) {
127
144
  const clamped = Math.max(0, Math.min(1, medium.opacity));
@@ -172,8 +189,8 @@ export function filterDrawingAnchors(anchors) {
172
189
  if (a.range?.br && a.shape) {
173
190
  return true;
174
191
  }
175
- // One-cell anchors need a valid picture or graphicFrame (charts)
176
- if (!a.range?.br && !a.picture && !a.graphicFrame) {
192
+ // One-cell anchors need a valid picture, graphicFrame (charts) or shape.
193
+ if (!a.range?.br && !a.picture && !a.graphicFrame && !a.shape) {
177
194
  return false;
178
195
  }
179
196
  // Two-cell anchors need either picture, shape, or graphicFrame (charts)
@@ -20,7 +20,7 @@ import { parseNumberFromCsv } from "../csv/utils/number.js";
20
20
  import { getChartSupport } from "./chart-host-registry.js";
21
21
  import { Chartsheet } from "./chartsheet.js";
22
22
  import { DefinedNames } from "./defined-names.js";
23
- import { ExcelDownloadError, ExcelNotSupportedError, WorksheetNameError } from "./errors.js";
23
+ import { ExcelDownloadError, ExcelNotSupportedError, ImageError, WorksheetNameError } from "./errors.js";
24
24
  import { withPivotChartSource } from "./pivot-chart.js";
25
25
  import { WorkbookReader } from "./stream/workbook-reader.browser.js";
26
26
  import { WorkbookWriter } from "./stream/workbook-writer.browser.js";
@@ -1733,10 +1733,40 @@ class Workbook {
1733
1733
  * const id = workbook.addImage({ extension: "png", link: "https://example.com/logo.png" });
1734
1734
  * worksheet.addImage(id, "B2:D6");
1735
1735
  * ```
1736
+ *
1737
+ * @example SVG with raster fallback — crisp in modern Excel, safe everywhere
1738
+ * ```typescript
1739
+ * const id = workbook.addImage({
1740
+ * buffer: pngFallbackBytes, // shown by older Excel / non-SVG consumers
1741
+ * extension: "png",
1742
+ * svg: { buffer: svgBytes } // shown by Excel 2016+
1743
+ * });
1744
+ * worksheet.addImage(id, "B2:D6");
1745
+ * ```
1736
1746
  */
1737
1747
  addImage(image) {
1748
+ const { svg, ...raster } = image;
1749
+ if (svg &&
1750
+ raster.link &&
1751
+ raster.buffer == null &&
1752
+ raster.base64 == null &&
1753
+ raster.filename == null) {
1754
+ // An SVG companion needs an embedded raster fallback; a *linked* (external)
1755
+ // raster has no package part to attach the svgBlip extension to.
1756
+ throw new ImageError("An SVG image requires an embedded raster fallback (buffer/base64/filename); it cannot be combined with an external link.");
1757
+ }
1738
1758
  const id = this.media.length;
1739
- this.media.push({ ...image, type: "image" });
1759
+ const rasterMedia = { ...raster, type: "image" };
1760
+ this.media.push(rasterMedia);
1761
+ if (svg) {
1762
+ // Register the SVG as a second `type: "image"` media so it flows through
1763
+ // the existing media naming, content-types, and zip-writing paths. Link
1764
+ // it back to the raster blip so the drawing serializer can emit the
1765
+ // asvg:svgBlip extension.
1766
+ const svgId = this.media.length;
1767
+ this.media.push({ ...svg, type: "image", extension: "svg" });
1768
+ rasterMedia.svgMediaId = svgId;
1769
+ }
1740
1770
  return id;
1741
1771
  }
1742
1772
  getImage(id) {
@@ -105,6 +105,8 @@ class Worksheet {
105
105
  this.autoFilter = options.autoFilter ?? null;
106
106
  // for images, etc
107
107
  this._media = [];
108
+ // for user-drawn shapes (rectangles, lines, text boxes, …)
109
+ this._shapes = [];
108
110
  // for charts
109
111
  this._charts = [];
110
112
  this._sparklineGroups = [];
@@ -1009,6 +1011,85 @@ class Worksheet {
1009
1011
  };
1010
1012
  this._media.push(new Image(this, model));
1011
1013
  }
1014
+ /**
1015
+ * Add a free-form drawing shape (rectangle, ellipse, line, text box, …) to
1016
+ * the worksheet, anchored to a cell range.
1017
+ *
1018
+ * Unlike images, shapes need no media file — the geometry, fill, outline and
1019
+ * optional text label are written directly into the drawing part.
1020
+ *
1021
+ * @example
1022
+ * ```typescript
1023
+ * worksheet.addShape({
1024
+ * type: "rect",
1025
+ * range: "B2:D5",
1026
+ * fillColor: "FFD966",
1027
+ * lineColor: "000000",
1028
+ * lineWidth: 1,
1029
+ * text: "Important"
1030
+ * });
1031
+ * ```
1032
+ */
1033
+ addShape(options) {
1034
+ const range = options.range;
1035
+ // A shape must cover an area, mirroring images. Reject inputs that resolve
1036
+ // to no size up front, with a clear shape-specific message — otherwise the
1037
+ // failure surfaces much later as a confusing `ImageError` from the internal
1038
+ // range parser when the worksheet is serialized.
1039
+ const hasArea = (typeof range === "string" && range.includes(":")) ||
1040
+ (typeof range === "object" &&
1041
+ range !== null &&
1042
+ ("br" in range || "ext" in range || "pos" in range));
1043
+ if (!hasArea) {
1044
+ throw new ImageError('addShape requires a range covering an area: a cell range like "B2:D5", or an object with `br`, `ext`, or `pos`.');
1045
+ }
1046
+ this._shapes.push({
1047
+ type: "shape",
1048
+ shapeType: options.type ?? "rect",
1049
+ range,
1050
+ fillColor: options.fillColor,
1051
+ lineColor: options.lineColor,
1052
+ lineWidth: options.lineWidth,
1053
+ text: options.text,
1054
+ name: options.name
1055
+ });
1056
+ }
1057
+ /** All shapes added to this worksheet. */
1058
+ getShapes() {
1059
+ return this._shapes.slice();
1060
+ }
1061
+ /**
1062
+ * Resolve a shape's `range` into concrete two-cell anchor coordinates,
1063
+ * reusing the `Image` range parser so cell-address/anchor handling stays in
1064
+ * one place. Returns a serializable ShapeModel for the worksheet xform.
1065
+ */
1066
+ _resolveShapeModel(shape) {
1067
+ let range;
1068
+ try {
1069
+ const probe = new Image(this, { type: "image", imageId: "", range: shape.range });
1070
+ // The probe is always an "image" type, so its model carries `range`.
1071
+ range = probe.model.range;
1072
+ }
1073
+ catch {
1074
+ // Range could not be parsed into an anchor (addShape validates the common
1075
+ // cases up front; this guards exotic inputs). Drop the anchor so the
1076
+ // serializer skips this shape rather than failing the whole worksheet.
1077
+ range = undefined;
1078
+ }
1079
+ if (!range) {
1080
+ return { ...shape, anchorRange: undefined };
1081
+ }
1082
+ return {
1083
+ ...shape,
1084
+ anchorRange: {
1085
+ tl: range.tl,
1086
+ br: range.br,
1087
+ ext: range.ext,
1088
+ pos: range.pos,
1089
+ editAs: range.editAs
1090
+ }
1091
+ };
1092
+ }
1012
1093
  getImages() {
1013
1094
  return this._media.filter(m => m.type === "image");
1014
1095
  }
@@ -1754,6 +1835,7 @@ class Worksheet {
1754
1835
  views: this.views,
1755
1836
  autoFilter: this.autoFilter,
1756
1837
  media: this._media.map(medium => medium.model),
1838
+ shapes: this._shapes.map(shape => this._resolveShapeModel(shape)),
1757
1839
  sheetProtection: this.sheetProtection,
1758
1840
  tables: Object.values(this.tables).map(table => table.model),
1759
1841
  pivotTables: this.pivotTables,
@@ -1819,6 +1901,7 @@ class Worksheet {
1819
1901
  this.views = value.views;
1820
1902
  this.autoFilter = value.autoFilter;
1821
1903
  this._media = value.media.map(medium => new Image(this, medium));
1904
+ this._shapes = value.shapes ? value.shapes.slice() : [];
1822
1905
  // Restore watermark state from media entries
1823
1906
  this._watermark = value.watermark ?? null;
1824
1907
  if (!this._watermark) {
@@ -1,6 +1,9 @@
1
1
  import { BaseXform } from "../base-xform.js";
2
2
  import { VmlClientDataXform } from "./vml-client-data-xform.js";
3
3
  import { VmlTextboxXform } from "./vml-textbox-xform.js";
4
+ /** Default comment box geometry in points (matches legacy Excel notes). */
5
+ const DEFAULT_NOTE_WIDTH_PT = 97.8;
6
+ const DEFAULT_NOTE_HEIGHT_PT = 59.1;
4
7
  class VmlShapeXform extends BaseXform {
5
8
  constructor() {
6
9
  super();
@@ -37,6 +40,21 @@ class VmlShapeXform extends BaseXform {
37
40
  editAs: "",
38
41
  protection: {}
39
42
  };
43
+ {
44
+ // Recover the comment box geometry from the VML style string
45
+ // (e.g. "...width:120pt;height:80pt;..."). Only surface width/height
46
+ // when they differ from the legacy defaults, so untouched notes keep
47
+ // a clean model (and stay byte-compatible with prior behaviour).
48
+ const style = node.attributes.style ?? "";
49
+ const width = VmlShapeXform.parseStyleLength(style, "width");
50
+ const height = VmlShapeXform.parseStyleLength(style, "height");
51
+ if (width !== undefined && width !== DEFAULT_NOTE_WIDTH_PT) {
52
+ this.model.width = width;
53
+ }
54
+ if (height !== undefined && height !== DEFAULT_NOTE_HEIGHT_PT) {
55
+ this.model.height = height;
56
+ }
57
+ }
40
58
  break;
41
59
  default:
42
60
  this.parser = this.map[node.name];
@@ -75,13 +93,29 @@ class VmlShapeXform extends BaseXform {
75
93
  return true;
76
94
  }
77
95
  }
96
+ /**
97
+ * Extract a points-valued length (e.g. `width:120pt`) from a VML style
98
+ * string. Returns `undefined` when the property is absent or not in `pt`.
99
+ */
100
+ static parseStyleLength(style, prop) {
101
+ const match = new RegExp(`(?:^|;)\\s*${prop}\\s*:\\s*([0-9.]+)pt`, "i").exec(style);
102
+ if (!match) {
103
+ return undefined;
104
+ }
105
+ const value = parseFloat(match[1]);
106
+ return Number.isFinite(value) ? value : undefined;
107
+ }
78
108
  }
79
- VmlShapeXform.V_SHAPE_ATTRIBUTES = (model, index) => ({
80
- id: `_x0000_s${1025 + index}`,
81
- type: "#_x0000_t202",
82
- style: "position:absolute; margin-left:105.3pt;margin-top:10.5pt;width:97.8pt;height:59.1pt;z-index:1;visibility:hidden",
83
- fillcolor: "infoBackground [80]",
84
- strokecolor: "none [81]",
85
- "o:insetmode": model.note.margins && model.note.margins.insetmode
86
- });
109
+ VmlShapeXform.V_SHAPE_ATTRIBUTES = (model, index) => {
110
+ const width = model.note?.width ?? DEFAULT_NOTE_WIDTH_PT;
111
+ const height = model.note?.height ?? DEFAULT_NOTE_HEIGHT_PT;
112
+ return {
113
+ id: `_x0000_s${1025 + index}`,
114
+ type: "#_x0000_t202",
115
+ style: `position:absolute; margin-left:105.3pt;margin-top:10.5pt;width:${width}pt;height:${height}pt;z-index:1;visibility:hidden`,
116
+ fillcolor: "infoBackground [80]",
117
+ strokecolor: "none [81]",
118
+ "o:insetmode": model.note.margins && model.note.margins.insetmode
119
+ };
120
+ };
87
121
  export { VmlShapeXform };
@@ -21,7 +21,9 @@ class ContentTypesXform extends BaseXform {
21
21
  mediaHash[imageType] = true;
22
22
  xmlStream.leafNode("Default", {
23
23
  Extension: imageType,
24
- ContentType: `image/${imageType}`
24
+ // SVG's IANA media type is "image/svg+xml"; everything else follows
25
+ // the "image/<ext>" convention.
26
+ ContentType: imageType === "svg" ? "image/svg+xml" : `image/${imageType}`
25
27
  });
26
28
  }
27
29
  }
@@ -3,6 +3,7 @@ import { BaseCellAnchorXform } from "./base-cell-anchor-xform.js";
3
3
  import { ExtXform } from "./ext-xform.js";
4
4
  import { GraphicFrameXform } from "./graphic-frame-xform.js";
5
5
  import { PicXform } from "./pic-xform.js";
6
+ import { ShapeXform } from "./shape-xform.js";
6
7
  import { StaticXform } from "../static-xform.js";
7
8
  /** https://en.wikipedia.org/wiki/Office_Open_XML_file_formats#DrawingML */
8
9
  const EMU_PER_PIXEL_AT_96_DPI = 9525;
@@ -70,6 +71,7 @@ class AbsoluteAnchorXform extends BaseCellAnchorXform {
70
71
  // `<xdr:absoluteAnchor><xdr:pos/><xdr:ext/><xdr:clientData/></xdr:absoluteAnchor>`
71
72
  // with no chart reference, so the anchor was ignored on open.
72
73
  "xdr:graphicFrame": new GraphicFrameXform(),
74
+ "xdr:userShape": new ShapeXform(),
73
75
  "xdr:clientData": new StaticXform({ tag: "xdr:clientData" })
74
76
  };
75
77
  }
@@ -94,6 +96,9 @@ class AbsoluteAnchorXform extends BaseCellAnchorXform {
94
96
  else if (model.graphicFrame) {
95
97
  this.map["xdr:graphicFrame"].render(xmlStream, model.graphicFrame);
96
98
  }
99
+ else if (model.shape?.kind === "userShape") {
100
+ this.map["xdr:userShape"].render(xmlStream, model.shape);
101
+ }
97
102
  this.map["xdr:clientData"].render(xmlStream, {});
98
103
  xmlStream.closeNode();
99
104
  }
@@ -47,8 +47,25 @@ class BaseCellAnchorXform extends BaseXform {
47
47
  const name = match[1];
48
48
  const mediaId = options.mediaIndex[name];
49
49
  const medium = options.media[mediaId];
50
+ if (!medium) {
51
+ return undefined;
52
+ }
53
+ // Resolve an SVG companion (asvg:svgBlip extension) back to its media
54
+ // index and record it on the raster media entry itself, so callers that
55
+ // look the image up by id (e.g. Workbook.getImage) surface the vector
56
+ // companion alongside the raster fallback.
57
+ if (model.svgRId) {
58
+ const svgRel = options.rels[model.svgRId];
59
+ const svgMatch = svgRel && svgRel.Target.match(/.*\/media\/(.+[.][a-zA-Z]{3,4})/);
60
+ if (svgMatch) {
61
+ const svgMediaId = options.mediaIndex[svgMatch[1]];
62
+ if (svgMediaId !== undefined) {
63
+ medium.svgMediaId = svgMediaId;
64
+ }
65
+ }
66
+ }
50
67
  // Preserve alphaModFix (transparency) from the picture model if present
51
- if (medium && model.alphaModFix !== undefined) {
68
+ if (model.alphaModFix !== undefined) {
52
69
  return { ...medium, alphaModFix: model.alphaModFix };
53
70
  }
54
71
  return medium;
@@ -1,4 +1,9 @@
1
1
  import { BaseXform } from "../base-xform.js";
2
+ /** OOXML extension URI for the SVG blip (Office 2016 SVG feature). */
3
+ const SVG_BLIP_EXT_URI = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}";
4
+ /** Namespace for the asvg:svgBlip element. */
5
+ const SVG_BLIP_NS = "http://schemas.microsoft.com/office/drawing/2016/SVG/main";
6
+ const REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
2
7
  class BlipXform extends BaseXform {
3
8
  constructor() {
4
9
  super();
@@ -10,23 +15,37 @@ class BlipXform extends BaseXform {
10
15
  render(xmlStream, model) {
11
16
  // External (linked) images use `r:link`; embedded images use `r:embed`.
12
17
  const relAttr = model.external ? "r:link" : "r:embed";
13
- if (model.alphaModFix !== undefined && model.alphaModFix < 100000) {
14
- // Render as open/close node with a:alphaModFix child
15
- xmlStream.openNode(this.tag, {
16
- "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
18
+ const hasAlpha = model.alphaModFix !== undefined && model.alphaModFix < 100000;
19
+ const hasSvg = model.svgRId !== undefined;
20
+ // A bare blip can be a leaf node; an alpha modulation or an SVG extension
21
+ // both require child elements, so switch to the open/close form.
22
+ if (!hasAlpha && !hasSvg) {
23
+ xmlStream.leafNode(this.tag, {
24
+ "xmlns:r": REL_NS,
17
25
  [relAttr]: model.rId,
18
26
  cstate: "print"
19
27
  });
28
+ return;
29
+ }
30
+ xmlStream.openNode(this.tag, {
31
+ "xmlns:r": REL_NS,
32
+ [relAttr]: model.rId,
33
+ cstate: "print"
34
+ });
35
+ if (hasAlpha) {
20
36
  xmlStream.leafNode("a:alphaModFix", { amt: String(model.alphaModFix) });
21
- xmlStream.closeNode();
22
37
  }
23
- else {
24
- xmlStream.leafNode(this.tag, {
25
- "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
26
- [relAttr]: model.rId,
27
- cstate: "print"
38
+ if (hasSvg) {
39
+ xmlStream.openNode("a:extLst");
40
+ xmlStream.openNode("a:ext", { uri: SVG_BLIP_EXT_URI });
41
+ xmlStream.leafNode("asvg:svgBlip", {
42
+ "xmlns:asvg": SVG_BLIP_NS,
43
+ "r:embed": model.svgRId
28
44
  });
45
+ xmlStream.closeNode(); // a:ext
46
+ xmlStream.closeNode(); // a:extLst
29
47
  }
48
+ xmlStream.closeNode(); // a:blip
30
49
  }
31
50
  parseOpen(node) {
32
51
  switch (node.name) {
@@ -46,6 +65,14 @@ class BlipXform extends BaseXform {
46
65
  this.model.alphaModFix = parseInt(node.attributes.amt, 10);
47
66
  }
48
67
  return true;
68
+ case "asvg:svgBlip": {
69
+ // Capture the SVG companion's relationship id for round-trip.
70
+ const embed = node.attributes["r:embed"];
71
+ if (embed !== undefined && this.model) {
72
+ this.model.svgRId = embed;
73
+ }
74
+ return true;
75
+ }
49
76
  default:
50
77
  return true;
51
78
  }
@@ -56,7 +83,7 @@ class BlipXform extends BaseXform {
56
83
  case this.tag:
57
84
  return false;
58
85
  default:
59
- // unprocessed internal nodes
86
+ // unprocessed internal nodes (a:extLst / a:ext / alphaModFix)
60
87
  return true;
61
88
  }
62
89
  }
@@ -3,6 +3,7 @@ import { CellPositionXform } from "./cell-position-xform.js";
3
3
  import { ExtXform } from "./ext-xform.js";
4
4
  import { GraphicFrameXform } from "./graphic-frame-xform.js";
5
5
  import { PicXform } from "./pic-xform.js";
6
+ import { ShapeXform } from "./shape-xform.js";
6
7
  import { StaticXform } from "../static-xform.js";
7
8
  class OneCellAnchorXform extends BaseCellAnchorXform {
8
9
  constructor() {
@@ -11,6 +12,7 @@ class OneCellAnchorXform extends BaseCellAnchorXform {
11
12
  "xdr:from": new CellPositionXform({ tag: "xdr:from" }),
12
13
  "xdr:ext": new ExtXform({ tag: "xdr:ext" }),
13
14
  "xdr:pic": new PicXform(),
15
+ "xdr:userShape": new ShapeXform(),
14
16
  "xdr:graphicFrame": new GraphicFrameXform(),
15
17
  "xdr:clientData": new StaticXform({ tag: "xdr:clientData" })
16
18
  };
@@ -36,6 +38,9 @@ class OneCellAnchorXform extends BaseCellAnchorXform {
36
38
  else if (model.graphicFrame) {
37
39
  this.map["xdr:graphicFrame"].render(xmlStream, model.graphicFrame);
38
40
  }
41
+ else if (model.shape?.kind === "userShape") {
42
+ this.map["xdr:userShape"].render(xmlStream, model.shape);
43
+ }
39
44
  this.map["xdr:clientData"].render(xmlStream, {});
40
45
  xmlStream.closeNode();
41
46
  }
@@ -25,7 +25,8 @@ class PicXform extends BaseXform {
25
25
  this.map["xdr:blipFill"].render(xmlStream, {
26
26
  rId: model.rId,
27
27
  alphaModFix: model.alphaModFix,
28
- external: model.external
28
+ external: model.external,
29
+ svgRId: model.svgRId
29
30
  });
30
31
  this.map["xdr:spPr"].render(xmlStream, model);
31
32
  xmlStream.closeNode();