@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
@@ -16,6 +16,8 @@ exports.alphaGsName = alphaGsName;
16
16
  exports.computeTextStartY = computeTextStartY;
17
17
  exports.computeTextX = computeTextX;
18
18
  exports.wrapTextLines = wrapTextLines;
19
+ exports.renderWatermark = renderWatermark;
20
+ exports.parseImageDimensions = parseImageDimensions;
19
21
  const pdf_stream_1 = require("../core/pdf-stream");
20
22
  const font_manager_1 = require("../font/font-manager");
21
23
  const constants_1 = require("./constants");
@@ -115,22 +117,57 @@ function drawCellFill(stream, cell, alphaValues) {
115
117
  }
116
118
  }
117
119
  // =============================================================================
120
+ // Rotation Helpers
121
+ // =============================================================================
122
+ /**
123
+ * Convert Excel textRotation to standard signed degrees.
124
+ * Excel uses 1-90 for CCW and 91-180 for CW (where 91 = -1°, 180 = -90°).
125
+ * Returns 0 for non-numeric values (e.g. "vertical").
126
+ */
127
+ function excelRotationToDegrees(textRotation) {
128
+ if (typeof textRotation !== "number") {
129
+ return 0;
130
+ }
131
+ return textRotation <= 90 ? textRotation : -(textRotation - 90);
132
+ }
133
+ // =============================================================================
118
134
  // Cell Borders
119
135
  // =============================================================================
136
+ /**
137
+ * Compute the horizontal slant offset for parallelogram borders.
138
+ * For general rotation angles (not 0°/90°), Excel renders cell borders as a
139
+ * parallelogram whose left/right edges tilt to match the text rotation angle.
140
+ * Returns 0 for straight borders (no rotation, 90°, -90°, or vertical stacked).
141
+ */
142
+ function computeSlantOffset(textRotation, height) {
143
+ const degrees = excelRotationToDegrees(textRotation);
144
+ if (degrees === 0) {
145
+ return 0;
146
+ }
147
+ const absDeg = Math.abs(degrees);
148
+ if (absDeg < 0.01 || absDeg > 89.99) {
149
+ return 0;
150
+ }
151
+ const radians = (absDeg * Math.PI) / 180;
152
+ const offset = (height * Math.cos(radians)) / Math.sin(radians);
153
+ return degrees < 0 ? -offset : offset;
154
+ }
120
155
  function drawCellBorders(stream, cell) {
121
- const { rect, borders } = cell;
156
+ const { rect, borders, textRotation } = cell;
122
157
  const { x, y, width, height } = rect;
158
+ // Compute slant for parallelogram borders on general-angle rotated cells
159
+ const slant = computeSlantOffset(textRotation, height);
123
160
  if (borders.top) {
124
- drawBorderLine(stream, borders.top, x, y + height, x + width, y + height, true);
161
+ drawBorderLine(stream, borders.top, x + slant, y + height, x + width + slant, y + height, true);
125
162
  }
126
163
  if (borders.bottom) {
127
164
  drawBorderLine(stream, borders.bottom, x, y, x + width, y, true);
128
165
  }
129
166
  if (borders.left) {
130
- drawBorderLine(stream, borders.left, x, y, x, y + height, false);
167
+ drawBorderLine(stream, borders.left, x, y, x + slant, y + height, false);
131
168
  }
132
169
  if (borders.right) {
133
- drawBorderLine(stream, borders.right, x + width, y, x + width, y + height, false);
170
+ drawBorderLine(stream, borders.right, x + width, y, x + width + slant, y + height, false);
134
171
  }
135
172
  }
