@cj-tech-master/excelts 9.4.1 → 9.4.2

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 (66) hide show
  1. package/dist/browser/modules/pdf/builder/document-builder.js +4 -23
  2. package/dist/browser/modules/pdf/core/pdf-stream.d.ts +15 -0
  3. package/dist/browser/modules/pdf/core/pdf-stream.js +47 -3
  4. package/dist/browser/modules/pdf/font/font-manager.d.ts +37 -6
  5. package/dist/browser/modules/pdf/font/font-manager.js +129 -17
  6. package/dist/browser/modules/pdf/font/system-fonts.d.ts +41 -0
  7. package/dist/browser/modules/pdf/font/system-fonts.js +188 -0
  8. package/dist/browser/modules/pdf/font/ttf-parser.js +29 -1
  9. package/dist/browser/modules/pdf/font/type3-font.d.ts +35 -0
  10. package/dist/browser/modules/pdf/font/type3-font.js +228 -0
  11. package/dist/browser/modules/pdf/font/type3-glyphs-extended.d.ts +33 -0
  12. package/dist/browser/modules/pdf/font/type3-glyphs-extended.js +4164 -0
  13. package/dist/browser/modules/pdf/font/type3-glyphs-extended2.d.ts +16 -0
  14. package/dist/browser/modules/pdf/font/type3-glyphs-extended2.js +9649 -0
  15. package/dist/browser/modules/pdf/font/type3-glyphs-fill.d.ts +17 -0
  16. package/dist/browser/modules/pdf/font/type3-glyphs-fill.js +5438 -0
  17. package/dist/browser/modules/pdf/font/type3-glyphs-quality.d.ts +28 -0
  18. package/dist/browser/modules/pdf/font/type3-glyphs-quality.js +5345 -0
  19. package/dist/browser/modules/pdf/font/type3-glyphs.d.ts +79 -0
  20. package/dist/browser/modules/pdf/font/type3-glyphs.js +2567 -0
  21. package/dist/browser/modules/pdf/render/layout-engine.js +36 -23
  22. package/dist/browser/modules/pdf/render/page-renderer.d.ts +9 -0
  23. package/dist/browser/modules/pdf/render/page-renderer.js +110 -78
  24. package/dist/browser/modules/pdf/render/pdf-exporter.js +73 -5
  25. package/dist/cjs/modules/pdf/builder/document-builder.js +3 -22
  26. package/dist/cjs/modules/pdf/core/pdf-stream.js +49 -3
  27. package/dist/cjs/modules/pdf/font/font-manager.js +129 -17
  28. package/dist/cjs/modules/pdf/font/system-fonts.js +194 -0
  29. package/dist/cjs/modules/pdf/font/ttf-parser.js +29 -1
  30. package/dist/cjs/modules/pdf/font/type3-font.js +231 -0
  31. package/dist/cjs/modules/pdf/font/type3-glyphs-extended.js +4167 -0
  32. package/dist/cjs/modules/pdf/font/type3-glyphs-extended2.js +9652 -0
  33. package/dist/cjs/modules/pdf/font/type3-glyphs-fill.js +5441 -0
  34. package/dist/cjs/modules/pdf/font/type3-glyphs-quality.js +5348 -0
  35. package/dist/cjs/modules/pdf/font/type3-glyphs.js +2573 -0
  36. package/dist/cjs/modules/pdf/render/layout-engine.js +36 -23
  37. package/dist/cjs/modules/pdf/render/page-renderer.js +111 -78
  38. package/dist/cjs/modules/pdf/render/pdf-exporter.js +71 -3
  39. package/dist/esm/modules/pdf/builder/document-builder.js +4 -23
  40. package/dist/esm/modules/pdf/core/pdf-stream.js +47 -3
  41. package/dist/esm/modules/pdf/font/font-manager.js +129 -17
  42. package/dist/esm/modules/pdf/font/system-fonts.js +188 -0
  43. package/dist/esm/modules/pdf/font/ttf-parser.js +29 -1
  44. package/dist/esm/modules/pdf/font/type3-font.js +228 -0
  45. package/dist/esm/modules/pdf/font/type3-glyphs-extended.js +4164 -0
  46. package/dist/esm/modules/pdf/font/type3-glyphs-extended2.js +9649 -0
  47. package/dist/esm/modules/pdf/font/type3-glyphs-fill.js +5438 -0
  48. package/dist/esm/modules/pdf/font/type3-glyphs-quality.js +5345 -0
  49. package/dist/esm/modules/pdf/font/type3-glyphs.js +2567 -0
  50. package/dist/esm/modules/pdf/render/layout-engine.js +36 -23
  51. package/dist/esm/modules/pdf/render/page-renderer.js +110 -78
  52. package/dist/esm/modules/pdf/render/pdf-exporter.js +73 -5
  53. package/dist/iife/excelts.iife.js +25445 -344
  54. package/dist/iife/excelts.iife.js.map +1 -1
  55. package/dist/iife/excelts.iife.min.js +48 -46
  56. package/dist/types/modules/pdf/core/pdf-stream.d.ts +15 -0
  57. package/dist/types/modules/pdf/font/font-manager.d.ts +37 -6
  58. package/dist/types/modules/pdf/font/system-fonts.d.ts +41 -0
  59. package/dist/types/modules/pdf/font/type3-font.d.ts +35 -0
  60. package/dist/types/modules/pdf/font/type3-glyphs-extended.d.ts +33 -0
  61. package/dist/types/modules/pdf/font/type3-glyphs-extended2.d.ts +16 -0
  62. package/dist/types/modules/pdf/font/type3-glyphs-fill.d.ts +17 -0
  63. package/dist/types/modules/pdf/font/type3-glyphs-quality.d.ts +28 -0
  64. package/dist/types/modules/pdf/font/type3-glyphs.d.ts +79 -0
  65. package/dist/types/modules/pdf/render/page-renderer.d.ts +9 -0
  66. package/package.json +1 -1
