@cj-tech-master/excelts 9.0.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/browser/index.browser.d.ts +2 -0
  2. package/dist/browser/index.browser.js +2 -0
  3. package/dist/browser/index.d.ts +2 -0
  4. package/dist/browser/index.js +2 -0
  5. package/dist/browser/modules/excel/image.d.ts +27 -2
  6. package/dist/browser/modules/excel/image.js +23 -1
  7. package/dist/browser/modules/excel/stream/worksheet-writer.d.ts +16 -1
  8. package/dist/browser/modules/excel/stream/worksheet-writer.js +68 -0
  9. package/dist/browser/modules/excel/types.d.ts +72 -0
  10. package/dist/browser/modules/excel/utils/drawing-utils.d.ts +4 -0
  11. package/dist/browser/modules/excel/utils/drawing-utils.js +5 -0
  12. package/dist/browser/modules/excel/utils/ooxml-paths.d.ts +4 -0
  13. package/dist/browser/modules/excel/utils/ooxml-paths.js +15 -0
  14. package/dist/browser/modules/excel/utils/watermark-image.d.ts +67 -0
  15. package/dist/browser/modules/excel/utils/watermark-image.js +383 -0
  16. package/dist/browser/modules/excel/worksheet.d.ts +39 -1
  17. package/dist/browser/modules/excel/worksheet.js +99 -0
  18. package/dist/browser/modules/excel/xlsx/xform/core/content-types-xform.js +3 -2
  19. package/dist/browser/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +6 -1
  20. package/dist/browser/modules/excel/xlsx/xform/drawing/blip-fill-xform.d.ts +2 -1
  21. package/dist/browser/modules/excel/xlsx/xform/drawing/blip-fill-xform.js +0 -1
  22. package/dist/browser/modules/excel/xlsx/xform/drawing/blip-xform.d.ts +3 -1
  23. package/dist/browser/modules/excel/xlsx/xform/drawing/blip-xform.js +22 -6
  24. package/dist/browser/modules/excel/xlsx/xform/drawing/pic-xform.d.ts +3 -0
  25. package/dist/browser/modules/excel/xlsx/xform/drawing/pic-xform.js +5 -1
  26. package/dist/browser/modules/excel/xlsx/xform/drawing/vml-drawing-xform.d.ts +19 -0
  27. package/dist/browser/modules/excel/xlsx/xform/drawing/vml-drawing-xform.js +103 -4
  28. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +135 -8
  29. package/dist/browser/modules/excel/xlsx/xlsx.browser.d.ts +1 -0
  30. package/dist/browser/modules/excel/xlsx/xlsx.browser.js +53 -1
  31. package/dist/browser/modules/pdf/core/pdf-writer.d.ts +1 -1
  32. package/dist/browser/modules/pdf/core/pdf-writer.js +2 -1
  33. package/dist/browser/modules/pdf/index.d.ts +1 -1
  34. package/dist/browser/modules/pdf/render/page-renderer.d.ts +29 -1
  35. package/dist/browser/modules/pdf/render/page-renderer.js +394 -25
  36. package/dist/browser/modules/pdf/render/pdf-exporter.js +84 -47
  37. package/dist/browser/modules/pdf/types.d.ts +235 -0
  38. package/dist/cjs/index.js +5 -2
  39. package/dist/cjs/modules/excel/image.js +23 -1
  40. package/dist/cjs/modules/excel/stream/worksheet-writer.js +68 -0
  41. package/dist/cjs/modules/excel/utils/drawing-utils.js +5 -0
  42. package/dist/cjs/modules/excel/utils/ooxml-paths.js +19 -0
  43. package/dist/cjs/modules/excel/utils/watermark-image.js +386 -0
  44. package/dist/cjs/modules/excel/worksheet.js +99 -0
  45. package/dist/cjs/modules/excel/xlsx/xform/core/content-types-xform.js +3 -2
  46. package/dist/cjs/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +6 -1
  47. package/dist/cjs/modules/excel/xlsx/xform/drawing/blip-fill-xform.js +0 -1
  48. package/dist/cjs/modules/excel/xlsx/xform/drawing/blip-xform.js +22 -6
  49. package/dist/cjs/modules/excel/xlsx/xform/drawing/pic-xform.js +5 -1
  50. package/dist/cjs/modules/excel/xlsx/xform/drawing/vml-drawing-xform.js +103 -4
  51. package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +134 -7
  52. package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +52 -0
  53. package/dist/cjs/modules/pdf/core/pdf-writer.js +2 -1
  54. package/dist/cjs/modules/pdf/render/page-renderer.js +396 -25
  55. package/dist/cjs/modules/pdf/render/pdf-exporter.js +83 -46
  56. package/dist/esm/index.browser.js +2 -0
  57. package/dist/esm/index.js +2 -0
  58. package/dist/esm/modules/excel/image.js +23 -1
  59. package/dist/esm/modules/excel/stream/worksheet-writer.js +68 -0
  60. package/dist/esm/modules/excel/utils/drawing-utils.js +5 -0
  61. package/dist/esm/modules/excel/utils/ooxml-paths.js +15 -0
  62. package/dist/esm/modules/excel/utils/watermark-image.js +383 -0
  63. package/dist/esm/modules/excel/worksheet.js +99 -0
  64. package/dist/esm/modules/excel/xlsx/xform/core/content-types-xform.js +3 -2
  65. package/dist/esm/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +6 -1
  66. package/dist/esm/modules/excel/xlsx/xform/drawing/blip-fill-xform.js +0 -1
  67. package/dist/esm/modules/excel/xlsx/xform/drawing/blip-xform.js +22 -6
  68. package/dist/esm/modules/excel/xlsx/xform/drawing/pic-xform.js +5 -1
  69. package/dist/esm/modules/excel/xlsx/xform/drawing/vml-drawing-xform.js +103 -4
  70. package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +135 -8
  71. package/dist/esm/modules/excel/xlsx/xlsx.browser.js +53 -1
  72. package/dist/esm/modules/pdf/core/pdf-writer.js +2 -1
  73. package/dist/esm/modules/pdf/render/page-renderer.js +394 -25
  74. package/dist/esm/modules/pdf/render/pdf-exporter.js +84 -47
  75. package/dist/iife/excelts.iife.js +2390 -469
  76. package/dist/iife/excelts.iife.js.map +1 -1
  77. package/dist/iife/excelts.iife.min.js +47 -47
  78. package/dist/types/index.browser.d.ts +2 -0
  79. package/dist/types/index.d.ts +2 -0
  80. package/dist/types/modules/excel/image.d.ts +27 -2
  81. package/dist/types/modules/excel/stream/worksheet-writer.d.ts +16 -1
  82. package/dist/types/modules/excel/types.d.ts +72 -0
  83. package/dist/types/modules/excel/utils/drawing-utils.d.ts +4 -0
  84. package/dist/types/modules/excel/utils/ooxml-paths.d.ts +4 -0
  85. package/dist/types/modules/excel/utils/watermark-image.d.ts +67 -0
  86. package/dist/types/modules/excel/worksheet.d.ts +39 -1
  87. package/dist/types/modules/excel/xlsx/xform/drawing/blip-fill-xform.d.ts +2 -1
  88. package/dist/types/modules/excel/xlsx/xform/drawing/blip-xform.d.ts +3 -1
  89. package/dist/types/modules/excel/xlsx/xform/drawing/pic-xform.d.ts +3 -0
  90. package/dist/types/modules/excel/xlsx/xform/drawing/vml-drawing-xform.d.ts +19 -0
  91. package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +1 -0
  92. package/dist/types/modules/pdf/core/pdf-writer.d.ts +1 -1
  93. package/dist/types/modules/pdf/index.d.ts +1 -1
  94. package/dist/types/modules/pdf/render/page-renderer.d.ts +29 -1
  95. package/dist/types/modules/pdf/types.d.ts +235 -0
  96. package/package.json +1 -1
