@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
@@ -18,12 +18,7 @@ exports.computeTextX = computeTextX;
18
18
  exports.wrapTextLines = wrapTextLines;
19
19
  const pdf_stream_1 = require("../core/pdf-stream");
20
20
  const font_manager_1 = require("../font/font-manager");
21
- // =============================================================================
22
- // Constants
23
- // =============================================================================
24
- /** Internal cell padding in points */
25
- const CELL_PADDING_H = 3;
26
- const CELL_PADDING_V = 2;
21
+ const constants_1 = require("./constants");
27
22
  /**
28
23
  * Render a single page to a PDF content stream.
29
24
  */
@@ -45,9 +40,10 @@ function renderPage(page, options, fontManager, totalPages) {
45
40
  drawCellBorders(stream, cell);
46
41
  }
47
42
  // --- Step 4: Draw cell text ---
43
+ const sf = page.scaleFactor;
48
44
  for (const cell of page.cells) {
49
45
  if (cell.text) {
50
- drawCellText(stream, cell, fontManager, alphaValues);
46
+ drawCellText(stream, cell, fontManager, alphaValues, sf);
51
47
  }
52
48
  }
53
49
  // --- Step 5: Draw page header (sheet name) ---
@@ -125,38 +121,56 @@ function drawCellBorders(stream, cell) {
125
121
  const { rect, borders } = cell;
126
122
  const { x, y, width, height } = rect;
127
123
  if (borders.top) {
128
- drawBorderLine(stream, borders.top, x, y + height, x + width, y + height);
124
+ drawBorderLine(stream, borders.top, x, y + height, x + width, y + height, true);
129
125
  }
130
126
  if (borders.bottom) {
131
- drawBorderLine(stream, borders.bottom, x, y, x + width, y);
127
+ drawBorderLine(stream, borders.bottom, x, y, x + width, y, true);
132
128
  }
133
129
  if (borders.left) {
134
- drawBorderLine(stream, borders.left, x, y, x, y + height);
130
+ drawBorderLine(stream, borders.left, x, y, x, y + height, false);
135
131
  }
136
132
  if (borders.right) {
137
- drawBorderLine(stream, borders.right, x + width, y, x + width, y + height);
133
+ drawBorderLine(stream, borders.right, x + width, y, x + width, y + height, false);
138
134
  }
139
135
  }