@@ -390,38 +390,49 @@ function computeRowHeights(sheet, scaleFactor, printRange, fontManager, options)
390
390
  height = row.height;
391
391
  }
392
392
  else if (row?.height) {
393
- // Excel auto-calculated height — trust it as-is
394
- height = row.height;
393
+ // Excel auto-calculated height — use it as a baseline, but ensure
394
+ // the row is tall enough for wrapped text. The stored height may be
395
+ // stale when columns are narrower in the PDF layout or when the PDF
396
+ // uses different font metrics than the original Excel file.
397
+ height = Math.max(row.height, autoRowHeight(row, scaleFactor, sheet, fontManager, options));
395
398
  }
396
399
  else {
397
400
  // No height info: auto-size based on cell content
398
- height = DEFAULT_ROW_HEIGHT;
399
- if (row) {
400
- for (const cell of row.cells.values()) {
401
- const fontSize = getCellFontSize(cell);
402
- const wrapLineCount = countWrapLines(cell, fontSize, scaleFactor, sheet, fontManager, options);
403
- const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
404
- // Account for border width: half of each border extends inward
405
- const borderTop = cell.style?.border?.top?.style
406
- ? borderStyleToLineWidth(cell.style.border.top.style) / 2
407
- : 0;
408
- const borderBottom = cell.style?.border?.bottom?.style
409
- ? borderStyleToLineWidth(cell.style.border.bottom.style) / 2
410
- : 0;
411
- const neededHeight = fontSize +
412
- (wrapLineCount - 1) * lineHeight +
413
- (CELL_PADDING_V + borderTop + borderBottom) * 2;
414
- if (neededHeight > height) {
415
- height = neededHeight;
416
- }
417
- }
418
- }
401
+ height = autoRowHeight(row, scaleFactor, sheet, fontManager, options);
419
402
  }
420
403
  rowHeights.push(height * scaleFactor);
421
404
  visibleRows.push(r);
422
405
  }
423
406
  return { rowHeights, visibleRows };
424
407
  }
