@jasy/pdf 1.0.0-alpha.6 → 1.0.0-alpha.7
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/api/descriptor.js +3 -1
- package/dist/api/layout.d.ts +47 -14
- package/dist/api/layout.js +47 -11
- package/dist/api/structure.d.ts +5 -0
- package/dist/api/structure.js +1 -0
- package/dist/api/text.d.ts +18 -2
- package/dist/api/text.js +15 -0
- package/dist/elements/container-element.d.ts +10 -1
- package/dist/elements/container-element.js +26 -8
- package/dist/elements/index.d.ts +2 -0
- package/dist/elements/index.js +2 -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/ir/display-list.d.ts +1 -0
- package/dist/layout/fragmentation.d.ts +12 -0
- package/dist/layout/fragmentation.js +45 -8
- 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/pdf-backend.d.ts +7 -0
- package/dist/renderer/pdf-backend.js +40 -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 +6 -0
- 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/font-metrics.d.ts +14 -0
- package/dist/utils/pdf-object-manager.d.ts +27 -1
- package/dist/utils/pdf-object-manager.js +134 -20
- package/dist/utils/ttf-parser.d.ts +53 -0
- package/dist/utils/ttf-parser.js +368 -0
- package/package.json +1 -1
|
@@ -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,4 +1,6 @@
|
|
|
1
1
|
import type { FontStyle } from "./pdf-object-manager.js";
|
|
2
|
+
import type { FontVerticals } from "../text/line-metrics.js";
|
|
3
|
+
import type { FontDecoration } from "../text/text-decoration.js";
|
|
2
4
|
/**
|
|
3
5
|
* Read-only font measurement - the slice of the object manager that the layout pass
|
|
4
6
|
* needs. Keeping layout (and, later, fragmentation) behind this interface means those
|
|
@@ -9,4 +11,16 @@ import type { FontStyle } from "./pdf-object-manager.js";
|
|
|
9
11
|
export interface FontMetrics {
|
|
10
12
|
getStringWidth(text: string, fontFamily: string, fontSize: number, fontStyle: FontStyle): number;
|
|
11
13
|
getCharWidth(char: string, fontSize: number, fullFontName?: string, fontName?: string, fontStyle?: FontStyle): number;
|
|
14
|
+
/** Ascent / descent / lineGap of a face, in em fractions - the vertical counterpart of the widths
|
|
15
|
+
* above. Drives the line box and the baseline (see `text/line-metrics.ts`). */
|
|
16
|
+
getFontVerticals(fontFamily: string, fontStyle: FontStyle): FontVerticals;
|
|
17
|
+
/** Underline / strikethrough geometry of a face, in em fractions. GLYPH metrics, deliberately kept
|
|
18
|
+
* apart from the LINE metrics above (see `text/text-decoration.ts`). */
|
|
19
|
+
getFontDecoration(fontFamily: string, fontStyle: FontStyle): FontDecoration;
|
|
20
|
+
/** Whether kerning is on for this document. `runAdvance` reads it so a measured advance matches the
|
|
21
|
+
* drawn one (the backend emits a `TJ` under the same flag). */
|
|
22
|
+
readonly kerningEnabled: boolean;
|
|
23
|
+
/** Per-adjacent-pair kerning of `text`, in em/1000 (negative tightens); length `codePoints - 1`,
|
|
24
|
+
* zero next to a space. Only meaningful when `kerningEnabled`. */
|
|
25
|
+
getKernPairs(text: string, fontFamily: string, fontStyle: FontStyle): number[];
|
|
12
26
|
}
|
|
@@ -2,6 +2,8 @@ import type { OverflowPolicy } from "../layout/fragmentation.js";
|
|
|
2
2
|
import { TTFParser } from "./ttf-parser.js";
|
|
3
3
|
import type { SecurityHandler } from "../crypto/security-handler.js";
|
|
4
4
|
import type { Gradient } from "../ir/display-list.js";
|
|
5
|
+
import type { FontVerticals } from "../text/line-metrics.js";
|
|
6
|
+
import type { FontDecoration } from "../text/text-decoration.js";
|
|
5
7
|
import { StructTree } from "./struct-tree.js";
|
|
6
8
|
import { OutlineBuilder } from "./outline.js";
|
|
7
9
|
import { DestRegistry } from "./dest-registry.js";
|
|
@@ -33,6 +35,8 @@ export declare class PDFObjectManager implements FontMetrics {
|
|
|
33
35
|
pageFormat: [number, number];
|
|
34
36
|
private afmParsers;
|
|
35
37
|
private customFonts;
|
|
38
|
+
private verticalsCache;
|
|
39
|
+
private decorationCache;
|
|
36
40
|
private customFontEmit;
|
|
37
41
|
private attachments;
|
|
38
42
|
private xmpMetadata?;
|
|
@@ -119,10 +123,32 @@ export declare class PDFObjectManager implements FontMetrics {
|
|
|
119
123
|
private buildCIDFont;
|
|
120
124
|
private buildToUnicode;
|
|
121
125
|
private buildType0;
|
|
126
|
+
getFontVerticals(fontFamily: string, fontStyle: FontStyle): FontVerticals;
|
|
127
|
+
getFontDecoration(fontFamily: string, fontStyle: FontStyle): FontDecoration;
|
|
128
|
+
kerningEnabled: boolean;
|
|
129
|
+
setKerning(enabled: boolean): void;
|
|
130
|
+
/**
|
|
131
|
+
* The kerning adjustment for each adjacent code-point pair of `text`, in em/1000 (the sign the
|
|
132
|
+
* font declares: negative tightens). Length is `codePointCount - 1`. Zero at any pair next to a
|
|
133
|
+
* space - letters are never kerned against a space, which also keeps a per-word measure equal to a
|
|
134
|
+
* whole-line one (the space pairs contribute nothing either way).
|
|
135
|
+
*
|
|
136
|
+
* Standard-14 answers from the AFM `KPX` pairs. An embedded font has no kerning yet (its `kern` /
|
|
137
|
+
* `GPOS` tables are unparsed), so it returns zeros - never a wrong value, just no adjustment.
|
|
138
|
+
*/
|
|
139
|
+
getKernPairs(text: string, fontFamily: string, fontStyle: FontStyle): number[];
|
|
140
|
+
/**
|
|
141
|
+
* Where the glyphs of `text` put ink inside a horizontal band, as x-intervals in POINTS measured
|
|
142
|
+
* from the start of the run. This is what an underline steps around ("skip-ink").
|
|
143
|
+
*
|
|
144
|
+
* `bandTop` / `bandBottom` are in points relative to the baseline, y UP (so both negative under
|
|
145
|
+
* it). Only an EMBEDDED font can answer: the standard-14 outlines live in the viewer, not here.
|
|
146
|
+
* Callers must check `isCustomFont` first rather than treat an empty result as "no descenders".
|
|
147
|
+
*/
|
|
148
|
+
getInkSpansInBand(text: string, fontSize: number, fontFamily: string, fontStyle: FontStyle, bandTop: number, bandBottom: number, letterSpacing?: number): Array<[number, number]>;
|
|
122
149
|
getStringWidth(text: string, fontFamily: string, fontSize: number, fontStyle: FontStyle): number;
|
|
123
150
|
private getAVMParserByFont;
|
|
124
151
|
getCharWidth(char: string, fontSize: number, fullFontName?: string, fontName?: string, fontStyle?: FontStyle): number;
|
|
125
|
-
private getKerning;
|
|
126
152
|
getAllFontsRaw(): Map<string, FontIndexes>;
|
|
127
153
|
getAllImagesRaw(): Map<string, number>;
|
|
128
154
|
getRenderedObjects(): string;
|
|
@@ -4,7 +4,7 @@ import { md5, md5Hex } from "./md5.js";
|
|
|
4
4
|
import { zlibSync } from "fflate";
|
|
5
5
|
import { bytesFromLatin1, latin1FromBytes } from "./bytes.js";
|
|
6
6
|
import { AFMParser } from "./afm-parser.js";
|
|
7
|
-
import { TTFParser } from "./ttf-parser.js";
|
|
7
|
+
import { mergeSpans, TTFParser } from "./ttf-parser.js";
|
|
8
8
|
import { subsetTTF } from "./ttf-subsetter.js";
|
|
9
9
|
import { getArrayBuffer } from "./utf8-to-windows1252-encoder.js";
|
|
10
10
|
import { isEmojiCodePoint } from "../text/emoji-codepoints.js";
|
|
@@ -150,6 +150,10 @@ export class PDFObjectManager {
|
|
|
150
150
|
// every lookup, and the lookups happen once per character - the string allocation alone was a third
|
|
151
151
|
// of a custom-font render. `customFontEmit` below keeps the flat key: it is touched once per face.
|
|
152
152
|
this.customFonts = new Map();
|
|
153
|
+
// Memoised getFontVerticals answers, family -> style -> metrics. Cleared when a font registers.
|
|
154
|
+
this.verticalsCache = new Map();
|
|
155
|
+
// Same, for getFontDecoration.
|
|
156
|
+
this.decorationCache = new Map();
|
|
153
157
|
// Per-registered face: the reserved font-object numbers + the glyph ids actually used. The font
|
|
154
158
|
// program is subsetted and the objects filled in by finalizeCustomFonts(), after the render pass.
|
|
155
159
|
this.customFontEmit = new Map();
|
|
@@ -170,6 +174,11 @@ export class PDFObjectManager {
|
|
|
170
174
|
// Named destinations (internal-link jump targets). Empty unless an `Anchor` was placed; then it emits
|
|
171
175
|
// a /Names /Dests tree.
|
|
172
176
|
this._dests = new DestRegistry();
|
|
177
|
+
// Whether to kern. Document-level: measuring (via `runAdvance`, which reads this off the metrics)
|
|
178
|
+
// and drawing (the backend emits a `TJ`) both consult it, so they can never disagree. OFF while the
|
|
179
|
+
// embedded-font path (kern/GPOS) is unbuilt - a standard-14-only kerning would set the 14 system
|
|
180
|
+
// fonts finer than a user's own font, which is worse than none. Flip to on when GPOS lands.
|
|
181
|
+
this.kerningEnabled = false;
|
|
173
182
|
if (pageSize)
|
|
174
183
|
this.pageFormat = pageFormats[pageSize];
|
|
175
184
|
this.fillConfigWithStandardValues();
|
|
@@ -462,6 +471,8 @@ endstream`;
|
|
|
462
471
|
if (this.fonts.hasFont(fontName, fontStyle)) {
|
|
463
472
|
return this.fonts.getFont(fontName, fontStyle); // Already exists? Return it!
|
|
464
473
|
}
|
|
474
|
+
this.verticalsCache.delete(fontName);
|
|
475
|
+
this.decorationCache.delete(fontName);
|
|
465
476
|
const data = STANDARD_AFM[fullName];
|
|
466
477
|
if (data !== undefined) {
|
|
467
478
|
this.afmParsers.push({
|
|
@@ -495,6 +506,8 @@ endstream`;
|
|
|
495
506
|
if (byStyle.has(style))
|
|
496
507
|
return;
|
|
497
508
|
byStyle.set(style, new TTFParser(data));
|
|
509
|
+
this.verticalsCache.delete(name); // this family now answers from the .ttf, not from an AFM
|
|
510
|
+
this.decorationCache.delete(name);
|
|
498
511
|
// Emission (the PDF font objects + page resource) is DEFERRED to first use via ensureEmitted(),
|
|
499
512
|
// so a registered-but-never-rendered face - e.g. a bundled family the document doesn't touch -
|
|
500
513
|
// costs nothing in the output. The metrics above are still available for layout immediately.
|
|
@@ -690,24 +703,132 @@ endstream`;
|
|
|
690
703
|
return (`<< /Type /Font /Subtype /Type0 /BaseFont /${pdfName(name)} /Encoding /Identity-H ` +
|
|
691
704
|
`/DescendantFonts [${cidFont} 0 R] /ToUnicode ${toUnicode} 0 R >>`);
|
|
692
705
|
}
|
|
706
|
+
// The vertical metrics of a face, in em fractions - what seats the baseline and sizes the line
|
|
707
|
+
// box (see text/line-metrics.ts). An embedded font answers from its own `hhea`; a standard-14
|
|
708
|
+
// font from its AFM header. Nothing here is guessed: both numbers come out of the font data.
|
|
709
|
+
//
|
|
710
|
+
// Asked once per LINE (not per character), but a long document has a lot of lines and the
|
|
711
|
+
// standard-14 lookup below is a linear scan, so the answer is memoised per face. Registering a
|
|
712
|
+
// font clears the cache - a name can go from standard-14 to embedded.
|
|
713
|
+
getFontVerticals(fontFamily, fontStyle) {
|
|
714
|
+
let byStyle = this.verticalsCache.get(fontFamily);
|
|
715
|
+
const hit = byStyle?.get(fontStyle);
|
|
716
|
+
if (hit)
|
|
717
|
+
return hit;
|
|
718
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
719
|
+
const verticals = ttf
|
|
720
|
+
? {
|
|
721
|
+
ascent: ttf.ascent / 1000,
|
|
722
|
+
descent: -ttf.descent / 1000, // hhea writes the descent negative
|
|
723
|
+
lineGap: ttf.lineGap / 1000,
|
|
724
|
+
}
|
|
725
|
+
: this.getAVMParserByFont(undefined, fontFamily, fontStyle).parser.verticals();
|
|
726
|
+
if (!byStyle) {
|
|
727
|
+
byStyle = new Map();
|
|
728
|
+
this.verticalsCache.set(fontFamily, byStyle);
|
|
729
|
+
}
|
|
730
|
+
byStyle.set(fontStyle, verticals);
|
|
731
|
+
return verticals;
|
|
732
|
+
}
|
|
733
|
+
// Underline / strikethrough geometry of a face, in em fractions. Same shape as getFontVerticals:
|
|
734
|
+
// an embedded font answers from its `post`/`OS/2` tables, a standard-14 one from its AFM header,
|
|
735
|
+
// and the answer is memoised per face because it is asked once per decorated run.
|
|
736
|
+
getFontDecoration(fontFamily, fontStyle) {
|
|
737
|
+
let byStyle = this.decorationCache.get(fontFamily);
|
|
738
|
+
const hit = byStyle?.get(fontStyle);
|
|
739
|
+
if (hit)
|
|
740
|
+
return hit;
|
|
741
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
742
|
+
const decoration = ttf
|
|
743
|
+
? ttf.decoration()
|
|
744
|
+
: this.getAVMParserByFont(undefined, fontFamily, fontStyle).parser.decoration();
|
|
745
|
+
if (!byStyle) {
|
|
746
|
+
byStyle = new Map();
|
|
747
|
+
this.decorationCache.set(fontFamily, byStyle);
|
|
748
|
+
}
|
|
749
|
+
byStyle.set(fontStyle, decoration);
|
|
750
|
+
return decoration;
|
|
751
|
+
}
|
|
752
|
+
setKerning(enabled) {
|
|
753
|
+
this.kerningEnabled = enabled;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* The kerning adjustment for each adjacent code-point pair of `text`, in em/1000 (the sign the
|
|
757
|
+
* font declares: negative tightens). Length is `codePointCount - 1`. Zero at any pair next to a
|
|
758
|
+
* space - letters are never kerned against a space, which also keeps a per-word measure equal to a
|
|
759
|
+
* whole-line one (the space pairs contribute nothing either way).
|
|
760
|
+
*
|
|
761
|
+
* Standard-14 answers from the AFM `KPX` pairs. An embedded font has no kerning yet (its `kern` /
|
|
762
|
+
* `GPOS` tables are unparsed), so it returns zeros - never a wrong value, just no adjustment.
|
|
763
|
+
*/
|
|
764
|
+
getKernPairs(text, fontFamily, fontStyle) {
|
|
765
|
+
const chars = [...text];
|
|
766
|
+
if (chars.length < 2)
|
|
767
|
+
return [];
|
|
768
|
+
const out = [];
|
|
769
|
+
// An embedded font kerns by GLYPH id (kern table / GPOS); a standard-14 one by character (AFM).
|
|
770
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
771
|
+
const parser = ttf
|
|
772
|
+
? undefined
|
|
773
|
+
: this.getAVMParserByFont(undefined, fontFamily, fontStyle).parser;
|
|
774
|
+
for (let i = 0; i < chars.length - 1; i++) {
|
|
775
|
+
const a = chars[i];
|
|
776
|
+
const b = chars[i + 1];
|
|
777
|
+
if (a === " " || b === " ") {
|
|
778
|
+
out.push(0);
|
|
779
|
+
}
|
|
780
|
+
else if (ttf) {
|
|
781
|
+
out.push(ttf.getKerning(ttf.getGlyphIndex(a.codePointAt(0)), ttf.getGlyphIndex(b.codePointAt(0))));
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
out.push(parser.getKerning(a, b));
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return out;
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Where the glyphs of `text` put ink inside a horizontal band, as x-intervals in POINTS measured
|
|
791
|
+
* from the start of the run. This is what an underline steps around ("skip-ink").
|
|
792
|
+
*
|
|
793
|
+
* `bandTop` / `bandBottom` are in points relative to the baseline, y UP (so both negative under
|
|
794
|
+
* it). Only an EMBEDDED font can answer: the standard-14 outlines live in the viewer, not here.
|
|
795
|
+
* Callers must check `isCustomFont` first rather than treat an empty result as "no descenders".
|
|
796
|
+
*/
|
|
797
|
+
getInkSpansInBand(text, fontSize, fontFamily, fontStyle, bandTop, bandBottom, letterSpacing = 0) {
|
|
798
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
799
|
+
if (!ttf)
|
|
800
|
+
return [];
|
|
801
|
+
const scale = fontSize / ttf.unitsPerEm;
|
|
802
|
+
const spans = [];
|
|
803
|
+
let pen = 0;
|
|
804
|
+
for (const ch of text) {
|
|
805
|
+
const codePoint = ch.codePointAt(0);
|
|
806
|
+
if (codePoint !== undefined) {
|
|
807
|
+
for (const [x0, x1] of ttf.inkSpansInBand(codePoint, bandTop / scale, bandBottom / scale)) {
|
|
808
|
+
spans.push([pen + x0 * scale, pen + x1 * scale]);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
// The pen advances by the glyph AND its letter-spacing, so the ink of the next glyph lands
|
|
812
|
+
// where `Tc` actually draws it - otherwise the skipped gaps drift under the wrong letters.
|
|
813
|
+
pen += this.getCharWidth(ch, fontSize, undefined, fontFamily, fontStyle) + letterSpacing;
|
|
814
|
+
}
|
|
815
|
+
return mergeSpans(spans);
|
|
816
|
+
}
|
|
693
817
|
// Returns the current width of a text, included kernings
|
|
694
818
|
getStringWidth(text, fontFamily, fontSize, fontStyle) {
|
|
695
819
|
let width = 0;
|
|
696
820
|
// Iterate code points, not UTF-16 units: an astral char (emoji, CJK-ext) is a surrogate pair,
|
|
697
821
|
// and indexing by unit would measure each half as its own (zero-width) "char". The spread splits
|
|
698
822
|
// on code points; for BMP text this is one unit each, so the result is unchanged.
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
const kerning = this.getKerning(char, nextChar, undefined, fontFamily, fontStyle);
|
|
709
|
-
width += kerning * fontSize; // Kerning must be scaled with the font size
|
|
710
|
-
}
|
|
823
|
+
//
|
|
824
|
+
// NO KERNING. We write a run as a single `Tj`, and a viewer advances that by the font's plain
|
|
825
|
+
// widths - PDF never kerns on its own; a producer has to say so with a `TJ` array. Folding the
|
|
826
|
+
// AFM's kerning into the MEASUREMENT while the OUTPUT ignores it made every kerned string draw
|
|
827
|
+
// wider than its box: "AVATAR Wave" at 40pt by 19pt, "Total" at 11pt by 5.7%. Measured must equal
|
|
828
|
+
// drawn. The AFM kerning pairs stay parsed (`getKerning`) for the day we emit `TJ` - see the
|
|
829
|
+
// "real kerning" item in todo.md, which must cover embedded fonts too (`kern`/`GPOS`).
|
|
830
|
+
for (const char of text) {
|
|
831
|
+
width += this.getCharWidth(char, fontSize, undefined, fontFamily, fontStyle);
|
|
711
832
|
}
|
|
712
833
|
return width;
|
|
713
834
|
}
|
|
@@ -767,13 +888,6 @@ endstream`;
|
|
|
767
888
|
return (advanceWidth / 1000) * fontSize;
|
|
768
889
|
}
|
|
769
890
|
// Method to get the kerning, if available, between two signs
|
|
770
|
-
getKerning(char, nextChar, fullFontName, fontName, fontStyle) {
|
|
771
|
-
// TrueType kerning (the kern/GPOS tables) is not wired up yet - no kerning for custom fonts.
|
|
772
|
-
if (this.getCustomFont(fullFontName ?? fontName, fontStyle))
|
|
773
|
-
return 0;
|
|
774
|
-
const currentParser = this.getAVMParserByFont(fullFontName, fontName, fontStyle);
|
|
775
|
-
return currentParser.parser.getKerning(char, nextChar) / 1000;
|
|
776
|
-
}
|
|
777
891
|
// Returns all fonts
|
|
778
892
|
getAllFontsRaw() {
|
|
779
893
|
return this.fonts.getAllFonts();
|