@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.
- package/dist/browser/index.browser.d.ts +2 -0
- package/dist/browser/index.browser.js +2 -0
- package/dist/browser/index.d.ts +2 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/modules/excel/image.d.ts +27 -2
- package/dist/browser/modules/excel/image.js +23 -1
- package/dist/browser/modules/excel/stream/worksheet-writer.d.ts +16 -1
- package/dist/browser/modules/excel/stream/worksheet-writer.js +68 -0
- package/dist/browser/modules/excel/types.d.ts +72 -0
- package/dist/browser/modules/excel/utils/drawing-utils.d.ts +4 -0
- package/dist/browser/modules/excel/utils/drawing-utils.js +5 -0
- package/dist/browser/modules/excel/utils/ooxml-paths.d.ts +4 -0
- package/dist/browser/modules/excel/utils/ooxml-paths.js +15 -0
- package/dist/browser/modules/excel/utils/watermark-image.d.ts +67 -0
- package/dist/browser/modules/excel/utils/watermark-image.js +383 -0
- package/dist/browser/modules/excel/worksheet.d.ts +39 -1
- package/dist/browser/modules/excel/worksheet.js +99 -0
- package/dist/browser/modules/excel/xlsx/xform/core/content-types-xform.js +3 -2
- package/dist/browser/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +6 -1
- package/dist/browser/modules/excel/xlsx/xform/drawing/blip-fill-xform.d.ts +2 -1
- package/dist/browser/modules/excel/xlsx/xform/drawing/blip-fill-xform.js +0 -1
- package/dist/browser/modules/excel/xlsx/xform/drawing/blip-xform.d.ts +3 -1
- package/dist/browser/modules/excel/xlsx/xform/drawing/blip-xform.js +22 -6
- package/dist/browser/modules/excel/xlsx/xform/drawing/pic-xform.d.ts +3 -0
- package/dist/browser/modules/excel/xlsx/xform/drawing/pic-xform.js +5 -1
- package/dist/browser/modules/excel/xlsx/xform/drawing/vml-drawing-xform.d.ts +19 -0
- package/dist/browser/modules/excel/xlsx/xform/drawing/vml-drawing-xform.js +103 -4
- package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +135 -8
- package/dist/browser/modules/excel/xlsx/xlsx.browser.d.ts +1 -0
- package/dist/browser/modules/excel/xlsx/xlsx.browser.js +53 -1
- package/dist/browser/modules/pdf/core/pdf-writer.d.ts +1 -1
- package/dist/browser/modules/pdf/core/pdf-writer.js +2 -1
- package/dist/browser/modules/pdf/index.d.ts +1 -1
- package/dist/browser/modules/pdf/render/page-renderer.d.ts +29 -1
- package/dist/browser/modules/pdf/render/page-renderer.js +394 -25
- package/dist/browser/modules/pdf/render/pdf-exporter.js +84 -47
- package/dist/browser/modules/pdf/types.d.ts +235 -0
- package/dist/cjs/index.js +5 -2
- package/dist/cjs/modules/excel/image.js +23 -1
- package/dist/cjs/modules/excel/stream/worksheet-writer.js +68 -0
- package/dist/cjs/modules/excel/utils/drawing-utils.js +5 -0
- package/dist/cjs/modules/excel/utils/ooxml-paths.js +19 -0
- package/dist/cjs/modules/excel/utils/watermark-image.js +386 -0
- package/dist/cjs/modules/excel/worksheet.js +99 -0
- package/dist/cjs/modules/excel/xlsx/xform/core/content-types-xform.js +3 -2
- package/dist/cjs/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +6 -1
- package/dist/cjs/modules/excel/xlsx/xform/drawing/blip-fill-xform.js +0 -1
- package/dist/cjs/modules/excel/xlsx/xform/drawing/blip-xform.js +22 -6
- package/dist/cjs/modules/excel/xlsx/xform/drawing/pic-xform.js +5 -1
- package/dist/cjs/modules/excel/xlsx/xform/drawing/vml-drawing-xform.js +103 -4
- package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +134 -7
- package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +52 -0
- package/dist/cjs/modules/pdf/core/pdf-writer.js +2 -1
- package/dist/cjs/modules/pdf/render/page-renderer.js +396 -25
- package/dist/cjs/modules/pdf/render/pdf-exporter.js +83 -46
- package/dist/esm/index.browser.js +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/modules/excel/image.js +23 -1
- package/dist/esm/modules/excel/stream/worksheet-writer.js +68 -0
- package/dist/esm/modules/excel/utils/drawing-utils.js +5 -0
- package/dist/esm/modules/excel/utils/ooxml-paths.js +15 -0
- package/dist/esm/modules/excel/utils/watermark-image.js +383 -0
- package/dist/esm/modules/excel/worksheet.js +99 -0
- package/dist/esm/modules/excel/xlsx/xform/core/content-types-xform.js +3 -2
- package/dist/esm/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +6 -1
- package/dist/esm/modules/excel/xlsx/xform/drawing/blip-fill-xform.js +0 -1
- package/dist/esm/modules/excel/xlsx/xform/drawing/blip-xform.js +22 -6
- package/dist/esm/modules/excel/xlsx/xform/drawing/pic-xform.js +5 -1
- package/dist/esm/modules/excel/xlsx/xform/drawing/vml-drawing-xform.js +103 -4
- package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +135 -8
- package/dist/esm/modules/excel/xlsx/xlsx.browser.js +53 -1
- package/dist/esm/modules/pdf/core/pdf-writer.js +2 -1
- package/dist/esm/modules/pdf/render/page-renderer.js +394 -25
- package/dist/esm/modules/pdf/render/pdf-exporter.js +84 -47
- package/dist/iife/excelts.iife.js +2390 -469
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +47 -47
- package/dist/types/index.browser.d.ts +2 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/modules/excel/image.d.ts +27 -2
- package/dist/types/modules/excel/stream/worksheet-writer.d.ts +16 -1
- package/dist/types/modules/excel/types.d.ts +72 -0
- package/dist/types/modules/excel/utils/drawing-utils.d.ts +4 -0
- package/dist/types/modules/excel/utils/ooxml-paths.d.ts +4 -0
- package/dist/types/modules/excel/utils/watermark-image.d.ts +67 -0
- package/dist/types/modules/excel/worksheet.d.ts +39 -1
- package/dist/types/modules/excel/xlsx/xform/drawing/blip-fill-xform.d.ts +2 -1
- package/dist/types/modules/excel/xlsx/xform/drawing/blip-xform.d.ts +3 -1
- package/dist/types/modules/excel/xlsx/xform/drawing/pic-xform.d.ts +3 -0
- package/dist/types/modules/excel/xlsx/xform/drawing/vml-drawing-xform.d.ts +19 -0
- package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +1 -0
- package/dist/types/modules/pdf/core/pdf-writer.d.ts +1 -1
- package/dist/types/modules/pdf/index.d.ts +1 -1
- package/dist/types/modules/pdf/render/page-renderer.d.ts +29 -1
- package/dist/types/modules/pdf/types.d.ts +235 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
521
|
+
// horizontalAlign controls X placement of line columns (same visual axis)
|
|
480
522
|
let startX;
|
|
481
|
-
if (horizontalAlign === "center"
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
554
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|