408
+ /**
409
+ * Compute the minimum row height required to display wrapped cell content.
410
+ * Returns at least `DEFAULT_ROW_HEIGHT`.
411
+ */
412
+ function autoRowHeight(row, scaleFactor, sheet, fontManager, options) {
413
+ let height = DEFAULT_ROW_HEIGHT;
414
+ if (row) {
415
+ for (const cell of row.cells.values()) {
416
+ const fontSize = getCellFontSize(cell);
417
+ const wrapLineCount = countWrapLines(cell, fontSize, scaleFactor, sheet, fontManager, options);
418
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
419
+ // Account for border width: half of each border extends inward
420
+ const borderTop = cell.style?.border?.top?.style
421
+ ? borderStyleToLineWidth(cell.style.border.top.style) / 2
422
+ : 0;
423
+ const borderBottom = cell.style?.border?.bottom?.style
424
+ ? borderStyleToLineWidth(cell.style.border.bottom.style) / 2
425
+ : 0;
426
+ const neededHeight = fontSize +
427
+ (wrapLineCount - 1) * lineHeight +
428
+ (CELL_PADDING_V + borderTop + borderBottom) * 2;
429
+ if (neededHeight > height) {
430
+ height = neededHeight;
431
+ }
432
+ }
433
+ }
434
+ return height;
435
+ }
425
436
  /**
426
437
  * Get the largest font size for a cell, checking rich text runs.
427
438
  */
