@jasy/pdf 1.0.0-alpha.8 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -10
- package/dist/api/color.js +69 -0
- package/dist/api/content.d.ts +45 -5
- package/dist/api/content.js +96 -7
- package/dist/api/descriptor.js +6 -1
- package/dist/api/dimension.d.ts +57 -0
- package/dist/api/dimension.js +27 -0
- package/dist/api/font-url.d.ts +28 -0
- package/dist/api/font-url.js +135 -0
- package/dist/api/gradient.d.ts +53 -0
- package/dist/api/gradient.js +97 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/insets.d.ts +19 -12
- package/dist/api/insets.js +18 -7
- package/dist/api/layout.d.ts +20 -6
- package/dist/api/layout.js +31 -6
- package/dist/api/page-builder.d.ts +8 -0
- package/dist/api/page-builder.js +12 -0
- package/dist/api/structure.d.ts +12 -0
- package/dist/api/structure.js +54 -7
- package/dist/api/text.d.ts +65 -8
- package/dist/api/text.js +38 -5
- package/dist/canvas/painter.d.ts +102 -0
- package/dist/canvas/painter.js +183 -0
- package/dist/edit/document.d.ts +14 -4
- package/dist/edit/document.js +15 -22
- package/dist/edit/fill.d.ts +19 -2
- package/dist/edit/fill.js +63 -34
- package/dist/edit/flatten.d.ts +13 -1
- package/dist/edit/flatten.js +38 -13
- package/dist/edit/index.d.ts +2 -0
- package/dist/edit/index.js +3 -1
- package/dist/edit/objects.d.ts +5 -0
- package/dist/edit/objects.js +27 -0
- package/dist/edit/writer.d.ts +7 -0
- package/dist/edit/writer.js +23 -1
- package/dist/elements/canvas-element.d.ts +38 -0
- package/dist/elements/canvas-element.js +47 -0
- package/dist/elements/container-element.d.ts +23 -1
- package/dist/elements/container-element.js +31 -10
- package/dist/elements/image-element.d.ts +18 -4
- package/dist/elements/image-element.js +27 -12
- package/dist/elements/index.d.ts +2 -0
- package/dist/elements/index.js +2 -0
- package/dist/elements/layout/expanded-element.d.ts +5 -1
- package/dist/elements/layout/expanded-element.js +16 -3
- package/dist/elements/layout/keep-together-element.d.ts +2 -0
- package/dist/elements/layout/keep-together-element.js +4 -0
- package/dist/elements/layout/padding-element.d.ts +9 -2
- package/dist/elements/layout/padding-element.js +12 -2
- package/dist/elements/layout/page-builder-element.js +2 -0
- package/dist/elements/layout/struct-group.d.ts +2 -0
- package/dist/elements/layout/struct-group.js +4 -0
- package/dist/elements/pdf-element.d.ts +75 -1
- package/dist/elements/pdf-element.js +80 -1
- package/dist/elements/rectangle-element.d.ts +22 -6
- package/dist/elements/rectangle-element.js +31 -12
- package/dist/elements/row-element.d.ts +26 -1
- package/dist/elements/row-element.js +31 -10
- package/dist/elements/svg-element.d.ts +41 -0
- package/dist/elements/svg-element.js +93 -0
- package/dist/elements/text-element.d.ts +92 -1
- package/dist/elements/text-element.js +208 -33
- package/dist/ir/display-list.d.ts +67 -11
- package/dist/ir/display-list.js +8 -1
- package/dist/layout/alignment.d.ts +10 -0
- package/dist/layout/alignment.js +6 -0
- package/dist/layout/box-constraints.d.ts +53 -0
- package/dist/layout/box-constraints.js +72 -0
- package/dist/layout/insets.d.ts +28 -0
- package/dist/layout/insets.js +20 -0
- package/dist/layout/min-intrinsic.d.ts +29 -0
- package/dist/layout/min-intrinsic.js +37 -0
- package/dist/renderer/anchor-renderer.js +1 -1
- package/dist/renderer/bookmark-renderer.js +1 -1
- package/dist/renderer/canvas-renderer.d.ts +14 -0
- package/dist/renderer/canvas-renderer.js +36 -0
- package/dist/renderer/container-renderer.js +1 -3
- package/dist/renderer/default-text-style-renderer.js +1 -1
- package/dist/renderer/deferred-renderer.js +1 -1
- package/dist/renderer/expanded-renderer.js +1 -1
- package/dist/renderer/image-renderer.js +2 -1
- package/dist/renderer/index.d.ts +2 -0
- package/dist/renderer/index.js +2 -0
- package/dist/renderer/keep-together-renderer.js +1 -1
- package/dist/renderer/link-renderer.js +1 -1
- package/dist/renderer/padding-renderer.js +1 -1
- package/dist/renderer/page-builder-renderer.js +1 -1
- package/dist/renderer/page-renderer.js +1 -3
- package/dist/renderer/pdf-backend.d.ts +21 -4
- package/dist/renderer/pdf-backend.js +198 -61
- package/dist/renderer/pdf-document-renderer.js +8 -1
- package/dist/renderer/pdf-renderer.js +5 -1
- package/dist/renderer/positioned-renderer.js +1 -1
- package/dist/renderer/rectangle-renderer.js +9 -5
- package/dist/renderer/repeating-header-renderer.js +1 -2
- package/dist/renderer/rotated-renderer.js +1 -1
- package/dist/renderer/row-renderer.js +1 -3
- package/dist/renderer/struct-group-renderer.js +1 -1
- package/dist/renderer/svg-renderer.d.ts +11 -0
- package/dist/renderer/svg-renderer.js +27 -0
- package/dist/renderer/text-renderer.d.ts +2 -2
- package/dist/renderer/text-renderer.js +176 -57
- package/dist/svg/css.d.ts +38 -0
- package/dist/svg/css.js +115 -0
- package/dist/svg/errors.d.ts +24 -0
- package/dist/svg/errors.js +30 -0
- package/dist/svg/gradients.d.ts +34 -0
- package/dist/svg/gradients.js +156 -0
- package/dist/svg/index.d.ts +22 -0
- package/dist/svg/index.js +331 -0
- package/dist/svg/shapes.d.ts +6 -0
- package/dist/svg/shapes.js +177 -0
- package/dist/svg/style.d.ts +61 -0
- package/dist/svg/style.js +144 -0
- package/dist/svg/transform.d.ts +13 -0
- package/dist/svg/transform.js +68 -0
- package/dist/svg/units.d.ts +11 -0
- package/dist/svg/units.js +73 -0
- package/dist/svg/xml.d.ts +24 -0
- package/dist/svg/xml.js +158 -0
- package/dist/text/advance.d.ts +3 -0
- package/dist/text/advance.js +8 -4
- package/dist/text/arabic.d.ts +20 -0
- package/dist/text/arabic.js +101 -0
- package/dist/text/bidi.d.ts +33 -0
- package/dist/text/bidi.js +94 -0
- package/dist/text/font-fallback.d.ts +25 -0
- package/dist/text/font-fallback.js +38 -0
- package/dist/text/glyph-coverage.d.ts +15 -0
- package/dist/text/glyph-coverage.js +77 -0
- package/dist/text/line-breaker.d.ts +43 -1
- package/dist/text/line-breaker.js +181 -45
- package/dist/text/orphans-widows.d.ts +27 -0
- package/dist/text/orphans-widows.js +53 -0
- package/dist/text/shape.d.ts +29 -0
- package/dist/text/shape.js +91 -0
- package/dist/text/text-style.d.ts +38 -0
- package/dist/text/text-style.js +50 -1
- package/dist/text/whitespace.d.ts +4 -0
- package/dist/text/whitespace.js +43 -0
- package/dist/text/word-splitting.d.ts +42 -0
- package/dist/text/word-splitting.js +72 -0
- package/dist/utils/flex-layout.d.ts +24 -4
- package/dist/utils/flex-layout.js +218 -7
- package/dist/utils/font-metrics.d.ts +13 -2
- package/dist/utils/gsub.d.ts +36 -0
- package/dist/utils/gsub.js +178 -0
- package/dist/utils/inflate.d.ts +16 -0
- package/dist/utils/inflate.js +52 -0
- package/dist/utils/pdf-object-manager.d.ts +44 -3
- package/dist/utils/pdf-object-manager.js +176 -14
- package/dist/utils/renderer-registry.d.ts +13 -1
- package/dist/utils/renderer-registry.js +22 -1
- package/dist/utils/ttf-parser.d.ts +6 -0
- package/dist/utils/ttf-parser.js +19 -0
- package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
- package/dist/utils/utf8-to-windows1252-encoder.js +10 -0
- package/dist/utils/woff.d.ts +10 -0
- package/dist/utils/woff.js +116 -0
- package/dist/utils/woff2.d.ts +6 -0
- package/dist/utils/woff2.js +651 -0
- package/dist/validators/element-validator.js +9 -2
- package/dist/vector/path.d.ts +33 -0
- package/dist/vector/path.js +81 -0
- package/package.json +6 -2
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A character with no glyph is still ENCODED and DRAWN: it comes out as the font's .notdef box, and
|
|
3
|
+
* veraPDF rejects the file for referencing .notdef (ISO 19005-3, 6.2.11.8). A `\n` in an invoice
|
|
4
|
+
* description was the first case we hit; zero-width characters are the same fault and arrive far more
|
|
5
|
+
* quietly - a paste out of Word or a web page carries them routinely.
|
|
6
|
+
*
|
|
7
|
+
* So nothing invisible survives into the breaker except `\n`, and that only because the breaker treats
|
|
8
|
+
* it as a hard break. The mapping follows a browser's `white-space: normal` apart from `\n`: CSS
|
|
9
|
+
* collapses that to a space, while a text COMPONENT breaks on it - Flutter's `Text` and react-pdf both
|
|
10
|
+
* do, and it is what anyone writing `"a\nb"` expects.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Everything that MEANS a line break, matched as a WHOLE so a two-character ending never becomes two
|
|
14
|
+
* breaks. `\n\r` is in here beside `\r\n` because it is what a mistyped CRLF looks like, and a stray
|
|
15
|
+
* blank line is a worse answer than reading it as the one break it was meant to be. U+2028/U+2029 are
|
|
16
|
+
* Unicode's own line and paragraph separators.
|
|
17
|
+
*/
|
|
18
|
+
const LINE_BREAKS = /\r\n|\n\r|\r|\u2028|\u2029/g;
|
|
19
|
+
/**
|
|
20
|
+
* Invisible and inert: a zero-width space and a byte-order mark carry no meaning we act on, so they are
|
|
21
|
+
* dropped rather than drawn. C0, C1 and DEL likewise.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately NOT here: U+200C/U+200D (zero-width non-joiner and joiner) drive Arabic shaping and
|
|
24
|
+
* emoji sequences, and U+200E/U+200F (the bidi marks) drive `text/bidi.ts`. Dropping those would
|
|
25
|
+
* silently change how correct text is laid out - worse than the box they can still leave behind in a
|
|
26
|
+
* run that neither shapes nor reorders.
|
|
27
|
+
*/
|
|
28
|
+
// oxlint-disable-next-line no-control-regex -- matching control characters is the whole point here.
|
|
29
|
+
const UNPRINTABLE = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F\u200B\uFEFF]/g;
|
|
30
|
+
export function normalizeWhitespace(text) {
|
|
31
|
+
return (text
|
|
32
|
+
// Every spelling of a break becomes `\n` first, so the breaker only ever sees one form.
|
|
33
|
+
.replace(LINE_BREAKS, "\n")
|
|
34
|
+
// A tab is collapsible whitespace; a PDF text operator has no tab stops to advance to.
|
|
35
|
+
.replace(/\t/g, " ")
|
|
36
|
+
.replace(UNPRINTABLE, ""));
|
|
37
|
+
}
|
|
38
|
+
/** The same, for content that may already be split into styled spans. */
|
|
39
|
+
export function normalizeContent(content) {
|
|
40
|
+
if (typeof content === "string")
|
|
41
|
+
return normalizeWhitespace(content);
|
|
42
|
+
return content.map((seg) => ({ ...seg, content: normalizeWhitespace(seg.content) }));
|
|
43
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { FontStyle } from "../utils/pdf-object-manager.js";
|
|
2
|
+
import type { FontMetrics } from "../utils/font-metrics.js";
|
|
3
|
+
/**
|
|
4
|
+
* What to do with a word that is wider than the box it has to fit in.
|
|
5
|
+
*
|
|
6
|
+
* Until now: nothing. It stayed on its line and drew straight over whatever was beside it - which on an
|
|
7
|
+
* invoice means a mandatory field (§14 UStG) painted across its own label. CSS overflows too, so this
|
|
8
|
+
* was never a bug; it was a missing feature, and it is the one that bites in German.
|
|
9
|
+
*
|
|
10
|
+
* Two layers, and they are NOT alternatives - CSS stacks them the same way:
|
|
11
|
+
*
|
|
12
|
+
* 1. HYPHENATION splits a word at a linguistically valid point and marks it with a hyphen:
|
|
13
|
+
* `Rechtsschutzver-` / `sicherungsgesellschaften`. It needs language knowledge, which is why it is
|
|
14
|
+
* PLUGGABLE rather than bundled - no pattern data in this package, no licence question, nothing
|
|
15
|
+
* added to a bundle that never asks for it.
|
|
16
|
+
* 2. BREAK-WORD is the floor: split wherever the box ends, no hyphen. It is what handles the things
|
|
17
|
+
* hyphenation cannot touch - an e-mail address, an IBAN, an invoice number. Those are single
|
|
18
|
+
* "words" with no valid point anywhere, and they are exactly what an invoice is full of.
|
|
19
|
+
*
|
|
20
|
+
* With both on, hyphenation is tried first and break-word catches what it cannot split.
|
|
21
|
+
*/
|
|
22
|
+
/** Splits one word into its hyphenation points, e.g. `["Rechts", "schutz", "ver", "siche", ...]`. */
|
|
23
|
+
export type Hyphenator = (word: string) => string[];
|
|
24
|
+
export interface WordSplitting {
|
|
25
|
+
/** Language-aware splitting, with a hyphen at the break. Supply `hyphen`, `hyphenopoly`, your own. */
|
|
26
|
+
hyphenate?: Hyphenator;
|
|
27
|
+
/** Split anywhere as a last resort, without a hyphen. CSS `overflow-wrap: break-word`. */
|
|
28
|
+
breakWord?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Splits `word` into pieces that each fit within `room`. Returns null when nothing applies - no
|
|
32
|
+
* splitting configured, or the word fits anyway - and the caller keeps its current behaviour, so a
|
|
33
|
+
* document that asks for neither is untouched.
|
|
34
|
+
*
|
|
35
|
+
* Every piece but the last is a finished line. The last is what carries on, so the caller keeps
|
|
36
|
+
* filling it as usual.
|
|
37
|
+
*/
|
|
38
|
+
export declare function splitLongWord(word: string, room: number, font: {
|
|
39
|
+
fontFamily: string;
|
|
40
|
+
fontSize: number;
|
|
41
|
+
fontStyle: FontStyle;
|
|
42
|
+
}, metrics: FontMetrics, letterSpacing: number, options: WordSplitting): string[] | null;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { runAdvance } from "./advance.js";
|
|
2
|
+
/** The hyphen actually drawn at a hyphenation break. U+002D, in every font we can encode. */
|
|
3
|
+
const HYPHEN = "-";
|
|
4
|
+
/**
|
|
5
|
+
* Splits `word` into pieces that each fit within `room`. Returns null when nothing applies - no
|
|
6
|
+
* splitting configured, or the word fits anyway - and the caller keeps its current behaviour, so a
|
|
7
|
+
* document that asks for neither is untouched.
|
|
8
|
+
*
|
|
9
|
+
* Every piece but the last is a finished line. The last is what carries on, so the caller keeps
|
|
10
|
+
* filling it as usual.
|
|
11
|
+
*/
|
|
12
|
+
export function splitLongWord(word, room, font, metrics, letterSpacing, options) {
|
|
13
|
+
const { hyphenate, breakWord } = options;
|
|
14
|
+
if (!hyphenate && !breakWord)
|
|
15
|
+
return null;
|
|
16
|
+
if (room <= 0)
|
|
17
|
+
return null;
|
|
18
|
+
if (runAdvance(metrics, word, font, letterSpacing) <= room)
|
|
19
|
+
return null;
|
|
20
|
+
const width = (s) => runAdvance(metrics, s, font, letterSpacing);
|
|
21
|
+
const pieces = [];
|
|
22
|
+
let rest = word;
|
|
23
|
+
// Bounded by construction: every pass either emits a piece and shortens `rest`, or gives up.
|
|
24
|
+
while (width(rest) > room) {
|
|
25
|
+
// The layering: a linguistically valid split if one is offered and reaches far enough, otherwise
|
|
26
|
+
// the blunt one - which is what an e-mail address or an IBAN needs, having no valid point at all.
|
|
27
|
+
const head = (hyphenate ? longestHyphenatedPrefix(rest, room, width, hyphenate) : null) ??
|
|
28
|
+
(breakWord ? longestPrefix(rest, room, width) : null);
|
|
29
|
+
// Nothing fits - not even one character, or hyphenation found nothing and break-word is off.
|
|
30
|
+
// Leave the remainder whole and let it overflow, exactly as before.
|
|
31
|
+
if (head === null)
|
|
32
|
+
break;
|
|
33
|
+
pieces.push(head.text);
|
|
34
|
+
rest = head.rest;
|
|
35
|
+
}
|
|
36
|
+
if (pieces.length === 0)
|
|
37
|
+
return null;
|
|
38
|
+
return [...pieces, rest];
|
|
39
|
+
}
|
|
40
|
+
/** The longest prefix that fits, plus a hyphen - split only where the hyphenator allows. */
|
|
41
|
+
function longestHyphenatedPrefix(word, room, width, hyphenate) {
|
|
42
|
+
const parts = hyphenate(word);
|
|
43
|
+
if (parts.length < 2)
|
|
44
|
+
return null;
|
|
45
|
+
let taken = "";
|
|
46
|
+
let best = null;
|
|
47
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
48
|
+
taken += parts[i];
|
|
49
|
+
// The hyphen has to fit too, or the line it lands on overflows by exactly that hyphen.
|
|
50
|
+
if (width(taken + HYPHEN) > room)
|
|
51
|
+
break;
|
|
52
|
+
best = { text: taken + HYPHEN, rest: parts.slice(i + 1).join("") };
|
|
53
|
+
}
|
|
54
|
+
return best;
|
|
55
|
+
}
|
|
56
|
+
/** The longest prefix that fits, split anywhere. Code points, so an astral character stays whole. */
|
|
57
|
+
function longestPrefix(word, room, width) {
|
|
58
|
+
const chars = [...word];
|
|
59
|
+
let taken = "";
|
|
60
|
+
let count = 0;
|
|
61
|
+
for (const char of chars) {
|
|
62
|
+
if (width(taken + char) > room)
|
|
63
|
+
break;
|
|
64
|
+
taken += char;
|
|
65
|
+
count += 1;
|
|
66
|
+
}
|
|
67
|
+
// Not even one character fits: the box is narrower than a single glyph. Splitting cannot help, and
|
|
68
|
+
// looping on it would never terminate.
|
|
69
|
+
if (count === 0 || count === chars.length)
|
|
70
|
+
return null;
|
|
71
|
+
return { text: taken, rest: chars.slice(count).join("") };
|
|
72
|
+
}
|
|
@@ -2,10 +2,8 @@ import { PDFElement, LayoutContext } from "../elements/pdf-element.js";
|
|
|
2
2
|
import { BoxConstraints, Offset, Size } from "../layout/box-constraints.js";
|
|
3
3
|
/** Distribution of the children ALONG the stacking (main) axis when there is leftover
|
|
4
4
|
* space and no flex child to absorb it. */
|
|
5
|
-
export type MainAlign
|
|
6
|
-
|
|
7
|
-
* the others place the child at its natural cross size. */
|
|
8
|
-
export type CrossAlign = "start" | "center" | "end" | "stretch";
|
|
5
|
+
export type { MainAlign, CrossAlign } from "../layout/alignment.js";
|
|
6
|
+
import type { CrossAlign, MainAlign } from "../layout/alignment.js";
|
|
9
7
|
/**
|
|
10
8
|
* Maps the abstract MAIN/CROSS axes onto concrete width/height + x/y, so one flex
|
|
11
9
|
* algorithm drives both a vertical Column (main = height) and a horizontal Row
|
|
@@ -42,6 +40,12 @@ export interface FlexOptions {
|
|
|
42
40
|
gap?: number;
|
|
43
41
|
main?: MainAlign;
|
|
44
42
|
cross?: CrossAlign;
|
|
43
|
+
/** Lay the children out along the main axis backwards (CSS `row-reverse` / `column-reverse`). */
|
|
44
|
+
reverse?: boolean;
|
|
45
|
+
/** Let the children flow onto further lines when they do not fit (CSS `flex-wrap: wrap`). */
|
|
46
|
+
wrap?: boolean;
|
|
47
|
+
/** How the BLOCK of wrapped lines sits across the axis (CSS `align-content`). Default `start`. */
|
|
48
|
+
alignContent?: MainAlign;
|
|
45
49
|
}
|
|
46
50
|
export declare class FlexLayoutHelper {
|
|
47
51
|
/**
|
|
@@ -53,8 +57,24 @@ export declare class FlexLayoutHelper {
|
|
|
53
57
|
* extent occupied. Vertical with `gap 0`, `main start`, `cross stretch` reproduces the
|
|
54
58
|
* previous Column layout exactly.
|
|
55
59
|
*/
|
|
60
|
+
/**
|
|
61
|
+
* Lays out the children along `axis`, on one line or several.
|
|
62
|
+
*
|
|
63
|
+
* Without `wrap` this is the single-line engine and nothing about it changed - the call goes
|
|
64
|
+
* straight through, which is what keeps every existing document byte-identical. With `wrap` the
|
|
65
|
+
* children are split into lines first and the SAME engine runs once per line.
|
|
66
|
+
*/
|
|
56
67
|
static layout(children: PDFElement[], axis: FlexAxis, mainAvail: number, crossAvail: number, mainStart: number, crossOrigin: number, options: FlexOptions, ctx: LayoutContext): {
|
|
57
68
|
mainUsed: number;
|
|
58
69
|
crossUsed: number;
|
|
59
70
|
};
|
|
71
|
+
/**
|
|
72
|
+
* The wrapping path: split into lines, lay each one out, then place the lines across the axis.
|
|
73
|
+
*
|
|
74
|
+
* Lines are laid out TWICE - once to learn how tall each is, once at its final cross position. That
|
|
75
|
+
* is the same measure-then-place shape the single-line engine already uses, and it is what
|
|
76
|
+
* `alignContent` needs: the block of lines can only be centred once its total is known.
|
|
77
|
+
*/
|
|
78
|
+
private static layoutWrapped;
|
|
79
|
+
private static layoutLine;
|
|
60
80
|
}
|
|
@@ -24,6 +24,20 @@ function crossOffset(align, crossExtent, childCross) {
|
|
|
24
24
|
return Math.max(0, crossExtent - childCross);
|
|
25
25
|
return 0; // start, stretch (stretch fills, so no offset)
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* The order children are LAID OUT in: by `order` (lowest first, ties keeping source order), then
|
|
29
|
+
* reversed if the container asks for it. The element tree itself is never touched, so the reading order
|
|
30
|
+
* a tagged PDF exposes stays the source order - which is what CSS says too.
|
|
31
|
+
*
|
|
32
|
+
* `sort` is stable in every engine we target, so equal `order` values keep their source positions and a
|
|
33
|
+
* container where nobody sets one comes back with the identical array.
|
|
34
|
+
*/
|
|
35
|
+
function inLayoutOrder(children, reverse) {
|
|
36
|
+
const ordered = children.some((c) => c.order !== 0)
|
|
37
|
+
? [...children].sort((a, b) => a.order - b.order)
|
|
38
|
+
: children;
|
|
39
|
+
return reverse ? [...ordered].reverse() : ordered;
|
|
40
|
+
}
|
|
27
41
|
export class FlexLayoutHelper {
|
|
28
42
|
/**
|
|
29
43
|
* Lays out a flex line along `axis`, IN SOURCE ORDER, and places every child.
|
|
@@ -34,10 +48,113 @@ export class FlexLayoutHelper {
|
|
|
34
48
|
* extent occupied. Vertical with `gap 0`, `main start`, `cross stretch` reproduces the
|
|
35
49
|
* previous Column layout exactly.
|
|
36
50
|
*/
|
|
51
|
+
/**
|
|
52
|
+
* Lays out the children along `axis`, on one line or several.
|
|
53
|
+
*
|
|
54
|
+
* Without `wrap` this is the single-line engine and nothing about it changed - the call goes
|
|
55
|
+
* straight through, which is what keeps every existing document byte-identical. With `wrap` the
|
|
56
|
+
* children are split into lines first and the SAME engine runs once per line.
|
|
57
|
+
*/
|
|
37
58
|
static layout(children, axis, mainAvail, crossAvail, mainStart, crossOrigin, options, ctx) {
|
|
59
|
+
if (!options.wrap || children.length === 0 || !Number.isFinite(mainAvail)) {
|
|
60
|
+
// An unbounded main axis has no edge to wrap at, so wrapping there is meaningless, not an error.
|
|
61
|
+
return FlexLayoutHelper.layoutLine(children, axis, mainAvail, crossAvail, mainStart, crossOrigin, options, ctx);
|
|
62
|
+
}
|
|
63
|
+
return FlexLayoutHelper.layoutWrapped(children, axis, mainAvail, crossAvail, mainStart, crossOrigin, options, ctx);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The wrapping path: split into lines, lay each one out, then place the lines across the axis.
|
|
67
|
+
*
|
|
68
|
+
* Lines are laid out TWICE - once to learn how tall each is, once at its final cross position. That
|
|
69
|
+
* is the same measure-then-place shape the single-line engine already uses, and it is what
|
|
70
|
+
* `alignContent` needs: the block of lines can only be centred once its total is known.
|
|
71
|
+
*/
|
|
72
|
+
static layoutWrapped(children, axis, mainAvail, crossAvail, mainStart, crossOrigin, options, ctx) {
|
|
73
|
+
const gap = options.gap ?? 0;
|
|
74
|
+
const ordered = inLayoutOrder(children, options.reverse ?? false);
|
|
75
|
+
// Which children share a line. A child is measured at its natural main extent; a flex child
|
|
76
|
+
// contributes its BASIS, which is 0 unless it asked for one - so it never forces a break by itself.
|
|
77
|
+
// A child's natural main extent. A PERCENTAGE child has no fixed answer: its size depends on how
|
|
78
|
+
// many gaps the line ends up with, which depends on who is on it. So it is asked again for every
|
|
79
|
+
// candidate membership below rather than measured once.
|
|
80
|
+
// A plain child's extent does not depend on the line, so it is laid out ONCE and remembered.
|
|
81
|
+
// Without this each membership probe re-lays every child already on the line - O(n^2) subtree
|
|
82
|
+
// layouts, which is expensive the moment those children hold a Table or a wrapping paragraph.
|
|
83
|
+
const measured = new Map();
|
|
84
|
+
const naturalMain = (child, base) => {
|
|
85
|
+
if (child instanceof FlexiblePDFElement)
|
|
86
|
+
return child.getBasis(base);
|
|
87
|
+
const factor = child.relativeSizeFactor(axis.mainHorizontal);
|
|
88
|
+
if (factor !== undefined)
|
|
89
|
+
return base * factor;
|
|
90
|
+
const cached = measured.get(child);
|
|
91
|
+
if (cached !== undefined)
|
|
92
|
+
return cached;
|
|
93
|
+
// Measured UNCAPPED, the same way the single-line engine measures a plain child. Handing the
|
|
94
|
+
// line width in as a cap would silently squash a child wider than the line - CSS lets such a
|
|
95
|
+
// child overflow, and so does our own non-wrapping path.
|
|
96
|
+
const needsBound = child.needsBoundedMain(axis.mainHorizontal);
|
|
97
|
+
const main = axis.mainOf(child.calculateLayout(axis.measureConstraints(crossAvail, needsBound ? mainAvail : Infinity), axis.offsetAt(mainStart, crossOrigin), ctx));
|
|
98
|
+
measured.set(child, main);
|
|
99
|
+
return main;
|
|
100
|
+
};
|
|
101
|
+
/** What a line of these children would occupy, resolving every `%` against ITS gap count. */
|
|
102
|
+
const lineExtent = (members) => {
|
|
103
|
+
const gaps = Math.max(0, members.length - 1) * gap;
|
|
104
|
+
const base = Math.max(0, mainAvail - gaps);
|
|
105
|
+
return members.reduce((n, c) => n + naturalMain(c, base), 0) + gaps;
|
|
106
|
+
};
|
|
107
|
+
const lines = [[]];
|
|
108
|
+
for (const child of ordered) {
|
|
109
|
+
const current = lines[lines.length - 1];
|
|
110
|
+
// A child wider than a whole line still gets one of its own rather than an empty line before it.
|
|
111
|
+
if (current.length > 0 && lineExtent([...current, child]) > mainAvail) {
|
|
112
|
+
lines.push([child]);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
current.push(child);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Each line keeps SOURCE order internally: the ordering was applied above, and applying it again
|
|
119
|
+
// per line would reverse twice.
|
|
120
|
+
const lineOptions = { ...options, wrap: false, reverse: false };
|
|
121
|
+
const measure = lines.map((line) => FlexLayoutHelper.layoutLine(line, axis, mainAvail, Infinity, // let each line report the cross extent it actually needs
|
|
122
|
+
mainStart, crossOrigin, lineOptions, ctx));
|
|
123
|
+
const heights = measure.map((m) => m.crossUsed);
|
|
124
|
+
const totalCross = heights.reduce((n, h) => n + h, 0) + Math.max(0, lines.length - 1) * gap;
|
|
125
|
+
// `alignContent` distributes the BLOCK of lines across the axis, using the same vocabulary the main
|
|
126
|
+
// axis uses. Only meaningful in a bounded cross axis with room left over.
|
|
127
|
+
const slack = Number.isFinite(crossAvail) ? crossAvail - totalCross : 0;
|
|
128
|
+
const align = options.alignContent ?? "start";
|
|
129
|
+
let cursor = crossOrigin;
|
|
130
|
+
let between = gap;
|
|
131
|
+
if (slack > 0) {
|
|
132
|
+
if (align === "center")
|
|
133
|
+
cursor += slack / 2;
|
|
134
|
+
else if (align === "end")
|
|
135
|
+
cursor += slack;
|
|
136
|
+
else if (align === "between" && lines.length > 1)
|
|
137
|
+
between = gap + slack / (lines.length - 1);
|
|
138
|
+
else if (align === "around") {
|
|
139
|
+
const unit = slack / lines.length;
|
|
140
|
+
cursor += unit / 2;
|
|
141
|
+
between = gap + unit;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
lines.forEach((line, i) => {
|
|
145
|
+
FlexLayoutHelper.layoutLine(line, axis, mainAvail, heights[i], // the line's own extent, so `stretch` fills the line and not the container
|
|
146
|
+
mainStart, cursor, lineOptions, ctx);
|
|
147
|
+
cursor += heights[i];
|
|
148
|
+
if (i < lines.length - 1)
|
|
149
|
+
cursor += between;
|
|
150
|
+
});
|
|
151
|
+
return { mainUsed: mainAvail, crossUsed: Math.max(totalCross, cursor - crossOrigin) };
|
|
152
|
+
}
|
|
153
|
+
static layoutLine(children, axis, mainAvail, crossAvail, mainStart, crossOrigin, options, ctx) {
|
|
38
154
|
const gap = options.gap ?? 0;
|
|
39
155
|
const main = options.main ?? "start";
|
|
40
156
|
const cross = options.cross ?? "stretch";
|
|
157
|
+
children = inLayoutOrder(children, options.reverse ?? false);
|
|
41
158
|
const count = children.length;
|
|
42
159
|
const totalGap = Math.max(0, count - 1) * gap;
|
|
43
160
|
// The main extent a percentage child resolves against: the line's own extent minus the gaps
|
|
@@ -48,7 +165,13 @@ export class FlexLayoutHelper {
|
|
|
48
165
|
// The main cap to hand a child: `percentBase` for a percentage child (so its fraction resolves) and
|
|
49
166
|
// for a child that cannot lay itself out without a bound (a nested stack holding an `Expanded`/
|
|
50
167
|
// `Spacer`); unbounded for everyone else, who keep their natural size and never fill the line.
|
|
168
|
+
// Filled by the shrink pass below; read by `mainCapFor` in pass 2. Declared here so the closure
|
|
169
|
+
// can see it - it is empty for every document that does not ask for shrinking.
|
|
170
|
+
const shrunkCap = new Map();
|
|
51
171
|
const mainCapFor = (child) => {
|
|
172
|
+
const target = shrunkCap.get(child);
|
|
173
|
+
if (target !== undefined)
|
|
174
|
+
return target;
|
|
52
175
|
if (percentBase === Infinity)
|
|
53
176
|
return Infinity;
|
|
54
177
|
const needsBound = child.relativeSizeFactor(axis.mainHorizontal) !== undefined ||
|
|
@@ -56,13 +179,17 @@ export class FlexLayoutHelper {
|
|
|
56
179
|
return needsBound ? percentBase : Infinity;
|
|
57
180
|
};
|
|
58
181
|
// Pass 1: measure the fixed children (main extent + cross size) and total the flex.
|
|
182
|
+
// A flex child's BASIS is reserved here too: it is main extent the leftover no longer contains,
|
|
183
|
+
// exactly like a fixed child's size. With the default basis of 0 this line adds nothing.
|
|
59
184
|
let fixedMain = 0;
|
|
60
185
|
let totalFlex = 0;
|
|
186
|
+
let totalBasis = 0;
|
|
61
187
|
let crossUsed = 0;
|
|
62
188
|
const fixedSize = new Map();
|
|
63
189
|
for (const child of children) {
|
|
64
190
|
if (child instanceof FlexiblePDFElement) {
|
|
65
191
|
totalFlex += child.getFlex();
|
|
192
|
+
totalBasis += child.getBasis(percentBase);
|
|
66
193
|
}
|
|
67
194
|
else {
|
|
68
195
|
const size = child.calculateLayout(axis.measureConstraints(crossAvail, mainCapFor(child)), axis.offsetAt(mainStart, crossOrigin), ctx);
|
|
@@ -71,7 +198,79 @@ export class FlexLayoutHelper {
|
|
|
71
198
|
fixedSize.set(child, size);
|
|
72
199
|
}
|
|
73
200
|
}
|
|
74
|
-
|
|
201
|
+
// Shrinking: the line overflows and some children said they would give space back. CSS weights a
|
|
202
|
+
// shrinker's share by `shrink x its own size`, so a big item yields more than a small one - which is
|
|
203
|
+
// why this cannot reuse the grow maths above. Nobody shrinks by default, so a document that never
|
|
204
|
+
// asks for it never enters this block.
|
|
205
|
+
const shrinkers = children.filter((c) => c.flexShrink > 0 && !(c instanceof FlexiblePDFElement));
|
|
206
|
+
const overflow = fixedMain + totalBasis + totalGap - mainAvail;
|
|
207
|
+
if (shrinkers.length > 0 && Number.isFinite(mainAvail) && overflow > 0) {
|
|
208
|
+
// Every child is floored at what it can actually hold - its `min-content` extent, which is CSS's
|
|
209
|
+
// automatic `min-width` on a flex item. Without it a badly overflowing line would grind a
|
|
210
|
+
// paragraph down to one word per line: the box gets narrow, the text gets tall, and nothing is
|
|
211
|
+
// gained. Past that floor a child overflows again, which is what a browser does too.
|
|
212
|
+
const naturalOf = (c) => axis.mainOf(fixedSize.get(c));
|
|
213
|
+
const floorOf = new Map(shrinkers.map((c) => [
|
|
214
|
+
c,
|
|
215
|
+
// Never above its natural size: the shrink pass may only take space away, never hand it out.
|
|
216
|
+
// Never below zero either - when the GAPS alone outgrow the line a proportional share asks
|
|
217
|
+
// for more than a child has, and a negative constraint is nonsense even though
|
|
218
|
+
// `constrainWidth` would floor it further down anyway.
|
|
219
|
+
Math.min(naturalOf(c), Math.max(0, c.minIntrinsicMain(axis.mainHorizontal, ctx))),
|
|
220
|
+
]));
|
|
221
|
+
// CSS resolves this iteratively (Flexbox 9.7): a child that lands on its floor is FROZEN there
|
|
222
|
+
// and the share it could not absorb is handed to the rest. A single pass would leave that share
|
|
223
|
+
// unspent, so one unsqueezable child would keep the whole line overflowing while its neighbours
|
|
224
|
+
// still had room to give.
|
|
225
|
+
const target = new Map(shrinkers.map((c) => [c, naturalOf(c)]));
|
|
226
|
+
const frozen = new Set();
|
|
227
|
+
let flexible = [...shrinkers];
|
|
228
|
+
// What the frozen ones took off the overflow; the rest is always shared out from scratch, never
|
|
229
|
+
// from what a previous round already assigned.
|
|
230
|
+
let absorbedByFrozen = 0;
|
|
231
|
+
while (flexible.length > 0) {
|
|
232
|
+
const toAbsorb = overflow - absorbedByFrozen;
|
|
233
|
+
if (toAbsorb <= 1e-9)
|
|
234
|
+
break;
|
|
235
|
+
const weight = (c) => c.flexShrink * naturalOf(c);
|
|
236
|
+
const totalWeight = flexible.reduce((n, c) => n + weight(c), 0);
|
|
237
|
+
if (totalWeight <= 0)
|
|
238
|
+
break;
|
|
239
|
+
const froze = [];
|
|
240
|
+
for (const child of flexible) {
|
|
241
|
+
const natural = naturalOf(child);
|
|
242
|
+
const floor = floorOf.get(child);
|
|
243
|
+
const wanted = natural - (weight(child) / totalWeight) * toAbsorb;
|
|
244
|
+
if (wanted < floor) {
|
|
245
|
+
target.set(child, floor);
|
|
246
|
+
froze.push(child);
|
|
247
|
+
absorbedByFrozen += natural - floor;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
target.set(child, wanted);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (froze.length === 0)
|
|
254
|
+
break;
|
|
255
|
+
for (const c of froze)
|
|
256
|
+
frozen.add(c);
|
|
257
|
+
flexible = flexible.filter((c) => !frozen.has(c));
|
|
258
|
+
}
|
|
259
|
+
for (const child of shrinkers) {
|
|
260
|
+
const natural = naturalOf(child);
|
|
261
|
+
const capped = target.get(child);
|
|
262
|
+
if (capped >= natural)
|
|
263
|
+
continue;
|
|
264
|
+
shrunkCap.set(child, capped);
|
|
265
|
+
// Re-measure NOW, not in pass 2: a narrower child may wrap to more lines, and the line's
|
|
266
|
+
// cross extent is settled just below.
|
|
267
|
+
const size = child.calculateLayout(axis.measureConstraints(crossAvail, capped), axis.offsetAt(mainStart, crossOrigin), ctx);
|
|
268
|
+
fixedMain += axis.mainOf(size) - natural;
|
|
269
|
+
crossUsed = Math.max(crossUsed, axis.crossOf(size));
|
|
270
|
+
fixedSize.set(child, size);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const leftover = mainAvail - fixedMain - totalBasis - totalGap;
|
|
75
274
|
// A flex child on an UNBOUNDED main axis has no leftover space to claim. It must collapse to zero,
|
|
76
275
|
// never to `Infinity`: an infinite extent would become the offset of every following sibling and get
|
|
77
276
|
// written into the content stream verbatim, silently corrupting the page from that point on.
|
|
@@ -97,7 +296,7 @@ export class FlexLayoutHelper {
|
|
|
97
296
|
if (totalFlex > 0) {
|
|
98
297
|
for (const child of children) {
|
|
99
298
|
if (child instanceof FlexiblePDFElement) {
|
|
100
|
-
const mainExtent = (child.getFlex() / totalFlex) * remaining;
|
|
299
|
+
const mainExtent = child.getBasis(percentBase) + (child.getFlex() / totalFlex) * remaining;
|
|
101
300
|
const size = child.calculateLayout(axis.flexConstraints(mainExtent, crossAvail), axis.offsetAt(mainStart, crossOrigin), ctx);
|
|
102
301
|
crossUsed = Math.max(crossUsed, axis.crossOf(size));
|
|
103
302
|
}
|
|
@@ -108,20 +307,32 @@ export class FlexLayoutHelper {
|
|
|
108
307
|
// `stretch` caps a child's cross to `crossExtent` (not `crossAvail`) so siblings end up
|
|
109
308
|
// equal across the axis. Bounded lines have crossExtent == crossAvail (byte-identical);
|
|
110
309
|
// only an unbounded line (a shrink-wrap Row) now equalises instead of staying natural.
|
|
111
|
-
|
|
112
|
-
|
|
310
|
+
// A child may override the container with its own `alignSelf` (CSS), so the decision is per child.
|
|
311
|
+
const alignFor = (child) => child.alignSelf ?? cross;
|
|
312
|
+
// Pass 2: place each child at the running main position, offset across by its alignment.
|
|
113
313
|
let mainPos = mainStart + leadingSpace;
|
|
114
314
|
let placedCross = 0;
|
|
115
315
|
children.forEach((child, index) => {
|
|
316
|
+
const align = alignFor(child);
|
|
317
|
+
const stretch = align === "stretch";
|
|
116
318
|
let mainExtent;
|
|
117
319
|
if (child instanceof FlexiblePDFElement) {
|
|
118
|
-
|
|
119
|
-
|
|
320
|
+
// `totalFlex` is 0 when every flex child asked for `flex: 0` (a pure `flexBasis` slot). The
|
|
321
|
+
// share is then 0/0 - a NaN that becomes the offset of every later sibling, which is the
|
|
322
|
+
// shape of the old Spacer bug. Such a child simply takes its basis.
|
|
323
|
+
const share = totalFlex > 0 ? (child.getFlex() / totalFlex) * remaining : 0;
|
|
324
|
+
mainExtent = child.getBasis(percentBase) + share;
|
|
325
|
+
// A flex child fills the MAIN axis, and its cross size is only known after layout - there is
|
|
326
|
+
// nothing to align against. So alignSelf is a no-op here, and that has to hold for the CROSS
|
|
327
|
+
// CONSTRAINT too: reading the per-child alignment would hand an `alignSelf: "start"` flex child
|
|
328
|
+
// an unbounded cross axis on a shrink-wrapping line, where the container's own rule gives it
|
|
329
|
+
// the line's extent. A no-op that changes the constraints is not a no-op.
|
|
330
|
+
const size = child.calculateLayout(axis.flexConstraints(mainExtent, cross === "stretch" ? crossExtent : crossAvail), axis.offsetAt(mainPos, crossOrigin), ctx);
|
|
120
331
|
placedCross = Math.max(placedCross, axis.crossOf(size));
|
|
121
332
|
}
|
|
122
333
|
else {
|
|
123
334
|
const childCross = axis.crossOf(fixedSize.get(child));
|
|
124
|
-
const size = child.calculateLayout(axis.measureConstraints(stretch ? crossExtent : crossAvail, mainCapFor(child)), axis.offsetAt(mainPos, crossOrigin + crossOffset(
|
|
335
|
+
const size = child.calculateLayout(axis.measureConstraints(stretch ? crossExtent : crossAvail, mainCapFor(child)), axis.offsetAt(mainPos, crossOrigin + crossOffset(align, crossExtent, childCross)), ctx);
|
|
125
336
|
mainExtent = axis.mainOf(size);
|
|
126
337
|
placedCross = Math.max(placedCross, axis.crossOf(size));
|
|
127
338
|
}
|
|
@@ -9,7 +9,7 @@ import type { FontDecoration } from "../text/text-decoration.js";
|
|
|
9
9
|
* touching layout code.
|
|
10
10
|
*/
|
|
11
11
|
export interface FontMetrics {
|
|
12
|
-
getStringWidth(text: string, fontFamily: string, fontSize: number, fontStyle: FontStyle): number;
|
|
12
|
+
getStringWidth(text: string, fontFamily: string, fontSize: number, fontStyle: FontStyle, ligatures?: boolean): number;
|
|
13
13
|
getCharWidth(char: string, fontSize: number, fullFontName?: string, fontName?: string, fontStyle?: FontStyle): number;
|
|
14
14
|
/** Ascent / descent / lineGap of a face, in em fractions - the vertical counterpart of the widths
|
|
15
15
|
* above. Drives the line box and the baseline (see `text/line-metrics.ts`). */
|
|
@@ -22,5 +22,16 @@ export interface FontMetrics {
|
|
|
22
22
|
readonly kerningEnabled: boolean;
|
|
23
23
|
/** Per-adjacent-pair kerning of `text`, in em/1000 (negative tightens); length `codePoints - 1`,
|
|
24
24
|
* zero next to a space. Only meaningful when `kerningEnabled`. */
|
|
25
|
-
getKernPairs(text: string, fontFamily: string, fontStyle: FontStyle): number[];
|
|
25
|
+
getKernPairs(text: string, fontFamily: string, fontStyle: FontStyle, ligatures?: boolean): number[];
|
|
26
|
+
/** Whether this family can draw the code point at all - what picks a face out of a fallback stack. */
|
|
27
|
+
hasGlyph(codePoint: number, fontFamily: string, fontStyle: FontStyle): boolean;
|
|
28
|
+
/** Whether the document draws this code point as colour emoji rather than from the text font - it
|
|
29
|
+
* is missing from that font on purpose, so glyph coverage must not touch it. */
|
|
30
|
+
rendersAsEmoji?(codePoint: number, fontFamily: string, fontStyle: FontStyle): boolean;
|
|
31
|
+
/** Told when a code point had to be removed because no font could draw it. Optional: a metrics
|
|
32
|
+
* object used only for measuring (a test double) has nothing to report to. */
|
|
33
|
+
reportMissingGlyph?(codePoints: number[]): void;
|
|
34
|
+
/** How many glyphs the run will DRAW, when a ligature made that differ from its code-point count.
|
|
35
|
+
* Absent or `undefined` means "the same", which is every Latin run. */
|
|
36
|
+
shapedGlyphCount?(text: string, fontFamily?: string, fontStyle?: FontStyle, ligatures?: boolean): number | undefined;
|
|
26
37
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export declare class GsubTable {
|
|
2
|
+
private data;
|
|
3
|
+
private base;
|
|
4
|
+
private scriptList;
|
|
5
|
+
private featureList;
|
|
6
|
+
private lookupList;
|
|
7
|
+
private lookupCache;
|
|
8
|
+
private coverageCache;
|
|
9
|
+
constructor(data: Uint8Array, base: number);
|
|
10
|
+
/** Whether the font carries this script at all - `arab` is the question the shaper asks. */
|
|
11
|
+
hasScript(script: string): boolean;
|
|
12
|
+
/** A feature's lookup indices, under the script's DEFAULT language system - Arabic shaping features
|
|
13
|
+
* are script-wide, so a language-specific override would only add typographic niceties. */
|
|
14
|
+
lookups(script: string, feature: string): number[];
|
|
15
|
+
/** Type 1: substitute one glyph. `null` when no subtable covers it - the feature does not apply. */
|
|
16
|
+
substituteSingle(lookupIndex: number, glyph: number): number | null;
|
|
17
|
+
/**
|
|
18
|
+
* Type 4: collapse a run of glyphs into one, starting at `at`. Longest match wins, as the spec
|
|
19
|
+
* requires - a three-glyph ligature beats the two-glyph one that starts the same way.
|
|
20
|
+
*/
|
|
21
|
+
substituteLigature(lookupIndex: number, glyphs: readonly number[], at: number): {
|
|
22
|
+
glyph: number;
|
|
23
|
+
consumed: number;
|
|
24
|
+
} | null;
|
|
25
|
+
/** The type of a lookup, for reporting what a font needs that we do not do. */
|
|
26
|
+
lookupType(lookupIndex: number): number | undefined;
|
|
27
|
+
private scriptOffset;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a lookup, unwrapping type 7 (extension) - which exists only because the 16-bit offsets
|
|
30
|
+
* elsewhere cannot reach far enough in a big font. Callers never learn it was wrapped.
|
|
31
|
+
*/
|
|
32
|
+
private lookup;
|
|
33
|
+
/** Coverage table -> glyph id to its INDEX in the table, which is how subtables address glyphs. */
|
|
34
|
+
private coverage;
|
|
35
|
+
private cacheCoverage;
|
|
36
|
+
}
|