@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.
Files changed (71) hide show
  1. package/README.md +2 -2
  2. package/README_zh.md +2 -2
  3. package/dist/browser/modules/excel/cell.js +11 -7
  4. package/dist/browser/modules/excel/column.js +7 -6
  5. package/dist/browser/modules/excel/row.js +5 -1
  6. package/dist/browser/modules/excel/stream/worksheet-reader.js +3 -2
  7. package/dist/browser/modules/excel/utils/cell-format.js +64 -2
  8. package/dist/browser/modules/pdf/excel-bridge.d.ts +4 -3
  9. package/dist/browser/modules/pdf/excel-bridge.js +18 -5
  10. package/dist/browser/modules/pdf/index.d.ts +3 -3
  11. package/dist/browser/modules/pdf/index.js +3 -3
  12. package/dist/browser/modules/pdf/pdf.d.ts +7 -6
  13. package/dist/browser/modules/pdf/pdf.js +7 -6
  14. package/dist/browser/modules/pdf/reader/pdf-reader.d.ts +8 -7
  15. package/dist/browser/modules/pdf/reader/pdf-reader.js +81 -74
  16. package/dist/browser/modules/pdf/render/constants.d.ts +30 -0
  17. package/dist/browser/modules/pdf/render/constants.js +30 -0
  18. package/dist/browser/modules/pdf/render/layout-engine.d.ts +2 -1
  19. package/dist/browser/modules/pdf/render/layout-engine.js +359 -156
  20. package/dist/browser/modules/pdf/render/page-renderer.d.ts +2 -2
  21. package/dist/browser/modules/pdf/render/page-renderer.js +245 -107
  22. package/dist/browser/modules/pdf/render/pdf-exporter.d.ts +3 -2
  23. package/dist/browser/modules/pdf/render/pdf-exporter.js +145 -105
  24. package/dist/browser/modules/pdf/render/style-converter.js +27 -26
  25. package/dist/browser/modules/pdf/types.d.ts +8 -0
  26. package/dist/browser/utils/utils.base.d.ts +5 -0
  27. package/dist/browser/utils/utils.base.js +10 -0
  28. package/dist/cjs/modules/excel/cell.js +11 -7
  29. package/dist/cjs/modules/excel/column.js +7 -6
  30. package/dist/cjs/modules/excel/row.js +5 -1
  31. package/dist/cjs/modules/excel/stream/worksheet-reader.js +3 -2
  32. package/dist/cjs/modules/excel/utils/cell-format.js +64 -2
  33. package/dist/cjs/modules/pdf/excel-bridge.js +18 -5
  34. package/dist/cjs/modules/pdf/index.js +3 -3
  35. package/dist/cjs/modules/pdf/pdf.js +7 -6
  36. package/dist/cjs/modules/pdf/reader/pdf-reader.js +81 -74
  37. package/dist/cjs/modules/pdf/render/constants.js +33 -0
  38. package/dist/cjs/modules/pdf/render/layout-engine.js +359 -156
  39. package/dist/cjs/modules/pdf/render/page-renderer.js +245 -107
  40. package/dist/cjs/modules/pdf/render/pdf-exporter.js +145 -105
  41. package/dist/cjs/modules/pdf/render/style-converter.js +27 -26
  42. package/dist/cjs/utils/utils.base.js +11 -0
  43. package/dist/esm/modules/excel/cell.js +11 -7
  44. package/dist/esm/modules/excel/column.js +7 -6
  45. package/dist/esm/modules/excel/row.js +5 -1
  46. package/dist/esm/modules/excel/stream/worksheet-reader.js +3 -2
  47. package/dist/esm/modules/excel/utils/cell-format.js +64 -2
  48. package/dist/esm/modules/pdf/excel-bridge.js +18 -5
  49. package/dist/esm/modules/pdf/index.js +3 -3
  50. package/dist/esm/modules/pdf/pdf.js +7 -6
  51. package/dist/esm/modules/pdf/reader/pdf-reader.js +81 -74
  52. package/dist/esm/modules/pdf/render/constants.js +30 -0
  53. package/dist/esm/modules/pdf/render/layout-engine.js +359 -156
  54. package/dist/esm/modules/pdf/render/page-renderer.js +245 -107
  55. package/dist/esm/modules/pdf/render/pdf-exporter.js +145 -105
  56. package/dist/esm/modules/pdf/render/style-converter.js +27 -26
  57. package/dist/esm/utils/utils.base.js +10 -0
  58. package/dist/iife/excelts.iife.js +1022 -677
  59. package/dist/iife/excelts.iife.js.map +1 -1
  60. package/dist/iife/excelts.iife.min.js +48 -48
  61. package/dist/types/modules/pdf/excel-bridge.d.ts +4 -3
  62. package/dist/types/modules/pdf/index.d.ts +3 -3
  63. package/dist/types/modules/pdf/pdf.d.ts +7 -6
  64. package/dist/types/modules/pdf/reader/pdf-reader.d.ts +8 -7
  65. package/dist/types/modules/pdf/render/constants.d.ts +30 -0
  66. package/dist/types/modules/pdf/render/layout-engine.d.ts +2 -1
  67. package/dist/types/modules/pdf/render/page-renderer.d.ts +2 -2
  68. package/dist/types/modules/pdf/render/pdf-exporter.d.ts +3 -2
  69. package/dist/types/modules/pdf/types.d.ts +8 -0
  70. package/dist/types/utils/utils.base.d.ts +5 -0
  71. 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