@@ -652,6 +663,8 @@ function buildLayoutCell(cell, x, y, width, height, colSpan, rowSpan, options, f
652
663
  else {
653
664
  const pdfFontName = resolvePdfFontName(fontProps.fontFamily, fontProps.bold, fontProps.italic);
654
665
  fontManager.ensureFont(pdfFontName);
666
+ // Track non-WinAnsi code points for Type3 fallback font generation
667
+ fontManager.trackText(text);
655
668
  }
656
669
  // Rich text runs
657
670
  const richText = buildRichTextRuns(cell, options, fontManager, scaleFactor);
@@ -258,23 +258,14 @@ function drawCellText(stream, cell, fontManager, alphaValues, scaleFactor = 1) {
258
258
  const totalTextHeight = lines.length * lineHeight;
259
259
  const textStartY = computeTextStartY(verticalAlign, rect, totalTextHeight, ascent, pad.top, pad.bottom);
260
260
  stream.setFillColor(cell.textColor);
261
- stream.beginText();
262
- stream.setFont(resourceName, fontSize);
261
+ const useType3 = fontManager.hasType3Fonts() && !isEmbedded;
263
262
  for (let i = 0; i < lines.length; i++) {
264
263
  const line = lines[i];
265
264
  const lineY = textStartY - i * lineHeight;
266
265
  const textWidth = measure(line);
267
266
  const textX = computeTextX(horizontalAlign, rect, textWidth, indentPts, pad.left, pad.right);
268
- stream.setTextMatrix(1, 0, 0, 1, textX, lineY);
269
- const hexEncoded = fontManager.encodeText(line, resourceName);
270
- if (hexEncoded) {
271
- stream.showTextHex(hexEncoded);
272
- }
273
- else {
274
- stream.showText(line);
275
- }
267
+ emitTextWithType3(stream, line, textX, lineY, resourceName, fontSize, fontManager, useType3);
276
268
  }
277
- stream.endText();
278
269
  drawTextDecorations(stream, cell, lines, lineHeight, textStartY, measure, resourceName, fontManager, indentPts, pad);
279
270
  stream.restore();
280
271
  }
@@ -359,21 +350,11 @@ function drawRichText(stream, cell, fontManager, indentPts, scaleFactor = 1) {
359
350
  lineWidth += fontManager.measureText(seg.text, seg.resourceName, seg.run.fontSize);
360
351
  }
361
352
  let textX = computeTextX(horizontalAlign, rect, lineWidth, indentPts, pad.left, pad.right);
353
+ const useType3 = fontManager.hasType3Fonts() && !isEmbedded;
362
354
  for (const seg of segments) {
363
355
  const { run, text, resourceName } = seg;
364
- const segWidth = fontManager.measureText(text, resourceName, run.fontSize);
365
356
  stream.setFillColor(run.textColor);
366
- stream.beginText();
367
- stream.setFont(resourceName, run.fontSize);
368
- stream.setTextMatrix(1, 0, 0, 1, textX, lineY);
369
- const hex = fontManager.encodeText(text, resourceName);
370
- if (hex) {
371
- stream.showTextHex(hex);
372
- }
373
- else {
374
- stream.showText(text);
375
- }
376
- stream.endText();
357
+ const segWidth = emitTextWithType3(stream, text, textX, lineY, resourceName, run.fontSize, fontManager, useType3);
377
358
  if (run.strike) {
378
359
  const descent = fontManager.getFontDescent(resourceName, run.fontSize);
379
360
  const y = lineY + descent + run.fontSize * 0.3;
@@ -403,23 +384,13 @@ function drawRichText(stream, cell, fontManager, indentPts, scaleFactor = 1) {
403
384
  const ascent = fontManager.getFontAscent(primaryResourceName, primaryFontSize);
404
385
  const textStartY = computeTextStartY(verticalAlign, rect, lineHeight, ascent, pad.top, pad.bottom);
405
386
  let textX = computeTextX(horizontalAlign, rect, totalWidth, indentPts, pad.left, pad.right);
387
+ const useType3 = fontManager.hasType3Fonts() && !isEmbedded;
406
388
  for (let i = 0; i < runs.length; i++) {
407
389
  const run = runs[i];
408
390
  const { resourceName } = runMetrics[i];
409
391
  stream.setFillColor(run.textColor);
410
- stream.beginText();
411
- stream.setFont(resourceName, run.fontSize);
412
- stream.setTextMatrix(1, 0, 0, 1, textX, textStartY);
413
- const hexEncoded = fontManager.encodeText(run.text, resourceName);
414
- if (hexEncoded) {
415
- stream.showTextHex(hexEncoded);
416
- }
417
- else {
418
- stream.showText(run.text);
419
- }
420
- stream.endText();
392
+ const runWidth = emitTextWithType3(stream, run.text, textX, textStartY, resourceName, run.fontSize, fontManager, useType3);
421
393
  // Draw per-run decorations (strikethrough, underline)
422
- const runWidth = runMetrics[i].width;
423
394
  if (run.strike) {
424
395
  const descent = fontManager.getFontDescent(resourceName, run.fontSize);
425
396
  const y = textStartY + descent + run.fontSize * 0.3;
@@ -430,7 +401,7 @@ function drawRichText(stream, cell, fontManager, indentPts, scaleFactor = 1) {
430
401
  const y = textStartY + descent * 0.5;
431
402
  stream.drawLine(textX, y, textX + runWidth, y, run.textColor, 0.5);
432
403
  }
433
- textX += runMetrics[i].width;
404
+ textX += runWidth;
434
405
  }
435
406
  }
436
407
  // =============================================================================
@@ -527,6 +498,7 @@ function drawRotated90(stream, cell, lines, fontManager, resourceName, fontSize,
527
498
  // left (default)
528
499
  startX = rect.x + pad.left + ascent;
529
500
  }
501
+ const useType3 = fontManager.hasType3Fonts() && !fontManager.hasEmbeddedFont();
530
502
  for (let i = 0; i < lines.length; i++) {
531
503
  const line = lines[i];
532
504
  const lineWidth = fontManager.measureText(line, resourceName, fontSize);
@@ -546,11 +518,7 @@ function drawRotated90(stream, cell, lines, fontManager, resourceName, fontSize,
546
518
  ty = rect.y + pad.bottom;
547
519
  }
548
520
  ty = Math.max(ty, rect.y + pad.bottom);
549
- stream.beginText();
550
- stream.setFont(resourceName, fontSize);
551
- stream.setTextMatrix(0, 1, -1, 0, colX, ty);
552
- emitText(stream, fontManager, line, resourceName);
553
- stream.endText();
521
+ emitTextWithMatrix(stream, line, 0, 1, -1, 0, colX, ty, resourceName, fontSize, fontManager, useType3);
554
522
  }
555
523
  }
556
524
  /** -90° (270° CW): text reads top-to-bottom, lines stack right-to-left. */
@@ -569,6 +537,7 @@ function drawRotatedMinus90(stream, cell, lines, fontManager, resourceName, font
569
537
  // left (default)
570
538
  startX = rect.x + pad.left + totalColumnsWidth - lineHeight + ascent;
571
539
  }
540
+ const useType3 = fontManager.hasType3Fonts() && !fontManager.hasEmbeddedFont();
572
541
  for (let i = 0; i < lines.length; i++) {
573
542
  const line = lines[i];
574
543
  const lineWidth = fontManager.measureText(line, resourceName, fontSize);
@@ -588,11 +557,7 @@ function drawRotatedMinus90(stream, cell, lines, fontManager, resourceName, font
588
557
  ty = rect.y + pad.bottom + lineWidth;
589
558
  }
590
559
  ty = Math.min(ty, rect.y + rect.height - pad.top);
591
- stream.beginText();
592
- stream.setFont(resourceName, fontSize);
593
- stream.setTextMatrix(0, -1, 1, 0, colX, ty);
594
- emitText(stream, fontManager, line, resourceName);
595
- stream.endText();
560
+ emitTextWithMatrix(stream, line, 0, -1, 1, 0, colX, ty, resourceName, fontSize, fontManager, useType3);
596
561
  }
597
562
  }
598
563
  /** General rotation — center a multi-line text block in the cell. */
@@ -644,6 +609,7 @@ function drawRotatedGeneral(stream, cell, lines, fontManager, resourceName, font
644
609
  // center (default for rotated)
645
610
  cx = rect.x + rect.width / 2 + indentOffset + slantAtCy;
646
611
  }
612
+ const useType3 = fontManager.hasType3Fonts() && !fontManager.hasEmbeddedFont();
647
613
  for (let i = 0; i < lines.length; i++) {
648
614
  const line = lines[i];
649
615
  const lineWidth = fontManager.measureText(line, resourceName, fontSize);
@@ -652,11 +618,7 @@ function drawRotatedGeneral(stream, cell, lines, fontManager, resourceName, font
652
618
  const offsetY = -ascent / 2 - lineOffset;
653
619
  const tx = cx + offsetX * cos - offsetY * sin;
654
620
  const ty = cy + offsetX * sin + offsetY * cos;
655
- stream.beginText();
656
- stream.setFont(resourceName, fontSize);
657
- stream.setTextMatrix(cos, sin, -sin, cos, tx, ty);
658
- emitText(stream, fontManager, line, resourceName);
659
- stream.endText();
621
+ emitTextWithMatrix(stream, line, cos, sin, -sin, cos, tx, ty, resourceName, fontSize, fontManager, useType3);
660
622
  }
661
623
  }
662
624
  /** Emit a text string with hex encoding if available. */
@@ -669,6 +631,97 @@ function emitText(stream, fontManager, text, resourceName) {
669
631
  stream.showText(text);
670
632
  }
671
633
  }
634
+ /**
635
+ * Render a text string with a custom text matrix, using Type3-aware splitting
636
+ * when needed. For each sub-run the matrix origin is advanced along the
637
+ * text direction (cos, sin) by the rendered width.
638
+ *
639
+ * When `useType3` is false this collapses to a single BT/ET pair — identical
640
+ * to the old `emitText()` path but wrapped in begin/end for convenience.
641
+ */
642
+ export function emitTextWithMatrix(stream, text, a, b, c, d, tx, ty, type1ResourceName, fontSize, fontManager, useType3) {
643
+ if (!useType3) {
644
+ stream.beginText();
645
+ stream.setFont(type1ResourceName, fontSize);
646
+ stream.setTextMatrix(a, b, c, d, tx, ty);
647
+ emitText(stream, fontManager, text, type1ResourceName);
648
+ stream.endText();
649
+ return;
650
+ }
651
+ // Type3 path: split into runs and advance origin along text direction
652
+ const runs = splitTextRuns(text, fontManager);
653
+ let curTx = tx;
654
+ let curTy = ty;
655
+ for (const run of runs) {
656
+ stream.beginText();
657
+ if (run.type3) {
658
+ stream.setFont(run.type3.resourceName, fontSize);
659
+ stream.setTextMatrix(a, b, c, d, curTx, curTy);
660
+ stream.showTextHex(run.type3.hex);
661
+ }
662
+ else {
663
+ stream.setFont(type1ResourceName, fontSize);
664
+ stream.setTextMatrix(a, b, c, d, curTx, curTy);
665
+ emitText(stream, fontManager, run.text, type1ResourceName);
666
+ }
667
+ stream.endText();
668
+ const w = fontManager.measureText(run.text, run.type3?.resourceName ?? type1ResourceName, fontSize);
669
+ // Advance along the text direction (first column of the matrix)
670
+ curTx += a * w;
671
+ curTy += b * w;
672
+ }
673
+ }
674
+ /**
675
+ * Split a line of text into runs of consecutive WinAnsi and non-WinAnsi
676
+ * characters. Each non-WinAnsi character becomes its own Type3 run (since
677
+ * different code points may map to different Type3 fonts). Consecutive
678
+ * WinAnsi characters are merged into a single Type1 run.
679
+ */
680
+ function splitTextRuns(text, fontManager) {
681
+ const runs = [];
682
+ let winAnsiBuffer = "";
683
+ const flushWinAnsi = () => {
684
+ if (winAnsiBuffer) {
685
+ runs.push({ text: winAnsiBuffer, type3: null });
686
+ winAnsiBuffer = "";
687
+ }
688
+ };
689
+ for (let i = 0; i < text.length; i++) {
690
+ const cp = text.codePointAt(i);
691
+ const ch = String.fromCodePoint(cp);
692
+ if (cp > 0xffff) {
693
+ i++; // skip low surrogate
694
+ }
695
+ if (fontManager.needsType3(cp)) {
696
+ flushWinAnsi();
697
+ const t3 = fontManager.resolveType3(cp);
698
+ if (t3) {
699
+ const hex = fontManager.encodeType3Char(cp);
700
+ runs.push({
701
+ text: ch,
702
+ type3: { resourceName: t3.resourceName, hex: hex ?? "<00>" }
703
+ });
704
+ }
705
+ else {
706
+ // Character tracked but Type3 not yet written — render as space
707
+ runs.push({ text: ch, type3: null });
708
+ }
709
+ }
710
+ else {
711
+ winAnsiBuffer += ch;
712
+ }
713
+ }
714
+ flushWinAnsi();
715
+ return runs;
716
+ }
717
+ /**
718
+ * Render a text string at (textX, textY) using Type3-aware splitting when needed.
719
+ * Returns the rendered width so the caller can advance textX.
720
+ */
721
+ function emitTextWithType3(stream, text, textX, textY, type1ResourceName, fontSize, fontManager, useType3) {
722
+ emitTextWithMatrix(stream, text, 1, 0, 0, 1, textX, textY, type1ResourceName, fontSize, fontManager, useType3);
723
+ return fontManager.measureText(text, type1ResourceName, fontSize);
724
+ }
672
725
  /**
673
726
  * Draw vertical stacked text (each character top-to-bottom).
674
727
  * Newlines (\n) start a new column to the right.
@@ -699,6 +752,7 @@ function drawVerticalStackedText(stream, cell, fontManager, _indentPts, scaleFac
699
752
  startX = rect.x + pad.left + columnWidth / 2;
700
753
  }
701
754
  stream.setFillColor(cell.textColor);
755
+ const useType3 = fontManager.hasType3Fonts() && !isEmbedded;
702
756
  for (let colIdx = 0; colIdx < columns.length; colIdx++) {
703
757
  const colText = columns[colIdx];
704
758
  const colX = startX + colIdx * columnWidth;
@@ -720,11 +774,7 @@ function drawVerticalStackedText(stream, cell, fontManager, _indentPts, scaleFac
720
774
  break;
721
775
  }
722
776
  const charWidth = fontManager.measureText(ch, resourceName, fontSize);
723
- stream.beginText();
724
- stream.setFont(resourceName, fontSize);
725
- stream.setTextMatrix(1, 0, 0, 1, colX - charWidth / 2, currentY);
726
- emitText(stream, fontManager, ch, resourceName);
727
- stream.endText();
777
+ emitTextWithMatrix(stream, ch, 1, 0, 0, 1, colX - charWidth / 2, currentY, resourceName, fontSize, fontManager, useType3);
728
778
  currentY -= charHeight;
729
779
  }
730
780
  }
@@ -865,17 +915,8 @@ function drawPageHeader(stream, page, options, fontManager) {
865
915
  const y = page.height - options.margins.top + 5;
866
916
  stream.save();
867
917
  stream.setFillColor({ r: 0.3, g: 0.3, b: 0.3 });
868
- stream.beginText();
869
- stream.setFont(resourceName, headerFontSize);
870
- stream.setTextMatrix(1, 0, 0, 1, x, y);
871
- const hex = fontManager.encodeText(headerText, resourceName);
872
- if (hex) {
873
- stream.showTextHex(hex);
874
- }
875
- else {
876
- stream.showText(headerText);
877
- }
878
- stream.endText();
918
+ const useType3 = fontManager.hasType3Fonts() && !fontManager.hasEmbeddedFont();
919
+ emitTextWithMatrix(stream, headerText, 1, 0, 0, 1, x, y, resourceName, headerFontSize, fontManager, useType3);
879
920
  stream.restore();
880
921
  }
881
922
  function drawPageFooter(stream, page, options, fontManager, totalPages) {
@@ -980,6 +1021,7 @@ function renderTextWatermark(stream, page, watermark, fontManager) {
980
1021
  const sin = Math.sin(radians);
981
1022
  const needsAlpha = opacity < 1;
982
1023
  const gsName = needsAlpha ? alphaGsName(opacity) : "";
1024
+ const useType3 = fontManager.hasType3Fonts() && !isEmbedded;
983
1025
  const drawSingleWatermark = (cx, cy) => {
984
1026
  // Center the text at (cx, cy), compensating for both width and ascent height
985
1027
  const halfW = textWidth / 2;
@@ -991,17 +1033,7 @@ function renderTextWatermark(stream, page, watermark, fontManager) {
991
1033
  stream.setGraphicsState(gsName);
992
1034
  }
993
1035
  stream.setFillColor(color);
994
- stream.beginText();
995
- stream.setFont(resourceName, fontSize);
996
- stream.setTextMatrix(cos, sin, -sin, cos, tx, ty);
997
- const hex = fontManager.encodeText(watermark.text, resourceName);
998
- if (hex) {
999
- stream.showTextHex(hex);
1000
- }
1001
- else {
1002
- stream.showText(watermark.text);
1003
- }
1004
- stream.endText();
1036
+ emitTextWithMatrix(stream, watermark.text, cos, sin, -sin, cos, tx, ty, resourceName, fontSize, fontManager, useType3);
1005
1037
  stream.restore();
1006
1038
  };
1007
1039
  if (watermark.repeat) {
@@ -11,12 +11,13 @@ import { yieldToEventLoop } from "../../../utils/utils.base.js";
11
11
  import { writeImageXObject } from "../builder/image-utils.js";
12
12
  import { initEncryption } from "../core/encryption.js";
13
13
  import { PdfDict, pdfRef, pdfNumber, pdfString as pdfStr } from "../core/pdf-object.js";
14
- import { PdfContentStream } from "../core/pdf-stream.js";
14
+ import { PdfContentStream, isWinAnsiCodePoint } from "../core/pdf-stream.js";
15
15
  import { PdfWriter } from "../core/pdf-writer.js";
16
16
  import { PdfError, PdfRenderError } from "../errors.js";
17
17
  import { FontManager, resolvePdfFontName } from "../font/font-manager.js";
18
+ import { discoverSystemFontCandidates } from "../font/system-fonts.js";
18
19
  import { parseTtf } from "../font/ttf-parser.js";
19
- import { PageSizes } from "../types.js";
20
+ import { PageSizes, PdfCellType } from "../types.js";
20
21
  import { layoutSheet } from "./layout-engine.js";
21
22
  import { renderPage, alphaGsName, renderWatermark } from "./page-renderer.js";
22
23
  import { argbToPdfColor } from "./style-converter.js";
@@ -50,13 +51,39 @@ function prepareExport(workbook, options) {
50
51
  }
51
52
  const fontManager = new FontManager();
52
53
  const writer = new PdfWriter();
53
- if (options?.font) {
54
+ // Determine font data: user-provided > auto-discovered system font
55
+ let fontData = options?.font ?? null;
56
+ if (!fontData) {
57
+ // Collect non-WinAnsi code points from the document (single pass)
58
+ const nonWinAnsi = collectNonWinAnsiCodePoints(sheets);
59
+ if (nonWinAnsi.size > 0) {
60
+ // Try system font candidates in preference order until one covers all chars
61
+ for (const candidate of discoverSystemFontCandidates()) {
62
+ try {
63
+ const testTtf = parseTtf(candidate);
64
+ const allCovered = [...nonWinAnsi].every(cp => testTtf.cmap.has(cp));
65
+ if (allCovered) {
66
+ fontData = candidate;
67
+ break;
68
+ }
69
+ }
70
+ catch {
71
+ // Parse failed — try next candidate
72
+ }
73
+ }
74
+ }
75
+ }
76
+ if (fontData) {
54
77
  try {
55
- const ttf = parseTtf(options.font);
78
+ const ttf = parseTtf(fontData);
56
79
  fontManager.registerEmbeddedFont(ttf);
57
80
  }
58
81
  catch (err) {
59
- throw new PdfRenderError("Failed to parse TrueType font", { cause: err });
82
+ if (options?.font) {
83
+ // Only throw if the user explicitly provided a font
84
+ throw new PdfRenderError("Failed to parse TrueType font", { cause: err });
85
+ }
86
+ // Auto-discovered font failed to parse — silently fall back to Type1 + Type3
60
87
  }
61
88
  }
62
89
  return { sheets, fontManager, writer, allPages: [] };
@@ -500,3 +527,44 @@ function isWatermarkApplicable(watermark, page) {
500
527
  }
501
528
  return true;
502
529
  }
530
+ // =============================================================================
531
+ // Non-WinAnsi Detection
532
+ // =============================================================================
533
+ /**
534
+ * Collect all non-WinAnsi code points from sheet text (single pass).
535
+ * Returns an empty set if all text is WinAnsi-representable.
536
+ */
537
+ function collectNonWinAnsiCodePoints(sheets) {
538
+ const result = new Set();
539
+ for (const sheet of sheets) {
540
+ for (const row of sheet.rows.values()) {
541
+ for (const cell of row.cells.values()) {
542
+ collectFromText(cell.text, result);
543
+ if (cell.type === PdfCellType.RichText &&
544
+ cell.value &&
545
+ typeof cell.value === "object" &&
546
+ "richText" in cell.value) {
547
+ const runs = cell.value.richText;
548
+ for (const run of runs) {
549
+ collectFromText(run.text, result);
550
+ }
551
+ }
552
+ }
553
+ }
554
+ }
555
+ return result;
556
+ }
557
+ function collectFromText(text, out) {
558
+ if (!text) {
559
+ return;
560
+ }
561
+ for (let i = 0; i < text.length; i++) {
562
+ const cp = text.codePointAt(i);
563
+ if (cp > 0xffff) {
564
+ i++;
565
+ }
566
+ if (!isWinAnsiCodePoint(cp)) {
567
+ out.add(cp);
568
+ }
569
+ }
570
+ }