@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
|
@@ -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.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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|