@@ -8,12 +8,13 @@
8
8
  * It is used internally by the public `pdf()` and `excelToPdf()` APIs.
9
9
  */
10
10
  import { PdfWriter } from "../core/pdf-writer.js";
11
+ import { PdfContentStream } from "../core/pdf-stream.js";
11
12
  import { PdfDict, pdfRef, pdfNumber, pdfString as pdfStr } from "../core/pdf-object.js";
12
13
  import { FontManager, resolvePdfFontName } from "../font/font-manager.js";
13
14
  import { parseTtf } from "../font/ttf-parser.js";
14
15
  import { initEncryption } from "../core/encryption.js";
15
16
  import { layoutSheet } from "./layout-engine.js";
16
- import { renderPage, alphaGsName } from "./page-renderer.js";
17
+ import { renderPage, alphaGsName, renderWatermark, parseImageDimensions } from "./page-renderer.js";
17
18
  import { decodePng } from "./png-decoder.js";
18
19
  import { PdfError, PdfRenderError } from "../errors.js";
19
20
  import { PageSizes } from "../types.js";
@@ -83,8 +84,21 @@ async function finishExport(ctx, workbook, options) {
83
84
  ensureAtLeastOnePage(allPages, documentOptions, sheets);
84
85
  fixPageNumbers(allPages);
85
86
  trackFontsForHeaders(allPages, fontManager);
87
+ // Track watermark fonts
88
+ const watermark = documentOptions.watermark;
89
+ if (watermark && watermark.type === "text") {
90
+ const wmFontFamily = watermark.fontFamily ?? "Helvetica";
91
+ const wmBold = watermark.bold ?? false;
92
+ const wmItalic = watermark.italic ?? false;
93
+ if (fontManager.hasEmbeddedFont()) {
94
+ fontManager.trackText(watermark.text);
95
+ }
96
+ else {
97
+ fontManager.ensureFont(resolvePdfFontName(wmFontFamily, wmBold, wmItalic));
98
+ }
99
+ }
86
100
  const fontObjectMap = fontManager.writeFontResources(writer);
87
- const { pageObjNums, sheetFirstPage, pagesTreeObjNum } = await renderAllPages(allPages, fontManager, writer, fontObjectMap);
101
+ const { pageObjNums, sheetFirstPage, pagesTreeObjNum } = await renderAllPages(allPages, fontManager, writer, fontObjectMap, watermark);
88
102
  return buildFinalPdf(writer, pageObjNums, pagesTreeObjNum, sheetFirstPage, documentOptions, workbook, options);
89
103
  }
