reamkit 1.34.0 → 1.35.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/README.md CHANGED
@@ -30,7 +30,8 @@ Parse once into the format-neutral interlayer, convert to any target. The
30
30
  format is sniffed from the bytes; no fonts to wire up — an open
31
31
  metric-compatible substitute font (Arimo for sans, Tinos for serif, Cousine for
32
32
  monospace, plus Carlito/Caladea for Calibri/Cambria — the same families LibreOffice
33
- substitutes) is fetched automatically based on the document's referenced fonts,
33
+ substitutes, with Cambria's Cyrillic and Greek from Noto Serif) is fetched
34
+ automatically based on the document's referenced fonts,
34
35
  with a Noto face for the Japanese, Korean, Chinese, Arabic, Hebrew, Thai or
35
36
  Indic (Hindi, Bengali, Tamil and six more scripts) text the document holds:
36
37
 
@@ -195,6 +195,21 @@ export declare class Ream {
195
195
  * @returns The font bytes and, for docx, optional per-family registries.
196
196
  */
197
197
  private resolveFonts;
198
+ /**
199
+ * Fetch the family each twin borrows the letters it lacks from: Caladea,
200
+ * Cambria's twin, has no Cyrillic and no Greek, and without its companion a
201
+ * Cambria run set every Russian or Kazakh letter in whichever face was loaded
202
+ * first — a grotesque beside the roman around it.
203
+ *
204
+ * A companion that fails to arrive is a recorded loss, not a throw: its
205
+ * letters still fall to another face on hand.
206
+ *
207
+ * @param into The registry map the run resolver looks in.
208
+ * @param companions The companion families the document's text needs.
209
+ * @param fetchOpt The injectable `fetch`, when the caller gave one.
210
+ * @param losses Where a companion that could not be fetched records itself.
211
+ */
212
+ private addCompanionFonts;
198
213
  /**
199
214
  * Fetch one face per writing system the document holds text in — the curated
200
215
  * families are Latin, and Han, Kana, Hangul, Arabic, the scripts of India and
@@ -1,5 +1,5 @@
1
1
  import { fetchFontSet, fetchScriptFont, resolveFamilyKey } from "../fonts/remote-fonts.js";
2
- import { scriptsInFlow } from "../fonts/scripts.js";
2
+ import { companionsInFlow, scriptsInFlow } from "../fonts/scripts.js";
3
3
  import { FontRegistry } from "../font/font-registry.js";
4
4
  import { createFontMeasure } from "../font/measure.js";
5
5
  import "../font/index.js";
@@ -287,7 +287,8 @@ var Ream = class Ream {
287
287
  }
288
288
  const fetchOpt = options.fontFetch ? { fetch: options.fontFetch } : {};
289
289
  const keys = options.fontFamily ? /* @__PURE__ */ new Set([resolveFamilyKey(options.fontFamily)]) : familiesInFlow(flow);
290
- if (keys.size <= 1 && scriptsInFlow(flow).scripts.size === 0) {
290
+ const companions = companionsInFlow(flow, keys);
291
+ if (keys.size <= 1 && companions.size === 0 && scriptsInFlow(flow).scripts.size === 0) {
291
292
  const family = keys.values().next().value;
292
293
  return { fonts: await fetchFontSet({
293
294
  ...family ? { family } : {},
@@ -304,6 +305,7 @@ var Ream = class Ream {
304
305
  registriesByFamily.set(key, FontRegistry.fromBytes(bytes));
305
306
  if (key === "arimo" || !baseBytes) baseBytes = bytes;
306
307
  }
308
+ await this.addCompanionFonts(registriesByFamily, companions, fetchOpt, losses);
307
309
  await this.addScriptFonts(registriesByFamily, flow, options, losses);
308
310
  return {
309
311
  fonts: baseBytes,
@@ -311,6 +313,37 @@ var Ream = class Ream {
311
313
  };
312
314
  }
313
315
  /**
316
+ * Fetch the family each twin borrows the letters it lacks from: Caladea,
317
+ * Cambria's twin, has no Cyrillic and no Greek, and without its companion a
318
+ * Cambria run set every Russian or Kazakh letter in whichever face was loaded
319
+ * first — a grotesque beside the roman around it.
320
+ *
321
+ * A companion that fails to arrive is a recorded loss, not a throw: its
322
+ * letters still fall to another face on hand.
323
+ *
324
+ * @param into The registry map the run resolver looks in.
325
+ * @param companions The companion families the document's text needs.
326
+ * @param fetchOpt The injectable `fetch`, when the caller gave one.
327
+ * @param losses Where a companion that could not be fetched records itself.
328
+ */
329
+ async addCompanionFonts(into, companions, fetchOpt, losses) {
330
+ for (const family of companions) {
331
+ if (into.has(family)) continue;
332
+ try {
333
+ into.set(family, FontRegistry.fromBytes(await fetchFontSet({
334
+ family,
335
+ ...fetchOpt
336
+ })));
337
+ } catch {
338
+ losses.push({
339
+ feature: "font",
340
+ detail: `the ${family} face could not be fetched; the letters it draws for a substitute fall to another face`,
341
+ severity: "substituted"
342
+ });
343
+ }
344
+ }
345
+ }
346
+ /**
314
347
  * Fetch one face per writing system the document holds text in — the curated
315
348
  * families are Latin, and Han, Kana, Hangul, Arabic, the scripts of India and
316
349
  * the geometric symbols are a notdef box in every one of them.
@@ -7,7 +7,7 @@ export { pickVariant } from './font-registry.js';
7
7
  export { parseGposKerning, parseGsubLigatures, shapeText } from './opentype-layout.js';
8
8
  export type { KerningMap, LigatureMap, ShapedRun } from './opentype-layout.js';
9
9
  export { hasSubstitutable, lettersForLigature, substituteLetters } from './ligatures.js';
10
- export { createFontMeasure, shapeGlyphs } from './measure.js';
10
+ export { createFontMeasure, shapeGlyphs, sharedGlyphCodes } from './measure.js';
11
11
  export type { FontMeasure } from './measure.js';
12
12
  export { buildTrueType, editableEmbedding, MAX_BUILT_GLYPHS } from './ttf-build.js';
13
13
  export type { BuiltFace, BuiltGlyph, BuiltKernPair, BuiltLigature, GlyphSeg, } from './ttf-build.js';
@@ -3,6 +3,6 @@ import { parseTtf } from "./ttf-parser.js";
3
3
  import { glyphClosure, subsetTtf } from "./ttf-subset.js";
4
4
  import { FontRegistry, pickVariant } from "./font-registry.js";
5
5
  import { hasSubstitutable, lettersForLigature, substituteLetters } from "./ligatures.js";
6
- import { createFontMeasure, shapeGlyphs } from "./measure.js";
6
+ import { createFontMeasure, shapeGlyphs, sharedGlyphCodes } from "./measure.js";
7
7
  import { MAX_BUILT_GLYPHS, buildTrueType, editableEmbedding } from "./ttf-build.js";
8
- export { FontRegistry, MAX_BUILT_GLYPHS, buildTrueType, createFontMeasure, editableEmbedding, glyphClosure, hasSubstitutable, lettersForLigature, parseTtf, pickVariant, shapeGlyphs, subsetTtf, substituteLetters };
8
+ export { FontRegistry, MAX_BUILT_GLYPHS, buildTrueType, createFontMeasure, editableEmbedding, glyphClosure, hasSubstitutable, lettersForLigature, parseTtf, pickVariant, shapeGlyphs, sharedGlyphCodes, subsetTtf, substituteLetters };
@@ -0,0 +1,27 @@
1
+ import { IndicRun } from './indic-shaper.js';
2
+ import { ParsedTtf } from './ttf-parser.js';
3
+ /**
4
+ * Whether `text` holds a combining mark of the blocks Latin, Greek and
5
+ * Cyrillic letters take — the text {@link shapeMarks} is for.
6
+ *
7
+ * @param text The text.
8
+ */
9
+ export declare function hasCombiningMark(text: string): boolean;
10
+ /**
11
+ * Whether a code point is one of those marks.
12
+ *
13
+ * @param cp The code point.
14
+ */
15
+ export declare function isCombiningMark(cp: number): boolean;
16
+ /**
17
+ * Shape a run whose letters carry combining marks, in one face: the face's
18
+ * substitutions, then its positioning, the marks set on their letters.
19
+ *
20
+ * @param parsed The face.
21
+ * @param text The text.
22
+ * @param kern Whether to apply the face's `kern` feature (the layout profile decides).
23
+ * @returns The glyphs and where each stands, or `undefined` when the text holds
24
+ * no such mark or the face has no positioning to set one with — the
25
+ * caller's own shaping applies then.
26
+ */
27
+ export declare function shapeMarks(parsed: ParsedTtf, text: string, kern?: boolean): IndicRun | undefined;
@@ -0,0 +1,116 @@
1
+ import { OtlFace, planLookups, position, substitute } from "./opentype-lookups.js";
2
+ //#region src/core/font/mark-shaper.ts
3
+ /**
4
+ * The combining marks a Latin, Greek or Cyrillic letter takes: the Combining
5
+ * Diacritical Marks and their Extended and Supplement blocks, the Cyrillic
6
+ * combining marks, the marks for symbols and the half marks.
7
+ */
8
+ var MARK_RANGES = [
9
+ [768, 879],
10
+ [1155, 1161],
11
+ [6832, 6911],
12
+ [7616, 7679],
13
+ [8400, 8447],
14
+ [65056, 65071]
15
+ ];
16
+ /** OpenType GDEF GlyphClassDef — the class a mark glyph is given. */
17
+ var MARK_CLASS = 3;
18
+ /**
19
+ * Whether `text` holds a combining mark of the blocks Latin, Greek and
20
+ * Cyrillic letters take — the text {@link shapeMarks} is for.
21
+ *
22
+ * @param text The text.
23
+ */
24
+ function hasCombiningMark(text) {
25
+ for (let i = 0; i < text.length; i++) if (isCombiningMark(text.charCodeAt(i))) return true;
26
+ return false;
27
+ }
28
+ /**
29
+ * Whether a code point is one of those marks.
30
+ *
31
+ * @param cp The code point.
32
+ */
33
+ function isCombiningMark(cp) {
34
+ if (cp < 768) return false;
35
+ for (const [lo, hi] of MARK_RANGES) if (cp >= lo && cp <= hi) return true;
36
+ return false;
37
+ }
38
+ /** The script tags a face's layout is looked up by, for the letters of `cps`. */
39
+ function scriptTags(cps) {
40
+ for (const cp of cps) {
41
+ if (cp >= 1024 && cp <= 1327) return ["cyrl"];
42
+ if (cp >= 880 && cp <= 1023 || cp >= 7936 && cp <= 8191) return ["grek"];
43
+ if (cp >= 65 && cp <= 591 || cp >= 7680 && cp <= 7935) return ["latn"];
44
+ }
45
+ return ["latn"];
46
+ }
47
+ var feature = (tag) => ({
48
+ tag,
49
+ mask: 1
50
+ });
51
+ /**
52
+ * Shape a run whose letters carry combining marks, in one face: the face's
53
+ * substitutions, then its positioning, the marks set on their letters.
54
+ *
55
+ * @param parsed The face.
56
+ * @param text The text.
57
+ * @param kern Whether to apply the face's `kern` feature (the layout profile decides).
58
+ * @returns The glyphs and where each stands, or `undefined` when the text holds
59
+ * no such mark or the face has no positioning to set one with — the
60
+ * caller's own shaping applies then.
61
+ */
62
+ function shapeMarks(parsed, text, kern = true) {
63
+ if (!hasCombiningMark(text)) return void 0;
64
+ const face = OtlFace.of(parsed);
65
+ if (!face.gpos) return void 0;
66
+ const cps = [...text].map((ch) => ch.codePointAt(0));
67
+ const tags = scriptTags(cps);
68
+ let base = 0;
69
+ const buf = cps.map((cp, i) => {
70
+ const mark = i > 0 && isCombiningMark(cp);
71
+ if (!mark) base = i;
72
+ const gid = parsed.glyphForCodepoint(cp);
73
+ const cls = face.glyphClass(gid) || (mark ? MARK_CLASS : 1);
74
+ return {
75
+ gid,
76
+ cluster: base,
77
+ mask: 1,
78
+ cls,
79
+ syllable: 0,
80
+ ligId: 0,
81
+ ligComp: 0,
82
+ sources: [i]
83
+ };
84
+ });
85
+ substitute(face, planLookups(face.gsub, tags, [
86
+ "ccmp",
87
+ "liga",
88
+ "clig"
89
+ ].map(feature)), buf);
90
+ const gpos = planLookups(face.gpos, tags, [
91
+ ...kern ? ["kern"] : [],
92
+ "mark",
93
+ "mkmk"
94
+ ].map(feature));
95
+ const pos = position(face, gpos, buf);
96
+ const run = {
97
+ gids: [],
98
+ clusters: [],
99
+ sources: [],
100
+ advances: [],
101
+ xOffsets: [],
102
+ yOffsets: []
103
+ };
104
+ buf.forEach((g, i) => {
105
+ const p = pos[i];
106
+ run.gids.push(g.gid);
107
+ run.clusters.push(g.cluster);
108
+ run.sources.push(g.sources ?? [g.cluster]);
109
+ run.advances.push(g.cls === MARK_CLASS ? 0 : p.xAdvance);
110
+ run.xOffsets.push(p.xOffset);
111
+ run.yOffsets.push(p.yOffset);
112
+ });
113
+ return run;
114
+ }
115
+ //#endregion
116
+ export { hasCombiningMark, isCombiningMark, shapeMarks };
@@ -80,6 +80,22 @@ export declare function shapeGlyphs(parsed: ParsedTtf, text: string): {
80
80
  readonly gids: ReadonlyArray<number>;
81
81
  readonly glyphText?: ReadonlyMap<number, string>;
82
82
  };
83
+ /**
84
+ * The characters a face's `cmap` sends to one glyph, when it sends more than
85
+ * one: a /ToUnicode can name only one of them, and which one is a question of
86
+ * what the document drew.
87
+ *
88
+ * Arimo, Tinos and Cousine draw the Latin schwa U+0259 and the Cyrillic one
89
+ * U+04D9 with the same glyph (U+018F and U+04D8 likewise), and a scan that
90
+ * keeps the first code reaching a glyph named it Latin: every "және" of a
91
+ * Kazakh document set in Arial read back as "жəне".
92
+ *
93
+ * @param parsed The face.
94
+ * @param gid The glyph.
95
+ * @returns The code points mapped to it, lowest first, or `undefined` when at
96
+ * most one is.
97
+ */
98
+ export declare function sharedGlyphCodes(parsed: ParsedTtf, gid: number): ReadonlyArray<number> | undefined;
83
99
  /**
84
100
  * Build a {@link FontMeasure} over a parsed font.
85
101
  *
@@ -1,5 +1,6 @@
1
1
  import { shapeText } from "./opentype-layout.js";
2
2
  import { hasIndic, shapeIndic } from "./indic-shaper.js";
3
+ import { hasCombiningMark, shapeMarks } from "./mark-shaper.js";
3
4
  //#region src/core/font/measure.ts
4
5
  var EMPTY_KERNING = /* @__PURE__ */ new Map();
5
6
  /** The most shaped words a face keeps; past it the cache starts over. */
@@ -15,7 +16,7 @@ var SHAPED_CACHE = 4096;
15
16
  * @returns The glyph ids, and the text of each glyph no character reaches.
16
17
  */
17
18
  function shapeGlyphs(parsed, text) {
18
- const run = hasIndic(text) ? shapeIndic(parsed, text) : void 0;
19
+ const run = hasIndic(text) ? shapeIndic(parsed, text) : hasCombiningMark(text) ? shapeMarks(parsed, text) : void 0;
19
20
  if (!run) return { gids: shapeText(text, parsed.glyphForCodepoint, parsed.advanceWidths, parsed.ligatures, parsed.kerning, parsed.joiningForms).gids };
20
21
  return {
21
22
  gids: run.gids,
@@ -41,6 +42,25 @@ function namesOf(parsed) {
41
42
  }
42
43
  return names;
43
44
  }
45
+ /**
46
+ * The characters a face's `cmap` sends to one glyph, when it sends more than
47
+ * one: a /ToUnicode can name only one of them, and which one is a question of
48
+ * what the document drew.
49
+ *
50
+ * Arimo, Tinos and Cousine draw the Latin schwa U+0259 and the Cyrillic one
51
+ * U+04D9 with the same glyph (U+018F and U+04D8 likewise), and a scan that
52
+ * keeps the first code reaching a glyph named it Latin: every "және" of a
53
+ * Kazakh document set in Arial read back as "жəне".
54
+ *
55
+ * @param parsed The face.
56
+ * @param gid The glyph.
57
+ * @returns The code points mapped to it, lowest first, or `undefined` when at
58
+ * most one is.
59
+ */
60
+ function sharedGlyphCodes(parsed, gid) {
61
+ const codes = namesOf(parsed).get(gid);
62
+ return codes !== void 0 && codes.length > 1 ? codes : void 0;
63
+ }
44
64
  /** A character's canonical parts: क़ is क and the nukta, ো is ে and া. */
45
65
  var partsOf = (cp) => [...String.fromCodePoint(cp).normalize("NFD")].map((ch) => ch.codePointAt(0));
46
66
  var IGNORABLE_RE = /\p{Default_Ignorable_Code_Point}/u;
@@ -184,6 +204,18 @@ function createFontMeasure(parsed, kern = true) {
184
204
  advances: run.advances,
185
205
  indic: run
186
206
  };
207
+ } else if (hasCombiningMark(text)) {
208
+ let run = indicRuns.get(text);
209
+ if (run === void 0) {
210
+ if (indicRuns.size >= SHAPED_CACHE) indicRuns.clear();
211
+ run = shapeMarks(parsed, text, kern) ?? null;
212
+ indicRuns.set(text, run);
213
+ }
214
+ if (run) return {
215
+ gids: run.gids,
216
+ advances: run.advances,
217
+ indic: run
218
+ };
187
219
  }
188
220
  return shapeText(text, parsed.glyphForCodepoint, parsed.advanceWidths, parsed.ligatures, kerning, parsed.joiningForms);
189
221
  };
@@ -343,4 +375,4 @@ function createFontMeasure(parsed, kern = true) {
343
375
  };
344
376
  }
345
377
  //#endregion
346
- export { createFontMeasure, shapeGlyphs };
378
+ export { createFontMeasure, shapeGlyphs, sharedGlyphCodes };
@@ -1,3 +1,3 @@
1
- export { fetchFontSet, fetchScriptFont, isScriptKey, knowsFamily, resolveFamilyKey, resolveFamilyStyle, clearFontCache, } from './remote-fonts.js';
1
+ export { companionOf, companionScale, fetchFontSet, fetchScriptFont, isScriptKey, knowsFamily, resolveFamilyKey, resolveFamilyStyle, clearFontCache, } from './remote-fonts.js';
2
2
  export { scriptForCodepoint, scriptsInFlow } from './scripts.js';
3
- export type { FamilyKey, FamilyStyle, FetchFontSetOptions, FetchLike, ScriptKey, SubstituteKey, } from './remote-fonts.js';
3
+ export type { Companion, FamilyKey, FamilyStyle, FetchFontSetOptions, FetchLike, ScriptKey, SubstituteKey, } from './remote-fonts.js';
@@ -1,6 +1,10 @@
1
- import { FontBytesByVariant } from '../font/index.js';
2
- /** A curated open substitute family (the Croscore + Carlito/Caladea set, like LibreOffice). */
3
- export type FamilyKey = 'arimo' | 'tinos' | 'cousine' | 'carlito' | 'caladea';
1
+ import { FontBytesByVariant, FontVariant } from '../font/index.js';
2
+ /**
3
+ * A curated open substitute family (the Croscore + Carlito/Caladea set, like
4
+ * LibreOffice), or the family a twin borrows the letters it lacks from (see
5
+ * {@link companionOf}).
6
+ */
7
+ export type FamilyKey = 'arimo' | 'tinos' | 'cousine' | 'carlito' | 'caladea' | 'notoserif';
4
8
  /**
5
9
  * A WRITING SYSTEM the curated five cannot draw at all. They are Latin families
6
10
  * — Greek, Cyrillic and Hebrew ride along, everything else is a notdef box —
@@ -26,6 +30,36 @@ export declare function isScriptKey(key: SubstituteKey): key is ScriptKey;
26
30
  * @returns The regular face, or `undefined` when the download fails.
27
31
  */
28
32
  export declare function fetchScriptFont(script: ScriptKey, fetchImpl?: FetchLike): Promise<FontBytesByVariant | undefined>;
33
+ /**
34
+ * The letters a metric twin does not draw, and the family it borrows them
35
+ * from — squeezed, per writing system and face, to the width of the face the
36
+ * twin stands in for.
37
+ */
38
+ export interface Companion {
39
+ /** The family that draws them. */
40
+ readonly family: FamilyKey;
41
+ /**
42
+ * The blocks it is borrowed for (inclusive ranges), each with the factor
43
+ * its advances are multiplied by in each face.
44
+ */
45
+ readonly ranges: ReadonlyArray<readonly [number, number, Readonly<Record<FontVariant, number>>]>;
46
+ }
47
+ /**
48
+ * The family a substitute borrows the letters it lacks from, if it borrows any.
49
+ *
50
+ * @param key The substitute.
51
+ * @returns Its companion, or `undefined`.
52
+ */
53
+ export declare function companionOf(key: string): Companion | undefined;
54
+ /**
55
+ * How much a companion squeezes a character it draws in a face.
56
+ *
57
+ * @param companion The companion.
58
+ * @param cp The code point.
59
+ * @param variant The face it is drawn in.
60
+ * @returns The factor, or `undefined` when the companion is not borrowed for it.
61
+ */
62
+ export declare function companionScale(companion: Companion, cp: number, variant: FontVariant): number | undefined;
29
63
  /** A family name, read: which substitute it maps to and what it says about the face. */
30
64
  export interface FamilyStyle {
31
65
  readonly key: FamilyKey;
@@ -109,8 +109,65 @@ var FAMILIES = {
109
109
  caladea: {
110
110
  pkg: "caladea",
111
111
  file: "Caladea"
112
+ },
113
+ notoserif: {
114
+ pkg: "noto-serif",
115
+ file: "NotoSerif",
116
+ nested: true
112
117
  }
113
118
  };
119
+ var CAMBRIA_CYRILLIC = {
120
+ regular: .894,
121
+ bold: .918,
122
+ italic: .9,
123
+ boldItalic: .915
124
+ };
125
+ var CAMBRIA_GREEK = {
126
+ regular: .932,
127
+ bold: .953,
128
+ italic: .909,
129
+ boldItalic: .938
130
+ };
131
+ var COMPANIONS = { caladea: {
132
+ family: "notoserif",
133
+ ranges: [
134
+ [
135
+ 880,
136
+ 1023,
137
+ CAMBRIA_GREEK
138
+ ],
139
+ [
140
+ 1024,
141
+ 1327,
142
+ CAMBRIA_CYRILLIC
143
+ ],
144
+ [
145
+ 7936,
146
+ 8191,
147
+ CAMBRIA_GREEK
148
+ ]
149
+ ]
150
+ } };
151
+ /**
152
+ * The family a substitute borrows the letters it lacks from, if it borrows any.
153
+ *
154
+ * @param key The substitute.
155
+ * @returns Its companion, or `undefined`.
156
+ */
157
+ function companionOf(key) {
158
+ return Object.hasOwn(COMPANIONS, key) ? COMPANIONS[key] : void 0;
159
+ }
160
+ /**
161
+ * How much a companion squeezes a character it draws in a face.
162
+ *
163
+ * @param companion The companion.
164
+ * @param cp The code point.
165
+ * @param variant The face it is drawn in.
166
+ * @returns The factor, or `undefined` when the companion is not borrowed for it.
167
+ */
168
+ function companionScale(companion, cp, variant) {
169
+ for (const [lo, hi, scale] of companion.ranges) if (cp >= lo && cp <= hi) return scale[variant];
170
+ }
114
171
  var EXACT = {
115
172
  calibri: "carlito",
116
173
  "calibri light": "carlito",
@@ -405,4 +462,4 @@ async function fetchFontSet(options = {}) {
405
462
  };
406
463
  }
407
464
  //#endregion
408
- export { fetchFontSet, fetchScriptFont, knowsFamily, resolveFamilyKey, resolveFamilyStyle };
465
+ export { companionOf, companionScale, fetchFontSet, fetchScriptFont, knowsFamily, resolveFamilyKey, resolveFamilyStyle };
@@ -1,5 +1,5 @@
1
1
  import { FlowDoc } from '../ir/flow.js';
2
- import { ScriptKey } from './remote-fonts.js';
2
+ import { FamilyKey, ScriptKey } from './remote-fonts.js';
3
3
  /**
4
4
  * The writing system a character needs a face for, or `undefined` when a Latin
5
5
  * family covers it.
@@ -43,3 +43,23 @@ export declare function scriptsInFlow(flow: FlowDoc): {
43
43
  scripts: Set<ScriptKey>;
44
44
  hanFace: ScriptKey;
45
45
  };
46
+ /**
47
+ * Hand every run's text in a document to `read`: the body, the bands, the
48
+ * notes and the comments, the cells of a table, the text of every shape and
49
+ * of the drawings over a spreadsheet grid.
50
+ *
51
+ * @param flow The parsed document.
52
+ * @param read Called with each run's text.
53
+ */
54
+ export declare function forEachTextInFlow(flow: FlowDoc, read: (text: string) => void): void;
55
+ /**
56
+ * The companions a document's families need: those whose blocks its text
57
+ * holds a character of (see `companionOf`). A Cambria document in Russian
58
+ * needs Noto Serif for its letters; one in English does not, and is not sent
59
+ * for it.
60
+ *
61
+ * @param flow The parsed document.
62
+ * @param families The substitute families it names.
63
+ * @returns The companion families to fetch.
64
+ */
65
+ export declare function companionsInFlow(flow: FlowDoc, families: Iterable<FamilyKey>): Set<FamilyKey>;
@@ -1,3 +1,4 @@
1
+ import { companionOf } from "./remote-fonts.js";
1
2
  //#region src/core/fonts/scripts.ts
2
3
  var RANGES = [
3
4
  [
@@ -252,6 +253,23 @@ function scriptsInFlow(flow) {
252
253
  scripts.add(key);
253
254
  }
254
255
  };
256
+ forEachTextInFlow(flow, read);
257
+ const hanFace = scripts.has("jp") ? "jp" : scripts.has("kr") ? "kr" : "sc";
258
+ if (marks.has("han")) scripts.add(hanFace);
259
+ return {
260
+ scripts,
261
+ hanFace
262
+ };
263
+ }
264
+ /**
265
+ * Hand every run's text in a document to `read`: the body, the bands, the
266
+ * notes and the comments, the cells of a table, the text of every shape and
267
+ * of the drawings over a spreadsheet grid.
268
+ *
269
+ * @param flow The parsed document.
270
+ * @param read Called with each run's text.
271
+ */
272
+ function forEachTextInFlow(flow, read) {
255
273
  const shapeText = (shape) => {
256
274
  if (shape.text) visit(shape.text.content);
257
275
  for (const child of shape.children ?? []) shapeText(child.shape);
@@ -268,12 +286,33 @@ function scriptsInFlow(flow) {
268
286
  for (const note of flow.footnotes?.values() ?? []) visit(note);
269
287
  for (const note of flow.endnotes?.values() ?? []) visit(note);
270
288
  for (const comment of flow.comments?.values() ?? []) visit(comment.content);
271
- const hanFace = scripts.has("jp") ? "jp" : scripts.has("kr") ? "kr" : "sc";
272
- if (marks.has("han")) scripts.add(hanFace);
273
- return {
274
- scripts,
275
- hanFace
276
- };
289
+ }
290
+ /**
291
+ * The companions a document's families need: those whose blocks its text
292
+ * holds a character of (see `companionOf`). A Cambria document in Russian
293
+ * needs Noto Serif for its letters; one in English does not, and is not sent
294
+ * for it.
295
+ *
296
+ * @param flow The parsed document.
297
+ * @param families The substitute families it names.
298
+ * @returns The companion families to fetch.
299
+ */
300
+ function companionsInFlow(flow, families) {
301
+ const wanted = [...families].flatMap((key) => {
302
+ const companion = companionOf(key);
303
+ return companion ? [companion] : [];
304
+ });
305
+ const out = /* @__PURE__ */ new Set();
306
+ if (wanted.length === 0) return out;
307
+ forEachTextInFlow(flow, (text) => {
308
+ if (out.size === wanted.length) return;
309
+ for (const ch of text) {
310
+ const cp = ch.codePointAt(0) ?? 0;
311
+ if (cp < 880) continue;
312
+ for (const c of wanted) if (c.ranges.some(([lo, hi]) => cp >= lo && cp <= hi)) out.add(c.family);
313
+ }
314
+ });
315
+ return out;
277
316
  }
278
317
  //#endregion
279
- export { fontSlotOf, scriptForCodepoint, scriptsInFlow };
318
+ export { companionsInFlow, fontSlotOf, forEachTextInFlow, scriptForCodepoint, scriptsInFlow };
@@ -528,10 +528,15 @@ function fontStack(family) {
528
528
  /** What follows a family in its stack, by the open substitute it maps to. */
529
529
  var FALLBACKS = {
530
530
  carlito: ["Carlito", "sans-serif"],
531
- caladea: ["Caladea", "serif"],
531
+ caladea: [
532
+ "Caladea",
533
+ "Noto Serif",
534
+ "serif"
535
+ ],
532
536
  arimo: ["sans-serif"],
533
537
  tinos: ["serif"],
534
- cousine: ["monospace"]
538
+ cousine: ["monospace"],
539
+ notoserif: ["Noto Serif", "serif"]
535
540
  };
536
541
  function decorationStyle(u) {
537
542
  switch (u) {
@@ -19,8 +19,8 @@ export declare const BORDER_DASHES: ReadonlyMap<BorderStyle, ReadonlyArray<numbe
19
19
  export declare function lineVisualOrder(line: Line): Array<number>;
20
20
  /**
21
21
  * The extra width each space of a justified line takes — 0 for a line that is
22
- * not justified, or the last line of a paragraph (which stays flush by
23
- * convention).
22
+ * not justified. The last line of a paragraph stays flush by convention: it is
23
+ * never stretched, only pulled in where the breaker packed it past the measure.
24
24
  *
25
25
  * @param line The laid-out line.
26
26
  * @returns Points added to (or, squeezed, taken from) each space token.
@@ -41,8 +41,8 @@ function lineVisualOrder(line) {
41
41
  }
42
42
  /**
43
43
  * The extra width each space of a justified line takes — 0 for a line that is
44
- * not justified, or the last line of a paragraph (which stays flush by
45
- * convention).
44
+ * not justified. The last line of a paragraph stays flush by convention: it is
45
+ * never stretched, only pulled in where the breaker packed it past the measure.
46
46
  *
47
47
  * @param line The laid-out line.
48
48
  * @returns Points added to (or, squeezed, taken from) each space token.
@@ -50,8 +50,7 @@ function lineVisualOrder(line) {
50
50
  function computeJustifyExtra(line) {
51
51
  const alignment = line.resolved.alignment;
52
52
  if (alignment !== "both" && alignment !== "distribute") return 0;
53
- if (line.noJustify === true) return 0;
54
- if (line.isLastInParagraph && alignment !== "distribute") return 0;
53
+ const flush = line.noJustify === true || line.isLastInParagraph && alignment !== "distribute";
55
54
  let last = line.tokens.length - 1;
56
55
  while (last >= 0 && line.tokens[last].kind === "text" && line.tokens[last].isSpace) last--;
57
56
  let spaces = 0;
@@ -66,7 +65,8 @@ function computeJustifyExtra(line) {
66
65
  }
67
66
  if (spaces === 0) return 0;
68
67
  const extra = (line.availableWidthPt - contentWidthPt) / spaces;
69
- return extra < 0 ? Math.max(extra, -narrowest * GLUE_SHRINK_RATIO) : extra;
68
+ if (extra < 0) return Math.max(extra, -narrowest * GLUE_SHRINK_RATIO);
69
+ return flush ? 0 : extra;
70
70
  }
71
71
  /**
72
72
  * Whether a line is ONE text matrix and a run of glyphs: nothing in it placed
@@ -21,6 +21,12 @@ export interface FontResource {
21
21
  * a reph, a vowel sign's contextual form — for the PDF's /ToUnicode.
22
22
  */
23
23
  readonly glyphText?: ReadonlyMap<number, string>;
24
+ /**
25
+ * For each collected glyph several characters share in the face's `cmap`,
26
+ * the one the document drew it for most often — what the PDF's /ToUnicode
27
+ * names it (see `sharedGlyphCodes`).
28
+ */
29
+ readonly glyphChars?: ReadonlyMap<number, number>;
24
30
  }
25
31
  /**
26
32
  * The face a run asked for and the registry could not give — what the page has