@cj-tech-master/excelts 9.5.9 → 9.6.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/stream/workbook-writer.browser.d.ts +17 -0
- package/dist/browser/modules/excel/stream/workbook-writer.browser.js +23 -1
- package/dist/browser/modules/excel/stream/workbook-writer.js +6 -0
- package/dist/browser/modules/excel/stream/worksheet-writer.d.ts +6 -0
- package/dist/browser/modules/excel/stream/worksheet-writer.js +29 -3
- package/dist/browser/modules/excel/types.d.ts +17 -0
- package/dist/browser/modules/excel/utils/drawing-utils.d.ts +46 -4
- package/dist/browser/modules/excel/utils/drawing-utils.js +64 -6
- package/dist/browser/modules/excel/workbook.browser.d.ts +38 -1
- package/dist/browser/modules/excel/workbook.browser.js +36 -1
- package/dist/browser/modules/excel/worksheet.d.ts +13 -1
- package/dist/browser/modules/excel/worksheet.js +35 -4
- package/dist/browser/modules/excel/xlsx/xform/core/content-types-xform.js +6 -0
- package/dist/browser/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.d.ts +6 -0
- package/dist/browser/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +32 -0
- package/dist/browser/modules/excel/xlsx/xform/drawing/blip-xform.d.ts +5 -0
- package/dist/browser/modules/excel/xlsx/xform/drawing/blip-xform.js +14 -6
- package/dist/browser/modules/excel/xlsx/xform/drawing/pic-xform.d.ts +2 -0
- package/dist/browser/modules/excel/xlsx/xform/drawing/pic-xform.js +2 -1
- package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +14 -10
- package/dist/browser/modules/excel/xlsx/xlsx.browser.d.ts +2 -0
- package/dist/browser/modules/excel/xlsx/xlsx.browser.js +7 -1
- package/dist/cjs/modules/excel/stream/workbook-writer.browser.js +22 -0
- package/dist/cjs/modules/excel/stream/workbook-writer.js +6 -0
- package/dist/cjs/modules/excel/stream/worksheet-writer.js +27 -1
- package/dist/cjs/modules/excel/utils/drawing-utils.js +67 -6
- package/dist/cjs/modules/excel/workbook.browser.js +36 -1
- package/dist/cjs/modules/excel/worksheet.js +34 -3
- package/dist/cjs/modules/excel/xlsx/xform/core/content-types-xform.js +6 -0
- package/dist/cjs/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +32 -0
- package/dist/cjs/modules/excel/xlsx/xform/drawing/blip-xform.js +14 -6
- package/dist/cjs/modules/excel/xlsx/xform/drawing/pic-xform.js +2 -1
- package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +13 -9
- package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +6 -0
- package/dist/esm/modules/excel/stream/workbook-writer.browser.js +23 -1
- package/dist/esm/modules/excel/stream/workbook-writer.js +6 -0
- package/dist/esm/modules/excel/stream/worksheet-writer.js +29 -3
- package/dist/esm/modules/excel/utils/drawing-utils.js +64 -6
- package/dist/esm/modules/excel/workbook.browser.js +36 -1
- package/dist/esm/modules/excel/worksheet.js +35 -4
- package/dist/esm/modules/excel/xlsx/xform/core/content-types-xform.js +6 -0
- package/dist/esm/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +32 -0
- package/dist/esm/modules/excel/xlsx/xform/drawing/blip-xform.js +14 -6
- package/dist/esm/modules/excel/xlsx/xform/drawing/pic-xform.js +2 -1
- package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +14 -10
- package/dist/esm/modules/excel/xlsx/xlsx.browser.js +7 -1
- package/dist/iife/excelts.iife.js +195 -26
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +35 -35
- package/dist/types/modules/excel/stream/workbook-writer.browser.d.ts +17 -0
- package/dist/types/modules/excel/stream/worksheet-writer.d.ts +6 -0
- package/dist/types/modules/excel/types.d.ts +17 -0
- package/dist/types/modules/excel/utils/drawing-utils.d.ts +46 -4
- package/dist/types/modules/excel/workbook.browser.d.ts +38 -1
- package/dist/types/modules/excel/worksheet.d.ts +13 -1
- package/dist/types/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.d.ts +6 -0
- package/dist/types/modules/excel/xlsx/xform/drawing/blip-xform.d.ts +5 -0
- package/dist/types/modules/excel/xlsx/xform/drawing/pic-xform.d.ts +2 -0
- package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +2 -0
- package/package.json +7 -7
|
@@ -410,17 +410,23 @@ class WorkSheetXform extends base_xform_1.BaseXform {
|
|
|
410
410
|
headerImageMedia.push(medium);
|
|
411
411
|
}
|
|
412
412
|
});
|
|
413
|
-
// Handle background images
|
|
413
|
+
// Handle background images. Background pictures are always embedded —
|
|
414
|
+
// external (linked) images are rejected in addBackgroundImage because Excel
|
|
415
|
+
// drops a background whose relationship uses TargetMode="External".
|
|
414
416
|
backgroundMedia.forEach(medium => {
|
|
415
|
-
const rId = nextRid(rels);
|
|
416
417
|
const bookImage = options.media[medium.imageId];
|
|
418
|
+
// Guard against an invalid imageId — same as the image/watermark paths.
|
|
419
|
+
if (!bookImage) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const rId = nextRid(rels);
|
|
417
423
|
rels.push({
|
|
418
424
|
Id: rId,
|
|
419
425
|
Type: rel_type_1.RelType.Image,
|
|
420
426
|
Target: (0, drawing_utils_1.resolveMediaTarget)(bookImage)
|
|
421
427
|
});
|
|
422
428
|
model.background = { rId };
|
|
423
|
-
model.image =
|
|
429
|
+
model.image = bookImage;
|
|
424
430
|
});
|
|
425
431
|
// Handle embedded images — create drawing model using shared utility
|
|
426
432
|
if (imageMedia.length > 0) {
|
|
@@ -468,12 +474,9 @@ class WorkSheetXform extends base_xform_1.BaseXform {
|
|
|
468
474
|
if (!bookImage) {
|
|
469
475
|
continue;
|
|
470
476
|
}
|
|
477
|
+
const isExternal = (0, drawing_utils_1.isExternalImage)(bookImage);
|
|
471
478
|
const rIdImage = nextRid(drawing.rels);
|
|
472
|
-
drawing.rels.push(
|
|
473
|
-
Id: rIdImage,
|
|
474
|
-
Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
|
|
475
|
-
Target: (0, drawing_utils_1.resolveMediaTarget)(bookImage)
|
|
476
|
-
});
|
|
479
|
+
drawing.rels.push((0, drawing_utils_1.buildImageRel)(rIdImage, bookImage));
|
|
477
480
|
// Convert opacity (0-1) to OOXML percentage (0-100000), clamped
|
|
478
481
|
const rawOpacity = medium.opacity !== undefined ? medium.opacity : 0.15;
|
|
479
482
|
const clampedOpacity = Math.max(0, Math.min(1, rawOpacity));
|
|
@@ -486,7 +489,8 @@ class WorkSheetXform extends base_xform_1.BaseXform {
|
|
|
486
489
|
drawing.anchors.push({
|
|
487
490
|
picture: {
|
|
488
491
|
rId: rIdImage,
|
|
489
|
-
alphaModFix
|
|
492
|
+
alphaModFix,
|
|
493
|
+
...(isExternal ? { external: true } : {})
|
|
490
494
|
},
|
|
491
495
|
// Cover the full data area with extra margin
|
|
492
496
|
range: {
|
|
@@ -4287,6 +4287,12 @@ class XLSX {
|
|
|
4287
4287
|
if (medium.type !== "image") {
|
|
4288
4288
|
throw new errors_1.ImageError("Unsupported media");
|
|
4289
4289
|
}
|
|
4290
|
+
// External (linked) images carry only a `link` target — no bytes are
|
|
4291
|
+
// written into the package; the relationship (TargetMode="External")
|
|
4292
|
+
// references the image in place.
|
|
4293
|
+
if ((0, drawing_utils_1.isExternalImage)(medium)) {
|
|
4294
|
+
return;
|
|
4295
|
+
}
|
|
4290
4296
|
// Preserve legacy behavior: `${undefined}` becomes "undefined" in template strings
|
|
4291
4297
|
const mediaName = medium.name ?? "undefined";
|
|
4292
4298
|
const filename = (0, ooxml_paths_1.mediaPath)(`${mediaName}.${medium.extension}`);
|
|
@@ -11,7 +11,7 @@ import { Zip, ZipDeflate } from "../../archive/zip/stream.js";
|
|
|
11
11
|
import { DefinedNames } from "../defined-names.js";
|
|
12
12
|
import { ExcelNotSupportedError, ImageError } from "../errors.js";
|
|
13
13
|
import { WorksheetWriter } from "./worksheet-writer.js";
|
|
14
|
-
import { filterDrawingAnchors } from "../utils/drawing-utils.js";
|
|
14
|
+
import { filterDrawingAnchors, isExternalImage } from "../utils/drawing-utils.js";
|
|
15
15
|
import { drawingPath, drawingRelsPath, mediaPath, OOXML_PATHS, OOXML_REL_TARGETS, worksheetRelTarget } from "../utils/ooxml-paths.js";
|
|
16
16
|
import { SharedStrings } from "../utils/shared-strings.js";
|
|
17
17
|
import { StreamBuf } from "../utils/stream-buf.js";
|
|
@@ -369,6 +369,23 @@ export class WorkbookWriterBase {
|
|
|
369
369
|
}
|
|
370
370
|
return this._worksheets.length || 1;
|
|
371
371
|
}
|
|
372
|
+
/**
|
|
373
|
+
* Register an image with the workbook and return its numeric id.
|
|
374
|
+
*
|
|
375
|
+
* Supply `buffer`/`base64`/`filename` to **embed** the bytes, or only `link`
|
|
376
|
+
* (a URL or local file path) to reference it **externally** — in which case
|
|
377
|
+
* no bytes are written into the package and the relationship is emitted with
|
|
378
|
+
* `TargetMode="External"`. If both are provided, embedding wins.
|
|
379
|
+
*
|
|
380
|
+
* Linked images work with cell pictures and overlay watermarks; worksheet
|
|
381
|
+
* background images and header/footer (VML) watermarks cannot be linked.
|
|
382
|
+
*
|
|
383
|
+
* @example
|
|
384
|
+
* ```typescript
|
|
385
|
+
* const id = wb.addImage({ extension: "png", link: "https://example.com/logo.png" });
|
|
386
|
+
* ws.addImage(id, "B2:D6");
|
|
387
|
+
* ```
|
|
388
|
+
*/
|
|
372
389
|
addImage(image) {
|
|
373
390
|
const id = this.media.length;
|
|
374
391
|
const medium = {
|
|
@@ -486,6 +503,11 @@ export class WorkbookWriterBase {
|
|
|
486
503
|
addMedia() {
|
|
487
504
|
return Promise.all(this.media.map(async (medium) => {
|
|
488
505
|
if (medium.type === "image") {
|
|
506
|
+
// External (linked) images carry only a `link` target — no bytes
|
|
507
|
+
// are written into the package.
|
|
508
|
+
if (isExternalImage(medium)) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
489
511
|
const filename = mediaPath(medium.name);
|
|
490
512
|
if (medium.buffer) {
|
|
491
513
|
this._addFile(medium.buffer, filename);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { ImageError } from "../errors.js";
|
|
7
7
|
import { WorkbookWriterBase } from "./workbook-writer.browser.js";
|
|
8
8
|
import { WorksheetWriter } from "./worksheet-writer.js";
|
|
9
|
+
import { isExternalImage } from "../utils/drawing-utils.js";
|
|
9
10
|
import { mediaPath } from "../utils/ooxml-paths.js";
|
|
10
11
|
import { readFileBytes, createWriteStream } from "../../../utils/fs.js";
|
|
11
12
|
class WorkbookWriter extends WorkbookWriterBase {
|
|
@@ -27,6 +28,11 @@ class WorkbookWriter extends WorkbookWriterBase {
|
|
|
27
28
|
addMedia() {
|
|
28
29
|
return Promise.all(this.media.map(async (medium) => {
|
|
29
30
|
if (medium.type === "image") {
|
|
31
|
+
// External (linked) images carry only a `link` target — no bytes
|
|
32
|
+
// are written into the package.
|
|
33
|
+
if (isExternalImage(medium)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
30
36
|
const filename = mediaPath(medium.name);
|
|
31
37
|
// Node.js: support loading from file
|
|
32
38
|
if (medium.filename) {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Anchor } from "../anchor.js";
|
|
2
2
|
import { Column } from "../column.js";
|
|
3
3
|
import { DataValidations } from "../data-validations.js";
|
|
4
|
-
import { ExcelStreamStateError, MergeConflictError, RowOutOfBoundsError } from "../errors.js";
|
|
4
|
+
import { ExcelStreamStateError, ImageError, MergeConflictError, RowOutOfBoundsError } from "../errors.js";
|
|
5
5
|
import { Dimensions } from "../range.js";
|
|
6
6
|
import { Row } from "../row.js";
|
|
7
7
|
import { SheetCommentsWriter } from "./sheet-comments-writer.js";
|
|
8
8
|
import { SheetRelsWriter } from "./sheet-rels-writer.js";
|
|
9
9
|
import { colCache } from "../utils/col-cache.js";
|
|
10
|
-
import { buildDrawingAnchorsAndRels } from "../utils/drawing-utils.js";
|
|
10
|
+
import { buildDrawingAnchorsAndRels, isExternalImage } from "../utils/drawing-utils.js";
|
|
11
11
|
import { applyMergeBorders, collectMergeBorders } from "../utils/merge-borders.js";
|
|
12
12
|
import { drawingRelTargetFromWorksheet, mediaRelTargetFromRels, worksheetPath } from "../utils/ooxml-paths.js";
|
|
13
13
|
import { buildSheetProtection } from "../utils/sheet-protection.js";
|
|
@@ -440,6 +440,12 @@ class WorksheetWriter {
|
|
|
440
440
|
}
|
|
441
441
|
// =========================================================================
|
|
442
442
|
addBackgroundImage(imageId) {
|
|
443
|
+
const bookImage = this._workbook.getImage(Number(imageId));
|
|
444
|
+
if (bookImage && isExternalImage(bookImage)) {
|
|
445
|
+
throw new ImageError("Background images cannot be external (linked) images. " +
|
|
446
|
+
"Use an embedded image (buffer/base64/filename). " +
|
|
447
|
+
"External images are only supported for cell pictures and overlay watermarks.");
|
|
448
|
+
}
|
|
443
449
|
this._background = {
|
|
444
450
|
imageId: Number(imageId)
|
|
445
451
|
};
|
|
@@ -469,14 +475,32 @@ class WorksheetWriter {
|
|
|
469
475
|
/**
|
|
470
476
|
* Add a watermark to the worksheet using an image from `WorkbookWriter.addImage()`.
|
|
471
477
|
* Supports overlay mode (DrawingML with transparency) and header mode (VML behind content).
|
|
478
|
+
*
|
|
479
|
+
* `mode: "overlay"` supports external (linked) images; `mode: "header"` does
|
|
480
|
+
* not — VML header/footer images require embedded media, so a linked image
|
|
481
|
+
* with `mode: "header"` throws an `ImageError`.
|
|
482
|
+
*
|
|
483
|
+
* @throws {ImageError} If `mode: "header"` is used with an external (linked) image.
|
|
472
484
|
*/
|
|
473
485
|
addWatermark(options) {
|
|
486
|
+
const mode = options.mode ?? "overlay";
|
|
487
|
+
// Validate BEFORE mutating any state: VML header/footer images use
|
|
488
|
+
// embedded media; external (linked) images are not representable here.
|
|
489
|
+
// Reject them up front so a failed call leaves existing watermark media
|
|
490
|
+
// untouched (no partial mutation).
|
|
491
|
+
if (mode === "header") {
|
|
492
|
+
const bookImage = this._workbook.getImage(Number(options.imageId));
|
|
493
|
+
if (bookImage && isExternalImage(bookImage)) {
|
|
494
|
+
throw new ImageError("Header watermark images cannot be external (linked) images. " +
|
|
495
|
+
"Use an embedded image (buffer/base64/filename), or use overlay mode for linked images.");
|
|
496
|
+
}
|
|
497
|
+
}
|
|
474
498
|
// Remove existing watermark entries (both stored type tags)
|
|
475
499
|
this._media = this._media.filter(m => m._watermarkTag !== true);
|
|
476
500
|
const opacity = options.opacity !== undefined ? Math.max(0, Math.min(1, options.opacity)) : 0.15;
|
|
477
501
|
this._watermark = {
|
|
478
502
|
imageId: String(options.imageId),
|
|
479
|
-
mode
|
|
503
|
+
mode,
|
|
480
504
|
opacity,
|
|
481
505
|
headerWidth: options.headerWidth,
|
|
482
506
|
headerHeight: options.headerHeight,
|
|
@@ -794,6 +818,8 @@ class WorksheetWriter {
|
|
|
794
818
|
if (!image) {
|
|
795
819
|
return;
|
|
796
820
|
}
|
|
821
|
+
// Background images are always embedded — external (linked) images are
|
|
822
|
+
// rejected up front in addBackgroundImage (Excel drops them).
|
|
797
823
|
const pictureId = this._sheetRelsWriter.addMedia({
|
|
798
824
|
Target: mediaRelTargetFromRels(image.name),
|
|
799
825
|
Type: RelType.Image
|
|
@@ -26,6 +26,65 @@ export function resolveMediaTarget(medium) {
|
|
|
26
26
|
: `${medium.name}.${medium.extension}`;
|
|
27
27
|
return mediaRelTargetFromRels(filename);
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Determine whether a media entry is an **external (linked) image** rather than
|
|
31
|
+
* an embedded one. An external image carries a `link` target and supplies no
|
|
32
|
+
* embedded bytes (`buffer`/`base64`/`filename`). Embedding always takes
|
|
33
|
+
* precedence: if any byte source is present the image is embedded even if a
|
|
34
|
+
* `link` was also provided.
|
|
35
|
+
*/
|
|
36
|
+
export function isExternalImage(medium) {
|
|
37
|
+
return !!medium.link && medium.buffer == null && medium.base64 == null && medium.filename == null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Best-effort image extension inference from an external link's path.
|
|
41
|
+
*
|
|
42
|
+
* Normalises to the extension vocabulary used by `ImageData`
|
|
43
|
+
* (`"jpeg" | "png" | "gif"`); unknown extensions fall back to `"png"`.
|
|
44
|
+
* The extension is advisory only for linked images — the relationship
|
|
45
|
+
* Target carries the real reference — but keeping it within the documented
|
|
46
|
+
* set avoids surprising consumers that branch on `medium.extension`.
|
|
47
|
+
*/
|
|
48
|
+
export function inferExternalImageExtension(link) {
|
|
49
|
+
const match = /\.([a-zA-Z0-9]{2,5})(?:[?#].*)?$/.exec(link);
|
|
50
|
+
const ext = match ? match[1].toLowerCase() : "";
|
|
51
|
+
switch (ext) {
|
|
52
|
+
case "jpg":
|
|
53
|
+
case "jpeg":
|
|
54
|
+
return "jpeg";
|
|
55
|
+
case "gif":
|
|
56
|
+
return "gif";
|
|
57
|
+
case "png":
|
|
58
|
+
default:
|
|
59
|
+
return "png";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// Anchor / Rel Building
|
|
64
|
+
// =============================================================================
|
|
65
|
+
/**
|
|
66
|
+
* Build an image relationship for the given rId, choosing between an embedded
|
|
67
|
+
* package target (`../media/imageN.ext`) and an external link target
|
|
68
|
+
* (`TargetMode="External"`) based on whether the image is external.
|
|
69
|
+
*
|
|
70
|
+
* Shared by the drawing, background, and watermark write paths so the
|
|
71
|
+
* embed-vs-link decision lives in exactly one place.
|
|
72
|
+
*/
|
|
73
|
+
export function buildImageRel(rId, bookImage) {
|
|
74
|
+
if (isExternalImage(bookImage)) {
|
|
75
|
+
return {
|
|
76
|
+
Id: rId,
|
|
77
|
+
Type: RelType.Image,
|
|
78
|
+
Target: bookImage.link,
|
|
79
|
+
TargetMode: "External"
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
Id: rId,
|
|
84
|
+
Type: RelType.Image,
|
|
85
|
+
Target: resolveMediaTarget(bookImage)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
29
88
|
/**
|
|
30
89
|
* Build the drawing anchors and relationships from a list of image media entries.
|
|
31
90
|
*
|
|
@@ -47,20 +106,19 @@ export function buildDrawingAnchorsAndRels(media, existingRels, options) {
|
|
|
47
106
|
if (!bookImage) {
|
|
48
107
|
continue;
|
|
49
108
|
}
|
|
109
|
+
// An external (linked) image has a `link` target and no embedded bytes.
|
|
110
|
+
const isExternal = isExternalImage(bookImage);
|
|
50
111
|
// Deduplicate: reuse rId if same imageId already has a drawing rel
|
|
51
112
|
let rIdImage = imageRIdMap[imageId];
|
|
52
113
|
if (!rIdImage) {
|
|
53
114
|
rIdImage = options.nextRId(rels);
|
|
54
115
|
imageRIdMap[imageId] = rIdImage;
|
|
55
|
-
rels.push(
|
|
56
|
-
Id: rIdImage,
|
|
57
|
-
Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
|
|
58
|
-
Target: resolveMediaTarget(bookImage)
|
|
59
|
-
});
|
|
116
|
+
rels.push(buildImageRel(rIdImage, bookImage));
|
|
60
117
|
}
|
|
61
118
|
const anchor = {
|
|
62
119
|
picture: {
|
|
63
|
-
rId: rIdImage
|
|
120
|
+
rId: rIdImage,
|
|
121
|
+
...(isExternal ? { external: true } : {})
|
|
64
122
|
},
|
|
65
123
|
range: medium.range
|
|
66
124
|
};
|
|
@@ -1697,7 +1697,42 @@ class Workbook {
|
|
|
1697
1697
|
// Images
|
|
1698
1698
|
// ===========================================================================
|
|
1699
1699
|
/**
|
|
1700
|
-
*
|
|
1700
|
+
* Register an image with the workbook and return its numeric id. Pass the id
|
|
1701
|
+
* to {@link Worksheet.addImage}, {@link Worksheet.addBackgroundImage}, or
|
|
1702
|
+
* {@link Worksheet.addWatermark} to place it.
|
|
1703
|
+
*
|
|
1704
|
+
* The image is either **embedded** or **linked (external)**:
|
|
1705
|
+
*
|
|
1706
|
+
* - **Embedded** — supply `buffer`, `base64`, or `filename`. The bytes are
|
|
1707
|
+
* written into the `.xlsx` package (`xl/media/imageN.ext`). Self-contained,
|
|
1708
|
+
* but inflates file size.
|
|
1709
|
+
* - **Linked (external)** — supply only `link` (a URL or local file path).
|
|
1710
|
+
* No bytes are stored; the package keeps a relationship with
|
|
1711
|
+
* `TargetMode="External"` and the picture is rendered via `<a:blip r:link>`.
|
|
1712
|
+
* Keeps the file small, but the image is resolved by Excel at open time.
|
|
1713
|
+
*
|
|
1714
|
+
* If both bytes and a `link` are provided, **embedding wins**.
|
|
1715
|
+
*
|
|
1716
|
+
* Linked images work with **cell pictures** ({@link Worksheet.addImage}) and
|
|
1717
|
+
* **overlay watermarks** ({@link Worksheet.addWatermark} with `mode:
|
|
1718
|
+
* "overlay"`). Worksheet background images and header/footer (VML) watermarks
|
|
1719
|
+
* cannot be linked — they require an embedded image.
|
|
1720
|
+
*
|
|
1721
|
+
* Note: Excel treats linked images as volatile — a moved/missing target
|
|
1722
|
+
* shows a broken-image placeholder, and modern Excel may not auto-load
|
|
1723
|
+
* remote URLs for security reasons. Prefer embedding for self-contained files.
|
|
1724
|
+
*
|
|
1725
|
+
* @example Embedded image
|
|
1726
|
+
* ```typescript
|
|
1727
|
+
* const id = workbook.addImage({ buffer: pngBytes, extension: "png" });
|
|
1728
|
+
* worksheet.addImage(id, "B2:D6");
|
|
1729
|
+
* ```
|
|
1730
|
+
*
|
|
1731
|
+
* @example Linked (external) image — no bytes stored
|
|
1732
|
+
* ```typescript
|
|
1733
|
+
* const id = workbook.addImage({ extension: "png", link: "https://example.com/logo.png" });
|
|
1734
|
+
* worksheet.addImage(id, "B2:D6");
|
|
1735
|
+
* ```
|
|
1701
1736
|
*/
|
|
1702
1737
|
addImage(image) {
|
|
1703
1738
|
const id = this.media.length;
|
|
@@ -7,7 +7,7 @@ import { getChartSupport, tryGetChartSupport } from "./chart-host-registry.js";
|
|
|
7
7
|
import { Column } from "./column.js";
|
|
8
8
|
import { DataValidations } from "./data-validations.js";
|
|
9
9
|
import { Enums } from "./enums.js";
|
|
10
|
-
import { MergeConflictError, TableError } from "./errors.js";
|
|
10
|
+
import { ImageError, MergeConflictError, TableError } from "./errors.js";
|
|
11
11
|
import { FormCheckbox } from "./form-control.js";
|
|
12
12
|
import { Image } from "./image.js";
|
|
13
13
|
import { withPivotChartSource } from "./pivot-chart.js";
|
|
@@ -20,6 +20,7 @@ import { decodeCell, decodeRange, encodeCol } from "./utils/address.js";
|
|
|
20
20
|
import { getCellDisplayText } from "./utils/cell-format.js";
|
|
21
21
|
import { colCache } from "./utils/col-cache.js";
|
|
22
22
|
import { copyStyle } from "./utils/copy-style.js";
|
|
23
|
+
import { isExternalImage } from "./utils/drawing-utils.js";
|
|
23
24
|
import { applyMergeBorders, collectMergeBorders } from "./utils/merge-borders.js";
|
|
24
25
|
import { buildSheetProtection } from "./utils/sheet-protection.js";
|
|
25
26
|
import { calculateAutoFitWidth, getMaxDigitWidth, getColumnContentWidthPx, getCellTextWidthPx, getCellHeightPt } from "./utils/text-metrics.js";
|
|
@@ -1306,9 +1307,20 @@ class Worksheet {
|
|
|
1306
1307
|
return true;
|
|
1307
1308
|
}
|
|
1308
1309
|
/**
|
|
1309
|
-
* Using the image id from `Workbook.addImage`, set the background to the worksheet
|
|
1310
|
+
* Using the image id from `Workbook.addImage`, set the background to the worksheet.
|
|
1311
|
+
*
|
|
1312
|
+
* The image must be **embedded** (`buffer`/`base64`/`filename`). Worksheet
|
|
1313
|
+
* background pictures (`<picture r:id>`) do not support external (linked)
|
|
1314
|
+
* images — Excel silently drops a background whose relationship uses
|
|
1315
|
+
* `TargetMode="External"`, so this rejects linked images up front.
|
|
1310
1316
|
*/
|
|
1311
1317
|
addBackgroundImage(imageId) {
|
|
1318
|
+
const bookImage = this._workbook.getImage(imageId);
|
|
1319
|
+
if (bookImage && isExternalImage(bookImage)) {
|
|
1320
|
+
throw new ImageError("Background images cannot be external (linked) images. " +
|
|
1321
|
+
"Use an embedded image (buffer/base64/filename). " +
|
|
1322
|
+
"External images are only supported for cell pictures and overlay watermarks.");
|
|
1323
|
+
}
|
|
1312
1324
|
const model = {
|
|
1313
1325
|
type: "background",
|
|
1314
1326
|
imageId: String(imageId)
|
|
@@ -1333,7 +1345,14 @@ class Worksheet {
|
|
|
1333
1345
|
* Visible in Page Layout view and when printed. Renders behind cell content.
|
|
1334
1346
|
* Transparency must be baked into the image (PNG with alpha channel).
|
|
1335
1347
|
*
|
|
1348
|
+
* **External (linked) images:** `mode: "overlay"` supports external images
|
|
1349
|
+
* (registered via `workbook.addImage({ link })`). `mode: "header"` does
|
|
1350
|
+
* **not** — VML header/footer images require embedded media, so passing a
|
|
1351
|
+
* linked image with `mode: "header"` throws an `ImageError`. Use an embedded
|
|
1352
|
+
* image (`buffer`/`base64`/`filename`) or switch to `mode: "overlay"`.
|
|
1353
|
+
*
|
|
1336
1354
|
* @param options - Watermark configuration
|
|
1355
|
+
* @throws {ImageError} If `mode: "header"` is used with an external (linked) image.
|
|
1337
1356
|
*
|
|
1338
1357
|
* @example Overlay watermark with transparency:
|
|
1339
1358
|
* ```typescript
|
|
@@ -1348,11 +1367,23 @@ class Worksheet {
|
|
|
1348
1367
|
* ```
|
|
1349
1368
|
*/
|
|
1350
1369
|
addWatermark(options) {
|
|
1370
|
+
const mode = options.mode ?? "overlay";
|
|
1371
|
+
// Validate BEFORE mutating any state: VML header/footer images use
|
|
1372
|
+
// embedded media (`<v:imagedata o:relid>`); external (linked) images are
|
|
1373
|
+
// not representable here. Reject them up front so a failed call leaves the
|
|
1374
|
+
// existing watermark untouched (no partial mutation).
|
|
1375
|
+
if (mode === "header") {
|
|
1376
|
+
const bookImage = this._workbook.getImage(options.imageId);
|
|
1377
|
+
if (bookImage && isExternalImage(bookImage)) {
|
|
1378
|
+
throw new ImageError("Header watermark images cannot be external (linked) images. " +
|
|
1379
|
+
"Use an embedded image (buffer/base64/filename), or use overlay mode for linked images.");
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1351
1382
|
// Remove any existing watermark media entries first
|
|
1352
1383
|
this._media = this._media.filter(m => m.type !== "watermark" && m.type !== "headerImage");
|
|
1353
1384
|
this._watermark = {
|
|
1354
1385
|
imageId: String(options.imageId),
|
|
1355
|
-
mode
|
|
1386
|
+
mode,
|
|
1356
1387
|
opacity: options.opacity,
|
|
1357
1388
|
headerWidth: options.headerWidth,
|
|
1358
1389
|
headerHeight: options.headerHeight,
|
|
@@ -1368,7 +1399,7 @@ class Worksheet {
|
|
|
1368
1399
|
this._media.push(new Image(this, model));
|
|
1369
1400
|
}
|
|
1370
1401
|
else {
|
|
1371
|
-
// Header mode: add as a "headerImage" media entry for the VML pipeline
|
|
1402
|
+
// Header mode: add as a "headerImage" media entry for the VML pipeline.
|
|
1372
1403
|
const model = {
|
|
1373
1404
|
type: "headerImage",
|
|
1374
1405
|
imageId: String(options.imageId),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isExternalImage } from "../../../utils/drawing-utils.js";
|
|
1
2
|
import { OOXML_PATHS, chartsheetPath, commentsPathFromName, ctrlPropPath, drawingPath, externalLinkPath, chartPath, chartUserShapesPath, chartExPath, chartStylePath, chartColorsPath, chartExStylePath, chartExColorsPath, pivotCacheDefinitionPath, pivotCacheRecordsPath, pivotTablePath, tablePath, toContentTypesPartName, worksheetPath } from "../../../utils/ooxml-paths.js";
|
|
2
3
|
import { BaseXform } from "../base-xform.js";
|
|
3
4
|
import { StdDocAttributes } from "../../../../xml/writer.js";
|
|
@@ -10,6 +11,11 @@ class ContentTypesXform extends BaseXform {
|
|
|
10
11
|
const mediaHash = {};
|
|
11
12
|
(model.media ?? []).forEach((medium) => {
|
|
12
13
|
if (medium.type === "image") {
|
|
14
|
+
// External (linked) images add no part to the package, so they need
|
|
15
|
+
// no Default content-type registration.
|
|
16
|
+
if (isExternalImage(medium)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
13
19
|
const imageType = medium.extension;
|
|
14
20
|
if (!mediaHash[imageType]) {
|
|
15
21
|
mediaHash[imageType] = true;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inferExternalImageExtension } from "../../../utils/drawing-utils.js";
|
|
1
2
|
import { BaseXform } from "../base-xform.js";
|
|
2
3
|
class BaseCellAnchorXform extends BaseXform {
|
|
3
4
|
parseOpen(node) {
|
|
@@ -34,6 +35,13 @@ class BaseCellAnchorXform extends BaseXform {
|
|
|
34
35
|
if (!rel) {
|
|
35
36
|
return undefined;
|
|
36
37
|
}
|
|
38
|
+
// External (linked) image: the relationship uses TargetMode="External"
|
|
39
|
+
// and there is no media part in the package. Synthesize a media entry
|
|
40
|
+
// carrying the `link` target so it round-trips and surfaces on the
|
|
41
|
+
// worksheet as an external image. Entries are deduplicated by link.
|
|
42
|
+
if (rel.TargetMode === "External" || model.external) {
|
|
43
|
+
return this.reconcileExternalPicture(rel.Target, options);
|
|
44
|
+
}
|
|
37
45
|
const match = rel.Target.match(/.*\/media\/(.+[.][a-zA-Z]{3,4})/);
|
|
38
46
|
if (match) {
|
|
39
47
|
const name = match[1];
|
|
@@ -48,5 +56,29 @@ class BaseCellAnchorXform extends BaseXform {
|
|
|
48
56
|
}
|
|
49
57
|
return undefined;
|
|
50
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolve (or create) the media entry for an external linked image. The
|
|
61
|
+
* synthesized entry is appended to `options.media` and indexed by its link
|
|
62
|
+
* so repeated references to the same external image share one entry.
|
|
63
|
+
*/
|
|
64
|
+
reconcileExternalPicture(link, options) {
|
|
65
|
+
if (!link) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const indexKey = `external:${link}`;
|
|
69
|
+
let mediaId = options.mediaIndex[indexKey];
|
|
70
|
+
if (mediaId === undefined) {
|
|
71
|
+
mediaId = options.media.length;
|
|
72
|
+
const medium = {
|
|
73
|
+
type: "image",
|
|
74
|
+
extension: inferExternalImageExtension(link),
|
|
75
|
+
link,
|
|
76
|
+
index: mediaId
|
|
77
|
+
};
|
|
78
|
+
options.media.push(medium);
|
|
79
|
+
options.mediaIndex[indexKey] = mediaId;
|
|
80
|
+
}
|
|
81
|
+
return options.media[mediaId];
|
|
82
|
+
}
|
|
51
83
|
}
|
|
52
84
|
export { BaseCellAnchorXform };
|
|
@@ -8,11 +8,13 @@ class BlipXform extends BaseXform {
|
|
|
8
8
|
return "a:blip";
|
|
9
9
|
}
|
|
10
10
|
render(xmlStream, model) {
|
|
11
|
+
// External (linked) images use `r:link`; embedded images use `r:embed`.
|
|
12
|
+
const relAttr = model.external ? "r:link" : "r:embed";
|
|
11
13
|
if (model.alphaModFix !== undefined && model.alphaModFix < 100000) {
|
|
12
14
|
// Render as open/close node with a:alphaModFix child
|
|
13
15
|
xmlStream.openNode(this.tag, {
|
|
14
16
|
"xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
|
|
15
|
-
|
|
17
|
+
[relAttr]: model.rId,
|
|
16
18
|
cstate: "print"
|
|
17
19
|
});
|
|
18
20
|
xmlStream.leafNode("a:alphaModFix", { amt: String(model.alphaModFix) });
|
|
@@ -21,18 +23,24 @@ class BlipXform extends BaseXform {
|
|
|
21
23
|
else {
|
|
22
24
|
xmlStream.leafNode(this.tag, {
|
|
23
25
|
"xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
|
|
24
|
-
|
|
26
|
+
[relAttr]: model.rId,
|
|
25
27
|
cstate: "print"
|
|
26
28
|
});
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
parseOpen(node) {
|
|
30
32
|
switch (node.name) {
|
|
31
|
-
case this.tag:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
case this.tag: {
|
|
34
|
+
// A blip may carry `r:embed` (embedded) or `r:link` (external linked).
|
|
35
|
+
const link = node.attributes["r:link"];
|
|
36
|
+
if (link !== undefined) {
|
|
37
|
+
this.model = { rId: link, external: true };
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
this.model = { rId: node.attributes["r:embed"] };
|
|
41
|
+
}
|
|
35
42
|
return true;
|
|
43
|
+
}
|
|
36
44
|
case "a:alphaModFix":
|
|
37
45
|
if (node.attributes.amt) {
|
|
38
46
|
this.model.alphaModFix = parseInt(node.attributes.amt, 10);
|
|
@@ -24,7 +24,8 @@ class PicXform extends BaseXform {
|
|
|
24
24
|
// Pass alphaModFix through to blipFill → blip
|
|
25
25
|
this.map["xdr:blipFill"].render(xmlStream, {
|
|
26
26
|
rId: model.rId,
|
|
27
|
-
alphaModFix: model.alphaModFix
|
|
27
|
+
alphaModFix: model.alphaModFix,
|
|
28
|
+
external: model.external
|
|
28
29
|
});
|
|
29
30
|
this.map["xdr:spPr"].render(xmlStream, model);
|
|
30
31
|
xmlStream.closeNode();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { colCache } from "../../../utils/col-cache.js";
|
|
2
|
-
import { buildDrawingAnchorsAndRels, resolveMediaTarget } from "../../../utils/drawing-utils.js";
|
|
2
|
+
import { buildDrawingAnchorsAndRels, buildImageRel, isExternalImage, resolveMediaTarget } from "../../../utils/drawing-utils.js";
|
|
3
3
|
import { chartRelTargetFromDrawing, chartExRelTargetFromDrawing, commentsRelTargetFromWorksheet, ctrlPropRelTargetFromWorksheet, drawingRelTargetFromWorksheet, pivotTableRelTargetFromWorksheet, resolveRelTarget, tableRelTargetFromWorksheet, vmlDrawingRelTargetFromWorksheet, vmlDrawingHFRelTargetFromWorksheet } from "../../../utils/ooxml-paths.js";
|
|
4
4
|
import { RelType } from "../../rel-type.js";
|
|
5
5
|
import { BaseXform } from "../base-xform.js";
|
|
@@ -407,17 +407,23 @@ class WorkSheetXform extends BaseXform {
|
|
|
407
407
|
headerImageMedia.push(medium);
|
|
408
408
|
}
|
|
409
409
|
});
|
|
410
|
-
// Handle background images
|
|
410
|
+
// Handle background images. Background pictures are always embedded —
|
|
411
|
+
// external (linked) images are rejected in addBackgroundImage because Excel
|
|
412
|
+
// drops a background whose relationship uses TargetMode="External".
|
|
411
413
|
backgroundMedia.forEach(medium => {
|
|
412
|
-
const rId = nextRid(rels);
|
|
413
414
|
const bookImage = options.media[medium.imageId];
|
|
415
|
+
// Guard against an invalid imageId — same as the image/watermark paths.
|
|
416
|
+
if (!bookImage) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const rId = nextRid(rels);
|
|
414
420
|
rels.push({
|
|
415
421
|
Id: rId,
|
|
416
422
|
Type: RelType.Image,
|
|
417
423
|
Target: resolveMediaTarget(bookImage)
|
|
418
424
|
});
|
|
419
425
|
model.background = { rId };
|
|
420
|
-
model.image =
|
|
426
|
+
model.image = bookImage;
|
|
421
427
|
});
|
|
422
428
|
// Handle embedded images — create drawing model using shared utility
|
|
423
429
|
if (imageMedia.length > 0) {
|
|
@@ -465,12 +471,9 @@ class WorkSheetXform extends BaseXform {
|
|
|
465
471
|
if (!bookImage) {
|
|
466
472
|
continue;
|
|
467
473
|
}
|
|
474
|
+
const isExternal = isExternalImage(bookImage);
|
|
468
475
|
const rIdImage = nextRid(drawing.rels);
|
|
469
|
-
drawing.rels.push(
|
|
470
|
-
Id: rIdImage,
|
|
471
|
-
Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
|
|
472
|
-
Target: resolveMediaTarget(bookImage)
|
|
473
|
-
});
|
|
476
|
+
drawing.rels.push(buildImageRel(rIdImage, bookImage));
|
|
474
477
|
// Convert opacity (0-1) to OOXML percentage (0-100000), clamped
|
|
475
478
|
const rawOpacity = medium.opacity !== undefined ? medium.opacity : 0.15;
|
|
476
479
|
const clampedOpacity = Math.max(0, Math.min(1, rawOpacity));
|
|
@@ -483,7 +486,8 @@ class WorkSheetXform extends BaseXform {
|
|
|
483
486
|
drawing.anchors.push({
|
|
484
487
|
picture: {
|
|
485
488
|
rId: rIdImage,
|
|
486
|
-
alphaModFix
|
|
489
|
+
alphaModFix,
|
|
490
|
+
...(isExternal ? { external: true } : {})
|
|
487
491
|
},
|
|
488
492
|
// Cover the full data area with extra margin
|
|
489
493
|
range: {
|
|
@@ -16,7 +16,7 @@ import { StreamingZip, ZipDeflateFile } from "../../archive/zip/stream.js";
|
|
|
16
16
|
// erased at runtime; runtime entry points route through `getChartSupport()`.
|
|
17
17
|
import { getChartSupport } from "../chart-host-registry.js";
|
|
18
18
|
import { ExcelStreamStateError, ExcelFileError, ImageError, ExcelNotSupportedError, XmlParseError, TableError, ChartOptionsError } from "../errors.js";
|
|
19
|
-
import { filterDrawingAnchors } from "../utils/drawing-utils.js";
|
|
19
|
+
import { filterDrawingAnchors, isExternalImage } from "../utils/drawing-utils.js";
|
|
20
20
|
import { rewriteExternalRefs } from "../utils/external-link-formula.js";
|
|
21
21
|
import { commentsPath, chartsheetPath, chartsheetRelsPath, getChartsheetNoFromPath, getChartsheetNoFromRelsPath, ctrlPropPath, drawingPath, drawingRelsPath, externalLinkPath, externalLinkRelsPath, externalLinkRelTargetFromWorkbook, OOXML_REL_TARGETS, pivotCacheDefinitionRelTargetFromWorkbook, pivotTablePathFromName, isCommentsPath, chartPath, chartRelsPath, chartStylePath, chartColorsPath, chartExStylePath, chartExColorsPath, chartStyleRelTarget, chartExStyleRelTarget, chartExPath, chartExRelsPath, getChartExNumberFromPath, getChartExNumberFromRelsPath, chartColorsRelTarget, chartExColorsRelTarget, chartRelTargetFromDrawing, chartExRelTargetFromDrawing, chartUserShapesPath, chartUserShapesRelTarget, getChartNumberFromPath, getChartNumberFromRelsPath, getChartStyleNumberFromPath, getChartColorsNumberFromPath, getChartExStyleNumberFromPath, getChartExColorsNumberFromPath, getDrawingNameFromPath, getChartUserShapesNameFromPath, getDrawingNameFromRelsPath, getExternalLinkIndexFromPath, getExternalLinkIndexFromRelsPath, getMediaFilenameFromPath, mediaPath, getPivotCacheDefinitionNameFromPath, getPivotCacheDefinitionNameFromRelsPath, getPivotCacheRecordsNameFromPath, getPivotTableNameFromPath, getPivotTableNameFromRelsPath, pivotCacheDefinitionPath, pivotCacheDefinitionRelsPath, pivotCacheDefinitionRelTargetFromPivotTable, pivotCacheRecordsPath, pivotCacheRecordsRelTarget, pivotTablePath, pivotTableRelsPath, getTableNameFromPath, tablePath, themePath, getThemeNameFromPath, getVmlDrawingNameFromPath, getVmlDrawingHFNameFromPath, getWorksheetNoFromWorksheetPath, getWorksheetNoFromWorksheetRelsPath, isBinaryEntryPath, normalizeZipPath, OOXML_PATHS, resolveRelTarget, vmlDrawingPath, vmlDrawingHFPath, vmlDrawingHFRelsPath, worksheetPath, worksheetRelsPath, worksheetRelTarget } from "../utils/ooxml-paths.js";
|
|
22
22
|
import { validateXlsxBuffer } from "../utils/ooxml-validator/index.js";
|
|
@@ -4284,6 +4284,12 @@ class XLSX {
|
|
|
4284
4284
|
if (medium.type !== "image") {
|
|
4285
4285
|
throw new ImageError("Unsupported media");
|
|
4286
4286
|
}
|
|
4287
|
+
// External (linked) images carry only a `link` target — no bytes are
|
|
4288
|
+
// written into the package; the relationship (TargetMode="External")
|
|
4289
|
+
// references the image in place.
|
|
4290
|
+
if (isExternalImage(medium)) {
|
|
4291
|
+
return;
|
|
4292
|
+
}
|
|
4287
4293
|
// Preserve legacy behavior: `${undefined}` becomes "undefined" in template strings
|
|
4288
4294
|
const mediaName = medium.name ?? "undefined";
|
|
4289
4295
|
const filename = mediaPath(`${mediaName}.${medium.extension}`);
|