reamkit 1.22.0 → 1.23.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/dist/esm/core/converter/ream.d.ts +16 -0
- package/dist/esm/core/converter/ream.js +59 -9
- package/dist/esm/core/document-model/types.d.ts +18 -0
- package/dist/esm/core/drawingml/chart-geometry.js +11 -5
- package/dist/esm/core/drawingml/colors.d.ts +4 -2
- package/dist/esm/core/drawingml/colors.js +15 -0
- package/dist/esm/core/drawingml/theme-parser.d.ts +38 -0
- package/dist/esm/core/drawingml/theme-parser.js +64 -1
- package/dist/esm/core/fonts/families.d.ts +10 -0
- package/dist/esm/core/fonts/families.js +44 -0
- package/dist/esm/core/fonts/index.d.ts +3 -2
- package/dist/esm/core/fonts/remote-fonts.d.ts +50 -0
- package/dist/esm/core/fonts/remote-fonts.js +114 -8
- package/dist/esm/core/fonts/scripts.d.ts +26 -0
- package/dist/esm/core/fonts/scripts.js +177 -0
- package/dist/esm/core/metafile/blit.d.ts +33 -0
- package/dist/esm/core/metafile/blit.js +67 -0
- package/dist/esm/core/metafile/dib.d.ts +61 -0
- package/dist/esm/core/metafile/dib.js +205 -0
- package/dist/esm/core/metafile/emf.js +137 -23
- package/dist/esm/core/metafile/picture.d.ts +12 -1
- package/dist/esm/core/metafile/wmf.js +149 -9
- package/dist/esm/core/ole/escher-blip.d.ts +12 -0
- package/dist/esm/core/ole/escher-blip.js +88 -0
- package/dist/esm/{pdf-reader → core}/png-encode.js +1 -1
- package/dist/esm/excel/print-model.js +3 -1
- package/dist/esm/excel/styles-parser.js +1 -1
- package/dist/esm/excel/xls/biff-reader.js +53 -18
- package/dist/esm/excel/xls/biff-styles.d.ts +5 -2
- package/dist/esm/excel/xls/biff-styles.js +9 -7
- package/dist/esm/excel/xls/escher.js +90 -24
- package/dist/esm/layout/page-doc.d.ts +39 -0
- package/dist/esm/layout/styled-layout.d.ts +2 -2
- package/dist/esm/layout/styled-layout.js +301 -35
- package/dist/esm/pdf/styled-page-emitter.js +35 -2
- package/dist/esm/pdf-reader/image-decode.js +1 -1
- package/dist/esm/pptx/embedded-fonts.d.ts +31 -0
- package/dist/esm/pptx/embedded-fonts.js +104 -0
- package/dist/esm/pptx/placeholder-cascade.d.ts +73 -6
- package/dist/esm/pptx/placeholder-cascade.js +114 -18
- package/dist/esm/pptx/ppt/ppt-reader.js +120 -10
- package/dist/esm/pptx/ppt/ppt-text.d.ts +66 -1
- package/dist/esm/pptx/ppt/ppt-text.js +576 -137
- package/dist/esm/pptx/pptx-reader.d.ts +8 -0
- package/dist/esm/pptx/pptx-reader.js +27 -5
- package/dist/esm/pptx/preset-table-styles.d.ts +9 -0
- package/dist/esm/pptx/preset-table-styles.js +27 -0
- package/dist/esm/pptx/slide-parser.d.ts +7 -1
- package/dist/esm/pptx/slide-parser.js +106 -33
- package/dist/esm/pptx/sp-helpers.d.ts +2 -1
- package/dist/esm/pptx/sp-helpers.js +3 -2
- package/dist/esm/pptx/table-style.d.ts +9 -1
- package/dist/esm/pptx/table-style.js +49 -7
- package/dist/esm/word/document-parser.d.ts +7 -1
- package/dist/esm/word/document-parser.js +6 -6
- package/dist/esm/word/docx-reader.js +6 -3
- package/dist/esm/word/docx-to-pdf.d.ts +8 -7
- package/dist/esm/word/docx-to-pdf.js +25 -7
- package/dist/esm/word/drawing-parser.js +31 -19
- package/dist/esm/word/numbering-parser.d.ts +2 -1
- package/dist/esm/word/numbering-parser.js +6 -6
- package/dist/esm/word/paragraph-properties.d.ts +2 -1
- package/dist/esm/word/paragraph-properties.js +2 -2
- package/dist/esm/word/run-properties.d.ts +5 -2
- package/dist/esm/word/run-properties.js +13 -8
- package/dist/esm/word/styles-parser.d.ts +2 -1
- package/dist/esm/word/styles-parser.js +12 -12
- package/dist/esm/word/table-parser.js +3 -3
- package/dist/esm/word/theme-fonts.d.ts +10 -0
- package/dist/esm/word/theme-fonts.js +28 -0
- package/package.json +1 -1
- /package/dist/esm/{pdf-reader → core}/png-encode.d.ts +0 -0
|
@@ -143,6 +143,22 @@ export declare class Ream {
|
|
|
143
143
|
* @returns The font bytes and, for docx, optional per-family registries.
|
|
144
144
|
*/
|
|
145
145
|
private resolveFonts;
|
|
146
|
+
/**
|
|
147
|
+
* Fetch one face per writing system the document holds text in — the curated
|
|
148
|
+
* families are Latin, and Han, Kana, Hangul, Arabic and the geometric symbols
|
|
149
|
+
* are a notdef box in every one of them.
|
|
150
|
+
*
|
|
151
|
+
* Only the regular weight is fetched: Noto Sans SC is ten megabytes, and a
|
|
152
|
+
* bold run in it is better stroked (see `SyntheticFace`) than downloaded four
|
|
153
|
+
* times over. A face that fails to arrive is a recorded loss, not a throw —
|
|
154
|
+
* the rest of the document still renders.
|
|
155
|
+
*
|
|
156
|
+
* @param into The registry map the run resolver looks in.
|
|
157
|
+
* @param flow The parsed document.
|
|
158
|
+
* @param options The convert options (for the injectable `fetch`).
|
|
159
|
+
* @param losses Where a face that could not be fetched records itself.
|
|
160
|
+
*/
|
|
161
|
+
private addScriptFonts;
|
|
146
162
|
/**
|
|
147
163
|
* In strict mode, throw {@link ConversionLossError} for the first recorded loss.
|
|
148
164
|
*
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { fetchFontSet } from "../fonts/remote-fonts.js";
|
|
1
|
+
import { fetchFontSet, fetchScriptFont, resolveFamilyKey } from "../fonts/remote-fonts.js";
|
|
2
|
+
import { scriptsInFlow } from "../fonts/scripts.js";
|
|
2
3
|
import { FontRegistry } from "../font/font-registry.js";
|
|
3
4
|
import { createFontMeasure } from "../font/measure.js";
|
|
4
5
|
import { ConversionLossError } from "../ir/loss.js";
|
|
@@ -12,8 +13,8 @@ import { writeDocx } from "../../word/docx-writer.js";
|
|
|
12
13
|
import { writeXlsx } from "../../excel/xlsx-writer.js";
|
|
13
14
|
import { writeHtml } from "../../html/html-writer.js";
|
|
14
15
|
import { writeSvg } from "../../svg/svg-writer.js";
|
|
15
|
-
import { resolveDocxAutoFonts } from "../../word/docx-to-pdf.js";
|
|
16
16
|
import { DEFAULT_READERS, resolveFontsViaChain, toFlowDoc } from "./facade.js";
|
|
17
|
+
import { familiesInFlow } from "../fonts/families.js";
|
|
17
18
|
import { decryptPackage, isEncryptedPackage } from "../crypto/offcrypto.js";
|
|
18
19
|
//#region src/core/converter/ream.ts
|
|
19
20
|
/**
|
|
@@ -150,7 +151,7 @@ var Ream = class Ream {
|
|
|
150
151
|
losses
|
|
151
152
|
};
|
|
152
153
|
}
|
|
153
|
-
const { fonts, registriesByFamily } = await this.resolveFonts(options, losses);
|
|
154
|
+
const { fonts, registriesByFamily } = await this.resolveFonts(options, losses, flow);
|
|
154
155
|
const registry = FontRegistry.fromBytes(fonts);
|
|
155
156
|
const paginated = this.sheet ? projectSheetDoc(this.sheet, {
|
|
156
157
|
...options.now ? { now: options.now } : {},
|
|
@@ -209,7 +210,7 @@ var Ream = class Ream {
|
|
|
209
210
|
* @param losses The mutable loss list a substitution is appended to.
|
|
210
211
|
* @returns The font bytes and, for docx, optional per-family registries.
|
|
211
212
|
*/
|
|
212
|
-
async resolveFonts(options, losses) {
|
|
213
|
+
async resolveFonts(options, losses, flow) {
|
|
213
214
|
const explicit = options.fonts ?? (options.fontBytes ? { regular: options.fontBytes } : void 0);
|
|
214
215
|
if (explicit) return { fonts: explicit };
|
|
215
216
|
if (options.fontProviders && options.fontProviders.length > 0) {
|
|
@@ -217,11 +218,60 @@ var Ream = class Ream {
|
|
|
217
218
|
if (loss) losses.push(loss);
|
|
218
219
|
if (fonts) return { fonts };
|
|
219
220
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
const fetchOpt = options.fontFetch ? { fetch: options.fontFetch } : {};
|
|
222
|
+
const keys = options.fontFamily ? new Set([resolveFamilyKey(options.fontFamily)]) : familiesInFlow(flow);
|
|
223
|
+
if (keys.size <= 1 && scriptsInFlow(flow).scripts.size === 0) {
|
|
224
|
+
const family = keys.values().next().value;
|
|
225
|
+
return { fonts: await fetchFontSet({
|
|
226
|
+
...family ? { family } : {},
|
|
227
|
+
...fetchOpt
|
|
228
|
+
}) };
|
|
229
|
+
}
|
|
230
|
+
const registriesByFamily = /* @__PURE__ */ new Map();
|
|
231
|
+
let baseBytes;
|
|
232
|
+
for (const key of keys) {
|
|
233
|
+
const bytes = await fetchFontSet({
|
|
234
|
+
family: key,
|
|
235
|
+
...fetchOpt
|
|
236
|
+
});
|
|
237
|
+
registriesByFamily.set(key, FontRegistry.fromBytes(bytes));
|
|
238
|
+
if (key === "arimo" || !baseBytes) baseBytes = bytes;
|
|
239
|
+
}
|
|
240
|
+
await this.addScriptFonts(registriesByFamily, flow, options, losses);
|
|
241
|
+
return {
|
|
242
|
+
fonts: baseBytes,
|
|
243
|
+
registriesByFamily
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Fetch one face per writing system the document holds text in — the curated
|
|
248
|
+
* families are Latin, and Han, Kana, Hangul, Arabic and the geometric symbols
|
|
249
|
+
* are a notdef box in every one of them.
|
|
250
|
+
*
|
|
251
|
+
* Only the regular weight is fetched: Noto Sans SC is ten megabytes, and a
|
|
252
|
+
* bold run in it is better stroked (see `SyntheticFace`) than downloaded four
|
|
253
|
+
* times over. A face that fails to arrive is a recorded loss, not a throw —
|
|
254
|
+
* the rest of the document still renders.
|
|
255
|
+
*
|
|
256
|
+
* @param into The registry map the run resolver looks in.
|
|
257
|
+
* @param flow The parsed document.
|
|
258
|
+
* @param options The convert options (for the injectable `fetch`).
|
|
259
|
+
* @param losses Where a face that could not be fetched records itself.
|
|
260
|
+
*/
|
|
261
|
+
async addScriptFonts(into, flow, options, losses) {
|
|
262
|
+
const { scripts } = scriptsInFlow(flow);
|
|
263
|
+
for (const script of scripts) {
|
|
264
|
+
const bytes = await fetchScriptFont(script, options.fontFetch);
|
|
265
|
+
if (!bytes) {
|
|
266
|
+
losses.push({
|
|
267
|
+
feature: "font",
|
|
268
|
+
detail: `no face could be fetched for the ${script} script; its text draws as boxes`,
|
|
269
|
+
severity: "substituted"
|
|
270
|
+
});
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
into.set(script, FontRegistry.fromBytes(bytes));
|
|
274
|
+
}
|
|
225
275
|
}
|
|
226
276
|
/**
|
|
227
277
|
* In strict mode, throw {@link ConversionLossError} for the first recorded loss.
|
|
@@ -1025,6 +1025,24 @@ export interface ShapeFill {
|
|
|
1025
1025
|
* text box: stretched, such a fill is a blur where it should be a pattern.
|
|
1026
1026
|
*/
|
|
1027
1027
|
readonly tiled?: boolean;
|
|
1028
|
+
/**
|
|
1029
|
+
* The size ONE copy of a tiled fill occupies, when the shape states it —
|
|
1030
|
+
* MS-ODRAW §2.3.7.11/.12 `fillWidth` / `fillHeight`. Absent, a tile is the
|
|
1031
|
+
* picture at its own resolution, which is the default both formats give.
|
|
1032
|
+
*/
|
|
1033
|
+
readonly tileSizePt?: {
|
|
1034
|
+
readonly widthPt: number;
|
|
1035
|
+
readonly heightPt: number;
|
|
1036
|
+
};
|
|
1037
|
+
/**
|
|
1038
|
+
* §20.1.8.58 `a:tile @sx @sy` — how far the picture is scaled BEFORE it is
|
|
1039
|
+
* repeated, as fractions of its own size (1 = 100 %). A texture halved tiles
|
|
1040
|
+
* four times as often, and read as a plain repeat it tiles once.
|
|
1041
|
+
*/
|
|
1042
|
+
readonly tileScale?: {
|
|
1043
|
+
readonly sx: number;
|
|
1044
|
+
readonly sy: number;
|
|
1045
|
+
};
|
|
1028
1046
|
/**
|
|
1029
1047
|
* §20.1.8.14 `a:blipFill` — the picture painted across the shape's box. A
|
|
1030
1048
|
* DrawingML picture IS a shape with one of these, which is how a `pic:pic`
|
|
@@ -46,7 +46,7 @@ function niceScale(dataMin, dataMax, maxTicks = 6) {
|
|
|
46
46
|
const lo = Math.min(dataMin, dataMax);
|
|
47
47
|
let hi = Math.max(dataMin, dataMax);
|
|
48
48
|
if (lo === hi) hi = lo + 1;
|
|
49
|
-
const step = niceNum(
|
|
49
|
+
const step = niceNum((hi - lo) / Math.max(1, maxTicks - 1), true);
|
|
50
50
|
const headroom = hi > 0 ? hi * 1.05 : hi;
|
|
51
51
|
return {
|
|
52
52
|
min: Math.floor(lo / step) * step,
|
|
@@ -712,11 +712,17 @@ function autoRange(vals) {
|
|
|
712
712
|
return [lo > 0 && lo < 5 / 6 * hi ? 0 : lo, hi];
|
|
713
713
|
}
|
|
714
714
|
/**
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
715
|
+
* How many ticks an axis `extentPt` long carries (mirrors {@link axisScale}).
|
|
716
|
+
*
|
|
717
|
+
* A tall axis takes more of them — a fixed six drew 0/100/200/300 down a plot
|
|
718
|
+
* where both references fit 0/50/…/300 — but the appetite is milder than it
|
|
719
|
+
* looks: asked for one every 32pt, a 427pt chart wanted ten, and Excel labels
|
|
720
|
+
* that chart's 0…5 data 0/1/…/6 where we drew half-steps (chart-texture-bg
|
|
721
|
+
* .pptx). One per ~48pt and never more than eight satisfies both, now that
|
|
722
|
+
* {@link niceScale} measures its step against the range the data spans rather
|
|
723
|
+
* than against Heckbert's rounded-up one.
|
|
718
724
|
*/
|
|
719
|
-
var tickBudget = (extentPt) => Math.min(
|
|
725
|
+
var tickBudget = (extentPt) => Math.min(8, Math.max(4, Math.round(extentPt / 48)));
|
|
720
726
|
/** Side (points) of the square stamped for a series that names no symbol. */
|
|
721
727
|
var DEFAULT_MARKER_PT = 4;
|
|
722
728
|
/**
|
|
@@ -3,10 +3,12 @@ import { PoNode } from '../po-helpers.js';
|
|
|
3
3
|
* A colour transform child (§20.1.2.3): `lumMod`/`lumOff` modulate luminance,
|
|
4
4
|
* `shade` darkens toward black, `tint` lightens toward white. `val` is normalised
|
|
5
5
|
* to 0..1 (the XML stores thousandths of a percent). `alpha` composites over white
|
|
6
|
-
* (solid fills emit no transparency).
|
|
6
|
+
* (solid fills emit no transparency). `gamma`/`invGamma` carry no value: they
|
|
7
|
+
* shift the colour into the other light and back, which changes what the
|
|
8
|
+
* transforms between them mean.
|
|
7
9
|
*/
|
|
8
10
|
export interface ColorMod {
|
|
9
|
-
readonly kind: 'lumMod' | 'lumOff' | 'shade' | 'tint' | 'alpha' | 'satMod' | 'hueMod' | 'hueOff' | 'satOff';
|
|
11
|
+
readonly kind: 'lumMod' | 'lumOff' | 'shade' | 'tint' | 'alpha' | 'satMod' | 'hueMod' | 'hueOff' | 'satOff' | 'gamma' | 'invGamma';
|
|
10
12
|
readonly val: number;
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
@@ -99,6 +99,13 @@ function applyColorMods(hex, mods) {
|
|
|
99
99
|
r = toSrgb(toLinear(r) * m.val + (1 - m.val));
|
|
100
100
|
g = toSrgb(toLinear(g) * m.val + (1 - m.val));
|
|
101
101
|
b = toSrgb(toLinear(b) * m.val + (1 - m.val));
|
|
102
|
+
} else if (m.kind === "gamma" || m.kind === "invGamma") {
|
|
103
|
+
const shift = m.kind === "gamma" ? toSrgb : toLinear;
|
|
104
|
+
[r, g, b] = [
|
|
105
|
+
shift(r),
|
|
106
|
+
shift(g),
|
|
107
|
+
shift(b)
|
|
108
|
+
];
|
|
102
109
|
} else if (m.kind === "lumMod" || m.kind === "lumOff") {
|
|
103
110
|
const [h, s, l] = rgbToHsl(r, g, b);
|
|
104
111
|
const l2 = m.kind === "lumMod" ? l * m.val : clamp01(l + m.val);
|
|
@@ -230,6 +237,14 @@ function readColorMods(colorNode) {
|
|
|
230
237
|
val: v / 1e5
|
|
231
238
|
});
|
|
232
239
|
}
|
|
240
|
+
if (poIs(c, "a:gamma")) mods.push({
|
|
241
|
+
kind: "gamma",
|
|
242
|
+
val: 0
|
|
243
|
+
});
|
|
244
|
+
if (poIs(c, "a:invGamma")) mods.push({
|
|
245
|
+
kind: "invGamma",
|
|
246
|
+
val: 0
|
|
247
|
+
});
|
|
233
248
|
if (poIs(c, "a:hueOff")) {
|
|
234
249
|
const v = poIntAttr(c, "val");
|
|
235
250
|
if (v !== void 0) mods.push({
|
|
@@ -1,4 +1,42 @@
|
|
|
1
1
|
import { PoNode } from '../po-helpers.js';
|
|
2
|
+
/**
|
|
3
|
+
* §20.1.4.1.16 `a:fontScheme` — the two typefaces a theme names, each in three
|
|
4
|
+
* scripts. A document does not repeat them: it refers to them by TOKEN
|
|
5
|
+
* (`+mj-lt`, `+mn-ea`, …), which every reader is expected to resolve.
|
|
6
|
+
*/
|
|
7
|
+
export interface ThemeFonts {
|
|
8
|
+
/** `a:majorFont` — headings. */
|
|
9
|
+
readonly major: ThemeFontSlots;
|
|
10
|
+
/** `a:minorFont` — body text. */
|
|
11
|
+
readonly minor: ThemeFontSlots;
|
|
12
|
+
}
|
|
13
|
+
/** One font of a scheme, in the three scripts a run may pick from. */
|
|
14
|
+
export interface ThemeFontSlots {
|
|
15
|
+
readonly latin?: string;
|
|
16
|
+
readonly ea?: string;
|
|
17
|
+
readonly cs?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Parse a theme's font scheme (§20.1.4.1.16).
|
|
21
|
+
*
|
|
22
|
+
* @param themeXml The raw theme part bytes, UTF-8.
|
|
23
|
+
* @returns The two fonts; slots the theme leaves empty are absent.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseThemeFonts(themeXml: Uint8Array): ThemeFonts;
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a typeface written as a theme TOKEN to the name it stands for.
|
|
28
|
+
*
|
|
29
|
+
* A slide states its fonts as `+mn-lt` far more often than by name, and left
|
|
30
|
+
* unresolved that string travels into the model as if it WERE a typeface: no
|
|
31
|
+
* substitution table knows it, so a deck whose theme is Times came out in a
|
|
32
|
+
* grotesque (45541_Header).
|
|
33
|
+
*
|
|
34
|
+
* @param typeface The `@typeface` as the file writes it.
|
|
35
|
+
* @param fonts The theme's font scheme, when the part was read.
|
|
36
|
+
* @returns The resolved name; the input unchanged when it is not a token, and
|
|
37
|
+
* `undefined` when it is a token the theme leaves empty.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveThemeFont(typeface: string | undefined, fonts: ThemeFonts | undefined): string | undefined;
|
|
2
40
|
/**
|
|
3
41
|
* Parse a DrawingML theme part (ECMA-376 §20.1.6.2, `a:clrScheme`) into a
|
|
4
42
|
* `name → hex` colour map. Reads the twelve scheme slots — `dk1`/`lt1`/`dk2`/
|
|
@@ -26,6 +26,69 @@ var SCHEME_SLOTS = new Set([
|
|
|
26
26
|
"folHlink"
|
|
27
27
|
]);
|
|
28
28
|
/**
|
|
29
|
+
* Parse a theme's font scheme (§20.1.4.1.16).
|
|
30
|
+
*
|
|
31
|
+
* @param themeXml The raw theme part bytes, UTF-8.
|
|
32
|
+
* @returns The two fonts; slots the theme leaves empty are absent.
|
|
33
|
+
*/
|
|
34
|
+
function parseThemeFonts(themeXml) {
|
|
35
|
+
const scheme = poFindByPath(parser.parse(decoder.decode(themeXml)), [
|
|
36
|
+
"a:theme",
|
|
37
|
+
"a:themeElements",
|
|
38
|
+
"a:fontScheme"
|
|
39
|
+
]);
|
|
40
|
+
const read = (tag) => {
|
|
41
|
+
const font = scheme ? poChildren(scheme).find((c) => poIs(c, tag)) : void 0;
|
|
42
|
+
const slot = (name) => {
|
|
43
|
+
const node = font ? poChildren(font).find((c) => poIs(c, name)) : void 0;
|
|
44
|
+
const face = node ? poAttr(node, "typeface")?.trim() : void 0;
|
|
45
|
+
return face === void 0 || face === "" ? void 0 : face;
|
|
46
|
+
};
|
|
47
|
+
const [latin, ea, cs] = [
|
|
48
|
+
slot("a:latin"),
|
|
49
|
+
slot("a:ea"),
|
|
50
|
+
slot("a:cs")
|
|
51
|
+
];
|
|
52
|
+
return {
|
|
53
|
+
...latin !== void 0 ? { latin } : {},
|
|
54
|
+
...ea !== void 0 ? { ea } : {},
|
|
55
|
+
...cs !== void 0 ? { cs } : {}
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
major: read("a:majorFont"),
|
|
60
|
+
minor: read("a:minorFont")
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
var THEME_FONT_TOKENS = new Map([
|
|
64
|
+
["+mj-lt", ["major", "latin"]],
|
|
65
|
+
["+mn-lt", ["minor", "latin"]],
|
|
66
|
+
["+mj-ea", ["major", "ea"]],
|
|
67
|
+
["+mn-ea", ["minor", "ea"]],
|
|
68
|
+
["+mj-cs", ["major", "cs"]],
|
|
69
|
+
["+mn-cs", ["minor", "cs"]]
|
|
70
|
+
]);
|
|
71
|
+
/**
|
|
72
|
+
* Resolve a typeface written as a theme TOKEN to the name it stands for.
|
|
73
|
+
*
|
|
74
|
+
* A slide states its fonts as `+mn-lt` far more often than by name, and left
|
|
75
|
+
* unresolved that string travels into the model as if it WERE a typeface: no
|
|
76
|
+
* substitution table knows it, so a deck whose theme is Times came out in a
|
|
77
|
+
* grotesque (45541_Header).
|
|
78
|
+
*
|
|
79
|
+
* @param typeface The `@typeface` as the file writes it.
|
|
80
|
+
* @param fonts The theme's font scheme, when the part was read.
|
|
81
|
+
* @returns The resolved name; the input unchanged when it is not a token, and
|
|
82
|
+
* `undefined` when it is a token the theme leaves empty.
|
|
83
|
+
*/
|
|
84
|
+
function resolveThemeFont(typeface, fonts) {
|
|
85
|
+
if (typeface === void 0 || !typeface.startsWith("+")) return typeface;
|
|
86
|
+
const slot = THEME_FONT_TOKENS.get(typeface.toLowerCase());
|
|
87
|
+
if (!slot || !fonts) return void 0;
|
|
88
|
+
const [which, script] = slot;
|
|
89
|
+
return fonts[which][script];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
29
92
|
* Parse a DrawingML theme part (ECMA-376 §20.1.6.2, `a:clrScheme`) into a
|
|
30
93
|
* `name → hex` colour map. Reads the twelve scheme slots — `dk1`/`lt1`/`dk2`/
|
|
31
94
|
* `lt2`, `accent1`–`accent6`, `hlink`/`folHlink` — taking each slot's
|
|
@@ -154,4 +217,4 @@ function colorOf(slot) {
|
|
|
154
217
|
}
|
|
155
218
|
}
|
|
156
219
|
//#endregion
|
|
157
|
-
export { parseTheme, parseThemeBgFillStyles, parseThemeEffectStyles, parseThemeFillStyles, parseThemeLineWidths };
|
|
220
|
+
export { parseTheme, parseThemeBgFillStyles, parseThemeEffectStyles, parseThemeFillStyles, parseThemeFonts, parseThemeLineWidths, resolveThemeFont };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { FamilyKey } from './remote-fonts.js';
|
|
2
|
+
import { FlowDoc } from '../ir/flow.js';
|
|
3
|
+
/**
|
|
4
|
+
* The curated substitute families a document's text needs, always including the
|
|
5
|
+
* sans default as the fallback for unstyled runs, math and chart labels.
|
|
6
|
+
*
|
|
7
|
+
* @param flow The parsed document.
|
|
8
|
+
* @returns One {@link FamilyKey} per distinct family the document names.
|
|
9
|
+
*/
|
|
10
|
+
export declare function familiesInFlow(flow: FlowDoc): Set<FamilyKey>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { resolveFamilyKey } from "./remote-fonts.js";
|
|
2
|
+
//#region src/core/fonts/families.ts
|
|
3
|
+
/**
|
|
4
|
+
* The curated substitute families a document's text needs, always including the
|
|
5
|
+
* sans default as the fallback for unstyled runs, math and chart labels.
|
|
6
|
+
*
|
|
7
|
+
* @param flow The parsed document.
|
|
8
|
+
* @returns One {@link FamilyKey} per distinct family the document names.
|
|
9
|
+
*/
|
|
10
|
+
function familiesInFlow(flow) {
|
|
11
|
+
const names = /* @__PURE__ */ new Set();
|
|
12
|
+
const add = (name) => {
|
|
13
|
+
if (name !== void 0 && name.trim() !== "") names.add(name.trim().toLowerCase());
|
|
14
|
+
};
|
|
15
|
+
add(flow.styles.defaultRunProperties.fontFamily?.ascii);
|
|
16
|
+
for (const style of flow.styles.styles.values()) {
|
|
17
|
+
add(style.runProperties.fontFamily?.ascii);
|
|
18
|
+
add(style.paragraphProperties.runProperties?.fontFamily?.ascii);
|
|
19
|
+
add(style.tableLayer?.runProperties?.fontFamily?.ascii);
|
|
20
|
+
for (const c of style.tableConditions ?? []) add(c.layer.runProperties?.fontFamily?.ascii);
|
|
21
|
+
}
|
|
22
|
+
for (const abstract of flow.numbering?.abstractNums.values() ?? []) for (const level of abstract.levels.values()) add(level.runProperties.fontFamily?.ascii);
|
|
23
|
+
const shapeText = (shape) => {
|
|
24
|
+
if (shape.text) visit(shape.text.content);
|
|
25
|
+
for (const child of shape.children ?? []) shapeText(child.shape);
|
|
26
|
+
};
|
|
27
|
+
const visit = (elements) => {
|
|
28
|
+
for (const el of elements) if (el.kind === "paragraph") {
|
|
29
|
+
add(el.paragraph.properties.runProperties?.fontFamily?.ascii);
|
|
30
|
+
for (const run of el.paragraph.runs) add(run.properties.fontFamily?.ascii);
|
|
31
|
+
} else if (el.kind === "table") for (const row of el.table.rows) for (const cell of row.cells) visit(cell.content);
|
|
32
|
+
else if (el.kind === "shape") shapeText(el.shape);
|
|
33
|
+
};
|
|
34
|
+
visit(flow.body);
|
|
35
|
+
for (const band of flow.headersFooters?.values() ?? []) visit(band);
|
|
36
|
+
for (const note of flow.footnotes?.values() ?? []) visit(note);
|
|
37
|
+
for (const note of flow.endnotes?.values() ?? []) visit(note);
|
|
38
|
+
for (const comment of flow.comments?.values() ?? []) visit(comment.content);
|
|
39
|
+
const keys = new Set(["arimo"]);
|
|
40
|
+
for (const name of names) keys.add(resolveFamilyKey(name));
|
|
41
|
+
return keys;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { familiesInFlow };
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { fetchFontSet, resolveFamilyKey, clearFontCache } from './remote-fonts.js';
|
|
2
|
-
export
|
|
1
|
+
export { fetchFontSet, fetchScriptFont, isScriptKey, resolveFamilyKey, resolveFamilyStyle, clearFontCache, } from './remote-fonts.js';
|
|
2
|
+
export { scriptForCodepoint, scriptsInFlow } from './scripts.js';
|
|
3
|
+
export type { FamilyKey, FamilyStyle, FetchFontSetOptions, FetchLike, ScriptKey, SubstituteKey, } from './remote-fonts.js';
|
|
@@ -1,11 +1,61 @@
|
|
|
1
1
|
import { FontBytesByVariant } from '../font/index.js';
|
|
2
2
|
/** A curated open substitute family (the Croscore + Carlito/Caladea set, like LibreOffice). */
|
|
3
3
|
export type FamilyKey = 'arimo' | 'tinos' | 'cousine' | 'carlito' | 'caladea';
|
|
4
|
+
/**
|
|
5
|
+
* A WRITING SYSTEM the curated five cannot draw at all. They are Latin families
|
|
6
|
+
* — Greek, Cyrillic and Hebrew ride along, everything else is a notdef box —
|
|
7
|
+
* and 2145 characters of the corpus fall outside them: Han, Kana, Hangul,
|
|
8
|
+
* Arabic, the geometric symbols a form's checkbox is made of.
|
|
9
|
+
*
|
|
10
|
+
* These are fetched only when a document actually holds such a character, and
|
|
11
|
+
* only in the regular weight: Noto Sans SC is ten megabytes, and a bold run in
|
|
12
|
+
* it is better stroked (see `SyntheticFace`) than downloaded four times over.
|
|
13
|
+
*/
|
|
14
|
+
export type ScriptKey = 'jp' | 'kr' | 'sc' | 'arabic' | 'hebrew' | 'thai' | 'symbols';
|
|
15
|
+
/** Either kind of substitute — a Latin family or a per-script face. */
|
|
16
|
+
export type SubstituteKey = FamilyKey | ScriptKey;
|
|
17
|
+
/** Whether a substitute key names a writing system rather than a Latin family. */
|
|
18
|
+
export declare function isScriptKey(key: SubstituteKey): key is ScriptKey;
|
|
19
|
+
/**
|
|
20
|
+
* Fetch the ONE face a writing system is drawn with.
|
|
21
|
+
*
|
|
22
|
+
* @param script The writing system.
|
|
23
|
+
* @param fetchImpl Injectable `fetch` (defaults to the global one).
|
|
24
|
+
* @returns The regular face, or `undefined` when the download fails.
|
|
25
|
+
*/
|
|
26
|
+
export declare function fetchScriptFont(script: ScriptKey, fetchImpl?: FetchLike): Promise<FontBytesByVariant | undefined>;
|
|
27
|
+
/** A family name, read: which substitute it maps to and what it says about the face. */
|
|
28
|
+
export interface FamilyStyle {
|
|
29
|
+
readonly key: FamilyKey;
|
|
30
|
+
/** The NAME asks for a heavy face (`Arial Black`, `DIN-Bold`). */
|
|
31
|
+
readonly bold?: boolean;
|
|
32
|
+
/** …or a slanted one (`Frutiger Oblique`). */
|
|
33
|
+
readonly italic?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* …or a narrow one (`Arial Narrow`), as the fraction of the normal advance it
|
|
36
|
+
* sets at. No curated family ships a condensed cut, so the substitute is
|
|
37
|
+
* squeezed instead — Arial Narrow's own widths are 82 % of Arial's, which is
|
|
38
|
+
* also what LibreOffice's Liberation Sans Narrow reproduces.
|
|
39
|
+
*/
|
|
40
|
+
readonly widthScale?: number;
|
|
41
|
+
}
|
|
4
42
|
/**
|
|
5
43
|
* Map a document-referenced font family to a curated open substitute: an exact
|
|
6
44
|
* metric twin when one is known (e.g. Calibri → Carlito), otherwise a
|
|
7
45
|
* serif/mono/sans style fallback.
|
|
8
46
|
*
|
|
47
|
+
* The name may also carry the FACE — `Times New Roman Bold` is the Times
|
|
48
|
+
* family, and read whole it matches no twin at all and fell through to the
|
|
49
|
+
* generic sans, which is how a Times heading came out in a grotesque.
|
|
50
|
+
*
|
|
51
|
+
* @param name The referenced family name (case-insensitive); empty ⇒ Arimo.
|
|
52
|
+
* @returns The chosen family and what the name said about the face.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveFamilyStyle(name: string | undefined): FamilyStyle;
|
|
55
|
+
/**
|
|
56
|
+
* The curated substitute for a family name — {@link resolveFamilyStyle} without
|
|
57
|
+
* the face it also carries.
|
|
58
|
+
*
|
|
9
59
|
* @param name The referenced family name (case-insensitive); empty ⇒ Arimo.
|
|
10
60
|
* @returns The chosen {@link FamilyKey}.
|
|
11
61
|
*/
|
|
@@ -6,6 +6,48 @@ var VARIANT_SUFFIX = {
|
|
|
6
6
|
italic: "400Regular_Italic",
|
|
7
7
|
boldItalic: "700Bold_Italic"
|
|
8
8
|
};
|
|
9
|
+
var SCRIPTS = {
|
|
10
|
+
jp: {
|
|
11
|
+
pkg: "noto-sans-jp",
|
|
12
|
+
file: "NotoSansJP"
|
|
13
|
+
},
|
|
14
|
+
kr: {
|
|
15
|
+
pkg: "noto-sans-kr",
|
|
16
|
+
file: "NotoSansKR"
|
|
17
|
+
},
|
|
18
|
+
sc: {
|
|
19
|
+
pkg: "noto-sans-sc",
|
|
20
|
+
file: "NotoSansSC"
|
|
21
|
+
},
|
|
22
|
+
arabic: {
|
|
23
|
+
pkg: "noto-sans-arabic",
|
|
24
|
+
file: "NotoSansArabic"
|
|
25
|
+
},
|
|
26
|
+
hebrew: {
|
|
27
|
+
pkg: "noto-sans-hebrew",
|
|
28
|
+
file: "NotoSansHebrew"
|
|
29
|
+
},
|
|
30
|
+
thai: {
|
|
31
|
+
pkg: "noto-sans-thai",
|
|
32
|
+
file: "NotoSansThai"
|
|
33
|
+
},
|
|
34
|
+
symbols: {
|
|
35
|
+
pkg: "noto-sans-symbols-2",
|
|
36
|
+
file: "NotoSansSymbols2"
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Fetch the ONE face a writing system is drawn with.
|
|
41
|
+
*
|
|
42
|
+
* @param script The writing system.
|
|
43
|
+
* @param fetchImpl Injectable `fetch` (defaults to the global one).
|
|
44
|
+
* @returns The regular face, or `undefined` when the download fails.
|
|
45
|
+
*/
|
|
46
|
+
async function fetchScriptFont(script, fetchImpl = globalThis.fetch.bind(globalThis)) {
|
|
47
|
+
const family = SCRIPTS[script];
|
|
48
|
+
const bytes = await fetchTtf(`${CDN_BASE}/${family.pkg}/400Regular/${family.file}_400Regular.ttf`, fetchImpl, false);
|
|
49
|
+
return bytes ? { regular: bytes } : void 0;
|
|
50
|
+
}
|
|
9
51
|
var FAMILIES = {
|
|
10
52
|
arimo: {
|
|
11
53
|
pkg: "arimo",
|
|
@@ -31,6 +73,7 @@ var FAMILIES = {
|
|
|
31
73
|
};
|
|
32
74
|
var EXACT = {
|
|
33
75
|
calibri: "carlito",
|
|
76
|
+
"calibri light": "carlito",
|
|
34
77
|
cambria: "caladea",
|
|
35
78
|
arial: "arimo",
|
|
36
79
|
helvetica: "arimo",
|
|
@@ -59,22 +102,85 @@ var MONO = new Set([
|
|
|
59
102
|
"dejavu sans mono",
|
|
60
103
|
"monospace"
|
|
61
104
|
]);
|
|
105
|
+
var WEIGHT_WORDS = new Map([
|
|
106
|
+
["black", "bold"],
|
|
107
|
+
["heavy", "bold"],
|
|
108
|
+
["bold", "bold"],
|
|
109
|
+
["semibold", "bold"],
|
|
110
|
+
["demibold", "bold"],
|
|
111
|
+
["demi", "bold"],
|
|
112
|
+
["italic", "italic"],
|
|
113
|
+
["oblique", "italic"],
|
|
114
|
+
["light", "none"],
|
|
115
|
+
["thin", "none"],
|
|
116
|
+
["medium", "none"],
|
|
117
|
+
["regular", "none"],
|
|
118
|
+
["book", "none"],
|
|
119
|
+
["narrow", "narrow"],
|
|
120
|
+
["condensed", "narrow"],
|
|
121
|
+
["cond", "narrow"],
|
|
122
|
+
["expanded", "none"],
|
|
123
|
+
["extra", "none"],
|
|
124
|
+
["ultra", "none"],
|
|
125
|
+
["pro", "none"]
|
|
126
|
+
]);
|
|
127
|
+
/** Arial Narrow's advance widths, as a fraction of Arial's. */
|
|
128
|
+
var NARROW_SCALE = .82;
|
|
62
129
|
/**
|
|
63
130
|
* Map a document-referenced font family to a curated open substitute: an exact
|
|
64
131
|
* metric twin when one is known (e.g. Calibri → Carlito), otherwise a
|
|
65
132
|
* serif/mono/sans style fallback.
|
|
66
133
|
*
|
|
134
|
+
* The name may also carry the FACE — `Times New Roman Bold` is the Times
|
|
135
|
+
* family, and read whole it matches no twin at all and fell through to the
|
|
136
|
+
* generic sans, which is how a Times heading came out in a grotesque.
|
|
137
|
+
*
|
|
138
|
+
* @param name The referenced family name (case-insensitive); empty ⇒ Arimo.
|
|
139
|
+
* @returns The chosen family and what the name said about the face.
|
|
140
|
+
*/
|
|
141
|
+
function resolveFamilyStyle(name) {
|
|
142
|
+
if (!name) return { key: "arimo" };
|
|
143
|
+
const words = name.trim().toLowerCase().split(/[\s\-_,]+/u).filter((w) => w !== "");
|
|
144
|
+
let bold = false;
|
|
145
|
+
let italic = false;
|
|
146
|
+
let narrow = false;
|
|
147
|
+
while (words.length > 1) {
|
|
148
|
+
const word = WEIGHT_WORDS.get(words[words.length - 1]);
|
|
149
|
+
if (word === void 0) break;
|
|
150
|
+
if (word === "bold") bold = true;
|
|
151
|
+
if (word === "italic") italic = true;
|
|
152
|
+
if (word === "narrow") narrow = true;
|
|
153
|
+
words.pop();
|
|
154
|
+
}
|
|
155
|
+
const tries = [
|
|
156
|
+
words.join(" "),
|
|
157
|
+
words[words.length - 1],
|
|
158
|
+
words[0]
|
|
159
|
+
];
|
|
160
|
+
let key = "arimo";
|
|
161
|
+
for (const n of tries) {
|
|
162
|
+
const found = EXACT[n] ?? (MONO.has(n) ? "cousine" : SERIF.has(n) ? "tinos" : void 0);
|
|
163
|
+
if (found) {
|
|
164
|
+
key = found;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
key,
|
|
170
|
+
...bold ? { bold } : {},
|
|
171
|
+
...italic ? { italic } : {},
|
|
172
|
+
...narrow ? { widthScale: NARROW_SCALE } : {}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The curated substitute for a family name — {@link resolveFamilyStyle} without
|
|
177
|
+
* the face it also carries.
|
|
178
|
+
*
|
|
67
179
|
* @param name The referenced family name (case-insensitive); empty ⇒ Arimo.
|
|
68
180
|
* @returns The chosen {@link FamilyKey}.
|
|
69
181
|
*/
|
|
70
182
|
function resolveFamilyKey(name) {
|
|
71
|
-
|
|
72
|
-
const n = name.trim().toLowerCase();
|
|
73
|
-
const exact = EXACT[n];
|
|
74
|
-
if (exact) return exact;
|
|
75
|
-
if (MONO.has(n)) return "cousine";
|
|
76
|
-
if (SERIF.has(n)) return "tinos";
|
|
77
|
-
return "arimo";
|
|
183
|
+
return resolveFamilyStyle(name).key;
|
|
78
184
|
}
|
|
79
185
|
function fontUrl(family, variant) {
|
|
80
186
|
const suffix = VARIANT_SUFFIX[variant];
|
|
@@ -128,4 +234,4 @@ async function fetchFontSet(options = {}) {
|
|
|
128
234
|
};
|
|
129
235
|
}
|
|
130
236
|
//#endregion
|
|
131
|
-
export { fetchFontSet, resolveFamilyKey };
|
|
237
|
+
export { fetchFontSet, fetchScriptFont, resolveFamilyKey, resolveFamilyStyle };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { FlowDoc } from '../ir/flow.js';
|
|
2
|
+
import { ScriptKey } from './remote-fonts.js';
|
|
3
|
+
/**
|
|
4
|
+
* The writing system a character needs a face for, or `undefined` when a Latin
|
|
5
|
+
* family covers it.
|
|
6
|
+
*
|
|
7
|
+
* @param cp The code point.
|
|
8
|
+
* @param hanFace Which face draws unified Han in this document (see
|
|
9
|
+
* {@link scriptsInFlow}); defaults to Simplified.
|
|
10
|
+
*/
|
|
11
|
+
export declare function scriptForCodepoint(cp: number, hanFace?: ScriptKey): ScriptKey | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* The writing systems a document holds text in, beside the Latin one.
|
|
14
|
+
*
|
|
15
|
+
* Unified Han is written the same in Japanese, Korean and Chinese and drawn
|
|
16
|
+
* differently in each, and the character does not say which — the document
|
|
17
|
+
* does: Kana beside it means Japanese, Hangul means Korean. So the whole text
|
|
18
|
+
* is read once, and the answer applies to every Han character in it.
|
|
19
|
+
*
|
|
20
|
+
* @param flow The parsed document.
|
|
21
|
+
* @returns The scripts to fetch a face for, and the one Han is drawn with.
|
|
22
|
+
*/
|
|
23
|
+
export declare function scriptsInFlow(flow: FlowDoc): {
|
|
24
|
+
scripts: Set<ScriptKey>;
|
|
25
|
+
hanFace: ScriptKey;
|
|
26
|
+
};
|