@cj-tech-master/excelts 8.1.2 → 9.0.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/README.md +2 -2
- package/README_zh.md +2 -2
- package/dist/browser/modules/excel/cell.js +11 -7
- package/dist/browser/modules/excel/column.js +7 -6
- package/dist/browser/modules/excel/row.js +5 -1
- package/dist/browser/modules/excel/stream/worksheet-reader.js +3 -2
- package/dist/browser/modules/excel/utils/cell-format.js +64 -2
- package/dist/browser/modules/pdf/excel-bridge.d.ts +4 -3
- package/dist/browser/modules/pdf/excel-bridge.js +18 -5
- package/dist/browser/modules/pdf/index.d.ts +3 -3
- package/dist/browser/modules/pdf/index.js +3 -3
- package/dist/browser/modules/pdf/pdf.d.ts +7 -6
- package/dist/browser/modules/pdf/pdf.js +7 -6
- package/dist/browser/modules/pdf/reader/pdf-reader.d.ts +8 -7
- package/dist/browser/modules/pdf/reader/pdf-reader.js +81 -74
- package/dist/browser/modules/pdf/render/constants.d.ts +30 -0
- package/dist/browser/modules/pdf/render/constants.js +30 -0
- package/dist/browser/modules/pdf/render/layout-engine.d.ts +2 -1
- package/dist/browser/modules/pdf/render/layout-engine.js +359 -156
- package/dist/browser/modules/pdf/render/page-renderer.d.ts +2 -2
- package/dist/browser/modules/pdf/render/page-renderer.js +245 -107
- package/dist/browser/modules/pdf/render/pdf-exporter.d.ts +3 -2
- package/dist/browser/modules/pdf/render/pdf-exporter.js +145 -105
- package/dist/browser/modules/pdf/render/style-converter.js +27 -26
- package/dist/browser/modules/pdf/types.d.ts +8 -0
- package/dist/browser/utils/utils.base.d.ts +5 -0
- package/dist/browser/utils/utils.base.js +10 -0
- package/dist/cjs/modules/excel/cell.js +11 -7
- package/dist/cjs/modules/excel/column.js +7 -6
- package/dist/cjs/modules/excel/row.js +5 -1
- package/dist/cjs/modules/excel/stream/worksheet-reader.js +3 -2
- package/dist/cjs/modules/excel/utils/cell-format.js +64 -2
- package/dist/cjs/modules/pdf/excel-bridge.js +18 -5
- package/dist/cjs/modules/pdf/index.js +3 -3
- package/dist/cjs/modules/pdf/pdf.js +7 -6
- package/dist/cjs/modules/pdf/reader/pdf-reader.js +81 -74
- package/dist/cjs/modules/pdf/render/constants.js +33 -0
- package/dist/cjs/modules/pdf/render/layout-engine.js +359 -156
- package/dist/cjs/modules/pdf/render/page-renderer.js +245 -107
- package/dist/cjs/modules/pdf/render/pdf-exporter.js +145 -105
- package/dist/cjs/modules/pdf/render/style-converter.js +27 -26
- package/dist/cjs/utils/utils.base.js +11 -0
- package/dist/esm/modules/excel/cell.js +11 -7
- package/dist/esm/modules/excel/column.js +7 -6
- package/dist/esm/modules/excel/row.js +5 -1
- package/dist/esm/modules/excel/stream/worksheet-reader.js +3 -2
- package/dist/esm/modules/excel/utils/cell-format.js +64 -2
- package/dist/esm/modules/pdf/excel-bridge.js +18 -5
- package/dist/esm/modules/pdf/index.js +3 -3
- package/dist/esm/modules/pdf/pdf.js +7 -6
- package/dist/esm/modules/pdf/reader/pdf-reader.js +81 -74
- package/dist/esm/modules/pdf/render/constants.js +30 -0
- package/dist/esm/modules/pdf/render/layout-engine.js +359 -156
- package/dist/esm/modules/pdf/render/page-renderer.js +245 -107
- package/dist/esm/modules/pdf/render/pdf-exporter.js +145 -105
- package/dist/esm/modules/pdf/render/style-converter.js +27 -26
- package/dist/esm/utils/utils.base.js +10 -0
- package/dist/iife/excelts.iife.js +1022 -677
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +48 -48
- package/dist/types/modules/pdf/excel-bridge.d.ts +4 -3
- package/dist/types/modules/pdf/index.d.ts +3 -3
- package/dist/types/modules/pdf/pdf.d.ts +7 -6
- package/dist/types/modules/pdf/reader/pdf-reader.d.ts +8 -7
- package/dist/types/modules/pdf/render/constants.d.ts +30 -0
- package/dist/types/modules/pdf/render/layout-engine.d.ts +2 -1
- package/dist/types/modules/pdf/render/page-renderer.d.ts +2 -2
- package/dist/types/modules/pdf/render/pdf-exporter.d.ts +3 -2
- package/dist/types/modules/pdf/types.d.ts +8 -0
- package/dist/types/utils/utils.base.d.ts +5 -0
- package/package.json +1 -1
|
@@ -11,12 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { PdfContentStream } from "../core/pdf-stream.js";
|
|
13
13
|
import { resolvePdfFontName } from "../font/font-manager.js";
|
|
14
|
-
|
|
15
|
-
// Constants
|
|
16
|
-
// =============================================================================
|
|
17
|
-
/** Internal cell padding in points */
|
|
18
|
-
const CELL_PADDING_H = 3;
|
|
19
|
-
const CELL_PADDING_V = 2;
|
|
14
|
+
import { CELL_PADDING_H, CELL_PADDING_V, LINE_HEIGHT_FACTOR, INDENT_WIDTH } from "./constants.js";
|
|
20
15
|
/**
|
|
21
16
|
* Render a single page to a PDF content stream.
|
|
22
17
|
*/
|
|
@@ -38,9 +33,10 @@ export function renderPage(page, options, fontManager, totalPages) {
|
|
|
38
33
|
drawCellBorders(stream, cell);
|
|
39
34
|
}
|
|
40
35
|
// --- Step 4: Draw cell text ---
|
|
36
|
+
const sf = page.scaleFactor;
|
|
41
37
|
for (const cell of page.cells) {
|
|
42
38
|
if (cell.text) {
|
|
43
|
-
drawCellText(stream, cell, fontManager, alphaValues);
|
|
39
|
+
drawCellText(stream, cell, fontManager, alphaValues, sf);
|
|
44
40
|
}
|
|
45
41
|
}
|
|
46
42
|
// --- Step 5: Draw page header (sheet name) ---
|
|
@@ -118,38 +114,56 @@ function drawCellBorders(stream, cell) {
|
|
|
118
114
|
const { rect, borders } = cell;
|
|
119
115
|
const { x, y, width, height } = rect;
|
|
120
116
|
if (borders.top) {
|
|
121
|
-
drawBorderLine(stream, borders.top, x, y + height, x + width, y + height);
|
|
117
|
+
drawBorderLine(stream, borders.top, x, y + height, x + width, y + height, true);
|
|
122
118
|
}
|
|
123
119
|
if (borders.bottom) {
|
|
124
|
-
drawBorderLine(stream, borders.bottom, x, y, x + width, y);
|
|
120
|
+
drawBorderLine(stream, borders.bottom, x, y, x + width, y, true);
|
|
125
121
|
}
|
|
126
122
|
if (borders.left) {
|
|
127
|
-
drawBorderLine(stream, borders.left, x, y, x, y + height);
|
|
123
|
+
drawBorderLine(stream, borders.left, x, y, x, y + height, false);
|
|
128
124
|
}
|
|
129
125
|
if (borders.right) {
|
|
130
|
-
drawBorderLine(stream, borders.right, x + width, y, x + width, y + height);
|
|
126
|
+
drawBorderLine(stream, borders.right, x + width, y, x + width, y + height, false);
|
|
131
127
|
}
|
|
132
128
|
}
|
|
133
|
-
function drawBorderLine(stream, border, x1, y1, x2, y2) {
|
|
134
|
-
|
|
129
|
+
function drawBorderLine(stream, border, x1, y1, x2, y2, isHorizontal) {
|
|
130
|
+
if (border.isDouble) {
|
|
131
|
+
// Draw two parallel thin lines with a small gap between them
|
|
132
|
+
const offset = 0.4;
|
|
133
|
+
const thinWidth = Math.min(border.width, 0.25);
|
|
134
|
+
if (isHorizontal) {
|
|
135
|
+
stream.drawLine(x1, y1 + offset, x2, y2 + offset, border.color, thinWidth, border.dashPattern);
|
|
136
|
+
stream.drawLine(x1, y1 - offset, x2, y2 - offset, border.color, thinWidth, border.dashPattern);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
stream.drawLine(x1 + offset, y1, x2 + offset, y2, border.color, thinWidth, border.dashPattern);
|
|
140
|
+
stream.drawLine(x1 - offset, y1, x2 - offset, y2, border.color, thinWidth, border.dashPattern);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
stream.drawLine(x1, y1, x2, y2, border.color, border.width, border.dashPattern);
|
|
145
|
+
}
|
|
135
146
|
}
|
|
136
147
|
// =============================================================================
|
|
137
148
|
// Cell Text
|
|
138
149
|
// =============================================================================
|
|
139
|
-
function drawCellText(stream, cell, fontManager, alphaValues) {
|
|
150
|
+
function drawCellText(stream, cell, fontManager, alphaValues, scaleFactor = 1) {
|
|
140
151
|
const { rect, text, fontSize, horizontalAlign, verticalAlign, wrapText } = cell;
|
|
141
152
|
if (!text && !cell.richText) {
|
|
142
153
|
return;
|
|
143
154
|
}
|
|
144
|
-
const
|
|
145
|
-
const
|
|
155
|
+
const padH = CELL_PADDING_H * scaleFactor;
|
|
156
|
+
const padV = CELL_PADDING_V * scaleFactor;
|
|
157
|
+
const availWidth = rect.width - padH * 2;
|
|
158
|
+
const availHeight = rect.height - padV * 2;
|
|
146
159
|
if (availWidth <= 0 || availHeight <= 0) {
|
|
147
160
|
return;
|
|
148
161
|
}
|
|
149
|
-
const indentPts = cell.indent * INDENT_WIDTH;
|
|
150
|
-
// Clip to cell bounds
|
|
162
|
+
const indentPts = cell.indent * INDENT_WIDTH * scaleFactor;
|
|
163
|
+
// Clip to cell bounds (extend for text overflow into adjacent empty cells)
|
|
164
|
+
const clipWidth = rect.width + (cell.textOverflowWidth || 0);
|
|
151
165
|
stream.save();
|
|
152
|
-
stream.rect(rect.x, rect.y,
|
|
166
|
+
stream.rect(rect.x, rect.y, clipWidth, rect.height);
|
|
153
167
|
stream.clip();
|
|
154
168
|
stream.endPath();
|
|
155
169
|
// Apply text color alpha if needed
|
|
@@ -160,18 +174,18 @@ function drawCellText(stream, cell, fontManager, alphaValues) {
|
|
|
160
174
|
}
|
|
161
175
|
// Handle text rotation
|
|
162
176
|
if (cell.textRotation === "vertical") {
|
|
163
|
-
drawVerticalStackedText(stream, cell, fontManager, indentPts);
|
|
177
|
+
drawVerticalStackedText(stream, cell, fontManager, indentPts, scaleFactor);
|
|
164
178
|
stream.restore();
|
|
165
179
|
return;
|
|
166
180
|
}
|
|
167
181
|
if (typeof cell.textRotation === "number" && cell.textRotation !== 0) {
|
|
168
|
-
drawRotatedText(stream, cell, fontManager, indentPts);
|
|
182
|
+
drawRotatedText(stream, cell, fontManager, indentPts, scaleFactor);
|
|
169
183
|
stream.restore();
|
|
170
184
|
return;
|
|
171
185
|
}
|
|
172
186
|
// Handle rich text runs
|
|
173
187
|
if (cell.richText && cell.richText.length > 0) {
|
|
174
|
-
drawRichText(stream, cell, fontManager, indentPts);
|
|
188
|
+
drawRichText(stream, cell, fontManager, indentPts, scaleFactor);
|
|
175
189
|
stream.restore();
|
|
176
190
|
return;
|
|
177
191
|
}
|
|
@@ -181,13 +195,13 @@ function drawCellText(stream, cell, fontManager, alphaValues) {
|
|
|
181
195
|
? fontManager.getEmbeddedResourceName()
|
|
182
196
|
: fontManager.ensureFont(resolvePdfFontName(cell.fontFamily, cell.bold, cell.italic));
|
|
183
197
|
const measure = (s) => fontManager.measureText(s, resourceName, fontSize);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const lines = wrapText ? wrapTextLines(text, measure, effectiveWidth) :
|
|
187
|
-
const lineHeight = fontSize *
|
|
198
|
+
const effectiveWidth = availWidth - indentPts;
|
|
199
|
+
// Always split on explicit newlines; additionally word-wrap if wrapText is set
|
|
200
|
+
const lines = wrapText ? wrapTextLines(text, measure, effectiveWidth) : text.split(/\r?\n/);
|
|
201
|
+
const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
|
|
188
202
|
const ascent = fontManager.getFontAscent(resourceName, fontSize);
|
|
189
203
|
const totalTextHeight = lines.length * lineHeight;
|
|
190
|
-
const textStartY = computeTextStartY(verticalAlign, rect, totalTextHeight, ascent);
|
|
204
|
+
const textStartY = computeTextStartY(verticalAlign, rect, totalTextHeight, ascent, padV);
|
|
191
205
|
stream.setFillColor(cell.textColor);
|
|
192
206
|
stream.beginText();
|
|
193
207
|
stream.setFont(resourceName, fontSize);
|
|
@@ -195,7 +209,7 @@ function drawCellText(stream, cell, fontManager, alphaValues) {
|
|
|
195
209
|
const line = lines[i];
|
|
196
210
|
const lineY = textStartY - i * lineHeight;
|
|
197
211
|
const textWidth = measure(line);
|
|
198
|
-
const textX = computeTextX(horizontalAlign, rect, textWidth, indentPts);
|
|
212
|
+
const textX = computeTextX(horizontalAlign, rect, textWidth, indentPts, padH);
|
|
199
213
|
stream.setTextMatrix(1, 0, 0, 1, textX, lineY);
|
|
200
214
|
const hexEncoded = fontManager.encodeText(line, resourceName);
|
|
201
215
|
if (hexEncoded) {
|
|
@@ -212,9 +226,11 @@ function drawCellText(stream, cell, fontManager, alphaValues) {
|
|
|
212
226
|
// =============================================================================
|
|
213
227
|
// Rich Text Rendering
|
|
214
228
|
// =============================================================================
|
|
215
|
-
function drawRichText(stream, cell, fontManager, indentPts) {
|
|
229
|
+
function drawRichText(stream, cell, fontManager, indentPts, scaleFactor = 1) {
|
|
216
230
|
const { rect, horizontalAlign, verticalAlign, wrapText } = cell;
|
|
217
231
|
const runs = cell.richText;
|
|
232
|
+
const padH = CELL_PADDING_H * scaleFactor;
|
|
233
|
+
const padV = CELL_PADDING_V * scaleFactor;
|
|
218
234
|
// Use the largest font size across all runs for line height calculation
|
|
219
235
|
let maxFontSize = cell.fontSize;
|
|
220
236
|
for (const run of runs) {
|
|
@@ -223,7 +239,7 @@ function drawRichText(stream, cell, fontManager, indentPts) {
|
|
|
223
239
|
}
|
|
224
240
|
}
|
|
225
241
|
const primaryFontSize = maxFontSize;
|
|
226
|
-
const lineHeight = primaryFontSize *
|
|
242
|
+
const lineHeight = primaryFontSize * LINE_HEIGHT_FACTOR;
|
|
227
243
|
const isEmbedded = fontManager.hasEmbeddedFont();
|
|
228
244
|
// Helper: resolve resource name for a run
|
|
229
245
|
const runResource = (run) => isEmbedded
|
|
@@ -231,7 +247,7 @@ function drawRichText(stream, cell, fontManager, indentPts) {
|
|
|
231
247
|
: fontManager.ensureFont(resolvePdfFontName(run.fontFamily, run.bold, run.italic));
|
|
232
248
|
// --- Wrapping path ---
|
|
233
249
|
if (wrapText) {
|
|
234
|
-
const availWidth = rect.width -
|
|
250
|
+
const availWidth = rect.width - padH * 2 - indentPts;
|
|
235
251
|
if (availWidth <= 0) {
|
|
236
252
|
return;
|
|
237
253
|
}
|
|
@@ -250,7 +266,7 @@ function drawRichText(stream, cell, fontManager, indentPts) {
|
|
|
250
266
|
const primaryResourceName = runResource(runs[0]);
|
|
251
267
|
const ascent = fontManager.getFontAscent(primaryResourceName, primaryFontSize);
|
|
252
268
|
const totalTextHeight = lines.length * lineHeight;
|
|
253
|
-
const textStartY = computeTextStartY(verticalAlign, rect, totalTextHeight, ascent);
|
|
269
|
+
const textStartY = computeTextStartY(verticalAlign, rect, totalTextHeight, ascent, padV);
|
|
254
270
|
let charPos = 0;
|
|
255
271
|
for (let li = 0; li < lines.length; li++) {
|
|
256
272
|
const lineY = textStartY - li * lineHeight;
|
|
@@ -288,7 +304,7 @@ function drawRichText(stream, cell, fontManager, indentPts) {
|
|
|
288
304
|
for (const seg of segments) {
|
|
289
305
|
lineWidth += fontManager.measureText(seg.text, seg.resourceName, seg.run.fontSize);
|
|
290
306
|
}
|
|
291
|
-
let textX = computeTextX(horizontalAlign, rect, lineWidth, indentPts);
|
|
307
|
+
let textX = computeTextX(horizontalAlign, rect, lineWidth, indentPts, padH);
|
|
292
308
|
for (const seg of segments) {
|
|
293
309
|
const { run, text, resourceName } = seg;
|
|
294
310
|
const segWidth = fontManager.measureText(text, resourceName, run.fontSize);
|
|
@@ -331,8 +347,8 @@ function drawRichText(stream, cell, fontManager, indentPts) {
|
|
|
331
347
|
}
|
|
332
348
|
const primaryResourceName = runMetrics[0]?.resourceName ?? "F1";
|
|
333
349
|
const ascent = fontManager.getFontAscent(primaryResourceName, primaryFontSize);
|
|
334
|
-
const textStartY = computeTextStartY(verticalAlign, rect, lineHeight, ascent);
|
|
335
|
-
let textX = computeTextX(horizontalAlign, rect, totalWidth, indentPts);
|
|
350
|
+
const textStartY = computeTextStartY(verticalAlign, rect, lineHeight, ascent, padV);
|
|
351
|
+
let textX = computeTextX(horizontalAlign, rect, totalWidth, indentPts, padH);
|
|
336
352
|
for (let i = 0; i < runs.length; i++) {
|
|
337
353
|
const run = runs[i];
|
|
338
354
|
const { resourceName } = runMetrics[i];
|
|
@@ -366,23 +382,20 @@ function drawRichText(stream, cell, fontManager, indentPts) {
|
|
|
366
382
|
// =============================================================================
|
|
367
383
|
// Rotated Text
|
|
368
384
|
// =============================================================================
|
|
369
|
-
function drawRotatedText(stream, cell, fontManager, indentPts) {
|
|
370
|
-
const { rect,
|
|
385
|
+
function drawRotatedText(stream, cell, fontManager, indentPts, scaleFactor = 1) {
|
|
386
|
+
const { rect, wrapText } = cell;
|
|
371
387
|
let { fontSize } = cell;
|
|
388
|
+
const padH = CELL_PADDING_H * scaleFactor;
|
|
389
|
+
const padV = CELL_PADDING_V * scaleFactor;
|
|
372
390
|
const isEmbedded = fontManager.hasEmbeddedFont();
|
|
373
391
|
const resourceName = isEmbedded
|
|
374
392
|
? fontManager.getEmbeddedResourceName()
|
|
375
393
|
: fontManager.ensureFont(resolvePdfFontName(cell.fontFamily, cell.bold, cell.italic));
|
|
376
|
-
// Convert Excel rotation to
|
|
394
|
+
// Convert Excel rotation to degrees
|
|
377
395
|
// 1-90: counterclockwise, 91-180: clockwise (value-90 degrees)
|
|
378
396
|
let degrees;
|
|
379
397
|
if (typeof cell.textRotation === "number") {
|
|
380
|
-
|
|
381
|
-
degrees = cell.textRotation;
|
|
382
|
-
}
|
|
383
|
-
else {
|
|
384
|
-
degrees = -(cell.textRotation - 90);
|
|
385
|
-
}
|
|
398
|
+
degrees = cell.textRotation <= 90 ? cell.textRotation : -(cell.textRotation - 90);
|
|
386
399
|
}
|
|
387
400
|
else {
|
|
388
401
|
degrees = 0;
|
|
@@ -390,81 +403,208 @@ function drawRotatedText(stream, cell, fontManager, indentPts) {
|
|
|
390
403
|
const radians = (degrees * Math.PI) / 180;
|
|
391
404
|
const cos = Math.cos(radians);
|
|
392
405
|
const sin = Math.sin(radians);
|
|
393
|
-
// Scale font size down if rotated bounding box exceeds cell dimensions
|
|
394
|
-
const textWidth = fontManager.measureText(text, resourceName, fontSize);
|
|
395
406
|
const absSin = Math.abs(sin);
|
|
396
407
|
const absCos = Math.abs(cos);
|
|
397
|
-
const
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
408
|
+
const maxWidth = rect.width - padH * 2;
|
|
409
|
+
const maxHeight = rect.height - padV * 2;
|
|
410
|
+
// Available length along the text flow direction for wrapping
|
|
411
|
+
let availTextLength;
|
|
412
|
+
if (absSin > 0.01 && absCos > 0.01) {
|
|
413
|
+
availTextLength = Math.min(maxHeight / absSin, maxWidth / absCos);
|
|
414
|
+
}
|
|
415
|
+
else if (absSin > 0.01) {
|
|
416
|
+
availTextLength = maxHeight / absSin;
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
availTextLength = maxWidth;
|
|
420
|
+
}
|
|
421
|
+
const measure = (s) => fontManager.measureText(s, resourceName, fontSize);
|
|
422
|
+
// Split on explicit newlines first, then optionally word-wrap each paragraph
|
|
423
|
+
let lines;
|
|
424
|
+
if (wrapText) {
|
|
425
|
+
lines = wrapTextLines(cell.text, measure, Math.max(availTextLength - 1, 1));
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
lines = cell.text.split(/\r?\n/);
|
|
429
|
+
}
|
|
430
|
+
const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
|
|
431
|
+
const totalTextHeight = lines.length * lineHeight;
|
|
432
|
+
// For non-wrapping text: scale font down if the rotated bounding box exceeds cell
|
|
433
|
+
if (!wrapText) {
|
|
434
|
+
let maxLineWidth = 0;
|
|
435
|
+
for (const line of lines) {
|
|
436
|
+
const w = measure(line);
|
|
437
|
+
if (w > maxLineWidth) {
|
|
438
|
+
maxLineWidth = w;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const rotatedWidth = maxLineWidth * absCos + totalTextHeight * absSin;
|
|
442
|
+
const rotatedHeight = maxLineWidth * absSin + totalTextHeight * absCos;
|
|
443
|
+
if (maxWidth > 0 && maxHeight > 0 && (rotatedWidth > maxWidth || rotatedHeight > maxHeight)) {
|
|
444
|
+
const fitScale = Math.min(maxWidth / rotatedWidth, maxHeight / rotatedHeight);
|
|
445
|
+
if (fitScale < 1) {
|
|
446
|
+
fontSize = fontSize * fitScale;
|
|
447
|
+
}
|
|
405
448
|
}
|
|
406
449
|
}
|
|
407
|
-
|
|
408
|
-
const indentOffset = cell.horizontalAlign === "left"
|
|
409
|
-
? indentPts / 2
|
|
410
|
-
: cell.horizontalAlign === "right"
|
|
411
|
-
? -indentPts / 2
|
|
412
|
-
: 0;
|
|
413
|
-
const cx = rect.x + rect.width / 2 + indentOffset;
|
|
414
|
-
const cy = rect.y + rect.height / 2;
|
|
415
|
-
const finalTextWidth = fontManager.measureText(text, resourceName, fontSize);
|
|
450
|
+
const scaledLineHeight = fontSize * LINE_HEIGHT_FACTOR;
|
|
416
451
|
const ascent = fontManager.getFontAscent(resourceName, fontSize);
|
|
417
|
-
|
|
418
|
-
const
|
|
419
|
-
const offsetY = -ascent / 2;
|
|
420
|
-
const tx = cx + offsetX * cos - offsetY * sin;
|
|
421
|
-
const ty = cy + offsetX * sin + offsetY * cos;
|
|
452
|
+
const is90 = Math.abs(degrees - 90) < 0.01;
|
|
453
|
+
const isMinus90 = Math.abs(degrees + 90) < 0.01;
|
|
422
454
|
stream.setFillColor(cell.textColor);
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
if (
|
|
428
|
-
|
|
455
|
+
if (is90) {
|
|
456
|
+
// Text reads bottom-to-top. Each line becomes a column drawn left-to-right.
|
|
457
|
+
drawRotated90(stream, cell, lines, fontManager, resourceName, fontSize, scaledLineHeight, ascent, padH, padV);
|
|
458
|
+
}
|
|
459
|
+
else if (isMinus90) {
|
|
460
|
+
// Text reads top-to-bottom. Each line becomes a column drawn right-to-left.
|
|
461
|
+
drawRotatedMinus90(stream, cell, lines, fontManager, resourceName, fontSize, scaledLineHeight, ascent, padH, padV);
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
// General rotation — center multi-line text block in cell
|
|
465
|
+
drawRotatedGeneral(stream, cell, lines, fontManager, resourceName, fontSize, scaledLineHeight, ascent, cos, sin, indentPts);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/** 90° CCW: text reads bottom-to-top, lines stack left-to-right. */
|
|
469
|
+
function drawRotated90(stream, cell, lines, fontManager, resourceName, fontSize, lineHeight, ascent, padH, padV) {
|
|
470
|
+
const { rect, horizontalAlign, verticalAlign } = cell;
|
|
471
|
+
const totalColumnsWidth = lines.length * lineHeight;
|
|
472
|
+
// Horizontal centering of line columns
|
|
473
|
+
let startX;
|
|
474
|
+
if (horizontalAlign === "center" || lines.length === 1) {
|
|
475
|
+
startX = rect.x + rect.width / 2 - totalColumnsWidth / 2 + ascent;
|
|
476
|
+
}
|
|
477
|
+
else if (horizontalAlign === "right") {
|
|
478
|
+
startX = rect.x + rect.width - padH - totalColumnsWidth + ascent;
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
startX = rect.x + padH + ascent;
|
|
482
|
+
}
|
|
483
|
+
for (let i = 0; i < lines.length; i++) {
|
|
484
|
+
const line = lines[i];
|
|
485
|
+
const lineWidth = fontManager.measureText(line, resourceName, fontSize);
|
|
486
|
+
const colX = startX + i * lineHeight;
|
|
487
|
+
// Vertical positioning: text flows upward from bottom
|
|
488
|
+
let ty;
|
|
489
|
+
if (verticalAlign === "top") {
|
|
490
|
+
ty = rect.y + padV;
|
|
491
|
+
}
|
|
492
|
+
else if (verticalAlign === "middle") {
|
|
493
|
+
ty = rect.y + (rect.height - lineWidth) / 2;
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
ty = rect.y + rect.height - padV - lineWidth;
|
|
497
|
+
}
|
|
498
|
+
ty = Math.max(ty, rect.y + padV);
|
|
499
|
+
stream.beginText();
|
|
500
|
+
stream.setFont(resourceName, fontSize);
|
|
501
|
+
stream.setTextMatrix(0, 1, -1, 0, colX, ty);
|
|
502
|
+
emitText(stream, fontManager, line, resourceName);
|
|
503
|
+
stream.endText();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/** -90° (270° CW): text reads top-to-bottom, lines stack right-to-left. */
|
|
507
|
+
function drawRotatedMinus90(stream, cell, lines, fontManager, resourceName, fontSize, lineHeight, ascent, padH, padV) {
|
|
508
|
+
const { rect, horizontalAlign, verticalAlign } = cell;
|
|
509
|
+
const totalColumnsWidth = lines.length * lineHeight;
|
|
510
|
+
let startX;
|
|
511
|
+
if (horizontalAlign === "center" || lines.length === 1) {
|
|
512
|
+
startX = rect.x + rect.width / 2 + totalColumnsWidth / 2 - lineHeight + ascent;
|
|
513
|
+
}
|
|
514
|
+
else if (horizontalAlign === "right") {
|
|
515
|
+
startX = rect.x + rect.width - padH - lineHeight + ascent;
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
startX = rect.x + padH + totalColumnsWidth - lineHeight + ascent;
|
|
519
|
+
}
|
|
520
|
+
for (let i = 0; i < lines.length; i++) {
|
|
521
|
+
const line = lines[i];
|
|
522
|
+
const lineWidth = fontManager.measureText(line, resourceName, fontSize);
|
|
523
|
+
const colX = startX - i * lineHeight;
|
|
524
|
+
let ty;
|
|
525
|
+
if (verticalAlign === "top") {
|
|
526
|
+
ty = rect.y + rect.height - padV;
|
|
527
|
+
}
|
|
528
|
+
else if (verticalAlign === "middle") {
|
|
529
|
+
ty = rect.y + (rect.height + lineWidth) / 2;
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
ty = rect.y + padV + lineWidth;
|
|
533
|
+
}
|
|
534
|
+
ty = Math.min(ty, rect.y + rect.height - padV);
|
|
535
|
+
stream.beginText();
|
|
536
|
+
stream.setFont(resourceName, fontSize);
|
|
537
|
+
stream.setTextMatrix(0, -1, 1, 0, colX, ty);
|
|
538
|
+
emitText(stream, fontManager, line, resourceName);
|
|
539
|
+
stream.endText();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/** General rotation — center a multi-line text block in the cell. */
|
|
543
|
+
function drawRotatedGeneral(stream, cell, lines, fontManager, resourceName, fontSize, lineHeight, ascent, cos, sin, indentPts) {
|
|
544
|
+
const { rect, horizontalAlign } = cell;
|
|
545
|
+
const indentOffset = horizontalAlign === "left" ? indentPts / 2 : horizontalAlign === "right" ? -indentPts / 2 : 0;
|
|
546
|
+
const cx = rect.x + rect.width / 2 + indentOffset;
|
|
547
|
+
const cy = rect.y + rect.height / 2;
|
|
548
|
+
for (let i = 0; i < lines.length; i++) {
|
|
549
|
+
const line = lines[i];
|
|
550
|
+
const lineWidth = fontManager.measureText(line, resourceName, fontSize);
|
|
551
|
+
const lineOffset = (i - (lines.length - 1) / 2) * lineHeight;
|
|
552
|
+
const offsetX = -lineWidth / 2;
|
|
553
|
+
const offsetY = -ascent / 2 - lineOffset;
|
|
554
|
+
const tx = cx + offsetX * cos - offsetY * sin;
|
|
555
|
+
const ty = cy + offsetX * sin + offsetY * cos;
|
|
556
|
+
stream.beginText();
|
|
557
|
+
stream.setFont(resourceName, fontSize);
|
|
558
|
+
stream.setTextMatrix(cos, sin, -sin, cos, tx, ty);
|
|
559
|
+
emitText(stream, fontManager, line, resourceName);
|
|
560
|
+
stream.endText();
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/** Emit a text string with hex encoding if available. */
|
|
564
|
+
function emitText(stream, fontManager, text, resourceName) {
|
|
565
|
+
const hex = fontManager.encodeText(text, resourceName);
|
|
566
|
+
if (hex) {
|
|
567
|
+
stream.showTextHex(hex);
|
|
429
568
|
}
|
|
430
569
|
else {
|
|
431
570
|
stream.showText(text);
|
|
432
571
|
}
|
|
433
|
-
stream.endText();
|
|
434
572
|
}
|
|
435
573
|
/**
|
|
436
574
|
* Draw vertical stacked text (each character top-to-bottom).
|
|
575
|
+
* Newlines (\n) start a new column to the right.
|
|
437
576
|
*/
|
|
438
|
-
function drawVerticalStackedText(stream, cell, fontManager, _indentPts) {
|
|
577
|
+
function drawVerticalStackedText(stream, cell, fontManager, _indentPts, scaleFactor = 1) {
|
|
439
578
|
const { rect, text, fontSize } = cell;
|
|
579
|
+
const padV = CELL_PADDING_V * scaleFactor;
|
|
440
580
|
const isEmbedded = fontManager.hasEmbeddedFont();
|
|
441
581
|
const resourceName = isEmbedded
|
|
442
582
|
? fontManager.getEmbeddedResourceName()
|
|
443
583
|
: fontManager.ensureFont(resolvePdfFontName(cell.fontFamily, cell.bold, cell.italic));
|
|
444
584
|
const charHeight = fontSize * 1.3;
|
|
445
585
|
const ascent = fontManager.getFontAscent(resourceName, fontSize);
|
|
446
|
-
|
|
447
|
-
|
|
586
|
+
// Split on newlines — each segment becomes a new column
|
|
587
|
+
const columns = text.split(/\r?\n/);
|
|
588
|
+
const columnWidth = fontSize * 1.4;
|
|
589
|
+
const totalColumnsWidth = columns.length * columnWidth;
|
|
590
|
+
const startX = rect.x + rect.width / 2 - totalColumnsWidth / 2 + columnWidth / 2;
|
|
448
591
|
stream.setFillColor(cell.textColor);
|
|
449
|
-
for (let
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
stream
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
stream.showText(ch);
|
|
592
|
+
for (let colIdx = 0; colIdx < columns.length; colIdx++) {
|
|
593
|
+
const colText = columns[colIdx];
|
|
594
|
+
const colX = startX + colIdx * columnWidth;
|
|
595
|
+
let currentY = rect.y + rect.height - padV - ascent;
|
|
596
|
+
for (const ch of colText) {
|
|
597
|
+
if (currentY < rect.y + padV) {
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
const charWidth = fontManager.measureText(ch, resourceName, fontSize);
|
|
601
|
+
stream.beginText();
|
|
602
|
+
stream.setFont(resourceName, fontSize);
|
|
603
|
+
stream.setTextMatrix(1, 0, 0, 1, colX - charWidth / 2, currentY);
|
|
604
|
+
emitText(stream, fontManager, ch, resourceName);
|
|
605
|
+
stream.endText();
|
|
606
|
+
currentY -= charHeight;
|
|
465
607
|
}
|
|
466
|
-
stream.endText();
|
|
467
|
-
currentY -= charHeight;
|
|
468
608
|
}
|
|
469
609
|
}
|
|
470
610
|
// =============================================================================
|
|
@@ -481,49 +621,47 @@ export function alphaGsName(alpha) {
|
|
|
481
621
|
// =============================================================================
|
|
482
622
|
// Text Layout Helpers
|
|
483
623
|
// =============================================================================
|
|
484
|
-
|
|
485
|
-
const INDENT_WIDTH = 10;
|
|
486
|
-
export function computeTextStartY(verticalAlign, rect, totalTextHeight, ascent) {
|
|
624
|
+
export function computeTextStartY(verticalAlign, rect, totalTextHeight, ascent, padV = CELL_PADDING_V) {
|
|
487
625
|
let y;
|
|
488
626
|
switch (verticalAlign) {
|
|
489
627
|
case "top":
|
|
490
|
-
y = rect.y + rect.height -
|
|
628
|
+
y = rect.y + rect.height - padV - ascent;
|
|
491
629
|
break;
|
|
492
630
|
case "middle":
|
|
493
631
|
y = rect.y + rect.height / 2 + totalTextHeight / 2 - ascent;
|
|
494
632
|
break;
|
|
495
633
|
case "bottom":
|
|
496
634
|
default:
|
|
497
|
-
y = rect.y +
|
|
635
|
+
y = rect.y + padV + (totalTextHeight - ascent);
|
|
498
636
|
break;
|
|
499
637
|
}
|
|
500
638
|
// Clamp: ensure text ascent doesn't exceed the cell top
|
|
501
|
-
const maxY = rect.y + rect.height -
|
|
639
|
+
const maxY = rect.y + rect.height - padV - ascent;
|
|
502
640
|
if (y > maxY) {
|
|
503
641
|
y = maxY;
|
|
504
642
|
}
|
|
505
643
|
// Clamp: ensure text descent doesn't go below cell bottom
|
|
506
|
-
const minY = rect.y +
|
|
644
|
+
const minY = rect.y + padV;
|
|
507
645
|
if (y < minY) {
|
|
508
646
|
y = minY;
|
|
509
647
|
}
|
|
510
648
|
return y;
|
|
511
649
|
}
|
|
512
|
-
export function computeTextX(align, rect, textWidth, indentPts = 0) {
|
|
650
|
+
export function computeTextX(align, rect, textWidth, indentPts = 0, padH = CELL_PADDING_H) {
|
|
513
651
|
let x;
|
|
514
652
|
switch (align) {
|
|
515
653
|
case "center":
|
|
516
654
|
x = rect.x + (rect.width - textWidth) / 2;
|
|
517
655
|
break;
|
|
518
656
|
case "right":
|
|
519
|
-
x = rect.x + rect.width -
|
|
657
|
+
x = rect.x + rect.width - padH - textWidth;
|
|
520
658
|
break;
|
|
521
659
|
default:
|
|
522
|
-
x = rect.x +
|
|
660
|
+
x = rect.x + padH + indentPts;
|
|
523
661
|
break;
|
|
524
662
|
}
|
|
525
663
|
// Clamp: don't start before cell left edge
|
|
526
|
-
const minX = rect.x +
|
|
664
|
+
const minX = rect.x + padH;
|
|
527
665
|
if (x < minX) {
|
|
528
666
|
x = minX;
|
|
529
667
|
}
|