@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.
Files changed (60) hide show
  1. package/dist/browser/modules/excel/stream/workbook-writer.browser.d.ts +17 -0
  2. package/dist/browser/modules/excel/stream/workbook-writer.browser.js +23 -1
  3. package/dist/browser/modules/excel/stream/workbook-writer.js +6 -0
  4. package/dist/browser/modules/excel/stream/worksheet-writer.d.ts +6 -0
  5. package/dist/browser/modules/excel/stream/worksheet-writer.js +29 -3
  6. package/dist/browser/modules/excel/types.d.ts +17 -0
  7. package/dist/browser/modules/excel/utils/drawing-utils.d.ts +46 -4
  8. package/dist/browser/modules/excel/utils/drawing-utils.js +64 -6
  9. package/dist/browser/modules/excel/workbook.browser.d.ts +38 -1
  10. package/dist/browser/modules/excel/workbook.browser.js +36 -1
  11. package/dist/browser/modules/excel/worksheet.d.ts +13 -1
  12. package/dist/browser/modules/excel/worksheet.js +35 -4
  13. package/dist/browser/modules/excel/xlsx/xform/core/content-types-xform.js +6 -0
  14. package/dist/browser/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.d.ts +6 -0
  15. package/dist/browser/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +32 -0
  16. package/dist/browser/modules/excel/xlsx/xform/drawing/blip-xform.d.ts +5 -0
  17. package/dist/browser/modules/excel/xlsx/xform/drawing/blip-xform.js +14 -6
  18. package/dist/browser/modules/excel/xlsx/xform/drawing/pic-xform.d.ts +2 -0
  19. package/dist/browser/modules/excel/xlsx/xform/drawing/pic-xform.js +2 -1
  20. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +14 -10
  21. package/dist/browser/modules/excel/xlsx/xlsx.browser.d.ts +2 -0
  22. package/dist/browser/modules/excel/xlsx/xlsx.browser.js +7 -1
  23. package/dist/cjs/modules/excel/stream/workbook-writer.browser.js +22 -0
  24. package/dist/cjs/modules/excel/stream/workbook-writer.js +6 -0
  25. package/dist/cjs/modules/excel/stream/worksheet-writer.js +27 -1
  26. package/dist/cjs/modules/excel/utils/drawing-utils.js +67 -6
  27. package/dist/cjs/modules/excel/workbook.browser.js +36 -1
  28. package/dist/cjs/modules/excel/worksheet.js +34 -3
  29. package/dist/cjs/modules/excel/xlsx/xform/core/content-types-xform.js +6 -0
  30. package/dist/cjs/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +32 -0
  31. package/dist/cjs/modules/excel/xlsx/xform/drawing/blip-xform.js +14 -6
  32. package/dist/cjs/modules/excel/xlsx/xform/drawing/pic-xform.js +2 -1
  33. package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +13 -9
  34. package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +6 -0
  35. package/dist/esm/modules/excel/stream/workbook-writer.browser.js +23 -1
  36. package/dist/esm/modules/excel/stream/workbook-writer.js +6 -0
  37. package/dist/esm/modules/excel/stream/worksheet-writer.js +29 -3
  38. package/dist/esm/modules/excel/utils/drawing-utils.js +64 -6
  39. package/dist/esm/modules/excel/workbook.browser.js +36 -1
  40. package/dist/esm/modules/excel/worksheet.js +35 -4
  41. package/dist/esm/modules/excel/xlsx/xform/core/content-types-xform.js +6 -0
  42. package/dist/esm/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +32 -0
  43. package/dist/esm/modules/excel/xlsx/xform/drawing/blip-xform.js +14 -6
  44. package/dist/esm/modules/excel/xlsx/xform/drawing/pic-xform.js +2 -1
  45. package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +14 -10
  46. package/dist/esm/modules/excel/xlsx/xlsx.browser.js +7 -1
  47. package/dist/iife/excelts.iife.js +195 -26
  48. package/dist/iife/excelts.iife.js.map +1 -1
  49. package/dist/iife/excelts.iife.min.js +35 -35
  50. package/dist/types/modules/excel/stream/workbook-writer.browser.d.ts +17 -0
  51. package/dist/types/modules/excel/stream/worksheet-writer.d.ts +6 -0
  52. package/dist/types/modules/excel/types.d.ts +17 -0
  53. package/dist/types/modules/excel/utils/drawing-utils.d.ts +46 -4
  54. package/dist/types/modules/excel/workbook.browser.d.ts +38 -1
  55. package/dist/types/modules/excel/worksheet.d.ts +13 -1
  56. package/dist/types/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.d.ts +6 -0
  57. package/dist/types/modules/excel/xlsx/xform/drawing/blip-xform.d.ts +5 -0
  58. package/dist/types/modules/excel/xlsx/xform/drawing/pic-xform.d.ts +2 -0
  59. package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +2 -0
  60. package/package.json +7 -7
