@openpresentation/opf-render 0.11.8 → 0.12.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 +139 -9
- package/dist/annotations.js +45 -0
- package/dist/chart-options.js +177 -0
- package/dist/charts.js +194 -36
- package/dist/color-fonts.js +32 -0
- package/dist/deck-runtime.js +343 -0
- package/dist/element-define.d.ts +1 -0
- package/dist/element-define.js +5 -0
- package/dist/element.d.ts +86 -0
- package/dist/element.js +521 -0
- package/dist/export-browser.d.ts +26 -0
- package/dist/export-browser.js +228 -0
- package/dist/font-manifest.js +158 -0
- package/dist/font-policy.js +232 -16
- package/dist/fonts.d.ts +30 -0
- package/dist/fonts.js +48 -22
- package/dist/index.d.ts +105 -2
- package/dist/pdf-fonts.js +333 -0
- package/dist/pdf-style.js +429 -0
- package/dist/pdf-subset.js +176 -0
- package/dist/pdf-text.js +338 -0
- package/dist/pdf-vector.js +1249 -0
- package/dist/pdf-writer.js +154 -0
- package/dist/pdf-xml.js +78 -0
- package/dist/player.d.ts +79 -0
- package/dist/player.js +706 -0
- package/dist/preview-fonts-cli.js +15 -0
- package/dist/preview-fonts-node.d.ts +14 -0
- package/dist/preview-fonts-node.js +86 -0
- package/dist/preview-fonts.d.ts +23 -0
- package/dist/preview-fonts.js +78 -0
- package/dist/raster-images.js +19 -6
- package/dist/raster-text.js +468 -0
- package/dist/raster.js +81 -12
- package/dist/script-font-pack.js +19 -1
- package/dist/script-fonts.js +214 -23
- package/dist/sha256.js +47 -0
- package/dist/svg.d.ts +14 -1
- package/dist/svg.js +457 -73
- package/dist/symbol-encodings.js +7 -0
- package/dist/symbol-fonts.js +96 -0
- package/package.json +84 -7
package/dist/script-fonts.js
CHANGED
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
// is reported as a note. `glyphFallback: "none"` (strict mode) keeps the exact
|
|
17
17
|
// faces and lets a missing glyph raise `missing-glyph`.
|
|
18
18
|
|
|
19
|
+
import { fontPolicyFor } from "./font-policy.js";
|
|
20
|
+
import { SYMBOL_PLACEHOLDER, SYMBOL_SCRIPT, mapSymbolText, symbolEncodingFor, symbolPreviewFaces } from "./symbol-fonts.js";
|
|
21
|
+
|
|
19
22
|
const freeze = value => { if (value && typeof value === "object") { for (const child of Object.values(value)) freeze(child); Object.freeze(value); } return value; };
|
|
20
23
|
|
|
21
24
|
/** Designated open replacement families per ISO 15924 script, by style. */
|
|
@@ -37,8 +40,33 @@ export const SCRIPT_FONT_FAMILIES = freeze({
|
|
|
37
40
|
Ethi: {sans: "Noto Sans Ethiopic"}, Armn: {sans: "Noto Sans Armenian"}, Geor: {sans: "Noto Sans Georgian"},
|
|
38
41
|
Mong: {sans: "Noto Sans Mongolian"}, Thaa: {sans: "Noto Sans Thaana"}, Syrc: {sans: "Noto Sans Syriac"},
|
|
39
42
|
Tibt: {serif: "Noto Serif Tibetan"},
|
|
43
|
+
// FF-45 special families. Zsye (emoji symbols) and Zmth (mathematical notation) are ISO 15924 keys for
|
|
44
|
+
// text, not scripts a character carries: emoji and math characters are Common, so they stay in their
|
|
45
|
+
// neighbours' run and reach these faces through the glyph fallback chain (emoji-presentation clusters
|
|
46
|
+
// take the emoji face first). The keys are last so the chain tries every real script face before them.
|
|
47
|
+
Zsye: {sans: "Noto Color Emoji"},
|
|
48
|
+
Zmth: {serif: "STIX Two Math", sans: "Noto Sans Math"},
|
|
40
49
|
});
|
|
41
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Emoji presentation (FF-45): a grapheme cluster that a browser or PowerPoint draws with the emoji font. Characters
|
|
53
|
+
* whose default presentation is emoji (Emoji_Presentation), any cluster with VS16 (U+FE0F), a skin tone modifier
|
|
54
|
+
* (U+1F3FB-1F3FF), a regional indicator (flags), a keycap (U+20E3) or an emoji tag (U+E0020-E007F). Digits, #, *
|
|
55
|
+
* and text-default pictographs (U+2764 heart, U+263A) count only with VS16; VS15 (U+FE0E) asks for text.
|
|
56
|
+
*/
|
|
57
|
+
const emojiPresentation = /\p{Emoji_Presentation}|\p{Emoji_Modifier}|\p{Regional_Indicator}|️|⃣|[\u{E0020}-\u{E007F}]/u;
|
|
58
|
+
/** True when the text holds an emoji-presentation cluster (planned in the emoji face, in measurement and drawing). */
|
|
59
|
+
export function hasEmojiPresentation(text) {
|
|
60
|
+
return typeof text === "string" && emojiPresentation.test(text);
|
|
61
|
+
}
|
|
62
|
+
const textPresentation = /︎/u;
|
|
63
|
+
/** Characters that need a math face rather than a text face: math alphanumerics, letterlike math sets and the rarer operator blocks. */
|
|
64
|
+
const mathNotation = /[\u{1D400}-\u{1D7FF}⟀-⟯⦀-⧿⨀-⫿ℂℇℊ-ℓℕℙ-ℝℤℨℬℭℯ-ℱℳ-ℸ]/u;
|
|
65
|
+
/** True when the text holds mathematical notation that ordinary text faces lack (loads the math pack under `scripts: 'auto'`). */
|
|
66
|
+
export function hasMathNotation(text) {
|
|
67
|
+
return typeof text === "string" && mathNotation.test(text);
|
|
68
|
+
}
|
|
69
|
+
|
|
42
70
|
const replacement = (script, substitutes, requestedFamilies) => requestedFamilies.map(requestedFamily => ({
|
|
43
71
|
requestedFamily, script, substitutes, compatibility: "visual",
|
|
44
72
|
note: "Designated open replacement for preview only; the PPTX keeps the requested family. Appearance and advances differ.",
|
|
@@ -85,8 +113,64 @@ export const SCRIPT_FONT_REPLACEMENTS = freeze([
|
|
|
85
113
|
...replacement("Thaa", sansThenSerif("Thaa"), ["MV Boli"]),
|
|
86
114
|
...replacement("Syrc", sansThenSerif("Syrc"), ["Estrangelo Edessa"]),
|
|
87
115
|
...replacement("Tibt", serifThenSans("Tibt"), ["Microsoft Himalaya"]),
|
|
116
|
+
// FF-45: Segoe UI Emoji previews with Noto Color Emoji (COLRv1 and SVG colour glyphs; Noto Emoji is the monochrome
|
|
117
|
+
// face the raster path draws), Cambria Math with STIX Two Math (serif math face with a MATH table; Noto Sans Math
|
|
118
|
+
// is the sans alternative). Both are visual: advances differ from the Microsoft fonts (see the policy rows).
|
|
119
|
+
...replacement("Zsye", ["Noto Color Emoji", "Noto Emoji"], ["Segoe UI Emoji"]),
|
|
120
|
+
...replacement("Zmth", ["STIX Two Math", "Noto Sans Math"], ["Cambria Math"]),
|
|
88
121
|
]);
|
|
89
122
|
|
|
123
|
+
/**
|
|
124
|
+
* RR-38: the preview size multiplier of a policy row (`replacement.sizeAdjust`) when `drawnFamily` is its replacement, the face the
|
|
125
|
+
* multiplier was measured on; otherwise undefined. `row` is a policy row, or the requested family name. A renderer scales the
|
|
126
|
+
* replacement's font size by it in measurement and drawing alike, so a replacement much wider than the real font (Noto Naskh Arabic
|
|
127
|
+
* for Arabic Typesetting: 1/0.64) draws lines of the length PowerPoint draws. The exported size and core's geometry never change.
|
|
128
|
+
*/
|
|
129
|
+
export function sizeAdjustFor(row, drawnFamily) {
|
|
130
|
+
const policy = typeof row === "string" ? fontPolicyFor(row) : row;
|
|
131
|
+
const replacement = policy?.replacement, adjust = replacement?.sizeAdjust;
|
|
132
|
+
return typeof adjust === "number" && adjust !== 1 && typeof replacement.family === "string" && replacement.family.toLowerCase() === String(drawnFamily).toLowerCase() ? adjust : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* RR-38: where PowerPoint puts the baseline of a line below its top, in em, for the policy row's replacement drawn as `drawnFamily`
|
|
137
|
+
* (`{lineAscent, lineAscentMixed}`: a line in the real font alone, and a line that also holds other fonts), or undefined. Measured in
|
|
138
|
+
* a native PowerPoint probe (Arabic Typesetting 0.70 and 0.78); core places baselines one em below the line top.
|
|
139
|
+
*/
|
|
140
|
+
export function lineAscentFor(row, drawnFamily) {
|
|
141
|
+
const policy = typeof row === "string" ? fontPolicyFor(row) : row;
|
|
142
|
+
const replacement = policy?.replacement;
|
|
143
|
+
if (!sizeAdjustFor(policy, drawnFamily) || typeof replacement.lineAscent !== "number") return undefined;
|
|
144
|
+
return {lineAscent: replacement.lineAscent, lineAscentMixed: typeof replacement.lineAscentMixed === "number" ? replacement.lineAscentMixed : replacement.lineAscent};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* RR-38: how far (em of the composed font size) the baseline of a line moves up so runs drawn in a policy replacement sit where PowerPoint
|
|
149
|
+
* puts the real font's baseline. `lines` is the planned runs of one line (one array per fragment). A line whose runs all carry a
|
|
150
|
+
* `lineAscent` takes it; a line that also holds other runs takes `lineAscentMixed`; a line with none is untouched (0).
|
|
151
|
+
*/
|
|
152
|
+
export function baselineShift(...lines) {
|
|
153
|
+
const runs = lines.flat().filter(run => run.text !== "");
|
|
154
|
+
const adjusted = runs.filter(run => typeof run.lineAscent === "number");
|
|
155
|
+
if (!adjusted.length) return 0;
|
|
156
|
+
const ascent = adjusted.length === runs.length ? Math.min(...adjusted.map(run => run.lineAscent)) : Math.min(...adjusted.map(run => run.lineAscentMixed ?? run.lineAscent));
|
|
157
|
+
return Math.max(0, 1 - ascent);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* RR-38: the font size a run with a policy size multiplier is measured and drawn at: `size x adjust` on a quarter-pixel grid. Chromium on
|
|
162
|
+
* Linux quantizes a fractional font size (a scan of Noto Naskh Arabic against fontkit: every multiple of 0.25 px within 0.09 px over
|
|
163
|
+
* 340 px lines, other fractions up to 0.27 px), so a size off that grid would break the 0.1 px browser-versus-measured advance gate.
|
|
164
|
+
* Measurement and drawing both go through this function, so they agree on the size whatever the grid.
|
|
165
|
+
*/
|
|
166
|
+
export function adjustedFontSize(size, adjust) {
|
|
167
|
+
return typeof adjust === "number" && adjust > 0 ? Math.max(0.25, Math.round(size * adjust * 4) / 4) : size;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The emoji faces (FF-45): they draw emoji-presentation clusters only, never a run's Latin text or digits. */
|
|
171
|
+
export const EMOJI_FONT_FAMILIES = SCRIPT_FONT_REPLACEMENTS.find(rule => rule.script === "Zsye").substitutes;
|
|
172
|
+
const emojiFaces = new Set(EMOJI_FONT_FAMILIES);
|
|
173
|
+
|
|
90
174
|
const eastAsianScripts = new Set(["Hani", "Hans", "Hant", "Hanb", "Jpan", "Kore", "Hang", "Jamo", "Hira", "Kana", "Hrkt", "Bopo", "Yiii"]);
|
|
91
175
|
const complexScripts = new Set([
|
|
92
176
|
"Arab", "Hebr", "Syrc", "Thaa", "Nkoo", "Adlm", "Rohg", "Mand", "Samr",
|
|
@@ -214,8 +298,11 @@ export function itemizeScripts(text, profile) {
|
|
|
214
298
|
*/
|
|
215
299
|
export function scriptsOfText(text, profile) {
|
|
216
300
|
const source = String(text ?? "");
|
|
217
|
-
|
|
218
|
-
|
|
301
|
+
// FF-45: emoji-presentation clusters and mathematical notation are Common characters inside a Latin (or other) run; their
|
|
302
|
+
// faces are needed all the same, so they are reported as the pseudo-scripts Zsye and Zmth, which the emoji and math packs serve.
|
|
303
|
+
const special = [...(hasEmojiPresentation(source) ? ["Zsye"] : []), ...(hasMathNotation(source) ? ["Zmth"] : [])];
|
|
304
|
+
if (latinOnly.test(source) && !(eastAsianLanguage(profile) && eastAsianAmbiguous.test(source))) return special;
|
|
305
|
+
return [...itemizeScripts(source, profile).filter(run => run.script !== "Latn").map(run => run.script), ...special];
|
|
219
306
|
}
|
|
220
307
|
|
|
221
308
|
/**
|
|
@@ -268,8 +355,11 @@ export function glyphFallbackFamilies(character, profile = {}, serif = profile?.
|
|
|
268
355
|
// otherwise every CJK face is tried in turn (Japanese, Simplified, Traditional, Korean).
|
|
269
356
|
const own = script === "Hani" ? (hanKeys.includes(language) ? language : "Zzzz") : fontKey(script, character, profile);
|
|
270
357
|
const keys = [own, language, "Latn", ...hanKeys, ...Object.keys(SCRIPT_FONT_FAMILIES)];
|
|
271
|
-
|
|
358
|
+
// 6. the open symbol faces (FF-45), when a host loaded them: mathematical operators, arrows, dingbats and pictographs.
|
|
359
|
+
return [...new Set([...new Set(keys)].flatMap(key => designatedFamilies(key, serif)).concat(SYMBOL_FALLBACK_FACES))];
|
|
272
360
|
}
|
|
361
|
+
/** The symbol faces at the end of the glyph fallback chain, in order: pictographs, then the mathematical operators. */
|
|
362
|
+
const SYMBOL_FALLBACK_FACES = ["Noto Sans Symbols 2", "Noto Sans Symbols", "Noto Sans Math"];
|
|
273
363
|
|
|
274
364
|
/**
|
|
275
365
|
* Aliases from proprietary script fonts to the first loaded designated
|
|
@@ -433,22 +523,94 @@ export function createScriptFonts(profile = {}, measurement, options = {}) {
|
|
|
433
523
|
const candidatesFor = (run, slots) => {
|
|
434
524
|
const list = [run.role === "eastAsian" ? slots.eastAsian : run.role === "complexScript" ? slots.complexScript : slots.latin];
|
|
435
525
|
if (run.script !== "Latn") {
|
|
526
|
+
// RR-38: a slot family whose policy replacement carries a size multiplier (Arabic Typesetting -> Noto Naskh Arabic, 0.64) names that
|
|
527
|
+
// replacement next: the multiplier was measured on it, and it is the face a registry resolves the family to, so a preview without a
|
|
528
|
+
// measurement provider draws the same face and size as a measured one (it drew the sans face, Noto Sans Arabic, at full size). Other
|
|
529
|
+
// proprietary script families keep the designated order below until they carry a measured multiplier of their own.
|
|
530
|
+
const scriptRule = list[0] ? SCRIPT_FONT_REPLACEMENTS.find(rule => rule.script === run.script && rule.requestedFamily.toLowerCase() === String(list[0]).toLowerCase()) : undefined;
|
|
531
|
+
if (scriptRule && sizeAdjustFor(list[0], scriptRule.substitutes[0])) list.push(scriptRule.substitutes[0]);
|
|
436
532
|
if (slots.supplement && slots.supplement.script === run.script) list.push(slots.supplement.family);
|
|
437
533
|
list.push(...designatedFamilies(run.script, serif));
|
|
438
534
|
}
|
|
439
535
|
return [...new Set(list.filter(Boolean))];
|
|
440
536
|
};
|
|
441
537
|
|
|
538
|
+
/**
|
|
539
|
+
* FF-45: runs for text in a symbol-encoded family (Symbol, Wingdings, Wingdings 2, Wingdings 3, Webdings). Each
|
|
540
|
+
* character's code (U+F0xx private use or its Windows-1252 character) maps to its Unicode equivalent, drawn with the
|
|
541
|
+
* first loaded face of the family's preview chain that has it (`symbolPreviewFaces`), one run per glyph so the
|
|
542
|
+
* preview can place each at the verified font's advance (`run.symbol.advance`, em). A code with no equivalent, or
|
|
543
|
+
* whose equivalent no loaded face has, draws the placeholder U+25A1 and says so (`run.symbol.placeholder`, the
|
|
544
|
+
* fallback note carries `codes`). Characters that are not codes (a CJK letter, an emoji) are planned as ordinary text.
|
|
545
|
+
* Without a measurement provider every symbol run names the whole chain as its stack.
|
|
546
|
+
*/
|
|
547
|
+
const planSymbols = (text, style, encoding) => {
|
|
548
|
+
const cacheKey = `${SYMBOL_SCRIPT}\u0000${encoding}\u0000${style.fontFamily}\u0000${style.fontWeight ?? 400}\u0000${!!style.italic}\u0000${textRole(style) ?? ""}\u0000${style.path ?? ""}\u0000${text}`;
|
|
549
|
+
const cached = plans.get(cacheKey);
|
|
550
|
+
if (cached) return cached;
|
|
551
|
+
const chain = symbolPreviewFaces(encoding);
|
|
552
|
+
const resolvedChain = measured ? chain.map(name => resolveName(name, style)).filter((name, index, all) => name && all.indexOf(name) === index) : chain;
|
|
553
|
+
const out = [], notes = new Map();
|
|
554
|
+
let foreign = "";
|
|
555
|
+
const flushForeign = () => {
|
|
556
|
+
if (!foreign) return;
|
|
557
|
+
for (const run of plan(foreign, {...style, symbolEncoding: null})) out.push(run);
|
|
558
|
+
foreign = "";
|
|
559
|
+
};
|
|
560
|
+
const note = (family, item, drawn, placeholder) => {
|
|
561
|
+
const key = `${family}\u0000${placeholder ? "placeholder" : "glyph"}`;
|
|
562
|
+
let entry = notes.get(key);
|
|
563
|
+
if (!entry) notes.set(key, entry = {fontFamily: encoding, fallbackFamily: family, scripts: [SYMBOL_SCRIPT], characters: [], codes: [], ...(placeholder ? {placeholder: drawn || "none"} : {}), ...(style.path ? {path: style.path} : {})});
|
|
564
|
+
const hex = item.code.toString(16).toUpperCase().padStart(2, "0");
|
|
565
|
+
if (!entry.codes.includes(hex) && entry.codes.length < 32) { entry.codes.push(hex); if (entry.characters.length < 16) entry.characters.push(item.source); }
|
|
566
|
+
};
|
|
567
|
+
for (const item of mapSymbolText(encoding, text)) {
|
|
568
|
+
if (item.code === null) { foreign += item.source; continue; }
|
|
569
|
+
flushForeign();
|
|
570
|
+
let drawn = item.unicode, family = null, placeholder = false;
|
|
571
|
+
if (!measured) { out.push({text: drawn ?? SYMBOL_PLACEHOLDER, family: chain[0], own: false, stack: chain, symbol: {family: encoding, code: item.code, source: item.source, advance: item.advance, ...(drawn === null ? {placeholder: true, reason: item.reason} : {})}}); continue; }
|
|
572
|
+
if (drawn !== null) family = resolvedChain.find(name => covers(name, drawn, style)) ?? null;
|
|
573
|
+
if (family === null) {
|
|
574
|
+
placeholder = true;
|
|
575
|
+
const reason = drawn === null ? item.reason : `no loaded face has U+${drawn.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}; load the symbol faces (scripts: ['${SYMBOL_SCRIPT}'])`;
|
|
576
|
+
// The placeholder is U+25A1 WHITE SQUARE from a loaded symbol face; without one, U+FFFD from the style's face (Noto Sans,
|
|
577
|
+
// Roboto and the office faces have it); a registry with neither draws nothing at the code's advance.
|
|
578
|
+
const own = resolveName(style.fontFamily, style) ?? style.fontFamily;
|
|
579
|
+
const choice = [[SYMBOL_PLACEHOLDER, resolvedChain.find(name => covers(name, SYMBOL_PLACEHOLDER, style))], ["�", [own, ...resolvedChain].find(name => covers(name, "�", style))]].find(([, name]) => name);
|
|
580
|
+
drawn = choice ? choice[0] : "";
|
|
581
|
+
family = choice ? choice[1] : own;
|
|
582
|
+
out.push({text: drawn, family, own: false, symbol: {family: encoding, code: item.code, source: item.source, advance: item.advance, placeholder: true, reason}});
|
|
583
|
+
} else out.push({text: drawn, family, own: false, symbol: {family: encoding, code: item.code, source: item.source, advance: item.advance}});
|
|
584
|
+
note(family, item, drawn, placeholder);
|
|
585
|
+
}
|
|
586
|
+
flushForeign();
|
|
587
|
+
for (const entry of notes.values()) {
|
|
588
|
+
const key = `${entry.fontFamily}\u0000${entry.fallbackFamily}\u0000${entry.placeholder ?? ""}\u0000${style.path ?? ""}`;
|
|
589
|
+
if (fallbackNotes.has(key)) continue;
|
|
590
|
+
fallbackNotes.set(key, entry);
|
|
591
|
+
options.onFallback?.(entry);
|
|
592
|
+
}
|
|
593
|
+
if (plans.size >= 4096) plans.delete(plans.keys().next().value);
|
|
594
|
+
plans.set(cacheKey, out);
|
|
595
|
+
return out;
|
|
596
|
+
};
|
|
597
|
+
|
|
442
598
|
/** Font runs for text drawn in `style` (the resolved latin style). `own` runs use the style's family. */
|
|
443
599
|
const plan = (text, style) => {
|
|
444
600
|
text = String(text ?? "");
|
|
445
|
-
|
|
601
|
+
// A symbol-encoded family (resolved styles carry `symbolEncoding`; an unresolved style names the family itself) is mapped code by
|
|
602
|
+
// code; `symbolEncoding: null` is the planner's own request for ordinary runs (characters that are not codes).
|
|
603
|
+
const encoding = style.symbolEncoding === null ? undefined : style.symbolEncoding ?? symbolEncodingFor(style.fontFamily)?.family;
|
|
604
|
+
if (encoding) return text ? planSymbols(text, style, encoding) : [{text, family: style.fontFamily, own: true}];
|
|
605
|
+
// An emoji face (Noto Color Emoji for a Segoe UI Emoji run) draws emoji-presentation clusters only: its Latin text and digits
|
|
606
|
+
// are planned per cluster below, where they take a text face (FF-45).
|
|
607
|
+
const ownFace = measured && fallbackEnabled ? resolveName(style.fontFamily, style) : undefined;
|
|
608
|
+
if (latinOnly.test(text) && !(eastAsianText && eastAsianAmbiguous.test(text)) && !(ownFace && emojiFaces.has(ownFace))) {
|
|
446
609
|
// Latin-group text is one run in the latin slot's face, unless that face lacks a character
|
|
447
610
|
// (Cyrillic or Greek beyond a Latin replacement): then it is planned per character below.
|
|
448
611
|
if (!fallbackEnabled || !nonAscii.test(text)) return [{text, family: style.fontFamily, own: true}];
|
|
449
612
|
if (!measured) return cyrillicOrGreek.test(text) ? [{text, family: style.fontFamily, own: false, stack: [style.fontFamily, ...designatedFamilies("Latn", serif)]}] : [{text, family: style.fontFamily, own: true}];
|
|
450
|
-
|
|
451
|
-
if (own === null || covers(own, text, style)) return [{text, family: style.fontFamily, own: true}];
|
|
613
|
+
if (ownFace === null || covers(ownFace, text, style)) return [{text, family: style.fontFamily, own: true}];
|
|
452
614
|
}
|
|
453
615
|
const cacheKey = `${style.fontFamily}\u0000${style.fontWeight ?? 400}\u0000${!!style.italic}\u0000${textRole(style) ?? ""}\u0000${style.path ?? ""}\u0000${text}`;
|
|
454
616
|
const cached = plans.get(cacheKey);
|
|
@@ -459,19 +621,32 @@ export function createScriptFonts(profile = {}, measurement, options = {}) {
|
|
|
459
621
|
const global = [...new Set([...runs.flatMap(run => run.candidates), ...designatedFamilies(fontKey(profile.script ?? "Zzzz", text, profile), serif), ...designatedFamilies("Latn", serif)])];
|
|
460
622
|
const unique = names => names.map(name => resolveName(name, style)).filter((name, index, all) => name && all.indexOf(name) === index);
|
|
461
623
|
const out = [];
|
|
462
|
-
const push = (text, family, stack) => {
|
|
624
|
+
const push = (text, family, stack, sizeAdjust, ascent) => {
|
|
463
625
|
const own = family === style.fontFamily && (!stack || stack.length === 1);
|
|
464
626
|
const last = out.at(-1);
|
|
465
|
-
if (last && last.family === family && JSON.stringify(last.stack) === JSON.stringify(stack)) last.text += text;
|
|
466
|
-
else out.push({text, family, own, ...(stack && !own ? {stack} : {})});
|
|
627
|
+
if (last && last.family === family && last.sizeAdjust === sizeAdjust && last.lineAscent === ascent?.lineAscent && JSON.stringify(last.stack) === JSON.stringify(stack)) last.text += text;
|
|
628
|
+
else out.push({text, family, own, ...(stack && !own ? {stack} : {}), ...(sizeAdjust ? {sizeAdjust} : {}), ...(ascent ?? {})});
|
|
467
629
|
};
|
|
468
630
|
for (const run of runs) {
|
|
469
|
-
|
|
631
|
+
// RR-38: the slot family's replacement draws at the policy's size multiplier. Unmeasured, the replacement is the first face
|
|
632
|
+
// the stack names after the requested family; the requested name leaves the stack so a host that has the real font installed
|
|
633
|
+
// (a browser on Windows) cannot draw it at the reduced size.
|
|
634
|
+
const adjustOf = family => sizeAdjustFor(run.candidates[0], family);
|
|
635
|
+
const ascentOf = family => lineAscentFor(run.candidates[0], family);
|
|
636
|
+
if (!measured) {
|
|
637
|
+
const adjust = adjustOf(run.candidates[1]);
|
|
638
|
+
push(run.text, adjust ? run.candidates[1] : run.candidates[0], adjust ? run.candidates.slice(1) : run.candidates, adjust, ascentOf(run.candidates[1]));
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
470
641
|
const chosen = resolveName(run.candidates[0], style);
|
|
471
|
-
|
|
642
|
+
// FF-45: a run with an emoji-presentation cluster, or whose face is an emoji face, is always planned per cluster: VS16 and
|
|
643
|
+
// emoji-default clusters take the emoji face even when the text face has a monochrome glyph for the base character, and the
|
|
644
|
+
// emoji face never draws the run's Latin text or digits.
|
|
645
|
+
const emojiRun = emojiFaces.has(chosen) || hasEmojiPresentation(run.text);
|
|
646
|
+
const whole = emojiRun ? undefined : run.candidates.map(family => resolveName(family, style)).find(name => name && covers(name, run.text, style));
|
|
472
647
|
if (whole) {
|
|
473
648
|
if (whole !== chosen) noteFallback(pending, chosen, whole, [...run.text].filter(character => !covers(chosen ?? whole, character, style)));
|
|
474
|
-
push(run.text, whole);
|
|
649
|
+
push(run.text, whole, undefined, adjustOf(whole), ascentOf(whole));
|
|
475
650
|
continue;
|
|
476
651
|
}
|
|
477
652
|
// No single candidate covers the run. Split it into grapheme clusters (a base character with
|
|
@@ -486,7 +661,14 @@ export function createScriptFonts(profile = {}, measurement, options = {}) {
|
|
|
486
661
|
let sticky = null;
|
|
487
662
|
// The note lists what the chosen face lacks (a mark, not its covered base letter); a cluster it only lacks as a whole lists all.
|
|
488
663
|
const missingFrom = (face, text) => { const lacking = [...text].filter(character => !covers(face, character, style)); return lacking.length ? lacking : [...text]; };
|
|
489
|
-
|
|
664
|
+
// An emoji-presentation cluster tries the emoji faces first (Noto Color Emoji, loaded with the emoji pack), then the text
|
|
665
|
+
// faces; any other cluster tries them last, so a run's digits and Latin text (which the emoji face draws emoji-sized) take a
|
|
666
|
+
// text face, while a text-presentation symbol only the emoji face has (U+2764 with VS15) is still drawn rather than missing.
|
|
667
|
+
const choose = (subject, sample, emoji = false) => {
|
|
668
|
+
const names = unique([run.candidates[0], ...(sticky ? [sticky] : []), ...run.candidates, ...global, ...(fallbackEnabled ? chainFor(sample) : [])]);
|
|
669
|
+
const emojiNames = names.filter(name => emojiFaces.has(name)), textNames = names.filter(name => !emojiFaces.has(name));
|
|
670
|
+
return (emoji ? [...emojiNames, ...textNames] : [...textNames, ...emojiNames]).find(name => covers(name, subject, style));
|
|
671
|
+
};
|
|
490
672
|
const segments = [];
|
|
491
673
|
for (const character of run.text) {
|
|
492
674
|
const script = scriptOfCharacter(character), last = segments.at(-1)?.clusters.at(-1);
|
|
@@ -504,10 +686,11 @@ export function createScriptFonts(profile = {}, measurement, options = {}) {
|
|
|
504
686
|
const strong = segment.clusters.filter(cluster => !cluster.weak);
|
|
505
687
|
const face = strong.length ? choose(strong.map(cluster => cluster.text).join(""), strong[0].base) : undefined;
|
|
506
688
|
for (const cluster of segment.clusters) {
|
|
507
|
-
const
|
|
689
|
+
const emoji = hasEmojiPresentation(cluster.text);
|
|
690
|
+
const family = !emoji && face && covers(face, cluster.text, style) ? face : choose(cluster.text, cluster.base, emoji) ?? choose(cluster.base, cluster.base, emoji) ?? primary;
|
|
508
691
|
if (family !== primary && family !== chosen) sticky = family;
|
|
509
692
|
if (chosen !== null && family !== chosen && covers(family, cluster.text, style)) noteFallback(pending, chosen, family, missingFrom(chosen, cluster.text));
|
|
510
|
-
push(cluster.text, family);
|
|
693
|
+
push(cluster.text, family, undefined, adjustOf(family), ascentOf(family));
|
|
511
694
|
}
|
|
512
695
|
}
|
|
513
696
|
}
|
|
@@ -517,32 +700,40 @@ export function createScriptFonts(profile = {}, measurement, options = {}) {
|
|
|
517
700
|
return out;
|
|
518
701
|
};
|
|
519
702
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
703
|
+
// A run drawn for another family drops the encoding: the open face's own glyphs are measured.
|
|
704
|
+
const styleFor = (run, style) => styled(run.own ? style : {...style, fontFamily: run.family, symbolEncoding: null});
|
|
705
|
+
/**
|
|
706
|
+
* The advance a planned run occupies: the open face's measured advance, except that a symbol run (FF-45) occupies the
|
|
707
|
+
* verified symbol font's advance for its code, so lines break and bullets sit where PowerPoint puts them; `natural`
|
|
708
|
+
* asks for the open face's advance of that run instead (the SVG compresses a wider glyph to fit, never stretches one).
|
|
709
|
+
*/
|
|
710
|
+
const runWidth = (run, size, style, natural = false) => run.symbol && !natural && run.symbol.advance !== null ? run.symbol.advance * size : inner.measure(run.text, adjustedFontSize(size, run.sizeAdjust), styleFor(run, style));
|
|
711
|
+
/** Advance of each planned run, in order (see `runWidth`); undefined without a measurement provider. */
|
|
712
|
+
const runWidths = (runs, size, style, {natural = false} = {}) => measured ? runs.map(run => runWidth(run, size, style, natural)) : undefined;
|
|
523
713
|
let textMeasurement;
|
|
524
714
|
if (measured) {
|
|
525
715
|
textMeasurement = {
|
|
526
716
|
...inner,
|
|
527
717
|
measure(text, size, style) {
|
|
528
718
|
const runs = plan(text, style);
|
|
529
|
-
|
|
530
|
-
|
|
719
|
+
// RR-38: a run drawn at the policy's size multiplier is not the style's own face at its own size: it takes the run path below.
|
|
720
|
+
if (runs.length === 1 && runs[0].own && !runs[0].sizeAdjust) return inner.measure(text, size, styled(style));
|
|
721
|
+
return runs.reduce((total, run) => total + runWidth(run, size, style), 0);
|
|
531
722
|
},
|
|
532
723
|
[scriptMeasurement]: {inner, profile, options},
|
|
533
724
|
};
|
|
534
725
|
if (typeof inner.outlineBounds === "function") {
|
|
535
726
|
textMeasurement.outlineBounds = (text, size, style) => {
|
|
536
727
|
const runs = plan(text, style);
|
|
537
|
-
if (runs.length === 1 && runs[0].own) return inner.outlineBounds(text, size, styled(style));
|
|
728
|
+
if (runs.length === 1 && runs[0].own && !runs[0].sizeAdjust) return inner.outlineBounds(text, size, styled(style));
|
|
538
729
|
let x = 0, box = null;
|
|
539
730
|
for (const run of runs) {
|
|
540
|
-
const runStyle = styleFor(run, style), bounds = inner.outlineBounds(run.text, size, runStyle);
|
|
731
|
+
const runStyle = styleFor(run, style), bounds = inner.outlineBounds(run.text, adjustedFontSize(size, run.sizeAdjust), runStyle);
|
|
541
732
|
if (bounds) {
|
|
542
733
|
const left = x + bounds.x, top = bounds.y, right = left + bounds.width, bottom = top + bounds.height;
|
|
543
734
|
box = box ? {left: Math.min(box.left, left), top: Math.min(box.top, top), right: Math.max(box.right, right), bottom: Math.max(box.bottom, bottom)} : {left, top, right, bottom};
|
|
544
735
|
}
|
|
545
|
-
x +=
|
|
736
|
+
x += runWidth(run, size, style);
|
|
546
737
|
}
|
|
547
738
|
return box && {x: box.left, y: box.top, width: box.right - box.left, height: box.bottom - box.top};
|
|
548
739
|
};
|
package/dist/sha256.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// SHA-256 in plain JavaScript (FIPS 180-4), so the vector PDF writer needs neither node:crypto nor Web Crypto (which is
|
|
2
|
+
// asynchronous) and runs the same in Node and in a browser. Returns the 32-byte digest.
|
|
3
|
+
|
|
4
|
+
const K = new Uint32Array([
|
|
5
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
6
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
7
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
8
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export function sha256Bytes(input) {
|
|
12
|
+
const data = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
13
|
+
const length = data.length;
|
|
14
|
+
const padded = new Uint8Array(((length + 9 + 63) >> 6) << 6);
|
|
15
|
+
padded.set(data);
|
|
16
|
+
padded[length] = 0x80;
|
|
17
|
+
const view = new DataView(padded.buffer);
|
|
18
|
+
view.setUint32(padded.length - 8, Math.floor(length / 0x20000000), false);
|
|
19
|
+
view.setUint32(padded.length - 4, (length << 3) >>> 0, false);
|
|
20
|
+
const h = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
|
|
21
|
+
const w = new Uint32Array(64);
|
|
22
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
23
|
+
for (let index = 0; index < 16; index++) w[index] = view.getUint32(offset + index * 4, false);
|
|
24
|
+
for (let index = 16; index < 64; index++) {
|
|
25
|
+
const a = w[index - 15], b = w[index - 2];
|
|
26
|
+
const s0 = ((a >>> 7) | (a << 25)) ^ ((a >>> 18) | (a << 14)) ^ (a >>> 3);
|
|
27
|
+
const s1 = ((b >>> 17) | (b << 15)) ^ ((b >>> 19) | (b << 13)) ^ (b >>> 10);
|
|
28
|
+
w[index] = (w[index - 16] + s0 + w[index - 7] + s1) | 0;
|
|
29
|
+
}
|
|
30
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
31
|
+
for (let index = 0; index < 64; index++) {
|
|
32
|
+
const s1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
|
|
33
|
+
const ch = (e & f) ^ (~e & g);
|
|
34
|
+
const t1 = (hh + s1 + ch + K[index] + w[index]) | 0;
|
|
35
|
+
const s0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
|
|
36
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
37
|
+
const t2 = (s0 + maj) | 0;
|
|
38
|
+
hh = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0;
|
|
39
|
+
}
|
|
40
|
+
h[0] = (h[0] + a) | 0; h[1] = (h[1] + b) | 0; h[2] = (h[2] + c) | 0; h[3] = (h[3] + d) | 0;
|
|
41
|
+
h[4] = (h[4] + e) | 0; h[5] = (h[5] + f) | 0; h[6] = (h[6] + g) | 0; h[7] = (h[7] + hh) | 0;
|
|
42
|
+
}
|
|
43
|
+
const out = new Uint8Array(32);
|
|
44
|
+
const outView = new DataView(out.buffer);
|
|
45
|
+
for (let index = 0; index < 8; index++) outView.setUint32(index * 4, h[index], false);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
package/dist/svg.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export type RenderDiagnostic = LayoutDiagnostic | {
|
|
|
41
41
|
scripts: string[];
|
|
42
42
|
characters: string[];
|
|
43
43
|
} | {
|
|
44
|
-
code: "unsupported-pattern" | "date-needs-value" | "language-preview-unavailable" | "language-preview-unresolved" | "paragraph-direction-unavailable";
|
|
44
|
+
code: "unsupported-pattern" | "variable-example-used" | "date-needs-value" | "language-preview-unavailable" | "language-preview-unresolved" | "paragraph-direction-unavailable";
|
|
45
45
|
path: string;
|
|
46
46
|
message: string;
|
|
47
47
|
} | {
|
|
@@ -74,6 +74,14 @@ export interface RenderSvgOptions {
|
|
|
74
74
|
onDiagnostic?: (diagnostic: RenderDiagnostic) => void;
|
|
75
75
|
/** When false, skip AJV boundary validation. Default true. */
|
|
76
76
|
validate?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Values for the deck's template variables, keyed by variable id (core `resolveVariables`). A deck that uses
|
|
79
|
+
* content variables, or is marked `template: true`, is resolved to a concrete deck first. A template previews with
|
|
80
|
+
* each unfilled variable's example (reported as `variable-example-used`); a normal deck with an unfilled required
|
|
81
|
+
* variable throws `unfilled-variables`. `false` draws the document as authored, with `{{id}}` tokens and
|
|
82
|
+
* `var:id` references visible and nothing resolved: the view an editor uses while the template itself is edited.
|
|
83
|
+
*/
|
|
84
|
+
variables?: Record<string, unknown> | false;
|
|
77
85
|
slideIndex?: number;
|
|
78
86
|
/**
|
|
79
87
|
* Today's calendar date (ISO YYYY-MM-DD) for `date: true` header/footer furniture. The renderer
|
|
@@ -82,6 +90,11 @@ export interface RenderSvgOptions {
|
|
|
82
90
|
date?: string;
|
|
83
91
|
trace?: boolean;
|
|
84
92
|
catalogs?: Record<string, { records?: unknown[] } | unknown[]>;
|
|
93
|
+
/**
|
|
94
|
+
* Records for catalog sources the host has already fetched, keyed by the source string a document's
|
|
95
|
+
* `catalogs.<kind>.source` names (a single source or an ordered array; first match wins, the bundled
|
|
96
|
+
* default catalog is appended). The renderer never fetches a source itself.
|
|
97
|
+
*/
|
|
85
98
|
catalogSources?: Record<string, { records?: unknown[] } | unknown[]>;
|
|
86
99
|
}
|
|
87
100
|
|