90
104
  function ensureAtLeastOnePage(allPages, documentOptions, sheets) {
@@ -133,20 +147,20 @@ function trackFontsForHeaders(allPages, fontManager) {
133
147
  }
134
148
  }
135
149
  }
136
- async function renderAllPages(allPages, fontManager, writer, fontObjectMap) {
150
+ async function renderAllPages(allPages, fontManager, writer, fontObjectMap, watermark) {
137
151
  const pageObjNums = [];
138
152
  const pagesTreeObjNum = writer.allocObject();
139
153
  const sheetFirstPage = new Map();
140
154
  const totalPages = allPages.length;
141
155
  for (let i = 0; i < allPages.length; i++) {
142
- renderSinglePage(allPages[i], fontManager, writer, fontObjectMap, totalPages, pageObjNums, pagesTreeObjNum, sheetFirstPage);
156
+ renderSinglePage(allPages[i], fontManager, writer, fontObjectMap, totalPages, pageObjNums, pagesTreeObjNum, sheetFirstPage, watermark);
143
157
  if (i < allPages.length - 1) {
144
158
  await yieldToEventLoop();
145
159
  }
146
160
  }
147
161
  return { pageObjNums, sheetFirstPage, pagesTreeObjNum };
148
162
  }
149
- function renderSinglePage(page, fontManager, writer, fontObjectMap, totalPages, pageObjNums, pagesTreeObjNum, sheetFirstPage) {
163
+ function renderSinglePage(page, fontManager, writer, fontObjectMap, totalPages, pageObjNums, pagesTreeObjNum, sheetFirstPage, watermark) {
150
164
  try {
151
165
  const { stream: contentStream, alphaValues } = renderPage(page, page.options, fontManager, totalPages);
152
166
  // Handle images: create XObject Image entries and draw them
@@ -160,10 +174,46 @@ function renderSinglePage(page, fontManager, writer, fontObjectMap, totalPages,
160
174
  contentStream.drawImage(imgName, img.rect.x, img.rect.y, img.rect.width, img.rect.height);
161
175
  }
162
176
  }
163
- // Add content stream object
177
+ // --- Render watermark into a separate content stream ---
178
+ // PDF supports Contents as an array of stream references. The watermark stream
179
+ // is placed BEFORE the main content stream so it renders behind everything.
180
+ let watermarkContentObjNum;
181
+ const shouldApplyWatermark = watermark && isWatermarkApplicable(watermark, page);
182
+ if (shouldApplyWatermark) {
183
+ const wmContentStream = new PdfContentStream();
184
+ const wmResult = renderWatermark(wmContentStream, page, watermark, fontManager);
185
+ // Register watermark alpha values in the shared set
186
+ for (const alpha of wmResult.alphaValues) {
187
+ alphaValues.add(alpha);
188
+ }
189
+ // Register watermark image XObjects
190
+ for (const wmImg of wmResult.imageXObjects) {
191
+ const imgObjNum = writeImageXObject(writer, wmImg.data, wmImg.format);
192
+ imageXObjects.set(wmImg.name, imgObjNum);
193
+ }
194
+ // Write watermark content stream object
195
+ watermarkContentObjNum = writer.allocObject();
196
+ writer.addStreamObject(watermarkContentObjNum, new PdfDict(), wmContentStream);
197
+ }
198
+ // Add main content stream object
164
199
  const contentObjNum = writer.allocObject();
165
- const contentDict = new PdfDict();
166
- writer.addStreamObject(contentObjNum, contentDict, contentStream);
200
+ writer.addStreamObject(contentObjNum, new PdfDict(), contentStream);
201
+ // Build Contents reference — array if watermark exists, single ref otherwise.
202
+ // placement "under" (default): watermark stream first, then content
203
+ // placement "over": content first, then watermark stream on top
204
+ let contentsRef;
205
+ if (watermarkContentObjNum) {
206
+ const placement = watermark?.placement ?? "under";
207
+ if (placement === "over") {
208
+ contentsRef = `[${pdfRef(contentObjNum)} ${pdfRef(watermarkContentObjNum)}]`;
209
+ }
210
+ else {
211
+ contentsRef = `[${pdfRef(watermarkContentObjNum)} ${pdfRef(contentObjNum)}]`;
212
+ }
213
+ }
214
+ else {
215
+ contentsRef = pdfRef(contentObjNum);
216
+ }
167
217
  // Add resources dictionary object
168
218
  const resourcesObjNum = writer.allocObject();
169
219
  const fontDictStr = fontManager.buildFontDictString(fontObjectMap);
@@ -212,7 +262,7 @@ function renderSinglePage(page, fontManager, writer, fontObjectMap, totalPages,
212
262
  parentRef: pagesTreeObjNum,
213
263
  width: page.width,
214
264
  height: page.height,
215
- contentsRef: contentObjNum,
265
+ contentsRef: contentsRef,
216
266
  resourcesRef: resourcesObjNum,
217
267
  annotRefs: annotRefs.length > 0 ? annotRefs : undefined
218
268
  });
@@ -337,7 +387,8 @@ function resolveOptions(options, sheet) {
337
387
  title: options?.title ?? "",
338
388
  author: options?.author ?? "",
339
389
  subject: options?.subject ?? "",
340
- creator: options?.creator ?? "excelts"
390
+ creator: options?.creator ?? "excelts",
391
+ watermark: options?.watermark
341
392
  };
342
393
  }
343
394
  /** Map PaperSize enum values to PDF page sizes. */
@@ -428,6 +479,28 @@ function buildOutlines(writer, sheetFirstPage, pageObjNums) {
428
479
  return outlinesObjNum;
429
480
  }
430
481
  // =============================================================================
482
+ // Watermark Filtering
483
+ // =============================================================================
484
+ /**
485
+ * Check if a watermark should be applied to a specific page based on
486
+ * optional page number and sheet name filters.
487
+ */
488
+ function isWatermarkApplicable(watermark, page) {
489
+ if (watermark.pages && watermark.pages.length > 0) {
490
+ if (!watermark.pages.includes(page.pageNumber)) {
491
+ return false;
492
+ }
493
+ }
494
+ if (watermark.sheets && watermark.sheets.length > 0) {
495
+ // Case-insensitive sheet name matching, consistent with the rest of the API
496
+ const sheetLower = page.sheetName.toLowerCase();
497
+ if (!watermark.sheets.some(s => s.toLowerCase() === sheetLower)) {
498
+ return false;
499
+ }
500
+ }
501
+ return true;
502
+ }
503
+ // =============================================================================
431
504
  // Image XObject
432
505
  // =============================================================================
433
506
  /**
@@ -444,7 +517,7 @@ function writeImageXObject(writer, data, format) {
444
517
  */
445
518
  function writeJpegImageXObject(writer, data) {
446
519
  const objNum = writer.allocObject();
447
- const dims = getJpegDimensions(data);
520
+ const dims = parseImageDimensions(data, "jpeg");
448
521
  const dict = new PdfDict()
449
522
  .set("Type", "/XObject")
450
523
  .set("Subtype", "/Image")
@@ -486,39 +559,3 @@ function writePngImageXObject(writer, data) {
486
559
  writer.addStreamObject(objNum, dict, png.pixels);
487
560
  return objNum;
488
561
  }
489
- /**
490
- * Extract width and height from a JPEG file header.
491
- * Scans for SOF markers (SOF0-SOF3, SOF5-SOF7, SOF9-SOF11, SOF13-SOF15)
492
- * which all share the same frame header layout.
493
- * Handles 0xFF padding bytes per JPEG spec.
494
- */
495
- function getJpegDimensions(data) {
496
- let offset = 2; // skip SOI marker (0xFFD8)
497
- while (offset < data.length - 1) {
498
- // Skip any 0xFF fill bytes
499
- while (offset < data.length && data[offset] === 0xff && data[offset + 1] === 0xff) {
500
- offset++;
501
- }
502
- if (offset >= data.length - 1 || data[offset] !== 0xff) {
503
- break;
504
- }
505
- const marker = data[offset + 1];
506
- // SOF markers: C0-C3, C5-C7, C9-CB, CD-CF (excluding C4=DHT, C8=JPG, CC=DAC)
507
- const isSof = marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc;
508
- if (isSof) {
509
- if (offset + 8 < data.length) {
510
- const height = (data[offset + 5] << 8) | data[offset + 6];
511
- const width = (data[offset + 7] << 8) | data[offset + 8];
512
- return { width, height };
513
- }
514
- break;
515
- }
516
- // Skip segment: 2 byte marker + segment length (includes the 2 length bytes)
517
- if (offset + 3 >= data.length) {
518
- break;
519
- }
520
- const segLen = (data[offset + 2] << 8) | data[offset + 3];
521
- offset += 2 + segLen;
522
- }
523
- return { width: 1, height: 1 };
524
- }
@@ -339,7 +339,241 @@ export interface PdfExportOptions {
339
339
  printHighQuality: boolean;
340
340
  }>;
341
341
  };
342
+ /**
343
+ * Watermark to render on every page.
344
+ * Supports text watermarks (e.g. "CONFIDENTIAL") and image watermarks (e.g. company logo).
345
+ *
346
+ * @example Text watermark:
347
+ * ```typescript
348
+ * watermark: {
349
+ * type: "text",
350
+ * text: "DRAFT",
351
+ * opacity: 0.1,
352
+ * rotation: -45
353
+ * }
354
+ * ```
355
+ *
356
+ * @example Image watermark:
357
+ * ```typescript
358
+ * watermark: {
359
+ * type: "image",
360
+ * data: logoPng,
361
+ * format: "png",
362
+ * opacity: 0.08
363
+ * }
364
+ * ```
365
+ */
366
+ watermark?: PdfWatermark;
342
367
  }
