@doccloak/core 0.12.1 → 0.13.0
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/CHANGELOG.md +333 -0
- package/NOTICE +5 -0
- package/README.md +131 -9
- package/dist/dom/errors.d.ts +1 -1
- package/dist/dom/ocr.d.ts +33 -7
- package/dist/dom/ocr.js +98 -4
- package/dist/dom/office.d.ts +20 -6
- package/dist/dom/office.js +44 -11
- package/dist/engine.js +4 -5
- package/dist/env.d.ts +3 -3
- package/dist/pdf/cmap.d.ts +83 -0
- package/dist/pdf/cmap.js +659 -0
- package/dist/pdf/encodings.d.ts +49 -0
- package/dist/pdf/encodings.js +181 -0
- package/dist/pdf/extract.d.ts +106 -0
- package/dist/pdf/extract.js +1064 -0
- package/dist/pdf/fallback-fonts.d.ts +146 -0
- package/dist/pdf/fallback-fonts.js +312 -0
- package/dist/pdf/fit.d.ts +35 -0
- package/dist/pdf/fit.js +60 -0
- package/dist/pdf/fonts.d.ts +82 -0
- package/dist/pdf/fonts.js +465 -0
- package/dist/pdf/glyphlist.d.ts +37 -0
- package/dist/pdf/glyphlist.js +485 -0
- package/dist/pdf/index.d.ts +21 -0
- package/dist/pdf/index.js +7 -0
- package/dist/pdf/lexer.d.ts +61 -0
- package/dist/pdf/lexer.js +554 -0
- package/dist/pdf/objects.d.ts +57 -0
- package/dist/pdf/objects.js +217 -0
- package/dist/pdf/oracle.d.ts +149 -0
- package/dist/pdf/oracle.js +384 -0
- package/dist/pdf/raster.d.ts +45 -0
- package/dist/pdf/raster.js +89 -0
- package/dist/pdf/read.d.ts +36 -0
- package/dist/pdf/read.js +65 -0
- package/dist/pdf/serializer.d.ts +21 -0
- package/dist/pdf/serializer.js +106 -0
- package/dist/pdf/standard-metrics.d.ts +27 -0
- package/dist/pdf/standard-metrics.js +72 -0
- package/dist/pdf/surgery.d.ts +131 -0
- package/dist/pdf/surgery.js +847 -0
- package/dist/pdf/textstate.d.ts +77 -0
- package/dist/pdf/textstate.js +280 -0
- package/dist/pdf/types.d.ts +191 -0
- package/dist/pdf/types.js +11 -0
- package/dist/pdf/verify.d.ts +33 -0
- package/dist/pdf/verify.js +446 -0
- package/dist/pdf/write.d.ts +52 -0
- package/dist/pdf/write.js +644 -0
- package/dist/pipeline.d.ts +15 -0
- package/dist/pipeline.js +123 -8
- package/dist/providers/bardsai.d.ts +1 -1
- package/dist/providers/bardsai.js +5 -1
- package/dist/providers/gliner-base.d.ts +1 -1
- package/dist/providers/gliner-base.js +5 -1
- package/dist/providers/gliner.d.ts +1 -1
- package/dist/providers/gliner.js +5 -1
- package/dist/regex/rules.data.js +13 -8
- package/dist/types.d.ts +17 -2
- package/dist/types.js +17 -0
- package/dist/worker-protocol.d.ts +11 -0
- package/dist/worker-protocol.js +41 -8
- package/fonts/liberation/LICENSE +102 -0
- package/fonts/liberation/LiberationMono-Bold.ttf +0 -0
- package/fonts/liberation/LiberationMono-BoldItalic.ttf +0 -0
- package/fonts/liberation/LiberationMono-Italic.ttf +0 -0
- package/fonts/liberation/LiberationMono-Regular.ttf +0 -0
- package/fonts/liberation/LiberationSans-Bold.ttf +0 -0
- package/fonts/liberation/LiberationSans-BoldItalic.ttf +0 -0
- package/fonts/liberation/LiberationSans-Italic.ttf +0 -0
- package/fonts/liberation/LiberationSans-Regular.ttf +0 -0
- package/fonts/liberation/LiberationSerif-Bold.ttf +0 -0
- package/fonts/liberation/LiberationSerif-BoldItalic.ttf +0 -0
- package/fonts/liberation/LiberationSerif-Italic.ttf +0 -0
- package/fonts/liberation/LiberationSerif-Regular.ttf +0 -0
- package/package.json +11 -2
- package/rules/pl.json +7 -3
- package/rules/universal.json +6 -5
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @doccloak/core/pdf - fallback fonts (T211).
|
|
3
|
+
*
|
|
4
|
+
* When the document's own font cannot show a placeholder such as
|
|
5
|
+
* "[PERSON_1]", the writer draws it in the closest Liberation face.
|
|
6
|
+
* Liberation Sans / Serif / Mono are metric-compatible with Arial /
|
|
7
|
+
* Times New Roman / Courier New, so a placeholder set in the fallback keeps
|
|
8
|
+
* the line geometry of the original text in the common case.
|
|
9
|
+
*
|
|
10
|
+
* Three pieces:
|
|
11
|
+
* - `styleFromFontName` + `faceFor` turn a font dictionary (BaseFont name,
|
|
12
|
+
* /Flags, /ItalicAngle, /StemV, /FontWeight) into one of the 12 faces.
|
|
13
|
+
* - `FallbackFontProvider` loads a face's TTF bytes through the host
|
|
14
|
+
* (`PdfAssetPaths.loadFont` or `fontsUrl` + fetch), parses it with fontkit
|
|
15
|
+
* and offers kerning-free width measurement and glyph coverage checks.
|
|
16
|
+
* - `FallbackFontEmbedder` embeds a face once per output `PDFDocument` as a
|
|
17
|
+
* subset Type0 / Identity-H font and encodes placeholder text into the
|
|
18
|
+
* 2-byte glyph ids the content stream needs.
|
|
19
|
+
*
|
|
20
|
+
* No `fs` here: this file runs in browsers and Node alike.
|
|
21
|
+
*/
|
|
22
|
+
import type { Font as FontkitFont } from '@cantoo/fontkit';
|
|
23
|
+
import type { PDFDocument, PDFFont, PDFRef } from '@cantoo/pdf-lib';
|
|
24
|
+
import type { FontStyle, PdfAssetPaths } from './types.js';
|
|
25
|
+
export type FallbackFace = 'LiberationSans-Regular' | 'LiberationSans-Bold' | 'LiberationSans-Italic' | 'LiberationSans-BoldItalic' | 'LiberationSerif-Regular' | 'LiberationSerif-Bold' | 'LiberationSerif-Italic' | 'LiberationSerif-BoldItalic' | 'LiberationMono-Regular' | 'LiberationMono-Bold' | 'LiberationMono-Italic' | 'LiberationMono-BoldItalic';
|
|
26
|
+
/** Every face, in a stable order (used for subset tags). */
|
|
27
|
+
export declare const FALLBACK_FACES: readonly FallbackFace[];
|
|
28
|
+
/** Picks the Liberation face matching a style descriptor. */
|
|
29
|
+
export declare function faceFor(style: FontStyle): FallbackFace;
|
|
30
|
+
/** File name of a face inside the fonts directory (`${fontsUrl}${fileName}`). */
|
|
31
|
+
export declare function faceFileName(face: FallbackFace): string;
|
|
32
|
+
/** PDF font descriptor /Flags bits (ISO 32000-1 table 123), as bit masks. */
|
|
33
|
+
export declare const FONT_FLAG_FIXED_PITCH: number;
|
|
34
|
+
export declare const FONT_FLAG_SERIF: number;
|
|
35
|
+
export declare const FONT_FLAG_ITALIC: number;
|
|
36
|
+
export declare const FONT_FLAG_FORCE_BOLD: number;
|
|
37
|
+
/**
|
|
38
|
+
* Derives family / bold / italic from a font's BaseFont name and, when the
|
|
39
|
+
* name is silent, its font descriptor facts.
|
|
40
|
+
*
|
|
41
|
+
* - family: mono when the name says so or /Flags has FixedPitch; sans when the
|
|
42
|
+
* name is a known sans face; serif when the name says so, else the /Flags
|
|
43
|
+
* Serif bit decides; sans by default.
|
|
44
|
+
* - bold: name says so (Bold/Black/Heavy/Semibold/Demi), else /FontWeight >= 600,
|
|
45
|
+
* /StemV >= 120 or the ForceBold flag when the name gives no weight at all.
|
|
46
|
+
* - italic: name says so (Italic/Oblique/-It), /Flags Italic bit, or /ItalicAngle != 0.
|
|
47
|
+
*/
|
|
48
|
+
export declare function styleFromFontName(baseFont: string, flags?: number, italicAngle?: number, stemV?: number, fontWeight?: number): FontStyle;
|
|
49
|
+
/**
|
|
50
|
+
* OpenType features used for every layout call, in the provider and in the
|
|
51
|
+
* embedder alike: kerning and ligatures off, so a placeholder is one glyph per
|
|
52
|
+
* character and its width is the plain sum of glyph advances - exactly what a
|
|
53
|
+
* Tj with Identity-H encoding renders from the embedded /W array.
|
|
54
|
+
*/
|
|
55
|
+
export declare const FALLBACK_LAYOUT_FEATURES: Record<string, boolean>;
|
|
56
|
+
export interface LoadedFallbackFont {
|
|
57
|
+
face: FallbackFace;
|
|
58
|
+
/** The complete TTF as loaded (what the embedder subsets). */
|
|
59
|
+
bytes: Uint8Array;
|
|
60
|
+
/** Advance width of `text` at `fontSize`, in text space units (points at Tz 100). Kerning is not applied. */
|
|
61
|
+
measure(text: string, fontSize: number): number;
|
|
62
|
+
/** True when every code point of `text` has a glyph in this face. */
|
|
63
|
+
canShow(text: string): boolean;
|
|
64
|
+
/** Typographic ascent, in 1/1000 em. */
|
|
65
|
+
ascent: number;
|
|
66
|
+
/** Typographic descent (negative), in 1/1000 em. */
|
|
67
|
+
descent: number;
|
|
68
|
+
/** Cap height, in 1/1000 em. */
|
|
69
|
+
capHeight: number;
|
|
70
|
+
/** x-height, in 1/1000 em. */
|
|
71
|
+
xHeight: number;
|
|
72
|
+
/** Font units per em of the TTF (2048 for Liberation). */
|
|
73
|
+
unitsPerEm: number;
|
|
74
|
+
}
|
|
75
|
+
/** Width helper shared by the provider and the embedder: sum of glyph advances at `size`. */
|
|
76
|
+
export declare function measureWithFont(font: FontkitFont, text: string, fontSize: number): number;
|
|
77
|
+
/**
|
|
78
|
+
* Loads and caches the Liberation faces. Bytes come from `assets.loadFont`
|
|
79
|
+
* when the host provides it (Node tests, Electron), else from
|
|
80
|
+
* `${assets.fontsUrl}${face}.ttf` through the global `fetch`.
|
|
81
|
+
*/
|
|
82
|
+
export declare class FallbackFontProvider {
|
|
83
|
+
private readonly assets;
|
|
84
|
+
private readonly cache;
|
|
85
|
+
constructor(assets?: PdfAssetPaths);
|
|
86
|
+
/** Loads (once) and returns the face; concurrent calls share the same promise. */
|
|
87
|
+
load(face: FallbackFace): Promise<LoadedFallbackFont>;
|
|
88
|
+
/** True when the face has already been loaded successfully or is loading. */
|
|
89
|
+
has(face: FallbackFace): boolean;
|
|
90
|
+
private fetchBytes;
|
|
91
|
+
}
|
|
92
|
+
export interface EmbeddedFallbackFont {
|
|
93
|
+
face: FallbackFace;
|
|
94
|
+
/** The pdf-lib font object; `pdfFont.ref` is the indirect reference of its Type0 dictionary. */
|
|
95
|
+
pdfFont: PDFFont;
|
|
96
|
+
/** Name to use in the page's /Resources /Font dictionary and in Tf ('DCFb1', 'DCFb2', ...). */
|
|
97
|
+
resourceName: string;
|
|
98
|
+
/**
|
|
99
|
+
* Encodes text into the bytes of a Tj hex string: two bytes per glyph
|
|
100
|
+
* (Identity-H CIDs = subset glyph ids). Every shown placeholder must pass
|
|
101
|
+
* through this call, because pdf-lib builds the subset from what was encoded.
|
|
102
|
+
*/
|
|
103
|
+
encode(text: string): Uint8Array;
|
|
104
|
+
/** Same measurement as `LoadedFallbackFont.measure`, from the same glyph set that gets embedded. */
|
|
105
|
+
measure(text: string, fontSize: number): number;
|
|
106
|
+
/** Glyph coverage of the face. */
|
|
107
|
+
canShow(text: string): boolean;
|
|
108
|
+
/** Metrics of the face, in 1/1000 em. */
|
|
109
|
+
ascent: number;
|
|
110
|
+
descent: number;
|
|
111
|
+
capHeight: number;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Embeds fallback faces into one output document, once per face, and hands the
|
|
115
|
+
* writer the resource name, the encoder and the measurer for each.
|
|
116
|
+
*
|
|
117
|
+
* pdf-lib subsets lazily: the glyphs written into the file at `save()` are the
|
|
118
|
+
* ones that went through `encode()`. Measuring never adds glyphs.
|
|
119
|
+
*/
|
|
120
|
+
export declare class FallbackFontEmbedder {
|
|
121
|
+
private readonly doc;
|
|
122
|
+
private readonly provider;
|
|
123
|
+
private readonly resourcePrefix;
|
|
124
|
+
private readonly embedded;
|
|
125
|
+
/** Resolved fonts in embedding order (resource numbers follow this order). */
|
|
126
|
+
private readonly ready;
|
|
127
|
+
/**
|
|
128
|
+
* @param doc Output document. fontkit is registered on it here; calling
|
|
129
|
+
* `registerFontkit` again on the caller's side is harmless.
|
|
130
|
+
* @param provider Source of the TTF bytes (shared across documents so each face is parsed once).
|
|
131
|
+
* @param resourcePrefix Prefix of the generated resource names; the writer must make sure it
|
|
132
|
+
* does not collide with the page's own font resource names.
|
|
133
|
+
*/
|
|
134
|
+
constructor(doc: PDFDocument, provider: FallbackFontProvider, resourcePrefix?: string);
|
|
135
|
+
/** Embeds the face into the document on first call; later calls return the same font. */
|
|
136
|
+
ensure(face: FallbackFace): Promise<EmbeddedFallbackFont>;
|
|
137
|
+
private embed;
|
|
138
|
+
/** Fonts embedded so far (resolved `ensure()` calls), in embedding order. */
|
|
139
|
+
fonts(): readonly EmbeddedFallbackFont[];
|
|
140
|
+
/**
|
|
141
|
+
* `[resourceName, ref]` for every embedded face, for the writer to merge
|
|
142
|
+
* into each page's /Resources /Font dictionary. Only faces whose `ensure()`
|
|
143
|
+
* has resolved are listed, so call it after the placeholders are encoded.
|
|
144
|
+
*/
|
|
145
|
+
resourceEntries(): Array<[string, PDFRef]>;
|
|
146
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @doccloak/core/pdf - fallback fonts (T211).
|
|
3
|
+
*
|
|
4
|
+
* When the document's own font cannot show a placeholder such as
|
|
5
|
+
* "[PERSON_1]", the writer draws it in the closest Liberation face.
|
|
6
|
+
* Liberation Sans / Serif / Mono are metric-compatible with Arial /
|
|
7
|
+
* Times New Roman / Courier New, so a placeholder set in the fallback keeps
|
|
8
|
+
* the line geometry of the original text in the common case.
|
|
9
|
+
*
|
|
10
|
+
* Three pieces:
|
|
11
|
+
* - `styleFromFontName` + `faceFor` turn a font dictionary (BaseFont name,
|
|
12
|
+
* /Flags, /ItalicAngle, /StemV, /FontWeight) into one of the 12 faces.
|
|
13
|
+
* - `FallbackFontProvider` loads a face's TTF bytes through the host
|
|
14
|
+
* (`PdfAssetPaths.loadFont` or `fontsUrl` + fetch), parses it with fontkit
|
|
15
|
+
* and offers kerning-free width measurement and glyph coverage checks.
|
|
16
|
+
* - `FallbackFontEmbedder` embeds a face once per output `PDFDocument` as a
|
|
17
|
+
* subset Type0 / Identity-H font and encodes placeholder text into the
|
|
18
|
+
* 2-byte glyph ids the content stream needs.
|
|
19
|
+
*
|
|
20
|
+
* No `fs` here: this file runs in browsers and Node alike.
|
|
21
|
+
*/
|
|
22
|
+
import fontkit from '@cantoo/fontkit';
|
|
23
|
+
/** Every face, in a stable order (used for subset tags). */
|
|
24
|
+
export const FALLBACK_FACES = [
|
|
25
|
+
'LiberationSans-Regular',
|
|
26
|
+
'LiberationSans-Bold',
|
|
27
|
+
'LiberationSans-Italic',
|
|
28
|
+
'LiberationSans-BoldItalic',
|
|
29
|
+
'LiberationSerif-Regular',
|
|
30
|
+
'LiberationSerif-Bold',
|
|
31
|
+
'LiberationSerif-Italic',
|
|
32
|
+
'LiberationSerif-BoldItalic',
|
|
33
|
+
'LiberationMono-Regular',
|
|
34
|
+
'LiberationMono-Bold',
|
|
35
|
+
'LiberationMono-Italic',
|
|
36
|
+
'LiberationMono-BoldItalic',
|
|
37
|
+
];
|
|
38
|
+
const FAMILY_NAME = {
|
|
39
|
+
sans: 'LiberationSans',
|
|
40
|
+
serif: 'LiberationSerif',
|
|
41
|
+
mono: 'LiberationMono',
|
|
42
|
+
};
|
|
43
|
+
/** Picks the Liberation face matching a style descriptor. */
|
|
44
|
+
export function faceFor(style) {
|
|
45
|
+
const weight = style.bold ? 'Bold' : '';
|
|
46
|
+
const slant = style.italic ? 'Italic' : '';
|
|
47
|
+
const suffix = weight + slant || 'Regular';
|
|
48
|
+
return `${FAMILY_NAME[style.family]}-${suffix}`;
|
|
49
|
+
}
|
|
50
|
+
/** File name of a face inside the fonts directory (`${fontsUrl}${fileName}`). */
|
|
51
|
+
export function faceFileName(face) {
|
|
52
|
+
return `${face}.ttf`;
|
|
53
|
+
}
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Style detection
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
/** PDF font descriptor /Flags bits (ISO 32000-1 table 123), as bit masks. */
|
|
58
|
+
export const FONT_FLAG_FIXED_PITCH = 1 << 0;
|
|
59
|
+
export const FONT_FLAG_SERIF = 1 << 1;
|
|
60
|
+
export const FONT_FLAG_ITALIC = 1 << 6;
|
|
61
|
+
export const FONT_FLAG_FORCE_BOLD = 1 << 18;
|
|
62
|
+
const MONO_HINTS = /courier|mono|consolas|menlo|monaco|lucidaconsole|inconsolata|sourcecodepro|firacode|couriernew|ocr[ab]|cascadiacode|cascadiamono|^cmtt|^cmsltt|nimbusmon|^lmmono|^lmtt/;
|
|
63
|
+
// Sans hints win over serif hints: "DejaVu Sans Serif"-style names do not exist, but
|
|
64
|
+
// "Sans" inside a name is a stronger signal than the family words below.
|
|
65
|
+
const SANS_HINTS = /arial|helvetica|calibri|verdana|segoe|tahoma|dejavusans|liberationsans|opensans|roboto|sans|gothic|futura|univers|frutiger|myriad|lato|montserrat|ubuntu|trebuchet|geneva|optima|avenir|gillsans|franklin|candara|corbel|noto(?!serif)|^cmss|nimbussan|^lmsans/;
|
|
66
|
+
const SERIF_HINTS = /times|georgia|garamond|cambria|book|serif|roman|minion|palatino|century|baskerville|charter|constantia|bodoni|didot|caslon|goudy|perpetua|plantin|sabon|utopia|notoserif|liberationserif|dejavuserif|merriweather|lora|playfair|^cm(?:r|bx|ti|b|sl|csc)\d|nimbusrom|^lmroman|charter|termes|pagella|bonum|schola/;
|
|
67
|
+
// Weight and slant words must start a "word" of the name: after a separator, at the start,
|
|
68
|
+
// or at a camel-case boundary (the normalized name has no separators, so "Bold" is looked for
|
|
69
|
+
// in the original name below). Inside a word ("Kobold", "Digital", "Hospital") they mean nothing.
|
|
70
|
+
const BOLD_HINTS = /(?:^|[^a-z])(?:bold|black|heavy|semibold|demibold|demi|extrabold|ultrabold)(?![a-z])|(?:^|[-,_ ])(?:bd|hv|blk)(?:it|mt|ps)*$|^cmbx|^cmb\d|^cmssbx|^lm\w*bold/;
|
|
71
|
+
const LIGHT_HINTS = /(?:^|[^a-z])(?:regular|light|thin|medium|normal|book|roman|extralight|ultralight|hairline)(?![a-z])/;
|
|
72
|
+
const ITALIC_HINTS = /(?:^|[^a-z])(?:italic|oblique|ital|kursiv|slanted)(?![a-z])|(?:^|[-,_ ])(?:bold)?it(?:mt)?$|(?:^|[-,_ ])(?:bd|hv|blk)it(?:mt)?$|^cmti|^cmsl|^cmssi|^cmbxti|^lm\w*(?:italic|oblique|slanted)/;
|
|
73
|
+
/** Name with separators replaced by one space and camel-case boundaries split ("BoldItalicMT" -> "Bold Italic MT"), lower-cased. */
|
|
74
|
+
function wordsOfFontName(baseFont) {
|
|
75
|
+
const stripped = baseFont.replace(/^\/?[A-Z]{6}\+/, '').replace(/^\//, '');
|
|
76
|
+
return stripped
|
|
77
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
78
|
+
.replace(/([A-Za-z])(\d)/g, '$1 $2')
|
|
79
|
+
.replace(/[\s_,\-.]+/g, ' ')
|
|
80
|
+
.toLowerCase()
|
|
81
|
+
.trim();
|
|
82
|
+
}
|
|
83
|
+
/** Strips a subset tag ("AAAAAA+") and returns the name lower-cased without separators. */
|
|
84
|
+
function normalizeFontName(baseFont) {
|
|
85
|
+
const stripped = baseFont.replace(/^\/?[A-Z]{6}\+/, '').replace(/^\//, '');
|
|
86
|
+
return stripped.toLowerCase().replace(/[\s_]+/g, '');
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Derives family / bold / italic from a font's BaseFont name and, when the
|
|
90
|
+
* name is silent, its font descriptor facts.
|
|
91
|
+
*
|
|
92
|
+
* - family: mono when the name says so or /Flags has FixedPitch; sans when the
|
|
93
|
+
* name is a known sans face; serif when the name says so, else the /Flags
|
|
94
|
+
* Serif bit decides; sans by default.
|
|
95
|
+
* - bold: name says so (Bold/Black/Heavy/Semibold/Demi), else /FontWeight >= 600,
|
|
96
|
+
* /StemV >= 120 or the ForceBold flag when the name gives no weight at all.
|
|
97
|
+
* - italic: name says so (Italic/Oblique/-It), /Flags Italic bit, or /ItalicAngle != 0.
|
|
98
|
+
*/
|
|
99
|
+
export function styleFromFontName(baseFont, flags = 0, italicAngle = 0, stemV = 0, fontWeight = 0) {
|
|
100
|
+
const name = normalizeFontName(baseFont ?? '');
|
|
101
|
+
// Names carry the style after a comma / hyphen ("Calibri,BoldItalic", "Arial-BoldMT") or at a
|
|
102
|
+
// camel-case boundary; `words` splits those so a weight word is matched only as a whole word.
|
|
103
|
+
const words = wordsOfFontName(baseFont ?? '');
|
|
104
|
+
const family = detectFamily(name, flags);
|
|
105
|
+
let bold;
|
|
106
|
+
if (BOLD_HINTS.test(words) || BOLD_HINTS.test(name.replace(/^[a-z]{6}\+/, ''))) {
|
|
107
|
+
bold = BOLD_HINTS.test(words);
|
|
108
|
+
}
|
|
109
|
+
else if (LIGHT_HINTS.test(words)) {
|
|
110
|
+
bold = false;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// /StemV is not a weight: Chrome/Skia writes 130+ for regular Georgia and 250+ for its italic.
|
|
114
|
+
bold = fontWeight >= 600 || (flags & FONT_FLAG_FORCE_BOLD) !== 0;
|
|
115
|
+
}
|
|
116
|
+
void stemV;
|
|
117
|
+
const italic = ITALIC_HINTS.test(words) || (flags & FONT_FLAG_ITALIC) !== 0 || (Number.isFinite(italicAngle) && italicAngle !== 0);
|
|
118
|
+
return { family, bold, italic };
|
|
119
|
+
}
|
|
120
|
+
function detectFamily(name, flags) {
|
|
121
|
+
if (MONO_HINTS.test(name) || (flags & FONT_FLAG_FIXED_PITCH) !== 0)
|
|
122
|
+
return 'mono';
|
|
123
|
+
if (SANS_HINTS.test(name))
|
|
124
|
+
return 'sans';
|
|
125
|
+
if (SERIF_HINTS.test(name))
|
|
126
|
+
return 'serif';
|
|
127
|
+
if ((flags & FONT_FLAG_SERIF) !== 0)
|
|
128
|
+
return 'serif';
|
|
129
|
+
return 'sans';
|
|
130
|
+
}
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Loading and measuring
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
/**
|
|
135
|
+
* OpenType features used for every layout call, in the provider and in the
|
|
136
|
+
* embedder alike: kerning and ligatures off, so a placeholder is one glyph per
|
|
137
|
+
* character and its width is the plain sum of glyph advances - exactly what a
|
|
138
|
+
* Tj with Identity-H encoding renders from the embedded /W array.
|
|
139
|
+
*/
|
|
140
|
+
export const FALLBACK_LAYOUT_FEATURES = {
|
|
141
|
+
kern: false,
|
|
142
|
+
liga: false,
|
|
143
|
+
clig: false,
|
|
144
|
+
dlig: false,
|
|
145
|
+
calt: false,
|
|
146
|
+
rlig: false,
|
|
147
|
+
};
|
|
148
|
+
function parseFont(bytes, face) {
|
|
149
|
+
const parsed = fontkit.create(bytes);
|
|
150
|
+
if ('fonts' in parsed) {
|
|
151
|
+
throw new Error(`Fallback font ${face} is a font collection, expected a single TrueType face`);
|
|
152
|
+
}
|
|
153
|
+
return parsed;
|
|
154
|
+
}
|
|
155
|
+
/** Width helper shared by the provider and the embedder: sum of glyph advances at `size`. */
|
|
156
|
+
export function measureWithFont(font, text, fontSize) {
|
|
157
|
+
if (text.length === 0)
|
|
158
|
+
return 0;
|
|
159
|
+
const { glyphs } = font.layout(text, FALLBACK_LAYOUT_FEATURES);
|
|
160
|
+
let units = 0;
|
|
161
|
+
for (const glyph of glyphs)
|
|
162
|
+
units += glyph.advanceWidth;
|
|
163
|
+
return (units * fontSize) / font.unitsPerEm;
|
|
164
|
+
}
|
|
165
|
+
function canShowWithFont(font, text) {
|
|
166
|
+
for (const ch of text) {
|
|
167
|
+
const cp = ch.codePointAt(0);
|
|
168
|
+
if (!font.hasGlyphForCodePoint(cp))
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
function toLoadedFont(face, bytes) {
|
|
174
|
+
const font = parseFont(bytes, face);
|
|
175
|
+
const k = 1000 / font.unitsPerEm;
|
|
176
|
+
return {
|
|
177
|
+
face,
|
|
178
|
+
bytes,
|
|
179
|
+
measure: (text, fontSize) => measureWithFont(font, text, fontSize),
|
|
180
|
+
canShow: (text) => canShowWithFont(font, text),
|
|
181
|
+
ascent: font.ascent * k,
|
|
182
|
+
descent: font.descent * k,
|
|
183
|
+
capHeight: font.capHeight * k,
|
|
184
|
+
xHeight: font.xHeight * k,
|
|
185
|
+
unitsPerEm: font.unitsPerEm,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Loads and caches the Liberation faces. Bytes come from `assets.loadFont`
|
|
190
|
+
* when the host provides it (Node tests, Electron), else from
|
|
191
|
+
* `${assets.fontsUrl}${face}.ttf` through the global `fetch`.
|
|
192
|
+
*/
|
|
193
|
+
export class FallbackFontProvider {
|
|
194
|
+
constructor(assets = {}) {
|
|
195
|
+
this.cache = new Map();
|
|
196
|
+
this.assets = assets;
|
|
197
|
+
}
|
|
198
|
+
/** Loads (once) and returns the face; concurrent calls share the same promise. */
|
|
199
|
+
load(face) {
|
|
200
|
+
let pending = this.cache.get(face);
|
|
201
|
+
if (!pending) {
|
|
202
|
+
pending = this.fetchBytes(face).then((bytes) => toLoadedFont(face, bytes));
|
|
203
|
+
// Drop a failed load so a later call can retry (e.g. after the host fixed its asset path).
|
|
204
|
+
pending.catch(() => this.cache.delete(face));
|
|
205
|
+
this.cache.set(face, pending);
|
|
206
|
+
}
|
|
207
|
+
return pending;
|
|
208
|
+
}
|
|
209
|
+
/** True when the face has already been loaded successfully or is loading. */
|
|
210
|
+
has(face) {
|
|
211
|
+
return this.cache.has(face);
|
|
212
|
+
}
|
|
213
|
+
async fetchBytes(face) {
|
|
214
|
+
const fileName = faceFileName(face);
|
|
215
|
+
const { loadFont, fontsUrl } = this.assets;
|
|
216
|
+
if (loadFont) {
|
|
217
|
+
const bytes = await loadFont(fileName);
|
|
218
|
+
if (!(bytes instanceof Uint8Array) || bytes.length === 0) {
|
|
219
|
+
throw new Error(`loadFont(${fileName}) returned no bytes`);
|
|
220
|
+
}
|
|
221
|
+
return bytes;
|
|
222
|
+
}
|
|
223
|
+
if (fontsUrl) {
|
|
224
|
+
if (typeof fetch !== 'function') {
|
|
225
|
+
throw new Error(`Cannot load fallback font ${fileName}: no global fetch; pass PdfAssetPaths.loadFont instead`);
|
|
226
|
+
}
|
|
227
|
+
const url = `${fontsUrl}${fileName}`;
|
|
228
|
+
const res = await fetch(url);
|
|
229
|
+
if (!res.ok) {
|
|
230
|
+
throw new Error(`Cannot load fallback font ${fileName}: ${res.status} ${res.statusText} from ${url}`);
|
|
231
|
+
}
|
|
232
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
233
|
+
}
|
|
234
|
+
throw new Error(`Cannot load fallback font ${fileName}: PdfAssetPaths.fontsUrl or PdfAssetPaths.loadFont is required to write placeholders in a fallback font`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/** Six-letter subset tag for a face ("DCAAAB+LiberationSans-Bold"), stable per face. */
|
|
238
|
+
function subsetTag(face) {
|
|
239
|
+
const index = Math.max(0, FALLBACK_FACES.indexOf(face));
|
|
240
|
+
const a = String.fromCharCode(65 + Math.floor(index / 26));
|
|
241
|
+
const b = String.fromCharCode(65 + (index % 26));
|
|
242
|
+
return `DCAA${a}${b}`;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Embeds fallback faces into one output document, once per face, and hands the
|
|
246
|
+
* writer the resource name, the encoder and the measurer for each.
|
|
247
|
+
*
|
|
248
|
+
* pdf-lib subsets lazily: the glyphs written into the file at `save()` are the
|
|
249
|
+
* ones that went through `encode()`. Measuring never adds glyphs.
|
|
250
|
+
*/
|
|
251
|
+
export class FallbackFontEmbedder {
|
|
252
|
+
/**
|
|
253
|
+
* @param doc Output document. fontkit is registered on it here; calling
|
|
254
|
+
* `registerFontkit` again on the caller's side is harmless.
|
|
255
|
+
* @param provider Source of the TTF bytes (shared across documents so each face is parsed once).
|
|
256
|
+
* @param resourcePrefix Prefix of the generated resource names; the writer must make sure it
|
|
257
|
+
* does not collide with the page's own font resource names.
|
|
258
|
+
*/
|
|
259
|
+
constructor(doc, provider, resourcePrefix = 'DCFb') {
|
|
260
|
+
this.embedded = new Map();
|
|
261
|
+
/** Resolved fonts in embedding order (resource numbers follow this order). */
|
|
262
|
+
this.ready = [];
|
|
263
|
+
this.doc = doc;
|
|
264
|
+
this.provider = provider;
|
|
265
|
+
this.resourcePrefix = resourcePrefix;
|
|
266
|
+
doc.registerFontkit(fontkit);
|
|
267
|
+
}
|
|
268
|
+
/** Embeds the face into the document on first call; later calls return the same font. */
|
|
269
|
+
ensure(face) {
|
|
270
|
+
let pending = this.embedded.get(face);
|
|
271
|
+
if (!pending) {
|
|
272
|
+
pending = this.embed(face);
|
|
273
|
+
pending.catch(() => this.embedded.delete(face));
|
|
274
|
+
this.embedded.set(face, pending);
|
|
275
|
+
}
|
|
276
|
+
return pending;
|
|
277
|
+
}
|
|
278
|
+
async embed(face) {
|
|
279
|
+
const loaded = await this.provider.load(face);
|
|
280
|
+
const pdfFont = await this.doc.embedFont(loaded.bytes, {
|
|
281
|
+
subset: true,
|
|
282
|
+
customName: `${subsetTag(face)}+${face}`,
|
|
283
|
+
features: FALLBACK_LAYOUT_FEATURES,
|
|
284
|
+
});
|
|
285
|
+
const resourceName = `${this.resourcePrefix}${this.ready.length + 1}`;
|
|
286
|
+
const font = {
|
|
287
|
+
face,
|
|
288
|
+
pdfFont,
|
|
289
|
+
resourceName,
|
|
290
|
+
encode: (text) => pdfFont.encodeText(text).asBytes(),
|
|
291
|
+
measure: loaded.measure,
|
|
292
|
+
canShow: loaded.canShow,
|
|
293
|
+
ascent: loaded.ascent,
|
|
294
|
+
descent: loaded.descent,
|
|
295
|
+
capHeight: loaded.capHeight,
|
|
296
|
+
};
|
|
297
|
+
this.ready.push(font);
|
|
298
|
+
return font;
|
|
299
|
+
}
|
|
300
|
+
/** Fonts embedded so far (resolved `ensure()` calls), in embedding order. */
|
|
301
|
+
fonts() {
|
|
302
|
+
return this.ready;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* `[resourceName, ref]` for every embedded face, for the writer to merge
|
|
306
|
+
* into each page's /Resources /Font dictionary. Only faces whose `ensure()`
|
|
307
|
+
* has resolved are listed, so call it after the placeholders are encoded.
|
|
308
|
+
*/
|
|
309
|
+
resourceEntries() {
|
|
310
|
+
return this.ready.map((font) => [font.resourceName, font.pdfFont.ref]);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @doccloak/core/pdf - placeholder fitting (T213).
|
|
3
|
+
*
|
|
4
|
+
* A placeholder such as "[PERSON_1]" rarely has the width of the text it
|
|
5
|
+
* replaces ("Li" is two glyphs, a Polish surname ten). The policy, agreed
|
|
6
|
+
* with the founder on 2026-09-24: keep the original font size when the
|
|
7
|
+
* placeholder fits the gap; otherwise shrink the size down to a floor, then
|
|
8
|
+
* condense horizontally (Tz) down to a floor, and only then let the rest of
|
|
9
|
+
* the line move right by the remaining overflow. A placeholder never gets
|
|
10
|
+
* bigger than the original text, and the visible change to the layout is
|
|
11
|
+
* the smallest that keeps the placeholder legible.
|
|
12
|
+
*/
|
|
13
|
+
import type { PlaceholderFit } from './types.js';
|
|
14
|
+
export interface FitInput {
|
|
15
|
+
/** Width of the gap in unscaled text space (the space `advances` are measured in). */
|
|
16
|
+
gap: number;
|
|
17
|
+
/** Original font size (Tfs) of the run. */
|
|
18
|
+
fontSize: number;
|
|
19
|
+
/** Original horizontal scaling (Tz / 100). */
|
|
20
|
+
hscale: number;
|
|
21
|
+
/** Placeholder width in 1/1000 em at size 1 (font units summed). */
|
|
22
|
+
widthPerEm: number;
|
|
23
|
+
}
|
|
24
|
+
export interface FitResult {
|
|
25
|
+
fontSize: number;
|
|
26
|
+
hscale: number;
|
|
27
|
+
/** Placeholder width in unscaled text space after fitting. */
|
|
28
|
+
width: number;
|
|
29
|
+
/** How much the rest of the line must move right (unscaled text space); 0 when it fits. */
|
|
30
|
+
overflow: number;
|
|
31
|
+
/** Which stage produced the result, for reports and tests. */
|
|
32
|
+
stage: 'fits' | 'shrunk' | 'condensed' | 'overflow';
|
|
33
|
+
}
|
|
34
|
+
export declare const DEFAULT_FIT: Required<PlaceholderFit>;
|
|
35
|
+
export declare function fitPlaceholder(input: FitInput, policy?: PlaceholderFit): FitResult;
|
package/dist/pdf/fit.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @doccloak/core/pdf - placeholder fitting (T213).
|
|
3
|
+
*
|
|
4
|
+
* A placeholder such as "[PERSON_1]" rarely has the width of the text it
|
|
5
|
+
* replaces ("Li" is two glyphs, a Polish surname ten). The policy, agreed
|
|
6
|
+
* with the founder on 2026-09-24: keep the original font size when the
|
|
7
|
+
* placeholder fits the gap; otherwise shrink the size down to a floor, then
|
|
8
|
+
* condense horizontally (Tz) down to a floor, and only then let the rest of
|
|
9
|
+
* the line move right by the remaining overflow. A placeholder never gets
|
|
10
|
+
* bigger than the original text, and the visible change to the layout is
|
|
11
|
+
* the smallest that keeps the placeholder legible.
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_FIT = {
|
|
14
|
+
// T225: 75 % (was 65 %); the planner moves the rest of the line before it shrinks at all.
|
|
15
|
+
minSizeRatio: 0.75,
|
|
16
|
+
minHorizontalScale: 0.8,
|
|
17
|
+
allowOverflow: false,
|
|
18
|
+
};
|
|
19
|
+
export function fitPlaceholder(input, policy = {}) {
|
|
20
|
+
const p = { ...DEFAULT_FIT, ...policy };
|
|
21
|
+
// A policy can only shrink: ratios above 1 (or non-numbers) fall back to the defaults, 0 to a hair.
|
|
22
|
+
p.minSizeRatio = clampRatio(p.minSizeRatio, DEFAULT_FIT.minSizeRatio);
|
|
23
|
+
p.minHorizontalScale = clampRatio(p.minHorizontalScale, DEFAULT_FIT.minHorizontalScale);
|
|
24
|
+
const { gap, fontSize, hscale, widthPerEm } = input;
|
|
25
|
+
const widthAt = (size, h) => (widthPerEm / 1000) * size * h;
|
|
26
|
+
if (![gap, fontSize, hscale, widthPerEm].every(Number.isFinite) || widthPerEm <= 0 || fontSize <= 0 || hscale <= 0) {
|
|
27
|
+
return { fontSize, hscale, width: 0, overflow: 0, stage: 'fits' };
|
|
28
|
+
}
|
|
29
|
+
const natural = widthAt(fontSize, hscale);
|
|
30
|
+
if (natural <= gap + 1e-6) {
|
|
31
|
+
return { fontSize, hscale, width: natural, overflow: 0, stage: 'fits' };
|
|
32
|
+
}
|
|
33
|
+
// Stage 1: shrink the font size down to the floor.
|
|
34
|
+
const minSize = fontSize * p.minSizeRatio;
|
|
35
|
+
const sizeNeeded = gap / ((widthPerEm / 1000) * hscale);
|
|
36
|
+
if (sizeNeeded >= minSize) {
|
|
37
|
+
const size = round(sizeNeeded);
|
|
38
|
+
return { fontSize: size, hscale, width: widthAt(size, hscale), overflow: 0, stage: 'shrunk' };
|
|
39
|
+
}
|
|
40
|
+
// Stage 2: condense horizontally down to the floor.
|
|
41
|
+
const size = round(minSize);
|
|
42
|
+
const minH = hscale * p.minHorizontalScale;
|
|
43
|
+
const hNeeded = gap / ((widthPerEm / 1000) * size);
|
|
44
|
+
if (hNeeded >= minH) {
|
|
45
|
+
const h = Math.floor(hNeeded * 1000) / 1000;
|
|
46
|
+
return { fontSize: size, hscale: h, width: widthAt(size, h), overflow: 0, stage: 'condensed' };
|
|
47
|
+
}
|
|
48
|
+
// Stage 3: overflow; the caller shifts the rest of the line (or lets it overlap when allowOverflow).
|
|
49
|
+
const h = Math.floor(minH * 1000) / 1000;
|
|
50
|
+
const width = widthAt(size, h);
|
|
51
|
+
return { fontSize: size, hscale: h, width, overflow: p.allowOverflow ? 0 : width - gap, stage: 'overflow' };
|
|
52
|
+
}
|
|
53
|
+
function round(v) {
|
|
54
|
+
return Math.floor(v * 100) / 100;
|
|
55
|
+
}
|
|
56
|
+
function clampRatio(v, fallback) {
|
|
57
|
+
if (!Number.isFinite(v) || v > 1)
|
|
58
|
+
return fallback;
|
|
59
|
+
return Math.max(0.05, v);
|
|
60
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @doccloak/core/pdf - font model (T209).
|
|
3
|
+
*
|
|
4
|
+
* A LoadedFont turns the bytes of a text-showing operand into glyphs (code,
|
|
5
|
+
* Unicode, advance width) and, in the other direction, tells whether a
|
|
6
|
+
* placeholder can be written with the same font and how to encode it.
|
|
7
|
+
*
|
|
8
|
+
* Decoding order (as pdf.js and the spec, 9.10.2): ToUnicode CMap first,
|
|
9
|
+
* then the encoding (base encoding + /Differences) through the Adobe Glyph
|
|
10
|
+
* List, then for composite fonts the CID-to-Unicode of Identity orderings is
|
|
11
|
+
* unknown, so a code with no ToUnicode entry yields '' (notdef for our
|
|
12
|
+
* purposes) and the page is later cross-checked against pdf.js.
|
|
13
|
+
*
|
|
14
|
+
* Encodability (placeholders in the original font): a character is
|
|
15
|
+
* showable when a code maps to it AND that code is known to have a glyph:
|
|
16
|
+
* either the code was shown somewhere in the document with this font (the
|
|
17
|
+
* subset contains it) or the font is a non-embedded standard 14 face whose
|
|
18
|
+
* AFM lists the glyph. This rule never guesses about subset contents.
|
|
19
|
+
*/
|
|
20
|
+
import type { PDFContext } from './objects.js';
|
|
21
|
+
import { PDFDict } from './objects.js';
|
|
22
|
+
import type { CMap } from './cmap.js';
|
|
23
|
+
import type { FontStyle, Matrix } from './types.js';
|
|
24
|
+
export type FontSubtype = 'Type1' | 'MMType1' | 'TrueType' | 'Type3' | 'Type0' | 'unknown';
|
|
25
|
+
export interface DecodedGlyph {
|
|
26
|
+
code: number;
|
|
27
|
+
/** Byte length of the code in the operand. */
|
|
28
|
+
length: number;
|
|
29
|
+
/** Byte offset of the code in the operand. */
|
|
30
|
+
offset: number;
|
|
31
|
+
/** '' when unmapped. */
|
|
32
|
+
unicode: string;
|
|
33
|
+
/** Advance width in 1/1000 text space units (Type3 already through FontMatrix). */
|
|
34
|
+
width: number;
|
|
35
|
+
/** True when the code is a single byte 32 in a simple font (word spacing applies). */
|
|
36
|
+
isSpaceCode: boolean;
|
|
37
|
+
/** True when the encoding names the glyph /.notdef: a viewer draws nothing there (blank by design, not unreadable). */
|
|
38
|
+
notdef?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface FontProblem {
|
|
41
|
+
/** Machine code for the writer's safety decisions. */
|
|
42
|
+
code: 'predefined-cmap' | 'usecmap' | 'bad-cmap' | 'vertical' | 'no-unicode' | 'unknown-subtype';
|
|
43
|
+
message: string;
|
|
44
|
+
}
|
|
45
|
+
export interface LoadedFont {
|
|
46
|
+
key: string;
|
|
47
|
+
dict: PDFDict;
|
|
48
|
+
subtype: FontSubtype;
|
|
49
|
+
baseFont: string;
|
|
50
|
+
composite: boolean;
|
|
51
|
+
type3: boolean;
|
|
52
|
+
/** Type3 glyph space to text space. */
|
|
53
|
+
fontMatrix: Matrix | null;
|
|
54
|
+
style: FontStyle;
|
|
55
|
+
embedded: boolean;
|
|
56
|
+
symbolic: boolean;
|
|
57
|
+
vertical: boolean;
|
|
58
|
+
/** Problems that make text of this font unsafe to edit (the page falls back to raster). */
|
|
59
|
+
problems: FontProblem[];
|
|
60
|
+
/** Codes shown somewhere in the document with this font (filled by the extractor). */
|
|
61
|
+
usedCodes: Set<number>;
|
|
62
|
+
/** Codes -> glyph name for simple fonts (after Differences); null for composite fonts. */
|
|
63
|
+
encodingNames: ReadonlyArray<string | undefined> | null;
|
|
64
|
+
toUnicode: CMap | null;
|
|
65
|
+
/** Byte length of every code: 1 for simple fonts; for composite fonts by the CMap (may vary). */
|
|
66
|
+
decode(bytes: Uint8Array): DecodedGlyph[];
|
|
67
|
+
/** Width in 1/1000 units of one code. */
|
|
68
|
+
widthOfCode(code: number): number;
|
|
69
|
+
/** Encode a placeholder in this font, or null when any character cannot be shown safely. */
|
|
70
|
+
encode(text: string): {
|
|
71
|
+
bytes: Uint8Array;
|
|
72
|
+
width: number;
|
|
73
|
+
codes: number[];
|
|
74
|
+
} | null;
|
|
75
|
+
/** Byte length of a code as the font's encoding defines it (1 for simple fonts; the encoding CMap for composite ones). */
|
|
76
|
+
codeLength(code: number): number;
|
|
77
|
+
/** Ascent/descent hints in 1/1000 units (for fitting), best effort. */
|
|
78
|
+
ascent: number;
|
|
79
|
+
descent: number;
|
|
80
|
+
}
|
|
81
|
+
/** Load (and cache by ref) the font at /Resources /Font /<name>. */
|
|
82
|
+
export declare function loadFont(ctx: PDFContext, fontsDict: PDFDict | undefined, resourceName: string, cache: Map<string, LoadedFont>): LoadedFont | undefined;
|