@openpresentation/opf-editor 0.12.1 → 0.14.2
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 +71 -60
- package/dist/annotations.d.ts +16 -16
- package/dist/annotations.js +71 -79
- package/dist/assets.d.ts +7 -7
- package/dist/assets.js +15 -15
- package/dist/background-options.d.ts +2 -2
- package/dist/background-options.js +6 -6
- package/dist/block-controls.js +7 -7
- package/dist/block-convert.d.ts +7 -7
- package/dist/block-convert.js +22 -21
- package/dist/blocks.d.ts +8 -8
- package/dist/blocks.js +41 -40
- package/dist/canvas-fields.js +35 -12
- package/dist/canvas.d.ts +22 -30
- package/dist/canvas.js +57 -64
- package/dist/chart-data.d.ts +10 -10
- package/dist/chart-data.js +21 -21
- package/dist/chart-options-panel.d.ts +2 -2
- package/dist/chart-options-panel.js +82 -8
- package/dist/chart-options.d.ts +35 -9
- package/dist/chart-options.js +133 -40
- package/dist/checks.js +21 -0
- package/dist/color-authoring.js +1 -1
- package/dist/content-actions.d.ts +15 -15
- package/dist/content-actions.js +71 -70
- package/dist/content-controls.js +12 -12
- package/dist/data-grid.js +7 -8
- package/dist/data.d.ts +4 -4
- package/dist/data.js +5 -5
- package/dist/dataset-refs.d.ts +2 -2
- package/dist/dataset-refs.js +5 -5
- package/dist/design-controls.js +61 -44
- package/dist/design-options.d.ts +16 -14
- package/dist/design-options.js +86 -74
- package/dist/edit-helpers.js +8 -7
- package/dist/exact-source.js +5 -5
- package/dist/export.d.ts +5 -7
- package/dist/export.js +40 -14
- package/dist/find-panel.js +5 -5
- package/dist/find-replace.d.ts +3 -3
- package/dist/find-replace.js +23 -20
- package/dist/font-gate.js +49 -58
- package/dist/galleries.js +36 -52
- package/dist/grid-model.d.ts +25 -27
- package/dist/grid-model.js +92 -102
- package/dist/image-crop.d.ts +4 -4
- package/dist/image-crop.js +20 -20
- package/dist/image-cropper.js +3 -3
- package/dist/index.d.ts +28 -28
- package/dist/index.js +74 -90
- package/dist/json-options.js +17 -16
- package/dist/layout-handles.js +5 -5
- package/dist/layout-placeholders.js +4 -4
- package/dist/layout.d.ts +2 -2
- package/dist/layout.js +8 -7
- package/dist/numbering-panel.js +3 -5
- package/dist/numbering.d.ts +3 -5
- package/dist/numbering.js +14 -26
- package/dist/outline-view.js +24 -24
- package/dist/outline.d.ts +12 -12
- package/dist/outline.js +52 -50
- package/dist/persistence.d.ts +1 -1
- package/dist/persistence.js +19 -18
- package/dist/presentation-ids.js +2 -2
- package/dist/react.js +3 -3
- package/dist/review-panel.d.ts +30 -13
- package/dist/review-panel.js +154 -53
- package/dist/review.d.ts +36 -43
- package/dist/review.js +92 -53
- package/dist/rich-text-input.js +4 -3
- package/dist/rich-text-toolbar.js +5 -3
- package/dist/rich-text.js +5 -2
- package/dist/schema-inspector.d.ts +2 -2
- package/dist/schema-inspector.js +16 -11
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +25 -5
- package/dist/slide-manager.d.ts +2 -2
- package/dist/slide-manager.js +18 -18
- package/dist/slides.d.ts +20 -20
- package/dist/slides.js +99 -97
- package/dist/svelte.js +1 -1
- package/dist/switches.d.ts +10 -10
- package/dist/switches.js +124 -114
- package/dist/table-options.d.ts +8 -8
- package/dist/table-options.js +28 -27
- package/dist/table-structure.d.ts +10 -10
- package/dist/table-structure.js +21 -21
- package/dist/template-panel.d.ts +3 -3
- package/dist/template-panel.js +37 -13
- package/dist/templates.d.ts +22 -11
- package/dist/templates.js +48 -55
- package/dist/transfer.d.ts +4 -4
- package/dist/transfer.js +49 -59
- package/package.json +8 -6
- package/dist/font-defaults.js +0 -25
package/dist/design-options.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// by default or on one slide with `slideIndex`, exactly like the dimension switches. Every
|
|
4
4
|
// function has a `prepare…` form that returns the patch without touching a session, and a
|
|
5
5
|
// session form that commits it with `meta.source: "design-option"`.
|
|
6
|
-
import { getValueAtPath, opfPathToJsonPointer
|
|
6
|
+
import { getValueAtPath, opfPathToJsonPointer } from "./index.js";
|
|
7
7
|
import { checkedDocument, designPatches, fail, same } from "./edit-helpers.js";
|
|
8
|
+
import { checkFormat } from "./checks.js";
|
|
8
9
|
|
|
9
10
|
/** The `design.logo` variant slots of a LogoSet, in schema order. */
|
|
10
11
|
export const LOGO_VARIANTS = Object.freeze([
|
|
@@ -23,8 +24,8 @@ export const LOGO_VARIANTS = Object.freeze([
|
|
|
23
24
|
]);
|
|
24
25
|
export const HEADER_FOOTER_ZONES = Object.freeze(["left", "center", "right"]);
|
|
25
26
|
// Flag fields of a header/footer zone: false is stored as "absent" (so is a `date` of false).
|
|
26
|
-
const ZONE_FLAGS = ["logo", "slideNumber", "organization", "socials", "section"];
|
|
27
|
-
export const ZONE_FIELDS = Object.freeze(["logo", "text", "image", "slideNumber", "slideNumberFormat", "date", "dateFormat", "organization", "socials", "section"]);
|
|
27
|
+
const ZONE_FLAGS = ["logo", "slideNumber", "organization", "speaker", "socials", "section"];
|
|
28
|
+
export const ZONE_FIELDS = Object.freeze(["logo", "text", "image", "slideNumber", "slideNumberFormat", "date", "dateFormat", "organization", "speaker", "socials", "section"]);
|
|
28
29
|
/** Date tokens a `dateFormat` understands (English names, independent of the host locale). */
|
|
29
30
|
export const DATE_FORMAT_TOKENS = Object.freeze(["yyyy", "yy", "MMMM", "MMM", "MM", "M", "dd", "d", "EEEE", "EEE"]);
|
|
30
31
|
const ISO_DATE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
|
|
@@ -32,7 +33,7 @@ const ISO_DATE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
|
|
|
32
33
|
// Friendly validation of one zone field before the schema sees it.
|
|
33
34
|
function checkZoneField(key, value) {
|
|
34
35
|
const bad = (message) => fail("invalid-design-value", message, { field: key, value });
|
|
35
|
-
if (
|
|
36
|
+
if (ZONE_FLAGS.includes(key) && typeof value !== "boolean") throw bad(`${key} is true or false.`);
|
|
36
37
|
if (key === "text" && typeof value !== "string") throw bad("Text is a string.");
|
|
37
38
|
if (key === "image" && typeof value !== "string" && !isObject(value)) throw bad("An image is a source, an asset reference or an asset object.");
|
|
38
39
|
if (key === "slideNumberFormat" && (typeof value !== "string" || !value.includes("{current}"))) throw bad("The slide number format must contain {current}, for example Page {current} of {total}.");
|
|
@@ -62,7 +63,7 @@ export const DESIGN_OPTIONS = Object.freeze([
|
|
|
62
63
|
{ id: "chartPrimary", label: "Primary chart position", type: "enum", values: ENUMS.chartPrimary, scopes: ["deck", "slide"], path: "design.chartPrimary" },
|
|
63
64
|
{ id: "listBullet", label: "List bullets", type: "enum", values: ENUMS.listBullet, scopes: ["deck", "slide"], path: "design.listBullet" },
|
|
64
65
|
{ id: "contentBox", label: "Content box", type: "boolean", scopes: ["deck", "slide"], path: "design.contentBox" },
|
|
65
|
-
{ id: "accentFont", label: "Accent font", type: "font", scopes: ["deck", "slide"], path: "design.fontScheme.accent
|
|
66
|
+
{ id: "accentFont", label: "Accent font", type: "font", scopes: ["deck", "slide"], path: "design.fontScheme.accent" },
|
|
66
67
|
{ id: "logo", label: "Logo", type: "logo", scopes: ["deck", "slide"], path: "design.logo" },
|
|
67
68
|
{ id: "organizationLogo", label: "Organization logo", type: "organization-logo", scopes: ["deck"], path: "organization.logo" },
|
|
68
69
|
{ id: "watermark", label: "Watermark", type: "watermark", scopes: ["deck", "slide"], path: "design.watermark" },
|
|
@@ -72,27 +73,32 @@ const BY_ID = Object.fromEntries(DESIGN_OPTIONS.map((option) => [option.id, opti
|
|
|
72
73
|
|
|
73
74
|
const isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
74
75
|
|
|
75
|
-
function scopeOf(
|
|
76
|
+
function scopeOf(presentation, option, options) {
|
|
76
77
|
const index = options.slideIndex;
|
|
77
78
|
if (index === undefined) return { scope: "deck", base: [] };
|
|
78
79
|
if (!BY_ID[option]?.scopes.includes("slide")) throw fail("invalid-scope", `${BY_ID[option]?.label ?? option} applies to the whole presentation, not one slide.`, { option });
|
|
79
|
-
if (!Number.isInteger(index) || !
|
|
80
|
+
if (!Number.isInteger(index) || !presentation.slides?.[index]) throw fail("slide-index-out-of-range", "Choose an existing slide for this option.", { slideIndex: index });
|
|
80
81
|
return { scope: "slide", base: ["slides", String(index)], slideIndex: index };
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
function ownDesign(
|
|
84
|
-
const design = getValueAtPath(
|
|
84
|
+
function ownDesign(presentation, base) {
|
|
85
|
+
const design = getValueAtPath(presentation, base.length ? [...base, "design"] : ["design"]);
|
|
85
86
|
return isObject(design) ? design : {};
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
function primaryOrganization(
|
|
89
|
-
const list = Array.isArray(
|
|
89
|
+
function primaryOrganization(presentation) {
|
|
90
|
+
const list = Array.isArray(presentation.organization) ? presentation.organization : presentation.organization ? [presentation.organization] : [];
|
|
90
91
|
return list.find((entry) => entry?.role === "primary") ?? list[0];
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
function firstSpeaker(presentation) {
|
|
95
|
+
const list = Array.isArray(presentation.speaker) ? presentation.speaker : presentation.speaker ? [presentation.speaker] : [];
|
|
96
|
+
return list[0];
|
|
97
|
+
}
|
|
98
|
+
|
|
93
99
|
/** Whether a logo resolves for the slide: slide design, then deck design, then the primary organization. */
|
|
94
|
-
export function hasResolvableLogo(
|
|
95
|
-
return Boolean(
|
|
100
|
+
export function hasResolvableLogo(presentation, slideIndex) {
|
|
101
|
+
return Boolean(presentation.slides?.[slideIndex]?.design?.logo ?? presentation.design?.logo ?? primaryOrganization(presentation)?.logo);
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
/**
|
|
@@ -100,21 +106,23 @@ export function hasResolvableLogo(document, slideIndex) {
|
|
|
100
106
|
* footer zone with `logo: true`, or picture bullets. The renderers fall back to a glyph or report
|
|
101
107
|
* unresolved content; the editor surfaces it before export.
|
|
102
108
|
*/
|
|
103
|
-
export function designWarnings(
|
|
104
|
-
const slide =
|
|
105
|
-
const design = { ...
|
|
109
|
+
export function designWarnings(presentation, slideIndex = 0) {
|
|
110
|
+
const slide = presentation.slides?.[slideIndex];
|
|
111
|
+
const design = { ...presentation.design, ...slide?.design };
|
|
106
112
|
const warnings = [];
|
|
107
|
-
const hasLogo = hasResolvableLogo(
|
|
113
|
+
const hasLogo = hasResolvableLogo(presentation, slideIndex);
|
|
108
114
|
for (const which of ["header", "footer"])
|
|
109
115
|
for (const zone of HEADER_FOOTER_ZONES)
|
|
110
116
|
if (!hasLogo && isObject(design[which]) && design[which][zone]?.logo === true)
|
|
111
117
|
warnings.push({ code: "unresolved-logo", path: `design.${which}.${zone}.logo`, message: `The ${which} ${zone} zone shows the logo, but no logo is set. Add a logo or an organization logo.` });
|
|
112
|
-
const organization = primaryOrganization(
|
|
118
|
+
const organization = primaryOrganization(presentation);
|
|
113
119
|
for (const which of ["header", "footer"])
|
|
114
120
|
for (const zone of HEADER_FOOTER_ZONES) {
|
|
115
121
|
const item = isObject(design[which]) ? design[which][zone] : undefined;
|
|
116
122
|
if (item?.organization === true && !organization)
|
|
117
123
|
warnings.push({ code: "unresolved-content", path: `design.${which}.${zone}.organization`, message: `The ${which} ${zone} zone shows the organization, but the presentation has none.` });
|
|
124
|
+
if (item?.speaker === true && !firstSpeaker(presentation)?.name)
|
|
125
|
+
warnings.push({ code: "unresolved-content", path: `design.${which}.${zone}.speaker`, message: `The ${which} ${zone} zone shows the speaker, but the presentation has no named speaker.` });
|
|
118
126
|
if (item?.socials === true && !organization?.socials)
|
|
119
127
|
warnings.push({ code: "unresolved-content", path: `design.${which}.${zone}.socials`, message: `The ${which} ${zone} zone shows social profiles, but the organization has none.` });
|
|
120
128
|
}
|
|
@@ -123,17 +131,17 @@ export function designWarnings(document, slideIndex = 0) {
|
|
|
123
131
|
return warnings;
|
|
124
132
|
}
|
|
125
133
|
|
|
126
|
-
function shadowed(
|
|
127
|
-
return (
|
|
134
|
+
function shadowed(presentation, keys) {
|
|
135
|
+
return (presentation.slides ?? []).flatMap((slide, index) => (keys.some((key) => slide?.design?.[key] !== undefined) ? [index] : []));
|
|
128
136
|
}
|
|
129
137
|
|
|
130
|
-
function finish(
|
|
131
|
-
const before =
|
|
132
|
-
const next = checkedDocument(
|
|
138
|
+
function finish(presentation, patches, extra) {
|
|
139
|
+
const before = checkFormat(presentation);
|
|
140
|
+
const next = checkedDocument(presentation, patches, before);
|
|
133
141
|
const slideIndex = extra.slideIndex;
|
|
134
142
|
return {
|
|
135
143
|
...extra,
|
|
136
|
-
|
|
144
|
+
presentation: structuredClone(next),
|
|
137
145
|
patches,
|
|
138
146
|
changed: patches.length > 0,
|
|
139
147
|
warnings: designWarnings(next, slideIndex ?? 0),
|
|
@@ -145,16 +153,20 @@ function finish(document, patches, extra) {
|
|
|
145
153
|
function mergedWatermark(existing, value) {
|
|
146
154
|
if (value === null || value === false) return value;
|
|
147
155
|
if (typeof value === "string") return value;
|
|
148
|
-
if (!isObject(value)) throw fail("invalid-design-value", "A watermark is false, an image source or {
|
|
156
|
+
if (!isObject(value)) throw fail("invalid-design-value", "A watermark is false, an image source, { src, opacity } or { text, opacity }.", { value });
|
|
149
157
|
const base = typeof existing === "string" ? { src: existing } : isObject(existing) ? { ...existing } : {};
|
|
150
158
|
const merged = { ...base };
|
|
151
159
|
for (const [key, entry] of Object.entries(value)) {
|
|
152
160
|
if (entry === null || entry === undefined) delete merged[key];
|
|
153
161
|
else merged[key] = entry;
|
|
154
162
|
}
|
|
163
|
+
// FA-13: a watermark is an image (src) or a text stamp (text), never both: the field set last replaces the other.
|
|
164
|
+
if (value.text !== undefined && value.text !== null && value.src === undefined) delete merged.src;
|
|
165
|
+
else if (value.src !== undefined && value.src !== null && value.text === undefined) delete merged.text;
|
|
166
|
+
if (merged.text !== undefined && (typeof merged.text !== "string" || !merged.text.trim())) throw fail("invalid-design-value", "Watermark text is a non-empty string.", { value });
|
|
155
167
|
if (merged.opacity !== undefined && (typeof merged.opacity !== "number" || merged.opacity < 0 || merged.opacity > 1))
|
|
156
168
|
throw fail("invalid-design-value", "Watermark opacity is a number from 0 to 1.", { value });
|
|
157
|
-
if (merged.src === undefined && Object.keys(merged).length) throw fail("invalid-design-value", "Choose the watermark image before setting its opacity.", { value });
|
|
169
|
+
if (merged.src === undefined && merged.text === undefined && Object.keys(merged).length) throw fail("invalid-design-value", "Choose the watermark image before setting its opacity (or give it text instead).", { value });
|
|
158
170
|
if (Object.keys(merged).length === 1 && typeof merged.src === "string") return merged.src;
|
|
159
171
|
if (!Object.keys(merged).length) return null;
|
|
160
172
|
if (merged.opacity === undefined) throw fail("invalid-design-value", "Set an opacity from 0 to 1 for the watermark.", { value });
|
|
@@ -177,17 +189,17 @@ function mergedSlideImage(existing, value) {
|
|
|
177
189
|
return merged;
|
|
178
190
|
}
|
|
179
191
|
|
|
180
|
-
function fontSchemeWithAccent(
|
|
192
|
+
function fontSchemeWithAccent(presentation, base, family) {
|
|
181
193
|
// The slide's own font scheme, else the deck's: an object form is kept with its overrides.
|
|
182
|
-
const own = ownDesign(
|
|
183
|
-
const inherited = base.length ?
|
|
194
|
+
const own = ownDesign(presentation, base).fontScheme;
|
|
195
|
+
const inherited = base.length ? presentation.design?.fontScheme : undefined;
|
|
184
196
|
const source = own !== undefined ? own : inherited;
|
|
185
197
|
const object = typeof source === "string" ? { id: source } : isObject(source) ? structuredClone(source) : {};
|
|
186
198
|
if (family === null) {
|
|
187
199
|
delete object.accent;
|
|
188
200
|
} else {
|
|
189
201
|
if (typeof family !== "string" || !family.trim()) throw fail("invalid-design-value", "Accent font is a non-empty family name, or null to clear it.", { value: family });
|
|
190
|
-
object.accent =
|
|
202
|
+
object.accent = family.trim();
|
|
191
203
|
}
|
|
192
204
|
const keys = Object.keys(object);
|
|
193
205
|
if (!keys.length) return null;
|
|
@@ -217,17 +229,17 @@ function collapseLogo(set) {
|
|
|
217
229
|
* remove slide-level values that hide it) and `index` (organizationLogo with several
|
|
218
230
|
* organizations).
|
|
219
231
|
*/
|
|
220
|
-
export function prepareDesignOption(
|
|
232
|
+
export function prepareDesignOption(presentation, option, value, options = {}) {
|
|
221
233
|
const descriptor = BY_ID[option];
|
|
222
234
|
if (!descriptor) throw fail("unknown-design-option", `Unknown design option: ${option}. Use one of ${DESIGN_OPTIONS.map((entry) => entry.id).join(", ")}.`, { option });
|
|
223
|
-
if (!isObject(
|
|
235
|
+
if (!isObject(presentation)) throw fail("invalid-input", "Design options need an OPF document object.");
|
|
224
236
|
if (value === undefined) throw fail("invalid-design-value", "Pass a value, or null to remove the option.", { option });
|
|
225
|
-
const { scope, base, slideIndex } = scopeOf(
|
|
237
|
+
const { scope, base, slideIndex } = scopeOf(presentation, option, options);
|
|
226
238
|
let patches = [];
|
|
227
239
|
let keys = [];
|
|
228
240
|
|
|
229
241
|
if (descriptor.type === "organization-logo") {
|
|
230
|
-
const organization =
|
|
242
|
+
const organization = presentation.organization;
|
|
231
243
|
const index = options.index ?? 0;
|
|
232
244
|
const owner = Array.isArray(organization) ? organization[index] : organization;
|
|
233
245
|
if (!isObject(owner)) throw fail("missing-owner", "Add an organization to the document before setting its logo.", { option });
|
|
@@ -245,38 +257,38 @@ export function prepareDesignOption(document, option, value, options = {}) {
|
|
|
245
257
|
if (value !== null && typeof value !== "boolean") throw fail("invalid-design-value", `${descriptor.label} is true or false.`, { option, value });
|
|
246
258
|
entries = { [option]: value };
|
|
247
259
|
} else if (descriptor.type === "font") {
|
|
248
|
-
entries = { fontScheme: fontSchemeWithAccent(
|
|
260
|
+
entries = { fontScheme: fontSchemeWithAccent(presentation, base, value) };
|
|
249
261
|
} else if (descriptor.type === "logo") {
|
|
250
262
|
if (value !== null && typeof value !== "string" && !isObject(value)) throw fail("invalid-design-value", "A logo is an image source, an asset object or a set of logo variants.", { option });
|
|
251
263
|
entries = { logo: value };
|
|
252
264
|
} else if (descriptor.type === "watermark") {
|
|
253
|
-
const existing = ownDesign(
|
|
265
|
+
const existing = ownDesign(presentation, base).watermark;
|
|
254
266
|
entries = { watermark: mergedWatermark(existing, value) };
|
|
255
267
|
} else {
|
|
256
|
-
const existing = ownDesign(
|
|
268
|
+
const existing = ownDesign(presentation, base).slideImage;
|
|
257
269
|
entries = { slideImage: mergedSlideImage(existing, value) };
|
|
258
270
|
}
|
|
259
271
|
keys = Object.keys(entries);
|
|
260
|
-
patches = designPatches(
|
|
272
|
+
patches = designPatches(presentation, base, entries);
|
|
261
273
|
}
|
|
262
274
|
|
|
263
275
|
let shadowedSlides = [];
|
|
264
276
|
if (scope === "deck" && keys.length) {
|
|
265
|
-
shadowedSlides = shadowed(
|
|
277
|
+
shadowedSlides = shadowed(presentation, keys);
|
|
266
278
|
if (options.clearSlideOverrides) {
|
|
267
279
|
for (const index of shadowedSlides)
|
|
268
280
|
for (const key of keys)
|
|
269
|
-
if (
|
|
281
|
+
if (presentation.slides[index].design?.[key] !== undefined) patches.push({ op: "remove", path: opfPathToJsonPointer(["slides", String(index), "design", key]) });
|
|
270
282
|
shadowedSlides = [];
|
|
271
283
|
}
|
|
272
284
|
}
|
|
273
|
-
return finish(
|
|
285
|
+
return finish(presentation, patches, { option, scope, ...(slideIndex !== undefined ? { slideIndex } : {}), shadowed: shadowedSlides });
|
|
274
286
|
}
|
|
275
287
|
|
|
276
288
|
function apply(editor, prepared, meta = {}) {
|
|
277
|
-
const {
|
|
278
|
-
void
|
|
279
|
-
if (!prepared.changed) return { ...summary,
|
|
289
|
+
const { presentation, patches, ...summary } = prepared;
|
|
290
|
+
void presentation;
|
|
291
|
+
if (!prepared.changed) return { ...summary, presentation: editor.presentation, patches: [], inversePatches: [], validation: editor.validation };
|
|
280
292
|
const change = editor.applyPatch(patches, { ...meta, source: meta.source ?? "design-option", option: prepared.option, scope: prepared.scope });
|
|
281
293
|
return { ...change, ...summary };
|
|
282
294
|
}
|
|
@@ -289,30 +301,30 @@ function checkEditor(editor) {
|
|
|
289
301
|
export function setDesignOption(editor, option, value, options = {}) {
|
|
290
302
|
checkEditor(editor);
|
|
291
303
|
const { meta, ...rest } = options;
|
|
292
|
-
return apply(editor, prepareDesignOption(editor.
|
|
304
|
+
return apply(editor, prepareDesignOption(editor.presentation, option, value, rest), meta);
|
|
293
305
|
}
|
|
294
306
|
|
|
295
307
|
/**
|
|
296
308
|
* Read one design option for a panel: `{ value, scope, inherited }` where `scope` is "slide" when the
|
|
297
309
|
* slide's own design sets it, "deck" when the deck does, and "default" when neither does.
|
|
298
|
-
* `accentFont` reads the family; `organizationLogo` reads the organization.
|
|
310
|
+
* `accentFont` reads the family name; `organizationLogo` reads the organization.
|
|
299
311
|
*/
|
|
300
|
-
export function getDesignOption(
|
|
312
|
+
export function getDesignOption(presentation, option, options = {}) {
|
|
301
313
|
const descriptor = BY_ID[option];
|
|
302
314
|
if (!descriptor) throw fail("unknown-design-option", `Unknown design option: ${option}.`, { option });
|
|
303
315
|
if (descriptor.type === "organization-logo") {
|
|
304
|
-
const organization =
|
|
316
|
+
const organization = presentation.organization;
|
|
305
317
|
const owner = Array.isArray(organization) ? organization[options.index ?? 0] : organization;
|
|
306
318
|
return { value: owner?.logo, scope: owner?.logo === undefined ? "default" : "deck", inherited: false };
|
|
307
319
|
}
|
|
308
320
|
const key = descriptor.type === "font" ? "fontScheme" : option;
|
|
309
321
|
const pick = (design) => {
|
|
310
322
|
const value = design?.[key];
|
|
311
|
-
return descriptor.type === "font" ? (isObject(value) ? value.accent
|
|
323
|
+
return descriptor.type === "font" ? (isObject(value) ? value.accent : undefined) : value;
|
|
312
324
|
};
|
|
313
|
-
const slideValue = options.slideIndex === undefined ? undefined : pick(
|
|
325
|
+
const slideValue = options.slideIndex === undefined ? undefined : pick(presentation.slides?.[options.slideIndex]?.design);
|
|
314
326
|
if (slideValue !== undefined) return { value: slideValue, scope: "slide", inherited: false };
|
|
315
|
-
const deckValue = pick(
|
|
327
|
+
const deckValue = pick(presentation.design);
|
|
316
328
|
if (deckValue !== undefined) return { value: deckValue, scope: "deck", inherited: options.slideIndex !== undefined };
|
|
317
329
|
return { value: undefined, scope: "default", inherited: false };
|
|
318
330
|
}
|
|
@@ -324,26 +336,26 @@ export function getDesignOption(document, option, options = {}) {
|
|
|
324
336
|
* variant of `design.logo`. A single default logo stays a bare source; adding a second variant turns
|
|
325
337
|
* it into a LogoSet, and clearing back to the default collapses it again.
|
|
326
338
|
*/
|
|
327
|
-
export function prepareLogoVariant(
|
|
339
|
+
export function prepareLogoVariant(presentation, variant, source, options = {}) {
|
|
328
340
|
if (!LOGO_VARIANTS.includes(variant)) throw fail("invalid-design-value", `Logo variant is one of ${LOGO_VARIANTS.join(", ")}.`, { variant });
|
|
329
341
|
if (source !== null && typeof source !== "string" && !isObject(source)) throw fail("invalid-design-value", "A logo variant is an image source, an asset object, or null to clear it.", { variant });
|
|
330
342
|
if (typeof source === "string" && !source.trim()) throw fail("invalid-design-value", "Enter an image source or asset reference, or clear the variant.", { variant });
|
|
331
|
-
const { scope, base, slideIndex } = scopeOf(
|
|
332
|
-
const set = logoSetOf(ownDesign(
|
|
343
|
+
const { scope, base, slideIndex } = scopeOf(presentation, "logo", options);
|
|
344
|
+
const set = logoSetOf(ownDesign(presentation, base).logo);
|
|
333
345
|
if (source === null) delete set[variant];
|
|
334
346
|
else set[variant] = typeof source === "string" ? source.trim() : source;
|
|
335
|
-
const patches = designPatches(
|
|
336
|
-
return finish(
|
|
347
|
+
const patches = designPatches(presentation, base, { logo: collapseLogo(set) });
|
|
348
|
+
return finish(presentation, patches, { option: "logo", variant, scope, ...(slideIndex !== undefined ? { slideIndex } : {}), shadowed: [] });
|
|
337
349
|
}
|
|
338
350
|
/** Set or clear one logo variant as a single undoable transaction. */
|
|
339
351
|
export function setLogoVariant(editor, variant, source, options = {}) {
|
|
340
352
|
checkEditor(editor);
|
|
341
353
|
const { meta, ...rest } = options;
|
|
342
|
-
return apply(editor, prepareLogoVariant(editor.
|
|
354
|
+
return apply(editor, prepareLogoVariant(editor.presentation, variant, source, rest), meta);
|
|
343
355
|
}
|
|
344
356
|
/** The variants `design.logo` sets at a scope: `{ variant: source }`, a bare logo reported as `default`. */
|
|
345
|
-
export function readLogoVariants(
|
|
346
|
-
const own = options.slideIndex === undefined ?
|
|
357
|
+
export function readLogoVariants(presentation, options = {}) {
|
|
358
|
+
const own = options.slideIndex === undefined ? presentation.design?.logo : presentation.slides?.[options.slideIndex]?.design?.logo;
|
|
347
359
|
return logoSetOf(own);
|
|
348
360
|
}
|
|
349
361
|
|
|
@@ -351,7 +363,7 @@ export function readLogoVariants(document, options = {}) {
|
|
|
351
363
|
|
|
352
364
|
/**
|
|
353
365
|
* Compute the patch that edits one header or footer zone. `fields` merges into the zone (logo, text,
|
|
354
|
-
* image, slideNumber, slideNumberFormat, date, dateFormat, organization, socials, section); `null`,
|
|
366
|
+
* image, slideNumber, slideNumberFormat, date, dateFormat, organization, speaker, socials, section); `null`,
|
|
355
367
|
* `false` for a flag, or an empty string removes a field. A zone left empty is removed, then an empty
|
|
356
368
|
* header or footer, so a slide never carries `{}`. A slide's own header or footer replaces the deck's
|
|
357
369
|
* whole one, so the first edit on a slide that has none of its own starts from a copy of the deck's
|
|
@@ -359,15 +371,15 @@ export function readLogoVariants(document, options = {}) {
|
|
|
359
371
|
* inheriting it again. Setting a field on a suppressed (`false`) header replaces the suppression.
|
|
360
372
|
* `logo: true` reports a warning when no logo resolves.
|
|
361
373
|
*/
|
|
362
|
-
export function prepareHeaderFooterZone(
|
|
374
|
+
export function prepareHeaderFooterZone(presentation, which, zone, fields, options = {}) {
|
|
363
375
|
if (!["header", "footer"].includes(which)) throw fail("invalid-design-value", "Choose header or footer.", { which });
|
|
364
376
|
if (!HEADER_FOOTER_ZONES.includes(zone)) throw fail("invalid-design-value", `Zone is one of ${HEADER_FOOTER_ZONES.join(", ")}.`, { zone });
|
|
365
377
|
if (!isObject(fields)) throw fail("invalid-design-value", "Pass the zone fields to change.", { fields });
|
|
366
378
|
const unknown = Object.keys(fields).filter((key) => !ZONE_FIELDS.includes(key));
|
|
367
379
|
if (unknown.length) throw fail("invalid-design-value", `Unknown header/footer field: ${unknown[0]}.`, { fields });
|
|
368
|
-
const { scope, base, slideIndex } = scopeOf(
|
|
369
|
-
const own = ownDesign(
|
|
370
|
-
const inherited = base.length ?
|
|
380
|
+
const { scope, base, slideIndex } = scopeOf(presentation, "logo", options);
|
|
381
|
+
const own = ownDesign(presentation, base)[which];
|
|
382
|
+
const inherited = base.length ? presentation.design?.[which] : undefined;
|
|
371
383
|
const current = own !== undefined ? own : inherited;
|
|
372
384
|
const container = isObject(current) ? structuredClone(current) : {};
|
|
373
385
|
const item = isObject(container[zone]) ? container[zone] : {};
|
|
@@ -386,27 +398,27 @@ export function prepareHeaderFooterZone(document, which, zone, fields, options =
|
|
|
386
398
|
let next = Object.keys(container).length ? container : null;
|
|
387
399
|
// Emptied: a deck value that would show through again is hidden explicitly; a suppressed header stays suppressed.
|
|
388
400
|
if (next === null && (own === false || isObject(inherited))) next = false;
|
|
389
|
-
const patches = designPatches(
|
|
390
|
-
return finish(
|
|
401
|
+
const patches = designPatches(presentation, base, { [which]: next });
|
|
402
|
+
return finish(presentation, patches, { option: which, zone, scope, ...(slideIndex !== undefined ? { slideIndex } : {}), shadowed: [] });
|
|
391
403
|
}
|
|
392
404
|
/** Edit one header or footer zone as a single undoable transaction. */
|
|
393
405
|
export function setHeaderFooterZone(editor, which, zone, fields, options = {}) {
|
|
394
406
|
checkEditor(editor);
|
|
395
407
|
const { meta, ...rest } = options;
|
|
396
|
-
return apply(editor, prepareHeaderFooterZone(editor.
|
|
408
|
+
return apply(editor, prepareHeaderFooterZone(editor.presentation, which, zone, fields, rest), meta);
|
|
397
409
|
}
|
|
398
410
|
/**
|
|
399
411
|
* One header or footer zone's fields as they apply at a scope: the slide's own header (or footer) when it
|
|
400
412
|
* has one, else the deck's (`{}` when absent or suppressed). `headerFooterState` says which.
|
|
401
413
|
*/
|
|
402
|
-
export function readHeaderFooterZone(
|
|
403
|
-
const own = options.slideIndex === undefined ? undefined :
|
|
404
|
-
const effective = own !== undefined ? own :
|
|
414
|
+
export function readHeaderFooterZone(presentation, which, zone, options = {}) {
|
|
415
|
+
const own = options.slideIndex === undefined ? undefined : presentation.slides?.[options.slideIndex]?.design?.[which];
|
|
416
|
+
const effective = own !== undefined ? own : presentation.design?.[which];
|
|
405
417
|
return isObject(effective) && isObject(effective[zone]) ? structuredClone(effective[zone]) : {};
|
|
406
418
|
}
|
|
407
419
|
/** `{ own, inherited, hidden }` for a header or footer at a scope: whether the scope sets it itself, shows the deck's, or hides it with `false`. */
|
|
408
|
-
export function headerFooterState(
|
|
409
|
-
const own = options.slideIndex === undefined ?
|
|
410
|
-
const inherited = options.slideIndex !== undefined && own === undefined &&
|
|
411
|
-
return { own: own !== undefined, inherited, hidden: (own !== undefined ? own : options.slideIndex !== undefined ?
|
|
420
|
+
export function headerFooterState(presentation, which, options = {}) {
|
|
421
|
+
const own = options.slideIndex === undefined ? presentation.design?.[which] : presentation.slides?.[options.slideIndex]?.design?.[which];
|
|
422
|
+
const inherited = options.slideIndex !== undefined && own === undefined && presentation.design?.[which] !== undefined;
|
|
423
|
+
return { own: own !== undefined, inherited, hidden: (own !== undefined ? own : options.slideIndex !== undefined ? presentation.design?.[which] : undefined) === false };
|
|
412
424
|
}
|
package/dist/edit-helpers.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Helpers shared by the dimension switches, the design options, the block conversions and the
|
|
2
2
|
// table options: the error constructor, structural equality and the design-key patch rules.
|
|
3
|
-
import { OPFEditorError, applyJsonPatch, getValueAtPath, opfPathToJsonPointer
|
|
3
|
+
import { OPFEditorError, applyJsonPatch, getValueAtPath, opfPathToJsonPointer } from "./index.js";
|
|
4
|
+
import { firstErrorMessage, errorFindings, checkFormat } from "./checks.js";
|
|
4
5
|
|
|
5
6
|
export function fail(code, message, details) {
|
|
6
7
|
return new OPFEditorError(code, message, details);
|
|
@@ -19,8 +20,8 @@ export function same(a, b) {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
// Set (value) or remove (null) design keys at deck or slide scope.
|
|
22
|
-
export function designPatches(
|
|
23
|
-
const design = getValueAtPath(
|
|
23
|
+
export function designPatches(presentation, base, entries) {
|
|
24
|
+
const design = getValueAtPath(presentation, base.length ? [...base, "design"] : ["design"]);
|
|
24
25
|
const at = (key) => opfPathToJsonPointer([...base, "design", key]);
|
|
25
26
|
const set = Object.entries(entries).filter(([, value]) => value !== undefined && value !== null);
|
|
26
27
|
if (!design || typeof design !== "object" || Array.isArray(design))
|
|
@@ -41,12 +42,12 @@ export function designPatches(document, base, entries) {
|
|
|
41
42
|
* Validate a candidate patch the way every switch does: the result must be valid OPF unless the
|
|
42
43
|
* input document was already invalid (then nothing new may be reported as the cause).
|
|
43
44
|
*/
|
|
44
|
-
export function checkedDocument(
|
|
45
|
-
const next = patches.length ? applyJsonPatch(
|
|
45
|
+
export function checkedDocument(presentation, patches, before) {
|
|
46
|
+
const next = patches.length ? applyJsonPatch(presentation, patches) : presentation;
|
|
46
47
|
if (patches.length) {
|
|
47
|
-
const validation =
|
|
48
|
+
const validation = checkFormat(next);
|
|
48
49
|
if (!validation.valid && before.valid)
|
|
49
|
-
throw fail("invalid-opf-edit", validation
|
|
50
|
+
throw fail("invalid-opf-edit", firstErrorMessage(validation, "This change produces an invalid document."), { issues: errorFindings(validation), patches });
|
|
50
51
|
}
|
|
51
52
|
return next;
|
|
52
53
|
}
|
package/dist/exact-source.js
CHANGED
|
@@ -228,11 +228,11 @@ function typingSignature(previous, ops) {
|
|
|
228
228
|
/**
|
|
229
229
|
* Change only the affected values. Keeps authored indentation, escaping and all unrelated
|
|
230
230
|
* metadata/content byte-for-byte, including across canvas Escape, Undo and Redo.
|
|
231
|
-
* Throws when `source` is not valid JSON. The result always parses to `
|
|
231
|
+
* Throws when `source` is not valid JSON. The result always parses to `presentation`; if an
|
|
232
232
|
* in-place edit cannot achieve that (for example the source repeats an object key), the whole
|
|
233
233
|
* document is written as indented JSON instead.
|
|
234
234
|
*/
|
|
235
|
-
export function updateJsonSource(source,
|
|
235
|
+
export function updateJsonSource(source, presentation, memory = sharedMemory) {
|
|
236
236
|
const previous = JSON.parse(source);
|
|
237
237
|
const previousKey = JSON.stringify(previous);
|
|
238
238
|
memory.limited = source.length > MAX_EXACT_SOURCE_LENGTH;
|
|
@@ -243,9 +243,9 @@ export function updateJsonSource(source, document, memory = sharedMemory) {
|
|
|
243
243
|
memory.last = null;
|
|
244
244
|
}
|
|
245
245
|
const ops = [];
|
|
246
|
-
diff(previous,
|
|
246
|
+
diff(previous, presentation, [], ops);
|
|
247
247
|
if (!ops.length) return source;
|
|
248
|
-
const key = JSON.stringify(
|
|
248
|
+
const key = JSON.stringify(presentation);
|
|
249
249
|
const remembered = recall(memory, key);
|
|
250
250
|
if (remembered !== undefined && equalOutside(source, remembered, maskPaths(ops))) {
|
|
251
251
|
memory.last = null;
|
|
@@ -306,7 +306,7 @@ export function updateJsonSource(source, document, memory = sharedMemory) {
|
|
|
306
306
|
valid = JSON.stringify(parsed) === key || canonical(parsed) === canonical(JSON.parse(key));
|
|
307
307
|
} catch { /* Fall back below. */ }
|
|
308
308
|
if (!valid) {
|
|
309
|
-
result = JSON.stringify(
|
|
309
|
+
result = JSON.stringify(presentation, null, indent).replaceAll("\n", eol);
|
|
310
310
|
if (/\n$/.test(source)) result += eol;
|
|
311
311
|
}
|
|
312
312
|
|
package/dist/export.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RenderSvgOptions } from "@openpresentation/opf-render";
|
|
2
|
-
import type {
|
|
2
|
+
import type { EditorFonts } from "./canvas.js";
|
|
3
3
|
|
|
4
4
|
export type ExportFormat = "pdf" | "png" | "svg";
|
|
5
5
|
export declare const EXPORT_FORMATS: Readonly<Record<ExportFormat, { extension: string; type: string; label: string }>>;
|
|
@@ -44,12 +44,10 @@ export interface ExportOptions {
|
|
|
44
44
|
pdfMode?: "vector" | "raster";
|
|
45
45
|
/** PNG pixel density (and the raster PDF's), 1 to 4; default 2. */
|
|
46
46
|
scale?: number;
|
|
47
|
-
/** The options the host draws its preview with (`
|
|
47
|
+
/** The options the host draws its preview with (`catalogs`, `date`, ...); the fonts go in `fonts`. */
|
|
48
48
|
renderOptions?: RenderSvgOptions;
|
|
49
|
-
/**
|
|
50
|
-
fonts?:
|
|
51
|
-
/** The browser font registry; its faces are what gets embedded. */
|
|
52
|
-
registry?: object;
|
|
49
|
+
/** The renderer's fonts handle (`loadFonts()`): it measures the text, loads the faces the deck needs before anything is drawn, and its registry's faces are what gets embedded. */
|
|
50
|
+
fonts?: EditorFonts;
|
|
53
51
|
/** Faces to embed instead of the registry's. */
|
|
54
52
|
embeddedFonts?: Array<Record<string, unknown>>;
|
|
55
53
|
/** PDF document properties beyond the title and subject taken from the deck. */
|
|
@@ -71,7 +69,7 @@ export interface ExportResult {
|
|
|
71
69
|
slides: number[];
|
|
72
70
|
}
|
|
73
71
|
/**
|
|
74
|
-
* Draw the deck with the renderer the preview uses and convert it. Rejects with an error whose `code` is `export-aborted`, `export-no-slides`,
|
|
72
|
+
* Draw the deck with the renderer the preview uses and convert it. Rejects with an error whose `code` is `export-aborted`, `export-fonts-unlicensed` (the license rule left the PDF converter no face for some text), `export-no-slides`,
|
|
75
73
|
* `export-format`, `export-unavailable` (PDF and PNG need the renderer's `export-browser` entry), `fonts-unavailable` or a renderer code.
|
|
76
74
|
*/
|
|
77
75
|
export declare function exportDeck(deck: unknown, options: ExportOptions): Promise<ExportResult>;
|
package/dist/export.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// RR-23: download a deck as PDF, PNG or SVG from the editor, next to the PowerPoint export. Everything runs in the page:
|
|
2
|
-
// the slides are drawn by the same renderer the preview uses (`
|
|
3
|
-
// a download is the preview, not a second layout. Fonts are the
|
|
4
|
-
//
|
|
2
|
+
// the slides are drawn by the same renderer the preview uses (`renderSvg`, with the fonts handle's text measurement), so
|
|
3
|
+
// a download is the preview, not a second layout. Fonts are the renderer's fonts handle's: the faces the document needs load through
|
|
4
|
+
// the handle first, and only faces its registry holds (bundled or hash-pinned, permissively licensed) are embedded, as
|
|
5
5
|
// @font-face data in each SVG and as subsets in the PDF. No system font is read and nothing is fetched at export time.
|
|
6
6
|
//
|
|
7
|
-
// const result = await exportDeck(editor.
|
|
7
|
+
// const result = await exportDeck(editor.presentation, { format: "pdf", fonts, renderOptions, signal, onProgress });
|
|
8
8
|
// // result.download is { name, type, bytes }: one PDF, one PNG or SVG, or a ZIP of the slides.
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { renderSvg } from "@openpresentation/opf-render/svg";
|
|
11
|
+
import { fontGate } from "./font-gate.js";
|
|
11
12
|
import { createZip } from "./zip.js";
|
|
12
13
|
|
|
13
14
|
export const EXPORT_FORMATS = Object.freeze({
|
|
@@ -69,6 +70,19 @@ function exportError(code, message, cause) {
|
|
|
69
70
|
return error;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
// The faces a PDF may embed: the same permissive-license list as the SVG (`embeddableFonts`), as the bytes the converter reads. The fonts
|
|
74
|
+
// handle itself is never given to `svgToPdf`: it would embed every face the registry holds, whatever its license.
|
|
75
|
+
function pdfFontData(faces) {
|
|
76
|
+
return faces.map((face) => {
|
|
77
|
+
const text = String(face.dataUrl ?? "");
|
|
78
|
+
const base64 = text.slice(text.indexOf(",") + 1);
|
|
79
|
+
const binary = atob(base64);
|
|
80
|
+
const data = new Uint8Array(binary.length);
|
|
81
|
+
for (let index = 0; index < binary.length; index += 1) data[index] = binary.charCodeAt(index);
|
|
82
|
+
return { family: face.family, data };
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
72
86
|
const fontLabel = (face) => `${face.family} ${face.weight}${face.italic ? " italic" : ""}`;
|
|
73
87
|
|
|
74
88
|
function slideOf(path) {
|
|
@@ -110,12 +124,17 @@ async function loadConverters(options) {
|
|
|
110
124
|
* - `slides`: "current" (with `slideIndex`), "all" (default; hidden slides only with `includeHidden`) or an array of slide numbers.
|
|
111
125
|
* - `pdfMode`: "vector" (default: selectable text, vector shapes) or "raster" (each slide an image).
|
|
112
126
|
* - `scale`: PNG pixel density, 1 to 4 (default 2); also the raster PDF's density.
|
|
113
|
-
* - `renderOptions`: the render options the host draws with (`
|
|
114
|
-
* - `fonts`:
|
|
115
|
-
*
|
|
127
|
+
* - `renderOptions`: the render options the host draws with (`catalogs`, `date`, ...), the same as its preview.
|
|
128
|
+
* - `fonts`: the renderer's fonts handle (`loadFonts()` from `@openpresentation/opf-render/fonts-browser`), the one the preview uses. Its
|
|
129
|
+
* `textMeasurement` lays the slides out, the faces the deck needs load through its `ensure` before anything is drawn, and its registry's
|
|
130
|
+
* faces are what gets embedded: the SVG carries the faces its slides draw, and the PDF embeds the registry's faces (script faces included).
|
|
131
|
+
* Both apply the same license rule (OFL-1.1, Apache-2.0, MIT or UFL-1.0): a face whose license text names none of them is left out of the SVG
|
|
132
|
+
* and the PDF and reported once as `export-font-license`. Text that needed it is drawn with another face the registry holds and
|
|
133
|
+
* reported by the converter (`pdf-font-substituted`, `pdf-glyph-missing`); when no face is left at all the PDF rejects with
|
|
134
|
+
* `export-fonts-unlicensed` (the converter's error is its `cause`).
|
|
116
135
|
* - `signal`, `onProgress({ stage, done, total, message })`, `onDiagnostic(diagnostic)`.
|
|
117
136
|
* Resolves `{ download, files, diagnostics, slides }`. Rejects with an error whose `code` is `export-aborted`,
|
|
118
|
-
* `export-no-slides`, `export-unavailable`, `fonts-unavailable` or a renderer code.
|
|
137
|
+
* `export-fonts-unlicensed`, `export-no-slides`, `export-unavailable`, `fonts-unavailable` or a renderer code.
|
|
119
138
|
*/
|
|
120
139
|
export async function exportDeck(deck, options = {}) {
|
|
121
140
|
const format = EXPORT_FORMATS[options.format];
|
|
@@ -133,20 +152,22 @@ export async function exportDeck(deck, options = {}) {
|
|
|
133
152
|
const renderOptions = options.renderOptions ?? {};
|
|
134
153
|
|
|
135
154
|
check();
|
|
136
|
-
|
|
155
|
+
const gate = fontGate(options.fonts);
|
|
156
|
+
if (gate?.pending(deck, renderOptions).length) {
|
|
137
157
|
progress("fonts", 0, 1, "Loading fonts…");
|
|
138
|
-
try { await
|
|
158
|
+
try { await gate.ensure(deck, { signal, renderOptions }); }
|
|
139
159
|
catch (error) { if (signal?.aborted) throw exportError("export-aborted", "The export was cancelled."); throw error; }
|
|
140
160
|
}
|
|
141
161
|
check();
|
|
142
162
|
const converters = format.extension === "svg" ? null : await loadConverters(options);
|
|
143
163
|
|
|
144
|
-
const
|
|
164
|
+
const dropped = [];
|
|
165
|
+
const embeddedFonts = options.embeddedFonts ?? embeddableFonts(options.fonts?.registry, (face) => { dropped.push(face); note({ code: "export-font-license", severity: "warning", message: `${fontLabel(face)} is not embedded: its license is not one of OFL-1.1, Apache-2.0, MIT or UFL-1.0.`, family: face.family }); });
|
|
145
166
|
progress("render", 0, indexes.length, "Drawing slides…");
|
|
146
167
|
await yieldToHost();
|
|
147
|
-
const rendered =
|
|
168
|
+
const rendered = renderSvg(deck, {
|
|
148
169
|
...renderOptions,
|
|
149
|
-
embeddedFonts,
|
|
170
|
+
fonts: { textMeasurement: options.fonts?.textMeasurement, embeddedFonts },
|
|
150
171
|
// The PDF reports problems by element path; the SVG and PNG are the plain drawing.
|
|
151
172
|
trace: format.extension === "pdf",
|
|
152
173
|
onDiagnostic: (diagnostic) => {
|
|
@@ -186,12 +207,17 @@ export async function exportDeck(deck, options = {}) {
|
|
|
186
207
|
scale,
|
|
187
208
|
metadata,
|
|
188
209
|
signal,
|
|
210
|
+
fontData: pdfFontData(embeddedFonts),
|
|
189
211
|
...(options.fallbackFamily ? { defaultFontFamily: options.fallbackFamily } : {}),
|
|
190
212
|
onDiagnostic: (diagnostic) => { const described = describeDiagnostic(diagnostic, "pdf"); if (keep(described)) note(described); },
|
|
191
213
|
onProgress: ({ page, pages }) => progress("convert", page, pages, `Page ${page} of ${pages}`),
|
|
192
214
|
});
|
|
193
215
|
} catch (error) {
|
|
194
216
|
if (signal?.aborted) throw exportError("export-aborted", "The export was cancelled.");
|
|
217
|
+
// The license rule can leave the converter with no face for some text (every face of a family was dropped): say why, not only what.
|
|
218
|
+
if (dropped.length && /font face/i.test(String(error?.message))) {
|
|
219
|
+
throw exportError("export-fonts-unlicensed", `The PDF could not be written: ${dropped.map(fontLabel).join(", ")} ${dropped.length === 1 ? "is" : "are"} not embedded because the license is not one of OFL-1.1, Apache-2.0, MIT or UFL-1.0, and no other face is available. ${error.message}`, error);
|
|
220
|
+
}
|
|
195
221
|
throw error;
|
|
196
222
|
}
|
|
197
223
|
files = [{ name: exportFileName(deck, "pdf"), type: format.type, bytes }];
|