368
+ /**
369
+ * Text watermark configuration for PDF export.
370
+ *
371
+ * Renders semi-transparent text (e.g. "CONFIDENTIAL", "DRAFT") on every page.
372
+ *
373
+ * @example
374
+ * ```typescript
375
+ * const bytes = await pdf(data, {
376
+ * watermark: {
377
+ * type: "text",
378
+ * text: "CONFIDENTIAL",
379
+ * color: { r: 0.8, g: 0, b: 0 },
380
+ * opacity: 0.1,
381
+ * rotation: -45
382
+ * }
383
+ * });
384
+ * ```
385
+ */
386
+ export interface PdfTextWatermark {
387
+ type: "text";
388
+ /** The watermark text to display. */
389
+ text: string;
390
+ /**
391
+ * Font size in points.
392
+ * @default 54
393
+ */
394
+ fontSize?: number;
395
+ /**
396
+ * Text color (RGB, each 0-1).
397
+ * @default { r: 0.75, g: 0.75, b: 0.75 }
398
+ */
399
+ color?: PdfColor;
400
+ /**
401
+ * Opacity (0 = fully transparent, 1 = fully opaque).
402
+ * @default 0.15
403
+ */
404
+ opacity?: number;
405
+ /**
406
+ * Rotation angle in degrees (positive = counter-clockwise).
407
+ * @default -45
408
+ */
409
+ rotation?: number;
410
+ /**
411
+ * Font family name. Must be a standard PDF font (Type1) or the embedded font.
412
+ * @default "Helvetica"
413
+ */
414
+ fontFamily?: string;
415
+ /**
416
+ * Whether to render in bold.
417
+ * @default false
418
+ */
419
+ bold?: boolean;
420
+ /**
421
+ * Whether to render in italic.
422
+ * @default false
423
+ */
424
+ italic?: boolean;
425
+ /**
426
+ * Position on the page. `"center"` places the watermark at the geometric center.
427
+ * A custom `{ x, y }` object specifies the **center point** of the watermark
428
+ * in PDF points (origin at bottom-left corner of the page).
429
+ * @default "center"
430
+ */
431
+ position?: "center" | {
432
+ x: number;
433
+ y: number;
434
+ };
435
+ /**
436
+ * When true, the watermark text is tiled in a repeating grid across the entire page.
437
+ * @default false
438
+ */
439
+ repeat?: boolean;
440
+ /**
441
+ * Horizontal spacing (in points) between repeated watermark tiles.
442
+ * Only used when `repeat` is true.
443
+ * @default 200
444
+ */
445
+ repeatSpacingX?: number;
446
+ /**
447
+ * Vertical spacing (in points) between repeated watermark tiles.
448
+ * Only used when `repeat` is true.
449
+ * @default 200
450
+ */
451
+ repeatSpacingY?: number;
452
+ }
453
+ /**
454
+ * Image watermark configuration for PDF export.
455
+ *
456
+ * Embeds a semi-transparent image (e.g. company logo) on every page.
457
+ *
458
+ * @example
459
+ * ```typescript
460
+ * import { readFileSync } from "fs";
461
+ * const logo = readFileSync("logo.png");
462
+ *
463
+ * const bytes = await pdf(data, {
464
+ * watermark: {
465
+ * type: "image",
466
+ * data: logo,
467
+ * format: "png",
468
+ * opacity: 0.08,
469
+ * scale: 0.4
470
+ * }
471
+ * });
472
+ * ```
473
+ */
474
+ export interface PdfImageWatermark {
475
+ type: "image";
476
+ /** Raw image bytes (JPEG or PNG). */
477
+ data: Uint8Array;
478
+ /** Image format. */
479
+ format: "jpeg" | "png";
480
+ /**
481
+ * Opacity (0 = fully transparent, 1 = fully opaque).
482
+ * @default 0.15
483
+ */
484
+ opacity?: number;
485
+ /**
486
+ * Rotation angle in degrees (positive = counter-clockwise).
487
+ * @default 0
488
+ */
489
+ rotation?: number;
490
+ /**
491
+ * Scale factor relative to the page size.
492
+ * 0.5 means the image's largest dimension will be scaled to
493
+ * 50% of the smaller page dimension (width or height).
494
+ * Ignored when `width` and `height` are explicitly provided.
495
+ * @default 0.5
496
+ */
497
+ scale?: number;
498
+ /**
499
+ * Explicit image width in PDF points. When set together with `height`,
500
+ * overrides `scale` and renders the image at the exact specified dimensions.
501
+ */
502
+ width?: number;
503
+ /**
504
+ * Explicit image height in PDF points. When set together with `width`,
505
+ * overrides `scale` and renders the image at the exact specified dimensions.
506
+ */
507
+ height?: number;
508
+ /**
509
+ * Position on the page. `"center"` places the watermark at the geometric center.
510
+ * A custom `{ x, y }` object specifies the **center point** of the watermark
511
+ * in PDF points (origin at bottom-left corner of the page).
512
+ * @default "center"
513
+ */
514
+ position?: "center" | {
515
+ x: number;
516
+ y: number;
517
+ };
518
+ /**
519
+ * When true, the watermark image is tiled in a repeating grid across the entire page.
520
+ * @default false
521
+ */
522
+ repeat?: boolean;
523
+ /**
524
+ * Horizontal spacing (in points) between repeated watermark tiles.
525
+ * Only used when `repeat` is true.
526
+ * @default 200
527
+ */
528
+ repeatSpacingX?: number;
529
+ /**
530
+ * Vertical spacing (in points) between repeated watermark tiles.
531
+ * Only used when `repeat` is true.
532
+ * @default 200
533
+ */
534
+ repeatSpacingY?: number;
535
+ }
536
+ /**
537
+ * Common watermark filter and placement options shared by text and image watermarks.
538
+ */
539
+ export interface PdfWatermarkFilter {
540
+ /**
541
+ * Restrict the watermark to specific page numbers (1-based, document-global).
542
+ * When set, only pages whose number is in this array get the watermark.
543
+ * If omitted, all pages receive the watermark.
544
+ *
545
+ * @example Only on the first page:
546
+ * ```typescript
547
+ * watermark: { type: "text", text: "COVER", pages: [1] }
548
+ * ```
549
+ */
550
+ pages?: number[];
551
+ /**
552
+ * Restrict the watermark to specific sheet names (case-insensitive).
553
+ * When set, only pages belonging to the named sheets get the watermark.
554
+ * If omitted, all sheets receive the watermark.
555
+ *
556
+ * @example Only on the "Summary" sheet:
557
+ * ```typescript
558
+ * watermark: { type: "text", text: "DRAFT", sheets: ["Summary"] }
559
+ * ```
560
+ */
561
+ sheets?: string[];
562
+ /**
563
+ * Watermark layering relative to page content.
564
+ *
565
+ * - `"under"` — watermark renders **behind** all page content including
566
+ * cell fills, borders, text, grid lines, headers, and footers (default)
567
+ * - `"over"` — watermark renders **on top of** all page content
568
+ *
569
+ * @default "under"
570
+ */
571
+ placement?: "under" | "over";
572
+ }
573
+ /**
574
+ * Watermark configuration — either text or image, with optional page/sheet filters.
575
+ */
576
+ export type PdfWatermark = (PdfTextWatermark | PdfImageWatermark) & PdfWatermarkFilter;
343
577
  /**
344
578
  * Page margins in PDF points.
345
579
  */
