@jasy/pdf 1.0.0-beta.2 → 1.0.0-beta.3
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.
|
@@ -130,6 +130,14 @@ export declare class TextElement extends SizedPDFElement implements Fragmentable
|
|
|
130
130
|
private readonly role?;
|
|
131
131
|
constructor({ fontSize, content, fontFamily, fontStyle, color, textAlignment, maxLines, orphans, widows, overflow, lineHeight, underline, strikethrough, skipInk, letterSpacing, direction, fontFallback, textTransform, wordSpacing, textIndent, breakWord, hyphenate, ligatures, role, }: TextElementParams);
|
|
132
132
|
private resolveStyle;
|
|
133
|
+
/**
|
|
134
|
+
* The last `display(metrics)` result, with what it depended on - recasing, coverage and the fallback
|
|
135
|
+
* split each walk the whole text, and every measure, break and draw asks again.
|
|
136
|
+
*
|
|
137
|
+
* COMPARED, not cleared in `resolveStyle`: that runs on every pass and resolves to the same values,
|
|
138
|
+
* so clearing would drop the answer exactly when it is about to be reused.
|
|
139
|
+
*/
|
|
140
|
+
private displayed?;
|
|
133
141
|
/** Justified lines may be squeezed to keep a word; every other alignment gets no slack. Read by the
|
|
134
142
|
* breaker in BOTH passes, or a measured line and a drawn one would disagree about where it ends. */
|
|
135
143
|
/**
|
|
@@ -73,14 +73,37 @@ export class TextElement extends SizedPDFElement {
|
|
|
73
73
|
* same width in most fonts).
|
|
74
74
|
*/
|
|
75
75
|
display(metrics) {
|
|
76
|
+
const memo = this.displayed;
|
|
77
|
+
if (metrics &&
|
|
78
|
+
memo &&
|
|
79
|
+
memo.metrics === metrics &&
|
|
80
|
+
// A face registered since then changes what is drawable and how the fallback stack splits.
|
|
81
|
+
memo.epoch === (metrics.fontEpoch ?? 0) &&
|
|
82
|
+
memo.family === this.fontFamily &&
|
|
83
|
+
memo.style === this.fontStyle &&
|
|
84
|
+
memo.transform === this.textTransform &&
|
|
85
|
+
memo.fallback === this.fontFallback)
|
|
86
|
+
return memo.value;
|
|
76
87
|
const cased = this.recased();
|
|
77
88
|
// A SPAN may bring its own stack, so the element's being empty is not enough to skip the pass.
|
|
78
89
|
const anyStack = this.fontFallback.length > 0 ||
|
|
79
90
|
(typeof cased !== "string" && cased.some((seg) => (seg.fontFallback?.length ?? 0) > 0));
|
|
80
91
|
if (!metrics)
|
|
81
92
|
return cased;
|
|
93
|
+
const remember = (value) => {
|
|
94
|
+
this.displayed = {
|
|
95
|
+
metrics,
|
|
96
|
+
epoch: metrics.fontEpoch ?? 0,
|
|
97
|
+
family: this.fontFamily,
|
|
98
|
+
style: this.fontStyle,
|
|
99
|
+
transform: this.textTransform,
|
|
100
|
+
fallback: this.fontFallback,
|
|
101
|
+
value,
|
|
102
|
+
};
|
|
103
|
+
return value;
|
|
104
|
+
};
|
|
82
105
|
if (!anyStack)
|
|
83
|
-
return this.covered(cased, metrics);
|
|
106
|
+
return remember(this.covered(cased, metrics));
|
|
84
107
|
// Font fallback turns the content into spans - one per family - which every later pass already
|
|
85
108
|
// knows how to handle. Nothing new downstream.
|
|
86
109
|
const pieces = typeof cased === "string" ? [{ content: cased }] : cased;
|
|
@@ -93,7 +116,7 @@ export class TextElement extends SizedPDFElement {
|
|
|
93
116
|
for (const run of runs)
|
|
94
117
|
out.push({ ...seg, content: run.text, fontFamily: run.fontFamily });
|
|
95
118
|
}
|
|
96
|
-
return this.covered(out, metrics);
|
|
119
|
+
return remember(this.covered(out, metrics));
|
|
97
120
|
}
|
|
98
121
|
/**
|
|
99
122
|
* Drops or substitutes whatever the RESOLVED font cannot draw. Runs last, after the fallback stack
|
package/dist/text/advance.js
CHANGED
|
@@ -39,6 +39,12 @@ export function codePointCount(text) {
|
|
|
39
39
|
* is thrown away when it flips; the same document can be rendered twice with different options.
|
|
40
40
|
*/
|
|
41
41
|
const ADVANCE_CACHE = new WeakMap();
|
|
42
|
+
/**
|
|
43
|
+
* The innermost map a `RunFont` resolves to, remembered on the font object itself. The breaker builds
|
|
44
|
+
* one font per run and asks for every word with it, so without this each word walks five maps of which
|
|
45
|
+
* four hold a single entry.
|
|
46
|
+
*/
|
|
47
|
+
const BY_FONT = new WeakMap();
|
|
42
48
|
function baseAdvance(metrics, text, font) {
|
|
43
49
|
let entry = ADVANCE_CACHE.get(metrics);
|
|
44
50
|
const epoch = metrics.fontEpoch ?? 0;
|
|
@@ -49,6 +55,20 @@ function baseAdvance(metrics, text, font) {
|
|
|
49
55
|
entry = { epoch, kerning, byFamily: new Map() };
|
|
50
56
|
ADVANCE_CACHE.set(metrics, entry);
|
|
51
57
|
}
|
|
58
|
+
// Same font object as last time, and the metrics entry has not been replaced: the map is already known.
|
|
59
|
+
// Its FIELDS are compared, not just its identity: a caller that reuses one object and changes the
|
|
60
|
+
// size on it would otherwise be handed the previous size's map.
|
|
61
|
+
const known = BY_FONT.get(font);
|
|
62
|
+
if (known?.entry === entry &&
|
|
63
|
+
known.family === font.fontFamily &&
|
|
64
|
+
known.size === font.fontSize &&
|
|
65
|
+
known.style === font.fontStyle &&
|
|
66
|
+
known.ligatures === font.ligatures) {
|
|
67
|
+
const seen = known.byText.get(text);
|
|
68
|
+
if (seen !== undefined)
|
|
69
|
+
return seen;
|
|
70
|
+
return measureAndStore(metrics, text, font, known.byText);
|
|
71
|
+
}
|
|
52
72
|
let byStyle = entry.byFamily.get(font.fontFamily);
|
|
53
73
|
if (!byStyle)
|
|
54
74
|
entry.byFamily.set(font.fontFamily, (byStyle = new Map()));
|
|
@@ -61,9 +81,21 @@ function baseAdvance(metrics, text, font) {
|
|
|
61
81
|
let byText = bySize.get(font.fontSize);
|
|
62
82
|
if (!byText)
|
|
63
83
|
bySize.set(font.fontSize, (byText = new Map()));
|
|
84
|
+
BY_FONT.set(font, {
|
|
85
|
+
entry,
|
|
86
|
+
family: font.fontFamily,
|
|
87
|
+
size: font.fontSize,
|
|
88
|
+
style: font.fontStyle,
|
|
89
|
+
ligatures: font.ligatures,
|
|
90
|
+
byText,
|
|
91
|
+
});
|
|
64
92
|
const hit = byText.get(text);
|
|
65
93
|
if (hit !== undefined)
|
|
66
94
|
return hit;
|
|
95
|
+
return measureAndStore(metrics, text, font, byText);
|
|
96
|
+
}
|
|
97
|
+
/** The measurement itself, once a miss has found the map it belongs in. */
|
|
98
|
+
function measureAndStore(metrics, text, font, byText) {
|
|
67
99
|
let advance = metrics.getStringWidth(text, font.fontFamily, font.fontSize, font.fontStyle, font.ligatures);
|
|
68
100
|
if (metrics.kerningEnabled) {
|
|
69
101
|
let units = 0;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FontVerticals } from "../text/line-metrics.js";
|
|
2
2
|
import type { FontDecoration } from "../text/text-decoration.js";
|
|
3
|
+
export declare function standardParser(fullName: string, data: string): AFMParser;
|
|
3
4
|
export declare class AFMParser {
|
|
4
5
|
private advanceWidths;
|
|
5
6
|
private kerningPairs;
|
package/dist/utils/afm-parser.js
CHANGED
|
@@ -13,6 +13,15 @@ function glyphList() {
|
|
|
13
13
|
}
|
|
14
14
|
return (sharedGlyphMap = map);
|
|
15
15
|
}
|
|
16
|
+
/** One parser per standard-14 face for the whole process: the AFM data is a constant and a parsed
|
|
17
|
+
* face is read-only, so every document can share one. */
|
|
18
|
+
const STANDARD_PARSERS = new Map();
|
|
19
|
+
export function standardParser(fullName, data) {
|
|
20
|
+
let parser = STANDARD_PARSERS.get(fullName);
|
|
21
|
+
if (!parser)
|
|
22
|
+
STANDARD_PARSERS.set(fullName, (parser = new AFMParser(data)));
|
|
23
|
+
return parser;
|
|
24
|
+
}
|
|
16
25
|
export class AFMParser {
|
|
17
26
|
constructor(afmData) {
|
|
18
27
|
this.advanceWidths = {};
|
|
@@ -3,7 +3,7 @@ import { STANDARD_AFM } from "../assets/font-data.js";
|
|
|
3
3
|
import { md5, md5Hex } from "./md5.js";
|
|
4
4
|
import { zlibSync } from "fflate";
|
|
5
5
|
import { bytesFromLatin1, latin1FromBytes } from "./bytes.js";
|
|
6
|
-
import {
|
|
6
|
+
import { standardParser } from "./afm-parser.js";
|
|
7
7
|
import { mergeSpans, TTFParser } from "./ttf-parser.js";
|
|
8
8
|
import { WoffError, isWoff, woffToSfnt } from "./woff.js";
|
|
9
9
|
import { isWoff2 } from "./woff2.js";
|
|
@@ -577,7 +577,7 @@ endstream`;
|
|
|
577
577
|
fontName,
|
|
578
578
|
fontStyle,
|
|
579
579
|
fullFontName: fullName,
|
|
580
|
-
parser:
|
|
580
|
+
parser: standardParser(fullName, data),
|
|
581
581
|
});
|
|
582
582
|
}
|
|
583
583
|
const resourceNumber = this.objects.length + 1; // New resource number
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jasy/pdf",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.3",
|
|
4
4
|
"description": "Declarative, component-based PDF generation in pure TypeScript - Flutter-style components, real AFM font metrics, a hand-rolled PDF writer. No headless browser, no Java.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"declarative",
|