@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
@@ -39,6 +39,7 @@ const zip_parser_1 = require("../../archive/unzip/zip-parser.js");
39
39
  const _stream_1 = require("../../stream/index.js");
40
40
  const binary_1 = require("../../../utils/binary.js");
41
41
  const ooxml_paths_1 = require("../utils/ooxml-paths.js");
42
+ const drawing_utils_1 = require("../utils/drawing-utils.js");
42
43
  const passthrough_manager_1 = require("../utils/passthrough-manager.js");
43
44
  class StreamingZipWriterAdapter {
44
45
  constructor(options) {
@@ -1150,25 +1151,7 @@ class XLSX {
1150
1151
  }
1151
1152
  else {
1152
1153
  // Use regenerated XML for normal drawings (images, shapes)
1153
- // Filter out invalid anchors (null, undefined, or missing content)
1154
- const filteredAnchors = (drawing.anchors ?? []).filter((a) => {
1155
- if (a == null) {
1156
- return false;
1157
- }
1158
- // Form controls have range.br and shape properties
1159
- if (a.range?.br && a.shape) {
1160
- return true;
1161
- }
1162
- // One-cell anchors need a valid picture
1163
- if (!a.br && !a.picture) {
1164
- return false;
1165
- }
1166
- // Two-cell anchors need either picture or shape
1167
- if (a.br && !a.picture && !a.shape) {
1168
- return false;
1169
- }
1170
- return true;
1171
- });
1154
+ const filteredAnchors = (0, drawing_utils_1.filterDrawingAnchors)(drawing.anchors ?? []);
1172
1155
  const drawingForWrite = drawing.anchors
1173
1156
  ? { ...drawing, anchors: filteredAnchors }
1174
1157
  : drawing;
@@ -52,18 +52,24 @@ class Anchor {
52
52
  return new Anchor(undefined, model);
53
53
  }
54
54
  get col() {
55
- return this.nativeCol + Math.min(this.colWidth - 1, this.nativeColOff) / this.colWidth;
55
+ return this.nativeColOff === 0
56
+ ? this.nativeCol
57
+ : this.nativeCol + Math.min(this.colWidth - 1, this.nativeColOff) / this.colWidth;
56
58
  }
57
59
  set col(v) {
58
60
  this.nativeCol = Math.floor(v);
59
- this.nativeColOff = Math.floor((v - this.nativeCol) * this.colWidth);
61
+ const fraction = v - this.nativeCol;
62
+ this.nativeColOff = fraction === 0 ? 0 : Math.floor(fraction * this.colWidth);
60
63
  }
61
64
  get row() {
62
- return this.nativeRow + Math.min(this.rowHeight - 1, this.nativeRowOff) / this.rowHeight;
65
+ return this.nativeRowOff === 0
66
+ ? this.nativeRow
67
+ : this.nativeRow + Math.min(this.rowHeight - 1, this.nativeRowOff) / this.rowHeight;
63
68
  }
64
69
  set row(v) {
65
70
  this.nativeRow = Math.floor(v);
66
- this.nativeRowOff = Math.floor((v - this.nativeRow) * this.rowHeight);
71
+ const fraction = v - this.nativeRow;
72
+ this.nativeRowOff = fraction === 0 ? 0 : Math.floor(fraction * this.rowHeight);
67
73
  }
68
74
  get colWidth() {
69
75
  return this.worksheet &&
@@ -22,10 +22,12 @@ import { AppXform } from "../xlsx/xform/core/app-xform.js";
22
22
  import { WorkbookXform } from "../xlsx/xform/book/workbook-xform.js";
23
23
  import { SharedStringsXform } from "../xlsx/xform/strings/shared-strings-xform.js";
24
24
  import { FeaturePropertyBagXform } from "../xlsx/xform/core/feature-property-bag-xform.js";
25
+ import { DrawingXform } from "../xlsx/xform/drawing/drawing-xform.js";
25
26
  import { theme1Xml } from "../xlsx/xml/theme1.js";
26
27
  import { toWritable } from "../../stream/index.js";
27
28
  import { stringToUint8Array } from "../../../utils/binary.js";
28
- import { mediaPath, OOXML_PATHS, OOXML_REL_TARGETS, worksheetRelTarget } from "../utils/ooxml-paths.js";
29
+ import { drawingPath, drawingRelsPath, mediaPath, OOXML_PATHS, OOXML_REL_TARGETS, worksheetRelTarget } from "../utils/ooxml-paths.js";
30
+ import { filterDrawingAnchors } from "../utils/drawing-utils.js";
29
31
  import { WorksheetWriter } from "./worksheet-writer.js";
30
32
  const EMPTY_U8 = new Uint8Array(0);
31
33
  const TEXT_DECODER = new TextDecoder();
@@ -135,6 +137,7 @@ export class WorkbookWriterBase {
135
137
  await this.promise;
136
138
  await this._commitWorksheets();
137
139
  await this.addMedia();
140
+ this.addDrawings();
138
141
  await Promise.all([
139
142
  this.addThemes(),
140
143
  this.addOfficeRels(),
@@ -238,11 +241,14 @@ export class WorkbookWriterBase {
238
241
  worksheets.forEach((ws) => {
239
242
  ws.fileIndex = ws.id;
240
243
  });
244
+ // Collect drawing models from worksheets that have images
245
+ const drawings = worksheets.filter(ws => ws.drawing).map(ws => ws.drawing);
241
246
  const model = {
242
247
  worksheets,
243
248
  sharedStrings: this.sharedStrings,
244
249
  commentRefs: this.commentRefs,
245
250
  media: this.media,
251
+ drawings,
246
252
  hasCheckboxes: this.styles.hasCheckboxes
247
253
  };
248
254
  const xform = new ContentTypesXform();
@@ -273,6 +279,31 @@ export class WorkbookWriterBase {
273
279
  throw new ImageError("Unsupported media");
274
280
  }));
275
281
  }
282
+ /**
283
+ * Generate drawing XML and drawing relationship files for worksheets that have images.
284
+ * Must be called after _commitWorksheets() so that each WorksheetWriter has built its
285
+ * drawing model, and after addMedia() so that media files are already in the ZIP.
286
+ */
287
+ addDrawings() {
288
+ const drawingXform = new DrawingXform();
289
+ const relsXform = new RelationshipsXform();
290
+ for (const ws of this._worksheets) {
291
+ if (!ws?.drawing) {
292
+ continue;
293
+ }
294
+ const { drawing } = ws;
295
+ // Filter out invalid anchors using shared utility
296
+ const filteredAnchors = filterDrawingAnchors(drawing.anchors);
297
+ const drawingForWrite = { ...drawing, anchors: filteredAnchors };
298
+ // Prepare and generate drawing XML
299
+ drawingXform.prepare(drawingForWrite);
300
+ const xml = drawingXform.toXml(drawingForWrite);
301
+ this._addFile(xml, drawingPath(drawing.name));
302
+ // Generate drawing relationships
303
+ const relsXml = relsXform.toXml(drawing.rels);
304
+ this._addFile(relsXml, drawingRelsPath(drawing.name));
305
+ }
306
+ }
276
307
  addApp() {
277
308
  return new Promise(resolve => {
278
309
  const xform = new AppXform();
@@ -7,11 +7,13 @@ import { Dimensions } from "../range.js";
7
7
  import { StringBuf } from "../utils/string-buf.js";
8
8
  import { Row } from "../row.js";
9
9
  import { Column } from "../column.js";
10
+ import { Anchor } from "../anchor.js";
10
11
  import { SheetRelsWriter } from "./sheet-rels-writer.js";
11
12
  import { SheetCommentsWriter } from "./sheet-comments-writer.js";
12
13
  import { DataValidations } from "../data-validations.js";
13
14
  import { applyMergeBorders, collectMergeBorders } from "../utils/merge-borders.js";
14
- import { mediaRelTargetFromRels, worksheetPath } from "../utils/ooxml-paths.js";
15
+ import { drawingRelTargetFromWorksheet, mediaRelTargetFromRels, worksheetPath } from "../utils/ooxml-paths.js";
16
+ import { buildDrawingAnchorsAndRels } from "../utils/drawing-utils.js";
15
17
  const xmlBuffer = /* @__PURE__ */ new StringBuf();
16
18
  // ============================================================================================
17
19
  // Xforms
@@ -32,6 +34,7 @@ import { ConditionalFormattingsXform } from "../xlsx/xform/sheet/cf/conditional-
32
34
  import { HeaderFooterXform } from "../xlsx/xform/sheet/header-footer-xform.js";
33
35
  import { RowBreaksXform } from "../xlsx/xform/sheet/row-breaks-xform.js";
34
36
  import { ColBreaksXform } from "../xlsx/xform/sheet/col-breaks-xform.js";
37
+ import { DrawingXform as DrawingPartXform } from "../xlsx/xform/sheet/drawing-xform.js";
35
38
  // since prepare and render are functional, we can use singletons
36
39
  const xform = {
37
40
  dataValidations: new DataValidationsXform(),
@@ -54,6 +57,7 @@ const xform = {
54
57
  pageSeteup: new PageSetupXform(),
55
58
  autoFilter: new AutoFilterXform(),
56
59
  picture: new PictureXform(),
60
+ drawing: new DrawingPartXform(),
57
61
  conditionalFormattings: new ConditionalFormattingsXform(),
58
62
  headerFooter: new HeaderFooterXform(),
59
63
  rowBreaks: new RowBreaksXform(),
@@ -198,10 +202,11 @@ class WorksheetWriter {
198
202
  this._writeDataValidations();
199
203
  this._writePageMargins();
200
204
  this._writePageSetup();
201
- this._writeBackground();
202
205
  this._writeHeaderFooter();
203
206
  this._writeRowBreaks();
204
207
  this._writeColBreaks();
208
+ this._writeDrawing(); // Note: must be after rowBreaks/colBreaks
209
+ this._writeBackground(); // Note: must be after drawing
205
210
  // Legacy Data tag for comments
206
211
  this._writeLegacyData();
207
212
  this._writeCloseWorksheet();
@@ -421,13 +426,65 @@ class WorksheetWriter {
421
426
  // =========================================================================
422
427
  addBackgroundImage(imageId) {
423
428
  this._background = {
424
- imageId
429
+ imageId: Number(imageId)
425
430
  };
426
431
  }
427
432
  getBackgroundImageId() {
428
433
  return this._background && this._background.imageId;
429
434
  }
430
435
  // =========================================================================
436
+ // Images
437
+ /**
438
+ * Using the image id from `WorkbookWriter.addImage`,
439
+ * embed an image within the worksheet to cover a range.
440
+ */
441
+ addImage(imageId, range) {
442
+ const model = this._parseImageRange(String(imageId), range);
443
+ this._media.push(model);
444
+ }
445
+ /**
446
+ * Return the images that have been added to this worksheet.
447
+ * Each entry contains imageId and the normalised range (with native anchors).
448
+ */
449
+ getImages() {
450
+ return this._media;
451
+ }
452
+ /**
453
+ * Parse the user-supplied range into a normalised internal model
454
+ * mirroring what the regular Worksheet / Image class does.
455
+ */
456
+ _parseImageRange(imageId, range) {
457
+ if (typeof range === "string") {
458
+ // e.g. "A1:C3"
459
+ const decoded = colCache.decode(range);
460
+ if ("top" in decoded) {
461
+ return {
462
+ type: "image",
463
+ imageId,
464
+ range: {
465
+ tl: new Anchor(this, { col: decoded.left, row: decoded.top }, -1).model,
466
+ br: new Anchor(this, { col: decoded.right, row: decoded.bottom }, 0).model,
467
+ editAs: "oneCell"
468
+ }
469
+ };
470
+ }
471
+ throw new Error(`Invalid image range: "${range}". Expected a range like "A1:C3".`);
472
+ }
473
+ const tl = new Anchor(this, range.tl, 0).model;
474
+ const br = range.br ? new Anchor(this, range.br, 0).model : undefined;
475
+ return {
476
+ type: "image",
477
+ imageId,
478
+ range: {
479
+ tl,
480
+ br,
481
+ ext: range.ext,
482
+ editAs: range.editAs
483
+ },
484
+ hyperlinks: range.hyperlinks
485
+ };
486
+ }
487
+ // =========================================================================
431
488
  // Worksheet Protection
432
489
  async protect(password, options) {
433
490
  this.sheetProtection = {
@@ -584,6 +641,36 @@ class WorksheetWriter {
584
641
  _writeAutoFilter() {
585
642
  this.stream.write(xform.autoFilter.toXml(this.autoFilter));
586
643
  }
644
+ _writeDrawing() {
645
+ if (this._media.length === 0) {
646
+ return;
647
+ }
648
+ // Build the drawing model from the stored images.
649
+ // The drawing XML will be generated later by WorkbookWriterBase.addDrawings().
650
+ const drawingName = `drawing${this.id}`;
651
+ const drawingRId = this._sheetRelsWriter.addRelationship({
652
+ Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
653
+ Target: drawingRelTargetFromWorksheet(drawingName)
654
+ });
655
+ // Build anchors and drawing-level rels using the shared utility
656
+ const { anchors, rels } = buildDrawingAnchorsAndRels(this._media, [], {
657
+ getBookImage: id => this._workbook.getImage(Number(id)),
658
+ nextRId: currentRels => `rId${currentRels.length + 1}`
659
+ });
660
+ // Store drawing model for the workbook writer to generate the actual drawing XML
661
+ this._drawing = {
662
+ rId: drawingRId,
663
+ name: drawingName,
664
+ anchors,
665
+ rels
666
+ };
667
+ // Write <drawing r:id="rIdN"/> into the worksheet XML
668
+ this.stream.write(xform.drawing.toXml({ rId: drawingRId }));
669
+ }
670
+ /** Returns the drawing model if images were added, for the workbook writer. */
671
+ get drawing() {
672
+ return this._drawing;
673
+ }
587
674
  _writeBackground() {
588
675
  if (this._background) {
589
676
  if (this._background.imageId !== undefined) {
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Shared utilities for building drawing models (anchors + relationships)
3
+ * used by both the streaming WorksheetWriter and the non-streaming WorkSheetXform.
4
+ *
5
+ * This eliminates the duplicated anchor/rel building logic and provides
6
+ * a single, correct image-rel deduplication strategy.
7
+ */
8
+ import { RelType } from "../xlsx/rel-type.js";
9
+ import { mediaRelTargetFromRels } from "./ooxml-paths.js";
10
+ /**
11
+ * Resolves a media filename into the drawing-level relative target path.
12
+ *
13
+ * In the non-streaming path, media entries have separate `name` and `extension`
14
+ * fields (e.g. name="image0", extension="png").
15
+ * In the streaming path, `name` already includes the extension (e.g. "image0.png").
16
+ *
17
+ * This function accepts both forms and returns e.g. `"../media/image0.png"`.
18
+ */
19
+ export function resolveMediaTarget(medium) {
20
+ // When name already contains the extension (streaming path), use it directly.
21
+ // Otherwise concatenate name + extension (non-streaming path).
22
+ // Note: name may be undefined in the non-streaming path; we preserve the legacy
23
+ // behavior of `${undefined}.${ext}` = "undefined.ext" to match addMedia().
24
+ const filename = medium.name && medium.extension && medium.name.endsWith(`.${medium.extension}`)
25
+ ? medium.name
26
+ : `${medium.name}.${medium.extension}`;
27
+ return mediaRelTargetFromRels(filename);
28
+ }
29
+ /**
30
+ * Build the drawing anchors and relationships from a list of image media entries.
31
+ *
32
+ * This is the core logic shared between:
33
+ * - `WorksheetWriter._writeDrawing()` (streaming)
34
+ * - `WorkSheetXform.prepare()` (non-streaming)
35
+ *
36
+ * It correctly deduplicates image rels: if the same `imageId` is used for
37
+ * multiple anchors, only one image relationship is created and shared.
38
+ */
39
+ export function buildDrawingAnchorsAndRels(media, existingRels, options) {
40
+ const anchors = [];
41
+ const rels = [...existingRels];
42
+ // Map imageId → rId for deduplication (handles non-consecutive duplicates correctly)
43
+ const imageRIdMap = {};
44
+ for (const medium of media) {
45
+ const imageId = String(medium.imageId);
46
+ const bookImage = options.getBookImage(medium.imageId);
47
+ if (!bookImage) {
48
+ continue;
49
+ }
50
+ // Deduplicate: reuse rId if same imageId already has a drawing rel
51
+ let rIdImage = imageRIdMap[imageId];
52
+ if (!rIdImage) {
53
+ rIdImage = options.nextRId(rels);
54
+ imageRIdMap[imageId] = rIdImage;
55
+ rels.push({
56
+ Id: rIdImage,
57
+ Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
58
+ Target: resolveMediaTarget(bookImage)
59
+ });
60
+ }
61
+ const anchor = {
62
+ picture: {
63
+ rId: rIdImage
64
+ },
65
+ range: medium.range
66
+ };
67
+ // Handle image hyperlinks
68
+ if (medium.hyperlinks && medium.hyperlinks.hyperlink) {
69
+ const rIdHyperlink = options.nextRId(rels);
70
+ anchor.picture.hyperlinks = {
71
+ tooltip: medium.hyperlinks.tooltip,
72
+ rId: rIdHyperlink
73
+ };
74
+ rels.push({
75
+ Id: rIdHyperlink,
76
+ Type: RelType.Hyperlink,
77
+ Target: medium.hyperlinks.hyperlink,
78
+ TargetMode: "External"
79
+ });
80
+ }
81
+ anchors.push(anchor);
82
+ }
83
+ return { anchors, rels };
84
+ }
85
+ // =============================================================================
86
+ // Anchor Filtering
87
+ // =============================================================================
88
+ /**
89
+ * Filter drawing anchors to remove invalid entries before XML generation.
90
+ *
91
+ * Shared between streaming `WorkbookWriterBase.addDrawings()` and
92
+ * non-streaming `XLSX.addDrawings()`.
93
+ */
94
+ export function filterDrawingAnchors(anchors) {
95
+ return anchors.filter(a => {
96
+ if (a == null) {
97
+ return false;
98
+ }
99
+ // Form controls have range.br and shape properties
100
+ if (a.range?.br && a.shape) {
101
+ return true;
102
+ }
103
+ // One-cell anchors need a valid picture
104
+ if (!a.range?.br && !a.picture) {
105
+ return false;
106
+ }
107
+ // Two-cell anchors need either picture or shape
108
+ if (a.range?.br && !a.picture && !a.shape) {
109
+ return false;
110
+ }
111
+ return true;
112
+ });
113
+ }
@@ -486,7 +486,7 @@ class Worksheet {
486
486
  duplicateRow(rowNum, count, insert = false) {
487
487
  // create count duplicates of rowNum
488
488
  // either inserting new or overwriting existing rows
489
- const rSrc = this._rows[rowNum - 1];
489
+ const rSrc = this.getRow(rowNum);
490
490
  const inserts = Array.from({ length: count }).fill(rSrc.values);
491
491
  // Collect single-row merges from the source row before splicing
492
492
  // (only merges where top == bottom == rowNum, i.e. horizontal merges within one row)
@@ -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.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;