@@ -369,6 +603,7 @@ export interface ResolvedPdfOptions {
369
603
  author: string;
370
604
  subject: string;
371
605
  creator: string;
606
+ watermark?: PdfWatermark;
372
607
  }
373
608
  /**
374
609
  * RGBA color used internally for PDF rendering.
package/dist/cjs/index.js CHANGED
@@ -17,8 +17,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
17
17
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.PdfRenderError = exports.PdfError = exports.PageSizes = exports.excelToPdf = exports.pdf = exports.uint8ArrayToString = exports.stringToUint8Array = exports.toUint8Array = exports.concatUint8Arrays = exports.getRootCause = exports.getErrorChain = exports.errorToJSON = exports.toError = exports.BaseError = exports.getSupportedFormats = exports.DateFormatter = exports.DateParser = exports.xmlDecode = exports.xmlEncode = exports.uint8ArrayToBase64 = exports.base64ToUint8Array = exports.excelToDate = exports.dateToExcel = exports.encodeRange = exports.decodeRange = exports.encodeCell = exports.decodeCell = exports.encodeRow = exports.decodeRow = exports.encodeCol = exports.decodeCol = exports.DefinedNames = exports.createCsvFormatterStream = exports.createCsvParserStream = exports.CsvFormatterStream = exports.CsvParserStream = exports.WorksheetReader = exports.WorksheetWriter = exports.WorkbookReader = exports.WorkbookWriter = exports.FormCheckbox = exports.DataValidations = exports.Table = exports.Image = exports.Range = exports.Cell = exports.Column = exports.Row = exports.Worksheet = exports.Workbook = void 0;
21
- exports.MarkdownParseError = exports.MarkdownError = exports.MaxItemsExceededError = exports.ImageError = exports.TableError = exports.PivotTableError = exports.WorksheetNameError = exports.XmlParseError = exports.InvalidValueTypeError = exports.MergeConflictError = exports.RowOutOfBoundsError = exports.ColumnOutOfBoundsError = exports.InvalidAddressError = exports.ExcelStreamStateError = exports.ExcelNotSupportedError = exports.ExcelDownloadError = exports.ExcelFileError = exports.isExcelError = exports.ExcelError = exports.isPdfError = exports.PdfStructureError = exports.PdfFontError = void 0;
20
+ exports.PdfError = exports.PageSizes = exports.excelToPdf = exports.pdf = exports.uint8ArrayToString = exports.stringToUint8Array = exports.toUint8Array = exports.concatUint8Arrays = exports.getRootCause = exports.getErrorChain = exports.errorToJSON = exports.toError = exports.BaseError = exports.getSupportedFormats = exports.DateFormatter = exports.DateParser = exports.xmlDecode = exports.xmlEncode = exports.uint8ArrayToBase64 = exports.base64ToUint8Array = exports.excelToDate = exports.dateToExcel = exports.encodeRange = exports.decodeRange = exports.encodeCell = exports.decodeCell = exports.encodeRow = exports.decodeRow = exports.encodeCol = exports.decodeCol = exports.DefinedNames = exports.createCsvFormatterStream = exports.createCsvParserStream = exports.CsvFormatterStream = exports.CsvParserStream = exports.createTextWatermarkImage = exports.WorksheetReader = exports.WorksheetWriter = exports.WorkbookReader = exports.WorkbookWriter = exports.FormCheckbox = exports.DataValidations = exports.Table = exports.Image = exports.Range = exports.Cell = exports.Column = exports.Row = exports.Worksheet = exports.Workbook = void 0;
21
+ exports.MarkdownParseError = exports.MarkdownError = exports.MaxItemsExceededError = exports.ImageError = exports.TableError = exports.PivotTableError = exports.WorksheetNameError = exports.XmlParseError = exports.InvalidValueTypeError = exports.MergeConflictError = exports.RowOutOfBoundsError = exports.ColumnOutOfBoundsError = exports.InvalidAddressError = exports.ExcelStreamStateError = exports.ExcelNotSupportedError = exports.ExcelDownloadError = exports.ExcelFileError = exports.isExcelError = exports.ExcelError = exports.isPdfError = exports.PdfStructureError = exports.PdfFontError = exports.PdfRenderError = void 0;
22
22
  var workbook_1 = require("./modules/excel/workbook.js");
23
23
  Object.defineProperty(exports, "Workbook", { enumerable: true, get: function () { return workbook_1.Workbook; } });
24
24
  var worksheet_1 = require("./modules/excel/worksheet.js");
@@ -61,6 +61,9 @@ __exportStar(require("./modules/excel/enums.js"), exports);
61
61
  // =============================================================================
62
62
  // Export all type definitions from types.ts
63
63
  __exportStar(require("./modules/excel/types.js"), exports);
64
+ // Watermark image generator utility
65
+ var watermark_image_1 = require("./modules/excel/utils/watermark-image.js");
66
+ Object.defineProperty(exports, "createTextWatermarkImage", { enumerable: true, get: function () { return watermark_image_1.createTextWatermarkImage; } });
64
67
  var stream_1 = require("./modules/csv/stream/index.js");
65
68
  Object.defineProperty(exports, "CsvParserStream", { enumerable: true, get: function () { return stream_1.CsvParserStream; } });
66
69
  Object.defineProperty(exports, "CsvFormatterStream", { enumerable: true, get: function () { return stream_1.CsvFormatterStream; } });
@@ -18,6 +18,20 @@ class Image {
18
18
  type: this.type,
19
19
  imageId: this.imageId ?? ""
20
20
  };
21
+ case "watermark":
22
+ return {
23
+ type: this.type,
24
+ imageId: this.imageId ?? "",
25
+ opacity: this.opacity
26
+ };
27
+ case "headerImage":
28
+ return {
29
+ type: this.type,
30
+ imageId: this.imageId ?? "",
31
+ headerWidth: this.headerWidth,
32
+ headerHeight: this.headerHeight,
33
+ applyTo: this.applyTo
34
+ };
21
35
  case "image": {
22
36
  const range = this.range;
23
37
  if (!range) {
@@ -39,9 +53,13 @@ class Image {
39
53
  throw new errors_1.ImageError("Invalid Image Type");
40
54
  }
41
55
  }
42
- set model({ type, imageId, range, hyperlinks }) {
56
+ set model({ type, imageId, range, hyperlinks, opacity, headerWidth, headerHeight, applyTo }) {
43
57
  this.type = type;
44
58
  this.imageId = imageId;
59
+ this.opacity = opacity;
60
+ this.headerWidth = headerWidth;
61
+ this.headerHeight = headerHeight;
62
+ this.applyTo = applyTo;
45
63
  if (type === "image") {
46
64
  if (typeof range === "string") {
47
65
  const decoded = col_cache_1.colCache.decode(range);
@@ -70,6 +88,10 @@ class Image {
70
88
  const cloned = new Image(target);
71
89
  cloned.type = this.type;
72
90
  cloned.imageId = this.imageId;
91
+ cloned.opacity = this.opacity;
92
+ cloned.headerWidth = this.headerWidth;
93
+ cloned.headerHeight = this.headerHeight;
94
+ cloned.applyTo = this.applyTo;
73
95
  if (this.range) {
74
96
  cloned.range = {
75
97
  tl: this.range.tl.clone(target),
@@ -154,6 +154,8 @@ class WorksheetWriter {
154
154
  // auto filter
155
155
  this.autoFilter = options.autoFilter ?? null;
156
156
  this._media = [];
157
+ // watermark
158
+ this._watermark = null;
157
159
  // worksheet protection
158
160
  this.sheetProtection = null;
159
161
  // start writing to stream now
@@ -451,6 +453,57 @@ class WorksheetWriter {
451
453
  getImages() {
452
454
  return this._media;
453
455
  }
456
+ // =========================================================================
457
+ // Watermark
458
+ /**
459
+ * Add a watermark to the worksheet using an image from `WorkbookWriter.addImage()`.
460
+ * Supports overlay mode (DrawingML with transparency) and header mode (VML behind content).
461
+ */
462
+ addWatermark(options) {
463
+ // Remove existing watermark entries (both stored type tags)
464
+ this._media = this._media.filter(m => m._watermarkTag !== true);
465
+ const opacity = options.opacity !== undefined ? Math.max(0, Math.min(1, options.opacity)) : 0.15;
466
+ this._watermark = {
467
+ imageId: String(options.imageId),
468
+ mode: options.mode ?? "overlay",
469
+ opacity,
470
+ headerWidth: options.headerWidth,
471
+ headerHeight: options.headerHeight,
472
+ applyTo: options.applyTo
473
+ };
474
+ if (this._watermark.mode === "overlay") {
475
+ // Coverage range is computed lazily during commit() via _resolveWatermarkRange()
476
+ const entry = {
477
+ type: "image",
478
+ imageId: String(options.imageId),
479
+ range: {
480
+ tl: { nativeCol: 0, nativeColOff: 0, nativeRow: 0, nativeRowOff: 0 },
481
+ br: { nativeCol: 100, nativeColOff: 0, nativeRow: 200, nativeRowOff: 0 },
482
+ editAs: "absolute"
483
+ },
484
+ // Internal tag for dedup — not part of the WriterImageModel type
485
+ _watermarkTag: true,
486
+ opacity
487
+ };
488
+ this._media.push(entry);
489
+ }
490
+ // Note: header mode for streaming writer is limited — the VML file generation
491
+ // happens in WorkbookWriter.addWorksheets(), which handles worksheet.headerImage.
492
+ // We store the config in _watermark and it's picked up by the commit path.
493
+ }
494
+ /**
495
+ * Get the current watermark configuration.
496
+ */
497
+ getWatermark() {
498
+ return this._watermark;
499
+ }
500
+ /**
501
+ * Remove the watermark from the worksheet.
502
+ */
503
+ removeWatermark() {
504
+ this._watermark = null;
505
+ this._media = this._media.filter(m => m._watermarkTag !== true);
506
+ }
454
507
  /**
455
508
  * Parse the user-supplied range into a normalised internal model
456
509
  * mirroring what the regular Worksheet / Image class does.
@@ -629,6 +682,21 @@ class WorksheetWriter {
629
682
  if (this._media.length === 0) {
630
683
  return;
631
684
  }
685
+ // Resolve watermark coverage range from actual worksheet dimensions
686
+ // (at commit time, all rows have been flushed so _dimensions is accurate)
687
+ for (const entry of this._media) {
688
+ if (entry._watermarkTag) {
689
+ const dims = this._dimensions.model;
690
+ const maxCol = dims ? Math.max(dims.right ?? 100, 100) : 100;
691
+ const maxRow = dims ? Math.max(dims.bottom ?? 200, 200) : 200;
692
+ entry.range.br = {
693
+ nativeCol: maxCol,
694
+ nativeColOff: 0,
695
+ nativeRow: maxRow,
696
+ nativeRowOff: 0
697
+ };
698
+ }
699
+ }
632
700
  // Build the drawing model from the stored images.
633
701
  // The drawing XML will be generated later by WorkbookWriterBase.addDrawings().
634
702
  const drawingName = `drawing${this.id}`;
@@ -69,6 +69,11 @@ function buildDrawingAnchorsAndRels(media, existingRels, options) {
69
69
  },
70
70
  range: medium.range
71
71
  };
72
+ // Pass through watermark opacity as alphaModFix
73
+ if (medium.opacity !== undefined) {
74
+ const clamped = Math.max(0, Math.min(1, medium.opacity));
75
+ anchor.picture.alphaModFix = Math.round(clamped * 100000);
76
+ }
72
77
  // Handle image hyperlinks
73
78
  if (medium.hyperlinks && medium.hyperlinks.hyperlink) {
74
79
  const rIdHyperlink = options.nextRId(rels);