@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.
- package/dist/browser/modules/pdf/builder/document-builder.js +4 -23
- package/dist/browser/modules/pdf/core/pdf-stream.d.ts +15 -0
- package/dist/browser/modules/pdf/core/pdf-stream.js +47 -3
- package/dist/browser/modules/pdf/font/font-manager.d.ts +37 -6
- package/dist/browser/modules/pdf/font/font-manager.js +129 -17
- package/dist/browser/modules/pdf/font/system-fonts.d.ts +41 -0
- package/dist/browser/modules/pdf/font/system-fonts.js +188 -0
- package/dist/browser/modules/pdf/font/ttf-parser.js +29 -1
- package/dist/browser/modules/pdf/font/type3-font.d.ts +35 -0
- package/dist/browser/modules/pdf/font/type3-font.js +228 -0
- package/dist/browser/modules/pdf/font/type3-glyphs-extended.d.ts +33 -0
- package/dist/browser/modules/pdf/font/type3-glyphs-extended.js +4164 -0
- package/dist/browser/modules/pdf/font/type3-glyphs-extended2.d.ts +16 -0
- package/dist/browser/modules/pdf/font/type3-glyphs-extended2.js +9649 -0
- package/dist/browser/modules/pdf/font/type3-glyphs-fill.d.ts +17 -0
- package/dist/browser/modules/pdf/font/type3-glyphs-fill.js +5438 -0
- package/dist/browser/modules/pdf/font/type3-glyphs-quality.d.ts +28 -0
- package/dist/browser/modules/pdf/font/type3-glyphs-quality.js +5345 -0
- package/dist/browser/modules/pdf/font/type3-glyphs.d.ts +79 -0
- package/dist/browser/modules/pdf/font/type3-glyphs.js +2567 -0
- package/dist/browser/modules/pdf/render/layout-engine.js +36 -23
- package/dist/browser/modules/pdf/render/page-renderer.d.ts +9 -0
- package/dist/browser/modules/pdf/render/page-renderer.js +110 -78
- package/dist/browser/modules/pdf/render/pdf-exporter.js +73 -5
- package/dist/cjs/modules/pdf/builder/document-builder.js +3 -22
- package/dist/cjs/modules/pdf/core/pdf-stream.js +49 -3
- package/dist/cjs/modules/pdf/font/font-manager.js +129 -17
- package/dist/cjs/modules/pdf/font/system-fonts.js +194 -0
- package/dist/cjs/modules/pdf/font/ttf-parser.js +29 -1
- package/dist/cjs/modules/pdf/font/type3-font.js +231 -0
- package/dist/cjs/modules/pdf/font/type3-glyphs-extended.js +4167 -0
- package/dist/cjs/modules/pdf/font/type3-glyphs-extended2.js +9652 -0
- package/dist/cjs/modules/pdf/font/type3-glyphs-fill.js +5441 -0
- package/dist/cjs/modules/pdf/font/type3-glyphs-quality.js +5348 -0
- package/dist/cjs/modules/pdf/font/type3-glyphs.js +2573 -0
- package/dist/cjs/modules/pdf/render/layout-engine.js +36 -23
- package/dist/cjs/modules/pdf/render/page-renderer.js +111 -78
- package/dist/cjs/modules/pdf/render/pdf-exporter.js +71 -3
- package/dist/esm/modules/pdf/builder/document-builder.js +4 -23
- package/dist/esm/modules/pdf/core/pdf-stream.js +47 -3
- package/dist/esm/modules/pdf/font/font-manager.js +129 -17
- package/dist/esm/modules/pdf/font/system-fonts.js +188 -0
- package/dist/esm/modules/pdf/font/ttf-parser.js +29 -1
- package/dist/esm/modules/pdf/font/type3-font.js +228 -0
- package/dist/esm/modules/pdf/font/type3-glyphs-extended.js +4164 -0
- package/dist/esm/modules/pdf/font/type3-glyphs-extended2.js +9649 -0
- package/dist/esm/modules/pdf/font/type3-glyphs-fill.js +5438 -0
- package/dist/esm/modules/pdf/font/type3-glyphs-quality.js +5345 -0
- package/dist/esm/modules/pdf/font/type3-glyphs.js +2567 -0
- package/dist/esm/modules/pdf/render/layout-engine.js +36 -23
- package/dist/esm/modules/pdf/render/page-renderer.js +110 -78
- package/dist/esm/modules/pdf/render/pdf-exporter.js +73 -5
- package/dist/iife/excelts.iife.js +25445 -344
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +48 -46
- package/dist/types/modules/pdf/core/pdf-stream.d.ts +15 -0
- package/dist/types/modules/pdf/font/font-manager.d.ts +37 -6
- package/dist/types/modules/pdf/font/system-fonts.d.ts +41 -0
- package/dist/types/modules/pdf/font/type3-font.d.ts +35 -0
- package/dist/types/modules/pdf/font/type3-glyphs-extended.d.ts +33 -0
- package/dist/types/modules/pdf/font/type3-glyphs-extended2.d.ts +16 -0
- package/dist/types/modules/pdf/font/type3-glyphs-fill.d.ts +17 -0
- package/dist/types/modules/pdf/font/type3-glyphs-quality.d.ts +28 -0
- package/dist/types/modules/pdf/font/type3-glyphs.d.ts +79 -0
- package/dist/types/modules/pdf/render/page-renderer.d.ts +9 -0
- 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 —
|
|
400
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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 +=
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
877
|
-
stream
|
|
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.
|
|
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
|
-
|
|
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)(
|
|
81
|
+
const ttf = (0, ttf_parser_1.parseTtf)(fontData);
|
|
59
82
|
fontManager.registerEmbeddedFont(ttf);
|
|
60
83
|
}
|
|
61
84
|
catch (err) {
|
|
62
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
}
|