@cj-tech-master/excelts 6.0.0 → 6.1.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 (33) hide show
  1. package/dist/browser/modules/excel/anchor.js +10 -4
  2. package/dist/browser/modules/excel/stream/workbook-writer.browser.d.ts +13 -0
  3. package/dist/browser/modules/excel/stream/workbook-writer.browser.js +32 -1
  4. package/dist/browser/modules/excel/stream/worksheet-writer.d.ts +56 -2
  5. package/dist/browser/modules/excel/stream/worksheet-writer.js +90 -3
  6. package/dist/browser/modules/excel/utils/drawing-utils.d.ts +77 -0
  7. package/dist/browser/modules/excel/utils/drawing-utils.js +113 -0
  8. package/dist/browser/modules/excel/worksheet.js +1 -1
  9. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.d.ts +0 -1
  10. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +43 -64
  11. package/dist/browser/modules/excel/xlsx/xlsx.browser.js +2 -19
  12. package/dist/cjs/modules/excel/anchor.js +10 -4
  13. package/dist/cjs/modules/excel/stream/workbook-writer.browser.js +31 -0
  14. package/dist/cjs/modules/excel/stream/worksheet-writer.js +89 -2
  15. package/dist/cjs/modules/excel/utils/drawing-utils.js +118 -0
  16. package/dist/cjs/modules/excel/worksheet.js +1 -1
  17. package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +42 -63
  18. package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +2 -19
  19. package/dist/esm/modules/excel/anchor.js +10 -4
  20. package/dist/esm/modules/excel/stream/workbook-writer.browser.js +32 -1
  21. package/dist/esm/modules/excel/stream/worksheet-writer.js +90 -3
  22. package/dist/esm/modules/excel/utils/drawing-utils.js +113 -0
  23. package/dist/esm/modules/excel/worksheet.js +1 -1
  24. package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +43 -64
  25. package/dist/esm/modules/excel/xlsx/xlsx.browser.js +2 -19
  26. package/dist/iife/excelts.iife.js +237 -73
  27. package/dist/iife/excelts.iife.js.map +1 -1
  28. package/dist/iife/excelts.iife.min.js +34 -34
  29. package/dist/types/modules/excel/stream/workbook-writer.browser.d.ts +13 -0
  30. package/dist/types/modules/excel/stream/worksheet-writer.d.ts +56 -2
  31. package/dist/types/modules/excel/utils/drawing-utils.d.ts +77 -0
  32. package/dist/types/modules/excel/xlsx/xform/sheet/worksheet-xform.d.ts +0 -1
  33. package/package.json +1 -1
@@ -26,7 +26,8 @@ import { ColBreaksXform } from "./col-breaks-xform.js";
26
26
  import { HeaderFooterXform } from "./header-footer-xform.js";
27
27
  import { ConditionalFormattingsXform } from "./cf/conditional-formattings-xform.js";
28
28
  import { ExtLstXform } from "./ext-lst-xform.js";
