@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
@@ -396,38 +396,49 @@ function computeRowHeights(sheet, scaleFactor, printRange, fontManager, options)
396
396
  height = row.height;
397
397
  }
398
398
  else if (row?.height) {
399
- // Excel auto-calculated height — trust it as-is
400
- height = row.height;
399
+ // Excel auto-calculated height — use it as a baseline, but ensure
400
+ // the row is tall enough for wrapped text. The stored height may be
401
+ // stale when columns are narrower in the PDF layout or when the PDF
402
+ // uses different font metrics than the original Excel file.
403
+ height = Math.max(row.height, autoRowHeight(row, scaleFactor, sheet, fontManager, options));
401
404
  }
402
405
  else {
403
406
  // No height info: auto-size based on cell content
404
- height = DEFAULT_ROW_HEIGHT;
405
- if (row) {
406
- for (const cell of row.cells.values()) {
407
- const fontSize = getCellFontSize(cell);
408
- const wrapLineCount = countWrapLines(cell, fontSize, scaleFactor, sheet, fontManager, options);
409
- const lineHeight = fontSize * constants_1.LINE_HEIGHT_FACTOR;
410
- // Account for border width: half of each border extends inward
411
- const borderTop = cell.style?.border?.top?.style
412
- ? (0, style_converter_1.borderStyleToLineWidth)(cell.style.border.top.style) / 2
413
- : 0;
414
- const borderBottom = cell.style?.border?.bottom?.style
415
- ? (0, style_converter_1.borderStyleToLineWidth)(cell.style.border.bottom.style) / 2
416
- : 0;
417
- const neededHeight = fontSize +
418
- (wrapLineCount - 1) * lineHeight +
419
- (constants_1.CELL_PADDING_V + borderTop + borderBottom) * 2;
420
- if (neededHeight > height) {
421
- height = neededHeight;
422
- }
423
- }
424
- }
407
+ height = autoRowHeight(row, scaleFactor, sheet, fontManager, options);
425
408
  }
426
409
  rowHeights.push(height * scaleFactor);
427
410
  visibleRows.push(r);
428
411
  }
429
412
  return { rowHeights, visibleRows };
430
413
  }
414
+ /**
415
+ * Compute the minimum row height required to display wrapped cell content.
416
+ * Returns at least `DEFAULT_ROW_HEIGHT`.
417
+ */
418
+ function autoRowHeight(row, scaleFactor, sheet, fontManager, options) {
419
+ let height = DEFAULT_ROW_HEIGHT;
420
+ if (row) {
421
+ for (const cell of row.cells.values()) {
422
+ const fontSize = getCellFontSize(cell);
423
+ const wrapLineCount = countWrapLines(cell, fontSize, scaleFactor, sheet, fontManager, options);
424
+ const lineHeight = fontSize * constants_1.LINE_HEIGHT_FACTOR;
425
+ // Account for border width: half of each border extends inward
426
+ const borderTop = cell.style?.border?.top?.style
427
+ ? (0, style_converter_1.borderStyleToLineWidth)(cell.style.border.top.style) / 2
428
+ : 0;
429
+ const borderBottom = cell.style?.border?.bottom?.style
430
+ ? (0, style_converter_1.borderStyleToLineWidth)(cell.style.border.bottom.style) / 2
431
+ : 0;
432
+ const neededHeight = fontSize +
433
+ (wrapLineCount - 1) * lineHeight +
434
+ (constants_1.CELL_PADDING_V + borderTop + borderBottom) * 2;
435
+ if (neededHeight > height) {
436
+ height = neededHeight;
437
+ }
438
+ }
439
+ }
440
+ return height;
441
+ }
431
442
  /**
432
443
  * Get the largest font size for a cell, checking rich text runs.
433
444
  */
