@jasy/pdf 1.0.0-alpha.6 → 1.0.0-alpha.8
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/README.md +7 -7
- package/dist/api/descriptor.js +26 -1
- package/dist/api/forms.d.ts +347 -0
- package/dist/api/forms.js +189 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/layout.d.ts +47 -14
- package/dist/api/layout.js +47 -11
- package/dist/api/structure.d.ts +11 -0
- package/dist/api/structure.js +2 -0
- package/dist/api/text.d.ts +18 -2
- package/dist/api/text.js +15 -0
- package/dist/crypto/legacy-handler.d.ts +43 -0
- package/dist/crypto/legacy-handler.js +145 -0
- package/dist/crypto/security-handler.d.ts +16 -0
- package/dist/crypto/security-handler.js +26 -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 +135 -0
- package/dist/edit/document.js +649 -0
- package/dist/edit/fill.d.ts +48 -0
- package/dist/edit/fill.js +395 -0
- package/dist/edit/flatten.d.ts +42 -0
- package/dist/edit/flatten.js +437 -0
- package/dist/edit/index.d.ts +18 -0
- package/dist/edit/index.js +16 -0
- package/dist/edit/lexer.d.ts +30 -0
- package/dist/edit/lexer.js +285 -0
- package/dist/edit/objects.d.ts +63 -0
- package/dist/edit/objects.js +56 -0
- package/dist/edit/writer.d.ts +53 -0
- package/dist/edit/writer.js +261 -0
- package/dist/elements/container-element.d.ts +10 -1
- package/dist/elements/container-element.js +26 -8
- 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/index.d.ts +8 -0
- package/dist/elements/index.js +8 -0
- 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/padding-element.d.ts +3 -0
- package/dist/elements/layout/padding-element.js +12 -0
- 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/positioned-element.d.ts +10 -7
- package/dist/elements/layout/positioned-element.js +21 -9
- package/dist/elements/page-element.d.ts +11 -1
- package/dist/elements/page-element.js +41 -16
- package/dist/elements/pdf-element.d.ts +28 -0
- package/dist/elements/pdf-element.js +32 -0
- package/dist/elements/rectangle-element.d.ts +10 -1
- package/dist/elements/rectangle-element.js +27 -6
- package/dist/elements/row-element.d.ts +9 -1
- package/dist/elements/row-element.js +9 -1
- package/dist/elements/text-element.d.ts +30 -4
- package/dist/elements/text-element.js +40 -17
- 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/ir/display-list.d.ts +18 -1
- package/dist/layout/fragmentation.d.ts +12 -0
- package/dist/layout/fragmentation.js +45 -8
- package/dist/renderer/form-field-renderer.d.ts +20 -0
- package/dist/renderer/form-field-renderer.js +12 -0
- package/dist/renderer/keep-together-renderer.d.ts +10 -0
- package/dist/renderer/keep-together-renderer.js +12 -0
- package/dist/renderer/page-break-renderer.d.ts +14 -0
- package/dist/renderer/page-break-renderer.js +15 -0
- package/dist/renderer/page-renderer.js +8 -2
- package/dist/renderer/pdf-backend.d.ts +7 -0
- package/dist/renderer/pdf-backend.js +48 -6
- package/dist/renderer/pdf-document-class.js +3 -0
- package/dist/renderer/pdf-document-renderer.js +26 -7
- package/dist/renderer/pdf-renderer.js +25 -1
- package/dist/renderer/text-renderer.d.ts +1 -1
- package/dist/renderer/text-renderer.js +107 -59
- package/dist/text/advance.d.ts +34 -0
- package/dist/text/advance.js +42 -0
- package/dist/text/line-breaker.d.ts +9 -8
- package/dist/text/line-breaker.js +28 -21
- package/dist/text/line-metrics.d.ts +49 -0
- package/dist/text/line-metrics.js +57 -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 +13 -3
- package/dist/text/text-style.js +10 -3
- package/dist/utils/afm-parser.d.ts +34 -0
- package/dist/utils/afm-parser.js +67 -0
- package/dist/utils/dest-registry.d.ts +5 -2
- package/dist/utils/dest-registry.js +5 -5
- package/dist/utils/font-metrics.d.ts +14 -0
- package/dist/utils/outline.js +1 -2
- package/dist/utils/pdf-object-manager.d.ts +45 -1
- package/dist/utils/pdf-object-manager.js +193 -26
- package/dist/utils/struct-tree.js +2 -3
- package/dist/utils/ttf-parser.d.ts +53 -0
- package/dist/utils/ttf-parser.js +368 -0
- package/dist/validators/element-validator.js +7 -3
- package/package.json +9 -2
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
/** Number of Unicode code points (not UTF-16 units): an astral char is ONE, so it takes one
|
|
22
|
+
* letter-spacing, not two. Matches how `getStringWidth` iterates. */
|
|
23
|
+
export function codePointCount(text) {
|
|
24
|
+
let n = 0;
|
|
25
|
+
for (const _ of text)
|
|
26
|
+
n++;
|
|
27
|
+
return n;
|
|
28
|
+
}
|
|
29
|
+
/** The advance of `text` in points: the plain glyph widths, plus kerning (if the document has it on),
|
|
30
|
+
* plus one `letterSpacing` per code point. */
|
|
31
|
+
export function runAdvance(metrics, text, font, letterSpacing = 0) {
|
|
32
|
+
let advance = metrics.getStringWidth(text, font.fontFamily, font.fontSize, font.fontStyle);
|
|
33
|
+
if (letterSpacing !== 0)
|
|
34
|
+
advance += codePointCount(text) * letterSpacing;
|
|
35
|
+
if (metrics.kerningEnabled) {
|
|
36
|
+
let units = 0;
|
|
37
|
+
for (const k of metrics.getKernPairs(text, font.fontFamily, font.fontStyle))
|
|
38
|
+
units += k;
|
|
39
|
+
advance += (units / 1000) * font.fontSize; // kern units are em/1000
|
|
40
|
+
}
|
|
41
|
+
return advance;
|
|
42
|
+
}
|
|
@@ -6,16 +6,17 @@ export interface SegmentDefaults {
|
|
|
6
6
|
fontFamily: string;
|
|
7
7
|
fontSize: number;
|
|
8
8
|
fontStyle: FontStyle;
|
|
9
|
+
/** Extra space after every glyph, in points; a segment may override it. Default 0. */
|
|
10
|
+
letterSpacing?: number;
|
|
9
11
|
}
|
|
10
12
|
/** What happens to text beyond `maxLines`: `"clip"` drops it, `"ellipsis"` ends the last kept line
|
|
11
13
|
* with an ellipsis. Mirrors Flutter's `TextOverflow`. */
|
|
12
14
|
export type TextOverflow = "clip" | "ellipsis";
|
|
13
|
-
/** One laid-out line of segments.
|
|
14
|
-
*
|
|
15
|
+
/** One laid-out line of segments. How TALL it is does not live here: the breaker owns the horizontal
|
|
16
|
+
* half, `text/line-metrics.ts` owns the vertical one and derives the box from the fonts on the line. */
|
|
15
17
|
export interface SegmentLine {
|
|
16
18
|
segments: TextSegment[];
|
|
17
19
|
width: number;
|
|
18
|
-
maxFontSize: number;
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
21
22
|
* Break a plain string into lines that each fit within `maxWidth`, splitting on
|
|
@@ -25,12 +26,12 @@ export interface SegmentLine {
|
|
|
25
26
|
* rendering call this, so they can never disagree. Depends only on `FontMetrics`,
|
|
26
27
|
* not the PDF byte writer - the future fragmentation pass can reuse it.
|
|
27
28
|
*/
|
|
28
|
-
export declare function wrapStringIntoLines(text: string, fontFamily: string, fontSize: number, fontStyle: FontStyle, maxWidth: number, metrics: FontMetrics, maxLines?: number, overflow?: TextOverflow): string[];
|
|
29
|
+
export declare function wrapStringIntoLines(text: string, fontFamily: string, fontSize: number, fontStyle: FontStyle, maxWidth: number, metrics: FontMetrics, maxLines?: number, overflow?: TextOverflow, letterSpacing?: number): string[];
|
|
29
30
|
/**
|
|
30
|
-
* Break styled segments into lines that fit within `maxWidth`. Same greedy
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* Break styled segments into lines that fit within `maxWidth`. Same greedy word-splitting as the
|
|
32
|
+
* string breaker; each line keeps the segments that landed on it, which is what `line-metrics.ts`
|
|
33
|
+
* later derives the line's height from. Single source of truth: both height measurement and
|
|
34
|
+
* rendering call this.
|
|
34
35
|
*/
|
|
35
36
|
export declare function breakSegmentsIntoLines(segments: TextSegment[], defaults: SegmentDefaults, maxWidth: number, metrics: FontMetrics, maxLines?: number, overflow?: TextOverflow): SegmentLine[];
|
|
36
37
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { runAdvance } from "./advance.js";
|
|
1
2
|
/** Three ASCII dots, NOT the "…" glyph (U+2026): plain dots encode in every font - standard-14
|
|
2
3
|
* (WinAnsi) and any embedded TTF - whereas U+2026 needs a glyph the font may not carry. */
|
|
3
4
|
const ELLIPSIS = "...";
|
|
@@ -9,14 +10,17 @@ const ELLIPSIS = "...";
|
|
|
9
10
|
* rendering call this, so they can never disagree. Depends only on `FontMetrics`,
|
|
10
11
|
* not the PDF byte writer - the future fragmentation pass can reuse it.
|
|
11
12
|
*/
|
|
12
|
-
export function wrapStringIntoLines(text, fontFamily, fontSize, fontStyle, maxWidth, metrics, maxLines, overflow) {
|
|
13
|
+
export function wrapStringIntoLines(text, fontFamily, fontSize, fontStyle, maxWidth, metrics, maxLines, overflow, letterSpacing = 0) {
|
|
13
14
|
let currentLine = "";
|
|
14
15
|
let currentWidth = 0;
|
|
15
16
|
const lines = [];
|
|
17
|
+
const font = { fontFamily, fontSize, fontStyle };
|
|
16
18
|
const words = text.split(" ");
|
|
17
19
|
words.forEach((word, index) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
// Word and space advances come from the one shared primitive (`advance.ts`), the same one
|
|
21
|
+
// `naturalWidth` uses - so a bounded and an unbounded layout of the same text agree bit for bit.
|
|
22
|
+
const wordWidth = runAdvance(metrics, word, font, letterSpacing);
|
|
23
|
+
const spaceWidth = runAdvance(metrics, " ", font, letterSpacing);
|
|
20
24
|
// Break before a word that won't fit - but only once the line has content. A single word wider
|
|
21
25
|
// than maxWidth must sit on its (empty) line and overflow, not push a phantom empty line before
|
|
22
26
|
// it (which would over-count the height by a line and shift the text down at render).
|
|
@@ -38,44 +42,41 @@ export function wrapStringIntoLines(text, fontFamily, fontSize, fontStyle, maxWi
|
|
|
38
42
|
const kept = lines.slice(0, maxLines);
|
|
39
43
|
if (overflow === "ellipsis") {
|
|
40
44
|
const last = kept.length - 1;
|
|
41
|
-
kept[last] = ellipsize(kept[last], fontFamily, fontSize, fontStyle, maxWidth, metrics);
|
|
45
|
+
kept[last] = ellipsize(kept[last], fontFamily, fontSize, fontStyle, maxWidth, metrics, letterSpacing);
|
|
42
46
|
}
|
|
43
47
|
return kept;
|
|
44
48
|
}
|
|
45
49
|
/**
|
|
46
|
-
* Break styled segments into lines that fit within `maxWidth`. Same greedy
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
+
* Break styled segments into lines that fit within `maxWidth`. Same greedy word-splitting as the
|
|
51
|
+
* string breaker; each line keeps the segments that landed on it, which is what `line-metrics.ts`
|
|
52
|
+
* later derives the line's height from. Single source of truth: both height measurement and
|
|
53
|
+
* rendering call this.
|
|
50
54
|
*/
|
|
51
55
|
export function breakSegmentsIntoLines(segments, defaults, maxWidth, metrics, maxLines, overflow) {
|
|
52
56
|
const lines = [];
|
|
53
57
|
let width = 0;
|
|
54
|
-
let maxFontSize = 0; // per line: starts at 0, grows to the tallest font on the line
|
|
55
58
|
let lineSegments = [];
|
|
56
59
|
let combined = "";
|
|
57
60
|
segments.forEach((segment) => {
|
|
58
61
|
const family = segment.fontFamily || defaults.fontFamily;
|
|
59
62
|
const size = segment.fontSize || defaults.fontSize;
|
|
60
63
|
const style = segment.fontStyle || defaults.fontStyle;
|
|
61
|
-
const
|
|
64
|
+
const letterSpacing = segment.letterSpacing ?? defaults.letterSpacing ?? 0;
|
|
65
|
+
const font = { fontFamily: family, fontSize: size, fontStyle: style };
|
|
66
|
+
const spaceWidth = runAdvance(metrics, " ", font, letterSpacing);
|
|
62
67
|
const words = segment.content.split(" ");
|
|
63
68
|
// Start this segment's piece empty; its content is filled word-by-word below. (Not
|
|
64
69
|
// the original content - otherwise a segment whose FIRST word overflows would carry
|
|
65
70
|
// its whole text into the line that just closed.)
|
|
66
71
|
lineSegments.push({ ...segment, fontFamily: family, content: "" });
|
|
67
72
|
combined = "";
|
|
68
|
-
if (maxFontSize < size)
|
|
69
|
-
maxFontSize = size;
|
|
70
73
|
words.forEach((word, wordIndex) => {
|
|
71
|
-
const wordWidth = metrics
|
|
74
|
+
const wordWidth = runAdvance(metrics, word, font, letterSpacing);
|
|
72
75
|
// Same guard as the string path: don't open a phantom empty line for an over-wide first word -
|
|
73
76
|
// place it (overflowing) on the current empty line instead.
|
|
74
77
|
if (width + wordWidth > maxWidth && width > 0) {
|
|
75
|
-
lines.push({ segments: lineSegments, width
|
|
76
|
-
// Start the next line; its leading resets to the wrapping segment's size.
|
|
78
|
+
lines.push({ segments: lineSegments, width });
|
|
77
79
|
width = 0;
|
|
78
|
-
maxFontSize = size;
|
|
79
80
|
lineSegments = [];
|
|
80
81
|
combined = word;
|
|
81
82
|
width += wordWidth + spaceWidth;
|
|
@@ -92,7 +93,7 @@ export function breakSegmentsIntoLines(segments, defaults, maxWidth, metrics, ma
|
|
|
92
93
|
});
|
|
93
94
|
});
|
|
94
95
|
if (lineSegments.length > 0) {
|
|
95
|
-
lines.push({ segments: lineSegments, width
|
|
96
|
+
lines.push({ segments: lineSegments, width });
|
|
96
97
|
}
|
|
97
98
|
// Open-end by default; cap only when maxLines is set.
|
|
98
99
|
if (maxLines == null || lines.length <= maxLines)
|
|
@@ -123,8 +124,9 @@ export function segmentLinesToSegments(lines) {
|
|
|
123
124
|
}
|
|
124
125
|
/** Appends "…" to a single line, dropping trailing words (then characters) until the line plus the
|
|
125
126
|
* ellipsis fits `maxWidth`. Falls back to a bare "…" if not even one character fits. */
|
|
126
|
-
function ellipsize(line, fontFamily, fontSize, fontStyle, maxWidth, metrics) {
|
|
127
|
-
const
|
|
127
|
+
function ellipsize(line, fontFamily, fontSize, fontStyle, maxWidth, metrics, letterSpacing = 0) {
|
|
128
|
+
const font = { fontFamily, fontSize, fontStyle };
|
|
129
|
+
const fits = (s) => runAdvance(metrics, s + ELLIPSIS, font, letterSpacing) <= maxWidth;
|
|
128
130
|
if (fits(line))
|
|
129
131
|
return line + ELLIPSIS;
|
|
130
132
|
const words = line.split(" ");
|
|
@@ -150,11 +152,16 @@ function ellipsizeSegmentLine(line, defaults, maxWidth, metrics) {
|
|
|
150
152
|
const segs = line.segments;
|
|
151
153
|
if (segs.length === 0)
|
|
152
154
|
return;
|
|
153
|
-
const
|
|
155
|
+
const lsOf = (seg) => seg.letterSpacing ?? defaults.letterSpacing ?? 0;
|
|
156
|
+
const widthOf = (seg) => runAdvance(metrics, seg.content, {
|
|
157
|
+
fontFamily: seg.fontFamily || defaults.fontFamily,
|
|
158
|
+
fontSize: seg.fontSize || defaults.fontSize,
|
|
159
|
+
fontStyle: seg.fontStyle || defaults.fontStyle,
|
|
160
|
+
}, lsOf(seg));
|
|
154
161
|
let prefix = 0;
|
|
155
162
|
for (let i = 0; i < segs.length - 1; i++)
|
|
156
163
|
prefix += widthOf(segs[i]);
|
|
157
164
|
const last = segs[segs.length - 1];
|
|
158
|
-
last.content = ellipsize(last.content, last.fontFamily || defaults.fontFamily, last.fontSize || defaults.fontSize, last.fontStyle || defaults.fontStyle, maxWidth - prefix, metrics);
|
|
165
|
+
last.content = ellipsize(last.content, last.fontFamily || defaults.fontFamily, last.fontSize || defaults.fontSize, last.fontStyle || defaults.fontStyle, maxWidth - prefix, metrics, lsOf(last));
|
|
159
166
|
line.width = prefix + widthOf(last);
|
|
160
167
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The vertical half of text layout: how tall a line box is, and where its baseline sits.
|
|
3
|
+
*
|
|
4
|
+
* One canonical answer, like `line-breaker.ts` is the one canonical answer for the horizontal
|
|
5
|
+
* half. Measuring (`calculateTextHeight`), fragmenting (`TextElement.fragment`) and drawing
|
|
6
|
+
* (`TextRenderer._buildRuns`) all call in here, so they can never disagree about a line's height
|
|
7
|
+
* again - the divergence that produced ISSUE-5.
|
|
8
|
+
*/
|
|
9
|
+
import type { FontStyle } from "../utils/pdf-object-manager.js";
|
|
10
|
+
import type { FontMetrics } from "../utils/font-metrics.js";
|
|
11
|
+
import type { SegmentDefaults, SegmentLine } from "./line-breaker.js";
|
|
12
|
+
/** A font's vertical metrics, as fractions of the em (so they scale by multiplying with fontSize). */
|
|
13
|
+
export interface FontVerticals {
|
|
14
|
+
/** Distance from the baseline up to the ascender. Positive. */
|
|
15
|
+
ascent: number;
|
|
16
|
+
/** Distance from the baseline down to the descender. Positive (unlike the AFM/hhea sign). */
|
|
17
|
+
descent: number;
|
|
18
|
+
/** Extra leading the font asks for between two lines. Often 0 for a TrueType face. */
|
|
19
|
+
lineGap: number;
|
|
20
|
+
}
|
|
21
|
+
/** One font used on a line: its metrics and the size it is set at. */
|
|
22
|
+
export interface LinePart {
|
|
23
|
+
verticals: FontVerticals;
|
|
24
|
+
fontSize: number;
|
|
25
|
+
}
|
|
26
|
+
/** A laid-out line box, in points, relative to the top of the box. */
|
|
27
|
+
export interface LineBox {
|
|
28
|
+
/** Total height of the line box. Lines stack by this. */
|
|
29
|
+
height: number;
|
|
30
|
+
/** Distance from the top of the line box down to the baseline. */
|
|
31
|
+
baseline: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The line box for a set of fonts sharing one line.
|
|
35
|
+
*
|
|
36
|
+
* `lineHeight` is a multiplier of the font size, like CSS. Leave it `undefined` for the font's
|
|
37
|
+
* NATURAL line height (`ascent + descent + lineGap`), which is what CSS calls `line-height: normal`.
|
|
38
|
+
*
|
|
39
|
+
* Whatever is left over between the box and the font's content is split evenly above and below
|
|
40
|
+
* ("half-leading", as CSS and Flutter do), so the glyphs sit centred in their own box. An explicit
|
|
41
|
+
* `lineHeight` smaller than the content makes the leading negative and the glyphs overflow - again
|
|
42
|
+
* exactly like CSS, and the author asked for it.
|
|
43
|
+
*/
|
|
44
|
+
export declare function lineBoxFor(parts: LinePart[], lineHeight?: number): LineBox;
|
|
45
|
+
/** The line box of a single-font line (the plain-string path). */
|
|
46
|
+
export declare function lineBoxForString(metrics: FontMetrics, fontFamily: string, fontStyle: FontStyle, fontSize: number, lineHeight?: number): LineBox;
|
|
47
|
+
/** The line box of a mixed-font line: the tallest ascent and the deepest descent on it win, so a
|
|
48
|
+
* big or a deep-descending span pushes the whole line apart rather than colliding with it. */
|
|
49
|
+
export declare function lineBoxForSegmentLine(line: SegmentLine, defaults: SegmentDefaults, metrics: FontMetrics, lineHeight?: number): LineBox;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The vertical half of text layout: how tall a line box is, and where its baseline sits.
|
|
3
|
+
*
|
|
4
|
+
* One canonical answer, like `line-breaker.ts` is the one canonical answer for the horizontal
|
|
5
|
+
* half. Measuring (`calculateTextHeight`), fragmenting (`TextElement.fragment`) and drawing
|
|
6
|
+
* (`TextRenderer._buildRuns`) all call in here, so they can never disagree about a line's height
|
|
7
|
+
* again - the divergence that produced ISSUE-5.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* The line box for a set of fonts sharing one line.
|
|
11
|
+
*
|
|
12
|
+
* `lineHeight` is a multiplier of the font size, like CSS. Leave it `undefined` for the font's
|
|
13
|
+
* NATURAL line height (`ascent + descent + lineGap`), which is what CSS calls `line-height: normal`.
|
|
14
|
+
*
|
|
15
|
+
* Whatever is left over between the box and the font's content is split evenly above and below
|
|
16
|
+
* ("half-leading", as CSS and Flutter do), so the glyphs sit centred in their own box. An explicit
|
|
17
|
+
* `lineHeight` smaller than the content makes the leading negative and the glyphs overflow - again
|
|
18
|
+
* exactly like CSS, and the author asked for it.
|
|
19
|
+
*/
|
|
20
|
+
export function lineBoxFor(parts, lineHeight) {
|
|
21
|
+
if (parts.length === 0)
|
|
22
|
+
return { height: 0, baseline: 0 };
|
|
23
|
+
let ascent = 0;
|
|
24
|
+
let descent = 0;
|
|
25
|
+
let gap = 0;
|
|
26
|
+
let maxFontSize = 0;
|
|
27
|
+
for (const { verticals, fontSize } of parts) {
|
|
28
|
+
ascent = Math.max(ascent, verticals.ascent * fontSize);
|
|
29
|
+
descent = Math.max(descent, verticals.descent * fontSize);
|
|
30
|
+
gap = Math.max(gap, verticals.lineGap * fontSize);
|
|
31
|
+
maxFontSize = Math.max(maxFontSize, fontSize);
|
|
32
|
+
}
|
|
33
|
+
const content = ascent + descent;
|
|
34
|
+
const height = lineHeight === undefined ? content + gap : maxFontSize * lineHeight;
|
|
35
|
+
return { height, baseline: (height - content) / 2 + ascent };
|
|
36
|
+
}
|
|
37
|
+
/** The line box of a single-font line (the plain-string path). */
|
|
38
|
+
export function lineBoxForString(metrics, fontFamily, fontStyle, fontSize, lineHeight) {
|
|
39
|
+
return lineBoxFor([{ verticals: metrics.getFontVerticals(fontFamily, fontStyle), fontSize }], lineHeight);
|
|
40
|
+
}
|
|
41
|
+
/** The line box of a mixed-font line: the tallest ascent and the deepest descent on it win, so a
|
|
42
|
+
* big or a deep-descending span pushes the whole line apart rather than colliding with it. */
|
|
43
|
+
export function lineBoxForSegmentLine(line, defaults, metrics, lineHeight) {
|
|
44
|
+
const parts = line.segments.map((segment) => {
|
|
45
|
+
const fontFamily = segment.fontFamily ?? defaults.fontFamily;
|
|
46
|
+
const fontStyle = segment.fontStyle ?? defaults.fontStyle;
|
|
47
|
+
return {
|
|
48
|
+
verticals: metrics.getFontVerticals(fontFamily, fontStyle),
|
|
49
|
+
fontSize: segment.fontSize ?? defaults.fontSize,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
// A line with no segments still occupies its default font's box (an empty paragraph keeps height).
|
|
53
|
+
if (parts.length === 0) {
|
|
54
|
+
return lineBoxForString(metrics, defaults.fontFamily, defaults.fontStyle, defaults.fontSize, lineHeight);
|
|
55
|
+
}
|
|
56
|
+
return lineBoxFor(parts, lineHeight);
|
|
57
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an underline and a strikethrough sit, and how thick they are.
|
|
3
|
+
*
|
|
4
|
+
* These are GLYPH metrics, and they are deliberately NOT part of `FontVerticals` in
|
|
5
|
+
* `line-metrics.ts`, which holds LINE metrics. Confusing the two is exactly what produced ISSUE-5:
|
|
6
|
+
* the AFM's `Ascender` (the height of a `b`) was used to seat a baseline, a job that belongs to the
|
|
7
|
+
* `FontBBox`. Keep the two kinds of number in two places so they cannot be mixed up again.
|
|
8
|
+
*
|
|
9
|
+
* Every number here comes out of the font: the standard-14 AFM header, or a TrueType `post` +
|
|
10
|
+
* `OS/2` table. Nothing is invented. Verified against `google-chrome --headless --print-to-pdf` on
|
|
11
|
+
* the same font: Chrome's strikethrough centre lands on `xHeight / 2` (measured 0.260 em vs the
|
|
12
|
+
* font's 0.262), and its underline stroke with `text-decoration-thickness: from-font` is the font's
|
|
13
|
+
* `underlineThickness` (0.048 em vs 0.050). Chrome does NOT use the font's underline POSITION - it
|
|
14
|
+
* has its own offset - so we follow the font there and land within 0.02 em of it.
|
|
15
|
+
*/
|
|
16
|
+
/** A font's decoration metrics, as fractions of the em. */
|
|
17
|
+
export interface FontDecoration {
|
|
18
|
+
/** Distance BELOW the baseline to the CENTRE of the underline stroke. Positive.
|
|
19
|
+
* (The AFM defines it as "the distance from the baseline for centering underlining strokes";
|
|
20
|
+
* FreeType reads TrueType's `post.underlinePosition` the same way.) */
|
|
21
|
+
underlinePosition: number;
|
|
22
|
+
/** Stroke thickness, used by both the underline and the strikethrough. */
|
|
23
|
+
underlineThickness: number;
|
|
24
|
+
/** Height of a lowercase `x` above the baseline. Sets where a strikethrough crosses. */
|
|
25
|
+
xHeight: number;
|
|
26
|
+
/** Height of a capital above the baseline. */
|
|
27
|
+
capHeight: number;
|
|
28
|
+
}
|
|
29
|
+
/** A decoration stroke: the y of its centre line, and how thick it is. Points, engine coords. */
|
|
30
|
+
export interface DecorationStroke {
|
|
31
|
+
y: number;
|
|
32
|
+
thickness: number;
|
|
33
|
+
}
|
|
34
|
+
/** The underline for a run whose baseline sits at `baselineY` (engine coords, y grows down). */
|
|
35
|
+
export declare function underlineStroke(decoration: FontDecoration, fontSize: number, baselineY: number): DecorationStroke;
|
|
36
|
+
/**
|
|
37
|
+
* The strikethrough for a run whose baseline sits at `baselineY`. It crosses at half the x-height,
|
|
38
|
+
* which is what a browser does and what looks right: through the middle of the lowercase letters,
|
|
39
|
+
* not through the middle of the capitals.
|
|
40
|
+
*/
|
|
41
|
+
export declare function strikethroughStroke(decoration: FontDecoration, fontSize: number, baselineY: number): DecorationStroke;
|
|
42
|
+
/**
|
|
43
|
+
* Cuts `[0, runWidth]` at every place the glyphs put ink, leaving the segments an underline should
|
|
44
|
+
* actually draw. `inkSpans` are x-intervals in points from the start of the run.
|
|
45
|
+
*
|
|
46
|
+
* This is CSS `text-decoration-skip-ink`, and it needs the real glyph outlines - so it only works
|
|
47
|
+
* for an embedded font. Segments narrower than the padding are dropped: a sliver of a line between
|
|
48
|
+
* two descenders reads as dirt, not as an underline.
|
|
49
|
+
*/
|
|
50
|
+
export declare function skipInkSegments(runWidth: number, inkSpans: Array<[number, number]>, thickness: number): Array<[number, number]>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an underline and a strikethrough sit, and how thick they are.
|
|
3
|
+
*
|
|
4
|
+
* These are GLYPH metrics, and they are deliberately NOT part of `FontVerticals` in
|
|
5
|
+
* `line-metrics.ts`, which holds LINE metrics. Confusing the two is exactly what produced ISSUE-5:
|
|
6
|
+
* the AFM's `Ascender` (the height of a `b`) was used to seat a baseline, a job that belongs to the
|
|
7
|
+
* `FontBBox`. Keep the two kinds of number in two places so they cannot be mixed up again.
|
|
8
|
+
*
|
|
9
|
+
* Every number here comes out of the font: the standard-14 AFM header, or a TrueType `post` +
|
|
10
|
+
* `OS/2` table. Nothing is invented. Verified against `google-chrome --headless --print-to-pdf` on
|
|
11
|
+
* the same font: Chrome's strikethrough centre lands on `xHeight / 2` (measured 0.260 em vs the
|
|
12
|
+
* font's 0.262), and its underline stroke with `text-decoration-thickness: from-font` is the font's
|
|
13
|
+
* `underlineThickness` (0.048 em vs 0.050). Chrome does NOT use the font's underline POSITION - it
|
|
14
|
+
* has its own offset - so we follow the font there and land within 0.02 em of it.
|
|
15
|
+
*/
|
|
16
|
+
/** The underline for a run whose baseline sits at `baselineY` (engine coords, y grows down). */
|
|
17
|
+
export function underlineStroke(decoration, fontSize, baselineY) {
|
|
18
|
+
return {
|
|
19
|
+
y: baselineY + decoration.underlinePosition * fontSize,
|
|
20
|
+
thickness: decoration.underlineThickness * fontSize,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The strikethrough for a run whose baseline sits at `baselineY`. It crosses at half the x-height,
|
|
25
|
+
* which is what a browser does and what looks right: through the middle of the lowercase letters,
|
|
26
|
+
* not through the middle of the capitals.
|
|
27
|
+
*/
|
|
28
|
+
export function strikethroughStroke(decoration, fontSize, baselineY) {
|
|
29
|
+
return {
|
|
30
|
+
y: baselineY - (decoration.xHeight / 2) * fontSize,
|
|
31
|
+
thickness: decoration.underlineThickness * fontSize,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Horizontal breathing room left on each side of a descender that interrupts an underline. Unlike
|
|
36
|
+
* everything else in this file this is a DESIGN choice, not a number read from the font - so it is
|
|
37
|
+
* expressed in the one quantity the font does give us, the stroke thickness.
|
|
38
|
+
*/
|
|
39
|
+
const SKIP_INK_PADDING = 1.0;
|
|
40
|
+
/**
|
|
41
|
+
* Cuts `[0, runWidth]` at every place the glyphs put ink, leaving the segments an underline should
|
|
42
|
+
* actually draw. `inkSpans` are x-intervals in points from the start of the run.
|
|
43
|
+
*
|
|
44
|
+
* This is CSS `text-decoration-skip-ink`, and it needs the real glyph outlines - so it only works
|
|
45
|
+
* for an embedded font. Segments narrower than the padding are dropped: a sliver of a line between
|
|
46
|
+
* two descenders reads as dirt, not as an underline.
|
|
47
|
+
*/
|
|
48
|
+
export function skipInkSegments(runWidth, inkSpans, thickness) {
|
|
49
|
+
const pad = SKIP_INK_PADDING * thickness;
|
|
50
|
+
const segments = [];
|
|
51
|
+
let cursor = 0;
|
|
52
|
+
for (const [start, end] of inkSpans) {
|
|
53
|
+
const gapStart = Math.max(0, start - pad);
|
|
54
|
+
const gapEnd = Math.min(runWidth, end + pad);
|
|
55
|
+
if (gapStart > cursor + pad)
|
|
56
|
+
segments.push([cursor, gapStart]);
|
|
57
|
+
cursor = Math.max(cursor, gapEnd);
|
|
58
|
+
}
|
|
59
|
+
if (runWidth > cursor + pad)
|
|
60
|
+
segments.push([cursor, runWidth]);
|
|
61
|
+
return segments;
|
|
62
|
+
}
|
|
@@ -13,12 +13,22 @@ export interface ResolvedTextStyle {
|
|
|
13
13
|
fontStyle: FontStyle;
|
|
14
14
|
color: Color;
|
|
15
15
|
textAlignment: HorizontalAlignment;
|
|
16
|
-
|
|
16
|
+
/** Multiplier of the font size. `undefined` means the font's natural line height
|
|
17
|
+
* (`ascent + descent + lineGap`), i.e. CSS `line-height: normal`. */
|
|
18
|
+
lineHeight?: number;
|
|
19
|
+
/** Draw a line under the text, at the position and thickness the font declares. */
|
|
20
|
+
underline: boolean;
|
|
21
|
+
/** Draw a line through the text, at half its x-height. */
|
|
22
|
+
strikethrough: boolean;
|
|
23
|
+
/** Let the underline step around descenders (CSS `text-decoration-skip-ink`). Needs an EMBEDDED
|
|
24
|
+
* font: the standard-14 outlines live in the viewer, not in the AFM. */
|
|
25
|
+
skipInk: boolean;
|
|
26
|
+
/** Extra space after every glyph, in points (CSS `letter-spacing`). Default 0. */
|
|
27
|
+
letterSpacing: number;
|
|
17
28
|
}
|
|
18
29
|
/**
|
|
19
30
|
* The root of the cascade: what a `Text` falls back to when neither it nor any ancestor sets a
|
|
20
|
-
* property.
|
|
21
|
-
* byte-for-byte as before.
|
|
31
|
+
* property.
|
|
22
32
|
*/
|
|
23
33
|
export declare const DEFAULT_TEXT_STYLE: ResolvedTextStyle;
|
|
24
34
|
/** Layers a partial override onto a complete style; an unset (undefined) field keeps the base. */
|
package/dist/text/text-style.js
CHANGED
|
@@ -3,8 +3,7 @@ import { FontStyle } from "../utils/pdf-object-manager.js";
|
|
|
3
3
|
import { HorizontalAlignment } from "../elements/pdf-element.js";
|
|
4
4
|
/**
|
|
5
5
|
* The root of the cascade: what a `Text` falls back to when neither it nor any ancestor sets a
|
|
6
|
-
* property.
|
|
7
|
-
* byte-for-byte as before.
|
|
6
|
+
* property.
|
|
8
7
|
*/
|
|
9
8
|
export const DEFAULT_TEXT_STYLE = {
|
|
10
9
|
fontSize: 12,
|
|
@@ -12,7 +11,11 @@ export const DEFAULT_TEXT_STYLE = {
|
|
|
12
11
|
fontStyle: FontStyle.Normal,
|
|
13
12
|
color: new Color(0, 0, 0),
|
|
14
13
|
textAlignment: HorizontalAlignment.left,
|
|
15
|
-
lineHeight:
|
|
14
|
+
lineHeight: undefined,
|
|
15
|
+
underline: false,
|
|
16
|
+
strikethrough: false,
|
|
17
|
+
skipInk: false,
|
|
18
|
+
letterSpacing: 0,
|
|
16
19
|
};
|
|
17
20
|
/** Layers a partial override onto a complete style; an unset (undefined) field keeps the base. */
|
|
18
21
|
export function mergeTextStyle(base, override) {
|
|
@@ -25,5 +28,9 @@ export function mergeTextStyle(base, override) {
|
|
|
25
28
|
color: override.color ?? base.color,
|
|
26
29
|
textAlignment: override.textAlignment ?? base.textAlignment,
|
|
27
30
|
lineHeight: override.lineHeight ?? base.lineHeight,
|
|
31
|
+
underline: override.underline ?? base.underline,
|
|
32
|
+
strikethrough: override.strikethrough ?? base.strikethrough,
|
|
33
|
+
skipInk: override.skipInk ?? base.skipInk,
|
|
34
|
+
letterSpacing: override.letterSpacing ?? base.letterSpacing,
|
|
28
35
|
};
|
|
29
36
|
}
|
|
@@ -1,12 +1,46 @@
|
|
|
1
|
+
import type { FontVerticals } from "../text/line-metrics.js";
|
|
2
|
+
import type { FontDecoration } from "../text/text-decoration.js";
|
|
1
3
|
export declare class AFMParser {
|
|
2
4
|
private advanceWidths;
|
|
3
5
|
private kerningPairs;
|
|
4
6
|
private glyphMap;
|
|
7
|
+
private bbox;
|
|
8
|
+
private cachedVerticals?;
|
|
9
|
+
private underlinePosition;
|
|
10
|
+
private underlineThickness;
|
|
11
|
+
private capHeight;
|
|
12
|
+
private xHeight;
|
|
13
|
+
private cachedDecoration?;
|
|
5
14
|
constructor(afmData: string);
|
|
6
15
|
private loadGlyphList;
|
|
7
16
|
private getGlyphName;
|
|
8
17
|
private parseAFMData;
|
|
9
18
|
private parseCharMetrics;
|
|
19
|
+
/**
|
|
20
|
+
* The font's LINE metrics, as fractions of the em: how far a line box must reach above and below
|
|
21
|
+
* the baseline. Taken from the `FontBBox`.
|
|
22
|
+
*
|
|
23
|
+
* Not from the AFM's `Ascender` / `Descender` lines, and that distinction is the whole point.
|
|
24
|
+
* `Ascender 718` is a GLYPH metric - the height of `b`, `d`, `h` - and for Helvetica it happens to
|
|
25
|
+
* equal the cap height. The line-metric equivalent is TrueType's `hhea.ascent`, which is much
|
|
26
|
+
* taller because it has to clear the accented capitals: Arial declares 0.905 where its caps reach
|
|
27
|
+
* 0.716. That surplus above the caps (0.189) is about as large as the descent below the baseline
|
|
28
|
+
* (0.212), which is exactly why capitals look vertically centred in a browser.
|
|
29
|
+
*
|
|
30
|
+
* Helvetica's `FontBBox` is `-166 -225 1000 931`: 0.931 up, 0.225 down - within a hair of Arial's
|
|
31
|
+
* hhea numbers. So the bbox IS the standard-14 line metric, and there is no leftover slack to
|
|
32
|
+
* call a lineGap. (Seating the baseline at `Ascender` instead put every capital too high; see
|
|
33
|
+
* ISSUE-5.)
|
|
34
|
+
*/
|
|
35
|
+
verticals(): FontVerticals;
|
|
36
|
+
/**
|
|
37
|
+
* The font's decoration metrics (underline / strikethrough), as fractions of the em.
|
|
38
|
+
*
|
|
39
|
+
* Symbol and ZapfDingbats declare no `CapHeight`/`XHeight` - they have no lowercase letters. For
|
|
40
|
+
* those the bbox top is the only height the font gives us, so the capitals reach it and a
|
|
41
|
+
* strikethrough crosses at half of that. Still read from the font, not guessed.
|
|
42
|
+
*/
|
|
43
|
+
decoration(): FontDecoration;
|
|
10
44
|
getAdvanceWidth(charName: string): number;
|
|
11
45
|
getKerning(firstChar: string, secondChar: string): number;
|
|
12
46
|
}
|
package/dist/utils/afm-parser.js
CHANGED
|
@@ -4,6 +4,15 @@ export class AFMParser {
|
|
|
4
4
|
this.advanceWidths = {};
|
|
5
5
|
this.kerningPairs = {};
|
|
6
6
|
this.glyphMap = {};
|
|
7
|
+
// The font's bounding box in glyph space (1000 units / em), from the AFM header. This - not the
|
|
8
|
+
// `Ascender` line - is what a line box is built from; see `verticals()`.
|
|
9
|
+
this.bbox = [0, 0, 0, 0];
|
|
10
|
+
// Glyph metrics from the AFM header, for underline / strikethrough. All 14 standard fonts carry
|
|
11
|
+
// the underline pair; Symbol and ZapfDingbats have no CapHeight/XHeight (they have no lowercase).
|
|
12
|
+
this.underlinePosition = 0; // negative, as the AFM writes it: below the baseline
|
|
13
|
+
this.underlineThickness = 0;
|
|
14
|
+
this.capHeight = 0;
|
|
15
|
+
this.xHeight = 0;
|
|
7
16
|
this.parseAFMData(afmData);
|
|
8
17
|
this.loadGlyphList();
|
|
9
18
|
}
|
|
@@ -45,6 +54,22 @@ export class AFMParser {
|
|
|
45
54
|
this.advanceWidths[charName] = wx;
|
|
46
55
|
}
|
|
47
56
|
}
|
|
57
|
+
if (line.startsWith("UnderlinePosition ")) {
|
|
58
|
+
this.underlinePosition = parseFloat(line.slice(18));
|
|
59
|
+
}
|
|
60
|
+
if (line.startsWith("UnderlineThickness ")) {
|
|
61
|
+
this.underlineThickness = parseFloat(line.slice(19));
|
|
62
|
+
}
|
|
63
|
+
if (line.startsWith("CapHeight ")) {
|
|
64
|
+
this.capHeight = parseFloat(line.slice(10));
|
|
65
|
+
}
|
|
66
|
+
if (line.startsWith("XHeight ")) {
|
|
67
|
+
this.xHeight = parseFloat(line.slice(8));
|
|
68
|
+
}
|
|
69
|
+
if (line.startsWith("FontBBox ")) {
|
|
70
|
+
const [x0, y0, x1, y1] = line.slice(9).trim().split(/\s+/).map(parseFloat);
|
|
71
|
+
this.bbox = [x0, y0, x1, y1];
|
|
72
|
+
}
|
|
48
73
|
if (line.startsWith("KPX")) {
|
|
49
74
|
const parts = line.split(/\s+/);
|
|
50
75
|
const firstChar = parts[1];
|
|
@@ -74,6 +99,48 @@ export class AFMParser {
|
|
|
74
99
|
}
|
|
75
100
|
return null;
|
|
76
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* The font's LINE metrics, as fractions of the em: how far a line box must reach above and below
|
|
104
|
+
* the baseline. Taken from the `FontBBox`.
|
|
105
|
+
*
|
|
106
|
+
* Not from the AFM's `Ascender` / `Descender` lines, and that distinction is the whole point.
|
|
107
|
+
* `Ascender 718` is a GLYPH metric - the height of `b`, `d`, `h` - and for Helvetica it happens to
|
|
108
|
+
* equal the cap height. The line-metric equivalent is TrueType's `hhea.ascent`, which is much
|
|
109
|
+
* taller because it has to clear the accented capitals: Arial declares 0.905 where its caps reach
|
|
110
|
+
* 0.716. That surplus above the caps (0.189) is about as large as the descent below the baseline
|
|
111
|
+
* (0.212), which is exactly why capitals look vertically centred in a browser.
|
|
112
|
+
*
|
|
113
|
+
* Helvetica's `FontBBox` is `-166 -225 1000 931`: 0.931 up, 0.225 down - within a hair of Arial's
|
|
114
|
+
* hhea numbers. So the bbox IS the standard-14 line metric, and there is no leftover slack to
|
|
115
|
+
* call a lineGap. (Seating the baseline at `Ascender` instead put every capital too high; see
|
|
116
|
+
* ISSUE-5.)
|
|
117
|
+
*/
|
|
118
|
+
verticals() {
|
|
119
|
+
if (this.cachedVerticals)
|
|
120
|
+
return this.cachedVerticals;
|
|
121
|
+
const [, yMin, , yMax] = this.bbox;
|
|
122
|
+
this.cachedVerticals = { ascent: yMax / 1000, descent: -yMin / 1000, lineGap: 0 };
|
|
123
|
+
return this.cachedVerticals;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The font's decoration metrics (underline / strikethrough), as fractions of the em.
|
|
127
|
+
*
|
|
128
|
+
* Symbol and ZapfDingbats declare no `CapHeight`/`XHeight` - they have no lowercase letters. For
|
|
129
|
+
* those the bbox top is the only height the font gives us, so the capitals reach it and a
|
|
130
|
+
* strikethrough crosses at half of that. Still read from the font, not guessed.
|
|
131
|
+
*/
|
|
132
|
+
decoration() {
|
|
133
|
+
if (this.cachedDecoration)
|
|
134
|
+
return this.cachedDecoration;
|
|
135
|
+
const capHeight = this.capHeight || this.bbox[3];
|
|
136
|
+
this.cachedDecoration = {
|
|
137
|
+
underlinePosition: -this.underlinePosition / 1000, // AFM writes it negative (below the baseline)
|
|
138
|
+
underlineThickness: this.underlineThickness / 1000,
|
|
139
|
+
capHeight: capHeight / 1000,
|
|
140
|
+
xHeight: (this.xHeight || capHeight / 2) / 1000,
|
|
141
|
+
};
|
|
142
|
+
return this.cachedDecoration;
|
|
143
|
+
}
|
|
77
144
|
getAdvanceWidth(charName) {
|
|
78
145
|
return this.advanceWidths[this.getGlyphName(charName)] || 0;
|
|
79
146
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PDFObjectManager } from "./pdf-object-manager.js";
|
|
1
2
|
interface Dest {
|
|
2
3
|
pageObjNum: number;
|
|
3
4
|
top: number;
|
|
@@ -13,7 +14,9 @@ export declare class DestRegistry {
|
|
|
13
14
|
add(name: string, dest: Dest): void;
|
|
14
15
|
get isEmpty(): boolean;
|
|
15
16
|
/** The `/Dests << /Names [ ... ] >>` fragment for the catalog `/Names` dictionary (merged with any
|
|
16
|
-
* `/EmbeddedFiles`). Returns "" when empty. Names are sorted lexically - a name tree requires it.
|
|
17
|
-
|
|
17
|
+
* `/EmbeddedFiles`). Returns "" when empty. Names are sorted lexically - a name tree requires it.
|
|
18
|
+
* Takes the object manager only so the names go through its string choke-point and get encrypted
|
|
19
|
+
* with everything else. */
|
|
20
|
+
finalize(om: PDFObjectManager): string;
|
|
18
21
|
}
|
|
19
22
|
export {};
|