29
- import { commentsRelTargetFromWorksheet, ctrlPropRelTargetFromWorksheet, drawingRelTargetFromWorksheet, mediaRelTargetFromRels, pivotTableRelTargetFromWorksheet, tableRelTargetFromWorksheet, vmlDrawingRelTargetFromWorksheet } from "../../../utils/ooxml-paths.js";
29
+ import { commentsRelTargetFromWorksheet, ctrlPropRelTargetFromWorksheet, drawingRelTargetFromWorksheet, pivotTableRelTargetFromWorksheet, tableRelTargetFromWorksheet, vmlDrawingRelTargetFromWorksheet } from "../../../utils/ooxml-paths.js";
30
+ import { buildDrawingAnchorsAndRels, resolveMediaTarget } from "../../../utils/drawing-utils.js";
30
31
  const mergeRule = (rule, extRule) => {
31
32
  Object.keys(extRule).forEach(key => {
32
33
  const value = rule[key];
@@ -223,75 +224,53 @@ class WorkSheetXform extends BaseXform {
223
224
  Target: drawingRelTargetFromWorksheet(drawing.name)
224
225
  });
225
226
  }
226
- const drawingRelsHash = [];
227
- let bookImage;
227
+ // Process background and image media entries
228
+ const backgroundMedia = [];
229
+ const imageMedia = [];
228
230
  model.media.forEach(medium => {
229
231
  if (medium.type === "background") {
230
- const rId = nextRid(rels);
231
- bookImage = options.media[medium.imageId];
232
- rels.push({
233
- Id: rId,
234
- Type: RelType.Image,
235
- Target: mediaRelTargetFromRels(`${bookImage.name}.${bookImage.extension}`)
236
- });
237
- model.background = {
238
- rId
239
- };
240
- model.image = options.media[medium.imageId];
232
+ backgroundMedia.push(medium);
241
233
  }
242
234
  else if (medium.type === "image") {
243
- let { drawing } = model;
244
- bookImage = options.media[medium.imageId];
245
- if (!drawing) {
246
- drawing = model.drawing = {
247
- rId: nextRid(rels),
248
- name: `drawing${++options.drawingsCount}`,
249
- anchors: [],
250
- rels: []
251
- };
252
- options.drawings.push(drawing);
253
- rels.push({
254
- Id: drawing.rId,
255
- Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
256
- Target: drawingRelTargetFromWorksheet(drawing.name)
257
- });
258
- }
259
- let rIdImage = this.preImageId === medium.imageId
260
- ? drawingRelsHash[medium.imageId]
261
- : drawingRelsHash[drawing.rels.length];
262
- if (!rIdImage) {
263
- rIdImage = nextRid(drawing.rels);
264
- drawingRelsHash[drawing.rels.length] = rIdImage;
265
- drawing.rels.push({
266
- Id: rIdImage,
267
- Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
268
- Target: mediaRelTargetFromRels(`${bookImage.name}.${bookImage.extension}`)
269
- });
270
- }
271
- const anchor = {
272
- picture: {
273
- rId: rIdImage
274
- },
275
- range: medium.range
276
- };
277
- if (medium.hyperlinks && medium.hyperlinks.hyperlink) {
278
- const rIdHyperLink = nextRid(drawing.rels);
279
- drawingRelsHash[drawing.rels.length] = rIdHyperLink;
280
- anchor.picture.hyperlinks = {
281
- tooltip: medium.hyperlinks.tooltip,
282
- rId: rIdHyperLink
283
- };
284
- drawing.rels.push({
285
- Id: rIdHyperLink,
286
- Type: RelType.Hyperlink,
287
- Target: medium.hyperlinks.hyperlink,
288
- TargetMode: "External"
289
- });
290
- }
291
- this.preImageId = medium.imageId;
292
- drawing.anchors.push(anchor);
235
+ imageMedia.push(medium);
293
236
  }
294
237
  });
238
+ // Handle background images
239
+ backgroundMedia.forEach(medium => {
240
+ const rId = nextRid(rels);
241
+ const bookImage = options.media[medium.imageId];
242
+ rels.push({
243
+ Id: rId,
244
+ Type: RelType.Image,
245
+ Target: resolveMediaTarget(bookImage)
246
+ });
247
+ model.background = { rId };
248
+ model.image = options.media[medium.imageId];
249
+ });
250
+ // Handle embedded images — create drawing model using shared utility
251
+ if (imageMedia.length > 0) {
252
+ let { drawing } = model;
253
+ if (!drawing) {
254
+ drawing = model.drawing = {
255
+ rId: nextRid(rels),
256
+ name: `drawing${++options.drawingsCount}`,
257
+ anchors: [],
258
+ rels: []
259
+ };
260
+ options.drawings.push(drawing);
261
+ rels.push({
262
+ Id: drawing.rId,
263
+ Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
264
+ Target: drawingRelTargetFromWorksheet(drawing.name)
265
+ });
266
+ }
267
+ const result = buildDrawingAnchorsAndRels(imageMedia, drawing.rels, {
268
+ getBookImage: id => options.media[id],
269
+ nextRId: currentRels => nextRid(currentRels)
270
+ });
271
+ drawing.anchors.push(...result.anchors);
272
+ drawing.rels = result.rels;
273
+ }
295
274
  // prepare tables
296
275
  model.tables.forEach(table => {
297
276
  // relationships
@@ -36,6 +36,7 @@ import { ZipParser } from "../../archive/unzip/zip-parser.js";
36
36
  import { PassThrough } from "../../stream/index.browser.js";
37
37
  import { concatUint8Arrays } from "../../../utils/binary.js";
38
38
  import { commentsPath, commentsRelTargetFromWorksheetName, ctrlPropPath, drawingPath, drawingRelsPath, OOXML_REL_TARGETS, pivotTableRelTargetFromWorksheetName, pivotCacheDefinitionRelTargetFromWorkbook, getCommentsIndexFromPath, getDrawingNameFromPath, getDrawingNameFromRelsPath, getMediaFilenameFromPath, mediaPath, getPivotCacheDefinitionNameFromPath, getPivotCacheDefinitionNameFromRelsPath, getPivotCacheRecordsNameFromPath, getPivotTableNameFromPath, getPivotTableNameFromRelsPath, pivotCacheDefinitionPath, pivotCacheDefinitionRelsPath, pivotCacheDefinitionRelTargetFromPivotTable, pivotCacheRecordsPath, pivotCacheRecordsRelTarget, pivotTablePath, pivotTableRelsPath, getTableNameFromPath, tablePath, tableRelTargetFromWorksheetName, themePath, getThemeNameFromPath, getVmlDrawingNameFromPath, getWorksheetNoFromWorksheetPath, getWorksheetNoFromWorksheetRelsPath, isBinaryEntryPath, normalizeZipPath, OOXML_PATHS, vmlDrawingRelTargetFromWorksheetName, vmlDrawingPath, worksheetPath, worksheetRelsPath, worksheetRelTarget } from "../utils/ooxml-paths.js";
39
+ import { filterDrawingAnchors } from "../utils/drawing-utils.js";
39
40
  import { PassthroughManager } from "../utils/passthrough-manager.js";
40
41
  class StreamingZipWriterAdapter {
41
42
  constructor(options) {
@@ -1147,25 +1148,7 @@ class XLSX {
1147
1148
  }
1148
1149
  else {
1149
1150
  // Use regenerated XML for normal drawings (images, shapes)
1150
- // Filter out invalid anchors (null, undefined, or missing content)
1151
- const filteredAnchors = (drawing.anchors ?? []).filter((a) => {
1152
- if (a == null) {
1153
- return false;
1154
- }
1155
- // Form controls have range.br and shape properties
1156
- if (a.range?.br && a.shape) {
1157
- return true;
1158
- }
1159
- // One-cell anchors need a valid picture
1160
- if (!a.br && !a.picture) {
1161
- return false;
1162
- }
1163
- // Two-cell anchors need either picture or shape
1164
- if (a.br && !a.picture && !a.shape) {
1165
- return false;
1166
- }
1167
- return true;
1168
- });
1151
+ const filteredAnchors = filterDrawingAnchors(drawing.anchors ?? []);
1169
1152
  const drawingForWrite = drawing.anchors
1170
1153
  ? { ...drawing, anchors: filteredAnchors }
1171
1154
  : drawing;
@@ -55,18 +55,24 @@ class Anchor {
55
55
  return new Anchor(undefined, model);
56
56
  }
57
57
  get col() {
58
- return this.nativeCol + Math.min(this.colWidth - 1, this.nativeColOff) / this.colWidth;
58
+ return this.nativeColOff === 0
59
+ ? this.nativeCol
60
+ : this.nativeCol + Math.min(this.colWidth - 1, this.nativeColOff) / this.colWidth;
59
61
  }
60
62
  set col(v) {
61
63
  this.nativeCol = Math.floor(v);
62
- this.nativeColOff = Math.floor((v - this.nativeCol) * this.colWidth);
64
+ const fraction = v - this.nativeCol;
65
+ this.nativeColOff = fraction === 0 ? 0 : Math.floor(fraction * this.colWidth);
63
66
  }
64
67
  get row() {
65
- return this.nativeRow + Math.min(this.rowHeight - 1, this.nativeRowOff) / this.rowHeight;
68
+ return this.nativeRowOff === 0
69
+ ? this.nativeRow
70
+ : this.nativeRow + Math.min(this.rowHeight - 1, this.nativeRowOff) / this.rowHeight;
66
71
  }
67
72
  set row(v) {
68
73
  this.nativeRow = Math.floor(v);
69
- this.nativeRowOff = Math.floor((v - this.nativeRow) * this.rowHeight);
74
+ const fraction = v - this.nativeRow;
75
+ this.nativeRowOff = fraction === 0 ? 0 : Math.floor(fraction * this.rowHeight);
70
76
  }
71
77
  get colWidth() {
72
78
  return this.worksheet &&
@@ -25,10 +25,12 @@ const app_xform_1 = require("../xlsx/xform/core/app-xform.js");
25
25
  const workbook_xform_1 = require("../xlsx/xform/book/workbook-xform.js");
26
26
  const shared_strings_xform_1 = require("../xlsx/xform/strings/shared-strings-xform.js");
27
27
  const feature_property_bag_xform_1 = require("../xlsx/xform/core/feature-property-bag-xform.js");
28
+ const drawing_xform_1 = require("../xlsx/xform/drawing/drawing-xform.js");
28
29
  const theme1_1 = require("../xlsx/xml/theme1.js");
29
30
  const _stream_1 = require("../../stream/index.js");
30
31
  const binary_1 = require("../../../utils/binary.js");
31
32
  const ooxml_paths_1 = require("../utils/ooxml-paths.js");
33
+ const drawing_utils_1 = require("../utils/drawing-utils.js");
32
34
  const worksheet_writer_1 = require("./worksheet-writer.js");
33
35
  const EMPTY_U8 = new Uint8Array(0);
34
36
  const TEXT_DECODER = new TextDecoder();
@@ -138,6 +140,7 @@ class WorkbookWriterBase {
138
140
  await this.promise;
139
141
  await this._commitWorksheets();
140
142
  await this.addMedia();
143
+ this.addDrawings();
141
144
  await Promise.all([
142
145
  this.addThemes(),
143
146
  this.addOfficeRels(),
@@ -241,11 +244,14 @@ class WorkbookWriterBase {
241
244
  worksheets.forEach((ws) => {
242
245
  ws.fileIndex = ws.id;
243
246
  });
247
+ // Collect drawing models from worksheets that have images
248
+ const drawings = worksheets.filter(ws => ws.drawing).map(ws => ws.drawing);
244
249
  const model = {
245
250
  worksheets,
246
251
  sharedStrings: this.sharedStrings,
247
252
  commentRefs: this.commentRefs,
248
253
  media: this.media,
254
+ drawings,
249
255
  hasCheckboxes: this.styles.hasCheckboxes
250
256
  };
251
257
  const xform = new content_types_xform_1.ContentTypesXform();
@@ -276,6 +282,31 @@ class WorkbookWriterBase {
276
282
  throw new errors_1.ImageError("Unsupported media");
277
283
  }));
278
284
  }
285
+ /**
286
+ * Generate drawing XML and drawing relationship files for worksheets that have images.
287
+ * Must be called after _commitWorksheets() so that each WorksheetWriter has built its
288
+ * drawing model, and after addMedia() so that media files are already in the ZIP.
289
+ */
290
+ addDrawings() {
291
+ const drawingXform = new drawing_xform_1.DrawingXform();
292
+ const relsXform = new relationships_xform_1.RelationshipsXform();
293
+ for (const ws of this._worksheets) {
294
+ if (!ws?.drawing) {
295
+ continue;
296
+ }
297
+ const { drawing } = ws;
298
+ // Filter out invalid anchors using shared utility
299
+ const filteredAnchors = (0, drawing_utils_1.filterDrawingAnchors)(drawing.anchors);
300
+ const drawingForWrite = { ...drawing, anchors: filteredAnchors };
301
+ // Prepare and generate drawing XML
302
+ drawingXform.prepare(drawingForWrite);
303
+ const xml = drawingXform.toXml(drawingForWrite);
304
+ this._addFile(xml, (0, ooxml_paths_1.drawingPath)(drawing.name));
305
+ // Generate drawing relationships
306
+ const relsXml = relsXform.toXml(drawing.rels);
307
+ this._addFile(relsXml, (0, ooxml_paths_1.drawingRelsPath)(drawing.name));
308
+ }
309
+ }
279
310
  addApp() {
280
311
  return new Promise(resolve => {
281
312
  const xform = new app_xform_1.AppXform();
@@ -10,11 +10,13 @@ const range_1 = require("../range.js");
10
10
  const string_buf_1 = require("../utils/string-buf.js");
11
11
  const row_1 = require("../row.js");
12
12
  const column_1 = require("../column.js");
13
+ const anchor_1 = require("../anchor.js");
13
14
  const sheet_rels_writer_1 = require("./sheet-rels-writer.js");
14
15
  const sheet_comments_writer_1 = require("./sheet-comments-writer.js");
15
16
  const data_validations_1 = require("../data-validations.js");
16
17
  const merge_borders_1 = require("../utils/merge-borders.js");
17
18
  const ooxml_paths_1 = require("../utils/ooxml-paths.js");
19
+ const drawing_utils_1 = require("../utils/drawing-utils.js");
18
20
  const xmlBuffer = /* @__PURE__ */ new string_buf_1.StringBuf();
19
21
  // ============================================================================================
20
22
  // Xforms
@@ -35,6 +37,7 @@ const conditional_formattings_xform_1 = require("../xlsx/xform/sheet/cf/conditio
35
37
  const header_footer_xform_1 = require("../xlsx/xform/sheet/header-footer-xform.js");
36
38
  const row_breaks_xform_1 = require("../xlsx/xform/sheet/row-breaks-xform.js");
37
39
  const col_breaks_xform_1 = require("../xlsx/xform/sheet/col-breaks-xform.js");
40
+ const drawing_xform_1 = require("../xlsx/xform/sheet/drawing-xform.js");
38
41
  // since prepare and render are functional, we can use singletons
39
42
  const xform = {
40
43
  dataValidations: new data_validations_xform_1.DataValidationsXform(),
@@ -57,6 +60,7 @@ const xform = {
57
60
  pageSeteup: new page_setup_xform_1.PageSetupXform(),
58
61
  autoFilter: new auto_filter_xform_1.AutoFilterXform(),
59
62
  picture: new picture_xform_1.PictureXform(),
63
+ drawing: new drawing_xform_1.DrawingXform(),
60
64
  conditionalFormattings: new conditional_formattings_xform_1.ConditionalFormattingsXform(),
61
65
  headerFooter: new header_footer_xform_1.HeaderFooterXform(),
62
66
  rowBreaks: new row_breaks_xform_1.RowBreaksXform(),
@@ -201,10 +205,11 @@ class WorksheetWriter {
201
205
  this._writeDataValidations();
202
206
  this._writePageMargins();
203
207
  this._writePageSetup();
204
- this._writeBackground();
205
208
  this._writeHeaderFooter();
206
209
  this._writeRowBreaks();
207
210
  this._writeColBreaks();
211
+ this._writeDrawing(); // Note: must be after rowBreaks/colBreaks
212
+ this._writeBackground(); // Note: must be after drawing
208
213
  // Legacy Data tag for comments
209
214
  this._writeLegacyData();
210
215
  this._writeCloseWorksheet();
@@ -424,13 +429,65 @@ class WorksheetWriter {
424
429
  // =========================================================================
425
430
  addBackgroundImage(imageId) {
426
431
  this._background = {
427
- imageId
432
+ imageId: Number(imageId)
428
433
  };
429
434
  }
430
435
  getBackgroundImageId() {
431
436
  return this._background && this._background.imageId;
432
437
  }
433
438
  // =========================================================================
439
+ // Images
440
+ /**
441
+ * Using the image id from `WorkbookWriter.addImage`,
442
+ * embed an image within the worksheet to cover a range.
443
+ */
444
+ addImage(imageId, range) {
445
+ const model = this._parseImageRange(String(imageId), range);
446
+ this._media.push(model);
447
+ }
448
+ /**
449
+ * Return the images that have been added to this worksheet.
450
+ * Each entry contains imageId and the normalised range (with native anchors).
451
+ */
452
+ getImages() {
453
+ return this._media;
454
+ }
455
+ /**
456
+ * Parse the user-supplied range into a normalised internal model
457
+ * mirroring what the regular Worksheet / Image class does.
458
+ */
459
+ _parseImageRange(imageId, range) {
460
+ if (typeof range === "string") {
461
+ // e.g. "A1:C3"
462
+ const decoded = col_cache_1.colCache.decode(range);
463
+ if ("top" in decoded) {
464
+ return {
465
+ type: "image",
466
+ imageId,
467
+ range: {
468
+ tl: new anchor_1.Anchor(this, { col: decoded.left, row: decoded.top }, -1).model,
469
+ br: new anchor_1.Anchor(this, { col: decoded.right, row: decoded.bottom }, 0).model,
470
+ editAs: "oneCell"
471
+ }
472
+ };
473
+ }
474
+ throw new Error(`Invalid image range: "${range}". Expected a range like "A1:C3".`);
475
+ }
476
+ const tl = new anchor_1.Anchor(this, range.tl, 0).model;
477
+ const br = range.br ? new anchor_1.Anchor(this, range.br, 0).model : undefined;
478
+ return {
479
+ type: "image",
480
+ imageId,
481
+ range: {
482
+ tl,
483
+ br,
484
+ ext: range.ext,
485
+ editAs: range.editAs
486
+ },
487
+ hyperlinks: range.hyperlinks
488
+ };
489
+ }
490
+ // =========================================================================
434
491
  // Worksheet Protection
435
492
  async protect(password, options) {
436
493
  this.sheetProtection = {
@@ -587,6 +644,36 @@ class WorksheetWriter {
587
644
  _writeAutoFilter() {
588
645
  this.stream.write(xform.autoFilter.toXml(this.autoFilter));
589
646
  }
647
+ _writeDrawing() {
648
+ if (this._media.length === 0) {
649
+ return;
650
+ }
651
+ // Build the drawing model from the stored images.
652
+ // The drawing XML will be generated later by WorkbookWriterBase.addDrawings().
653
+ const drawingName = `drawing${this.id}`;
654
+ const drawingRId = this._sheetRelsWriter.addRelationship({
655
+ Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
656
+ Target: (0, ooxml_paths_1.drawingRelTargetFromWorksheet)(drawingName)
657
+ });
658
+ // Build anchors and drawing-level rels using the shared utility
659
+ const { anchors, rels } = (0, drawing_utils_1.buildDrawingAnchorsAndRels)(this._media, [], {
660
+ getBookImage: id => this._workbook.getImage(Number(id)),
661
+ nextRId: currentRels => `rId${currentRels.length + 1}`
662
+ });
663
+ // Store drawing model for the workbook writer to generate the actual drawing XML
664
+ this._drawing = {
665
+ rId: drawingRId,
666
+ name: drawingName,
667
+ anchors,
668
+ rels
669
+ };
670
+ // Write <drawing r:id="rIdN"/> into the worksheet XML
671
+ this.stream.write(xform.drawing.toXml({ rId: drawingRId }));
672
+ }
673
+ /** Returns the drawing model if images were added, for the workbook writer. */
674
+ get drawing() {
675
+ return this._drawing;
676
+ }
590
677
  _writeBackground() {
591
678
  if (this._background) {
592
679
  if (this._background.imageId !== undefined) {
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ /**
3
+ * Shared utilities for building drawing models (anchors + relationships)
4
+ * used by both the streaming WorksheetWriter and the non-streaming WorkSheetXform.
5
+ *
6
+ * This eliminates the duplicated anchor/rel building logic and provides
7
+ * a single, correct image-rel deduplication strategy.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.resolveMediaTarget = resolveMediaTarget;
11
+ exports.buildDrawingAnchorsAndRels = buildDrawingAnchorsAndRels;
12
+ exports.filterDrawingAnchors = filterDrawingAnchors;
13
+ const rel_type_1 = require("../xlsx/rel-type.js");
14
+ const ooxml_paths_1 = require("./ooxml-paths.js");
15
+ /**
16
+ * Resolves a media filename into the drawing-level relative target path.
17
+ *
18
+ * In the non-streaming path, media entries have separate `name` and `extension`
19
+ * fields (e.g. name="image0", extension="png").
20
+ * In the streaming path, `name` already includes the extension (e.g. "image0.png").
21
+ *
22
+ * This function accepts both forms and returns e.g. `"../media/image0.png"`.
23
+ */
24
+ function resolveMediaTarget(medium) {
25
+ // When name already contains the extension (streaming path), use it directly.
26
+ // Otherwise concatenate name + extension (non-streaming path).
27
+ // Note: name may be undefined in the non-streaming path; we preserve the legacy
28
+ // behavior of `${undefined}.${ext}` = "undefined.ext" to match addMedia().
29
+ const filename = medium.name && medium.extension && medium.name.endsWith(`.${medium.extension}`)
30
+ ? medium.name
31
+ : `${medium.name}.${medium.extension}`;
32
+ return (0, ooxml_paths_1.mediaRelTargetFromRels)(filename);
33
+ }
34
+ /**
35
+ * Build the drawing anchors and relationships from a list of image media entries.
36
+ *
37
+ * This is the core logic shared between:
38
+ * - `WorksheetWriter._writeDrawing()` (streaming)
39
+ * - `WorkSheetXform.prepare()` (non-streaming)
40
+ *
41
+ * It correctly deduplicates image rels: if the same `imageId` is used for
42
+ * multiple anchors, only one image relationship is created and shared.
43
+ */
44
+ function buildDrawingAnchorsAndRels(media, existingRels, options) {
45
+ const anchors = [];
46
+ const rels = [...existingRels];
47
+ // Map imageId → rId for deduplication (handles non-consecutive duplicates correctly)
48
+ const imageRIdMap = {};
49
+ for (const medium of media) {
50
+ const imageId = String(medium.imageId);
51
+ const bookImage = options.getBookImage(medium.imageId);
52
+ if (!bookImage) {
53
+ continue;
54
+ }
55
+ // Deduplicate: reuse rId if same imageId already has a drawing rel
56
+ let rIdImage = imageRIdMap[imageId];
57
+ if (!rIdImage) {
58
+ rIdImage = options.nextRId(rels);
59
+ imageRIdMap[imageId] = rIdImage;
60
+ rels.push({
61
+ Id: rIdImage,
62
+ Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
63
+ Target: resolveMediaTarget(bookImage)
64
+ });
65
+ }
66
+ const anchor = {
67
+ picture: {
68
+ rId: rIdImage
69
+ },
70
+ range: medium.range
71
+ };
72
+ // Handle image hyperlinks
73
+ if (medium.hyperlinks && medium.hyperlinks.hyperlink) {
74
+ const rIdHyperlink = options.nextRId(rels);
75
+ anchor.picture.hyperlinks = {
76
+ tooltip: medium.hyperlinks.tooltip,
77
+ rId: rIdHyperlink
78
+ };
79
+ rels.push({
80
+ Id: rIdHyperlink,
81
+ Type: rel_type_1.RelType.Hyperlink,
82
+ Target: medium.hyperlinks.hyperlink,
83
+ TargetMode: "External"
84
+ });
85
+ }
86
+ anchors.push(anchor);
87
+ }
88
+ return { anchors, rels };
89
+ }
90
+ // =============================================================================
91
+ // Anchor Filtering
92
+ // =============================================================================
93
+ /**
94
+ * Filter drawing anchors to remove invalid entries before XML generation.
95
+ *
96
+ * Shared between streaming `WorkbookWriterBase.addDrawings()` and
97
+ * non-streaming `XLSX.addDrawings()`.
98
+ */
99
+ function filterDrawingAnchors(anchors) {
100
+ return anchors.filter(a => {
101
+ if (a == null) {
102
+ return false;
103
+ }
104
+ // Form controls have range.br and shape properties
105
+ if (a.range?.br && a.shape) {
106
+ return true;
107
+ }
108
+ // One-cell anchors need a valid picture
109
+ if (!a.range?.br && !a.picture) {
110
+ return false;
111
+ }
112
+ // Two-cell anchors need either picture or shape
113
+ if (a.range?.br && !a.picture && !a.shape) {
114
+ return false;
115
+ }
116
+ return true;
117
+ });
118
+ }
@@ -489,7 +489,7 @@ class Worksheet {
489
489
  duplicateRow(rowNum, count, insert = false) {
490
490
  // create count duplicates of rowNum
491
491
  // either inserting new or overwriting existing rows
492
- const rSrc = this._rows[rowNum - 1];
492
+ const rSrc = this.getRow(rowNum);
493
493
  const inserts = Array.from({ length: count }).fill(rSrc.values);
494
494
  // Collect single-row merges from the source row before splicing
495
495
  // (only merges where top == bottom == rowNum, i.e. horizontal merges within one row)
@@ -30,6 +30,7 @@ const header_footer_xform_1 = require("./header-footer-xform.js");
30
30
  const conditional_formattings_xform_1 = require("./cf/conditional-formattings-xform.js");
31
31
  const ext_lst_xform_1 = require("./ext-lst-xform.js");
32
32
  const ooxml_paths_1 = require("../../../utils/ooxml-paths.js");
33
+ const drawing_utils_1 = require("../../../utils/drawing-utils.js");
33
34
  const mergeRule = (rule, extRule) => {
34
35
  Object.keys(extRule).forEach(key => {
35
36
  const value = rule[key];
@@ -226,75 +227,53 @@ class WorkSheetXform extends base_xform_1.BaseXform {
226
227
  Target: (0, ooxml_paths_1.drawingRelTargetFromWorksheet)(drawing.name)
227
228
  });
228
229
  }
229
- const drawingRelsHash = [];
230
- let bookImage;
230
+ // Process background and image media entries
231
+ const backgroundMedia = [];
232
+ const imageMedia = [];
231
233
  model.media.forEach(medium => {
232
234
  if (medium.type === "background") {
233
- const rId = nextRid(rels);
234
- bookImage = options.media[medium.imageId];
235
- rels.push({
236
- Id: rId,
237
- Type: rel_type_1.RelType.Image,
238
- Target: (0, ooxml_paths_1.mediaRelTargetFromRels)(`${bookImage.name}.${bookImage.extension}`)
239
- });
240
- model.background = {
241
- rId
242
- };
243
- model.image = options.media[medium.imageId];
235
+ backgroundMedia.push(medium);
244
236
  }
245
237
  else if (medium.type === "image") {
246
- let { drawing } = model;
247
- bookImage = options.media[medium.imageId];
248
- if (!drawing) {
249
- drawing = model.drawing = {
250
- rId: nextRid(rels),
251
- name: `drawing${++options.drawingsCount}`,
252
- anchors: [],
253
- rels: []
254
- };
255
- options.drawings.push(drawing);
256
- rels.push({
257
- Id: drawing.rId,
258
- Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
259
- Target: (0, ooxml_paths_1.drawingRelTargetFromWorksheet)(drawing.name)
260
- });
261
- }
262
- let rIdImage = this.preImageId === medium.imageId
263
- ? drawingRelsHash[medium.imageId]
264
- : drawingRelsHash[drawing.rels.length];
265
- if (!rIdImage) {
266
- rIdImage = nextRid(drawing.rels);
267
- drawingRelsHash[drawing.rels.length] = rIdImage;
268
- drawing.rels.push({
269
- Id: rIdImage,
270
- Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
271
- Target: (0, ooxml_paths_1.mediaRelTargetFromRels)(`${bookImage.name}.${bookImage.extension}`)
272
- });
273
- }
274
- const anchor = {
275
- picture: {
276
- rId: rIdImage
277
- },
278
- range: medium.range
279
- };
280
- if (medium.hyperlinks && medium.hyperlinks.hyperlink) {
281
- const rIdHyperLink = nextRid(drawing.rels);
282
- drawingRelsHash[drawing.rels.length] = rIdHyperLink;
283
- anchor.picture.hyperlinks = {
284
- tooltip: medium.hyperlinks.tooltip,
285
- rId: rIdHyperLink
286
- };
287
- drawing.rels.push({
288
- Id: rIdHyperLink,
289
- Type: rel_type_1.RelType.Hyperlink,
290
- Target: medium.hyperlinks.hyperlink,
291
- TargetMode: "External"
292
- });
293
- }
294
- this.preImageId = medium.imageId;
295
- drawing.anchors.push(anchor);
238
+ imageMedia.push(medium);
296
239
  }
297
240
  });
241
+ // Handle background images
242
+ backgroundMedia.forEach(medium => {
243
+ const rId = nextRid(rels);
244
+ const bookImage = options.media[medium.imageId];
245
+ rels.push({
246
+ Id: rId,
247
+ Type: rel_type_1.RelType.Image,
248
+ Target: (0, drawing_utils_1.resolveMediaTarget)(bookImage)
249
+ });
250
+ model.background = { rId };
251
+ model.image = options.media[medium.imageId];
252
+ });
253
+ // Handle embedded images — create drawing model using shared utility
254
+ if (imageMedia.length > 0) {
255
+ let { drawing } = model;
256
+ if (!drawing) {
257
+ drawing = model.drawing = {
258
+ rId: nextRid(rels),
259
+ name: `drawing${++options.drawingsCount}`,
260
+ anchors: [],
261
+ rels: []
262
+ };
263
+ options.drawings.push(drawing);
264
+ rels.push({
265
+ Id: drawing.rId,
266
+ Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
267
+ Target: (0, ooxml_paths_1.drawingRelTargetFromWorksheet)(drawing.name)
268
+ });
269
+ }
270
+ const result = (0, drawing_utils_1.buildDrawingAnchorsAndRels)(imageMedia, drawing.rels, {
271
+ getBookImage: id => options.media[id],
272
+ nextRId: currentRels => nextRid(currentRels)
273
+ });
274
+ drawing.anchors.push(...result.anchors);
275
+ drawing.rels = result.rels;
276
+ }
298
277
  // prepare tables
299
278
  model.tables.forEach(table => {
300
279
  // relationships