@@ -658,6 +669,8 @@ function buildLayoutCell(cell, x, y, width, height, colSpan, rowSpan, options, f
658
669
  else {
659
670
  const pdfFontName = (0, font_manager_1.resolvePdfFontName)(fontProps.fontFamily, fontProps.bold, fontProps.italic);
660
671
  fontManager.ensureFont(pdfFontName);
672
+ // Track non-WinAnsi code points for Type3 fallback font generation
673
+ fontManager.trackText(text);
661
674
  }
662
675
  // Rich text runs
663
676
  const richText = buildRichTextRuns(cell, options, fontManager, scaleFactor);
@@ -12,6 +12,7 @@
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.renderPage = renderPage;
15
+ exports.emitTextWithMatrix = emitTextWithMatrix;
15
16
  exports.alphaGsName = alphaGsName;
16
17
  exports.computeTextStartY = computeTextStartY;
17
18
  exports.computeTextX = computeTextX;
@@ -266,23 +267,14 @@ function drawCellText(stream, cell, fontManager, alphaValues, scaleFactor = 1) {
266
267
  const totalTextHeight = lines.length * lineHeight;
267
268
  const textStartY = computeTextStartY(verticalAlign, rect, totalTextHeight, ascent, pad.top, pad.bottom);
268
269
  stream.setFillColor(cell.textColor);
269
- stream.beginText();
270
- stream.setFont(resourceName, fontSize);
270
+ const useType3 = fontManager.hasType3Fonts() && !isEmbedded;
271
271
  for (let i = 0; i < lines.length; i++) {
272
272
  const line = lines[i];
273
273
  const lineY = textStartY - i * lineHeight;
274
274
  const textWidth = measure(line);
275
275
  const textX = computeTextX(horizontalAlign, rect, textWidth, indentPts, pad.left, pad.right);
276
- stream.setTextMatrix(1, 0, 0, 1, textX, lineY);
277
- const hexEncoded = fontManager.encodeText(line, resourceName);
278
- if (hexEncoded) {
279
- stream.showTextHex(hexEncoded);
280
- }
281
- else {
282
- stream.showText(line);
283
- }
276
+ emitTextWithType3(stream, line, textX, lineY, resourceName, fontSize, fontManager, useType3);
284
277
  }
285
- stream.endText();
286
278
  drawTextDecorations(stream, cell, lines, lineHeight, textStartY, measure, resourceName, fontManager, indentPts, pad);
287
279
  stream.restore();
288
280
  }
@@ -367,21 +359,11 @@ function drawRichText(stream, cell, fontManager, indentPts, scaleFactor = 1) {
367
359
  lineWidth += fontManager.measureText(seg.text, seg.resourceName, seg.run.fontSize);
368
360
  }
369
361
  let textX = computeTextX(horizontalAlign, rect, lineWidth, indentPts, pad.left, pad.right);
362
+ const useType3 = fontManager.hasType3Fonts() && !isEmbedded;
370
363
  for (const seg of segments) {
371
364
  const { run, text, resourceName } = seg;
372
- const segWidth = fontManager.measureText(text, resourceName, run.fontSize);
373
365
  stream.setFillColor(run.textColor);
374
- stream.beginText();
375
- stream.setFont(resourceName, run.fontSize);
376
- stream.setTextMatrix(1, 0, 0, 1, textX, lineY);
377
- const hex = fontManager.encodeText(text, resourceName);
378
- if (hex) {
379
- stream.showTextHex(hex);
380
- }
381
- else {
382
- stream.showText(text);
383
- }
384
- stream.endText();
366
+ const segWidth = emitTextWithType3(stream, text, textX, lineY, resourceName, run.fontSize, fontManager, useType3);
385
367
  if (run.strike) {
386
368
  const descent = fontManager.getFontDescent(resourceName, run.fontSize);
387
369
  const y = lineY + descent + run.fontSize * 0.3;
@@ -411,23 +393,13 @@ function drawRichText(stream, cell, fontManager, indentPts, scaleFactor = 1) {
411
393
  const ascent = fontManager.getFontAscent(primaryResourceName, primaryFontSize);
412
394
  const textStartY = computeTextStartY(verticalAlign, rect, lineHeight, ascent, pad.top, pad.bottom);
413
395
  let textX = computeTextX(horizontalAlign, rect, totalWidth, indentPts, pad.left, pad.right);
396
+ const useType3 = fontManager.hasType3Fonts() && !isEmbedded;
414
397
  for (let i = 0; i < runs.length; i++) {
415
398
  const run = runs[i];
416
399
  const { resourceName } = runMetrics[i];
417
400
  stream.setFillColor(run.textColor);
418
- stream.beginText();
419
- stream.setFont(resourceName, run.fontSize);
420
- stream.setTextMatrix(1, 0, 0, 1, textX, textStartY);
421
- const hexEncoded = fontManager.encodeText(run.text, resourceName);
422
- if (hexEncoded) {
423
- stream.showTextHex(hexEncoded);
424
- }
425
- else {
426
- stream.showText(run.text);
427
- }
428
- stream.endText();
401
+ const runWidth = emitTextWithType3(stream, run.text, textX, textStartY, resourceName, run.fontSize, fontManager, useType3);
429
402
  // Draw per-run decorations (strikethrough, underline)
430
- const runWidth = runMetrics[i].width;
431
403
  if (run.strike) {
432
404
  const descent = fontManager.getFontDescent(resourceName, run.fontSize);
433
405
  const y = textStartY + descent + run.fontSize * 0.3;
@@ -438,7 +410,7 @@ function drawRichText(stream, cell, fontManager, indentPts, scaleFactor = 1) {
438
410
  const y = textStartY + descent * 0.5;
439
411
  stream.drawLine(textX, y, textX + runWidth, y, run.textColor, 0.5);
440
412
  }
441
- textX += runMetrics[i].width;
413
+ textX += runWidth;
442
414
  }
443
415
  }
444
416
  // =============================================================================
@@ -535,6 +507,7 @@ function drawRotated90(stream, cell, lines, fontManager, resourceName, fontSize,
535
507
  // left (default)
536
508
  startX = rect.x + pad.left + ascent;
537
509
  }
510
+ const useType3 = fontManager.hasType3Fonts() && !fontManager.hasEmbeddedFont();
538
511
  for (let i = 0; i < lines.length; i++) {
539
512
  const line = lines[i];
540
513
  const lineWidth = fontManager.measureText(line, resourceName, fontSize);
@@ -554,11 +527,7 @@ function drawRotated90(stream, cell, lines, fontManager, resourceName, fontSize,
554
527
  ty = rect.y + pad.bottom;
555
528
  }
556
529
  ty = Math.max(ty, rect.y + pad.bottom);
557
- stream.beginText();
558
- stream.setFont(resourceName, fontSize);
559
- stream.setTextMatrix(0, 1, -1, 0, colX, ty);
560
- emitText(stream, fontManager, line, resourceName);
561
- stream.endText();
530
+ emitTextWithMatrix(stream, line, 0, 1, -1, 0, colX, ty, resourceName, fontSize, fontManager, useType3);
562
531
  }
563
532
  }
564
533
  /** -90° (270° CW): text reads top-to-bottom, lines stack right-to-left. */
@@ -577,6 +546,7 @@ function drawRotatedMinus90(stream, cell, lines, fontManager, resourceName, font
577
546
  // left (default)
578
547
  startX = rect.x + pad.left + totalColumnsWidth - lineHeight + ascent;
579
548
  }
549
+ const useType3 = fontManager.hasType3Fonts() && !fontManager.hasEmbeddedFont();
580
550
  for (let i = 0; i < lines.length; i++) {
581
551
  const line = lines[i];
582
552
  const lineWidth = fontManager.measureText(line, resourceName, fontSize);
@@ -596,11 +566,7 @@ function drawRotatedMinus90(stream, cell, lines, fontManager, resourceName, font
596
566
  ty = rect.y + pad.bottom + lineWidth;
597
567
  }
598
568
  ty = Math.min(ty, rect.y + rect.height - pad.top);
599
- stream.beginText();
600
- stream.setFont(resourceName, fontSize);
601
- stream.setTextMatrix(0, -1, 1, 0, colX, ty);
602
- emitText(stream, fontManager, line, resourceName);
603
- stream.endText();
569
+ emitTextWithMatrix(stream, line, 0, -1, 1, 0, colX, ty, resourceName, fontSize, fontManager, useType3);
604
570
  }
605
571
  }
606
572
  /** General rotation — center a multi-line text block in the cell. */
@@ -652,6 +618,7 @@ function drawRotatedGeneral(stream, cell, lines, fontManager, resourceName, font
652
618
  // center (default for rotated)
653
619
  cx = rect.x + rect.width / 2 + indentOffset + slantAtCy;
654
620
  }
621
+ const useType3 = fontManager.hasType3Fonts() && !fontManager.hasEmbeddedFont();
655
622
  for (let i = 0; i < lines.length; i++) {
656
623
  const line = lines[i];
657
624
  const lineWidth = fontManager.measureText(line, resourceName, fontSize);
@@ -660,11 +627,7 @@ function drawRotatedGeneral(stream, cell, lines, fontManager, resourceName, font
660
627
  const offsetY = -ascent / 2 - lineOffset;
661
628
  const tx = cx + offsetX * cos - offsetY * sin;
662
629
  const ty = cy + offsetX * sin + offsetY * cos;
663
- stream.beginText();
664
- stream.setFont(resourceName, fontSize);
665
- stream.setTextMatrix(cos, sin, -sin, cos, tx, ty);
666
- emitText(stream, fontManager, line, resourceName);
667
- stream.endText();
630
+ emitTextWithMatrix(stream, line, cos, sin, -sin, cos, tx, ty, resourceName, fontSize, fontManager, useType3);
668
631
  }
669
632
  }
670
633
  /** Emit a text string with hex encoding if available. */
@@ -677,6 +640,97 @@ function emitText(stream, fontManager, text, resourceName) {
677
640
  stream.showText(text);
678
641
  }
679
642
  }
643
+ /**
644
+ * Render a text string with a custom text matrix, using Type3-aware splitting
645
+ * when needed. For each sub-run the matrix origin is advanced along the
646
+ * text direction (cos, sin) by the rendered width.
647
+ *
648
+ * When `useType3` is false this collapses to a single BT/ET pair — identical
649
+ * to the old `emitText()` path but wrapped in begin/end for convenience.
650
+ */
651
+ function emitTextWithMatrix(stream, text, a, b, c, d, tx, ty, type1ResourceName, fontSize, fontManager, useType3) {
652
+ if (!useType3) {
653
+ stream.beginText();
654
+ stream.setFont(type1ResourceName, fontSize);
655
+ stream.setTextMatrix(a, b, c, d, tx, ty);
656
+ emitText(stream, fontManager, text, type1ResourceName);
657
+ stream.endText();
658
+ return;
659
+ }
660
+ // Type3 path: split into runs and advance origin along text direction
661
+ const runs = splitTextRuns(text, fontManager);
662
+ let curTx = tx;
663
+ let curTy = ty;
664
+ for (const run of runs) {
665
+ stream.beginText();
666
+ if (run.type3) {
667
+ stream.setFont(run.type3.resourceName, fontSize);
668
+ stream.setTextMatrix(a, b, c, d, curTx, curTy);
669
+ stream.showTextHex(run.type3.hex);
670
+ }
671
+ else {
672
+ stream.setFont(type1ResourceName, fontSize);
673
+ stream.setTextMatrix(a, b, c, d, curTx, curTy);
674
+ emitText(stream, fontManager, run.text, type1ResourceName);
675
+ }
676
+ stream.endText();
677
+ const w = fontManager.measureText(run.text, run.type3?.resourceName ?? type1ResourceName, fontSize);
678
+ // Advance along the text direction (first column of the matrix)
679
+ curTx += a * w;
680
+ curTy += b * w;
681
+ }
682
+ }
683
+ /**
684
+ * Split a line of text into runs of consecutive WinAnsi and non-WinAnsi
685
+ * characters. Each non-WinAnsi character becomes its own Type3 run (since
686
+ * different code points may map to different Type3 fonts). Consecutive
687
+ * WinAnsi characters are merged into a single Type1 run.
688
+ */
689
+ function splitTextRuns(text, fontManager) {
690
+ const runs = [];
691
+ let winAnsiBuffer = "";
692
+ const flushWinAnsi = () => {
693
+ if (winAnsiBuffer) {
694
+ runs.push({ text: winAnsiBuffer, type3: null });
695
+ winAnsiBuffer = "";
696
+ }
697
+ };
698
+ for (let i = 0; i < text.length; i++) {
699
+ const cp = text.codePointAt(i);
700
+ const ch = String.fromCodePoint(cp);
701
+ if (cp > 0xffff) {
702
+ i++; // skip low surrogate
703
+ }
704
+ if (fontManager.needsType3(cp)) {
705
+ flushWinAnsi();
706
+ const t3 = fontManager.resolveType3(cp);
707
+ if (t3) {
708
+ const hex = fontManager.encodeType3Char(cp);
709
+ runs.push({
710
+ text: ch,
711
+ type3: { resourceName: t3.resourceName, hex: hex ?? "<00>" }
712
+ });
713
+ }
714
+ else {
715
+ // Character tracked but Type3 not yet written — render as space
716
+ runs.push({ text: ch, type3: null });
717
+ }
718
+ }
719
+ else {
720
+ winAnsiBuffer += ch;
721
+ }
722
+ }
723
+ flushWinAnsi();
724
+ return runs;
725
+ }
726
+ /**
727
+ * Render a text string at (textX, textY) using Type3-aware splitting when needed.
728
+ * Returns the rendered width so the caller can advance textX.
729
+ */
730
+ function emitTextWithType3(stream, text, textX, textY, type1ResourceName, fontSize, fontManager, useType3) {
731
+ emitTextWithMatrix(stream, text, 1, 0, 0, 1, textX, textY, type1ResourceName, fontSize, fontManager, useType3);
732
+ return fontManager.measureText(text, type1ResourceName, fontSize);
733
+ }
680
734
  /**
681
735
  * Draw vertical stacked text (each character top-to-bottom).
682
736
  * Newlines (\n) start a new column to the right.
@@ -707,6 +761,7 @@ function drawVerticalStackedText(stream, cell, fontManager, _indentPts, scaleFac
707
761
  startX = rect.x + pad.left + columnWidth / 2;
708
762
  }
709
763
  stream.setFillColor(cell.textColor);
764
+ const useType3 = fontManager.hasType3Fonts() && !isEmbedded;
710
765
  for (let colIdx = 0; colIdx < columns.length; colIdx++) {
711
766
  const colText = columns[colIdx];
712
767
  const colX = startX + colIdx * columnWidth;
@@ -728,11 +783,7 @@ function drawVerticalStackedText(stream, cell, fontManager, _indentPts, scaleFac
728
783
  break;
729
784
  }
730
785
  const charWidth = fontManager.measureText(ch, resourceName, fontSize);
731
- stream.beginText();
732
- stream.setFont(resourceName, fontSize);
733
- stream.setTextMatrix(1, 0, 0, 1, colX - charWidth / 2, currentY);
734
- emitText(stream, fontManager, ch, resourceName);
735
- stream.endText();
786
+ emitTextWithMatrix(stream, ch, 1, 0, 0, 1, colX - charWidth / 2, currentY, resourceName, fontSize, fontManager, useType3);
736
787
  currentY -= charHeight;
737
788
  }
738
789
  }
@@ -873,17 +924,8 @@ function drawPageHeader(stream, page, options, fontManager) {
873
924
  const y = page.height - options.margins.top + 5;
874
925
  stream.save();
875
926
  stream.setFillColor({ r: 0.3, g: 0.3, b: 0.3 });
876
- stream.beginText();
877
- stream.setFont(resourceName, headerFontSize);
878
- stream.setTextMatrix(1, 0, 0, 1, x, y);
879
- const hex = fontManager.encodeText(headerText, resourceName);
880
- if (hex) {
881
- stream.showTextHex(hex);
882
- }
883
- else {
884
- stream.showText(headerText);
885
- }
886
- stream.endText();
927
+ const useType3 = fontManager.hasType3Fonts() && !fontManager.hasEmbeddedFont();
928
+ emitTextWithMatrix(stream, headerText, 1, 0, 0, 1, x, y, resourceName, headerFontSize, fontManager, useType3);
887
929
  stream.restore();
888
930
  }
889
931
  function drawPageFooter(stream, page, options, fontManager, totalPages) {
@@ -988,6 +1030,7 @@ function renderTextWatermark(stream, page, watermark, fontManager) {
988
1030
  const sin = Math.sin(radians);
989
1031
  const needsAlpha = opacity < 1;
990
1032
  const gsName = needsAlpha ? alphaGsName(opacity) : "";
1033
+ const useType3 = fontManager.hasType3Fonts() && !isEmbedded;
991
1034
  const drawSingleWatermark = (cx, cy) => {
992
1035
  // Center the text at (cx, cy), compensating for both width and ascent height
993
1036
  const halfW = textWidth / 2;
@@ -999,17 +1042,7 @@ function renderTextWatermark(stream, page, watermark, fontManager) {
999
1042
  stream.setGraphicsState(gsName);
1000
1043
  }
1001
1044
  stream.setFillColor(color);
1002
- stream.beginText();
1003
- stream.setFont(resourceName, fontSize);
1004
- stream.setTextMatrix(cos, sin, -sin, cos, tx, ty);
1005
- const hex = fontManager.encodeText(watermark.text, resourceName);
1006
- if (hex) {
1007
- stream.showTextHex(hex);
1008
- }
1009
- else {
1010
- stream.showText(watermark.text);
1011
- }
1012
- stream.endText();
1045
+ emitTextWithMatrix(stream, watermark.text, cos, sin, -sin, cos, tx, ty, resourceName, fontSize, fontManager, useType3);
1013
1046
  stream.restore();
1014
1047
  };
1015
1048
  if (watermark.repeat) {
@@ -18,6 +18,7 @@ const pdf_stream_1 = require("../core/pdf-stream");
18
18
  const pdf_writer_1 = require("../core/pdf-writer");
19
19
  const errors_1 = require("../errors");
20
20
  const font_manager_1 = require("../font/font-manager");
21
+ const system_fonts_1 = require("../font/system-fonts");
21
22
  const ttf_parser_1 = require("../font/ttf-parser");
22
23
  const types_1 = require("../types");
23
24
  const layout_engine_1 = require("./layout-engine");
@@ -53,13 +54,39 @@ function prepareExport(workbook, options) {
53
54
  }
54
55
  const fontManager = new font_manager_1.FontManager();
55
56
  const writer = new pdf_writer_1.PdfWriter();
56
- if (options?.font) {
57
+ // Determine font data: user-provided > auto-discovered system font
58
+ let fontData = options?.font ?? null;
59
+ if (!fontData) {
60
+ // Collect non-WinAnsi code points from the document (single pass)
61
+ const nonWinAnsi = collectNonWinAnsiCodePoints(sheets);
62
+ if (nonWinAnsi.size > 0) {
63
+ // Try system font candidates in preference order until one covers all chars
64
+ for (const candidate of (0, system_fonts_1.discoverSystemFontCandidates)()) {
65
+ try {
66
+ const testTtf = (0, ttf_parser_1.parseTtf)(candidate);
67
+ const allCovered = [...nonWinAnsi].every(cp => testTtf.cmap.has(cp));
68
+ if (allCovered) {
69
+ fontData = candidate;
70
+ break;
71
+ }
72
+ }
73
+ catch {
74
+ // Parse failed — try next candidate
75
+ }
76
+ }
77
+ }
78
+ }
79
+ if (fontData) {
57
80
  try {
58
- const ttf = (0, ttf_parser_1.parseTtf)(options.font);
81
+ const ttf = (0, ttf_parser_1.parseTtf)(fontData);
59
82
  fontManager.registerEmbeddedFont(ttf);
60
83
  }
61
84
  catch (err) {
62
- throw new errors_1.PdfRenderError("Failed to parse TrueType font", { cause: err });
85
+ if (options?.font) {
86
+ // Only throw if the user explicitly provided a font
87
+ throw new errors_1.PdfRenderError("Failed to parse TrueType font", { cause: err });
88
+ }
89
+ // Auto-discovered font failed to parse — silently fall back to Type1 + Type3
63
90
  }
64
91
  }
65
92
  return { sheets, fontManager, writer, allPages: [] };
@@ -503,3 +530,44 @@ function isWatermarkApplicable(watermark, page) {
503
530
  }
504
531
  return true;
505
532
  }
533
+ // =============================================================================
534
+ // Non-WinAnsi Detection
535
+ // =============================================================================
536
+ /**
537
+ * Collect all non-WinAnsi code points from sheet text (single pass).
538
+ * Returns an empty set if all text is WinAnsi-representable.
539
+ */
540
+ function collectNonWinAnsiCodePoints(sheets) {
541
+ const result = new Set();
542
+ for (const sheet of sheets) {
543
+ for (const row of sheet.rows.values()) {
544
+ for (const cell of row.cells.values()) {
545
+ collectFromText(cell.text, result);
546
+ if (cell.type === types_1.PdfCellType.RichText &&
547
+ cell.value &&
548
+ typeof cell.value === "object" &&
549
+ "richText" in cell.value) {
550
+ const runs = cell.value.richText;
551
+ for (const run of runs) {
552
+ collectFromText(run.text, result);
553
+ }
554
+ }
555
+ }
556
+ }
557
+ }
558
+ return result;
559
+ }
560
+ function collectFromText(text, out) {
561
+ if (!text) {
562
+ return;
563
+ }
564
+ for (let i = 0; i < text.length; i++) {
565
+ const cp = text.codePointAt(i);
566
+ if (cp > 0xffff) {
567
+ i++;
568
+ }
569
+ if (!(0, pdf_stream_1.isWinAnsiCodePoint)(cp)) {
570
+ out.add(cp);
571
+ }
572
+ }
573
+ }
@@ -25,7 +25,7 @@ import { PdfWriter } from "../core/pdf-writer.js";
25
25
  import { writePdfAMetadata, writePdfAOutputIntent } from "../core/pdfa.js";
26
26
  import { FontManager } from "../font/font-manager.js";
27
27
  import { parseTtf } from "../font/ttf-parser.js";
28
- import { wrapTextLines } from "../render/page-renderer.js";
28
+ import { wrapTextLines, emitTextWithMatrix } from "../render/page-renderer.js";
29
29
  import { writeImageXObject } from "./image-utils.js";
30
30
  // =============================================================================
31
31
  // Constants
@@ -87,8 +87,8 @@ export class PdfPageBuilder {
87
87
  const fontFamily = options.fontFamily ?? "Helvetica";
88
88
  // Resolve font
89
89
  const resourceName = this._fontManager.resolveFont(fontFamily, bold, italic);
90
- const encodedText = this._fontManager.encodeText(text, resourceName);
91
90
  this._fontManager.trackText(text);
91
+ const useType3 = this._fontManager.hasType3Fonts() && !this._fontManager.hasEmbeddedFont();
92
92
  if (options.maxWidth) {
93
93
  // Word-wrap (reuses the shared wrapTextLines from page-renderer)
94
94
  const measure = (s) => this._fontManager.measureText(s, resourceName, fontSize);
@@ -96,36 +96,17 @@ export class PdfPageBuilder {
96
96
  const leading = fontSize * lineHeightFactor;
97
97
  this._stream.save();
98
98
  this._stream.setFillColor(color);
99
- this._stream.beginText();
100
- this._stream.setFont(resourceName, fontSize);
101
99
  for (let i = 0; i < lines.length; i++) {
102
100
  const lineY = options.y - i * leading;
103
- this._stream.setTextMatrix(1, 0, 0, 1, options.x, lineY);
104
- const lineEncoded = this._fontManager.encodeText(lines[i], resourceName);
105
- if (lineEncoded) {
106
- this._stream.showTextHex(lineEncoded);
107
- }
108
- else {
109
- this._stream.showText(lines[i]);
110
- }
101
+ emitTextWithMatrix(this._stream, lines[i], 1, 0, 0, 1, options.x, lineY, resourceName, fontSize, this._fontManager, useType3);
111
102
  }
112
- this._stream.endText();
113
103
  this._stream.restore();
114
104
  }
115
105
  else {
116
106
  // Single line
117
107
  this._stream.save();
118
108
  this._stream.setFillColor(color);
119
- this._stream.beginText();
120
- this._stream.setFont(resourceName, fontSize);
121
- this._stream.setTextMatrix(1, 0, 0, 1, options.x, options.y);
122
- if (encodedText) {
123
- this._stream.showTextHex(encodedText);
124
- }
125
- else {
126
- this._stream.showText(text);
127
- }
128
- this._stream.endText();
109
+ emitTextWithMatrix(this._stream, text, 1, 0, 0, 1, options.x, options.y, resourceName, fontSize, this._fontManager, useType3);
129
110
  this._stream.restore();
130
111
  }
131
112
  return this;
@@ -26,6 +26,17 @@ export class PdfContentStream {
26
26
  this.parts = [];
27
27
  }
28
28
  // ===========================================================================
29
+ // Raw Operator
30
+ // ===========================================================================
31
+ /**
32
+ * Append a raw PDF operator string to the content stream.
33
+ * Use this for operators not covered by the typed API (e.g. `d1` for Type3 glyphs).
34
+ */
35
+ raw(operator) {
36
+ this.parts.push(operator);
37
+ return this;
38
+ }
39
+ // ===========================================================================
29
40
  // Graphics State
30
41
  // ===========================================================================
31
42
  /**
@@ -502,7 +513,9 @@ const UNICODE_TO_WINANSI = new Map([
502
513
  ]);
503
514
  /**
504
515
  * Convert a Unicode code point to a WinAnsi byte value.
505
- * Returns 0x3F ('?') for unmappable characters.
516
+ * Returns 0x20 (space) for unmappable characters — standard Type1 fonts
517
+ * do not contain glyphs outside the WinAnsi repertoire, so a space is
518
+ * less misleading than a literal '?'.
506
519
  */
507
520
  function unicodeToWinAnsi(cp) {
508
521
  // Direct mapping for Latin-1 range (0x00-0xFF), excluding 0x80-0x9F
@@ -517,6 +530,37 @@ function unicodeToWinAnsi(cp) {
517
530
  if (mapped !== undefined) {
518
531
  return mapped;
519
532
  }
520
- // Unmappable — use '?'
521
- return 0x3f;
533
+ // Unmappable — use space instead of '?' to avoid misleading output.
534
+ // Full Unicode support requires an embedded TrueType font (the `font`
535
+ // option in PdfExportOptions).
536
+ return 0x20;
537
+ }
538
+ /**
539
+ * Check whether a single code point is representable in WinAnsi encoding.
540
+ */
541
+ export function isWinAnsiCodePoint(cp) {
542
+ if (cp < 0x80) {
543
+ return true;
544
+ }
545
+ if (cp >= 0xa0 && cp <= 0xff) {
546
+ return true;
547
+ }
548
+ return UNICODE_TO_WINANSI.has(cp);
549
+ }
550
+ /**
551
+ * Check whether a string contains characters outside the WinAnsi repertoire.
552
+ * When true, standard Type1 fonts cannot render those characters and an
553
+ * embedded TrueType font is required for correct output.
554
+ */
555
+ export function hasNonWinAnsiChars(text) {
556
+ for (let i = 0; i < text.length; i++) {
557
+ const cp = text.codePointAt(i);
558
+ if (cp > 0xffff) {
559
+ i++;
560
+ }
561
+ if (!isWinAnsiCodePoint(cp)) {
562
+ return true;
563
+ }
564
+ }
565
+ return false;
522
566
  }