@jasy/pdf 1.0.0-beta.1 → 1.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/text/advance.js +49 -7
- package/dist/utils/afm-parser.d.ts +0 -1
- package/dist/utils/afm-parser.js +15 -16
- package/dist/utils/font-metrics.d.ts +3 -0
- package/dist/utils/pdf-object-manager.d.ts +6 -0
- package/dist/utils/pdf-object-manager.js +28 -6
- package/dist/utils/utf8-to-windows1252-encoder.js +5 -3
- package/package.json +1 -1
package/dist/text/advance.js
CHANGED
|
@@ -26,21 +26,63 @@ export function codePointCount(text) {
|
|
|
26
26
|
n++;
|
|
27
27
|
return n;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Glyph widths plus kerning, memoised per metrics object - the line breaker asks for the same run
|
|
31
|
+
* many times over.
|
|
32
|
+
*
|
|
33
|
+
* Keyed by nested maps on primitives, never a composed string key: building one allocates per call,
|
|
34
|
+
* which costs more than the lookup saves.
|
|
35
|
+
*
|
|
36
|
+
* `letterSpacing` is not in the key: it is added outside, so a spaced and an unspaced run share the
|
|
37
|
+
* width. Font size IS in it - scaling a cached em width drifts in the last bit, which is enough to
|
|
38
|
+
* move a line break. Kerning is a flag on the metrics rather than a key, so the entry records it and
|
|
39
|
+
* is thrown away when it flips; the same document can be rendered twice with different options.
|
|
40
|
+
*/
|
|
41
|
+
const ADVANCE_CACHE = new WeakMap();
|
|
42
|
+
function baseAdvance(metrics, text, font) {
|
|
43
|
+
let entry = ADVANCE_CACHE.get(metrics);
|
|
44
|
+
const epoch = metrics.fontEpoch ?? 0;
|
|
45
|
+
const kerning = metrics.kerningEnabled;
|
|
46
|
+
// A face registered since we measured makes a family name mean something else; a flipped kerning
|
|
47
|
+
// flag makes every width wrong. Either way the answers are stale.
|
|
48
|
+
if (!entry || entry.epoch !== epoch || entry.kerning !== kerning) {
|
|
49
|
+
entry = { epoch, kerning, byFamily: new Map() };
|
|
50
|
+
ADVANCE_CACHE.set(metrics, entry);
|
|
51
|
+
}
|
|
52
|
+
let byStyle = entry.byFamily.get(font.fontFamily);
|
|
53
|
+
if (!byStyle)
|
|
54
|
+
entry.byFamily.set(font.fontFamily, (byStyle = new Map()));
|
|
55
|
+
let byLigatures = byStyle.get(font.fontStyle);
|
|
56
|
+
if (!byLigatures)
|
|
57
|
+
byStyle.set(font.fontStyle, (byLigatures = new Map()));
|
|
58
|
+
let bySize = byLigatures.get(font.ligatures);
|
|
59
|
+
if (!bySize)
|
|
60
|
+
byLigatures.set(font.ligatures, (bySize = new Map()));
|
|
61
|
+
let byText = bySize.get(font.fontSize);
|
|
62
|
+
if (!byText)
|
|
63
|
+
bySize.set(font.fontSize, (byText = new Map()));
|
|
64
|
+
const hit = byText.get(text);
|
|
65
|
+
if (hit !== undefined)
|
|
66
|
+
return hit;
|
|
67
|
+
let advance = metrics.getStringWidth(text, font.fontFamily, font.fontSize, font.fontStyle, font.ligatures);
|
|
68
|
+
if (metrics.kerningEnabled) {
|
|
69
|
+
let units = 0;
|
|
70
|
+
for (const k of metrics.getKernPairs(text, font.fontFamily, font.fontStyle, font.ligatures))
|
|
71
|
+
units += k;
|
|
72
|
+
advance += (units / 1000) * font.fontSize; // kern units are em/1000
|
|
73
|
+
}
|
|
74
|
+
byText.set(text, advance);
|
|
75
|
+
return advance;
|
|
76
|
+
}
|
|
29
77
|
/** The advance of `text` in points: the plain glyph widths, plus kerning (if the document has it on),
|
|
30
78
|
* plus one `letterSpacing` per code point. */
|
|
31
79
|
export function runAdvance(metrics, text, font, letterSpacing = 0) {
|
|
32
|
-
let advance = metrics
|
|
80
|
+
let advance = baseAdvance(metrics, text, font);
|
|
33
81
|
if (letterSpacing !== 0) {
|
|
34
82
|
// Per DRAWN glyph, which is what `Tc` applies to; only a ligature makes that differ.
|
|
35
83
|
const glyphs = metrics.shapedGlyphCount?.(text, font.fontFamily, font.fontStyle, font.ligatures) ??
|
|
36
84
|
codePointCount(text);
|
|
37
85
|
advance += glyphs * letterSpacing;
|
|
38
86
|
}
|
|
39
|
-
if (metrics.kerningEnabled) {
|
|
40
|
-
let units = 0;
|
|
41
|
-
for (const k of metrics.getKernPairs(text, font.fontFamily, font.fontStyle, font.ligatures))
|
|
42
|
-
units += k;
|
|
43
|
-
advance += (units / 1000) * font.fontSize; // kern units are em/1000
|
|
44
|
-
}
|
|
45
87
|
return advance;
|
|
46
88
|
}
|
package/dist/utils/afm-parser.js
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { AGL } from "../assets/font-data.js";
|
|
2
|
+
/** The Adobe Glyph List: code point -> glyph name. Constant, so it is parsed once per process and
|
|
3
|
+
* shared by every face rather than rebuilt per `AFMParser`. */
|
|
4
|
+
let sharedGlyphMap;
|
|
5
|
+
function glyphList() {
|
|
6
|
+
if (sharedGlyphMap)
|
|
7
|
+
return sharedGlyphMap;
|
|
8
|
+
const map = {};
|
|
9
|
+
for (const line of AGL.split("\n")) {
|
|
10
|
+
const parts = line.trim().split(";");
|
|
11
|
+
if (parts.length >= 2)
|
|
12
|
+
map[String.fromCharCode(parseInt(parts[0], 16))] = parts[1];
|
|
13
|
+
}
|
|
14
|
+
return (sharedGlyphMap = map);
|
|
15
|
+
}
|
|
2
16
|
export class AFMParser {
|
|
3
17
|
constructor(afmData) {
|
|
4
18
|
this.advanceWidths = {};
|
|
5
19
|
this.kerningPairs = {};
|
|
6
|
-
this.glyphMap =
|
|
20
|
+
this.glyphMap = glyphList();
|
|
7
21
|
// The font's bounding box in glyph space (1000 units / em), from the AFM header. This - not the
|
|
8
22
|
// `Ascender` line - is what a line box is built from; see `verticals()`.
|
|
9
23
|
this.bbox = [0, 0, 0, 0];
|
|
@@ -14,21 +28,6 @@ export class AFMParser {
|
|
|
14
28
|
this.capHeight = 0;
|
|
15
29
|
this.xHeight = 0;
|
|
16
30
|
this.parseAFMData(afmData);
|
|
17
|
-
this.loadGlyphList();
|
|
18
|
-
}
|
|
19
|
-
loadGlyphList() {
|
|
20
|
-
const lines = AGL.split("\n");
|
|
21
|
-
for (const line of lines) {
|
|
22
|
-
const parts = line.trim().split(";");
|
|
23
|
-
if (parts.length >= 2) {
|
|
24
|
-
const unicodeHex = parts[0];
|
|
25
|
-
const glyphName = parts[1];
|
|
26
|
-
// Converts the Unicode hex code into the corresponding character
|
|
27
|
-
const unicodeChar = String.fromCharCode(parseInt(unicodeHex, 16));
|
|
28
|
-
// Adds the character as the key and the glyph name as the value in the list
|
|
29
|
-
this.glyphMap[unicodeChar] = glyphName;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
31
|
}
|
|
33
32
|
getGlyphName(char) {
|
|
34
33
|
return this.glyphMap[char] || char;
|
|
@@ -20,6 +20,9 @@ export interface FontMetrics {
|
|
|
20
20
|
/** Whether kerning is on for this document. `runAdvance` reads it so a measured advance matches the
|
|
21
21
|
* drawn one (the backend emits a `TJ` under the same flag). */
|
|
22
22
|
readonly kerningEnabled: boolean;
|
|
23
|
+
/** Bumped whenever a face is registered, so a memoised measurement knows to drop it: the same family
|
|
24
|
+
* name can mean a different face afterwards. Optional - leaving it out means never invalidating. */
|
|
25
|
+
readonly fontEpoch?: number;
|
|
23
26
|
/** Per-adjacent-pair kerning of `text`, in em/1000 (negative tightens); length `codePoints - 1`,
|
|
24
27
|
* zero next to a space. Only meaningful when `kerningEnabled`. */
|
|
25
28
|
getKernPairs(text: string, fontFamily: string, fontStyle: FontStyle, ligatures?: boolean): number[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OverflowPolicy } from "../layout/fragmentation.js";
|
|
2
2
|
import { TTFParser } from "./ttf-parser.js";
|
|
3
3
|
import { type ShapedGlyph } from "../text/shape.js";
|
|
4
|
+
export declare function featureKey(f: readonly string[]): string;
|
|
4
5
|
import type { SecurityHandler } from "../crypto/security-handler.js";
|
|
5
6
|
import type { Gradient } from "../ir/display-list.js";
|
|
6
7
|
import type { FontVerticals } from "../text/line-metrics.js";
|
|
@@ -40,7 +41,12 @@ export declare class PDFObjectManager implements FontMetrics {
|
|
|
40
41
|
private verticalsCache;
|
|
41
42
|
private decorationCache;
|
|
42
43
|
private customFontEmit;
|
|
44
|
+
/** Shaped runs, cached face -> feature set -> text. Nested, not one map under a composed key: that
|
|
45
|
+
* key would carry the whole run's text, so every lookup would copy it into a fresh string. */
|
|
43
46
|
private shapeCache;
|
|
47
|
+
/** Bumped by every font registration; `runAdvance` watches it to drop memoised widths, since a
|
|
48
|
+
* family name can mean a different face afterwards. */
|
|
49
|
+
fontEpoch: number;
|
|
44
50
|
private shapedOrigins;
|
|
45
51
|
private attachments;
|
|
46
52
|
private xmpMetadata?;
|
|
@@ -13,8 +13,18 @@ import { shapeRun } from "../text/shape.js";
|
|
|
13
13
|
* drew for it. On by default like kerning - CSS, browsers and every other renderer do the same,
|
|
14
14
|
* and nobody should have to know the word to get text that is set properly. */
|
|
15
15
|
const LATIN_FEATURES = ["liga"];
|
|
16
|
-
/**
|
|
17
|
-
const
|
|
16
|
+
/** Stable, so `features(false)` does not allocate per call. */
|
|
17
|
+
const NO_FEATURES = [];
|
|
18
|
+
/** A feature set's cache key. Memoised only for the two arrays this module owns - a caller's array is
|
|
19
|
+
* joined every time, since holding on to its key would outlive a mutation of it. */
|
|
20
|
+
const OWN_FEATURE_KEYS = new Map([
|
|
21
|
+
[LATIN_FEATURES, LATIN_FEATURES.join(",")],
|
|
22
|
+
[NO_FEATURES, ""],
|
|
23
|
+
]);
|
|
24
|
+
export function featureKey(f) {
|
|
25
|
+
return OWN_FEATURE_KEYS.get(f) ?? f.join(",");
|
|
26
|
+
}
|
|
27
|
+
const features = (ligatures) => ligatures ? LATIN_FEATURES : NO_FEATURES;
|
|
18
28
|
import { getArrayBuffer, isWindows1252 } from "./utf8-to-windows1252-encoder.js";
|
|
19
29
|
import { isEmojiCodePoint } from "../text/emoji-codepoints.js";
|
|
20
30
|
import { StructTree } from "./struct-tree.js";
|
|
@@ -171,7 +181,12 @@ export class PDFObjectManager {
|
|
|
171
181
|
// program is subsetted and the objects filled in by finalizeCustomFonts(), after the render pass.
|
|
172
182
|
this.customFontEmit = new Map();
|
|
173
183
|
// Shaped runs, keyed by font + text; `null` means "asked, and there was nothing to shape".
|
|
184
|
+
/** Shaped runs, cached face -> feature set -> text. Nested, not one map under a composed key: that
|
|
185
|
+
* key would carry the whole run's text, so every lookup would copy it into a fresh string. */
|
|
174
186
|
this.shapeCache = new Map();
|
|
187
|
+
/** Bumped by every font registration; `runAdvance` watches it to drop memoised widths, since a
|
|
188
|
+
* family name can mean a different face afterwards. */
|
|
189
|
+
this.fontEpoch = 0;
|
|
175
190
|
// Per font, glyph -> the code points it stands for. A shaped glyph is often absent from the cmap (a
|
|
176
191
|
// ligature) or maps back to a presentation form, so `ToUnicode` cannot come from the cmap alone.
|
|
177
192
|
this.shapedOrigins = new Map();
|
|
@@ -550,6 +565,7 @@ endstream`;
|
|
|
550
565
|
}
|
|
551
566
|
// Registers a font
|
|
552
567
|
registerFont(fontName, fontStyle = FontStyle.Normal, fullName = fontName) {
|
|
568
|
+
this.fontEpoch++;
|
|
553
569
|
if (this.fonts.hasFont(fontName, fontStyle)) {
|
|
554
570
|
return this.fonts.getFont(fontName, fontStyle); // Already exists? Return it!
|
|
555
571
|
}
|
|
@@ -580,6 +596,7 @@ endstream`;
|
|
|
580
596
|
// metrics and emits its PDF font objects. All variants share the family `name`; bold/italic are
|
|
581
597
|
// separate .ttf files registered under the same name with a different style.
|
|
582
598
|
registerCustomFont(name, data, style = FontStyle.Normal) {
|
|
599
|
+
this.fontEpoch++;
|
|
583
600
|
let byStyle = this.customFonts.get(name);
|
|
584
601
|
if (!byStyle) {
|
|
585
602
|
byStyle = new Map();
|
|
@@ -767,13 +784,18 @@ endstream`;
|
|
|
767
784
|
const resolved = this.resolveCustomStyle(fontFamily, fontStyle);
|
|
768
785
|
if (!resolved)
|
|
769
786
|
return undefined;
|
|
770
|
-
const
|
|
771
|
-
|
|
787
|
+
const ttf = this.customFonts.get(fontFamily).get(resolved);
|
|
788
|
+
let byFeature = this.shapeCache.get(ttf);
|
|
789
|
+
if (!byFeature)
|
|
790
|
+
this.shapeCache.set(ttf, (byFeature = new Map()));
|
|
791
|
+
let byText = byFeature.get(featureKey(features));
|
|
792
|
+
if (!byText)
|
|
793
|
+
byFeature.set(featureKey(features), (byText = new Map()));
|
|
794
|
+
const cached = byText.get(text);
|
|
772
795
|
if (cached !== undefined)
|
|
773
796
|
return cached ?? undefined;
|
|
774
|
-
const ttf = this.customFonts.get(fontFamily).get(resolved);
|
|
775
797
|
const shaped = shapeRun([...text].map((c) => c.codePointAt(0)), ttf, features) ?? null;
|
|
776
|
-
|
|
798
|
+
byText.set(text, shaped);
|
|
777
799
|
if (shaped) {
|
|
778
800
|
// Recorded at shaping time - the only moment the glyph and its code points are known together.
|
|
779
801
|
const fontKey = this.customKey(fontFamily, resolved);
|
|
@@ -39,9 +39,11 @@ const CP1252_FROM_UNICODE = {
|
|
|
39
39
|
* except the C1 range, which the table above fills with printable punctuation instead.
|
|
40
40
|
*/
|
|
41
41
|
export function isWindows1252(codePoint) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
// Latin-1 first: every key in the table above is > 0xff, so this range can never be in it. Order
|
|
43
|
+
// matters - this runs per character of every measured string, and the object lookup is the slow half.
|
|
44
|
+
if (codePoint <= 0xff)
|
|
45
|
+
return codePoint < 0x80 || codePoint > 0x9f;
|
|
46
|
+
return codePoint in CP1252_FROM_UNICODE;
|
|
45
47
|
}
|
|
46
48
|
/** Encodes a JavaScript string to a Windows-1252 byte buffer (the PDF text encoding). */
|
|
47
49
|
export function getArrayBuffer(data) {
|
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.2",
|
|
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",
|