- stream.drawLine(x1, y1, x2, y2, border.color, border.width, border.dashPattern);
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 availWidth = rect.width - CELL_PADDING_H * 2;
145
- const availHeight = rect.height - CELL_PADDING_V * 2;
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, rect.width, rect.height);
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
- // Leave a small buffer (1pt) for wrap width to account for font metrics rounding
185
- const effectiveWidth = availWidth - indentPts - 1;
186
- const lines = wrapText ? wrapTextLines(text, measure, effectiveWidth) : [text];
187
- const lineHeight = fontSize * 1.2;
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 * 1.2;
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 - CELL_PADDING_H * 2 - indentPts - 1;
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, text } = cell;
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 radians
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
- if (cell.textRotation <= 90) {
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 rotatedWidth = textWidth * absCos + fontSize * absSin;
398
- const rotatedHeight = textWidth * absSin + fontSize * absCos;
399
- const maxWidth = rect.width - CELL_PADDING_H * 2;
400
- const maxHeight = rect.height - CELL_PADDING_V * 2;
401
- if (maxWidth > 0 && maxHeight > 0 && (rotatedWidth > maxWidth || rotatedHeight > maxHeight)) {
402
- const fitScale = Math.min(maxWidth / rotatedWidth, maxHeight / rotatedHeight);
403
- if (fitScale < 1) {
404
- fontSize = fontSize * fitScale;
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
- // Center text at rotation point
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
- // Offset to center the text around the rotation point
418
- const offsetX = -finalTextWidth / 2;
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
- stream.beginText();
424
- stream.setFont(resourceName, fontSize);
425
- stream.setTextMatrix(cos, sin, -sin, cos, tx, ty);
426
- const hexEncoded = fontManager.encodeText(text, resourceName);
427
- if (hexEncoded) {
428
- stream.showTextHex(hexEncoded);
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
- const startX = rect.x + rect.width / 2;
447
- let currentY = rect.y + rect.height - CELL_PADDING_V - ascent;
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 i = 0; i < text.length; i++) {
450
- // Stop if next character would be below cell bottom
451
- if (currentY < rect.y + CELL_PADDING_V) {
452
- break;
453
- }
454
- const ch = text[i];
455
- const charWidth = fontManager.measureText(ch, resourceName, fontSize);
456
- stream.beginText();
457
- stream.setFont(resourceName, fontSize);
458
- stream.setTextMatrix(1, 0, 0, 1, startX - charWidth / 2, currentY);
459
- const hexEncoded = fontManager.encodeText(ch, resourceName);
460
- if (hexEncoded) {
461
- stream.showTextHex(hexEncoded);
462
- }
463
- else {
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
- /** Indent width per level in points (~3 characters at 11pt) */
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 - CELL_PADDING_V - ascent;
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 + CELL_PADDING_V + (totalTextHeight - ascent);
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 - CELL_PADDING_V - ascent;
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 + CELL_PADDING_V;
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 - CELL_PADDING_H - textWidth;
657
+ x = rect.x + rect.width - padH - textWidth;
520
658
  break;
521
659
  default:
522
- x = rect.x + CELL_PADDING_H + indentPts;
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 + CELL_PADDING_H;
664
+ const minX = rect.x + padH;
527
665
  if (x < minX) {
528
666
  x = minX;
529
667
  }