136
173
  function drawBorderLine(stream, border, x1, y1, x2, y2, isHorizontal) {
@@ -168,9 +205,21 @@ function drawCellText(stream, cell, fontManager, alphaValues, scaleFactor = 1) {
168
205
  }
169
206
  const indentPts = cell.indent * constants_1.INDENT_WIDTH * scaleFactor;
170
207
  // Clip to cell bounds (extend for text overflow into adjacent empty cells)
208
+ // For rotated text with slanted borders, use a parallelogram clip path
171
209
  const clipWidth = rect.width + (cell.textOverflowWidth || 0);
172
210
  stream.save();
173
- stream.rect(rect.x, rect.y, clipWidth, rect.height);
211
+ const slantClip = computeSlantOffset(cell.textRotation, rect.height);
212
+ if (slantClip !== 0) {
213
+ // Parallelogram clip: bottom-left, bottom-right, top-right (shifted), top-left (shifted)
214
+ stream.moveTo(rect.x, rect.y);
215
+ stream.lineTo(rect.x + clipWidth, rect.y);
216
+ stream.lineTo(rect.x + clipWidth + slantClip, rect.y + rect.height);
217
+ stream.lineTo(rect.x + slantClip, rect.y + rect.height);
218
+ stream.closePath();
219
+ }
220
+ else {
221
+ stream.rect(rect.x, rect.y, clipWidth, rect.height);
222
+ }
174
223
  stream.clip();
175
224
  stream.endPath();
176
225
  // Apply text color alpha if needed
@@ -399,14 +448,7 @@ function drawRotatedText(stream, cell, fontManager, indentPts, scaleFactor = 1)
399
448
  ? fontManager.getEmbeddedResourceName()
400
449
  : fontManager.ensureFont((0, font_manager_1.resolvePdfFontName)(cell.fontFamily, cell.bold, cell.italic));
401
450
  // Convert Excel rotation to degrees
402
- // 1-90: counterclockwise, 91-180: clockwise (value-90 degrees)
403
- let degrees;
404
- if (typeof cell.textRotation === "number") {
405
- degrees = cell.textRotation <= 90 ? cell.textRotation : -(cell.textRotation - 90);
406
- }
407
- else {
408
- degrees = 0;
409
- }
451
+ const degrees = excelRotationToDegrees(cell.textRotation);
410
452
  const radians = (degrees * Math.PI) / 180;
411
453
  const cos = Math.cos(radians);
412
454
  const sin = Math.sin(radians);
@@ -476,31 +518,35 @@ function drawRotatedText(stream, cell, fontManager, indentPts, scaleFactor = 1)
476
518
  function drawRotated90(stream, cell, lines, fontManager, resourceName, fontSize, lineHeight, ascent, padH, padV) {
477
519
  const { rect, horizontalAlign, verticalAlign } = cell;
478
520
  const totalColumnsWidth = lines.length * lineHeight;
479
- // Horizontal centering of line columns
521
+ // horizontalAlign controls X placement of line columns (same visual axis)
480
522
  let startX;
481
- if (horizontalAlign === "center" || lines.length === 1) {
523
+ if (horizontalAlign === "center") {
482
524
  startX = rect.x + rect.width / 2 - totalColumnsWidth / 2 + ascent;
483
525
  }
484
526
  else if (horizontalAlign === "right") {
485
527
  startX = rect.x + rect.width - padH - totalColumnsWidth + ascent;
486
528
  }
487
529
  else {
530
+ // left (default)
488
531
  startX = rect.x + padH + ascent;
489
532
  }
490
533
  for (let i = 0; i < lines.length; i++) {
491
534
  const line = lines[i];
492
535
  const lineWidth = fontManager.measureText(line, resourceName, fontSize);
493
536
  const colX = startX + i * lineHeight;
494
- // Vertical positioning: text flows upward from bottom
537
+ // verticalAlign controls Y placement (text flows upward from ty)
538
+ // In PDF coords: higher y = top of cell
495
539
  let ty;
496
540
  if (verticalAlign === "top") {
497
- ty = rect.y + padV;
541
+ // text at top → text end near top → ty starts at bottom so text reaches top
542
+ ty = rect.y + rect.height - padV - lineWidth;
498
543
  }
499
544
  else if (verticalAlign === "middle") {
500
545
  ty = rect.y + (rect.height - lineWidth) / 2;
501
546
  }
502
547
  else {
503
- ty = rect.y + rect.height - padV - lineWidth;
548
+ // bottom (default) text at bottom ty near bottom
549
+ ty = rect.y + padV;
504
550
  }
505
551
  ty = Math.max(ty, rect.y + padV);
506
552
  stream.beginText();
@@ -514,28 +560,34 @@ function drawRotated90(stream, cell, lines, fontManager, resourceName, fontSize,
514
560
  function drawRotatedMinus90(stream, cell, lines, fontManager, resourceName, fontSize, lineHeight, ascent, padH, padV) {
515
561
  const { rect, horizontalAlign, verticalAlign } = cell;
516
562
  const totalColumnsWidth = lines.length * lineHeight;
563
+ // horizontalAlign controls X placement: lines stack right-to-left
517
564
  let startX;
518
- if (horizontalAlign === "center" || lines.length === 1) {
565
+ if (horizontalAlign === "center") {
519
566
  startX = rect.x + rect.width / 2 + totalColumnsWidth / 2 - lineHeight + ascent;
520
567
  }
521
568
  else if (horizontalAlign === "right") {
522
569
  startX = rect.x + rect.width - padH - lineHeight + ascent;
523
570
  }
524
571
  else {
572
+ // left (default)
525
573
  startX = rect.x + padH + totalColumnsWidth - lineHeight + ascent;
526
574
  }
527
575
  for (let i = 0; i < lines.length; i++) {
528
576
  const line = lines[i];
529
577
  const lineWidth = fontManager.measureText(line, resourceName, fontSize);
530
578
  const colX = startX - i * lineHeight;
579
+ // verticalAlign controls Y placement (text flows downward from ty)
580
+ // In PDF coords: higher y = top of cell; text drawn downward = toward lower y
531
581
  let ty;
532
582
  if (verticalAlign === "top") {
583
+ // text at top → ty near top (high PDF y)
533
584
  ty = rect.y + rect.height - padV;
534
585
  }
535
586
  else if (verticalAlign === "middle") {
536
587
  ty = rect.y + (rect.height + lineWidth) / 2;
537
588
  }
538
589
  else {
590
+ // bottom (default) → text at bottom → ty so text ends at bottom
539
591
  ty = rect.y + padV + lineWidth;
540
592
  }
541
593
  ty = Math.min(ty, rect.y + rect.height - padV);
@@ -548,10 +600,53 @@ function drawRotatedMinus90(stream, cell, lines, fontManager, resourceName, font
548
600
  }
549
601
  /** General rotation — center a multi-line text block in the cell. */
550
602
  function drawRotatedGeneral(stream, cell, lines, fontManager, resourceName, fontSize, lineHeight, ascent, cos, sin, indentPts) {
551
- const { rect, horizontalAlign } = cell;
603
+ const { rect, horizontalAlign, verticalAlign } = cell;
604
+ const padH = constants_1.CELL_PADDING_H;
605
+ const padV = constants_1.CELL_PADDING_V;
606
+ // Compute the rotated bounding box of the text block
607
+ let maxLineWidth = 0;
608
+ for (const line of lines) {
609
+ const w = fontManager.measureText(line, resourceName, fontSize);
610
+ if (w > maxLineWidth) {
611
+ maxLineWidth = w;
612
+ }
613
+ }
614
+ const totalTextHeight = lines.length * lineHeight;
615
+ const absSin = Math.abs(sin);
616
+ const absCos = Math.abs(cos);
617
+ const rotatedWidth = maxLineWidth * absCos + totalTextHeight * absSin;
618
+ const rotatedHeight = maxLineWidth * absSin + totalTextHeight * absCos;
619
+ // Compute slant offset to match parallelogram border shape
620
+ const slantShift = computeSlantOffset(cell.textRotation, rect.height) / 2;
621
+ // Determine vertical position first, then horizontal (because slant depends on Y position)
552
622
  const indentOffset = horizontalAlign === "left" ? indentPts / 2 : horizontalAlign === "right" ? -indentPts / 2 : 0;
553
- const cx = rect.x + rect.width / 2 + indentOffset;
554
- const cy = rect.y + rect.height / 2;
623
+ let cy;
624
+ if (verticalAlign === "top") {
625
+ cy = rect.y + rect.height - padV - rotatedHeight / 2;
626
+ }
627
+ else if (verticalAlign === "bottom") {
628
+ cy = rect.y + padV + rotatedHeight / 2;
629
+ }
630
+ else {
631
+ // middle (default)
632
+ cy = rect.y + rect.height / 2;
633
+ }
634
+ // For slanted parallelogram, the horizontal offset depends on the vertical position
635
+ // At bottom (y), left edge is at x; at top (y+height), left edge is at x+slantOffset
636
+ // At cy, the horizontal shift is proportional: slantOffset * (cy - y) / height
637
+ const verticalRatio = rect.height > 0 ? (cy - rect.y) / rect.height : 0.5;
638
+ const slantAtCy = slantShift * 2 * verticalRatio; // slantShift*2 = full slantOffset
639
+ let cx;
640
+ if (horizontalAlign === "right") {
641
+ cx = rect.x + rect.width - padH - rotatedWidth / 2 + indentOffset + slantAtCy;
642
+ }
643
+ else if (horizontalAlign === "left") {
644
+ cx = rect.x + padH + rotatedWidth / 2 + indentOffset + slantAtCy;
645
+ }
646
+ else {
647
+ // center (default for rotated)
648
+ cx = rect.x + rect.width / 2 + indentOffset + slantAtCy;
649
+ }
555
650
  for (let i = 0; i < lines.length; i++) {
556
651
  const line = lines[i];
557
652
  const lineWidth = fontManager.measureText(line, resourceName, fontSize);
@@ -582,7 +677,8 @@ function emitText(stream, fontManager, text, resourceName) {
582
677
  * Newlines (\n) start a new column to the right.
583
678
  */
584
679
  function drawVerticalStackedText(stream, cell, fontManager, _indentPts, scaleFactor = 1) {
585
- const { rect, text, fontSize } = cell;
680
+ const { rect, text, fontSize, horizontalAlign, verticalAlign } = cell;
681
+ const padH = constants_1.CELL_PADDING_H * scaleFactor;
586
682
  const padV = constants_1.CELL_PADDING_V * scaleFactor;
587
683
  const isEmbedded = fontManager.hasEmbeddedFont();
588
684
  const resourceName = isEmbedded
@@ -594,12 +690,35 @@ function drawVerticalStackedText(stream, cell, fontManager, _indentPts, scaleFac
594
690
  const columns = text.split(/\r?\n/);
595
691
  const columnWidth = fontSize * 1.4;
596
692
  const totalColumnsWidth = columns.length * columnWidth;
597
- const startX = rect.x + rect.width / 2 - totalColumnsWidth / 2 + columnWidth / 2;
693
+ // Horizontal alignment controls column X positioning
694
+ let startX;
695
+ if (horizontalAlign === "center") {
696
+ startX = rect.x + rect.width / 2 - totalColumnsWidth / 2 + columnWidth / 2;
697
+ }
698
+ else if (horizontalAlign === "right") {
699
+ startX = rect.x + rect.width - padH - totalColumnsWidth + columnWidth / 2;
700
+ }
701
+ else {
702
+ // left (default)
703
+ startX = rect.x + padH + columnWidth / 2;
704
+ }
598
705
  stream.setFillColor(cell.textColor);
599
706
  for (let colIdx = 0; colIdx < columns.length; colIdx++) {
600
707
  const colText = columns[colIdx];
601
708
  const colX = startX + colIdx * columnWidth;
602
- let currentY = rect.y + rect.height - padV - ascent;
709
+ const totalTextHeight = colText.length * charHeight;
710
+ // Vertical alignment controls starting Y position (PDF y-axis: higher = top of cell)
711
+ let currentY;
712
+ if (verticalAlign === "middle") {
713
+ currentY = rect.y + rect.height / 2 + totalTextHeight / 2 - ascent;
714
+ }
715
+ else if (verticalAlign === "bottom") {
716
+ currentY = rect.y + padV + totalTextHeight - ascent;
717
+ }
718
+ else {
719
+ // top (default)
720
+ currentY = rect.y + rect.height - padV - ascent;
721
+ }
603
722
  for (const ch of colText) {
604
723
  if (currentY < rect.y + padV) {
605
724
  break;
@@ -780,3 +899,255 @@ function drawPageFooter(stream, page, options, fontManager, totalPages) {
780
899
  stream.endText();
781
900
  stream.restore();
782
901
  }
902
+ // =============================================================================
903
+ // Watermark Rendering
904
+ // =============================================================================
905
+ /** Default values for text watermarks. */
906
+ const TEXT_WM_DEFAULTS = {
907
+ fontSize: 54,
908
+ color: { r: 0.75, g: 0.75, b: 0.75 },
909
+ opacity: 0.15,
910
+ rotation: -45,
911
+ fontFamily: "Helvetica",
912
+ bold: false,
913
+ italic: false,
914
+ repeatSpacingX: 200,
915
+ repeatSpacingY: 200
916
+ };
917
+ /** Default values for image watermarks. */
918
+ const IMAGE_WM_DEFAULTS = {
919
+ opacity: 0.15,
920
+ rotation: 0,
921
+ scale: 0.5,
922
+ repeatSpacingX: 200,
923
+ repeatSpacingY: 200
924
+ };
925
+ /** Minimum allowed spacing for repeat patterns (prevents infinite loops). */
926
+ const MIN_REPEAT_SPACING = 10;
927
+ /**
928
+ * Render a watermark onto a PDF content stream.
929
+ * This should be called BEFORE the cell/grid content is rendered so the
930
+ * watermark sits behind everything (under-content).
931
+ */
932
+ function renderWatermark(stream, page, watermark, fontManager) {
933
+ if (watermark.type === "text") {
934
+ return renderTextWatermark(stream, page, normalizeTextWatermark(watermark), fontManager);
935
+ }
936
+ return renderImageWatermark(stream, page, normalizeImageWatermark(watermark));
937
+ }
938
+ /** Clamp/normalize text watermark options to safe ranges. */
939
+ function normalizeTextWatermark(wm) {
940
+ return {
941
+ ...wm,
942
+ opacity: clamp01(wm.opacity ?? TEXT_WM_DEFAULTS.opacity),
943
+ fontSize: Math.max(1, wm.fontSize ?? TEXT_WM_DEFAULTS.fontSize),
944
+ repeatSpacingX: Math.max(MIN_REPEAT_SPACING, wm.repeatSpacingX ?? TEXT_WM_DEFAULTS.repeatSpacingX),
945
+ repeatSpacingY: Math.max(MIN_REPEAT_SPACING, wm.repeatSpacingY ?? TEXT_WM_DEFAULTS.repeatSpacingY)
946
+ };
947
+ }
948
+ /** Clamp/normalize image watermark options to safe ranges. */
949
+ function normalizeImageWatermark(wm) {
950
+ return {
951
+ ...wm,
952
+ opacity: clamp01(wm.opacity ?? IMAGE_WM_DEFAULTS.opacity),
953
+ scale: Math.max(0.01, wm.scale ?? IMAGE_WM_DEFAULTS.scale),
954
+ width: wm.width !== undefined ? Math.max(1, wm.width) : undefined,
955
+ height: wm.height !== undefined ? Math.max(1, wm.height) : undefined,
956
+ repeatSpacingX: Math.max(MIN_REPEAT_SPACING, wm.repeatSpacingX ?? IMAGE_WM_DEFAULTS.repeatSpacingX),
957
+ repeatSpacingY: Math.max(MIN_REPEAT_SPACING, wm.repeatSpacingY ?? IMAGE_WM_DEFAULTS.repeatSpacingY)
958
+ };
959
+ }
960
+ /** Clamp a number to the 0..1 range. */
961
+ function clamp01(v) {
962
+ return Math.max(0, Math.min(1, v));
963
+ }
964
+ /**
965
+ * Render a text watermark on a single page.
966
+ */
967
+ function renderTextWatermark(stream, page, watermark, fontManager) {
968
+ const fontSize = watermark.fontSize ?? TEXT_WM_DEFAULTS.fontSize;
969
+ const color = watermark.color ?? TEXT_WM_DEFAULTS.color;
970
+ const opacity = watermark.opacity ?? TEXT_WM_DEFAULTS.opacity;
971
+ const rotation = watermark.rotation ?? TEXT_WM_DEFAULTS.rotation;
972
+ const fontFamily = watermark.fontFamily ?? TEXT_WM_DEFAULTS.fontFamily;
973
+ const bold = watermark.bold ?? TEXT_WM_DEFAULTS.bold;
974
+ const italic = watermark.italic ?? TEXT_WM_DEFAULTS.italic;
975
+ const isEmbedded = fontManager.hasEmbeddedFont();
976
+ const resourceName = isEmbedded
977
+ ? fontManager.getEmbeddedResourceName()
978
+ : fontManager.ensureFont((0, font_manager_1.resolvePdfFontName)(fontFamily, bold, italic));
979
+ const textWidth = fontManager.measureText(watermark.text, resourceName, fontSize);
980
+ // Approximate text height using ascent (roughly 0.7 * fontSize for most fonts)
981
+ const textHeight = fontSize * 0.7;
982
+ const radians = (rotation * Math.PI) / 180;
983
+ const cos = Math.cos(radians);
984
+ const sin = Math.sin(radians);
985
+ const needsAlpha = opacity < 1;
986
+ const gsName = needsAlpha ? alphaGsName(opacity) : "";
987
+ const drawSingleWatermark = (cx, cy) => {
988
+ // Center the text at (cx, cy), compensating for both width and ascent height
989
+ const halfW = textWidth / 2;
990
+ const halfH = textHeight / 2;
991
+ const tx = cx - halfW * cos + halfH * sin;
992
+ const ty = cy - halfW * sin - halfH * cos;
993
+ stream.save();
994
+ if (needsAlpha) {
995
+ stream.setGraphicsState(gsName);
996
+ }
997
+ stream.setFillColor(color);
998
+ stream.beginText();
999
+ stream.setFont(resourceName, fontSize);
1000
+ stream.setTextMatrix(cos, sin, -sin, cos, tx, ty);
1001
+ const hex = fontManager.encodeText(watermark.text, resourceName);
1002
+ if (hex) {
1003
+ stream.showTextHex(hex);
1004
+ }
1005
+ else {
1006
+ stream.showText(watermark.text);
1007
+ }
1008
+ stream.endText();
1009
+ stream.restore();
1010
+ };
1011
+ if (watermark.repeat) {
1012
+ const spacingX = watermark.repeatSpacingX ?? TEXT_WM_DEFAULTS.repeatSpacingX;
1013
+ const spacingY = watermark.repeatSpacingY ?? TEXT_WM_DEFAULTS.repeatSpacingY;
1014
+ renderRepeatedPattern(page.width, page.height, spacingX, spacingY, drawSingleWatermark);
1015
+ }
1016
+ else {
1017
+ const { cx, cy } = resolveWatermarkCenter(page, watermark.position);
1018
+ drawSingleWatermark(cx, cy);
1019
+ }
1020
+ return { alphaValues: needsAlpha ? [opacity] : [], imageXObjects: [] };
1021
+ }
1022
+ /**
1023
+ * Render an image watermark on a single page.
1024
+ */
1025
+ function renderImageWatermark(stream, page, watermark) {
1026
+ const opacity = watermark.opacity ?? IMAGE_WM_DEFAULTS.opacity;
1027
+ const rotation = watermark.rotation ?? IMAGE_WM_DEFAULTS.rotation;
1028
+ const scale = watermark.scale ?? IMAGE_WM_DEFAULTS.scale;
1029
+ const needsAlpha = opacity < 1;
1030
+ // Determine image dimensions — use explicit width/height if provided,
1031
+ // otherwise parse actual dimensions from image data and scale proportionally
1032
+ let imgWidth;
1033
+ let imgHeight;
1034
+ if (watermark.width !== undefined && watermark.height !== undefined) {
1035
+ imgWidth = watermark.width;
1036
+ imgHeight = watermark.height;
1037
+ }
1038
+ else {
1039
+ const dims = parseImageDimensions(watermark.data, watermark.format);
1040
+ const minDim = Math.min(page.width, page.height);
1041
+ const targetSize = minDim * scale;
1042
+ const maxDim = Math.max(dims.width, dims.height);
1043
+ const ratio = maxDim > 0 ? targetSize / maxDim : 1;
1044
+ imgWidth = dims.width * ratio;
1045
+ imgHeight = dims.height * ratio;
1046
+ }
1047
+ const radians = (rotation * Math.PI) / 180;
1048
+ const cos = Math.cos(radians);
1049
+ const sin = Math.sin(radians);
1050
+ const gsName = needsAlpha ? alphaGsName(opacity) : "";
1051
+ const imgName = "WmImg";
1052
+ const drawSingleWatermark = (cx, cy) => {
1053
+ stream.save();
1054
+ if (needsAlpha) {
1055
+ stream.setGraphicsState(gsName);
1056
+ }
1057
+ const halfW = imgWidth / 2;
1058
+ const halfH = imgHeight / 2;
1059
+ const tx = cx - halfW * cos + halfH * sin;
1060
+ const ty = cy - halfW * sin - halfH * cos;
1061
+ stream.concat(imgWidth * cos, imgWidth * sin, -imgHeight * sin, imgHeight * cos, tx, ty);
1062
+ stream.doXObject(imgName);
1063
+ stream.restore();
1064
+ };
1065
+ if (watermark.repeat) {
1066
+ const spacingX = watermark.repeatSpacingX ?? IMAGE_WM_DEFAULTS.repeatSpacingX;
1067
+ const spacingY = watermark.repeatSpacingY ?? IMAGE_WM_DEFAULTS.repeatSpacingY;
1068
+ renderRepeatedPattern(page.width, page.height, spacingX, spacingY, drawSingleWatermark);
1069
+ }
1070
+ else {
1071
+ const { cx, cy } = resolveWatermarkCenter(page, watermark.position);
1072
+ drawSingleWatermark(cx, cy);
1073
+ }
1074
+ return {
1075
+ alphaValues: needsAlpha ? [opacity] : [],
1076
+ imageXObjects: [{ name: imgName, data: watermark.data, format: watermark.format }]
1077
+ };
1078
+ }
1079
+ /**
1080
+ * Parse image dimensions from raw JPEG or PNG data without a full decode.
1081
+ */
1082
+ function parseImageDimensions(data, format) {
1083
+ if (format === "png") {
1084
+ return parsePngDimensions(data);
1085
+ }
1086
+ return parseJpegDimensions(data);
1087
+ }
1088
+ /** Read width/height from a PNG IHDR chunk (bytes 16-23). */
1089
+ function parsePngDimensions(data) {
1090
+ // PNG header: 8 byte signature, then IHDR chunk: 4 byte length, 4 byte type, 4 byte width, 4 byte height
1091
+ if (data.length >= 24 &&
1092
+ data[12] === 0x49 &&
1093
+ data[13] === 0x48 &&
1094
+ data[14] === 0x44 &&
1095
+ data[15] === 0x52) {
1096
+ const width = (data[16] << 24) | (data[17] << 16) | (data[18] << 8) | data[19];
1097
+ const height = (data[20] << 24) | (data[21] << 16) | (data[22] << 8) | data[23];
1098
+ return { width, height };
1099
+ }
1100
+ return { width: 1, height: 1 };
1101
+ }
1102
+ /** Read width/height from JPEG SOF marker. */
1103
+ function parseJpegDimensions(data) {
1104
+ let offset = 2; // skip SOI marker
1105
+ while (offset < data.length - 1) {
1106
+ while (offset < data.length && data[offset] === 0xff && data[offset + 1] === 0xff) {
1107
+ offset++;
1108
+ }
1109
+ if (offset >= data.length - 1 || data[offset] !== 0xff) {
1110
+ break;
1111
+ }
1112
+ const marker = data[offset + 1];
1113
+ const isSof = marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc;
1114
+ if (isSof && offset + 8 < data.length) {
1115
+ return {
1116
+ width: (data[offset + 7] << 8) | data[offset + 8],
1117
+ height: (data[offset + 5] << 8) | data[offset + 6]
1118
+ };
1119
+ }
1120
+ if (offset + 3 >= data.length) {
1121
+ break;
1122
+ }
1123
+ const segLen = (data[offset + 2] << 8) | data[offset + 3];
1124
+ offset += 2 + segLen;
1125
+ }
1126
+ return { width: 1, height: 1 };
1127
+ }
1128
+ /**
1129
+ * Resolve the center position for a watermark on a given page.
1130
+ */
1131
+ function resolveWatermarkCenter(page, position) {
1132
+ if (!position || position === "center") {
1133
+ return { cx: page.width / 2, cy: page.height / 2 };
1134
+ }
1135
+ return { cx: position.x, cy: position.y };
1136
+ }
1137
+ /**
1138
+ * Render a repeated pattern of watermarks across the entire page.
1139
+ * Uses a staggered grid for a natural diagonal tiling effect.
1140
+ */
1141
+ function renderRepeatedPattern(pageWidth, pageHeight, spacingX, spacingY, drawFn) {
1142
+ // Start from beyond the page edges to ensure full coverage with rotation
1143
+ const margin = Math.max(pageWidth, pageHeight) * 0.5;
1144
+ let rowIndex = 0;
1145
+ for (let y = -margin; y < pageHeight + margin; y += spacingY) {
1146
+ // Stagger every other row by half the horizontal spacing
1147
+ const offsetX = rowIndex % 2 === 1 ? spacingX / 2 : 0;
1148
+ for (let x = -margin; x < pageWidth + margin; x += spacingX) {
1149
+ drawFn(x + offsetX, y);
1150
+ }
1151
+ rowIndex++;
1152
+ }
1153
+ }