@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.
- package/dist/browser/modules/excel/anchor.js +10 -4
- package/dist/browser/modules/excel/stream/workbook-writer.browser.d.ts +13 -0
- package/dist/browser/modules/excel/stream/workbook-writer.browser.js +32 -1
- package/dist/browser/modules/excel/stream/worksheet-writer.d.ts +56 -2
- package/dist/browser/modules/excel/stream/worksheet-writer.js +90 -3
- package/dist/browser/modules/excel/utils/drawing-utils.d.ts +77 -0
- package/dist/browser/modules/excel/utils/drawing-utils.js +113 -0
- package/dist/browser/modules/excel/worksheet.js +1 -1
- package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.d.ts +0 -1
- package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +43 -64
- package/dist/browser/modules/excel/xlsx/xlsx.browser.js +2 -19
- package/dist/cjs/modules/excel/anchor.js +10 -4
- package/dist/cjs/modules/excel/stream/workbook-writer.browser.js +31 -0
- package/dist/cjs/modules/excel/stream/worksheet-writer.js +89 -2
- package/dist/cjs/modules/excel/utils/drawing-utils.js +118 -0
- package/dist/cjs/modules/excel/worksheet.js +1 -1
- package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +42 -63
- package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +2 -19
- package/dist/esm/modules/excel/anchor.js +10 -4
- package/dist/esm/modules/excel/stream/workbook-writer.browser.js +32 -1
- package/dist/esm/modules/excel/stream/worksheet-writer.js +90 -3
- package/dist/esm/modules/excel/utils/drawing-utils.js +113 -0
- package/dist/esm/modules/excel/worksheet.js +1 -1
- package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +43 -64
- package/dist/esm/modules/excel/xlsx/xlsx.browser.js +2 -19
- package/dist/iife/excelts.iife.js +237 -73
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +34 -34
- package/dist/types/modules/excel/stream/workbook-writer.browser.d.ts +13 -0
- package/dist/types/modules/excel/stream/worksheet-writer.d.ts +56 -2
- package/dist/types/modules/excel/utils/drawing-utils.d.ts +77 -0
- package/dist/types/modules/excel/xlsx/xform/sheet/worksheet-xform.d.ts +0 -1
- 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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|