@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
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @cj-tech-master/excelts v9.0.0
2
+ * @cj-tech-master/excelts v9.1.0
3
3
  * Zero-dependency TypeScript toolkit — Excel (XLSX), PDF, CSV, Markdown, XML, ZIP/TAR, and streaming.
4
4
  * (c) 2026 cjnoname
5
5
  * Released under the MIT License
@@ -2590,6 +2590,18 @@ var ExcelTS = (function(exports) {
2590
2590
  type: this.type,
2591
2591
  imageId: this.imageId ?? ""
2592
2592
  };
2593
+ case "watermark": return {
2594
+ type: this.type,
2595
+ imageId: this.imageId ?? "",
2596
+ opacity: this.opacity
2597
+ };
2598
+ case "headerImage": return {
2599
+ type: this.type,
2600
+ imageId: this.imageId ?? "",
2601
+ headerWidth: this.headerWidth,
2602
+ headerHeight: this.headerHeight,
2603
+ applyTo: this.applyTo
2604
+ };
2593
2605
  case "image": {
2594
2606
  const range = this.range;
2595
2607
  if (!range) throw new ImageError("Image has no range");
@@ -2608,9 +2620,13 @@ var ExcelTS = (function(exports) {
2608
2620
  default: throw new ImageError("Invalid Image Type");
2609
2621
  }
2610
2622
  }
2611
- set model({ type, imageId, range, hyperlinks }) {
2623
+ set model({ type, imageId, range, hyperlinks, opacity, headerWidth, headerHeight, applyTo }) {
2612
2624
  this.type = type;
2613
2625
  this.imageId = imageId;
2626
+ this.opacity = opacity;
2627
+ this.headerWidth = headerWidth;
2628
+ this.headerHeight = headerHeight;
2629
+ this.applyTo = applyTo;
2614
2630
  if (type === "image") {
2615
2631
  if (typeof range === "string") {
2616
2632
  const decoded = colCache.decode(range);
@@ -2639,6 +2655,10 @@ var ExcelTS = (function(exports) {
2639
2655
  const cloned = new Image(target);
2640
2656
  cloned.type = this.type;
2641
2657
  cloned.imageId = this.imageId;
2658
+ cloned.opacity = this.opacity;
2659
+ cloned.headerWidth = this.headerWidth;
2660
+ cloned.headerHeight = this.headerHeight;
2661
+ cloned.applyTo = this.applyTo;
2642
2662
  if (this.range) cloned.range = {
2643
2663
  tl: this.range.tl.clone(target),
2644
2664
  br: this.range.br ? this.range.br.clone(target) : void 0,
@@ -9693,6 +9713,7 @@ var ExcelTS = (function(exports) {
9693
9713
  this.pivotTables = [];
9694
9714
  this.conditionalFormattings = [];
9695
9715
  this.formControls = [];
9716
+ this._watermark = null;
9696
9717
  }
9697
9718
  get name() {
9698
9719
  return this._name;
@@ -10276,6 +10297,73 @@ var ExcelTS = (function(exports) {
10276
10297
  return image && image.imageId;
10277
10298
  }
10278
10299
  /**
10300
+ * Add a watermark to the worksheet using an image from `workbook.addImage()`.
10301
+ *
10302
+ * The watermark can be placed in one of two modes:
10303
+ *
10304
+ * - **overlay** (default): Places the watermark image as a drawing on top of cells.
10305
+ * Visible on screen AND when printed. Supports transparency via DrawingML `alphaModFix`.
10306
+ *
10307
+ * - **header**: Places the watermark image in the page header using VML.
10308
+ * Visible in Page Layout view and when printed. Renders behind cell content.
10309
+ * Transparency must be baked into the image (PNG with alpha channel).
10310
+ *
10311
+ * @param options - Watermark configuration
10312
+ *
10313
+ * @example Overlay watermark with transparency:
10314
+ * ```typescript
10315
+ * const imgId = workbook.addImage({ buffer: pngData, extension: "png" });
10316
+ * worksheet.addWatermark({ imageId: imgId, opacity: 0.15 });
10317
+ * ```
10318
+ *
10319
+ * @example Header watermark (behind content):
10320
+ * ```typescript
10321
+ * const imgId = workbook.addImage({ buffer: pngData, extension: "png" });
10322
+ * worksheet.addWatermark({ imageId: imgId, mode: "header" });
10323
+ * ```
10324
+ */
10325
+ addWatermark(options) {
10326
+ this._media = this._media.filter((m) => m.type !== "watermark" && m.type !== "headerImage");
10327
+ this._watermark = {
10328
+ imageId: String(options.imageId),
10329
+ mode: options.mode ?? "overlay",
10330
+ opacity: options.opacity,
10331
+ headerWidth: options.headerWidth,
10332
+ headerHeight: options.headerHeight,
10333
+ applyTo: options.applyTo
10334
+ };
10335
+ if (this._watermark.mode === "overlay") {
10336
+ const model = {
10337
+ type: "watermark",
10338
+ imageId: String(options.imageId),
10339
+ opacity: options.opacity
10340
+ };
10341
+ this._media.push(new Image(this, model));
10342
+ } else {
10343
+ const model = {
10344
+ type: "headerImage",
10345
+ imageId: String(options.imageId),
10346
+ headerWidth: options.headerWidth,
10347
+ headerHeight: options.headerHeight,
10348
+ applyTo: options.applyTo
10349
+ };
10350
+ this._media.push(new Image(this, model));
10351
+ }
10352
+ }
10353
+ /**
10354
+ * Get the current watermark configuration, or null if none is set.
10355
+ */
10356
+ getWatermark() {
10357
+ return this._watermark;
10358
+ }
10359
+ /**
10360
+ * Remove the watermark from the worksheet.
10361
+ */
10362
+ removeWatermark() {
10363
+ this._watermark = null;
10364
+ this._media = this._media.filter((m) => m.type !== "watermark" && m.type !== "headerImage");
10365
+ }
10366
+ /**
10279
10367
  * Add a form control checkbox to the worksheet.
10280
10368
  *
10281
10369
  * Form control checkboxes are the legacy style that work in Office 2007+,
@@ -10511,6 +10599,7 @@ var ExcelTS = (function(exports) {
10511
10599
  pivotTables: this.pivotTables,
10512
10600
  conditionalFormattings: this.conditionalFormattings,
10513
10601
  formControls: this.formControls.map((fc) => fc.model),
10602
+ watermark: this._watermark,
10514
10603
  drawing: this._drawing
10515
10604
  };
10516
10605
  model.cols = Column.toModel(this.columns);
@@ -10554,6 +10643,25 @@ var ExcelTS = (function(exports) {
10554
10643
  this.views = value.views;
10555
10644
  this.autoFilter = value.autoFilter;
10556
10645
  this._media = value.media.map((medium) => new Image(this, medium));
10646
+ this._watermark = value.watermark ?? null;
10647
+ if (!this._watermark) {
10648
+ for (const medium of this._media) if (medium.type === "watermark") {
10649
+ this._watermark = {
10650
+ imageId: medium.imageId ?? "",
10651
+ mode: "overlay",
10652
+ opacity: medium.opacity
10653
+ };
10654
+ break;
10655
+ } else if (medium.type === "headerImage") {
10656
+ this._watermark = {
10657
+ imageId: medium.imageId ?? "",
10658
+ mode: "header",
10659
+ headerWidth: medium.headerWidth,
10660
+ headerHeight: medium.headerHeight
10661
+ };
10662
+ break;
10663
+ }
10664
+ }
10557
10665
  this.sheetProtection = value.sheetProtection;
10558
10666
  this.tables = value.tables.reduce((tables, table) => {
10559
10667
  const t = new Table(this, table);
@@ -15419,6 +15527,7 @@ var ExcelTS = (function(exports) {
15419
15527
  const drawingXmlRegex = /^xl\/drawings\/(drawing\d+)[.]xml$/;
15420
15528
  const drawingRelsXmlRegex = /^xl\/drawings\/_rels\/(drawing\d+)[.]xml[.]rels$/;
15421
15529
  const vmlDrawingRegex = /^xl\/drawings\/(vmlDrawing\d+)[.]vml$/;
15530
+ const vmlDrawingHFRegex = /^xl\/drawings\/(vmlDrawingHF\d+)[.]vml$/;
15422
15531
  const commentsXmlRegex = /^xl\/comments(\d+)[.]xml$/;
15423
15532
  const tableXmlRegex = /^xl\/tables\/(table\d+)[.]xml$/;
15424
15533
  const pivotTableXmlRegex = /^xl\/pivotTables\/(pivotTable\d+)[.]xml$/;
@@ -15468,6 +15577,10 @@ var ExcelTS = (function(exports) {
15468
15577
  const match = vmlDrawingRegex.exec(path);
15469
15578
  return match ? match[1] : void 0;
15470
15579
  }
15580
+ function getVmlDrawingHFNameFromPath(path) {
15581
+ const match = vmlDrawingHFRegex.exec(path);
15582
+ return match ? match[1] : void 0;
15583
+ }
15471
15584
  function getCommentsIndexFromPath(path) {
15472
15585
  const match = commentsXmlRegex.exec(path);
15473
15586
  return match ? match[1] : void 0;
@@ -15523,6 +15636,12 @@ var ExcelTS = (function(exports) {
15523
15636
  function vmlDrawingPath(sheetId) {
15524
15637
  return `xl/drawings/vmlDrawing${sheetId}.vml`;
15525
15638
  }
15639
+ function vmlDrawingHFPath(sheetId) {
15640
+ return `xl/drawings/vmlDrawingHF${sheetId}.vml`;
15641
+ }
15642
+ function vmlDrawingHFRelsPath(sheetId) {
15643
+ return `xl/drawings/_rels/vmlDrawingHF${sheetId}.vml.rels`;
15644
+ }
15526
15645
  function tablePath(target) {
15527
15646
  return `xl/tables/${target}`;
15528
15647
  }
@@ -15568,6 +15687,9 @@ var ExcelTS = (function(exports) {
15568
15687
  function vmlDrawingRelTargetFromWorksheet(sheetId) {
15569
15688
  return `../drawings/vmlDrawing${sheetId}.vml`;
15570
15689
  }
15690
+ function vmlDrawingHFRelTargetFromWorksheet(sheetId) {
15691
+ return `../drawings/vmlDrawingHF${sheetId}.vml`;
15692
+ }
15571
15693
  function drawingRelTargetFromWorksheet(drawingName) {
15572
15694
  return `../drawings/${drawingName}.xml`;
15573
15695
  }
@@ -15687,7 +15809,8 @@ var ExcelTS = (function(exports) {
15687
15809
  });
15688
15810
  const hasComments = model.commentRefs && model.commentRefs.length > 0;
15689
15811
  const hasFormControls = model.formControlRefs && model.formControlRefs.length > 0;
15690
- if (hasComments || hasFormControls) xmlStream.leafNode("Default", {
15812
+ const hasHeaderWatermark = model.hasHeaderWatermark === true;
15813
+ if (hasComments || hasFormControls || hasHeaderWatermark) xmlStream.leafNode("Default", {
15691
15814
  Extension: "vml",
15692
15815
  ContentType: "application/vnd.openxmlformats-officedocument.vmlDrawing"
15693
15816
  });
@@ -19025,6 +19148,10 @@ var ExcelTS = (function(exports) {
19025
19148
  picture: { rId: rIdImage },
19026
19149
  range: medium.range
19027
19150
  };
19151
+ if (medium.opacity !== void 0) {
19152
+ const clamped = Math.max(0, Math.min(1, medium.opacity));
19153
+ anchor.picture.alphaModFix = Math.round(clamped * 1e5);
19154
+ }
19028
19155
  if (medium.hyperlinks && medium.hyperlinks.hyperlink) {
19029
19156
  const rIdHyperlink = options.nextRId(rels);
19030
19157
  anchor.picture.hyperlinks = {
@@ -19225,9 +19352,13 @@ var ExcelTS = (function(exports) {
19225
19352
  }
19226
19353
  const backgroundMedia = [];
19227
19354
  const imageMedia = [];
19355
+ const watermarkMedia = [];
19356
+ const headerImageMedia = [];
19228
19357
  model.media.forEach((medium) => {
19229
19358
  if (medium.type === "background") backgroundMedia.push(medium);
19230
19359
  else if (medium.type === "image") imageMedia.push(medium);
19360
+ else if (medium.type === "watermark") watermarkMedia.push(medium);
19361
+ else if (medium.type === "headerImage") headerImageMedia.push(medium);
19231
19362
  });
19232
19363
  backgroundMedia.forEach((medium) => {
19233
19364
  const rId = nextRid(rels);
@@ -19263,6 +19394,96 @@ var ExcelTS = (function(exports) {
19263
19394
  drawing.anchors.push(...result.anchors);
19264
19395
  drawing.rels = result.rels;
19265
19396
  }
19397
+ if (watermarkMedia.length > 0) {
19398
+ let { drawing } = model;
19399
+ if (!drawing) {
19400
+ drawing = model.drawing = {
19401
+ rId: nextRid(rels),
19402
+ name: `drawing${++options.drawingsCount}`,
19403
+ anchors: [],
19404
+ rels: []
19405
+ };
19406
+ options.drawings.push(drawing);
19407
+ rels.push({
19408
+ Id: drawing.rId,
19409
+ Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
19410
+ Target: drawingRelTargetFromWorksheet(drawing.name)
19411
+ });
19412
+ }
19413
+ for (const medium of watermarkMedia) {
19414
+ const bookImage = options.media[medium.imageId];
19415
+ if (!bookImage) continue;
19416
+ const rIdImage = nextRid(drawing.rels);
19417
+ drawing.rels.push({
19418
+ Id: rIdImage,
19419
+ Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
19420
+ Target: resolveMediaTarget(bookImage)
19421
+ });
19422
+ const rawOpacity = medium.opacity !== void 0 ? medium.opacity : .15;
19423
+ const alphaModFix = Math.round(Math.max(0, Math.min(1, rawOpacity)) * 1e5);
19424
+ const dims = model.dimensions;
19425
+ const maxCol = dims ? Math.max(dims.model?.right ?? 100, 100) : 100;
19426
+ const maxRow = dims ? Math.max(dims.model?.bottom ?? 200, 200) : 200;
19427
+ drawing.anchors.push({
19428
+ picture: {
19429
+ rId: rIdImage,
19430
+ alphaModFix
19431
+ },
19432
+ range: {
19433
+ editAs: "absolute",
19434
+ tl: {
19435
+ nativeCol: 0,
19436
+ nativeColOff: 0,
19437
+ nativeRow: 0,
19438
+ nativeRowOff: 0
19439
+ },
19440
+ br: {
19441
+ nativeCol: maxCol,
19442
+ nativeColOff: 0,
19443
+ nativeRow: maxRow,
19444
+ nativeRowOff: 0
19445
+ }
19446
+ }
19447
+ });
19448
+ }
19449
+ }
19450
+ if (headerImageMedia.length > 0) {
19451
+ const medium = headerImageMedia[0];
19452
+ const bookImage = options.media[medium.imageId];
19453
+ if (bookImage) {
19454
+ const rIdVml = nextRid(rels);
19455
+ rels.push({
19456
+ Id: rIdVml,
19457
+ Type: RelType.VmlDrawing,
19458
+ Target: vmlDrawingHFRelTargetFromWorksheet(fileIndex)
19459
+ });
19460
+ model.headerImage = {
19461
+ vmlRelId: rIdVml,
19462
+ imageId: medium.imageId,
19463
+ bookImage,
19464
+ headerWidth: medium.headerWidth,
19465
+ headerHeight: medium.headerHeight
19466
+ };
19467
+ options.hasHeaderWatermark = true;
19468
+ if (!model.headerFooter) model.headerFooter = {};
19469
+ const applyTo = medium.applyTo || "all";
19470
+ const insertG = (field) => {
19471
+ const existing = model.headerFooter[field] || "";
19472
+ if (existing.includes("&G")) return existing;
19473
+ if (existing.includes("&C")) return existing.replace("&C", "&C&G");
19474
+ return existing + "&C&G";
19475
+ };
19476
+ if (applyTo === "all" || applyTo === "odd") model.headerFooter.oddHeader = insertG("oddHeader");
19477
+ if (applyTo === "all" || applyTo === "even") {
19478
+ model.headerFooter.evenHeader = insertG("evenHeader");
19479
+ model.headerFooter.differentOddEven = true;
19480
+ }
19481
+ if (applyTo === "all" || applyTo === "first") {
19482
+ model.headerFooter.firstHeader = insertG("firstHeader");
19483
+ model.headerFooter.differentFirst = true;
19484
+ }
19485
+ }
19486
+ }
19266
19487
  model.tables.forEach((table) => {
19267
19488
  const rId = nextRid(rels);
19268
19489
  table.rId = rId;
@@ -19392,8 +19613,12 @@ var ExcelTS = (function(exports) {
19392
19613
  this.map.drawing.render(xmlStream, model.drawing);
19393
19614
  this.map.picture.render(xmlStream, model.background);
19394
19615
  if (model.rels) model.rels.forEach((rel) => {
19395
- if (rel.Type === RelType.VmlDrawing) xmlStream.leafNode("legacyDrawing", { "r:id": rel.Id });
19616
+ if (rel.Type === RelType.VmlDrawing) {
19617
+ if (model.headerImage && rel.Id === model.headerImage.vmlRelId) return;
19618
+ xmlStream.leafNode("legacyDrawing", { "r:id": rel.Id });
19619
+ }
19396
19620
  });
19621
+ if (model.headerImage) xmlStream.leafNode("legacyDrawingHF", { "r:id": model.headerImage.vmlRelId });
19397
19622
  if (model.formControls && model.formControls.length > 0) {
19398
19623
  xmlStream.openNode("mc:AlternateContent");
19399
19624
  xmlStream.openNode("mc:Choice", { Requires: "x14" });
@@ -19561,15 +19786,17 @@ var ExcelTS = (function(exports) {
19561
19786
  rels: options.drawingRels?.[drawingName] ?? drawing.rels ?? []
19562
19787
  };
19563
19788
  drawing.anchors.forEach((anchor) => {
19564
- if (anchor.medium) {
19565
- const image = {
19566
- type: "image",
19567
- imageId: anchor.medium.index,
19568
- range: anchor.range,
19569
- hyperlinks: anchor.picture.hyperlinks
19570
- };
19571
- model.media.push(image);
19572
- }
19789
+ if (anchor.medium) if (anchor.medium.alphaModFix !== void 0 && anchor.medium.alphaModFix < 1e5) model.media.push({
19790
+ type: "watermark",
19791
+ imageId: anchor.medium.index,
19792
+ opacity: anchor.medium.alphaModFix / 1e5
19793
+ });
19794
+ else model.media.push({
19795
+ type: "image",
19796
+ imageId: anchor.medium.index,
19797
+ range: anchor.range,
19798
+ hyperlinks: anchor.picture.hyperlinks
19799
+ });
19573
19800
  });
19574
19801
  } else model.drawing = void 0;
19575
19802
  } else model.drawing = void 0;
@@ -19668,7 +19895,12 @@ var ExcelTS = (function(exports) {
19668
19895
  if (match) {
19669
19896
  const name = match[1];
19670
19897
  const mediaId = options.mediaIndex[name];
19671
- return options.media[mediaId];
19898
+ const medium = options.media[mediaId];
19899
+ if (medium && model.alphaModFix !== void 0) return {
19900
+ ...medium,
19901
+ alphaModFix: model.alphaModFix
19902
+ };
19903
+ return medium;
19672
19904
  }
19673
19905
  }
19674
19906
  }
@@ -19760,7 +19992,15 @@ var ExcelTS = (function(exports) {
19760
19992
  return "a:blip";
19761
19993
  }
19762
19994
  render(xmlStream, model) {
19763
- xmlStream.leafNode(this.tag, {
19995
+ if (model.alphaModFix !== void 0 && model.alphaModFix < 1e5) {
19996
+ xmlStream.openNode(this.tag, {
19997
+ "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
19998
+ "r:embed": model.rId,
19999
+ cstate: "print"
20000
+ });
20001
+ xmlStream.leafNode("a:alphaModFix", { amt: String(model.alphaModFix) });
20002
+ xmlStream.closeNode();
20003
+ } else xmlStream.leafNode(this.tag, {
19764
20004
  "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
19765
20005
  "r:embed": model.rId,
19766
20006
  cstate: "print"
@@ -19771,6 +20011,9 @@ var ExcelTS = (function(exports) {
19771
20011
  case this.tag:
19772
20012
  this.model = { rId: node.attributes["r:embed"] };
19773
20013
  return true;
20014
+ case "a:alphaModFix":
20015
+ if (node.attributes.amt) this.model.alphaModFix = parseInt(node.attributes.amt, 10);
20016
+ return true;
19774
20017
  default: return true;
19775
20018
  }
19776
20019
  }
@@ -20065,7 +20308,10 @@ var ExcelTS = (function(exports) {
20065
20308
  render(xmlStream, model) {
20066
20309
  xmlStream.openNode(this.tag);
20067
20310
  this.map["xdr:nvPicPr"].render(xmlStream, model);
20068
- this.map["xdr:blipFill"].render(xmlStream, model);
20311
+ this.map["xdr:blipFill"].render(xmlStream, {
20312
+ rId: model.rId,
20313
+ alphaModFix: model.alphaModFix
20314
+ });
20069
20315
  this.map["xdr:spPr"].render(xmlStream, model);
20070
20316
  xmlStream.closeNode();
20071
20317
  }
@@ -22980,6 +23226,7 @@ var ExcelTS = (function(exports) {
22980
23226
  var VmlDrawingXform = class VmlDrawingXform extends BaseXform {
22981
23227
  constructor() {
22982
23228
  super();
23229
+ this._parsingHeaderImage = false;
22983
23230
  this.map = { "v:shape": new VmlShapeXform() };
22984
23231
  this.model = {
22985
23232
  comments: [],
@@ -22996,8 +23243,10 @@ var ExcelTS = (function(exports) {
22996
23243
  const renderModel = model || this.model;
22997
23244
  const comments = renderModel.comments;
22998
23245
  const formControls = renderModel.formControls;
23246
+ const headerImage = renderModel.headerImage;
22999
23247
  const hasComments = comments && comments.length > 0;
23000
23248
  const hasFormControls = formControls && formControls.length > 0;
23249
+ const hasHeaderImage = !!headerImage;
23001
23250
  xmlStream.openXml(StdDocAttributes);
23002
23251
  xmlStream.openNode(this.tag, VmlDrawingXform.DRAWING_ATTRIBUTES);
23003
23252
  xmlStream.openNode("o:shapelayout", { "v:ext": "edit" });
@@ -23041,8 +23290,67 @@ var ExcelTS = (function(exports) {
23041
23290
  });
23042
23291
  xmlStream.closeNode();
23043
23292
  }
23293
+ if (hasHeaderImage) {
23294
+ xmlStream.openNode("v:shapetype", {
23295
+ id: "_x0000_t75",
23296
+ coordsize: "21600,21600",
23297
+ "o:spt": "75",
23298
+ "o:preferrelative": "t",
23299
+ path: "m@4@5l@4@11@9@11@9@5xe",
23300
+ filled: "f",
23301
+ stroked: "f"
23302
+ });
23303
+ xmlStream.leafNode("v:stroke", { joinstyle: "miter" });
23304
+ xmlStream.openNode("v:formulas");
23305
+ xmlStream.leafNode("v:f", { eqn: "if lineDrawn pixelLineWidth 0" });
23306
+ xmlStream.leafNode("v:f", { eqn: "sum @0 1 0" });
23307
+ xmlStream.leafNode("v:f", { eqn: "sum 0 0 @1" });
23308
+ xmlStream.leafNode("v:f", { eqn: "prod @2 1 2" });
23309
+ xmlStream.leafNode("v:f", { eqn: "prod @3 21600 pixelWidth" });
23310
+ xmlStream.leafNode("v:f", { eqn: "prod @3 21600 pixelHeight" });
23311
+ xmlStream.leafNode("v:f", { eqn: "sum @0 0 1" });
23312
+ xmlStream.leafNode("v:f", { eqn: "prod @6 1 2" });
23313
+ xmlStream.leafNode("v:f", { eqn: "prod @7 21600 pixelWidth" });
23314
+ xmlStream.leafNode("v:f", { eqn: "sum @8 21600 0" });
23315
+ xmlStream.leafNode("v:f", { eqn: "prod @7 21600 pixelHeight" });
23316
+ xmlStream.leafNode("v:f", { eqn: "sum @10 21600 0" });
23317
+ xmlStream.closeNode();
23318
+ xmlStream.leafNode("v:path", {
23319
+ "o:extrusionok": "f",
23320
+ gradientshapeok: "t",
23321
+ "o:connecttype": "rect"
23322
+ });
23323
+ xmlStream.leafNode("o:lock", {
23324
+ "v:ext": "edit",
23325
+ aspectratio: "t"
23326
+ });
23327
+ xmlStream.closeNode();
23328
+ }
23044
23329
  if (hasComments) for (let i = 0; i < comments.length; i++) this.map["v:shape"].render(xmlStream, comments[i], i);
23045
23330
  if (hasFormControls) for (const control of formControls) this._renderCheckboxShape(xmlStream, control);
23331
+ if (hasHeaderImage) this._renderHeaderImageShape(xmlStream, headerImage);
23332
+ xmlStream.closeNode();
23333
+ }
23334
+ /**
23335
+ * Render a header/footer image shape for watermark
23336
+ */
23337
+ _renderHeaderImageShape(xmlStream, headerImage) {
23338
+ const width = headerImage.width ?? 467.25;
23339
+ const height = headerImage.height ?? 311.25;
23340
+ xmlStream.openNode("v:shape", {
23341
+ id: "CH",
23342
+ "o:spid": "_x0000_s2049",
23343
+ type: "#_x0000_t75",
23344
+ style: `position:absolute;margin-left:0;margin-top:0;width:${width}pt;height:${height}pt;z-index:1`
23345
+ });
23346
+ xmlStream.leafNode("v:imagedata", {
23347
+ "o:relid": headerImage.imageRelId,
23348
+ "o:title": "watermark"
23349
+ });
23350
+ xmlStream.leafNode("o:lock", {
23351
+ "v:ext": "edit",
23352
+ rotation: "t"
23353
+ });
23046
23354
  xmlStream.closeNode();
23047
23355
  }
23048
23356
  /**
@@ -23116,9 +23424,25 @@ var ExcelTS = (function(exports) {
23116
23424
  formControls: []
23117
23425
  };
23118
23426
  break;
23427
+ case "v:shape":
23428
+ if (node.attributes.type === "#_x0000_t75") {
23429
+ this._parsingHeaderImage = true;
23430
+ const style = node.attributes.style || "";
23431
+ const widthMatch = /width:([0-9.]+)pt/.exec(style);
23432
+ const heightMatch = /height:([0-9.]+)pt/.exec(style);
23433
+ this._headerImageWidth = widthMatch ? parseFloat(widthMatch[1]) : void 0;
23434
+ this._headerImageHeight = heightMatch ? parseFloat(heightMatch[1]) : void 0;
23435
+ } else {
23436
+ this.parser = this.map[node.name];
23437
+ if (this.parser) this.parser.parseOpen(node);
23438
+ }
23439
+ break;
23119
23440
  default:
23120
- this.parser = this.map[node.name];
23121
- if (this.parser) this.parser.parseOpen(node);
23441
+ if (this._parsingHeaderImage && node.name === "v:imagedata") this._headerImageRelId = node.attributes["o:relid"];
23442
+ else {
23443
+ this.parser = this.map[node.name];
23444
+ if (this.parser) this.parser.parseOpen(node);
23445
+ }
23122
23446
  break;
23123
23447
  }
23124
23448
  return true;
@@ -23135,6 +23459,17 @@ var ExcelTS = (function(exports) {
23135
23459
  return true;
23136
23460
  }
23137
23461
  switch (name) {
23462
+ case "v:shape":
23463
+ if (this._parsingHeaderImage && this._headerImageRelId) this.model.headerImage = {
23464
+ imageRelId: this._headerImageRelId,
23465
+ width: this._headerImageWidth,
23466
+ height: this._headerImageHeight
23467
+ };
23468
+ this._parsingHeaderImage = false;
23469
+ this._headerImageRelId = void 0;
23470
+ this._headerImageWidth = void 0;
23471
+ this._headerImageHeight = void 0;
23472
+ return true;
23138
23473
  case this.tag: return false;
23139
23474
  default: return true;
23140
23475
  }
@@ -25658,7 +25993,7 @@ var ExcelTS = (function(exports) {
25658
25993
  * @param data - Input data
25659
25994
  * @returns 32-bit Adler-32 checksum
25660
25995
  */
25661
- function adler32(data) {
25996
+ function adler32$1(data) {
25662
25997
  let a = 1;
25663
25998
  let b = 0;
25664
25999
  const chunkSize = 5552;
@@ -27200,7 +27535,7 @@ self.onmessage = async function(event) {
27200
27535
  return concatUint8Arrays([
27201
27536
  getZlibHeader(level),
27202
27537
  deflated,
27203
- buildZlibTrailer(adler32(original))
27538
+ buildZlibTrailer(adler32$1(original))
27204
27539
  ]);
27205
27540
  }
27206
27541
  /**
@@ -35661,6 +35996,13 @@ self.onmessage = async function(event) {
35661
35996
  const vmlDrawing = await new VmlDrawingXform().parseStream(entry);
35662
35997
  model.vmlDrawings[vmlDrawingRelTargetFromWorksheetName(name)] = vmlDrawing;
35663
35998
  }
35999
+ async _processVmlDrawingHFEntry(entry, model, _name) {
36000
+ const vmlDrawing = await new VmlDrawingXform().parseStream(entry);
36001
+ if (vmlDrawing && vmlDrawing.headerImage) {
36002
+ if (!model.vmlDrawingHF) model.vmlDrawingHF = {};
36003
+ model.vmlDrawingHF[_name] = vmlDrawing.headerImage;
36004
+ }
36005
+ }
35664
36006
  async _processThemeEntry(stream, model, name) {
35665
36007
  await new Promise((resolve, reject) => {
35666
36008
  const streamBuf = this.createStreamBuf();
@@ -35747,6 +36089,11 @@ self.onmessage = async function(event) {
35747
36089
  await this._processVmlDrawingEntry(stream, model, vmlDrawingName);
35748
36090
  return true;
35749
36091
  }
36092
+ const vmlHFName = getVmlDrawingHFNameFromPath(entryName);
36093
+ if (vmlHFName) {
36094
+ await this._processVmlDrawingHFEntry(stream, model, vmlHFName);
36095
+ return true;
36096
+ }
35750
36097
  const commentsIndex = getCommentsIndexFromPath(entryName);
35751
36098
  if (commentsIndex) {
35752
36099
  await this._processCommentEntry(stream, model, `comments${commentsIndex}`);
@@ -35925,6 +36272,25 @@ self.onmessage = async function(event) {
35925
36272
  comments: hasComments ? worksheet.comments : [],
35926
36273
  formControls: hasFormControls ? worksheet.formControls : []
35927
36274
  });
36275
+ if (worksheet.headerImage) {
36276
+ const hdrImage = worksheet.headerImage;
36277
+ const bookImage = hdrImage.bookImage;
36278
+ const imageRelTarget = `../media/${bookImage.name && bookImage.extension && bookImage.name.endsWith(`.${bookImage.extension}`) ? bookImage.name : `${bookImage.name}.${bookImage.extension}`}`;
36279
+ await this._renderToZip(zip, vmlDrawingHFPath(fileIndex), vmlDrawingXform, {
36280
+ comments: [],
36281
+ formControls: [],
36282
+ headerImage: {
36283
+ imageRelId: "rId1",
36284
+ width: hdrImage.headerWidth,
36285
+ height: hdrImage.headerHeight
36286
+ }
36287
+ });
36288
+ await this._renderToZip(zip, vmlDrawingHFRelsPath(fileIndex), relationshipsXform, [{
36289
+ Id: "rId1",
36290
+ Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
36291
+ Target: imageRelTarget
36292
+ }]);
36293
+ }
35928
36294
  if (hasFormControls) for (const control of worksheet.formControls) await this._renderToZip(zip, ctrlPropPath(control.ctrlPropId), ctrlPropXform, control);
35929
36295
  }
35930
36296
  }
@@ -36036,6 +36402,7 @@ self.onmessage = async function(event) {
36036
36402
  worksheetOptions.drawings = model.drawings = [];
36037
36403
  worksheetOptions.commentRefs = model.commentRefs = [];
36038
36404
  worksheetOptions.formControlRefs = model.formControlRefs = [];
36405
+ model.hasHeaderWatermark = false;
36039
36406
  let tableCount = 0;
36040
36407
  model.tables = [];
36041
36408
  model.worksheets.forEach((worksheet, index) => {
@@ -36049,6 +36416,7 @@ self.onmessage = async function(event) {
36049
36416
  worksheetXform.prepare(worksheet, worksheetOptions);
36050
36417
  });
36051
36418
  model.hasCheckboxes = model.styles.hasCheckboxes;
36419
+ if (worksheetOptions.hasHeaderWatermark) model.hasHeaderWatermark = true;
36052
36420
  const passthrough = model.passthrough || {};
36053
36421
  const passthroughManager = new PassthroughManager();
36054
36422
  passthroughManager.fromRecord(passthrough);
@@ -36347,6 +36715,7 @@ self.onmessage = async function(event) {
36347
36715
  this._views = options.views ?? [];
36348
36716
  this.autoFilter = options.autoFilter ?? null;
36349
36717
  this._media = [];
36718
+ this._watermark = null;
36350
36719
  this.sheetProtection = null;
36351
36720
  this._writeOpenWorksheet();
36352
36721
  this.startedData = false;
@@ -36550,6 +36919,59 @@ self.onmessage = async function(event) {
36550
36919
  return this._media;
36551
36920
  }
36552
36921
  /**
36922
+ * Add a watermark to the worksheet using an image from `WorkbookWriter.addImage()`.
36923
+ * Supports overlay mode (DrawingML with transparency) and header mode (VML behind content).
36924
+ */
36925
+ addWatermark(options) {
36926
+ this._media = this._media.filter((m) => m._watermarkTag !== true);
36927
+ const opacity = options.opacity !== void 0 ? Math.max(0, Math.min(1, options.opacity)) : .15;
36928
+ this._watermark = {
36929
+ imageId: String(options.imageId),
36930
+ mode: options.mode ?? "overlay",
36931
+ opacity,
36932
+ headerWidth: options.headerWidth,
36933
+ headerHeight: options.headerHeight,
36934
+ applyTo: options.applyTo
36935
+ };
36936
+ if (this._watermark.mode === "overlay") {
36937
+ const entry = {
36938
+ type: "image",
36939
+ imageId: String(options.imageId),
36940
+ range: {
36941
+ tl: {
36942
+ nativeCol: 0,
36943
+ nativeColOff: 0,
36944
+ nativeRow: 0,
36945
+ nativeRowOff: 0
36946
+ },
36947
+ br: {
36948
+ nativeCol: 100,
36949
+ nativeColOff: 0,
36950
+ nativeRow: 200,
36951
+ nativeRowOff: 0
36952
+ },
36953
+ editAs: "absolute"
36954
+ },
36955
+ _watermarkTag: true,
36956
+ opacity
36957
+ };
36958
+ this._media.push(entry);
36959
+ }
36960
+ }
36961
+ /**
36962
+ * Get the current watermark configuration.
36963
+ */
36964
+ getWatermark() {
36965
+ return this._watermark;
36966
+ }
36967
+ /**
36968
+ * Remove the watermark from the worksheet.
36969
+ */
36970
+ removeWatermark() {
36971
+ this._watermark = null;
36972
+ this._media = this._media.filter((m) => m._watermarkTag !== true);
36973
+ }
36974
+ /**
36553
36975
  * Parse the user-supplied range into a normalised internal model
36554
36976
  * mirroring what the regular Worksheet / Image class does.
36555
36977
  */
@@ -36707,6 +37129,17 @@ self.onmessage = async function(event) {
36707
37129
  }
36708
37130
  _writeDrawing() {
36709
37131
  if (this._media.length === 0) return;
37132
+ for (const entry of this._media) if (entry._watermarkTag) {
37133
+ const dims = this._dimensions.model;
37134
+ const maxCol = dims ? Math.max(dims.right ?? 100, 100) : 100;
37135
+ const maxRow = dims ? Math.max(dims.bottom ?? 200, 200) : 200;
37136
+ entry.range.br = {
37137
+ nativeCol: maxCol,
37138
+ nativeColOff: 0,
37139
+ nativeRow: maxRow,
37140
+ nativeRowOff: 0
37141
+ };
37142
+ }
36710
37143
  const drawingName = `drawing${this.id}`;
36711
37144
  const drawingRId = this._sheetRelsWriter.addRelationship({
36712
37145
  Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
@@ -44520,6 +44953,1198 @@ onmessage = async (ev) => {
44520
44953
  return PaperSize;
44521
44954
  }({});
44522
44955
  //#endregion
44956
+ //#region src/modules/excel/utils/watermark-image.ts
44957
+ /**
44958
+ * Zero-dependency text-to-PNG watermark image generator.
44959
+ *
44960
+ * Renders text into a semi-transparent PNG suitable for use as an Excel watermark.
44961
+ * Uses a built-in bitmap font for ASCII characters — no Canvas or external fonts required.
44962
+ * PNG data is deflate-compressed using the archive module's built-in compressor.
44963
+ *
44964
+ * @example
44965
+ * ```typescript
44966
+ * const png = createTextWatermarkImage("CONFIDENTIAL", {
44967
+ * fontSize: 48,
44968
+ * color: { r: 128, g: 128, b: 128 },
44969
+ * opacity: 40,
44970
+ * rotation: -45
44971
+ * });
44972
+ * const imgId = workbook.addImage({ buffer: png, extension: "png" });
44973
+ * worksheet.addWatermark({ imageId: imgId });
44974
+ * ```
44975
+ */
44976
+ /**
44977
+ * Generate a PNG image containing watermark text.
44978
+ *
44979
+ * The image has an alpha channel so the watermark is semi-transparent.
44980
+ * Works in both Node.js and browsers with zero dependencies.
44981
+ */
44982
+ function createTextWatermarkImage(text, options) {
44983
+ const fontSize = options?.fontSize ?? 48;
44984
+ const color = options?.color ?? {
44985
+ r: 128,
44986
+ g: 128,
44987
+ b: 128
44988
+ };
44989
+ const opacity = Math.max(0, Math.min(100, options?.opacity ?? 40));
44990
+ const rotation = options?.rotation ?? -45;
44991
+ const padding = options?.padding ?? 20;
44992
+ const { width: textW, height: textH, pixels: textPixels } = renderTextBitmap(text, Math.max(1, Math.round(fontSize / GLYPH_HEIGHT)));
44993
+ const paddedW = textW + padding * 2;
44994
+ const paddedH = textH + padding * 2;
44995
+ const paddedPixels = new Uint8Array(paddedW * paddedH);
44996
+ for (let y = 0; y < textH; y++) for (let x = 0; x < textW; x++) paddedPixels[(y + padding) * paddedW + (x + padding)] = textPixels[y * textW + x];
44997
+ const { width: rotW, height: rotH, pixels: rotPixels } = rotateBitmap(paddedPixels, paddedW, paddedH, rotation);
44998
+ const alpha = Math.round(opacity / 100 * 255);
44999
+ const rgba = new Uint8Array(rotW * rotH * 4);
45000
+ for (let i = 0; i < rotW * rotH; i++) {
45001
+ const a = rotPixels[i];
45002
+ if (a > 0) {
45003
+ rgba[i * 4] = color.r;
45004
+ rgba[i * 4 + 1] = color.g;
45005
+ rgba[i * 4 + 2] = color.b;
45006
+ rgba[i * 4 + 3] = Math.round(a / 255 * alpha);
45007
+ }
45008
+ }
45009
+ return encodePng(rgba, rotW, rotH);
45010
+ }
45011
+ const GLYPH_WIDTH = 6;
45012
+ const GLYPH_HEIGHT = 8;
45013
+ /**
45014
+ * Compact glyph data: each character is 8 bytes (one byte per row, 6 bits used).
45015
+ * Bit 5 = leftmost pixel, bit 0 = rightmost pixel.
45016
+ */
45017
+ const FONT_DATA = {
45018
+ 32: [
45019
+ 0,
45020
+ 0,
45021
+ 0,
45022
+ 0,
45023
+ 0,
45024
+ 0,
45025
+ 0,
45026
+ 0
45027
+ ],
45028
+ 33: [
45029
+ 4,
45030
+ 4,
45031
+ 4,
45032
+ 4,
45033
+ 4,
45034
+ 0,
45035
+ 4,
45036
+ 0
45037
+ ],
45038
+ 34: [
45039
+ 10,
45040
+ 10,
45041
+ 10,
45042
+ 0,
45043
+ 0,
45044
+ 0,
45045
+ 0,
45046
+ 0
45047
+ ],
45048
+ 35: [
45049
+ 10,
45050
+ 10,
45051
+ 31,
45052
+ 10,
45053
+ 31,
45054
+ 10,
45055
+ 10,
45056
+ 0
45057
+ ],
45058
+ 36: [
45059
+ 4,
45060
+ 15,
45061
+ 20,
45062
+ 14,
45063
+ 5,
45064
+ 30,
45065
+ 4,
45066
+ 0
45067
+ ],
45068
+ 37: [
45069
+ 24,
45070
+ 25,
45071
+ 2,
45072
+ 4,
45073
+ 8,
45074
+ 19,
45075
+ 3,
45076
+ 0
45077
+ ],
45078
+ 38: [
45079
+ 12,
45080
+ 18,
45081
+ 20,
45082
+ 8,
45083
+ 21,
45084
+ 18,
45085
+ 13,
45086
+ 0
45087
+ ],
45088
+ 39: [
45089
+ 4,
45090
+ 4,
45091
+ 8,
45092
+ 0,
45093
+ 0,
45094
+ 0,
45095
+ 0,
45096
+ 0
45097
+ ],
45098
+ 40: [
45099
+ 2,
45100
+ 4,
45101
+ 8,
45102
+ 8,
45103
+ 8,
45104
+ 4,
45105
+ 2,
45106
+ 0
45107
+ ],
45108
+ 41: [
45109
+ 8,
45110
+ 4,
45111
+ 2,
45112
+ 2,
45113
+ 2,
45114
+ 4,
45115
+ 8,
45116
+ 0
45117
+ ],
45118
+ 42: [
45119
+ 0,
45120
+ 4,
45121
+ 21,
45122
+ 14,
45123
+ 21,
45124
+ 4,
45125
+ 0,
45126
+ 0
45127
+ ],
45128
+ 43: [
45129
+ 0,
45130
+ 4,
45131
+ 4,
45132
+ 31,
45133
+ 4,
45134
+ 4,
45135
+ 0,
45136
+ 0
45137
+ ],
45138
+ 44: [
45139
+ 0,
45140
+ 0,
45141
+ 0,
45142
+ 0,
45143
+ 0,
45144
+ 4,
45145
+ 4,
45146
+ 8
45147
+ ],
45148
+ 45: [
45149
+ 0,
45150
+ 0,
45151
+ 0,
45152
+ 31,
45153
+ 0,
45154
+ 0,
45155
+ 0,
45156
+ 0
45157
+ ],
45158
+ 46: [
45159
+ 0,
45160
+ 0,
45161
+ 0,
45162
+ 0,
45163
+ 0,
45164
+ 0,
45165
+ 4,
45166
+ 0
45167
+ ],
45168
+ 47: [
45169
+ 0,
45170
+ 1,
45171
+ 2,
45172
+ 4,
45173
+ 8,
45174
+ 16,
45175
+ 0,
45176
+ 0
45177
+ ],
45178
+ 48: [
45179
+ 14,
45180
+ 17,
45181
+ 19,
45182
+ 21,
45183
+ 25,
45184
+ 17,
45185
+ 14,
45186
+ 0
45187
+ ],
45188
+ 49: [
45189
+ 4,
45190
+ 12,
45191
+ 4,
45192
+ 4,
45193
+ 4,
45194
+ 4,
45195
+ 14,
45196
+ 0
45197
+ ],
45198
+ 50: [
45199
+ 14,
45200
+ 17,
45201
+ 1,
45202
+ 2,
45203
+ 4,
45204
+ 8,
45205
+ 31,
45206
+ 0
45207
+ ],
45208
+ 51: [
45209
+ 31,
45210
+ 2,
45211
+ 4,
45212
+ 2,
45213
+ 1,
45214
+ 17,
45215
+ 14,
45216
+ 0
45217
+ ],
45218
+ 52: [
45219
+ 2,
45220
+ 6,
45221
+ 10,
45222
+ 18,
45223
+ 31,
45224
+ 2,
45225
+ 2,
45226
+ 0
45227
+ ],
45228
+ 53: [
45229
+ 31,
45230
+ 16,
45231
+ 30,
45232
+ 1,
45233
+ 1,
45234
+ 17,
45235
+ 14,
45236
+ 0
45237
+ ],
45238
+ 54: [
45239
+ 6,
45240
+ 8,
45241
+ 16,
45242
+ 30,
45243
+ 17,
45244
+ 17,
45245
+ 14,
45246
+ 0
45247
+ ],
45248
+ 55: [
45249
+ 31,
45250
+ 1,
45251
+ 2,
45252
+ 4,
45253
+ 8,
45254
+ 8,
45255
+ 8,
45256
+ 0
45257
+ ],
45258
+ 56: [
45259
+ 14,
45260
+ 17,
45261
+ 17,
45262
+ 14,
45263
+ 17,
45264
+ 17,
45265
+ 14,
45266
+ 0
45267
+ ],
45268
+ 57: [
45269
+ 14,
45270
+ 17,
45271
+ 17,
45272
+ 15,
45273
+ 1,
45274
+ 2,
45275
+ 12,
45276
+ 0
45277
+ ],
45278
+ 58: [
45279
+ 0,
45280
+ 0,
45281
+ 4,
45282
+ 0,
45283
+ 0,
45284
+ 4,
45285
+ 0,
45286
+ 0
45287
+ ],
45288
+ 59: [
45289
+ 0,
45290
+ 0,
45291
+ 4,
45292
+ 0,
45293
+ 0,
45294
+ 4,
45295
+ 4,
45296
+ 8
45297
+ ],
45298
+ 60: [
45299
+ 2,
45300
+ 4,
45301
+ 8,
45302
+ 16,
45303
+ 8,
45304
+ 4,
45305
+ 2,
45306
+ 0
45307
+ ],
45308
+ 61: [
45309
+ 0,
45310
+ 0,
45311
+ 31,
45312
+ 0,
45313
+ 31,
45314
+ 0,
45315
+ 0,
45316
+ 0
45317
+ ],
45318
+ 62: [
45319
+ 8,
45320
+ 4,
45321
+ 2,
45322
+ 1,
45323
+ 2,
45324
+ 4,
45325
+ 8,
45326
+ 0
45327
+ ],
45328
+ 63: [
45329
+ 14,
45330
+ 17,
45331
+ 1,
45332
+ 2,
45333
+ 4,
45334
+ 0,
45335
+ 4,
45336
+ 0
45337
+ ],
45338
+ 64: [
45339
+ 14,
45340
+ 17,
45341
+ 23,
45342
+ 21,
45343
+ 23,
45344
+ 16,
45345
+ 14,
45346
+ 0
45347
+ ],
45348
+ 65: [
45349
+ 14,
45350
+ 17,
45351
+ 17,
45352
+ 31,
45353
+ 17,
45354
+ 17,
45355
+ 17,
45356
+ 0
45357
+ ],
45358
+ 66: [
45359
+ 30,
45360
+ 17,
45361
+ 17,
45362
+ 30,
45363
+ 17,
45364
+ 17,
45365
+ 30,
45366
+ 0
45367
+ ],
45368
+ 67: [
45369
+ 14,
45370
+ 17,
45371
+ 16,
45372
+ 16,
45373
+ 16,
45374
+ 17,
45375
+ 14,
45376
+ 0
45377
+ ],
45378
+ 68: [
45379
+ 28,
45380
+ 18,
45381
+ 17,
45382
+ 17,
45383
+ 17,
45384
+ 18,
45385
+ 28,
45386
+ 0
45387
+ ],
45388
+ 69: [
45389
+ 31,
45390
+ 16,
45391
+ 16,
45392
+ 30,
45393
+ 16,
45394
+ 16,
45395
+ 31,
45396
+ 0
45397
+ ],
45398
+ 70: [
45399
+ 31,
45400
+ 16,
45401
+ 16,
45402
+ 30,
45403
+ 16,
45404
+ 16,
45405
+ 16,
45406
+ 0
45407
+ ],
45408
+ 71: [
45409
+ 14,
45410
+ 17,
45411
+ 16,
45412
+ 23,
45413
+ 17,
45414
+ 17,
45415
+ 15,
45416
+ 0
45417
+ ],
45418
+ 72: [
45419
+ 17,
45420
+ 17,
45421
+ 17,
45422
+ 31,
45423
+ 17,
45424
+ 17,
45425
+ 17,
45426
+ 0
45427
+ ],
45428
+ 73: [
45429
+ 14,
45430
+ 4,
45431
+ 4,
45432
+ 4,
45433
+ 4,
45434
+ 4,
45435
+ 14,
45436
+ 0
45437
+ ],
45438
+ 74: [
45439
+ 7,
45440
+ 2,
45441
+ 2,
45442
+ 2,
45443
+ 2,
45444
+ 18,
45445
+ 12,
45446
+ 0
45447
+ ],
45448
+ 75: [
45449
+ 17,
45450
+ 18,
45451
+ 20,
45452
+ 24,
45453
+ 20,
45454
+ 18,
45455
+ 17,
45456
+ 0
45457
+ ],
45458
+ 76: [
45459
+ 16,
45460
+ 16,
45461
+ 16,
45462
+ 16,
45463
+ 16,
45464
+ 16,
45465
+ 31,
45466
+ 0
45467
+ ],
45468
+ 77: [
45469
+ 17,
45470
+ 27,
45471
+ 21,
45472
+ 21,
45473
+ 17,
45474
+ 17,
45475
+ 17,
45476
+ 0
45477
+ ],
45478
+ 78: [
45479
+ 17,
45480
+ 25,
45481
+ 21,
45482
+ 19,
45483
+ 17,
45484
+ 17,
45485
+ 17,
45486
+ 0
45487
+ ],
45488
+ 79: [
45489
+ 14,
45490
+ 17,
45491
+ 17,
45492
+ 17,
45493
+ 17,
45494
+ 17,
45495
+ 14,
45496
+ 0
45497
+ ],
45498
+ 80: [
45499
+ 30,
45500
+ 17,
45501
+ 17,
45502
+ 30,
45503
+ 16,
45504
+ 16,
45505
+ 16,
45506
+ 0
45507
+ ],
45508
+ 81: [
45509
+ 14,
45510
+ 17,
45511
+ 17,
45512
+ 17,
45513
+ 21,
45514
+ 18,
45515
+ 13,
45516
+ 0
45517
+ ],
45518
+ 82: [
45519
+ 30,
45520
+ 17,
45521
+ 17,
45522
+ 30,
45523
+ 20,
45524
+ 18,
45525
+ 17,
45526
+ 0
45527
+ ],
45528
+ 83: [
45529
+ 15,
45530
+ 16,
45531
+ 16,
45532
+ 14,
45533
+ 1,
45534
+ 1,
45535
+ 30,
45536
+ 0
45537
+ ],
45538
+ 84: [
45539
+ 31,
45540
+ 4,
45541
+ 4,
45542
+ 4,
45543
+ 4,
45544
+ 4,
45545
+ 4,
45546
+ 0
45547
+ ],
45548
+ 85: [
45549
+ 17,
45550
+ 17,
45551
+ 17,
45552
+ 17,
45553
+ 17,
45554
+ 17,
45555
+ 14,
45556
+ 0
45557
+ ],
45558
+ 86: [
45559
+ 17,
45560
+ 17,
45561
+ 17,
45562
+ 17,
45563
+ 17,
45564
+ 10,
45565
+ 4,
45566
+ 0
45567
+ ],
45568
+ 87: [
45569
+ 17,
45570
+ 17,
45571
+ 17,
45572
+ 21,
45573
+ 21,
45574
+ 27,
45575
+ 17,
45576
+ 0
45577
+ ],
45578
+ 88: [
45579
+ 17,
45580
+ 17,
45581
+ 10,
45582
+ 4,
45583
+ 10,
45584
+ 17,
45585
+ 17,
45586
+ 0
45587
+ ],
45588
+ 89: [
45589
+ 17,
45590
+ 17,
45591
+ 10,
45592
+ 4,
45593
+ 4,
45594
+ 4,
45595
+ 4,
45596
+ 0
45597
+ ],
45598
+ 90: [
45599
+ 31,
45600
+ 1,
45601
+ 2,
45602
+ 4,
45603
+ 8,
45604
+ 16,
45605
+ 31,
45606
+ 0
45607
+ ],
45608
+ 91: [
45609
+ 14,
45610
+ 8,
45611
+ 8,
45612
+ 8,
45613
+ 8,
45614
+ 8,
45615
+ 14,
45616
+ 0
45617
+ ],
45618
+ 92: [
45619
+ 0,
45620
+ 16,
45621
+ 8,
45622
+ 4,
45623
+ 2,
45624
+ 1,
45625
+ 0,
45626
+ 0
45627
+ ],
45628
+ 93: [
45629
+ 14,
45630
+ 2,
45631
+ 2,
45632
+ 2,
45633
+ 2,
45634
+ 2,
45635
+ 14,
45636
+ 0
45637
+ ],
45638
+ 94: [
45639
+ 4,
45640
+ 10,
45641
+ 17,
45642
+ 0,
45643
+ 0,
45644
+ 0,
45645
+ 0,
45646
+ 0
45647
+ ],
45648
+ 95: [
45649
+ 0,
45650
+ 0,
45651
+ 0,
45652
+ 0,
45653
+ 0,
45654
+ 0,
45655
+ 31,
45656
+ 0
45657
+ ],
45658
+ 96: [
45659
+ 8,
45660
+ 4,
45661
+ 2,
45662
+ 0,
45663
+ 0,
45664
+ 0,
45665
+ 0,
45666
+ 0
45667
+ ],
45668
+ 97: [
45669
+ 0,
45670
+ 0,
45671
+ 14,
45672
+ 1,
45673
+ 15,
45674
+ 17,
45675
+ 15,
45676
+ 0
45677
+ ],
45678
+ 98: [
45679
+ 16,
45680
+ 16,
45681
+ 22,
45682
+ 25,
45683
+ 17,
45684
+ 17,
45685
+ 30,
45686
+ 0
45687
+ ],
45688
+ 99: [
45689
+ 0,
45690
+ 0,
45691
+ 14,
45692
+ 16,
45693
+ 16,
45694
+ 17,
45695
+ 14,
45696
+ 0
45697
+ ],
45698
+ 100: [
45699
+ 1,
45700
+ 1,
45701
+ 13,
45702
+ 19,
45703
+ 17,
45704
+ 17,
45705
+ 15,
45706
+ 0
45707
+ ],
45708
+ 101: [
45709
+ 0,
45710
+ 0,
45711
+ 14,
45712
+ 17,
45713
+ 31,
45714
+ 16,
45715
+ 14,
45716
+ 0
45717
+ ],
45718
+ 102: [
45719
+ 6,
45720
+ 9,
45721
+ 8,
45722
+ 28,
45723
+ 8,
45724
+ 8,
45725
+ 8,
45726
+ 0
45727
+ ],
45728
+ 103: [
45729
+ 0,
45730
+ 0,
45731
+ 15,
45732
+ 17,
45733
+ 15,
45734
+ 1,
45735
+ 14,
45736
+ 0
45737
+ ],
45738
+ 104: [
45739
+ 16,
45740
+ 16,
45741
+ 22,
45742
+ 25,
45743
+ 17,
45744
+ 17,
45745
+ 17,
45746
+ 0
45747
+ ],
45748
+ 105: [
45749
+ 4,
45750
+ 0,
45751
+ 12,
45752
+ 4,
45753
+ 4,
45754
+ 4,
45755
+ 14,
45756
+ 0
45757
+ ],
45758
+ 106: [
45759
+ 2,
45760
+ 0,
45761
+ 6,
45762
+ 2,
45763
+ 2,
45764
+ 18,
45765
+ 12,
45766
+ 0
45767
+ ],
45768
+ 107: [
45769
+ 16,
45770
+ 16,
45771
+ 18,
45772
+ 20,
45773
+ 24,
45774
+ 20,
45775
+ 18,
45776
+ 0
45777
+ ],
45778
+ 108: [
45779
+ 12,
45780
+ 4,
45781
+ 4,
45782
+ 4,
45783
+ 4,
45784
+ 4,
45785
+ 14,
45786
+ 0
45787
+ ],
45788
+ 109: [
45789
+ 0,
45790
+ 0,
45791
+ 26,
45792
+ 21,
45793
+ 21,
45794
+ 17,
45795
+ 17,
45796
+ 0
45797
+ ],
45798
+ 110: [
45799
+ 0,
45800
+ 0,
45801
+ 22,
45802
+ 25,
45803
+ 17,
45804
+ 17,
45805
+ 17,
45806
+ 0
45807
+ ],
45808
+ 111: [
45809
+ 0,
45810
+ 0,
45811
+ 14,
45812
+ 17,
45813
+ 17,
45814
+ 17,
45815
+ 14,
45816
+ 0
45817
+ ],
45818
+ 112: [
45819
+ 0,
45820
+ 0,
45821
+ 30,
45822
+ 17,
45823
+ 30,
45824
+ 16,
45825
+ 16,
45826
+ 0
45827
+ ],
45828
+ 113: [
45829
+ 0,
45830
+ 0,
45831
+ 13,
45832
+ 19,
45833
+ 15,
45834
+ 1,
45835
+ 1,
45836
+ 0
45837
+ ],
45838
+ 114: [
45839
+ 0,
45840
+ 0,
45841
+ 22,
45842
+ 25,
45843
+ 16,
45844
+ 16,
45845
+ 16,
45846
+ 0
45847
+ ],
45848
+ 115: [
45849
+ 0,
45850
+ 0,
45851
+ 14,
45852
+ 16,
45853
+ 14,
45854
+ 1,
45855
+ 30,
45856
+ 0
45857
+ ],
45858
+ 116: [
45859
+ 8,
45860
+ 8,
45861
+ 28,
45862
+ 8,
45863
+ 8,
45864
+ 9,
45865
+ 6,
45866
+ 0
45867
+ ],
45868
+ 117: [
45869
+ 0,
45870
+ 0,
45871
+ 17,
45872
+ 17,
45873
+ 17,
45874
+ 19,
45875
+ 13,
45876
+ 0
45877
+ ],
45878
+ 118: [
45879
+ 0,
45880
+ 0,
45881
+ 17,
45882
+ 17,
45883
+ 17,
45884
+ 10,
45885
+ 4,
45886
+ 0
45887
+ ],
45888
+ 119: [
45889
+ 0,
45890
+ 0,
45891
+ 17,
45892
+ 17,
45893
+ 21,
45894
+ 21,
45895
+ 10,
45896
+ 0
45897
+ ],
45898
+ 120: [
45899
+ 0,
45900
+ 0,
45901
+ 17,
45902
+ 10,
45903
+ 4,
45904
+ 10,
45905
+ 17,
45906
+ 0
45907
+ ],
45908
+ 121: [
45909
+ 0,
45910
+ 0,
45911
+ 17,
45912
+ 17,
45913
+ 15,
45914
+ 1,
45915
+ 14,
45916
+ 0
45917
+ ],
45918
+ 122: [
45919
+ 0,
45920
+ 0,
45921
+ 31,
45922
+ 2,
45923
+ 4,
45924
+ 8,
45925
+ 31,
45926
+ 0
45927
+ ],
45928
+ 123: [
45929
+ 2,
45930
+ 4,
45931
+ 4,
45932
+ 8,
45933
+ 4,
45934
+ 4,
45935
+ 2,
45936
+ 0
45937
+ ],
45938
+ 124: [
45939
+ 4,
45940
+ 4,
45941
+ 4,
45942
+ 4,
45943
+ 4,
45944
+ 4,
45945
+ 4,
45946
+ 0
45947
+ ],
45948
+ 125: [
45949
+ 8,
45950
+ 4,
45951
+ 4,
45952
+ 2,
45953
+ 4,
45954
+ 4,
45955
+ 8,
45956
+ 0
45957
+ ],
45958
+ 126: [
45959
+ 0,
45960
+ 0,
45961
+ 8,
45962
+ 21,
45963
+ 2,
45964
+ 0,
45965
+ 0,
45966
+ 0
45967
+ ]
45968
+ };
45969
+ /** Render text string to a grayscale bitmap (0 = transparent, 255 = opaque). */
45970
+ function renderTextBitmap(text, scale) {
45971
+ const charW = GLYPH_WIDTH * scale;
45972
+ const charH = GLYPH_HEIGHT * scale;
45973
+ const width = text.length * charW;
45974
+ const height = charH;
45975
+ const pixels = new Uint8Array(width * height);
45976
+ for (let ci = 0; ci < text.length; ci++) {
45977
+ const glyph = FONT_DATA[text.charCodeAt(ci)] ?? FONT_DATA[63];
45978
+ const xOff = ci * charW;
45979
+ for (let row = 0; row < GLYPH_HEIGHT; row++) {
45980
+ const bits = glyph[row];
45981
+ for (let col = 0; col < GLYPH_WIDTH; col++) if (bits & 1 << GLYPH_WIDTH - 1 - col) for (let sy = 0; sy < scale; sy++) for (let sx = 0; sx < scale; sx++) {
45982
+ const px = xOff + col * scale + sx;
45983
+ const py = row * scale + sy;
45984
+ if (px < width && py < height) pixels[py * width + px] = 255;
45985
+ }
45986
+ }
45987
+ }
45988
+ return {
45989
+ width,
45990
+ height,
45991
+ pixels
45992
+ };
45993
+ }
45994
+ /** Rotate a grayscale bitmap by the given angle in degrees. */
45995
+ function rotateBitmap(pixels, srcW, srcH, angleDeg) {
45996
+ if (angleDeg === 0) return {
45997
+ width: srcW,
45998
+ height: srcH,
45999
+ pixels
46000
+ };
46001
+ const rad = angleDeg * Math.PI / 180;
46002
+ const cos = Math.cos(rad);
46003
+ const sin = Math.sin(rad);
46004
+ const corners = [
46005
+ {
46006
+ x: 0,
46007
+ y: 0
46008
+ },
46009
+ {
46010
+ x: srcW,
46011
+ y: 0
46012
+ },
46013
+ {
46014
+ x: srcW,
46015
+ y: srcH
46016
+ },
46017
+ {
46018
+ x: 0,
46019
+ y: srcH
46020
+ }
46021
+ ];
46022
+ let minX = Infinity;
46023
+ let minY = Infinity;
46024
+ let maxX = -Infinity;
46025
+ let maxY = -Infinity;
46026
+ for (const c of corners) {
46027
+ const rx = c.x * cos - c.y * sin;
46028
+ const ry = c.x * sin + c.y * cos;
46029
+ minX = Math.min(minX, rx);
46030
+ minY = Math.min(minY, ry);
46031
+ maxX = Math.max(maxX, rx);
46032
+ maxY = Math.max(maxY, ry);
46033
+ }
46034
+ const dstW = Math.ceil(maxX - minX);
46035
+ const dstH = Math.ceil(maxY - minY);
46036
+ const dst = new Uint8Array(dstW * dstH);
46037
+ const invCos = cos;
46038
+ const invSin = -sin;
46039
+ for (let dy = 0; dy < dstH; dy++) for (let dx = 0; dx < dstW; dx++) {
46040
+ const wx = dx + minX;
46041
+ const wy = dy + minY;
46042
+ const sx = Math.round(wx * invCos - wy * invSin);
46043
+ const sy = Math.round(wx * invSin + wy * invCos);
46044
+ if (sx >= 0 && sx < srcW && sy >= 0 && sy < srcH) dst[dy * dstW + dx] = pixels[sy * srcW + sx];
46045
+ }
46046
+ return {
46047
+ width: dstW,
46048
+ height: dstH,
46049
+ pixels: dst
46050
+ };
46051
+ }
46052
+ /** Encode RGBA pixel data to a PNG file. */
46053
+ function encodePng(rgba, width, height) {
46054
+ const rawRowSize = 1 + width * 4;
46055
+ const rawData = new Uint8Array(rawRowSize * height);
46056
+ for (let y = 0; y < height; y++) {
46057
+ rawData[y * rawRowSize] = 0;
46058
+ rawData.set(rgba.subarray(y * width * 4, (y + 1) * width * 4), y * rawRowSize + 1);
46059
+ }
46060
+ const deflated = zlibCompress(rawData);
46061
+ const sig = new Uint8Array([
46062
+ 137,
46063
+ 80,
46064
+ 78,
46065
+ 71,
46066
+ 13,
46067
+ 10,
46068
+ 26,
46069
+ 10
46070
+ ]);
46071
+ const ihdr = new Uint8Array(13);
46072
+ writeU32BE(ihdr, 0, width);
46073
+ writeU32BE(ihdr, 4, height);
46074
+ ihdr[8] = 8;
46075
+ ihdr[9] = 6;
46076
+ ihdr[10] = 0;
46077
+ ihdr[11] = 0;
46078
+ ihdr[12] = 0;
46079
+ const ihdrChunk = pngChunk(1229472850, ihdr);
46080
+ const idatChunk = pngChunk(1229209940, deflated);
46081
+ const iendChunk = pngChunk(1229278788, new Uint8Array(0));
46082
+ const result = new Uint8Array(sig.length + ihdrChunk.length + idatChunk.length + iendChunk.length);
46083
+ let offset = 0;
46084
+ result.set(sig, offset);
46085
+ offset += sig.length;
46086
+ result.set(ihdrChunk, offset);
46087
+ offset += ihdrChunk.length;
46088
+ result.set(idatChunk, offset);
46089
+ offset += idatChunk.length;
46090
+ result.set(iendChunk, offset);
46091
+ return result;
46092
+ }
46093
+ /** Build a PNG chunk: length(4) + type(4) + data + crc32(4). */
46094
+ function pngChunk(type, data) {
46095
+ const chunk = new Uint8Array(12 + data.length);
46096
+ writeU32BE(chunk, 0, data.length);
46097
+ writeU32BE(chunk, 4, type);
46098
+ chunk.set(data, 8);
46099
+ const crc = crc32(chunk.subarray(4, 8 + data.length));
46100
+ writeU32BE(chunk, 8 + data.length, crc);
46101
+ return chunk;
46102
+ }
46103
+ /** Write a 32-bit big-endian unsigned int. */
46104
+ function writeU32BE(buf, offset, value) {
46105
+ buf[offset] = value >>> 24 & 255;
46106
+ buf[offset + 1] = value >>> 16 & 255;
46107
+ buf[offset + 2] = value >>> 8 & 255;
46108
+ buf[offset + 3] = value & 255;
46109
+ }
46110
+ /** Wrap raw data in a zlib stream with deflate compression. */
46111
+ function zlibCompress(data) {
46112
+ const deflated = deflateRawCompressed(data, 6);
46113
+ const adler = adler32(data);
46114
+ const result = new Uint8Array(2 + deflated.length + 4);
46115
+ result[0] = 120;
46116
+ result[1] = 1;
46117
+ result.set(deflated, 2);
46118
+ writeU32BE(result, 2 + deflated.length, adler);
46119
+ return result;
46120
+ }
46121
+ /** Compute Adler-32 checksum. */
46122
+ function adler32(data) {
46123
+ let a = 1;
46124
+ let b = 0;
46125
+ for (let i = 0; i < data.length; i++) {
46126
+ a = (a + data[i]) % 65521;
46127
+ b = (b + a) % 65521;
46128
+ }
46129
+ return b << 16 | a;
46130
+ }
46131
+ /** CRC32 lookup table. */
46132
+ const CRC_TABLE = /* @__PURE__ */ (() => {
46133
+ const table = new Uint32Array(256);
46134
+ for (let n = 0; n < 256; n++) {
46135
+ let c = n;
46136
+ for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
46137
+ table[n] = c;
46138
+ }
46139
+ return table;
46140
+ })();
46141
+ /** Compute CRC32 checksum. */
46142
+ function crc32(data) {
46143
+ let crc = 4294967295;
46144
+ for (let i = 0; i < data.length; i++) crc = CRC_TABLE[(crc ^ data[i]) & 255] ^ crc >>> 8;
46145
+ return (crc ^ 4294967295) >>> 0;
46146
+ }
46147
+ //#endregion
44523
46148
  //#region src/modules/pdf/types.ts
44524
46149
  /**
44525
46150
  * Type definitions for the PDF module.
@@ -45612,7 +47237,8 @@ onmessage = async (ev) => {
45612
47237
  addPage(options) {
45613
47238
  const objNum = this.allocObject();
45614
47239
  const mediaBox = `[0 0 ${pdfNumber(options.width)} ${pdfNumber(options.height)}]`;
45615
- const dict = new PdfDict().set("Type", "/Page").set("Parent", pdfRef(options.parentRef)).set("MediaBox", mediaBox).set("Contents", pdfRef(options.contentsRef)).set("Resources", pdfRef(options.resourcesRef));
47240
+ const contentsValue = typeof options.contentsRef === "string" ? options.contentsRef : pdfRef(options.contentsRef);
47241
+ const dict = new PdfDict().set("Type", "/Page").set("Parent", pdfRef(options.parentRef)).set("MediaBox", mediaBox).set("Contents", contentsValue).set("Resources", pdfRef(options.resourcesRef));
45616
47242
  if (options.annotRefs && options.annotRefs.length > 0) dict.set("Annots", "[" + options.annotRefs.map((r) => pdfRef(r)).join(" ") + "]");
45617
47243
  this.addObject(objNum, dict);
45618
47244
  return objNum;
@@ -45861,6 +47487,396 @@ onmessage = async (ev) => {
45861
47487
  return result;
45862
47488
  }
45863
47489
  //#endregion
47490
+ //#region src/modules/pdf/core/pdf-stream.ts
47491
+ /**
47492
+ * PDF content stream builder.
47493
+ *
47494
+ * Provides a high-level API for constructing PDF content streams using
47495
+ * PDF graphics operators. Content streams control what is drawn on a page:
47496
+ * text, lines, rectangles, colors, etc.
47497
+ *
47498
+ * @see PDF Reference 1.7, Chapter 4 - Graphics
47499
+ * @see PDF Reference 1.7, Chapter 5 - Text
47500
+ */
47501
+ /**
47502
+ * Builds a PDF content stream using graphics and text operators.
47503
+ *
47504
+ * PDF uses a postfix notation where operands precede the operator.
47505
+ * For example: `100 200 m` means "move to point (100, 200)".
47506
+ *
47507
+ * Color model: PDF uses separate color state for stroking (lines/borders)
47508
+ * and non-stroking (fills/text). We provide methods for both.
47509
+ */
47510
+ var PdfContentStream = class {
47511
+ constructor() {
47512
+ this.parts = [];
47513
+ }
47514
+ /**
47515
+ * Save the current graphics state (push onto state stack).
47516
+ * Must be balanced with a corresponding restore().
47517
+ */
47518
+ save() {
47519
+ this.parts.push("q");
47520
+ return this;
47521
+ }
47522
+ /**
47523
+ * Restore the previously saved graphics state (pop from state stack).
47524
+ */
47525
+ restore() {
47526
+ this.parts.push("Q");
47527
+ return this;
47528
+ }
47529
+ /**
47530
+ * Set the current graphics state from an ExtGState resource.
47531
+ * Used for transparency (alpha), blend modes, etc.
47532
+ */
47533
+ setGraphicsState(name) {
47534
+ this.parts.push(`/${name} gs`);
47535
+ return this;
47536
+ }
47537
+ /**
47538
+ * Set the stroking color (used for lines, borders).
47539
+ */
47540
+ setStrokeColor(color) {
47541
+ this.parts.push(`${pdfNumber(color.r)} ${pdfNumber(color.g)} ${pdfNumber(color.b)} RG`);
47542
+ return this;
47543
+ }
47544
+ /**
47545
+ * Set the non-stroking color (used for fills, text).
47546
+ */
47547
+ setFillColor(color) {
47548
+ this.parts.push(`${pdfNumber(color.r)} ${pdfNumber(color.g)} ${pdfNumber(color.b)} rg`);
47549
+ return this;
47550
+ }
47551
+ /**
47552
+ * Set the line width for stroking operations.
47553
+ */
47554
+ setLineWidth(width) {
47555
+ this.parts.push(`${pdfNumber(width)} w`);
47556
+ return this;
47557
+ }
47558
+ /**
47559
+ * Set the line dash pattern.
47560
+ * @param dashArray - Array of dash/gap lengths. Empty = solid line.
47561
+ * @param phase - Starting phase offset.
47562
+ */
47563
+ setDashPattern(dashArray, phase = 0) {
47564
+ const arr = dashArray.map(pdfNumber).join(" ");
47565
+ this.parts.push(`[${arr}] ${pdfNumber(phase)} d`);
47566
+ return this;
47567
+ }
47568
+ /**
47569
+ * Set the line cap style.
47570
+ * 0 = butt cap, 1 = round cap, 2 = projecting square cap
47571
+ */
47572
+ setLineCap(style) {
47573
+ this.parts.push(`${style} J`);
47574
+ return this;
47575
+ }
47576
+ /**
47577
+ * Set the line join style.
47578
+ * 0 = miter join, 1 = round join, 2 = bevel join
47579
+ */
47580
+ setLineJoin(style) {
47581
+ this.parts.push(`${style} j`);
47582
+ return this;
47583
+ }
47584
+ /**
47585
+ * Begin a new subpath by moving to the given point.
47586
+ */
47587
+ moveTo(x, y) {
47588
+ this.parts.push(`${pdfNumber(x)} ${pdfNumber(y)} m`);
47589
+ return this;
47590
+ }
47591
+ /**
47592
+ * Append a straight line segment from the current point to (x, y).
47593
+ */
47594
+ lineTo(x, y) {
47595
+ this.parts.push(`${pdfNumber(x)} ${pdfNumber(y)} l`);
47596
+ return this;
47597
+ }
47598
+ /**
47599
+ * Append a rectangle to the current path.
47600
+ * PDF convention: (x, y) is the lower-left corner.
47601
+ */
47602
+ rect(x, y, width, height) {
47603
+ this.parts.push(`${pdfNumber(x)} ${pdfNumber(y)} ${pdfNumber(width)} ${pdfNumber(height)} re`);
47604
+ return this;
47605
+ }
47606
+ /**
47607
+ * Stroke the current path.
47608
+ */
47609
+ stroke() {
47610
+ this.parts.push("S");
47611
+ return this;
47612
+ }
47613
+ /**
47614
+ * Fill the current path using the nonzero winding number rule.
47615
+ */
47616
+ fill() {
47617
+ this.parts.push("f");
47618
+ return this;
47619
+ }
47620
+ /**
47621
+ * Fill and then stroke the current path.
47622
+ */
47623
+ fillAndStroke() {
47624
+ this.parts.push("B");
47625
+ return this;
47626
+ }
47627
+ /**
47628
+ * Close the current subpath by appending a line from current point to start.
47629
+ */
47630
+ closePath() {
47631
+ this.parts.push("h");
47632
+ return this;
47633
+ }
47634
+ /**
47635
+ * End the path without filling or stroking (used for clipping).
47636
+ */
47637
+ endPath() {
47638
+ this.parts.push("n");
47639
+ return this;
47640
+ }
47641
+ /**
47642
+ * Set the current path as the clipping boundary (nonzero winding rule).
47643
+ * Must be followed by endPath() or a painting operator.
47644
+ */
47645
+ clip() {
47646
+ this.parts.push("W");
47647
+ return this;
47648
+ }
47649
+ /**
47650
+ * Begin a text object.
47651
+ */
47652
+ beginText() {
47653
+ this.parts.push("BT");
47654
+ return this;
47655
+ }
47656
+ /**
47657
+ * End the current text object.
47658
+ */
47659
+ endText() {
47660
+ this.parts.push("ET");
47661
+ return this;
47662
+ }
47663
+ /**
47664
+ * Set the font and size for subsequent text operations.
47665
+ * @param fontName - The font resource name (e.g., "F1")
47666
+ * @param size - Font size in points
47667
+ */
47668
+ setFont(fontName, size) {
47669
+ this.parts.push(`/${fontName} ${pdfNumber(size)} Tf`);
47670
+ return this;
47671
+ }
47672
+ /**
47673
+ * Set the text matrix (position and transform for text).
47674
+ * For simple positioning, use Td instead.
47675
+ * @param a - Horizontal scaling
47676
+ * @param b - Vertical skew
47677
+ * @param c - Horizontal skew
47678
+ * @param d - Vertical scaling
47679
+ * @param e - Horizontal translation
47680
+ * @param f - Vertical translation
47681
+ */
47682
+ setTextMatrix(a, b, c, d, e, f) {
47683
+ this.parts.push(`${pdfNumber(a)} ${pdfNumber(b)} ${pdfNumber(c)} ${pdfNumber(d)} ${pdfNumber(e)} ${pdfNumber(f)} Tm`);
47684
+ return this;
47685
+ }
47686
+ /**
47687
+ * Move to the start of the next line, offset from the start of the current line.
47688
+ */
47689
+ moveText(tx, ty) {
47690
+ this.parts.push(`${pdfNumber(tx)} ${pdfNumber(ty)} Td`);
47691
+ return this;
47692
+ }
47693
+ /**
47694
+ * Set the text leading (line spacing) for T* operator.
47695
+ */
47696
+ setTextLeading(leading) {
47697
+ this.parts.push(`${pdfNumber(leading)} TL`);
47698
+ return this;
47699
+ }
47700
+ /**
47701
+ * Show a text string. The string is escaped for PDF.
47702
+ */
47703
+ showText(text) {
47704
+ if (hasNonAscii(text)) return this.showTextWinAnsi(text);
47705
+ this.parts.push(`(${escapeTextForPdf(text)}) Tj`);
47706
+ return this;
47707
+ }
47708
+ /**
47709
+ * Show a text string encoded as WinAnsi hex string.
47710
+ * Used for Type1 fonts where non-ASCII characters need single-byte encoding.
47711
+ */
47712
+ showTextWinAnsi(text) {
47713
+ let hex = "<";
47714
+ for (let i = 0; i < text.length; i++) {
47715
+ const cp = text.codePointAt(i);
47716
+ if (cp > 65535) i++;
47717
+ const byte = unicodeToWinAnsi(cp);
47718
+ hex += byte.toString(16).padStart(2, "0");
47719
+ }
47720
+ hex += ">";
47721
+ this.parts.push(`${hex} Tj`);
47722
+ return this;
47723
+ }
47724
+ /**
47725
+ * Show a text string using a pre-encoded hex string (for CIDFonts).
47726
+ * The hexString should be in the format `<0012003A...>`.
47727
+ */
47728
+ showTextHex(hexString) {
47729
+ this.parts.push(`${hexString} Tj`);
47730
+ return this;
47731
+ }
47732
+ /**
47733
+ * Move to the next line and show a text string.
47734
+ */
47735
+ nextLineShowText(text) {
47736
+ if (hasNonAscii(text)) {
47737
+ let hex = "<";
47738
+ for (let i = 0; i < text.length; i++) {
47739
+ const cp = text.codePointAt(i);
47740
+ if (cp > 65535) i++;
47741
+ const byte = unicodeToWinAnsi(cp);
47742
+ hex += byte.toString(16).padStart(2, "0");
47743
+ }
47744
+ hex += ">";
47745
+ this.parts.push(`${hex} '`);
47746
+ return this;
47747
+ }
47748
+ this.parts.push(`(${escapeTextForPdf(text)}) '`);
47749
+ return this;
47750
+ }
47751
+ /**
47752
+ * Set the text rise (baseline offset), used for superscript/subscript.
47753
+ */
47754
+ setTextRise(rise) {
47755
+ this.parts.push(`${pdfNumber(rise)} Ts`);
47756
+ return this;
47757
+ }
47758
+ /**
47759
+ * Set character spacing (extra space between characters).
47760
+ */
47761
+ setCharacterSpacing(spacing) {
47762
+ this.parts.push(`${pdfNumber(spacing)} Tc`);
47763
+ return this;
47764
+ }
47765
+ /**
47766
+ * Set word spacing (extra space for space character).
47767
+ */
47768
+ setWordSpacing(spacing) {
47769
+ this.parts.push(`${pdfNumber(spacing)} Tw`);
47770
+ return this;
47771
+ }
47772
+ /**
47773
+ * Draw an XObject (image) at the given position and size.
47774
+ * The image must be registered as a resource with the given name.
47775
+ */
47776
+ drawImage(name, x, y, width, height) {
47777
+ return this.save().concat(width, 0, 0, height, x, y).doXObject(name).restore();
47778
+ }
47779
+ /**
47780
+ * Apply a transformation matrix (cm operator).
47781
+ */
47782
+ concat(a, b, c, d, e, f) {
47783
+ this.parts.push(`${pdfNumber(a)} ${pdfNumber(b)} ${pdfNumber(c)} ${pdfNumber(d)} ${pdfNumber(e)} ${pdfNumber(f)} cm`);
47784
+ return this;
47785
+ }
47786
+ /**
47787
+ * Invoke a named XObject (Do operator).
47788
+ */
47789
+ doXObject(name) {
47790
+ this.parts.push(`/${name} Do`);
47791
+ return this;
47792
+ }
47793
+ /**
47794
+ * Draw a filled rectangle.
47795
+ */
47796
+ fillRect(x, y, width, height, color) {
47797
+ return this.save().setFillColor(color).rect(x, y, width, height).fill().restore();
47798
+ }
47799
+ /**
47800
+ * Draw a stroked line.
47801
+ */
47802
+ drawLine(x1, y1, x2, y2, color, lineWidth, dashPattern = []) {
47803
+ this.save().setStrokeColor(color).setLineWidth(lineWidth);
47804
+ if (dashPattern.length > 0) this.setDashPattern(dashPattern);
47805
+ return this.moveTo(x1, y1).lineTo(x2, y2).stroke().restore();
47806
+ }
47807
+ /**
47808
+ * Get the content stream as a string.
47809
+ */
47810
+ toString() {
47811
+ return this.parts.join("\n");
47812
+ }
47813
+ /**
47814
+ * Get the content stream as a Uint8Array (UTF-8 encoded).
47815
+ */
47816
+ toUint8Array() {
47817
+ return new TextEncoder().encode(this.toString());
47818
+ }
47819
+ };
47820
+ /**
47821
+ * Escape a text string for use inside a PDF text operator.
47822
+ * PDF text strings are delimited by parentheses.
47823
+ */
47824
+ function escapeTextForPdf(text) {
47825
+ return text.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)").replace(/\r\n/g, "\\n").replace(/\r/g, "\\n").replace(/\n/g, "\\n");
47826
+ }
47827
+ /**
47828
+ * Check if a string contains any non-ASCII characters (code point > 127).
47829
+ */
47830
+ function hasNonAscii(text) {
47831
+ for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) > 127) return true;
47832
+ return false;
47833
+ }
47834
+ /**
47835
+ * Map from Unicode code point to WinAnsi (Windows-1252) byte value.
47836
+ * Only the 0x80-0x9F range differs from Latin-1; everything else maps 1:1
47837
+ * for code points 0x00-0xFF.
47838
+ */
47839
+ const UNICODE_TO_WINANSI = new Map([
47840
+ [8364, 128],
47841
+ [8218, 130],
47842
+ [402, 131],
47843
+ [8222, 132],
47844
+ [8230, 133],
47845
+ [8224, 134],
47846
+ [8225, 135],
47847
+ [710, 136],
47848
+ [8240, 137],
47849
+ [352, 138],
47850
+ [8249, 139],
47851
+ [338, 140],
47852
+ [381, 142],
47853
+ [8216, 145],
47854
+ [8217, 146],
47855
+ [8220, 147],
47856
+ [8221, 148],
47857
+ [8226, 149],
47858
+ [8211, 150],
47859
+ [8212, 151],
47860
+ [732, 152],
47861
+ [8482, 153],
47862
+ [353, 154],
47863
+ [8250, 155],
47864
+ [339, 156],
47865
+ [382, 158],
47866
+ [376, 159]
47867
+ ]);
47868
+ /**
47869
+ * Convert a Unicode code point to a WinAnsi byte value.
47870
+ * Returns 0x3F ('?') for unmappable characters.
47871
+ */
47872
+ function unicodeToWinAnsi(cp) {
47873
+ if (cp < 128) return cp;
47874
+ if (cp >= 160 && cp <= 255) return cp;
47875
+ const mapped = UNICODE_TO_WINANSI.get(cp);
47876
+ if (mapped !== void 0) return mapped;
47877
+ return 63;
47878
+ }
47879
+ //#endregion
45864
47880
  //#region src/modules/pdf/font/metrics.ts
45865
47881
  /**
45866
47882
  * Character width metrics for the 14 PDF standard Type 1 fonts.
@@ -47778,396 +49794,6 @@ onmessage = async (ev) => {
47778
49794
  }
47779
49795
  }
47780
49796
  //#endregion
47781
- //#region src/modules/pdf/core/pdf-stream.ts
47782
- /**
47783
- * PDF content stream builder.
47784
- *
47785
- * Provides a high-level API for constructing PDF content streams using
47786
- * PDF graphics operators. Content streams control what is drawn on a page:
47787
- * text, lines, rectangles, colors, etc.
47788
- *
47789
- * @see PDF Reference 1.7, Chapter 4 - Graphics
47790
- * @see PDF Reference 1.7, Chapter 5 - Text
47791
- */
47792
- /**
47793
- * Builds a PDF content stream using graphics and text operators.
47794
- *
47795
- * PDF uses a postfix notation where operands precede the operator.
47796
- * For example: `100 200 m` means "move to point (100, 200)".
47797
- *
47798
- * Color model: PDF uses separate color state for stroking (lines/borders)
47799
- * and non-stroking (fills/text). We provide methods for both.
47800
- */
47801
- var PdfContentStream = class {
47802
- constructor() {
47803
- this.parts = [];
47804
- }
47805
- /**
47806
- * Save the current graphics state (push onto state stack).
47807
- * Must be balanced with a corresponding restore().
47808
- */
47809
- save() {
47810
- this.parts.push("q");
47811
- return this;
47812
- }
47813
- /**
47814
- * Restore the previously saved graphics state (pop from state stack).
47815
- */
47816
- restore() {
47817
- this.parts.push("Q");
47818
- return this;
47819
- }
47820
- /**
47821
- * Set the current graphics state from an ExtGState resource.
47822
- * Used for transparency (alpha), blend modes, etc.
47823
- */
47824
- setGraphicsState(name) {
47825
- this.parts.push(`/${name} gs`);
47826
- return this;
47827
- }
47828
- /**
47829
- * Set the stroking color (used for lines, borders).
47830
- */
47831
- setStrokeColor(color) {
47832
- this.parts.push(`${pdfNumber(color.r)} ${pdfNumber(color.g)} ${pdfNumber(color.b)} RG`);
47833
- return this;
47834
- }
47835
- /**
47836
- * Set the non-stroking color (used for fills, text).
47837
- */
47838
- setFillColor(color) {
47839
- this.parts.push(`${pdfNumber(color.r)} ${pdfNumber(color.g)} ${pdfNumber(color.b)} rg`);
47840
- return this;
47841
- }
47842
- /**
47843
- * Set the line width for stroking operations.
47844
- */
47845
- setLineWidth(width) {
47846
- this.parts.push(`${pdfNumber(width)} w`);
47847
- return this;
47848
- }
47849
- /**
47850
- * Set the line dash pattern.
47851
- * @param dashArray - Array of dash/gap lengths. Empty = solid line.
47852
- * @param phase - Starting phase offset.
47853
- */
47854
- setDashPattern(dashArray, phase = 0) {
47855
- const arr = dashArray.map(pdfNumber).join(" ");
47856
- this.parts.push(`[${arr}] ${pdfNumber(phase)} d`);
47857
- return this;
47858
- }
47859
- /**
47860
- * Set the line cap style.
47861
- * 0 = butt cap, 1 = round cap, 2 = projecting square cap
47862
- */
47863
- setLineCap(style) {
47864
- this.parts.push(`${style} J`);
47865
- return this;
47866
- }
47867
- /**
47868
- * Set the line join style.
47869
- * 0 = miter join, 1 = round join, 2 = bevel join
47870
- */
47871
- setLineJoin(style) {
47872
- this.parts.push(`${style} j`);
47873
- return this;
47874
- }
47875
- /**
47876
- * Begin a new subpath by moving to the given point.
47877
- */
47878
- moveTo(x, y) {
47879
- this.parts.push(`${pdfNumber(x)} ${pdfNumber(y)} m`);
47880
- return this;
47881
- }
47882
- /**
47883
- * Append a straight line segment from the current point to (x, y).
47884
- */
47885
- lineTo(x, y) {
47886
- this.parts.push(`${pdfNumber(x)} ${pdfNumber(y)} l`);
47887
- return this;
47888
- }
47889
- /**
47890
- * Append a rectangle to the current path.
47891
- * PDF convention: (x, y) is the lower-left corner.
47892
- */
47893
- rect(x, y, width, height) {
47894
- this.parts.push(`${pdfNumber(x)} ${pdfNumber(y)} ${pdfNumber(width)} ${pdfNumber(height)} re`);
47895
- return this;
47896
- }
47897
- /**
47898
- * Stroke the current path.
47899
- */
47900
- stroke() {
47901
- this.parts.push("S");
47902
- return this;
47903
- }
47904
- /**
47905
- * Fill the current path using the nonzero winding number rule.
47906
- */
47907
- fill() {
47908
- this.parts.push("f");
47909
- return this;
47910
- }
47911
- /**
47912
- * Fill and then stroke the current path.
47913
- */
47914
- fillAndStroke() {
47915
- this.parts.push("B");
47916
- return this;
47917
- }
47918
- /**
47919
- * Close the current subpath by appending a line from current point to start.
47920
- */
47921
- closePath() {
47922
- this.parts.push("h");
47923
- return this;
47924
- }
47925
- /**
47926
- * End the path without filling or stroking (used for clipping).
47927
- */
47928
- endPath() {
47929
- this.parts.push("n");
47930
- return this;
47931
- }
47932
- /**
47933
- * Set the current path as the clipping boundary (nonzero winding rule).
47934
- * Must be followed by endPath() or a painting operator.
47935
- */
47936
- clip() {
47937
- this.parts.push("W");
47938
- return this;
47939
- }
47940
- /**
47941
- * Begin a text object.
47942
- */
47943
- beginText() {
47944
- this.parts.push("BT");
47945
- return this;
47946
- }
47947
- /**
47948
- * End the current text object.
47949
- */
47950
- endText() {
47951
- this.parts.push("ET");
47952
- return this;
47953
- }
47954
- /**
47955
- * Set the font and size for subsequent text operations.
47956
- * @param fontName - The font resource name (e.g., "F1")
47957
- * @param size - Font size in points
47958
- */
47959
- setFont(fontName, size) {
47960
- this.parts.push(`/${fontName} ${pdfNumber(size)} Tf`);
47961
- return this;
47962
- }
47963
- /**
47964
- * Set the text matrix (position and transform for text).
47965
- * For simple positioning, use Td instead.
47966
- * @param a - Horizontal scaling
47967
- * @param b - Vertical skew
47968
- * @param c - Horizontal skew
47969
- * @param d - Vertical scaling
47970
- * @param e - Horizontal translation
47971
- * @param f - Vertical translation
47972
- */
47973
- setTextMatrix(a, b, c, d, e, f) {
47974
- this.parts.push(`${pdfNumber(a)} ${pdfNumber(b)} ${pdfNumber(c)} ${pdfNumber(d)} ${pdfNumber(e)} ${pdfNumber(f)} Tm`);
47975
- return this;
47976
- }
47977
- /**
47978
- * Move to the start of the next line, offset from the start of the current line.
47979
- */
47980
- moveText(tx, ty) {
47981
- this.parts.push(`${pdfNumber(tx)} ${pdfNumber(ty)} Td`);
47982
- return this;
47983
- }
47984
- /**
47985
- * Set the text leading (line spacing) for T* operator.
47986
- */
47987
- setTextLeading(leading) {
47988
- this.parts.push(`${pdfNumber(leading)} TL`);
47989
- return this;
47990
- }
47991
- /**
47992
- * Show a text string. The string is escaped for PDF.
47993
- */
47994
- showText(text) {
47995
- if (hasNonAscii(text)) return this.showTextWinAnsi(text);
47996
- this.parts.push(`(${escapeTextForPdf(text)}) Tj`);
47997
- return this;
47998
- }
47999
- /**
48000
- * Show a text string encoded as WinAnsi hex string.
48001
- * Used for Type1 fonts where non-ASCII characters need single-byte encoding.
48002
- */
48003
- showTextWinAnsi(text) {
48004
- let hex = "<";
48005
- for (let i = 0; i < text.length; i++) {
48006
- const cp = text.codePointAt(i);
48007
- if (cp > 65535) i++;
48008
- const byte = unicodeToWinAnsi(cp);
48009
- hex += byte.toString(16).padStart(2, "0");
48010
- }
48011
- hex += ">";
48012
- this.parts.push(`${hex} Tj`);
48013
- return this;
48014
- }
48015
- /**
48016
- * Show a text string using a pre-encoded hex string (for CIDFonts).
48017
- * The hexString should be in the format `<0012003A...>`.
48018
- */
48019
- showTextHex(hexString) {
48020
- this.parts.push(`${hexString} Tj`);
48021
- return this;
48022
- }
48023
- /**
48024
- * Move to the next line and show a text string.
48025
- */
48026
- nextLineShowText(text) {
48027
- if (hasNonAscii(text)) {
48028
- let hex = "<";
48029
- for (let i = 0; i < text.length; i++) {
48030
- const cp = text.codePointAt(i);
48031
- if (cp > 65535) i++;
48032
- const byte = unicodeToWinAnsi(cp);
48033
- hex += byte.toString(16).padStart(2, "0");
48034
- }
48035
- hex += ">";
48036
- this.parts.push(`${hex} '`);
48037
- return this;
48038
- }
48039
- this.parts.push(`(${escapeTextForPdf(text)}) '`);
48040
- return this;
48041
- }
48042
- /**
48043
- * Set the text rise (baseline offset), used for superscript/subscript.
48044
- */
48045
- setTextRise(rise) {
48046
- this.parts.push(`${pdfNumber(rise)} Ts`);
48047
- return this;
48048
- }
48049
- /**
48050
- * Set character spacing (extra space between characters).
48051
- */
48052
- setCharacterSpacing(spacing) {
48053
- this.parts.push(`${pdfNumber(spacing)} Tc`);
48054
- return this;
48055
- }
48056
- /**
48057
- * Set word spacing (extra space for space character).
48058
- */
48059
- setWordSpacing(spacing) {
48060
- this.parts.push(`${pdfNumber(spacing)} Tw`);
48061
- return this;
48062
- }
48063
- /**
48064
- * Draw an XObject (image) at the given position and size.
48065
- * The image must be registered as a resource with the given name.
48066
- */
48067
- drawImage(name, x, y, width, height) {
48068
- return this.save().concat(width, 0, 0, height, x, y).doXObject(name).restore();
48069
- }
48070
- /**
48071
- * Apply a transformation matrix (cm operator).
48072
- */
48073
- concat(a, b, c, d, e, f) {
48074
- this.parts.push(`${pdfNumber(a)} ${pdfNumber(b)} ${pdfNumber(c)} ${pdfNumber(d)} ${pdfNumber(e)} ${pdfNumber(f)} cm`);
48075
- return this;
48076
- }
48077
- /**
48078
- * Invoke a named XObject (Do operator).
48079
- */
48080
- doXObject(name) {
48081
- this.parts.push(`/${name} Do`);
48082
- return this;
48083
- }
48084
- /**
48085
- * Draw a filled rectangle.
48086
- */
48087
- fillRect(x, y, width, height, color) {
48088
- return this.save().setFillColor(color).rect(x, y, width, height).fill().restore();
48089
- }
48090
- /**
48091
- * Draw a stroked line.
48092
- */
48093
- drawLine(x1, y1, x2, y2, color, lineWidth, dashPattern = []) {
48094
- this.save().setStrokeColor(color).setLineWidth(lineWidth);
48095
- if (dashPattern.length > 0) this.setDashPattern(dashPattern);
48096
- return this.moveTo(x1, y1).lineTo(x2, y2).stroke().restore();
48097
- }
48098
- /**
48099
- * Get the content stream as a string.
48100
- */
48101
- toString() {
48102
- return this.parts.join("\n");
48103
- }
48104
- /**
48105
- * Get the content stream as a Uint8Array (UTF-8 encoded).
48106
- */
48107
- toUint8Array() {
48108
- return new TextEncoder().encode(this.toString());
48109
- }
48110
- };
48111
- /**
48112
- * Escape a text string for use inside a PDF text operator.
48113
- * PDF text strings are delimited by parentheses.
48114
- */
48115
- function escapeTextForPdf(text) {
48116
- return text.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)").replace(/\r\n/g, "\\n").replace(/\r/g, "\\n").replace(/\n/g, "\\n");
48117
- }
48118
- /**
48119
- * Check if a string contains any non-ASCII characters (code point > 127).
48120
- */
48121
- function hasNonAscii(text) {
48122
- for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) > 127) return true;
48123
- return false;
48124
- }
48125
- /**
48126
- * Map from Unicode code point to WinAnsi (Windows-1252) byte value.
48127
- * Only the 0x80-0x9F range differs from Latin-1; everything else maps 1:1
48128
- * for code points 0x00-0xFF.
48129
- */
48130
- const UNICODE_TO_WINANSI = new Map([
48131
- [8364, 128],
48132
- [8218, 130],
48133
- [402, 131],
48134
- [8222, 132],
48135
- [8230, 133],
48136
- [8224, 134],
48137
- [8225, 135],
48138
- [710, 136],
48139
- [8240, 137],
48140
- [352, 138],
48141
- [8249, 139],
48142
- [338, 140],
48143
- [381, 142],
48144
- [8216, 145],
48145
- [8217, 146],
48146
- [8220, 147],
48147
- [8221, 148],
48148
- [8226, 149],
48149
- [8211, 150],
48150
- [8212, 151],
48151
- [732, 152],
48152
- [8482, 153],
48153
- [353, 154],
48154
- [8250, 155],
48155
- [339, 156],
48156
- [382, 158],
48157
- [376, 159]
48158
- ]);
48159
- /**
48160
- * Convert a Unicode code point to a WinAnsi byte value.
48161
- * Returns 0x3F ('?') for unmappable characters.
48162
- */
48163
- function unicodeToWinAnsi(cp) {
48164
- if (cp < 128) return cp;
48165
- if (cp >= 160 && cp <= 255) return cp;
48166
- const mapped = UNICODE_TO_WINANSI.get(cp);
48167
- if (mapped !== void 0) return mapped;
48168
- return 63;
48169
- }
48170
- //#endregion
48171
49797
  //#region src/modules/pdf/render/constants.ts
48172
49798
  /**
48173
49799
  * Line-height multiplier applied to the font size.
@@ -48248,13 +49874,38 @@ onmessage = async (ev) => {
48248
49874
  stream.restore();
48249
49875
  } else stream.fillRect(cell.rect.x, cell.rect.y, cell.rect.width, cell.rect.height, cell.fillColor);
48250
49876
  }
49877
+ /**
49878
+ * Convert Excel textRotation to standard signed degrees.
49879
+ * Excel uses 1-90 for CCW and 91-180 for CW (where 91 = -1°, 180 = -90°).
49880
+ * Returns 0 for non-numeric values (e.g. "vertical").
49881
+ */
49882
+ function excelRotationToDegrees(textRotation) {
49883
+ if (typeof textRotation !== "number") return 0;
49884
+ return textRotation <= 90 ? textRotation : -(textRotation - 90);
49885
+ }
49886
+ /**
49887
+ * Compute the horizontal slant offset for parallelogram borders.
49888
+ * For general rotation angles (not 0°/90°), Excel renders cell borders as a
49889
+ * parallelogram whose left/right edges tilt to match the text rotation angle.
49890
+ * Returns 0 for straight borders (no rotation, 90°, -90°, or vertical stacked).
49891
+ */
49892
+ function computeSlantOffset(textRotation, height) {
49893
+ const degrees = excelRotationToDegrees(textRotation);
49894
+ if (degrees === 0) return 0;
49895
+ const absDeg = Math.abs(degrees);
49896
+ if (absDeg < .01 || absDeg > 89.99) return 0;
49897
+ const radians = absDeg * Math.PI / 180;
49898
+ const offset = height * Math.cos(radians) / Math.sin(radians);
49899
+ return degrees < 0 ? -offset : offset;
49900
+ }
48251
49901
  function drawCellBorders(stream, cell) {
48252
- const { rect, borders } = cell;
49902
+ const { rect, borders, textRotation } = cell;
48253
49903
  const { x, y, width, height } = rect;
48254
- if (borders.top) drawBorderLine(stream, borders.top, x, y + height, x + width, y + height, true);
49904
+ const slant = computeSlantOffset(textRotation, height);
49905
+ if (borders.top) drawBorderLine(stream, borders.top, x + slant, y + height, x + width + slant, y + height, true);
48255
49906
  if (borders.bottom) drawBorderLine(stream, borders.bottom, x, y, x + width, y, true);
48256
- if (borders.left) drawBorderLine(stream, borders.left, x, y, x, y + height, false);
48257
- if (borders.right) drawBorderLine(stream, borders.right, x + width, y, x + width, y + height, false);
49907
+ if (borders.left) drawBorderLine(stream, borders.left, x, y, x + slant, y + height, false);
49908
+ if (borders.right) drawBorderLine(stream, borders.right, x + width, y, x + width + slant, y + height, false);
48258
49909
  }
48259
49910
  function drawBorderLine(stream, border, x1, y1, x2, y2, isHorizontal) {
48260
49911
  if (border.isDouble) {
@@ -48280,7 +49931,14 @@ onmessage = async (ev) => {
48280
49931
  const indentPts = cell.indent * 10 * scaleFactor;
48281
49932
  const clipWidth = rect.width + (cell.textOverflowWidth || 0);
48282
49933
  stream.save();
48283
- stream.rect(rect.x, rect.y, clipWidth, rect.height);
49934
+ const slantClip = computeSlantOffset(cell.textRotation, rect.height);
49935
+ if (slantClip !== 0) {
49936
+ stream.moveTo(rect.x, rect.y);
49937
+ stream.lineTo(rect.x + clipWidth, rect.y);
49938
+ stream.lineTo(rect.x + clipWidth + slantClip, rect.y + rect.height);
49939
+ stream.lineTo(rect.x + slantClip, rect.y + rect.height);
49940
+ stream.closePath();
49941
+ } else stream.rect(rect.x, rect.y, clipWidth, rect.height);
48284
49942
  stream.clip();
48285
49943
  stream.endPath();
48286
49944
  const textAlpha = cell.textColor.a;
@@ -48444,9 +50102,7 @@ onmessage = async (ev) => {
48444
50102
  const padH = 3 * scaleFactor;
48445
50103
  const padV = 2 * scaleFactor;
48446
50104
  const resourceName = fontManager.hasEmbeddedFont() ? fontManager.getEmbeddedResourceName() : fontManager.ensureFont(resolvePdfFontName(cell.fontFamily, cell.bold, cell.italic));
48447
- let degrees;
48448
- if (typeof cell.textRotation === "number") degrees = cell.textRotation <= 90 ? cell.textRotation : -(cell.textRotation - 90);
48449
- else degrees = 0;
50105
+ const degrees = excelRotationToDegrees(cell.textRotation);
48450
50106
  const radians = degrees * Math.PI / 180;
48451
50107
  const cos = Math.cos(radians);
48452
50108
  const sin = Math.sin(radians);
@@ -48491,7 +50147,7 @@ onmessage = async (ev) => {
48491
50147
  const { rect, horizontalAlign, verticalAlign } = cell;
48492
50148
  const totalColumnsWidth = lines.length * lineHeight;
48493
50149
  let startX;
48494
- if (horizontalAlign === "center" || lines.length === 1) startX = rect.x + rect.width / 2 - totalColumnsWidth / 2 + ascent;
50150
+ if (horizontalAlign === "center") startX = rect.x + rect.width / 2 - totalColumnsWidth / 2 + ascent;
48495
50151
  else if (horizontalAlign === "right") startX = rect.x + rect.width - padH - totalColumnsWidth + ascent;
48496
50152
  else startX = rect.x + padH + ascent;
48497
50153
  for (let i = 0; i < lines.length; i++) {
@@ -48499,9 +50155,9 @@ onmessage = async (ev) => {
48499
50155
  const lineWidth = fontManager.measureText(line, resourceName, fontSize);
48500
50156
  const colX = startX + i * lineHeight;
48501
50157
  let ty;
48502
- if (verticalAlign === "top") ty = rect.y + padV;
50158
+ if (verticalAlign === "top") ty = rect.y + rect.height - padV - lineWidth;
48503
50159
  else if (verticalAlign === "middle") ty = rect.y + (rect.height - lineWidth) / 2;
48504
- else ty = rect.y + rect.height - padV - lineWidth;
50160
+ else ty = rect.y + padV;
48505
50161
  ty = Math.max(ty, rect.y + padV);
48506
50162
  stream.beginText();
48507
50163
  stream.setFont(resourceName, fontSize);
@@ -48515,7 +50171,7 @@ onmessage = async (ev) => {
48515
50171
  const { rect, horizontalAlign, verticalAlign } = cell;
48516
50172
  const totalColumnsWidth = lines.length * lineHeight;
48517
50173
  let startX;
48518
- if (horizontalAlign === "center" || lines.length === 1) startX = rect.x + rect.width / 2 + totalColumnsWidth / 2 - lineHeight + ascent;
50174
+ if (horizontalAlign === "center") startX = rect.x + rect.width / 2 + totalColumnsWidth / 2 - lineHeight + ascent;
48519
50175
  else if (horizontalAlign === "right") startX = rect.x + rect.width - padH - lineHeight + ascent;
48520
50176
  else startX = rect.x + padH + totalColumnsWidth - lineHeight + ascent;
48521
50177
  for (let i = 0; i < lines.length; i++) {
@@ -48536,10 +50192,31 @@ onmessage = async (ev) => {
48536
50192
  }
48537
50193
  /** General rotation — center a multi-line text block in the cell. */
48538
50194
  function drawRotatedGeneral(stream, cell, lines, fontManager, resourceName, fontSize, lineHeight, ascent, cos, sin, indentPts) {
48539
- const { rect, horizontalAlign } = cell;
50195
+ const { rect, horizontalAlign, verticalAlign } = cell;
50196
+ const padH = 3;
50197
+ const padV = 2;
50198
+ let maxLineWidth = 0;
50199
+ for (const line of lines) {
50200
+ const w = fontManager.measureText(line, resourceName, fontSize);
50201
+ if (w > maxLineWidth) maxLineWidth = w;
50202
+ }
50203
+ const totalTextHeight = lines.length * lineHeight;
50204
+ const absSin = Math.abs(sin);
50205
+ const absCos = Math.abs(cos);
50206
+ const rotatedWidth = maxLineWidth * absCos + totalTextHeight * absSin;
50207
+ const rotatedHeight = maxLineWidth * absSin + totalTextHeight * absCos;
50208
+ const slantShift = computeSlantOffset(cell.textRotation, rect.height) / 2;
48540
50209
  const indentOffset = horizontalAlign === "left" ? indentPts / 2 : horizontalAlign === "right" ? -indentPts / 2 : 0;
48541
- const cx = rect.x + rect.width / 2 + indentOffset;
48542
- const cy = rect.y + rect.height / 2;
50210
+ let cy;
50211
+ if (verticalAlign === "top") cy = rect.y + rect.height - padV - rotatedHeight / 2;
50212
+ else if (verticalAlign === "bottom") cy = rect.y + padV + rotatedHeight / 2;
50213
+ else cy = rect.y + rect.height / 2;
50214
+ const verticalRatio = rect.height > 0 ? (cy - rect.y) / rect.height : .5;
50215
+ const slantAtCy = slantShift * 2 * verticalRatio;
50216
+ let cx;
50217
+ if (horizontalAlign === "right") cx = rect.x + rect.width - padH - rotatedWidth / 2 + indentOffset + slantAtCy;
50218
+ else if (horizontalAlign === "left") cx = rect.x + padH + rotatedWidth / 2 + indentOffset + slantAtCy;
50219
+ else cx = rect.x + rect.width / 2 + indentOffset + slantAtCy;
48543
50220
  for (let i = 0; i < lines.length; i++) {
48544
50221
  const line = lines[i];
48545
50222
  const lineWidth = fontManager.measureText(line, resourceName, fontSize);
@@ -48566,7 +50243,8 @@ onmessage = async (ev) => {
48566
50243
  * Newlines (\n) start a new column to the right.
48567
50244
  */
48568
50245
  function drawVerticalStackedText(stream, cell, fontManager, _indentPts, scaleFactor = 1) {
48569
- const { rect, text, fontSize } = cell;
50246
+ const { rect, text, fontSize, horizontalAlign, verticalAlign } = cell;
50247
+ const padH = 3 * scaleFactor;
48570
50248
  const padV = 2 * scaleFactor;
48571
50249
  const resourceName = fontManager.hasEmbeddedFont() ? fontManager.getEmbeddedResourceName() : fontManager.ensureFont(resolvePdfFontName(cell.fontFamily, cell.bold, cell.italic));
48572
50250
  const charHeight = fontSize * 1.3;
@@ -48574,12 +50252,19 @@ onmessage = async (ev) => {
48574
50252
  const columns = text.split(/\r?\n/);
48575
50253
  const columnWidth = fontSize * 1.4;
48576
50254
  const totalColumnsWidth = columns.length * columnWidth;
48577
- const startX = rect.x + rect.width / 2 - totalColumnsWidth / 2 + columnWidth / 2;
50255
+ let startX;
50256
+ if (horizontalAlign === "center") startX = rect.x + rect.width / 2 - totalColumnsWidth / 2 + columnWidth / 2;
50257
+ else if (horizontalAlign === "right") startX = rect.x + rect.width - padH - totalColumnsWidth + columnWidth / 2;
50258
+ else startX = rect.x + padH + columnWidth / 2;
48578
50259
  stream.setFillColor(cell.textColor);
48579
50260
  for (let colIdx = 0; colIdx < columns.length; colIdx++) {
48580
50261
  const colText = columns[colIdx];
48581
50262
  const colX = startX + colIdx * columnWidth;
48582
- let currentY = rect.y + rect.height - padV - ascent;
50263
+ const totalTextHeight = colText.length * charHeight;
50264
+ let currentY;
50265
+ if (verticalAlign === "middle") currentY = rect.y + rect.height / 2 + totalTextHeight / 2 - ascent;
50266
+ else if (verticalAlign === "bottom") currentY = rect.y + padV + totalTextHeight - ascent;
50267
+ else currentY = rect.y + rect.height - padV - ascent;
48583
50268
  for (const ch of colText) {
48584
50269
  if (currentY < rect.y + padV) break;
48585
50270
  const charWidth = fontManager.measureText(ch, resourceName, fontSize);
@@ -48729,6 +50414,234 @@ onmessage = async (ev) => {
48729
50414
  stream.endText();
48730
50415
  stream.restore();
48731
50416
  }
50417
+ /** Default values for text watermarks. */
50418
+ const TEXT_WM_DEFAULTS = {
50419
+ fontSize: 54,
50420
+ color: {
50421
+ r: .75,
50422
+ g: .75,
50423
+ b: .75
50424
+ },
50425
+ opacity: .15,
50426
+ rotation: -45,
50427
+ fontFamily: "Helvetica",
50428
+ bold: false,
50429
+ italic: false,
50430
+ repeatSpacingX: 200,
50431
+ repeatSpacingY: 200
50432
+ };
50433
+ /** Default values for image watermarks. */
50434
+ const IMAGE_WM_DEFAULTS = {
50435
+ opacity: .15,
50436
+ rotation: 0,
50437
+ scale: .5,
50438
+ repeatSpacingX: 200,
50439
+ repeatSpacingY: 200
50440
+ };
50441
+ /** Minimum allowed spacing for repeat patterns (prevents infinite loops). */
50442
+ const MIN_REPEAT_SPACING = 10;
50443
+ /**
50444
+ * Render a watermark onto a PDF content stream.
50445
+ * This should be called BEFORE the cell/grid content is rendered so the
50446
+ * watermark sits behind everything (under-content).
50447
+ */
50448
+ function renderWatermark(stream, page, watermark, fontManager) {
50449
+ if (watermark.type === "text") return renderTextWatermark(stream, page, normalizeTextWatermark(watermark), fontManager);
50450
+ return renderImageWatermark(stream, page, normalizeImageWatermark(watermark));
50451
+ }
50452
+ /** Clamp/normalize text watermark options to safe ranges. */
50453
+ function normalizeTextWatermark(wm) {
50454
+ return {
50455
+ ...wm,
50456
+ opacity: clamp01(wm.opacity ?? TEXT_WM_DEFAULTS.opacity),
50457
+ fontSize: Math.max(1, wm.fontSize ?? TEXT_WM_DEFAULTS.fontSize),
50458
+ repeatSpacingX: Math.max(MIN_REPEAT_SPACING, wm.repeatSpacingX ?? TEXT_WM_DEFAULTS.repeatSpacingX),
50459
+ repeatSpacingY: Math.max(MIN_REPEAT_SPACING, wm.repeatSpacingY ?? TEXT_WM_DEFAULTS.repeatSpacingY)
50460
+ };
50461
+ }
50462
+ /** Clamp/normalize image watermark options to safe ranges. */
50463
+ function normalizeImageWatermark(wm) {
50464
+ return {
50465
+ ...wm,
50466
+ opacity: clamp01(wm.opacity ?? IMAGE_WM_DEFAULTS.opacity),
50467
+ scale: Math.max(.01, wm.scale ?? IMAGE_WM_DEFAULTS.scale),
50468
+ width: wm.width !== void 0 ? Math.max(1, wm.width) : void 0,
50469
+ height: wm.height !== void 0 ? Math.max(1, wm.height) : void 0,
50470
+ repeatSpacingX: Math.max(MIN_REPEAT_SPACING, wm.repeatSpacingX ?? IMAGE_WM_DEFAULTS.repeatSpacingX),
50471
+ repeatSpacingY: Math.max(MIN_REPEAT_SPACING, wm.repeatSpacingY ?? IMAGE_WM_DEFAULTS.repeatSpacingY)
50472
+ };
50473
+ }
50474
+ /** Clamp a number to the 0..1 range. */
50475
+ function clamp01(v) {
50476
+ return Math.max(0, Math.min(1, v));
50477
+ }
50478
+ /**
50479
+ * Render a text watermark on a single page.
50480
+ */
50481
+ function renderTextWatermark(stream, page, watermark, fontManager) {
50482
+ const fontSize = watermark.fontSize ?? TEXT_WM_DEFAULTS.fontSize;
50483
+ const color = watermark.color ?? TEXT_WM_DEFAULTS.color;
50484
+ const opacity = watermark.opacity ?? TEXT_WM_DEFAULTS.opacity;
50485
+ const rotation = watermark.rotation ?? TEXT_WM_DEFAULTS.rotation;
50486
+ const fontFamily = watermark.fontFamily ?? TEXT_WM_DEFAULTS.fontFamily;
50487
+ const bold = watermark.bold ?? TEXT_WM_DEFAULTS.bold;
50488
+ const italic = watermark.italic ?? TEXT_WM_DEFAULTS.italic;
50489
+ const resourceName = fontManager.hasEmbeddedFont() ? fontManager.getEmbeddedResourceName() : fontManager.ensureFont(resolvePdfFontName(fontFamily, bold, italic));
50490
+ const textWidth = fontManager.measureText(watermark.text, resourceName, fontSize);
50491
+ const textHeight = fontSize * .7;
50492
+ const radians = rotation * Math.PI / 180;
50493
+ const cos = Math.cos(radians);
50494
+ const sin = Math.sin(radians);
50495
+ const needsAlpha = opacity < 1;
50496
+ const gsName = needsAlpha ? alphaGsName(opacity) : "";
50497
+ const drawSingleWatermark = (cx, cy) => {
50498
+ const halfW = textWidth / 2;
50499
+ const halfH = textHeight / 2;
50500
+ const tx = cx - halfW * cos + halfH * sin;
50501
+ const ty = cy - halfW * sin - halfH * cos;
50502
+ stream.save();
50503
+ if (needsAlpha) stream.setGraphicsState(gsName);
50504
+ stream.setFillColor(color);
50505
+ stream.beginText();
50506
+ stream.setFont(resourceName, fontSize);
50507
+ stream.setTextMatrix(cos, sin, -sin, cos, tx, ty);
50508
+ const hex = fontManager.encodeText(watermark.text, resourceName);
50509
+ if (hex) stream.showTextHex(hex);
50510
+ else stream.showText(watermark.text);
50511
+ stream.endText();
50512
+ stream.restore();
50513
+ };
50514
+ if (watermark.repeat) {
50515
+ const spacingX = watermark.repeatSpacingX ?? TEXT_WM_DEFAULTS.repeatSpacingX;
50516
+ const spacingY = watermark.repeatSpacingY ?? TEXT_WM_DEFAULTS.repeatSpacingY;
50517
+ renderRepeatedPattern(page.width, page.height, spacingX, spacingY, drawSingleWatermark);
50518
+ } else {
50519
+ const { cx, cy } = resolveWatermarkCenter(page, watermark.position);
50520
+ drawSingleWatermark(cx, cy);
50521
+ }
50522
+ return {
50523
+ alphaValues: needsAlpha ? [opacity] : [],
50524
+ imageXObjects: []
50525
+ };
50526
+ }
50527
+ /**
50528
+ * Render an image watermark on a single page.
50529
+ */
50530
+ function renderImageWatermark(stream, page, watermark) {
50531
+ const opacity = watermark.opacity ?? IMAGE_WM_DEFAULTS.opacity;
50532
+ const rotation = watermark.rotation ?? IMAGE_WM_DEFAULTS.rotation;
50533
+ const scale = watermark.scale ?? IMAGE_WM_DEFAULTS.scale;
50534
+ const needsAlpha = opacity < 1;
50535
+ let imgWidth;
50536
+ let imgHeight;
50537
+ if (watermark.width !== void 0 && watermark.height !== void 0) {
50538
+ imgWidth = watermark.width;
50539
+ imgHeight = watermark.height;
50540
+ } else {
50541
+ const dims = parseImageDimensions(watermark.data, watermark.format);
50542
+ const targetSize = Math.min(page.width, page.height) * scale;
50543
+ const maxDim = Math.max(dims.width, dims.height);
50544
+ const ratio = maxDim > 0 ? targetSize / maxDim : 1;
50545
+ imgWidth = dims.width * ratio;
50546
+ imgHeight = dims.height * ratio;
50547
+ }
50548
+ const radians = rotation * Math.PI / 180;
50549
+ const cos = Math.cos(radians);
50550
+ const sin = Math.sin(radians);
50551
+ const gsName = needsAlpha ? alphaGsName(opacity) : "";
50552
+ const imgName = "WmImg";
50553
+ const drawSingleWatermark = (cx, cy) => {
50554
+ stream.save();
50555
+ if (needsAlpha) stream.setGraphicsState(gsName);
50556
+ const halfW = imgWidth / 2;
50557
+ const halfH = imgHeight / 2;
50558
+ const tx = cx - halfW * cos + halfH * sin;
50559
+ const ty = cy - halfW * sin - halfH * cos;
50560
+ stream.concat(imgWidth * cos, imgWidth * sin, -imgHeight * sin, imgHeight * cos, tx, ty);
50561
+ stream.doXObject(imgName);
50562
+ stream.restore();
50563
+ };
50564
+ if (watermark.repeat) {
50565
+ const spacingX = watermark.repeatSpacingX ?? IMAGE_WM_DEFAULTS.repeatSpacingX;
50566
+ const spacingY = watermark.repeatSpacingY ?? IMAGE_WM_DEFAULTS.repeatSpacingY;
50567
+ renderRepeatedPattern(page.width, page.height, spacingX, spacingY, drawSingleWatermark);
50568
+ } else {
50569
+ const { cx, cy } = resolveWatermarkCenter(page, watermark.position);
50570
+ drawSingleWatermark(cx, cy);
50571
+ }
50572
+ return {
50573
+ alphaValues: needsAlpha ? [opacity] : [],
50574
+ imageXObjects: [{
50575
+ name: imgName,
50576
+ data: watermark.data,
50577
+ format: watermark.format
50578
+ }]
50579
+ };
50580
+ }
50581
+ /**
50582
+ * Parse image dimensions from raw JPEG or PNG data without a full decode.
50583
+ */
50584
+ function parseImageDimensions(data, format) {
50585
+ if (format === "png") return parsePngDimensions(data);
50586
+ return parseJpegDimensions(data);
50587
+ }
50588
+ /** Read width/height from a PNG IHDR chunk (bytes 16-23). */
50589
+ function parsePngDimensions(data) {
50590
+ if (data.length >= 24 && data[12] === 73 && data[13] === 72 && data[14] === 68 && data[15] === 82) return {
50591
+ width: data[16] << 24 | data[17] << 16 | data[18] << 8 | data[19],
50592
+ height: data[20] << 24 | data[21] << 16 | data[22] << 8 | data[23]
50593
+ };
50594
+ return {
50595
+ width: 1,
50596
+ height: 1
50597
+ };
50598
+ }
50599
+ /** Read width/height from JPEG SOF marker. */
50600
+ function parseJpegDimensions(data) {
50601
+ let offset = 2;
50602
+ while (offset < data.length - 1) {
50603
+ while (offset < data.length && data[offset] === 255 && data[offset + 1] === 255) offset++;
50604
+ if (offset >= data.length - 1 || data[offset] !== 255) break;
50605
+ const marker = data[offset + 1];
50606
+ if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204 && offset + 8 < data.length) return {
50607
+ width: data[offset + 7] << 8 | data[offset + 8],
50608
+ height: data[offset + 5] << 8 | data[offset + 6]
50609
+ };
50610
+ if (offset + 3 >= data.length) break;
50611
+ const segLen = data[offset + 2] << 8 | data[offset + 3];
50612
+ offset += 2 + segLen;
50613
+ }
50614
+ return {
50615
+ width: 1,
50616
+ height: 1
50617
+ };
50618
+ }
50619
+ /**
50620
+ * Resolve the center position for a watermark on a given page.
50621
+ */
50622
+ function resolveWatermarkCenter(page, position) {
50623
+ if (!position || position === "center") return {
50624
+ cx: page.width / 2,
50625
+ cy: page.height / 2
50626
+ };
50627
+ return {
50628
+ cx: position.x,
50629
+ cy: position.y
50630
+ };
50631
+ }
50632
+ /**
50633
+ * Render a repeated pattern of watermarks across the entire page.
50634
+ * Uses a staggered grid for a natural diagonal tiling effect.
50635
+ */
50636
+ function renderRepeatedPattern(pageWidth, pageHeight, spacingX, spacingY, drawFn) {
50637
+ const margin = Math.max(pageWidth, pageHeight) * .5;
50638
+ let rowIndex = 0;
50639
+ for (let y = -margin; y < pageHeight + margin; y += spacingY) {
50640
+ const offsetX = rowIndex % 2 === 1 ? spacingX / 2 : 0;
50641
+ for (let x = -margin; x < pageWidth + margin; x += spacingX) drawFn(x + offsetX, y);
50642
+ rowIndex++;
50643
+ }
50644
+ }
48732
50645
  //#endregion
48733
50646
  //#region src/modules/pdf/render/layout-engine.ts
48734
50647
  const DEFAULT_COLUMN_WIDTH = 8.43;
@@ -49631,7 +51544,15 @@ onmessage = async (ev) => {
49631
51544
  ensureAtLeastOnePage(allPages, documentOptions, sheets);
49632
51545
  fixPageNumbers(allPages);
49633
51546
  trackFontsForHeaders(allPages, fontManager);
49634
- const { pageObjNums, sheetFirstPage, pagesTreeObjNum } = await renderAllPages(allPages, fontManager, writer, fontManager.writeFontResources(writer));
51547
+ const watermark = documentOptions.watermark;
51548
+ if (watermark && watermark.type === "text") {
51549
+ const wmFontFamily = watermark.fontFamily ?? "Helvetica";
51550
+ const wmBold = watermark.bold ?? false;
51551
+ const wmItalic = watermark.italic ?? false;
51552
+ if (fontManager.hasEmbeddedFont()) fontManager.trackText(watermark.text);
51553
+ else fontManager.ensureFont(resolvePdfFontName(wmFontFamily, wmBold, wmItalic));
51554
+ }
51555
+ const { pageObjNums, sheetFirstPage, pagesTreeObjNum } = await renderAllPages(allPages, fontManager, writer, fontManager.writeFontResources(writer), watermark);
49635
51556
  return buildFinalPdf(writer, pageObjNums, pagesTreeObjNum, sheetFirstPage, documentOptions, workbook, options);
49636
51557
  }
49637
51558
  function ensureAtLeastOnePage(allPages, documentOptions, sheets) {
@@ -49664,13 +51585,13 @@ onmessage = async (ev) => {
49664
51585
  for (const page of allPages) if (page.options.showSheetNames) fontManager.ensureFont(resolvePdfFontName(page.options.defaultFontFamily, true, false));
49665
51586
  }
49666
51587
  }
49667
- async function renderAllPages(allPages, fontManager, writer, fontObjectMap) {
51588
+ async function renderAllPages(allPages, fontManager, writer, fontObjectMap, watermark) {
49668
51589
  const pageObjNums = [];
49669
51590
  const pagesTreeObjNum = writer.allocObject();
49670
51591
  const sheetFirstPage = /* @__PURE__ */ new Map();
49671
51592
  const totalPages = allPages.length;
49672
51593
  for (let i = 0; i < allPages.length; i++) {
49673
- renderSinglePage(allPages[i], fontManager, writer, fontObjectMap, totalPages, pageObjNums, pagesTreeObjNum, sheetFirstPage);
51594
+ renderSinglePage(allPages[i], fontManager, writer, fontObjectMap, totalPages, pageObjNums, pagesTreeObjNum, sheetFirstPage, watermark);
49674
51595
  if (i < allPages.length - 1) await yieldToEventLoop();
49675
51596
  }
49676
51597
  return {
@@ -49679,7 +51600,7 @@ onmessage = async (ev) => {
49679
51600
  pagesTreeObjNum
49680
51601
  };
49681
51602
  }
49682
- function renderSinglePage(page, fontManager, writer, fontObjectMap, totalPages, pageObjNums, pagesTreeObjNum, sheetFirstPage) {
51603
+ function renderSinglePage(page, fontManager, writer, fontObjectMap, totalPages, pageObjNums, pagesTreeObjNum, sheetFirstPage, watermark) {
49683
51604
  try {
49684
51605
  const { stream: contentStream, alphaValues } = renderPage(page, page.options, fontManager, totalPages);
49685
51606
  const imageXObjects = /* @__PURE__ */ new Map();
@@ -49690,9 +51611,24 @@ onmessage = async (ev) => {
49690
51611
  imageXObjects.set(imgName, imgObjNum);
49691
51612
  contentStream.drawImage(imgName, img.rect.x, img.rect.y, img.rect.width, img.rect.height);
49692
51613
  }
51614
+ let watermarkContentObjNum;
51615
+ if (watermark && isWatermarkApplicable(watermark, page)) {
51616
+ const wmContentStream = new PdfContentStream();
51617
+ const wmResult = renderWatermark(wmContentStream, page, watermark, fontManager);
51618
+ for (const alpha of wmResult.alphaValues) alphaValues.add(alpha);
51619
+ for (const wmImg of wmResult.imageXObjects) {
51620
+ const imgObjNum = writeImageXObject(writer, wmImg.data, wmImg.format);
51621
+ imageXObjects.set(wmImg.name, imgObjNum);
51622
+ }
51623
+ watermarkContentObjNum = writer.allocObject();
51624
+ writer.addStreamObject(watermarkContentObjNum, new PdfDict(), wmContentStream);
51625
+ }
49693
51626
  const contentObjNum = writer.allocObject();
49694
- const contentDict = new PdfDict();
49695
- writer.addStreamObject(contentObjNum, contentDict, contentStream);
51627
+ writer.addStreamObject(contentObjNum, new PdfDict(), contentStream);
51628
+ let contentsRef;
51629
+ if (watermarkContentObjNum) if ((watermark?.placement ?? "under") === "over") contentsRef = `[${pdfRef(contentObjNum)} ${pdfRef(watermarkContentObjNum)}]`;
51630
+ else contentsRef = `[${pdfRef(watermarkContentObjNum)} ${pdfRef(contentObjNum)}]`;
51631
+ else contentsRef = pdfRef(contentObjNum);
49696
51632
  const resourcesObjNum = writer.allocObject();
49697
51633
  const fontDictStr = fontManager.buildFontDictString(fontObjectMap);
49698
51634
  const resourcesDict = new PdfDict().set("Font", fontDictStr);
@@ -49726,7 +51662,7 @@ onmessage = async (ev) => {
49726
51662
  parentRef: pagesTreeObjNum,
49727
51663
  width: page.width,
49728
51664
  height: page.height,
49729
- contentsRef: contentObjNum,
51665
+ contentsRef,
49730
51666
  resourcesRef: resourcesObjNum,
49731
51667
  annotRefs: annotRefs.length > 0 ? annotRefs : void 0
49732
51668
  });
@@ -49805,7 +51741,8 @@ onmessage = async (ev) => {
49805
51741
  title: options?.title ?? "",
49806
51742
  author: options?.author ?? "",
49807
51743
  subject: options?.subject ?? "",
49808
- creator: options?.creator ?? "excelts"
51744
+ creator: options?.creator ?? "excelts",
51745
+ watermark: options?.watermark
49809
51746
  };
49810
51747
  }
49811
51748
  /** Map PaperSize enum values to PDF page sizes. */
@@ -49872,6 +51809,20 @@ onmessage = async (ev) => {
49872
51809
  return outlinesObjNum;
49873
51810
  }
49874
51811
  /**
51812
+ * Check if a watermark should be applied to a specific page based on
51813
+ * optional page number and sheet name filters.
51814
+ */
51815
+ function isWatermarkApplicable(watermark, page) {
51816
+ if (watermark.pages && watermark.pages.length > 0) {
51817
+ if (!watermark.pages.includes(page.pageNumber)) return false;
51818
+ }
51819
+ if (watermark.sheets && watermark.sheets.length > 0) {
51820
+ const sheetLower = page.sheetName.toLowerCase();
51821
+ if (!watermark.sheets.some((s) => s.toLowerCase() === sheetLower)) return false;
51822
+ }
51823
+ return true;
51824
+ }
51825
+ /**
49875
51826
  * Write a JPEG or PNG image as a PDF XObject Image.
49876
51827
  */
49877
51828
  function writeImageXObject(writer, data, format) {
@@ -49883,7 +51834,7 @@ onmessage = async (ev) => {
49883
51834
  */
49884
51835
  function writeJpegImageXObject(writer, data) {
49885
51836
  const objNum = writer.allocObject();
49886
- const dims = getJpegDimensions(data);
51837
+ const dims = parseImageDimensions(data, "jpeg");
49887
51838
  const dict = new PdfDict().set("Type", "/XObject").set("Subtype", "/Image").set("Width", pdfNumber(dims.width)).set("Height", pdfNumber(dims.height)).set("ColorSpace", "/DeviceRGB").set("BitsPerComponent", "8").set("Filter", "/DCTDecode");
49888
51839
  writer.addStreamObject(objNum, dict, data);
49889
51840
  return objNum;
@@ -49904,37 +51855,6 @@ onmessage = async (ev) => {
49904
51855
  writer.addStreamObject(objNum, dict, png.pixels);
49905
51856
  return objNum;
49906
51857
  }
49907
- /**
49908
- * Extract width and height from a JPEG file header.
49909
- * Scans for SOF markers (SOF0-SOF3, SOF5-SOF7, SOF9-SOF11, SOF13-SOF15)
49910
- * which all share the same frame header layout.
49911
- * Handles 0xFF padding bytes per JPEG spec.
49912
- */
49913
- function getJpegDimensions(data) {
49914
- let offset = 2;
49915
- while (offset < data.length - 1) {
49916
- while (offset < data.length && data[offset] === 255 && data[offset + 1] === 255) offset++;
49917
- if (offset >= data.length - 1 || data[offset] !== 255) break;
49918
- const marker = data[offset + 1];
49919
- if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
49920
- if (offset + 8 < data.length) {
49921
- const height = data[offset + 5] << 8 | data[offset + 6];
49922
- return {
49923
- width: data[offset + 7] << 8 | data[offset + 8],
49924
- height
49925
- };
49926
- }
49927
- break;
49928
- }
49929
- if (offset + 3 >= data.length) break;
49930
- const segLen = data[offset + 2] << 8 | data[offset + 3];
49931
- offset += 2 + segLen;
49932
- }
49933
- return {
49934
- width: 1,
49935
- height: 1
49936
- };
49937
- }
49938
51858
  //#endregion
49939
51859
  //#region src/modules/pdf/pdf.ts
49940
51860
  /**
@@ -50509,6 +52429,7 @@ onmessage = async (ev) => {
50509
52429
  exports.concatUint8Arrays = concatUint8Arrays;
50510
52430
  exports.createCsvFormatterStream = createCsvFormatterStream;
50511
52431
  exports.createCsvParserStream = createCsvParserStream;
52432
+ exports.createTextWatermarkImage = createTextWatermarkImage;
50512
52433
  exports.dateToExcel = dateToExcel;
50513
52434
  exports.decodeCell = decodeCell;
50514
52435
  exports.decodeCol = decodeCol;