@jasy/pdf 1.0.0-alpha.1 → 1.0.0-alpha.10
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/LICENSE +21 -0
- package/README.md +35 -21
- package/dist/api/args.d.ts +1 -1
- package/dist/api/args.js +2 -5
- package/dist/api/color.d.ts +4 -4
- package/dist/api/color.js +11 -17
- package/dist/api/content.d.ts +20 -13
- package/dist/api/content.js +44 -29
- package/dist/api/descriptor.d.ts +2 -2
- package/dist/api/descriptor.js +121 -31
- package/dist/api/dimension.d.ts +76 -0
- package/dist/api/dimension.js +43 -0
- package/dist/api/font-url.d.ts +28 -0
- package/dist/api/font-url.js +134 -0
- package/dist/api/forms.d.ts +347 -0
- package/dist/api/forms.js +189 -0
- package/dist/api/gradient.d.ts +53 -0
- package/dist/api/gradient.js +97 -0
- package/dist/api/index.d.ts +13 -8
- package/dist/api/index.js +13 -24
- package/dist/api/insets.d.ts +19 -12
- package/dist/api/insets.js +19 -12
- package/dist/api/layout.d.ts +149 -25
- package/dist/api/layout.js +182 -51
- package/dist/api/page-builder.d.ts +40 -0
- package/dist/api/page-builder.js +44 -0
- package/dist/api/structure.d.ts +100 -13
- package/dist/api/structure.js +179 -88
- package/dist/api/table.d.ts +5 -5
- package/dist/api/table.js +38 -26
- package/dist/api/text.d.ts +90 -4
- package/dist/api/text.js +116 -31
- package/dist/assets/font-data.d.ts +2 -0
- package/dist/assets/font-data.js +21 -0
- package/dist/assets/font-data.ts +37 -0
- package/dist/common/color.js +1 -5
- package/dist/constants/page-sizes.js +3 -6
- package/dist/constants/pdf-parts.js +1 -4
- package/dist/crypto/legacy-handler.d.ts +43 -0
- package/dist/crypto/legacy-handler.js +145 -0
- package/dist/crypto/security-handler.d.ts +62 -0
- package/dist/crypto/security-handler.js +155 -0
- package/dist/crypto/webcrypto.d.ts +11 -0
- package/dist/crypto/webcrypto.js +62 -0
- package/dist/edit/acroform-reader.d.ts +70 -0
- package/dist/edit/acroform-reader.js +126 -0
- package/dist/edit/appearance.d.ts +47 -0
- package/dist/edit/appearance.js +171 -0
- package/dist/edit/document.d.ts +145 -0
- package/dist/edit/document.js +642 -0
- package/dist/edit/fill.d.ts +65 -0
- package/dist/edit/fill.js +424 -0
- package/dist/edit/flatten.d.ts +54 -0
- package/dist/edit/flatten.js +462 -0
- package/dist/edit/index.d.ts +20 -0
- package/dist/edit/index.js +18 -0
- package/dist/edit/lexer.d.ts +30 -0
- package/dist/edit/lexer.js +285 -0
- package/dist/edit/objects.d.ts +68 -0
- package/dist/edit/objects.js +83 -0
- package/dist/edit/writer.d.ts +60 -0
- package/dist/edit/writer.js +283 -0
- package/dist/elements/container-element.d.ts +50 -5
- package/dist/elements/container-element.js +100 -39
- package/dist/elements/forms/checkbox-element.d.ts +51 -0
- package/dist/elements/forms/checkbox-element.js +48 -0
- package/dist/elements/forms/choice-element.d.ts +53 -0
- package/dist/elements/forms/choice-element.js +59 -0
- package/dist/elements/forms/push-button-element.d.ts +56 -0
- package/dist/elements/forms/push-button-element.js +49 -0
- package/dist/elements/forms/radio-element.d.ts +52 -0
- package/dist/elements/forms/radio-element.js +49 -0
- package/dist/elements/forms/signature-element.d.ts +54 -0
- package/dist/elements/forms/signature-element.js +47 -0
- package/dist/elements/forms/text-field-element.d.ts +61 -0
- package/dist/elements/forms/text-field-element.js +54 -0
- package/dist/elements/image-element.d.ts +58 -6
- package/dist/elements/image-element.js +145 -111
- package/dist/elements/index.d.ts +26 -10
- package/dist/elements/index.js +26 -28
- package/dist/elements/layout/anchor-element.d.ts +23 -0
- package/dist/elements/layout/anchor-element.js +27 -0
- package/dist/elements/layout/bookmark-element.d.ts +27 -0
- package/dist/elements/layout/bookmark-element.js +30 -0
- package/dist/elements/layout/default-text-style-element.d.ts +30 -0
- package/dist/elements/layout/default-text-style-element.js +47 -0
- package/dist/elements/layout/deferred-element.d.ts +3 -3
- package/dist/elements/layout/deferred-element.js +4 -8
- package/dist/elements/layout/expanded-element.d.ts +6 -4
- package/dist/elements/layout/expanded-element.js +22 -17
- package/dist/elements/layout/keep-together-element.d.ts +42 -0
- package/dist/elements/layout/keep-together-element.js +85 -0
- package/dist/elements/layout/link-element.d.ts +33 -0
- package/dist/elements/layout/link-element.js +39 -0
- package/dist/elements/layout/padding-element.d.ts +10 -5
- package/dist/elements/layout/padding-element.js +25 -16
- package/dist/elements/layout/page-break-element.d.ts +17 -0
- package/dist/elements/layout/page-break-element.js +24 -0
- package/dist/elements/layout/page-builder-element.d.ts +32 -0
- package/dist/elements/layout/page-builder-element.js +45 -0
- package/dist/elements/layout/positioned-element.d.ts +47 -0
- package/dist/elements/layout/positioned-element.js +73 -0
- package/dist/elements/layout/repeating-header-element.d.ts +3 -3
- package/dist/elements/layout/repeating-header-element.js +8 -12
- package/dist/elements/layout/rotated-box-element.d.ts +35 -0
- package/dist/elements/layout/rotated-box-element.js +54 -0
- package/dist/elements/layout/rotated-element.d.ts +32 -0
- package/dist/elements/layout/rotated-element.js +41 -0
- package/dist/elements/layout/sized-container-element.d.ts +2 -2
- package/dist/elements/layout/sized-container-element.js +6 -11
- package/dist/elements/layout/struct-group.d.ts +28 -0
- package/dist/elements/layout/struct-group.js +42 -0
- package/dist/elements/line-element.d.ts +3 -3
- package/dist/elements/line-element.js +5 -10
- package/dist/elements/page-element.d.ts +26 -6
- package/dist/elements/page-element.js +72 -27
- package/dist/elements/pdf-document-element.d.ts +10 -4
- package/dist/elements/pdf-document-element.js +11 -10
- package/dist/elements/pdf-element.d.ts +150 -4
- package/dist/elements/pdf-element.js +138 -20
- package/dist/elements/rectangle-element.d.ts +52 -11
- package/dist/elements/rectangle-element.js +129 -36
- package/dist/elements/row-element.d.ts +45 -4
- package/dist/elements/row-element.js +66 -18
- package/dist/elements/text-element.d.ts +132 -11
- package/dist/elements/text-element.js +206 -56
- package/dist/forms/acroform.d.ts +63 -0
- package/dist/forms/acroform.js +427 -0
- package/dist/forms/appearance.d.ts +58 -0
- package/dist/forms/appearance.js +204 -0
- package/dist/forms/field.d.ts +164 -0
- package/dist/forms/field.js +1 -0
- package/dist/forms/pdf.d.ts +19 -0
- package/dist/forms/pdf.js +28 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -19
- package/dist/ir/display-list.d.ts +197 -8
- package/dist/ir/display-list.js +8 -2
- package/dist/layout/alignment.d.ts +10 -0
- package/dist/layout/alignment.js +6 -0
- package/dist/layout/box-constraints.d.ts +61 -0
- package/dist/layout/box-constraints.js +88 -6
- package/dist/layout/collect-images.d.ts +9 -0
- package/dist/layout/collect-images.js +34 -0
- package/dist/layout/fragmentation.d.ts +20 -1
- package/dist/layout/fragmentation.js +74 -18
- package/dist/layout/insets.d.ts +28 -0
- package/dist/layout/insets.js +20 -0
- package/dist/platform/browser-fs.d.ts +2 -0
- package/dist/platform/browser-fs.js +9 -0
- package/dist/platform/browser-image.d.ts +5 -0
- package/dist/platform/browser-image.js +13 -0
- package/dist/platform/node-fs.d.ts +2 -0
- package/dist/platform/node-fs.js +10 -0
- package/dist/platform/node-image.d.ts +5 -0
- package/dist/platform/node-image.js +9 -0
- package/dist/renderer/anchor-renderer.d.ts +6 -0
- package/dist/renderer/anchor-renderer.js +11 -0
- package/dist/renderer/bookmark-renderer.d.ts +6 -0
- package/dist/renderer/bookmark-renderer.js +11 -0
- package/dist/renderer/container-renderer.d.ts +3 -3
- package/dist/renderer/container-renderer.js +11 -28
- package/dist/renderer/default-text-style-renderer.d.ts +6 -0
- package/dist/renderer/default-text-style-renderer.js +10 -0
- package/dist/renderer/deferred-renderer.d.ts +3 -3
- package/dist/renderer/deferred-renderer.js +8 -23
- package/dist/renderer/emoji-image.d.ts +9 -0
- package/dist/renderer/emoji-image.js +68 -0
- package/dist/renderer/expanded-renderer.d.ts +3 -3
- package/dist/renderer/expanded-renderer.js +6 -21
- package/dist/renderer/form-field-renderer.d.ts +20 -0
- package/dist/renderer/form-field-renderer.js +12 -0
- package/dist/renderer/image-renderer.d.ts +4 -4
- package/dist/renderer/image-renderer.js +86 -75
- package/dist/renderer/index.d.ts +10 -10
- package/dist/renderer/index.js +10 -26
- package/dist/renderer/keep-together-renderer.d.ts +10 -0
- package/dist/renderer/keep-together-renderer.js +12 -0
- package/dist/renderer/line-renderer.d.ts +3 -3
- package/dist/renderer/line-renderer.js +13 -28
- package/dist/renderer/link-renderer.d.ts +6 -0
- package/dist/renderer/link-renderer.js +11 -0
- package/dist/renderer/padding-renderer.d.ts +3 -3
- package/dist/renderer/padding-renderer.js +6 -21
- package/dist/renderer/page-break-renderer.d.ts +14 -0
- package/dist/renderer/page-break-renderer.js +15 -0
- package/dist/renderer/page-builder-renderer.d.ts +6 -0
- package/dist/renderer/page-builder-renderer.js +11 -0
- package/dist/renderer/page-renderer.d.ts +2 -2
- package/dist/renderer/page-renderer.js +115 -77
- package/dist/renderer/pdf-backend.d.ts +31 -4
- package/dist/renderer/pdf-backend.js +287 -49
- package/dist/renderer/pdf-config.js +4 -7
- package/dist/renderer/pdf-document-class.d.ts +5 -5
- package/dist/renderer/pdf-document-class.js +27 -41
- package/dist/renderer/pdf-document-renderer.d.ts +19 -10
- package/dist/renderer/pdf-document-renderer.js +135 -91
- package/dist/renderer/pdf-renderer.d.ts +2 -2
- package/dist/renderer/pdf-renderer.js +165 -90
- package/dist/renderer/positioned-renderer.d.ts +6 -0
- package/dist/renderer/positioned-renderer.js +10 -0
- package/dist/renderer/rectangle-renderer.d.ts +3 -3
- package/dist/renderer/rectangle-renderer.js +49 -44
- package/dist/renderer/repeating-header-renderer.d.ts +3 -3
- package/dist/renderer/repeating-header-renderer.js +10 -26
- package/dist/renderer/rotated-renderer.d.ts +7 -0
- package/dist/renderer/rotated-renderer.js +29 -0
- package/dist/renderer/row-renderer.d.ts +3 -3
- package/dist/renderer/row-renderer.js +11 -28
- package/dist/renderer/struct-group-renderer.d.ts +10 -0
- package/dist/renderer/struct-group-renderer.js +24 -0
- package/dist/renderer/text-renderer.d.ts +9 -5
- package/dist/renderer/text-renderer.js +437 -89
- package/dist/text/advance.d.ts +34 -0
- package/dist/text/advance.js +45 -0
- package/dist/text/arabic.d.ts +20 -0
- package/dist/text/arabic.js +101 -0
- package/dist/text/bidi.d.ts +33 -0
- package/dist/text/bidi.js +94 -0
- package/dist/text/emoji-codepoints.d.ts +1 -0
- package/dist/text/emoji-codepoints.js +29 -0
- package/dist/text/font-fallback.d.ts +25 -0
- package/dist/text/font-fallback.js +38 -0
- package/dist/text/line-breaker.d.ts +50 -12
- package/dist/text/line-breaker.js +158 -37
- package/dist/text/line-metrics.d.ts +49 -0
- package/dist/text/line-metrics.js +57 -0
- package/dist/text/orphans-widows.d.ts +27 -0
- package/dist/text/orphans-widows.js +53 -0
- package/dist/text/shape.d.ts +29 -0
- package/dist/text/shape.js +67 -0
- package/dist/text/text-decoration.d.ts +50 -0
- package/dist/text/text-decoration.js +62 -0
- package/dist/text/text-style.d.ts +60 -0
- package/dist/text/text-style.js +79 -0
- package/dist/utils/afm-parser.d.ts +34 -0
- package/dist/utils/afm-parser.js +76 -17
- package/dist/utils/bytes.d.ts +24 -0
- package/dist/utils/bytes.js +76 -0
- package/dist/utils/dest-registry.d.ts +22 -0
- package/dist/utils/dest-registry.js +32 -0
- package/dist/utils/flex-layout.d.ts +36 -9
- package/dist/utils/flex-layout.js +207 -30
- package/dist/utils/font-metrics.d.ts +20 -1
- package/dist/utils/font-metrics.js +1 -2
- package/dist/utils/font-path.js +3 -6
- package/dist/utils/gsub.d.ts +36 -0
- package/dist/utils/gsub.js +178 -0
- package/dist/utils/image-helper.d.ts +6 -5
- package/dist/utils/image-helper.js +101 -111
- package/dist/utils/inflate.d.ts +16 -0
- package/dist/utils/inflate.js +52 -0
- package/dist/utils/md5.d.ts +4 -0
- package/dist/utils/md5.js +79 -0
- package/dist/utils/outline.d.ts +21 -0
- package/dist/utils/outline.js +79 -0
- package/dist/utils/pdf-object-manager.d.ts +110 -8
- package/dist/utils/pdf-object-manager.js +583 -141
- package/dist/utils/renderer-registry.d.ts +13 -1
- package/dist/utils/renderer-registry.js +23 -6
- package/dist/utils/struct-tree.d.ts +44 -0
- package/dist/utils/struct-tree.js +129 -0
- package/dist/utils/ttf-parser.d.ts +134 -2
- package/dist/utils/ttf-parser.js +873 -50
- package/dist/utils/ttf-subsetter.d.ts +1 -1
- package/dist/utils/ttf-subsetter.js +40 -42
- package/dist/utils/ua-xmp.d.ts +9 -0
- package/dist/utils/ua-xmp.js +32 -0
- package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
- package/dist/utils/utf8-to-windows1252-encoder.js +11 -4
- package/dist/utils/woff.d.ts +10 -0
- package/dist/utils/woff.js +116 -0
- package/dist/validators/element-validator.d.ts +2 -2
- package/dist/validators/element-validator.js +31 -26
- package/package.json +22 -2
|
@@ -1,125 +1,473 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
exports.TextRenderer = void 0;
|
|
13
|
-
const pdf_element_1 = require("../elements/pdf-element");
|
|
14
|
-
const line_breaker_1 = require("../text/line-breaker");
|
|
15
|
-
// Distance from the top of a line down to its baseline, as a fraction of the font
|
|
16
|
-
// size. ~0.683 is the standard-14 ascent ratio used to seat the first baseline.
|
|
17
|
-
const BASELINE_RATIO = 683 / 1000;
|
|
18
|
-
class TextRenderer {
|
|
1
|
+
import { Color } from "../common/color.js";
|
|
2
|
+
import { HorizontalAlignment } from "../elements/pdf-element.js";
|
|
3
|
+
import { VERTICAL_TEXT_SHIFT } from "../elements/text-element.js";
|
|
4
|
+
import { isEmojiCodePoint } from "../text/emoji-codepoints.js";
|
|
5
|
+
import { emojiImageNode } from "./emoji-image.js";
|
|
6
|
+
import { singleLineWidth, MAX_SPACE_SHRINK, wrapStringIntoLines, breakSegmentsIntoLines, } from "../text/line-breaker.js";
|
|
7
|
+
import { lineBoxForSegmentLine, lineBoxForString } from "../text/line-metrics.js";
|
|
8
|
+
import { runAdvance } from "../text/advance.js";
|
|
9
|
+
import { visualRuns, visualRunsOf } from "../text/bidi.js";
|
|
10
|
+
import { skipInkSegments, strikethroughStroke, underlineStroke, } from "../text/text-decoration.js";
|
|
11
|
+
export class TextRenderer {
|
|
19
12
|
// Measuring only needs metrics, not the full object manager. (The render pass below
|
|
20
13
|
// still receives the manager because it also registers fonts/images.)
|
|
21
|
-
static calculateTextHeight(content, fontSize, fontFamily, fontStyle, objectManager, maxWidth) {
|
|
22
|
-
// Plain string:
|
|
14
|
+
static calculateTextHeight(content, fontSize, fontFamily, fontStyle, objectManager, maxWidth, maxLines, overflow, lineHeight, letterSpacing = 0, lineOptions = {}) {
|
|
15
|
+
// Plain string: every wrapped line gets the same box.
|
|
23
16
|
if (typeof content === "string") {
|
|
24
|
-
const lines =
|
|
25
|
-
|
|
17
|
+
const lines = wrapStringIntoLines(content, fontFamily, fontSize, fontStyle, maxWidth, objectManager, maxLines, overflow, letterSpacing, lineOptions);
|
|
18
|
+
const box = lineBoxForString(objectManager, fontFamily, fontStyle, fontSize, lineHeight);
|
|
19
|
+
return lines.length * box.height;
|
|
26
20
|
}
|
|
27
|
-
// Segments: each line
|
|
28
|
-
const
|
|
29
|
-
|
|
21
|
+
// Segments: each line's box comes from the fonts actually on it.
|
|
22
|
+
const defaults = { fontFamily, fontSize, fontStyle, letterSpacing };
|
|
23
|
+
const lines = breakSegmentsIntoLines(content, defaults, maxWidth, objectManager, maxLines, overflow);
|
|
24
|
+
return lines.reduce((total, line) => total + lineBoxForSegmentLine(line, defaults, objectManager, lineHeight).height, 0);
|
|
30
25
|
}
|
|
31
|
-
static render(textElement, objectManager) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
26
|
+
static async render(textElement, objectManager) {
|
|
27
|
+
const { x, y, width, fontSize, color, content, fontFamily, fontStyle, textAlignment, maxLines, overflow, lineHeight, underline, strikethrough, skipInk, letterSpacing, direction, wordSpacing, textIndent, role, } = textElement.getProps();
|
|
28
|
+
// Component -> display list. Wrapping and positioning stay here; the backend
|
|
29
|
+
// turns each run into BT/Tf/Td/Tj/ET. The wrapping algorithm is unchanged from
|
|
30
|
+
// the original renderer - unifying it into the engine is Phase 3.
|
|
31
|
+
const { runs, links, decorations } = TextRenderer._buildRuns(content, fontSize, fontFamily, fontStyle, objectManager, width ?? Number.NaN, textAlignment, color, x, y, maxLines, overflow, lineHeight, underline, strikethrough, skipInk, letterSpacing, direction, wordSpacing, textIndent);
|
|
32
|
+
// Accessible tagging: this whole text block is one structure element (a paragraph P, or a heading
|
|
33
|
+
// from `role`); each run's text lands under it. Emoji sub-nodes produced below stay untagged (drawn
|
|
34
|
+
// as artifacts). Keyed by structId so a block split across pages stays one element.
|
|
35
|
+
if (objectManager.struct.enabled) {
|
|
36
|
+
const pdfRole = role ? role.toUpperCase() : "P"; // "h1" -> "H1"; default paragraph
|
|
37
|
+
const key = objectManager.struct.openElement(textElement.structId, pdfRole);
|
|
38
|
+
for (const run of runs)
|
|
39
|
+
run.tag = { role: pdfRole, key };
|
|
40
|
+
}
|
|
41
|
+
// Color fonts (COLR/CPAL): expand each run's emoji into filled vector layers (or fetched images
|
|
42
|
+
// for an image emoji source); plain runs and monochrome fonts pass straight through unchanged.
|
|
43
|
+
const drawn = (await Promise.all(runs.map((run) => TextRenderer._expandColorGlyphs(run, objectManager)))).flat();
|
|
44
|
+
// Underline / strikethrough go on TOP of the glyphs (a strikethrough must), and stay untagged,
|
|
45
|
+
// so the structure tree treats them as artifacts rather than as text.
|
|
46
|
+
// Inline hyperlinks (href spans) ride along as /Link annotations - they draw nothing, so order
|
|
47
|
+
// vs the drawn runs does not matter; the page renderer peels them off into /Annots.
|
|
48
|
+
return [...drawn, ...decorations, ...links];
|
|
49
|
+
}
|
|
50
|
+
// Splits a text run into normal text sub-runs + filled `Path` layers for any COLR color glyphs,
|
|
51
|
+
// walking code point by code point and advancing the pen by each glyph's width. Returns `[run]`
|
|
52
|
+
// untouched when the font has no color glyphs (the common case).
|
|
53
|
+
static async _expandColorGlyphs(run, om) {
|
|
54
|
+
// The run's own font (if it's a color font) draws emoji directly; otherwise a document-level
|
|
55
|
+
// emoji source (`Document({ emoji })`) supplies them - a fallback FONT (color glyphs, native
|
|
56
|
+
// vector) or an IMAGE source (fetched emoji images) - for code points the run's font lacks, so
|
|
57
|
+
// `Text("Hallo 😅")` works in one string. No emoji source at all -> pass the run through.
|
|
58
|
+
const own = om.getColorFont(run.fontFamily, run.fontStyle);
|
|
59
|
+
const emojiName = om.getEmojiFont();
|
|
60
|
+
const fallback = emojiName && emojiName !== run.fontFamily
|
|
61
|
+
? om.getColorFont(emojiName, run.fontStyle)
|
|
62
|
+
: undefined;
|
|
63
|
+
const imageSource = om.getEmojiImageSource();
|
|
64
|
+
if (!own && !fallback && !imageSource)
|
|
65
|
+
return [run];
|
|
66
|
+
// A SHAPED run cannot be split here: the sub-runs are cut by code point, while `glyphs` is a
|
|
67
|
+
// drawn-order list in which a ligature is one glyph for several code points - so no sub-run could
|
|
68
|
+
// be given the right slice, and inheriting the whole list would draw every glyph again per piece.
|
|
69
|
+
// Passed through whole instead: an emoji inside Arabic then comes from the text font rather than
|
|
70
|
+
// the emoji source (`todo.md`).
|
|
71
|
+
if (run.glyphs)
|
|
72
|
+
return [run];
|
|
73
|
+
const nodes = [];
|
|
74
|
+
let cursorX = run.x; // absolute x of the next glyph
|
|
75
|
+
let pending = ""; // run of consecutive non-color characters
|
|
76
|
+
let pendingX = run.x; // where that run started
|
|
77
|
+
const flushPending = () => {
|
|
78
|
+
if (pending) {
|
|
79
|
+
nodes.push({ ...run, x: pendingX, text: pending });
|
|
80
|
+
pending = "";
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
for (const ch of run.text) {
|
|
84
|
+
const cp = ch.codePointAt(0);
|
|
85
|
+
// Which color font renders this code point: the run's own font first, then the emoji fallback.
|
|
86
|
+
let source;
|
|
87
|
+
let layers = null;
|
|
88
|
+
if (own) {
|
|
89
|
+
const l = own.getColorGlyph(own.getGlyphIndex(cp));
|
|
90
|
+
if (l)
|
|
91
|
+
[source, layers] = [own, l];
|
|
92
|
+
}
|
|
93
|
+
if (!layers && fallback) {
|
|
94
|
+
const l = fallback.getColorGlyph(fallback.getGlyphIndex(cp));
|
|
95
|
+
if (l)
|
|
96
|
+
[source, layers] = [fallback, l];
|
|
97
|
+
}
|
|
98
|
+
// The advance matches measuring (getCharWidth applies the same emoji fallback), plus the run's
|
|
99
|
+
// letter-spacing so a spaced run's emoji sit where `Tc` advances them.
|
|
100
|
+
const advance = om.getCharWidth(ch, run.fontSize, undefined, run.fontFamily, run.fontStyle) +
|
|
101
|
+
(run.letterSpacing ?? 0);
|
|
102
|
+
if (source && layers) {
|
|
103
|
+
flushPending();
|
|
104
|
+
const scale = run.fontSize / source.unitsPerEm;
|
|
105
|
+
for (const layer of layers) {
|
|
106
|
+
const commands = TextRenderer._glyphToPath(source.getGlyphPath(layer.glyphId), cursorX, run.y, scale, layer.transform);
|
|
107
|
+
if (commands.length === 0)
|
|
108
|
+
continue; // an empty layer outline draws nothing
|
|
109
|
+
const paint = layer.paint;
|
|
110
|
+
let fill;
|
|
111
|
+
if (paint.type === "solid") {
|
|
112
|
+
const c = paint.color;
|
|
113
|
+
fill = c ? new Color(c.r, c.g, c.b, c.a / 255) : run.color; // null -> foreground
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
fill = TextRenderer._toGradient(paint, cursorX, run.y, scale, run.color, layer.transform);
|
|
117
|
+
}
|
|
118
|
+
nodes.push({ type: "path", commands, fill });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else if (imageSource && isEmojiCodePoint(cp)) {
|
|
122
|
+
// Image emoji source: fetch + embed a 1em square. The emoji breaks the text run either way;
|
|
123
|
+
// a failed fetch (offline/404) leaves a blank em gap (advance already applied) rather than
|
|
124
|
+
// drawing a broken glyph.
|
|
125
|
+
flushPending();
|
|
126
|
+
const node = await emojiImageNode(cp, imageSource, cursorX, run.y, run.fontSize);
|
|
127
|
+
if (node)
|
|
128
|
+
nodes.push(node);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
if (!pending)
|
|
132
|
+
pendingX = cursorX;
|
|
133
|
+
pending += ch;
|
|
134
|
+
}
|
|
135
|
+
cursorX += advance;
|
|
136
|
+
}
|
|
137
|
+
flushPending();
|
|
138
|
+
return nodes;
|
|
139
|
+
}
|
|
140
|
+
// Maps a glyph outline (font units, y-up) to absolute IR path commands at the pen origin, scaling
|
|
141
|
+
// by `scale` and flipping the axis into the engine's top-left space (the seam flips it once more
|
|
142
|
+
// to PDF space). TrueType quadratics become cubics: C1 = P0 + 2/3(C-P0), C2 = P1 + 2/3(C-P1).
|
|
143
|
+
static _glyphToPath(cmds, originX, baselineY, scale, transform) {
|
|
144
|
+
// Apply the layer's affine (font units) first, then scale to points, position at the pen origin
|
|
145
|
+
// and flip into engine space. `mapX`/`mapY` take BOTH glyph coords because an affine mixes them.
|
|
146
|
+
const [a, b, c, d, e, f] = transform ?? [1, 0, 0, 1, 0, 0];
|
|
147
|
+
const mapX = (gx, gy) => originX + (a * gx + c * gy + e) * scale;
|
|
148
|
+
const mapY = (gx, gy) => baselineY - (b * gx + d * gy + f) * scale;
|
|
149
|
+
const out = [];
|
|
150
|
+
let curX = 0;
|
|
151
|
+
let curY = 0;
|
|
152
|
+
for (const cmd of cmds) {
|
|
153
|
+
if (cmd.type === "M") {
|
|
154
|
+
curX = mapX(cmd.x, cmd.y);
|
|
155
|
+
curY = mapY(cmd.x, cmd.y);
|
|
156
|
+
out.push({ op: "m", x: curX, y: curY });
|
|
157
|
+
}
|
|
158
|
+
else if (cmd.type === "L") {
|
|
159
|
+
curX = mapX(cmd.x, cmd.y);
|
|
160
|
+
curY = mapY(cmd.x, cmd.y);
|
|
161
|
+
out.push({ op: "l", x: curX, y: curY });
|
|
162
|
+
}
|
|
163
|
+
else if (cmd.type === "Q") {
|
|
164
|
+
const ctrlX = mapX(cmd.cx, cmd.cy);
|
|
165
|
+
const ctrlY = mapY(cmd.cx, cmd.cy);
|
|
166
|
+
const endX = mapX(cmd.x, cmd.y);
|
|
167
|
+
const endY = mapY(cmd.x, cmd.y);
|
|
168
|
+
out.push({
|
|
169
|
+
op: "c",
|
|
170
|
+
x1: curX + (2 / 3) * (ctrlX - curX),
|
|
171
|
+
y1: curY + (2 / 3) * (ctrlY - curY),
|
|
172
|
+
x2: endX + (2 / 3) * (ctrlX - endX),
|
|
173
|
+
y2: endY + (2 / 3) * (ctrlY - endY),
|
|
174
|
+
x: endX,
|
|
175
|
+
y: endY,
|
|
176
|
+
});
|
|
177
|
+
curX = endX;
|
|
178
|
+
curY = endY;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
out.push({ op: "z" });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
// Maps a COLR v1 gradient paint (font-unit coordinates) into an IR `Gradient` positioned at the
|
|
187
|
+
// pen origin, using the SAME scale + baseline flip as the glyph outline so the gradient axis lines
|
|
188
|
+
// up with the shape. Radii scale uniformly (the em scale is uniform). A null stop color (the COLR
|
|
189
|
+
// "use foreground" sentinel) becomes the text color.
|
|
190
|
+
static _toGradient(paint, originX, baselineY, scale, foreground, transform) {
|
|
191
|
+
const [a, b, c, d, e, f] = transform ?? [1, 0, 0, 1, 0, 0];
|
|
192
|
+
const mapX = (gx, gy) => originX + (a * gx + c * gy + e) * scale;
|
|
193
|
+
const mapY = (gx, gy) => baselineY - (b * gx + d * gy + f) * scale;
|
|
194
|
+
// A radius has no single value under a general affine (a circle can shear into an ellipse); use
|
|
195
|
+
// the affine's geometric-mean scale sqrt(|det|) times the em scale - exact for uniform scaling.
|
|
196
|
+
const radiusScale = scale * Math.sqrt(Math.abs(a * d - b * c));
|
|
197
|
+
const stops = paint.stops.map((s) => ({
|
|
198
|
+
offset: s.offset,
|
|
199
|
+
color: s.color ? new Color(s.color.r, s.color.g, s.color.b, s.color.a / 255) : foreground,
|
|
200
|
+
}));
|
|
201
|
+
if (paint.type === "linearGradient") {
|
|
202
|
+
return {
|
|
203
|
+
type: "linear",
|
|
204
|
+
x0: mapX(paint.p0[0], paint.p0[1]),
|
|
205
|
+
y0: mapY(paint.p0[0], paint.p0[1]),
|
|
206
|
+
x1: mapX(paint.p1[0], paint.p1[1]),
|
|
207
|
+
y1: mapY(paint.p1[0], paint.p1[1]),
|
|
208
|
+
stops,
|
|
209
|
+
extend: paint.extend,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
type: "radial",
|
|
214
|
+
x0: mapX(paint.c0[0], paint.c0[1]),
|
|
215
|
+
y0: mapY(paint.c0[0], paint.c0[1]),
|
|
216
|
+
r0: paint.c0[2] * radiusScale,
|
|
217
|
+
x1: mapX(paint.c1[0], paint.c1[1]),
|
|
218
|
+
y1: mapY(paint.c1[0], paint.c1[1]),
|
|
219
|
+
r1: paint.c1[2] * radiusScale,
|
|
220
|
+
stops,
|
|
221
|
+
extend: paint.extend,
|
|
222
|
+
};
|
|
39
223
|
}
|
|
40
224
|
// Lay the content out into absolutely-positioned text runs. Glyph positions match
|
|
41
225
|
// the previous hand-written operators exactly (verified by pixel-identical render);
|
|
42
226
|
// only the output form changed from PDF strings to `TextRun`s.
|
|
43
|
-
static _buildRuns(content, fontSize, fontFamily, fontStyle, objectManager, maxWidth, textAlignment, color, initialX, yPosition) {
|
|
227
|
+
static _buildRuns(content, fontSize, fontFamily, fontStyle, objectManager, maxWidth, textAlignment, color, initialX, yPosition, maxLines, overflow, lineHeight, underline = false, strikethrough = false, skipInk = false, letterSpacing = 0, direction = "ltr", wordSpacing = 0, textIndent = 0) {
|
|
44
228
|
const runs = [];
|
|
229
|
+
// /Link annotation rects for any `href` spans, collected as we place segments. Empty for the
|
|
230
|
+
// common (no-href) case, so plain text keeps producing exactly the runs it did before.
|
|
231
|
+
const links = [];
|
|
232
|
+
// Underline / strikethrough strokes, one per decorated run (so a wrapped paragraph gets one
|
|
233
|
+
// per line, and a decorated span only spans its own glyphs).
|
|
234
|
+
const decorations = [];
|
|
45
235
|
// Horizontal offset of a line of the given width under the current alignment.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
236
|
+
// `start` is the one alignment that depends on the direction, resolved at the single place
|
|
237
|
+
// alignment is consumed.
|
|
238
|
+
const align = textAlignment === HorizontalAlignment.start
|
|
239
|
+
? direction === "rtl"
|
|
240
|
+
? HorizontalAlignment.right
|
|
241
|
+
: HorizontalAlignment.left
|
|
242
|
+
: textAlignment;
|
|
243
|
+
const alignmentOffset = (lineWidth, indent = 0) => {
|
|
244
|
+
const room = maxWidth - indent;
|
|
245
|
+
if (align === HorizontalAlignment.center)
|
|
246
|
+
return (room - lineWidth) / 2;
|
|
247
|
+
if (align === HorizontalAlignment.right)
|
|
248
|
+
return room - lineWidth;
|
|
51
249
|
return 0;
|
|
52
250
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
251
|
+
/**
|
|
252
|
+
* Justification: how much extra to put in EACH space of a line so it reaches the full width.
|
|
253
|
+
* The LAST line of a paragraph keeps its natural spacing, as it does in CSS and in print - a
|
|
254
|
+
* justified last line is the classic sign of a broken text engine.
|
|
255
|
+
*
|
|
256
|
+
* Spaces are stretched by moving the pen, not by the `Tw` operator: `Tw` applies only to the
|
|
257
|
+
* single byte 32, so an embedded (Identity-H) font would ignore it entirely.
|
|
258
|
+
*/
|
|
259
|
+
const justifyExtra = (line, naturalWidth, isLast, indent = 0) => {
|
|
260
|
+
if (align !== HorizontalAlignment.justify)
|
|
261
|
+
return 0;
|
|
262
|
+
const gaps = [...line].filter((c) => c === " ").length;
|
|
263
|
+
if (gaps === 0)
|
|
264
|
+
return 0;
|
|
265
|
+
const slack = maxWidth - indent - naturalWidth;
|
|
266
|
+
// Negative slack is real now: the breaker keeps a word when the line can be SQUEEZED to hold
|
|
267
|
+
// it, so justification has to deliver that squeeze. The floor is the same limit the breaker
|
|
268
|
+
// used, which is what stops a line it never promised from being crushed.
|
|
269
|
+
const spaceWidth = runAdvance(objectManager, " ", { fontFamily, fontSize, fontStyle }, letterSpacing);
|
|
270
|
+
const extra = Math.max(slack / gaps, -spaceWidth * MAX_SPACE_SHRINK);
|
|
271
|
+
// The last line is never STRETCHED - the rule print and CSS follow. It may still be SQUEEZED:
|
|
272
|
+
// the breaker was allowed to pack it that way, and drawing it loose would push it out of its box.
|
|
273
|
+
return isLast ? Math.min(0, extra) : extra;
|
|
274
|
+
};
|
|
275
|
+
/**
|
|
276
|
+
* Push one drawn piece and return the advance it took. When the line is justified the pen jumps an
|
|
277
|
+
* extra `extra` at every space, which means one run per word - the only way that works for an
|
|
278
|
+
* embedded font, whose spaces `Tw` cannot reach.
|
|
279
|
+
*
|
|
280
|
+
* A SHAPED piece is never split: its glyph list is in drawn order with ligatures merged, so no
|
|
281
|
+
* slice of it is correct. Such a piece keeps its natural spacing.
|
|
282
|
+
*/
|
|
283
|
+
const emitPiece = (piece, x, extra, style, runFont, spacing) => {
|
|
284
|
+
const push = (at, text, glyphs) => {
|
|
285
|
+
runs.push({ type: "text", x: at, text, ...(glyphs ? { glyphs } : {}), ...style });
|
|
286
|
+
};
|
|
287
|
+
if (extra === 0 || piece.glyphs || !piece.text.includes(" ")) {
|
|
288
|
+
push(x, piece.text, piece.glyphs);
|
|
289
|
+
return runAdvance(objectManager, piece.text, runFont, spacing);
|
|
290
|
+
}
|
|
291
|
+
const spaceWidth = runAdvance(objectManager, " ", runFont, spacing);
|
|
292
|
+
const words = piece.text.split(" ");
|
|
293
|
+
let pen = x;
|
|
294
|
+
words.forEach((word, i) => {
|
|
295
|
+
if (word !== "") {
|
|
296
|
+
push(pen, word);
|
|
297
|
+
pen += runAdvance(objectManager, word, runFont, spacing);
|
|
298
|
+
}
|
|
299
|
+
if (i < words.length - 1)
|
|
300
|
+
pen += spaceWidth + extra;
|
|
301
|
+
});
|
|
302
|
+
return pen - x;
|
|
303
|
+
};
|
|
304
|
+
// The decoration strokes for one drawn run, at the geometry ITS OWN font declares. A mixed-size
|
|
305
|
+
// line therefore gets a thicker line under the bigger span, which is what a browser does too.
|
|
306
|
+
const decorate = (text, runX, runWidth, baselineY, family, style, size, runColor, wantsUnderline, wantsStrikethrough, runLetterSpacing) => {
|
|
307
|
+
if ((!wantsUnderline && !wantsStrikethrough) || runWidth <= 0)
|
|
308
|
+
return;
|
|
309
|
+
const metrics = objectManager.getFontDecoration(family, style);
|
|
310
|
+
const push = (from, to, stroke) => {
|
|
311
|
+
decorations.push({
|
|
312
|
+
type: "line",
|
|
313
|
+
x1: runX + from,
|
|
314
|
+
y1: stroke.y,
|
|
315
|
+
x2: runX + to,
|
|
316
|
+
y2: stroke.y,
|
|
317
|
+
stroke: runColor,
|
|
318
|
+
strokeWidth: stroke.thickness,
|
|
319
|
+
});
|
|
320
|
+
};
|
|
321
|
+
if (wantsUnderline) {
|
|
322
|
+
const stroke = underlineStroke(metrics, size, baselineY);
|
|
323
|
+
for (const [from, to] of underlineSegments(text, runWidth, baselineY, size, family, style, stroke, runLetterSpacing)) {
|
|
324
|
+
push(from, to, stroke);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// A strikethrough crosses the letters by definition, so it is never interrupted.
|
|
328
|
+
if (wantsStrikethrough) {
|
|
329
|
+
const stroke = strikethroughStroke(metrics, size, baselineY);
|
|
330
|
+
push(0, runWidth, stroke);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
// The x-ranges the underline actually draws. Without `skipInk` that is the whole run; with it,
|
|
334
|
+
// the descenders are cut out of it (which needs the real outlines, i.e. an embedded font).
|
|
335
|
+
const underlineSegments = (text, runWidth, baselineY, size, family, style, stroke, runLetterSpacing) => {
|
|
336
|
+
if (!skipInk)
|
|
337
|
+
return [[0, runWidth]];
|
|
338
|
+
if (!objectManager.isCustomFont(family, style)) {
|
|
339
|
+
throw new Error(`skipInk needs an embedded font, but "${family}" is a standard-14 font whose glyph outlines ` +
|
|
340
|
+
`live in the PDF viewer, not in the document - we cannot see where its ink is. Register a ` +
|
|
341
|
+
`font (the document's \`fonts\` option) or drop \`skipInk\`.`);
|
|
60
342
|
}
|
|
61
|
-
|
|
343
|
+
// The band the stroke covers, in points from the baseline, y UP (so both values are negative).
|
|
344
|
+
const below = stroke.y - baselineY;
|
|
345
|
+
const half = stroke.thickness / 2;
|
|
346
|
+
const inkSpans = objectManager.getInkSpansInBand(text, size, family, style, -below + half, -below - half, runLetterSpacing);
|
|
347
|
+
return skipInkSegments(runWidth, inkSpans, stroke.thickness);
|
|
348
|
+
};
|
|
349
|
+
/**
|
|
350
|
+
* One drawn piece of a line. SHAPING must see the run's LOGICAL text - a letter's form comes from
|
|
351
|
+
* its neighbours, and a reversed run has those swapped - so we shape that and then reverse the
|
|
352
|
+
* GLYPHS for an rtl run. A font that does not shape keeps the old path exactly: draw the visual
|
|
353
|
+
* text, no glyph list, byte-identical output.
|
|
354
|
+
*/
|
|
355
|
+
const shapedPiece = (part, family, style) => {
|
|
356
|
+
const shaped = objectManager.shapeText(part.logical, family, style);
|
|
357
|
+
if (!shaped)
|
|
358
|
+
return { text: part.text };
|
|
359
|
+
const ordered = part.rtl ? [...shaped].reverse() : shaped;
|
|
360
|
+
return { text: part.logical, glyphs: ordered.map((g) => g.glyph) };
|
|
62
361
|
};
|
|
362
|
+
// The advance the viewer will use: plain glyph widths (a `Tj` never kerns) plus one
|
|
363
|
+
// `letterSpacing` per code point (the `Tc` operator), from the one shared `advance.ts` primitive.
|
|
364
|
+
const font = { fontFamily, fontSize, fontStyle };
|
|
63
365
|
// --- Plain string: one run per wrapped line. ---
|
|
64
366
|
if (typeof content === "string") {
|
|
65
|
-
const lines =
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
367
|
+
const lines = wrapStringIntoLines(content, fontFamily, fontSize, fontStyle, maxWidth, objectManager, maxLines, overflow, letterSpacing,
|
|
368
|
+
// The SAME knobs the measure pass used (`TextElement.lineOptions`), or a line would be
|
|
369
|
+
// measured one width and drawn another.
|
|
370
|
+
{
|
|
371
|
+
wordSpacing,
|
|
372
|
+
indent: textIndent,
|
|
373
|
+
shrink: align === HorizontalAlignment.justify ? MAX_SPACE_SHRINK : 0,
|
|
374
|
+
});
|
|
375
|
+
// yPosition is the top of the text box (top-left). The line box seats its own baseline; lines
|
|
376
|
+
// then stack by that box's height. Same numbers as calculateTextHeight, by construction.
|
|
377
|
+
const box = lineBoxForString(objectManager, fontFamily, fontStyle, fontSize, lineHeight);
|
|
69
378
|
lines.forEach((line, index) => {
|
|
70
|
-
const lineWidth =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
379
|
+
const lineWidth = singleLineWidth(line, font, objectManager, letterSpacing, wordSpacing);
|
|
380
|
+
// CSS `text-indent`: the FIRST line starts in, and had that much less room to fill.
|
|
381
|
+
const indent = index === 0 ? textIndent : 0;
|
|
382
|
+
let x = initialX + indent + alignmentOffset(lineWidth, indent);
|
|
383
|
+
const baseline = yPosition + box.baseline + box.height * index;
|
|
384
|
+
// Latin-only text in an `ltr` paragraph comes back as one unchanged piece.
|
|
385
|
+
// Word-spacing is a per-gap advance just like the justification slack, so it rides the same
|
|
386
|
+
// mechanism: `Tw` would reach only the single byte 32 and miss every embedded font.
|
|
387
|
+
const extra = wordSpacing + justifyExtra(line, lineWidth, index === lines.length - 1, indent);
|
|
388
|
+
for (const part of visualRuns(line, direction)) {
|
|
389
|
+
const piece = shapedPiece(part, fontFamily, fontStyle);
|
|
390
|
+
const partWidth = emitPiece(piece, x, extra, {
|
|
391
|
+
y: baseline,
|
|
392
|
+
fontFamily,
|
|
393
|
+
fontStyle,
|
|
394
|
+
fontSize,
|
|
395
|
+
color,
|
|
396
|
+
...(letterSpacing ? { letterSpacing } : {}),
|
|
397
|
+
}, font, letterSpacing);
|
|
398
|
+
decorate(piece.text, x, partWidth, baseline, fontFamily, fontStyle, fontSize, color, underline, strikethrough, letterSpacing);
|
|
399
|
+
x += partWidth;
|
|
400
|
+
}
|
|
81
401
|
});
|
|
82
|
-
return runs;
|
|
402
|
+
return { runs, links, decorations };
|
|
83
403
|
}
|
|
84
404
|
// --- Segments: break into lines (shared breaker), then emit one run per segment.
|
|
85
405
|
// Segment 0 starts at the aligned line origin; each following segment is offset by
|
|
86
406
|
// the previous segment's kerning-free advance. Each line drops by its OWN leading
|
|
87
407
|
// (tallest font on that line), so mixed-font lines space correctly and the drawn
|
|
88
408
|
// height matches the measured height.
|
|
89
|
-
const pushLine = (line, lineY) => {
|
|
409
|
+
const pushLine = (line, lineY, isLast) => {
|
|
90
410
|
let x = initialX + alignmentOffset(line.width);
|
|
91
|
-
line.segments.
|
|
411
|
+
const extra = justifyExtra(line.segments.map((seg) => seg.content).join(""), line.width, isLast);
|
|
412
|
+
// Across span boundaries, so a Hebrew span is ordered against its neighbours rather than inside
|
|
413
|
+
// itself. Each run keeps the segment it came from, and with it that span's style.
|
|
414
|
+
const ordered = visualRunsOf(line.segments.map((segment) => ({ text: segment.content, source: segment })), direction);
|
|
415
|
+
ordered.forEach((part) => {
|
|
416
|
+
const segment = part.source;
|
|
92
417
|
const family = segment.fontFamily || fontFamily;
|
|
93
418
|
const size = segment.fontSize || fontSize;
|
|
94
419
|
const style = segment.fontStyle || fontStyle;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
420
|
+
const segLetterSpacing = segment.letterSpacing ?? letterSpacing;
|
|
421
|
+
const piece = shapedPiece(part, family, style);
|
|
422
|
+
const runText = piece.text;
|
|
423
|
+
const runColor = segment.fontColor || color;
|
|
424
|
+
// `vertical-align`: this run alone leaves the line's baseline. Its own decoration and link box
|
|
425
|
+
// ride along, since both are placed from the same y.
|
|
426
|
+
const shift = VERTICAL_TEXT_SHIFT[segment.verticalAlign ?? "baseline"] * size;
|
|
427
|
+
const runY = lineY - shift;
|
|
428
|
+
const drawn = emitPiece(piece, x, extra, {
|
|
429
|
+
y: runY,
|
|
100
430
|
fontFamily: family,
|
|
101
431
|
fontStyle: style,
|
|
102
432
|
fontSize: size,
|
|
103
|
-
color:
|
|
104
|
-
|
|
105
|
-
|
|
433
|
+
color: runColor,
|
|
434
|
+
...(segLetterSpacing ? { letterSpacing: segLetterSpacing } : {}),
|
|
435
|
+
}, { fontFamily: family, fontSize: size, fontStyle: style }, segLetterSpacing);
|
|
436
|
+
decorate(runText, x, drawn, runY, family, style, size, runColor, segment.underline ?? underline, segment.strikethrough ?? strikethrough, segLetterSpacing);
|
|
437
|
+
// An href/to span becomes a /Link annotation over exactly this run's glyph box: from its own
|
|
438
|
+
// ascent above the baseline down to its own descender. A span wrapped across lines yields
|
|
439
|
+
// one rect per line.
|
|
440
|
+
if (segment.href !== undefined || segment.dest !== undefined) {
|
|
441
|
+
const v = objectManager.getFontVerticals(family, style);
|
|
442
|
+
links.push({
|
|
443
|
+
type: "link",
|
|
444
|
+
x,
|
|
445
|
+
y: runY - v.ascent * size,
|
|
446
|
+
width: drawn,
|
|
447
|
+
height: (v.ascent + v.descent) * size,
|
|
448
|
+
href: segment.href,
|
|
449
|
+
dest: segment.dest,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
x += drawn;
|
|
106
453
|
});
|
|
107
454
|
};
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
// the whole thing to PDF space.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
455
|
+
// yPosition is the top of the text box (top-left). Each line seats its own baseline inside its
|
|
456
|
+
// own box (tallest ascent / deepest descent ON THAT LINE), then the next line starts below it.
|
|
457
|
+
// The seam flips the whole thing to PDF space. `letterSpacing` MUST be in the defaults so a span
|
|
458
|
+
// that does not override it wraps and aligns with the element's spacing - the same defaults the
|
|
459
|
+
// measure path (calculateTextHeight) uses, or segmented spaced text mis-wraps (measured != drawn).
|
|
460
|
+
const defaults = { fontFamily, fontSize, fontStyle, letterSpacing };
|
|
461
|
+
let top = yPosition;
|
|
462
|
+
// Materialised, because justification has to know which line is the LAST one of the paragraph.
|
|
463
|
+
const segmentLines = [
|
|
464
|
+
...breakSegmentsIntoLines(content, defaults, maxWidth, objectManager, maxLines, overflow),
|
|
465
|
+
];
|
|
466
|
+
segmentLines.forEach((line, index) => {
|
|
467
|
+
const box = lineBoxForSegmentLine(line, defaults, objectManager, lineHeight);
|
|
468
|
+
pushLine(line, top + box.baseline, index === segmentLines.length - 1);
|
|
469
|
+
top += box.height;
|
|
470
|
+
});
|
|
471
|
+
return { runs, links, decorations };
|
|
123
472
|
}
|
|
124
473
|
}
|
|
125
|
-
exports.TextRenderer = TextRenderer;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How wide a run of text is: the horizontal counterpart to `line-metrics.ts` (how tall a line is)
|
|
3
|
+
* and `line-breaker.ts` (where a line breaks). The ONE canonical answer to "how far does the pen
|
|
4
|
+
* move", so measuring and drawing can never disagree about it.
|
|
5
|
+
*
|
|
6
|
+
* An advance has three parts, each with its own PDF mechanism:
|
|
7
|
+
*
|
|
8
|
+
* advance = sum(glyph widths) + sum(kerning) + n * letterSpacing
|
|
9
|
+
* /Widths TJ array Tc
|
|
10
|
+
*
|
|
11
|
+
* `letterSpacing` is added after EVERY code point, the last one included - exactly what the `Tc`
|
|
12
|
+
* operator does, and exactly what CSS `letter-spacing` does (which is why right-aligned spaced text
|
|
13
|
+
* looks a hair offset: a spacing hangs off the final glyph). Measuring `(n - 1)` spacings while `Tc`
|
|
14
|
+
* applies `n` would make every line draw one spacing wider than it was wrapped - text out of its box.
|
|
15
|
+
*
|
|
16
|
+
* Kerning is added ONLY when the document has it on (`metrics.kerningEnabled`), and then the backend
|
|
17
|
+
* emits a `TJ` with the same per-pair adjustments - so a measured advance always equals the drawn
|
|
18
|
+
* one. With kerning off, `getStringWidth` is the whole advance (its plain glyph sum), byte-identical
|
|
19
|
+
* to before. Folding kerning into the measurement while the output ignored it was the 2026-07-10 bug.
|
|
20
|
+
*/
|
|
21
|
+
import type { FontStyle } from "../utils/pdf-object-manager.js";
|
|
22
|
+
import type { FontMetrics } from "../utils/font-metrics.js";
|
|
23
|
+
/** The font a run is set in. */
|
|
24
|
+
export interface RunFont {
|
|
25
|
+
fontFamily: string;
|
|
26
|
+
fontSize: number;
|
|
27
|
+
fontStyle: FontStyle;
|
|
28
|
+
}
|
|
29
|
+
/** Number of Unicode code points (not UTF-16 units): an astral char is ONE, so it takes one
|
|
30
|
+
* letter-spacing, not two. Matches how `getStringWidth` iterates. */
|
|
31
|
+
export declare function codePointCount(text: string): number;
|
|
32
|
+
/** The advance of `text` in points: the plain glyph widths, plus kerning (if the document has it on),
|
|
33
|
+
* plus one `letterSpacing` per code point. */
|
|
34
|
+
export declare function runAdvance(metrics: FontMetrics, text: string, font: RunFont, letterSpacing?: number): number;
|