@@ -3,6 +3,11 @@ interface BlipModel {
3
3
  rId: string;
4
4
  /** Alpha modulation (opacity) as OOXML percentage (e.g. 15000 = 15%). */
5
5
  alphaModFix?: number;
6
+ /**
7
+ * When true, the blip references an external linked image via `r:link`
8
+ * instead of an embedded one via `r:embed`.
9
+ */
10
+ external?: boolean;
6
11
  }
7
12
  declare class BlipXform extends BaseXform<BlipModel> {
8
13
  constructor();
@@ -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
- "r:embed": model.rId,
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
- "r:embed": model.rId,
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
- this.model = {
33
- rId: node.attributes["r:embed"]
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);
@@ -4,6 +4,8 @@ interface PicModel {
4
4
  rId?: string;
5
5
  /** Alpha modulation for transparency (OOXML percentage, e.g. 15000 = 15%). */
6
6
  alphaModFix?: number;
7
+ /** When true, render the picture as an external linked image (`r:link`). */
8
+ external?: boolean;
7
9
  [key: string]: any;
8
10
  }
9
11
  declare class PicXform extends BaseXform {
@@ -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 = options.media[medium.imageId];
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: {
@@ -187,6 +187,8 @@ export interface WorkbookMediaLike {
187
187
  filename?: string;
188
188
  buffer?: Uint8Array;
189
189
  base64?: string;
190
+ /** External link target — when set, the image is referenced, not embedded. */
191
+ link?: string;
190
192
  }
191
193
  export interface MediaModel {
192
194
  media: WorkbookMediaLike[];
@@ -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}`);
@@ -372,6 +372,23 @@ class WorkbookWriterBase {
372
372
  }
373
373
  return this._worksheets.length || 1;
374
374
  }
375
+ /**
376
+ * Register an image with the workbook and return its numeric id.
377
+ *
378
+ * Supply `buffer`/`base64`/`filename` to **embed** the bytes, or only `link`
379
+ * (a URL or local file path) to reference it **externally** — in which case
380
+ * no bytes are written into the package and the relationship is emitted with
381
+ * `TargetMode="External"`. If both are provided, embedding wins.
382
+ *
383
+ * Linked images work with cell pictures and overlay watermarks; worksheet
384
+ * background images and header/footer (VML) watermarks cannot be linked.
385
+ *
386
+ * @example
387
+ * ```typescript
388
+ * const id = wb.addImage({ extension: "png", link: "https://example.com/logo.png" });
389
+ * ws.addImage(id, "B2:D6");
390
+ * ```
391
+ */
375
392
  addImage(image) {
376
393
  const id = this.media.length;
377
394
  const medium = {
@@ -489,6 +506,11 @@ class WorkbookWriterBase {
489
506
  addMedia() {
490
507
  return Promise.all(this.media.map(async (medium) => {
491
508
  if (medium.type === "image") {
509
+ // External (linked) images carry only a `link` target — no bytes
510
+ // are written into the package.
511
+ if ((0, drawing_utils_1.isExternalImage)(medium)) {
512
+ return;
513
+ }
492
514
  const filename = (0, ooxml_paths_1.mediaPath)(medium.name);
493
515
  if (medium.buffer) {
494
516
  this._addFile(medium.buffer, filename);
@@ -9,6 +9,7 @@ exports.WorkbookWriter = void 0;
9
9
  const errors_1 = require("../errors.js");
10
10
  const workbook_writer_browser_1 = require("./workbook-writer.browser.js");
11
11
  const worksheet_writer_1 = require("./worksheet-writer.js");
12
+ const drawing_utils_1 = require("../utils/drawing-utils.js");
12
13
  const ooxml_paths_1 = require("../utils/ooxml-paths.js");
13
14
  const fs_1 = require("../../../utils/fs.js");
14
15
  class WorkbookWriter extends workbook_writer_browser_1.WorkbookWriterBase {
@@ -30,6 +31,11 @@ class WorkbookWriter extends workbook_writer_browser_1.WorkbookWriterBase {
30
31
  addMedia() {
31
32
  return Promise.all(this.media.map(async (medium) => {
32
33
  if (medium.type === "image") {
34
+ // External (linked) images carry only a `link` target — no bytes
35
+ // are written into the package.
36
+ if ((0, drawing_utils_1.isExternalImage)(medium)) {
37
+ return;
38
+ }
33
39
  const filename = (0, ooxml_paths_1.mediaPath)(medium.name);
34
40
  // Node.js: support loading from file
35
41
  if (medium.filename) {
@@ -443,6 +443,12 @@ class WorksheetWriter {
443
443
  }
444
444
  // =========================================================================
445
445
  addBackgroundImage(imageId) {
446
+ const bookImage = this._workbook.getImage(Number(imageId));
447
+ if (bookImage && (0, drawing_utils_1.isExternalImage)(bookImage)) {
448
+ throw new errors_1.ImageError("Background images cannot be external (linked) images. " +
449
+ "Use an embedded image (buffer/base64/filename). " +
450
+ "External images are only supported for cell pictures and overlay watermarks.");
451
+ }
446
452
  this._background = {
447
453
  imageId: Number(imageId)
448
454
  };
@@ -472,14 +478,32 @@ class WorksheetWriter {
472
478
  /**
473
479
  * Add a watermark to the worksheet using an image from `WorkbookWriter.addImage()`.
474
480
  * Supports overlay mode (DrawingML with transparency) and header mode (VML behind content).
481
+ *
482
+ * `mode: "overlay"` supports external (linked) images; `mode: "header"` does
483
+ * not — VML header/footer images require embedded media, so a linked image
484
+ * with `mode: "header"` throws an `ImageError`.
485
+ *
486
+ * @throws {ImageError} If `mode: "header"` is used with an external (linked) image.
475
487
  */
476
488
  addWatermark(options) {
489
+ const mode = options.mode ?? "overlay";
490
+ // Validate BEFORE mutating any state: VML header/footer images use
491
+ // embedded media; external (linked) images are not representable here.
492
+ // Reject them up front so a failed call leaves existing watermark media
493
+ // untouched (no partial mutation).
494
+ if (mode === "header") {
495
+ const bookImage = this._workbook.getImage(Number(options.imageId));
496
+ if (bookImage && (0, drawing_utils_1.isExternalImage)(bookImage)) {
497
+ throw new errors_1.ImageError("Header watermark images cannot be external (linked) images. " +
498
+ "Use an embedded image (buffer/base64/filename), or use overlay mode for linked images.");
499
+ }
500
+ }
477
501
  // Remove existing watermark entries (both stored type tags)
478
502
  this._media = this._media.filter(m => m._watermarkTag !== true);
479
503
  const opacity = options.opacity !== undefined ? Math.max(0, Math.min(1, options.opacity)) : 0.15;
480
504
  this._watermark = {
481
505
  imageId: String(options.imageId),
482
- mode: options.mode ?? "overlay",
506
+ mode,
483
507
  opacity,
484
508
  headerWidth: options.headerWidth,
485
509
  headerHeight: options.headerHeight,
@@ -797,6 +821,8 @@ class WorksheetWriter {
797
821
  if (!image) {
798
822
  return;
799
823
  }
824
+ // Background images are always embedded — external (linked) images are
825
+ // rejected up front in addBackgroundImage (Excel drops them).
800
826
  const pictureId = this._sheetRelsWriter.addMedia({
801
827
  Target: (0, ooxml_paths_1.mediaRelTargetFromRels)(image.name),
802
828
  Type: rel_type_1.RelType.Image
@@ -8,6 +8,9 @@
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.resolveMediaTarget = resolveMediaTarget;
11
+ exports.isExternalImage = isExternalImage;
12
+ exports.inferExternalImageExtension = inferExternalImageExtension;
13
+ exports.buildImageRel = buildImageRel;
11
14
  exports.buildDrawingAnchorsAndRels = buildDrawingAnchorsAndRels;
12
15
  exports.filterDrawingAnchors = filterDrawingAnchors;
13
16
  const ooxml_paths_1 = require("./ooxml-paths.js");
@@ -31,6 +34,65 @@ function resolveMediaTarget(medium) {
31
34
  : `${medium.name}.${medium.extension}`;
32
35
  return (0, ooxml_paths_1.mediaRelTargetFromRels)(filename);
33
36
  }
37
+ /**
38
+ * Determine whether a media entry is an **external (linked) image** rather than
39
+ * an embedded one. An external image carries a `link` target and supplies no
40
+ * embedded bytes (`buffer`/`base64`/`filename`). Embedding always takes
41
+ * precedence: if any byte source is present the image is embedded even if a
42
+ * `link` was also provided.
43
+ */
44
+ function isExternalImage(medium) {
45
+ return !!medium.link && medium.buffer == null && medium.base64 == null && medium.filename == null;
46
+ }
47
+ /**
48
+ * Best-effort image extension inference from an external link's path.
49
+ *
50
+ * Normalises to the extension vocabulary used by `ImageData`
51
+ * (`"jpeg" | "png" | "gif"`); unknown extensions fall back to `"png"`.
52
+ * The extension is advisory only for linked images — the relationship
53
+ * Target carries the real reference — but keeping it within the documented
54
+ * set avoids surprising consumers that branch on `medium.extension`.
55
+ */
56
+ function inferExternalImageExtension(link) {
57
+ const match = /\.([a-zA-Z0-9]{2,5})(?:[?#].*)?$/.exec(link);
58
+ const ext = match ? match[1].toLowerCase() : "";
59
+ switch (ext) {
60
+ case "jpg":
61
+ case "jpeg":
62
+ return "jpeg";
63
+ case "gif":
64
+ return "gif";
65
+ case "png":
66
+ default:
67
+ return "png";
68
+ }
69
+ }
70
+ // =============================================================================
71
+ // Anchor / Rel Building
72
+ // =============================================================================
73
+ /**
74
+ * Build an image relationship for the given rId, choosing between an embedded
75
+ * package target (`../media/imageN.ext`) and an external link target
76
+ * (`TargetMode="External"`) based on whether the image is external.
77
+ *
78
+ * Shared by the drawing, background, and watermark write paths so the
79
+ * embed-vs-link decision lives in exactly one place.
80
+ */
81
+ function buildImageRel(rId, bookImage) {
82
+ if (isExternalImage(bookImage)) {
83
+ return {
84
+ Id: rId,
85
+ Type: rel_type_1.RelType.Image,
86
+ Target: bookImage.link,
87
+ TargetMode: "External"
88
+ };
89
+ }
90
+ return {
91
+ Id: rId,
92
+ Type: rel_type_1.RelType.Image,
93
+ Target: resolveMediaTarget(bookImage)
94
+ };
95
+ }
34
96
  /**
35
97
  * Build the drawing anchors and relationships from a list of image media entries.
36
98
  *
@@ -52,20 +114,19 @@ function buildDrawingAnchorsAndRels(media, existingRels, options) {
52
114
  if (!bookImage) {
53
115
  continue;
54
116
  }
117
+ // An external (linked) image has a `link` target and no embedded bytes.
118
+ const isExternal = isExternalImage(bookImage);
55
119
  // Deduplicate: reuse rId if same imageId already has a drawing rel
56
120
  let rIdImage = imageRIdMap[imageId];
57
121
  if (!rIdImage) {
58
122
  rIdImage = options.nextRId(rels);
59
123
  imageRIdMap[imageId] = rIdImage;
60
- rels.push({
61
- Id: rIdImage,
62
- Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
63
- Target: resolveMediaTarget(bookImage)
64
- });
124
+ rels.push(buildImageRel(rIdImage, bookImage));
65
125
  }
66
126
  const anchor = {
67
127
  picture: {
68
- rId: rIdImage
128
+ rId: rIdImage,
129
+ ...(isExternal ? { external: true } : {})
69
130
  },
70
131
  range: medium.range
71
132
  };
@@ -1700,7 +1700,42 @@ class Workbook {
1700
1700
  // Images
1701
1701
  // ===========================================================================
1702
1702
  /**
1703
- * Add Image to Workbook and return the id
1703
+ * Register an image with the workbook and return its numeric id. Pass the id
1704
+ * to {@link Worksheet.addImage}, {@link Worksheet.addBackgroundImage}, or
1705
+ * {@link Worksheet.addWatermark} to place it.
1706
+ *
1707
+ * The image is either **embedded** or **linked (external)**:
1708
+ *
1709
+ * - **Embedded** — supply `buffer`, `base64`, or `filename`. The bytes are
1710
+ * written into the `.xlsx` package (`xl/media/imageN.ext`). Self-contained,
1711
+ * but inflates file size.
1712
+ * - **Linked (external)** — supply only `link` (a URL or local file path).
1713
+ * No bytes are stored; the package keeps a relationship with
1714
+ * `TargetMode="External"` and the picture is rendered via `<a:blip r:link>`.
1715
+ * Keeps the file small, but the image is resolved by Excel at open time.
1716
+ *
1717
+ * If both bytes and a `link` are provided, **embedding wins**.
1718
+ *
1719
+ * Linked images work with **cell pictures** ({@link Worksheet.addImage}) and
1720
+ * **overlay watermarks** ({@link Worksheet.addWatermark} with `mode:
1721
+ * "overlay"`). Worksheet background images and header/footer (VML) watermarks
1722
+ * cannot be linked — they require an embedded image.
1723
+ *
1724
+ * Note: Excel treats linked images as volatile — a moved/missing target
1725
+ * shows a broken-image placeholder, and modern Excel may not auto-load
1726
+ * remote URLs for security reasons. Prefer embedding for self-contained files.
1727
+ *
1728
+ * @example Embedded image
1729
+ * ```typescript
1730
+ * const id = workbook.addImage({ buffer: pngBytes, extension: "png" });
1731
+ * worksheet.addImage(id, "B2:D6");
1732
+ * ```
1733
+ *
1734
+ * @example Linked (external) image — no bytes stored
1735
+ * ```typescript
1736
+ * const id = workbook.addImage({ extension: "png", link: "https://example.com/logo.png" });
1737
+ * worksheet.addImage(id, "B2:D6");
1738
+ * ```
1704
1739
  */
1705
1740
  addImage(image) {
1706
1741
  const id = this.media.length;
@@ -23,6 +23,7 @@ const address_1 = require("./utils/address.js");
23
23
  const cell_format_1 = require("./utils/cell-format.js");
24
24
  const col_cache_1 = require("./utils/col-cache.js");
25
25
  const copy_style_1 = require("./utils/copy-style.js");
26
+ const drawing_utils_1 = require("./utils/drawing-utils.js");
26
27
  const merge_borders_1 = require("./utils/merge-borders.js");
27
28
  const sheet_protection_1 = require("./utils/sheet-protection.js");
28
29
  const text_metrics_1 = require("./utils/text-metrics.js");
@@ -1309,9 +1310,20 @@ class Worksheet {
1309
1310
  return true;
1310
1311
  }
1311
1312
  /**
1312
- * Using the image id from `Workbook.addImage`, set the background to the worksheet
1313
+ * Using the image id from `Workbook.addImage`, set the background to the worksheet.
1314
+ *
1315
+ * The image must be **embedded** (`buffer`/`base64`/`filename`). Worksheet
1316
+ * background pictures (`<picture r:id>`) do not support external (linked)
1317
+ * images — Excel silently drops a background whose relationship uses
1318
+ * `TargetMode="External"`, so this rejects linked images up front.
1313
1319
  */
1314
1320
  addBackgroundImage(imageId) {
1321
+ const bookImage = this._workbook.getImage(imageId);
1322
+ if (bookImage && (0, drawing_utils_1.isExternalImage)(bookImage)) {
1323
+ throw new errors_1.ImageError("Background images cannot be external (linked) images. " +
1324
+ "Use an embedded image (buffer/base64/filename). " +
1325
+ "External images are only supported for cell pictures and overlay watermarks.");
1326
+ }
1315
1327
  const model = {
1316
1328
  type: "background",
1317
1329
  imageId: String(imageId)
@@ -1336,7 +1348,14 @@ class Worksheet {
1336
1348
  * Visible in Page Layout view and when printed. Renders behind cell content.
1337
1349
  * Transparency must be baked into the image (PNG with alpha channel).
1338
1350
  *
1351
+ * **External (linked) images:** `mode: "overlay"` supports external images
1352
+ * (registered via `workbook.addImage({ link })`). `mode: "header"` does
1353
+ * **not** — VML header/footer images require embedded media, so passing a
1354
+ * linked image with `mode: "header"` throws an `ImageError`. Use an embedded
1355
+ * image (`buffer`/`base64`/`filename`) or switch to `mode: "overlay"`.
1356
+ *
1339
1357
  * @param options - Watermark configuration
1358
+ * @throws {ImageError} If `mode: "header"` is used with an external (linked) image.
1340
1359
  *
1341
1360
  * @example Overlay watermark with transparency:
1342
1361
  * ```typescript
@@ -1351,11 +1370,23 @@ class Worksheet {
1351
1370
  * ```
1352
1371
  */
1353
1372
  addWatermark(options) {
1373
+ const mode = options.mode ?? "overlay";
1374
+ // Validate BEFORE mutating any state: VML header/footer images use
1375
+ // embedded media (`<v:imagedata o:relid>`); external (linked) images are
1376
+ // not representable here. Reject them up front so a failed call leaves the
1377
+ // existing watermark untouched (no partial mutation).
1378
+ if (mode === "header") {
1379
+ const bookImage = this._workbook.getImage(options.imageId);
1380
+ if (bookImage && (0, drawing_utils_1.isExternalImage)(bookImage)) {
1381
+ throw new errors_1.ImageError("Header watermark images cannot be external (linked) images. " +
1382
+ "Use an embedded image (buffer/base64/filename), or use overlay mode for linked images.");
1383
+ }
1384
+ }
1354
1385
  // Remove any existing watermark media entries first
1355
1386
  this._media = this._media.filter(m => m.type !== "watermark" && m.type !== "headerImage");
1356
1387
  this._watermark = {
1357
1388
  imageId: String(options.imageId),
1358
- mode: options.mode ?? "overlay",
1389
+ mode,
1359
1390
  opacity: options.opacity,
1360
1391
  headerWidth: options.headerWidth,
1361
1392
  headerHeight: options.headerHeight,
@@ -1371,7 +1402,7 @@ class Worksheet {
1371
1402
  this._media.push(new image_1.Image(this, model));
1372
1403
  }
1373
1404
  else {
1374
- // Header mode: add as a "headerImage" media entry for the VML pipeline
1405
+ // Header mode: add as a "headerImage" media entry for the VML pipeline.
1375
1406
  const model = {
1376
1407
  type: "headerImage",
1377
1408
  imageId: String(options.imageId),
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ContentTypesXform = void 0;
4
+ const drawing_utils_1 = require("../../../utils/drawing-utils.js");
4
5
  const ooxml_paths_1 = require("../../../utils/ooxml-paths.js");
5
6
  const base_xform_1 = require("../base-xform.js");
6
7
  const writer_1 = require("../../../../xml/writer.js");
@@ -13,6 +14,11 @@ class ContentTypesXform extends base_xform_1.BaseXform {
13
14
  const mediaHash = {};
14
15
  (model.media ?? []).forEach((medium) => {
15
16
  if (medium.type === "image") {
17
+ // External (linked) images add no part to the package, so they need
18
+ // no Default content-type registration.
19
+ if ((0, drawing_utils_1.isExternalImage)(medium)) {
20
+ return;
21
+ }
16
22
  const imageType = medium.extension;
17
23
  if (!mediaHash[imageType]) {
18
24
  mediaHash[imageType] = true;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BaseCellAnchorXform = void 0;
4
+ const drawing_utils_1 = require("../../../utils/drawing-utils.js");
4
5
  const base_xform_1 = require("../base-xform.js");
5
6
  class BaseCellAnchorXform extends base_xform_1.BaseXform {
6
7
  parseOpen(node) {
@@ -37,6 +38,13 @@ class BaseCellAnchorXform extends base_xform_1.BaseXform {
37
38
  if (!rel) {
38
39
  return undefined;
39
40
  }
41
+ // External (linked) image: the relationship uses TargetMode="External"
42
+ // and there is no media part in the package. Synthesize a media entry
43
+ // carrying the `link` target so it round-trips and surfaces on the
44
+ // worksheet as an external image. Entries are deduplicated by link.
45
+ if (rel.TargetMode === "External" || model.external) {
46
+ return this.reconcileExternalPicture(rel.Target, options);
47
+ }
40
48
  const match = rel.Target.match(/.*\/media\/(.+[.][a-zA-Z]{3,4})/);
41
49
  if (match) {
42
50
  const name = match[1];
@@ -51,5 +59,29 @@ class BaseCellAnchorXform extends base_xform_1.BaseXform {
51
59
  }
52
60
  return undefined;
53
61
  }
62
+ /**
63
+ * Resolve (or create) the media entry for an external linked image. The
64
+ * synthesized entry is appended to `options.media` and indexed by its link
65
+ * so repeated references to the same external image share one entry.
66
+ */
67
+ reconcileExternalPicture(link, options) {
68
+ if (!link) {
69
+ return undefined;
70
+ }
71
+ const indexKey = `external:${link}`;
72
+ let mediaId = options.mediaIndex[indexKey];
73
+ if (mediaId === undefined) {
74
+ mediaId = options.media.length;
75
+ const medium = {
76
+ type: "image",
77
+ extension: (0, drawing_utils_1.inferExternalImageExtension)(link),
78
+ link,
79
+ index: mediaId
80
+ };
81
+ options.media.push(medium);
82
+ options.mediaIndex[indexKey] = mediaId;
83
+ }
84
+ return options.media[mediaId];
85
+ }
54
86
  }
55
87
  exports.BaseCellAnchorXform = BaseCellAnchorXform;
@@ -11,11 +11,13 @@ class BlipXform extends base_xform_1.BaseXform {
11
11
  return "a:blip";
12
12
  }
13
13
  render(xmlStream, model) {
14
+ // External (linked) images use `r:link`; embedded images use `r:embed`.
15
+ const relAttr = model.external ? "r:link" : "r:embed";
14
16
  if (model.alphaModFix !== undefined && model.alphaModFix < 100000) {
15
17
  // Render as open/close node with a:alphaModFix child
16
18
  xmlStream.openNode(this.tag, {
17
19
  "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
18
- "r:embed": model.rId,
20
+ [relAttr]: model.rId,
19
21
  cstate: "print"
20
22
  });
21
23
  xmlStream.leafNode("a:alphaModFix", { amt: String(model.alphaModFix) });
@@ -24,18 +26,24 @@ class BlipXform extends base_xform_1.BaseXform {
24
26
  else {
25
27
  xmlStream.leafNode(this.tag, {
26
28
  "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
27
- "r:embed": model.rId,
29
+ [relAttr]: model.rId,
28
30
  cstate: "print"
29
31
  });
30
32
  }
31
33
  }
32
34
  parseOpen(node) {
33
35
  switch (node.name) {
34
- case this.tag:
35
- this.model = {
36
- rId: node.attributes["r:embed"]
37
- };
36
+ case this.tag: {
37
+ // A blip may carry `r:embed` (embedded) or `r:link` (external linked).
38
+ const link = node.attributes["r:link"];
39
+ if (link !== undefined) {
40
+ this.model = { rId: link, external: true };
41
+ }
42
+ else {
43
+ this.model = { rId: node.attributes["r:embed"] };
44
+ }
38
45
  return true;
46
+ }
39
47
  case "a:alphaModFix":
40
48
  if (node.attributes.amt) {
41
49
  this.model.alphaModFix = parseInt(node.attributes.amt, 10);
@@ -27,7 +27,8 @@ class PicXform extends base_xform_1.BaseXform {
27
27
  // Pass alphaModFix through to blipFill → blip
28
28
  this.map["xdr:blipFill"].render(xmlStream, {
29
29
  rId: model.rId,
30
- alphaModFix: model.alphaModFix
30
+ alphaModFix: model.alphaModFix,
31
+ external: model.external
31
32
  });
32
33
  this.map["xdr:spPr"].render(xmlStream, model);
33
34
  xmlStream.closeNode();