140
- function drawBorderLine(stream, border, x1, y1, x2, y2) {
141
- stream.drawLine(x1, y1, x2, y2, border.color, border.width, border.dashPattern);
136
+ function drawBorderLine(stream, border, x1, y1, x2, y2, isHorizontal) {
137
+ if (border.isDouble) {
138
+ // Draw two parallel thin lines with a small gap between them
139
+ const offset = 0.4;
140
+ const thinWidth = Math.min(border.width, 0.25);
141
+ if (isHorizontal) {
142
+ stream.drawLine(x1, y1 + offset, x2, y2 + offset, border.color, thinWidth, border.dashPattern);
143
+ stream.drawLine(x1, y1 - offset, x2, y2 - offset, border.color, thinWidth, border.dashPattern);
144
+ }
145
+ else {
146
+ stream.drawLine(x1 + offset, y1, x2 + offset, y2, border.color, thinWidth, border.dashPattern);
147
+ stream.drawLine(x1 - offset, y1, x2 - offset, y2, border.color, thinWidth, border.dashPattern);
148
+ }
149
+ }
150
+ else {
151
+ stream.drawLine(x1, y1, x2, y2, border.color, border.width, border.dashPattern);
152
+ }
142
153
  }
143
154
  // =============================================================================
144
155
  // Cell Text
145
156
  // =============================================================================
146
- function drawCellText(stream, cell, fontManager, alphaValues) {
157
+ function drawCellText(stream, cell, fontManager, alphaValues, scaleFactor = 1) {
147
158
  const { rect, text, fontSize, horizontalAlign, verticalAlign, wrapText } = cell;
148
159
  if (!text && !cell.richText) {
149
160
  return;
150
161
  }
151
- const availWidth = rect.width - CELL_PADDING_H * 2;
152
- const availHeight = rect.height - CELL_PADDING_V * 2;
162
+ const padH = constants_1.CELL_PADDING_H * scaleFactor;
163
+ const padV = constants_1.CELL_PADDING_V * scaleFactor;
164
+ const availWidth = rect.width - padH * 2;
165
+ const availHeight = rect.height - padV * 2;
153
166
  if (availWidth <= 0 || availHeight <= 0) {
154
167
  return;
155
168
  }
156
- const indentPts = cell.indent * INDENT_WIDTH;
157
- // Clip to cell bounds
169
+ const indentPts = cell.indent * constants_1.INDENT_WIDTH * scaleFactor;
170
+ // Clip to cell bounds (extend for text overflow into adjacent empty cells)
171
+ const clipWidth = rect.width + (cell.textOverflowWidth || 0);
158
172
  stream.save();
159
- stream.rect(rect.x, rect.y, rect.width, rect.height);
173
+ stream.rect(rect.x, rect.y, clipWidth, rect.height);
160
174
  stream.clip();
161
175
  stream.endPath();
162
176
  // Apply text color alpha if needed
@@ -167,18 +181,18 @@ function drawCellText(stream, cell, fontManager, alphaValues) {
167
181
  }
168
182
  // Handle text rotation
169
183
  if (cell.textRotation === "vertical") {
170
- drawVerticalStackedText(stream, cell, fontManager, indentPts);
184
+ drawVerticalStackedText(stream, cell, fontManager, indentPts, scaleFactor);
171
185
  stream.restore();
172
186
  return;
173
187
  }
174
188
  if (typeof cell.textRotation === "number" && cell.textRotation !== 0) {
175
- drawRotatedText(stream, cell, fontManager, indentPts);
189
+ drawRotatedText(stream, cell, fontManager, indentPts, scaleFactor);
176
190
  stream.restore();
177
191
  return;
178
192
  }
179
193
  // Handle rich text runs
180
194
  if (cell.richText && cell.richText.length > 0) {
181
- drawRichText(stream, cell, fontManager, indentPts);
195
+ drawRichText(stream, cell, fontManager, indentPts, scaleFactor);
182
196
  stream.restore();
183
197
  return;
184
198
  }
@@ -188,13 +202,13 @@ function drawCellText(stream, cell, fontManager, alphaValues) {
188
202
  ? fontManager.getEmbeddedResourceName()
189
203
  : fontManager.ensureFont((0, font_manager_1.resolvePdfFontName)(cell.fontFamily, cell.bold, cell.italic));
190
204
  const measure = (s) => fontManager.measureText(s, resourceName, fontSize);
191
- // Leave a small buffer (1pt) for wrap width to account for font metrics rounding
192
- const effectiveWidth = availWidth - indentPts - 1;
193
- const lines = wrapText ? wrapTextLines(text, measure, effectiveWidth) : [text];
194
- const lineHeight = fontSize * 1.2;
205
+ const effectiveWidth = availWidth - indentPts;
206
+ // Always split on explicit newlines; additionally word-wrap if wrapText is set
207
+ const lines = wrapText ? wrapTextLines(text, measure, effectiveWidth) : text.split(/\r?\n/);
208
+ const lineHeight = fontSize * constants_1.LINE_HEIGHT_FACTOR;
195
209
  const ascent = fontManager.getFontAscent(resourceName, fontSize);
196
210
  const totalTextHeight = lines.length * lineHeight;
197
- const textStartY = computeTextStartY(verticalAlign, rect, totalTextHeight, ascent);
211
+ const textStartY = computeTextStartY(verticalAlign, rect, totalTextHeight, ascent, padV);
198
212
  stream.setFillColor(cell.textColor);
199
213
  stream.beginText();
200
214
  stream.setFont(resourceName, fontSize);
@@ -202,7 +216,7 @@ function drawCellText(stream, cell, fontManager, alphaValues) {
202
216
  const line = lines[i];
203
217
  const lineY = textStartY - i * lineHeight;
204
218
  const textWidth = measure(line);
205
- const textX = computeTextX(horizontalAlign, rect, textWidth, indentPts);
219
+ const textX = computeTextX(horizontalAlign, rect, textWidth, indentPts, padH);
206
220
  stream.setTextMatrix(1, 0, 0, 1, textX, lineY);
207
221
  const hexEncoded = fontManager.encodeText(line, resourceName);
208
222
  if (hexEncoded) {
@@ -219,9 +233,11 @@ function drawCellText(stream, cell, fontManager, alphaValues) {
219
233
  // =============================================================================
220
234
  // Rich Text Rendering
221
235
  // =============================================================================
222
- function drawRichText(stream, cell, fontManager, indentPts) {
236
+ function drawRichText(stream, cell, fontManager, indentPts, scaleFactor = 1) {
223
237
  const { rect, horizontalAlign, verticalAlign, wrapText } = cell;
224
238
  const runs = cell.richText;
239
+ const padH = constants_1.CELL_PADDING_H * scaleFactor;
240
+ const padV = constants_1.CELL_PADDING_V * scaleFactor;
225
241
  // Use the largest font size across all runs for line height calculation
226
242
  let maxFontSize = cell.fontSize;
227
243
  for (const run of runs) {
@@ -230,7 +246,7 @@ function drawRichText(stream, cell, fontManager, indentPts) {
230
246
  }
231
247
  }
232
248
  const primaryFontSize = maxFontSize;
233
- const lineHeight = primaryFontSize * 1.2;
249
+ const lineHeight = primaryFontSize * constants_1.LINE_HEIGHT_FACTOR;
234
250
  const isEmbedded = fontManager.hasEmbeddedFont();
235
251
  // Helper: resolve resource name for a run
236
252
  const runResource = (run) => isEmbedded
@@ -238,7 +254,7 @@ function drawRichText(stream, cell, fontManager, indentPts) {
238
254
  : fontManager.ensureFont((0, font_manager_1.resolvePdfFontName)(run.fontFamily, run.bold, run.italic));
239
255
  // --- Wrapping path ---
240
256
  if (wrapText) {
241
- const availWidth = rect.width - CELL_PADDING_H * 2 - indentPts - 1;
257
+ const availWidth = rect.width - padH * 2 - indentPts;
242
258
  if (availWidth <= 0) {
243
259
  return;
244
260
  }
@@ -257,7 +273,7 @@ function drawRichText(stream, cell, fontManager, indentPts) {
257
273
  const primaryResourceName = runResource(runs[0]);
258
274
  const ascent = fontManager.getFontAscent(primaryResourceName, primaryFontSize);
259
275
  const totalTextHeight = lines.length * lineHeight;
260
- const textStartY = computeTextStartY(verticalAlign, rect, totalTextHeight, ascent);
276
+ const textStartY = computeTextStartY(verticalAlign, rect, totalTextHeight, ascent, padV);
261
277
  let charPos = 0;
262
278
  for (let li = 0; li < lines.length; li++) {
263
279
  const lineY = textStartY - li * lineHeight;
@@ -295,7 +311,7 @@ function drawRichText(stream, cell, fontManager, indentPts) {
295
311
  for (const seg of segments) {
296
312
  lineWidth += fontManager.measureText(seg.text, seg.resourceName, seg.run.fontSize);
297
313
  }
298
- let textX = computeTextX(horizontalAlign, rect, lineWidth, indentPts);
314
+ let textX = computeTextX(horizontalAlign, rect, lineWidth, indentPts, padH);
299
315
  for (const seg of segments) {
300
316
  const { run, text, resourceName } = seg;
301
317
  const segWidth = fontManager.measureText(text, resourceName, run.fontSize);
@@ -338,8 +354,8 @@ function drawRichText(stream, cell, fontManager, indentPts) {
338
354
  }
339
355
  const primaryResourceName = runMetrics[0]?.resourceName ?? "F1";
340
356
  const ascent = fontManager.getFontAscent(primaryResourceName, primaryFontSize);
341
- const textStartY = computeTextStartY(verticalAlign, rect, lineHeight, ascent);
342
- let textX = computeTextX(horizontalAlign, rect, totalWidth, indentPts);
357
+ const textStartY = computeTextStartY(verticalAlign, rect, lineHeight, ascent, padV);
358
+ let textX = computeTextX(horizontalAlign, rect, totalWidth, indentPts, padH);
343
359
  for (let i = 0; i < runs.length; i++) {
344
360
  const run = runs[i];
345
361
  const { resourceName } = runMetrics[i];
@@ -373,23 +389,20 @@ function drawRichText(stream, cell, fontManager, indentPts) {
373
389
  // =============================================================================
374
390
  // Rotated Text
375
391
  // =============================================================================
376
- function drawRotatedText(stream, cell, fontManager, indentPts) {
377
- const { rect, text } = cell;
392
+ function drawRotatedText(stream, cell, fontManager, indentPts, scaleFactor = 1) {
393
+ const { rect, wrapText } = cell;
378
394
  let { fontSize } = cell;
395
+ const padH = constants_1.CELL_PADDING_H * scaleFactor;
396
+ const padV = constants_1.CELL_PADDING_V * scaleFactor;
379
397
  const isEmbedded = fontManager.hasEmbeddedFont();
380
398
  const resourceName = isEmbedded
381
399
  ? fontManager.getEmbeddedResourceName()
382
400
  : fontManager.ensureFont((0, font_manager_1.resolvePdfFontName)(cell.fontFamily, cell.bold, cell.italic));
383
- // Convert Excel rotation to radians
401
+ // Convert Excel rotation to degrees
384
402
  // 1-90: counterclockwise, 91-180: clockwise (value-90 degrees)
385
403
  let degrees;
386
404
  if (typeof cell.textRotation === "number") {
387
- if (cell.textRotation <= 90) {
388
- degrees = cell.textRotation;
389
- }
390
- else {
391
- degrees = -(cell.textRotation - 90);
392
- }
405
+ degrees = cell.textRotation <= 90 ? cell.textRotation : -(cell.textRotation - 90);
393
406
  }
394
407
  else {
395
408
  degrees = 0;
@@ -397,81 +410,208 @@ function drawRotatedText(stream, cell, fontManager, indentPts) {
397
410
  const radians = (degrees * Math.PI) / 180;
398
411
  const cos = Math.cos(radians);
399
412
  const sin = Math.sin(radians);
400
- // Scale font size down if rotated bounding box exceeds cell dimensions
401
- const textWidth = fontManager.measureText(text, resourceName, fontSize);
402
413
  const absSin = Math.abs(sin);
403
414
  const absCos = Math.abs(cos);
404
- const rotatedWidth = textWidth * absCos + fontSize * absSin;
405
- const rotatedHeight = textWidth * absSin + fontSize * absCos;
406
- const maxWidth = rect.width - CELL_PADDING_H * 2;
407
- const maxHeight = rect.height - CELL_PADDING_V * 2;
408
- if (maxWidth > 0 && maxHeight > 0 && (rotatedWidth > maxWidth || rotatedHeight > maxHeight)) {
409
- const fitScale = Math.min(maxWidth / rotatedWidth, maxHeight / rotatedHeight);
410
- if (fitScale < 1) {
411
- fontSize = fontSize * fitScale;
415
+ const maxWidth = rect.width - padH * 2;
416
+ const maxHeight = rect.height - padV * 2;
417
+ // Available length along the text flow direction for wrapping
418
+ let availTextLength;
419
+ if (absSin > 0.01 && absCos > 0.01) {
420
+ availTextLength = Math.min(maxHeight / absSin, maxWidth / absCos);
421
+ }
422
+ else if (absSin > 0.01) {
423
+ availTextLength = maxHeight / absSin;
424
+ }
425
+ else {
426
+ availTextLength = maxWidth;
427
+ }
428
+ const measure = (s) => fontManager.measureText(s, resourceName, fontSize);
429
+ // Split on explicit newlines first, then optionally word-wrap each paragraph
430
+ let lines;
431
+ if (wrapText) {
432
+ lines = wrapTextLines(cell.text, measure, Math.max(availTextLength - 1, 1));
433
+ }
434
+ else {
435
+ lines = cell.text.split(/\r?\n/);
436
+ }
437
+ const lineHeight = fontSize * constants_1.LINE_HEIGHT_FACTOR;
438
+ const totalTextHeight = lines.length * lineHeight;
439
+ // For non-wrapping text: scale font down if the rotated bounding box exceeds cell
440
+ if (!wrapText) {
441
+ let maxLineWidth = 0;
442
+ for (const line of lines) {
443
+ const w = measure(line);
444
+ if (w > maxLineWidth) {
445
+ maxLineWidth = w;
446
+ }
447
+ }
448
+ const rotatedWidth = maxLineWidth * absCos + totalTextHeight * absSin;
449
+ const rotatedHeight = maxLineWidth * absSin + totalTextHeight * absCos;
450
+ if (maxWidth > 0 && maxHeight > 0 && (rotatedWidth > maxWidth || rotatedHeight > maxHeight)) {
451
+ const fitScale = Math.min(maxWidth / rotatedWidth, maxHeight / rotatedHeight);
452
+ if (fitScale < 1) {
453
+ fontSize = fontSize * fitScale;
454
+ }
412
455
  }
413
456
  }
414
- // Center text at rotation point
415
- const indentOffset = cell.horizontalAlign === "left"
416
- ? indentPts / 2
417
- : cell.horizontalAlign === "right"
418
- ? -indentPts / 2
419
- : 0;
420
- const cx = rect.x + rect.width / 2 + indentOffset;
421
- const cy = rect.y + rect.height / 2;
422
- const finalTextWidth = fontManager.measureText(text, resourceName, fontSize);
457
+ const scaledLineHeight = fontSize * constants_1.LINE_HEIGHT_FACTOR;
423
458
  const ascent = fontManager.getFontAscent(resourceName, fontSize);
424
- // Offset to center the text around the rotation point
425
- const offsetX = -finalTextWidth / 2;
426
- const offsetY = -ascent / 2;
427
- const tx = cx + offsetX * cos - offsetY * sin;
428
- const ty = cy + offsetX * sin + offsetY * cos;
459
+ const is90 = Math.abs(degrees - 90) < 0.01;
460
+ const isMinus90 = Math.abs(degrees + 90) < 0.01;
429
461
  stream.setFillColor(cell.textColor);
430
- stream.beginText();
431
- stream.setFont(resourceName, fontSize);
432
- stream.setTextMatrix(cos, sin, -sin, cos, tx, ty);
433
- const hexEncoded = fontManager.encodeText(text, resourceName);
434
- if (hexEncoded) {
435
- stream.showTextHex(hexEncoded);
462
+ if (is90) {
463
+ // Text reads bottom-to-top. Each line becomes a column drawn left-to-right.
464
+ drawRotated90(stream, cell, lines, fontManager, resourceName, fontSize, scaledLineHeight, ascent, padH, padV);
465
+ }
466
+ else if (isMinus90) {
467
+ // Text reads top-to-bottom. Each line becomes a column drawn right-to-left.
468
+ drawRotatedMinus90(stream, cell, lines, fontManager, resourceName, fontSize, scaledLineHeight, ascent, padH, padV);
469
+ }
470
+ else {
471
+ // General rotation — center multi-line text block in cell
472
+ drawRotatedGeneral(stream, cell, lines, fontManager, resourceName, fontSize, scaledLineHeight, ascent, cos, sin, indentPts);
473
+ }
474
+ }
475
+ /** 90° CCW: text reads bottom-to-top, lines stack left-to-right. */
476
+ function drawRotated90(stream, cell, lines, fontManager, resourceName, fontSize, lineHeight, ascent, padH, padV) {
477
+ const { rect, horizontalAlign, verticalAlign } = cell;
478
+ const totalColumnsWidth = lines.length * lineHeight;
479
+ // Horizontal centering of line columns
480
+ let startX;
481
+ if (horizontalAlign === "center" || lines.length === 1) {
482
+ startX = rect.x + rect.width / 2 - totalColumnsWidth / 2 + ascent;
483
+ }
484
+ else if (horizontalAlign === "right") {
485
+ startX = rect.x + rect.width - padH - totalColumnsWidth + ascent;
486
+ }
487
+ else {
488
+ startX = rect.x + padH + ascent;
489
+ }
490
+ for (let i = 0; i < lines.length; i++) {
491
+ const line = lines[i];
492
+ const lineWidth = fontManager.measureText(line, resourceName, fontSize);
493
+ const colX = startX + i * lineHeight;
494
+ // Vertical positioning: text flows upward from bottom
495
+ let ty;
496
+ if (verticalAlign === "top") {
497
+ ty = rect.y + padV;
498
+ }
499
+ else if (verticalAlign === "middle") {
500
+ ty = rect.y + (rect.height - lineWidth) / 2;
501
+ }
502
+ else {
503
+ ty = rect.y + rect.height - padV - lineWidth;
504
+ }
505
+ ty = Math.max(ty, rect.y + padV);
506
+ stream.beginText();
507
+ stream.setFont(resourceName, fontSize);
508
+ stream.setTextMatrix(0, 1, -1, 0, colX, ty);
509
+ emitText(stream, fontManager, line, resourceName);
510
+ stream.endText();
511
+ }
512
+ }
513
+ /** -90° (270° CW): text reads top-to-bottom, lines stack right-to-left. */
514
+ function drawRotatedMinus90(stream, cell, lines, fontManager, resourceName, fontSize, lineHeight, ascent, padH, padV) {
515
+ const { rect, horizontalAlign, verticalAlign } = cell;
516
+ const totalColumnsWidth = lines.length * lineHeight;
517
+ let startX;
518
+ if (horizontalAlign === "center" || lines.length === 1) {
519
+ startX = rect.x + rect.width / 2 + totalColumnsWidth / 2 - lineHeight + ascent;
520
+ }
521
+ else if (horizontalAlign === "right") {
522
+ startX = rect.x + rect.width - padH - lineHeight + ascent;
523
+ }
524
+ else {
525
+ startX = rect.x + padH + totalColumnsWidth - lineHeight + ascent;
526
+ }
527
+ for (let i = 0; i < lines.length; i++) {
528
+ const line = lines[i];
529
+ const lineWidth = fontManager.measureText(line, resourceName, fontSize);
530
+ const colX = startX - i * lineHeight;
531
+ let ty;
532
+ if (verticalAlign === "top") {
533
+ ty = rect.y + rect.height - padV;
534
+ }
535
+ else if (verticalAlign === "middle") {
536
+ ty = rect.y + (rect.height + lineWidth) / 2;
537
+ }
538
+ else {
539
+ ty = rect.y + padV + lineWidth;
540
+ }
541
+ ty = Math.min(ty, rect.y + rect.height - padV);
542
+ stream.beginText();
543
+ stream.setFont(resourceName, fontSize);
544
+ stream.setTextMatrix(0, -1, 1, 0, colX, ty);
545
+ emitText(stream, fontManager, line, resourceName);
546
+ stream.endText();
547
+ }
548
+ }
549
+ /** General rotation — center a multi-line text block in the cell. */
550
+ function drawRotatedGeneral(stream, cell, lines, fontManager, resourceName, fontSize, lineHeight, ascent, cos, sin, indentPts) {
551
+ const { rect, horizontalAlign } = cell;
552
+ const indentOffset = horizontalAlign === "left" ? indentPts / 2 : horizontalAlign === "right" ? -indentPts / 2 : 0;
553
+ const cx = rect.x + rect.width / 2 + indentOffset;
554
+ const cy = rect.y + rect.height / 2;
555
+ for (let i = 0; i < lines.length; i++) {
556
+ const line = lines[i];
557
+ const lineWidth = fontManager.measureText(line, resourceName, fontSize);
558
+ const lineOffset = (i - (lines.length - 1) / 2) * lineHeight;
559
+ const offsetX = -lineWidth / 2;
560
+ const offsetY = -ascent / 2 - lineOffset;
561
+ const tx = cx + offsetX * cos - offsetY * sin;
562
+ const ty = cy + offsetX * sin + offsetY * cos;
563
+ stream.beginText();
564
+ stream.setFont(resourceName, fontSize);
565
+ stream.setTextMatrix(cos, sin, -sin, cos, tx, ty);
566
+ emitText(stream, fontManager, line, resourceName);
567
+ stream.endText();
568
+ }
569
+ }
570
+ /** Emit a text string with hex encoding if available. */
571
+ function emitText(stream, fontManager, text, resourceName) {
572
+ const hex = fontManager.encodeText(text, resourceName);
573
+ if (hex) {
574
+ stream.showTextHex(hex);
436
575
  }
437
576
  else {
438
577
  stream.showText(text);
439
578
  }
440
- stream.endText();
441
579
  }
442
580
  /**
443
581
  * Draw vertical stacked text (each character top-to-bottom).
582
+ * Newlines (\n) start a new column to the right.
444
583
  */
445
- function drawVerticalStackedText(stream, cell, fontManager, _indentPts) {
584
+ function drawVerticalStackedText(stream, cell, fontManager, _indentPts, scaleFactor = 1) {
446
585
  const { rect, text, fontSize } = cell;
586
+ const padV = constants_1.CELL_PADDING_V * scaleFactor;
447
587
  const isEmbedded = fontManager.hasEmbeddedFont();
448
588
  const resourceName = isEmbedded
449
589
  ? fontManager.getEmbeddedResourceName()
450
590
  : fontManager.ensureFont((0, font_manager_1.resolvePdfFontName)(cell.fontFamily, cell.bold, cell.italic));
451
591
  const charHeight = fontSize * 1.3;
452
592
  const ascent = fontManager.getFontAscent(resourceName, fontSize);
453
- const startX = rect.x + rect.width / 2;
454
- let currentY = rect.y + rect.height - CELL_PADDING_V - ascent;
593
+ // Split on newlines each segment becomes a new column
594
+ const columns = text.split(/\r?\n/);
595
+ const columnWidth = fontSize * 1.4;
596
+ const totalColumnsWidth = columns.length * columnWidth;
597
+ const startX = rect.x + rect.width / 2 - totalColumnsWidth / 2 + columnWidth / 2;
455
598
  stream.setFillColor(cell.textColor);
456
- for (let i = 0; i < text.length; i++) {
457
- // Stop if next character would be below cell bottom
458
- if (currentY < rect.y + CELL_PADDING_V) {
459
- break;
460
- }
461
- const ch = text[i];
462
- const charWidth = fontManager.measureText(ch, resourceName, fontSize);
463
- stream.beginText();
464
- stream.setFont(resourceName, fontSize);
465
- stream.setTextMatrix(1, 0, 0, 1, startX - charWidth / 2, currentY);
466
- const hexEncoded = fontManager.encodeText(ch, resourceName);
467
- if (hexEncoded) {
468
- stream.showTextHex(hexEncoded);
469
- }
470
- else {
471
- stream.showText(ch);
599
+ for (let colIdx = 0; colIdx < columns.length; colIdx++) {
600
+ const colText = columns[colIdx];
601
+ const colX = startX + colIdx * columnWidth;
602
+ let currentY = rect.y + rect.height - padV - ascent;
603
+ for (const ch of colText) {
604
+ if (currentY < rect.y + padV) {
605
+ break;
606
+ }
607
+ const charWidth = fontManager.measureText(ch, resourceName, fontSize);
608
+ stream.beginText();
609
+ stream.setFont(resourceName, fontSize);
610
+ stream.setTextMatrix(1, 0, 0, 1, colX - charWidth / 2, currentY);
611
+ emitText(stream, fontManager, ch, resourceName);
612
+ stream.endText();
613
+ currentY -= charHeight;
472
614
  }
473
- stream.endText();
474
- currentY -= charHeight;
475
615
  }
476
616
  }
477
617
  // =============================================================================
@@ -488,49 +628,47 @@ function alphaGsName(alpha) {
488
628
  // =============================================================================
489
629
  // Text Layout Helpers
490
630
  // =============================================================================
491
- /** Indent width per level in points (~3 characters at 11pt) */
492
- const INDENT_WIDTH = 10;
493
- function computeTextStartY(verticalAlign, rect, totalTextHeight, ascent) {
631
+ function computeTextStartY(verticalAlign, rect, totalTextHeight, ascent, padV = constants_1.CELL_PADDING_V) {
494
632
  let y;
495
633
  switch (verticalAlign) {
496
634
  case "top":
497
- y = rect.y + rect.height - CELL_PADDING_V - ascent;
635
+ y = rect.y + rect.height - padV - ascent;
498
636
  break;
499
637
  case "middle":
500
638
  y = rect.y + rect.height / 2 + totalTextHeight / 2 - ascent;
501
639
  break;
502
640
  case "bottom":
503
641
  default:
504
- y = rect.y + CELL_PADDING_V + (totalTextHeight - ascent);
642
+ y = rect.y + padV + (totalTextHeight - ascent);
505
643
  break;
506
644
  }
507
645
  // Clamp: ensure text ascent doesn't exceed the cell top
508
- const maxY = rect.y + rect.height - CELL_PADDING_V - ascent;
646
+ const maxY = rect.y + rect.height - padV - ascent;
509
647
  if (y > maxY) {
510
648
  y = maxY;
511
649
  }
512
650
  // Clamp: ensure text descent doesn't go below cell bottom
513
- const minY = rect.y + CELL_PADDING_V;
651
+ const minY = rect.y + padV;
514
652
  if (y < minY) {
515
653
  y = minY;
516
654
  }
517
655
  return y;
518
656
  }
519
- function computeTextX(align, rect, textWidth, indentPts = 0) {
657
+ function computeTextX(align, rect, textWidth, indentPts = 0, padH = constants_1.CELL_PADDING_H) {
520
658
  let x;
521
659
  switch (align) {
522
660
  case "center":
523
661
  x = rect.x + (rect.width - textWidth) / 2;
524
662
  break;
525
663
  case "right":
526
- x = rect.x + rect.width - CELL_PADDING_H - textWidth;
664
+ x = rect.x + rect.width - padH - textWidth;
527
665
  break;
528
666
  default:
529
- x = rect.x + CELL_PADDING_H + indentPts;
667
+ x = rect.x + padH + indentPts;
530
668
  break;
531
669
  }
532
670
  // Clamp: don't start before cell left edge
533
- const minX = rect.x + CELL_PADDING_H;
671
+ const minX = rect.x + padH;
534
672
  if (x < minX) {
535
673
  x = minX;
536
674
  }