@openpresentation/opf-pptx 0.13.2 → 0.15.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 +57 -37
- package/dist/annotation-export.js +1 -1
- package/dist/annotation-provenance.js +4 -0
- package/dist/background-import.js +33 -17
- package/dist/background.js +30 -28
- package/dist/body-text-import.js +16 -4
- package/dist/chart-alt.js +54 -0
- package/dist/chart-data.js +29 -54
- package/dist/chart-highlight.js +113 -0
- package/dist/chart-options.js +46 -24
- package/dist/chart-types.js +61 -75
- package/dist/code-provenance.js +20 -4
- package/dist/color-ref.js +12 -6
- package/dist/data-provenance.js +18 -14
- package/dist/document-provenance.js +208 -129
- package/dist/format-check.js +10 -0
- package/dist/furniture-provenance.js +30 -3
- package/dist/heading-provenance.js +10 -3
- package/dist/image-fallback-node.js +4 -1
- package/dist/image-provenance.js +273 -0
- package/dist/index.d.ts +57 -35
- package/dist/index.js +724 -399
- package/dist/media-provenance.js +3 -3
- package/dist/metric-provenance.js +3 -3
- package/dist/native-text-style.js +2 -0
- package/dist/package-fonts.js +2 -2
- package/dist/quote-provenance.js +70 -26
- package/dist/rich-heading.js +119 -0
- package/dist/run-colors.js +3 -0
- package/dist/script-fonts.js +102 -101
- package/dist/theme-colors.js +16 -11
- package/dist/timeline-provenance.js +17 -6
- package/dist/typeface-inventory.js +8 -8
- package/dist/watermark-provenance.js +65 -3
- package/package.json +4 -4
- package/dist/slide-image-provenance.js +0 -177
package/dist/script-fonts.js
CHANGED
|
@@ -4,43 +4,31 @@
|
|
|
4
4
|
// reads it back on import. Design: core docs/programs/font-fidelity-everywhere/
|
|
5
5
|
// script-font-model.md ("OOXML mapping").
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
// `language-export-unavailable` for a document that names a language. A core
|
|
11
|
-
// with the resolver but without paragraphDirection() marks no paragraph
|
|
12
|
-
// direction and reports `paragraph-direction-unavailable` for an RTL deck.
|
|
13
|
-
import * as opfCore from "@openpresentation/opf";
|
|
14
|
-
|
|
15
|
-
const resolver = typeof opfCore.resolveScriptFonts === "function" ? opfCore.resolveScriptFonts : null;
|
|
16
|
-
// RR-05: authored alignment is logical for right-to-left text (`left` is the start edge). Core owns the rule; a core without it
|
|
17
|
-
// reports no line directions either, so the fallback never has anything to flip.
|
|
18
|
-
export const physicalAlignment = typeof opfCore.physicalAlignment === "function" ? opfCore.physicalAlignment : alignment => alignment;
|
|
19
|
-
// The one paragraph-direction rule shared with the renderer (core FF-07).
|
|
20
|
-
const paragraphDirection = typeof opfCore.paragraphDirection === "function" ? opfCore.paragraphDirection : null;
|
|
7
|
+
// When the resolver throws for a deck, the exporter writes what it wrote before FF-07 (lang="en-US", empty theme ea/cs, no
|
|
8
|
+
// rtl) and reports `language-export-unavailable`.
|
|
9
|
+
import {paragraphDirection, physicalAlignment, resolveScriptFonts as coreResolveScriptFonts} from "@openpresentation/opf/composition";
|
|
21
10
|
|
|
22
|
-
|
|
11
|
+
// OPF 0.15 (FA-23): core resolves the font-scheme references a deck or a language object names against the document's
|
|
12
|
+
// catalogs and the host's registered `catalogs` (Catalog[]), the same option the slide context takes. `catalogs` is passed
|
|
13
|
+
// only when the host gave one, so core's own default applies otherwise.
|
|
14
|
+
const resolver = (input, options = {}, catalogs = undefined) => coreResolveScriptFonts(input, catalogs === undefined ? options : {...options, catalogs});
|
|
23
15
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
// RR-05: authored alignment is logical for right-to-left text (`left` is the start edge). Core owns the rule, and the
|
|
17
|
+
// paragraph-direction rule is the one the renderer shares (core FF-07).
|
|
18
|
+
export {physicalAlignment};
|
|
19
|
+
|
|
20
|
+
const SCRIPT_SLOTS = [["ea", "eastAsian"], ["cs", "complexScript"]];
|
|
28
21
|
|
|
29
22
|
/**
|
|
30
23
|
* Resolve the deck (slide 0, which also sets the theme) and every slide.
|
|
31
|
-
* Returns null when
|
|
24
|
+
* Returns null when the resolver throws for the deck.
|
|
32
25
|
*/
|
|
33
|
-
export function planScriptFonts(presentation, report) {
|
|
34
|
-
if (!resolver) {
|
|
35
|
-
if (presentation.language !== undefined) report?.({code: "language-export-unavailable", path: "language",
|
|
36
|
-
message: "The installed @openpresentation/opf has no resolveScriptFonts, so the PPTX keeps lang=\"en-US\", empty theme East Asian/complex-script fonts and left-to-right paragraphs. Use a core release with the FF-18 language model."});
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
26
|
+
export function planScriptFonts(presentation, report, {catalogs} = {}) {
|
|
39
27
|
const count = Array.isArray(presentation.slides) ? presentation.slides.length : 0;
|
|
40
28
|
let slides, deck;
|
|
41
29
|
try {
|
|
42
|
-
slides = Array.from({length: count}, (_, slideIndex) => resolver(presentation, {slideIndex}));
|
|
43
|
-
deck = slides[0] ?? resolver(presentation);
|
|
30
|
+
slides = Array.from({length: count}, (_, slideIndex) => resolver(presentation, {slideIndex}, catalogs));
|
|
31
|
+
deck = slides[0] ?? resolver(presentation, {}, catalogs);
|
|
44
32
|
} catch (error) {
|
|
45
33
|
// Keep exporting as before FF-07 rather than failing on the language model.
|
|
46
34
|
report?.({code: "language-export-unavailable", path: "language",
|
|
@@ -49,17 +37,65 @@ export function planScriptFonts(presentation, report) {
|
|
|
49
37
|
}
|
|
50
38
|
if (presentation.language !== undefined && deck.languageSource === "default") {
|
|
51
39
|
report?.({code: "language-unresolved", path: "language",
|
|
52
|
-
message: `The presentation language could not be resolved
|
|
53
|
-
}
|
|
54
|
-
let rtl = deck.rtl;
|
|
55
|
-
if (rtl && !paragraphDirection) {
|
|
56
|
-
rtl = false;
|
|
57
|
-
report?.({code: "paragraph-direction-unavailable", path: "language",
|
|
58
|
-
message: "The installed @openpresentation/opf has no paragraphDirection, so right-to-left paragraphs are not marked; the preview and export must share that rule. Use a core release that exports it."});
|
|
40
|
+
message: `The presentation language could not be resolved (a BCP-47 tag whose script core does not know), so the PPTX uses ${deck.lang}.`});
|
|
59
41
|
}
|
|
60
42
|
// opf-pptx#168: which slides have notes (a presentation has one notes master; see reportPerSlideNotesScriptFonts).
|
|
61
43
|
const notes = (presentation.slides ?? []).map(slide => typeof slide?.notes === "string" ? slide.notes.trim() !== "" : Boolean(slide?.notes));
|
|
62
|
-
return {deck, slides, notes, report, lang: deck.lang, rtl, contentEastAsian: contentEastAsianFonts(presentation, deck, report)};
|
|
44
|
+
return {deck, slides, notes, report, lang: deck.lang, rtl: deck.rtl, presentation, catalogs, contentEastAsian: contentEastAsianFonts(presentation, deck, report, catalogs)};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* FA-13: the OOXML tag a run's own `lang` (TextRun.lang) is written with: core's curated tag for the language (fr becomes
|
|
49
|
+
* fr-FR), or undefined when it is the deck language (the run then follows the deck). Without a plan the tag is written as given.
|
|
50
|
+
*/
|
|
51
|
+
export function runLanguageTag(plan, tag) {
|
|
52
|
+
if (typeof tag !== "string" || !tag) return undefined;
|
|
53
|
+
if (!plan) return tag.toLowerCase() === "en-us" ? undefined : tag;
|
|
54
|
+
const cache = plan.runLanguages ??= new Map();
|
|
55
|
+
if (!cache.has(tag)) {
|
|
56
|
+
let lang = tag;
|
|
57
|
+
try {
|
|
58
|
+
const resolved = resolver(plan.presentation, {language: tag}, plan.catalogs);
|
|
59
|
+
if (resolved.languageSource !== "default") lang = resolved.lang;
|
|
60
|
+
} catch { /* an unresolvable tag is written as given */ }
|
|
61
|
+
cache.set(tag, lang.toLowerCase() === String(plan.lang).toLowerCase() ? undefined : lang);
|
|
62
|
+
}
|
|
63
|
+
return cache.get(tag);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* FA-13: a run with its own language (TextRun.lang) names the East Asian and complex-script faces of that language, so a Han
|
|
68
|
+
* run tagged ja-JP draws in the Japanese face and one tagged zh-CN in the Simplified Chinese face, as the preview does. Only a
|
|
69
|
+
* slot with a script-specific font for the run's language is written; every other run keeps following the theme. Runs the
|
|
70
|
+
* exporter writes carry no ea/cs of their own (stripRunScriptFonts), so this runs after that strip. `xml` is a slide part.
|
|
71
|
+
*/
|
|
72
|
+
export function runLanguageFonts(xml, plan, slideIndex) {
|
|
73
|
+
if (!plan || !xml.includes(' altLang="en-US"')) return xml;
|
|
74
|
+
return xml.replace(/<p:(sp|graphicFrame)>[\s\S]*?<\/p:\1>/g, shape => {
|
|
75
|
+
const heading = /<p:cNvPr\b[^>]*\bname="OPF heading /.test(shape);
|
|
76
|
+
return shape.replace(OWN_LANGUAGE_RUN, (run, lang) => {
|
|
77
|
+
const resolved = runScriptPlan(plan, slideIndex, lang);
|
|
78
|
+
if (!resolved) return run;
|
|
79
|
+
const slots = heading ? resolved.heading : resolved.body;
|
|
80
|
+
const latin = /<a:latin typeface="([^"]*)"/.exec(run)?.[1];
|
|
81
|
+
const insert = SCRIPT_SLOTS.map(([element, slot]) => {
|
|
82
|
+
const face = slots?.[slot];
|
|
83
|
+
return resolved.sources?.[slot] !== "latin" && face && escapeAttribute(face) !== latin && !new RegExp(`<a:${element}\\b`).test(run) ? `<a:${element} typeface="${escapeAttribute(face)}"/>` : "";
|
|
84
|
+
}).join("");
|
|
85
|
+
return insert ? run.replace(/(<a:latin\b[^>]*\/>)/, `$1${insert}`) : run;
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The script fonts of one slide for a run language (FA-13), memoized; undefined when the language cannot be resolved. */
|
|
91
|
+
function runScriptPlan(plan, slideIndex, lang) {
|
|
92
|
+
const cache = plan.runScripts ??= new Map(), key = `${slideIndex}|${lang}`;
|
|
93
|
+
if (!cache.has(key)) {
|
|
94
|
+
let resolved;
|
|
95
|
+
try { resolved = resolver(plan.presentation, {slideIndex, language: lang}, plan.catalogs); } catch { resolved = undefined; }
|
|
96
|
+
cache.set(key, resolved && resolved.languageSource !== "default" ? resolved : undefined);
|
|
97
|
+
}
|
|
98
|
+
return cache.get(key);
|
|
63
99
|
}
|
|
64
100
|
|
|
65
101
|
// ---------------------------------------------------------------------------
|
|
@@ -162,7 +198,7 @@ export function reportPerSlideNotesScriptFonts(plan, {themes, assignment}) {
|
|
|
162
198
|
// script of the text picks the language core resolves it for: kana is Japanese, hangul Korean, Han alone Simplified
|
|
163
199
|
// Chinese. Returns {heading, body} families, or null when the deck language already selects one or the text has none.
|
|
164
200
|
const KANA = /[\p{Script=Hiragana}\p{Script=Katakana}]/u, HANGUL = /\p{Script=Hangul}/u, HAN = /\p{Script=Han}/u;
|
|
165
|
-
function contentEastAsianFonts(presentation, deck, report) {
|
|
201
|
+
function contentEastAsianFonts(presentation, deck, report, catalogs) {
|
|
166
202
|
if (deck.sources.eastAsian !== "latin") return null;
|
|
167
203
|
let kana = false, hangul = false, han = false;
|
|
168
204
|
const seen = new Set();
|
|
@@ -175,10 +211,10 @@ function contentEastAsianFonts(presentation, deck, report) {
|
|
|
175
211
|
}
|
|
176
212
|
};
|
|
177
213
|
walk(presentation.slides);
|
|
178
|
-
const language = kana ? "
|
|
214
|
+
const language = kana ? "ja" : hangul ? "ko" : han ? "zh-Hans" : null;
|
|
179
215
|
if (!language) return null;
|
|
180
216
|
try {
|
|
181
|
-
const resolved = resolver({...presentation, language}, {slideIndex: 0});
|
|
217
|
+
const resolved = resolver({...presentation, language}, {slideIndex: 0}, catalogs);
|
|
182
218
|
return resolved.sources.eastAsian === "latin" ? null : {heading: resolved.heading.eastAsian, body: resolved.body.eastAsian};
|
|
183
219
|
} catch (error) {
|
|
184
220
|
report?.({code: "language-export-unavailable", path: "language",
|
|
@@ -274,7 +310,9 @@ function runScriptFonts(xml, resolved, headingShape) {
|
|
|
274
310
|
});
|
|
275
311
|
}
|
|
276
312
|
|
|
277
|
-
|
|
313
|
+
// A run with its own language (TextRun.lang, FA-13) is written with an altLang attribute; the deck language never replaces it.
|
|
314
|
+
const RUN_LANGUAGE = /(<a:(?:rPr|endParaRPr|defRPr)\b[^>]*?\slang=")en-US("(?! altLang="))/g;
|
|
315
|
+
const OWN_LANGUAGE_RUN = /<a:rPr\b[^>]*?\slang="([^"]+)" altLang="en-US"[^>]*[^/]>[\s\S]*?<\/a:rPr>/g;
|
|
278
316
|
|
|
279
317
|
const decodeText = value => value.replace(/&(?:#(\d+)|#x([0-9a-f]+)|(amp|lt|gt|quot|apos));/gi, (entity, decimal, hex, name) =>
|
|
280
318
|
decimal ? String.fromCodePoint(Number(decimal)) : hex ? String.fromCodePoint(parseInt(hex, 16)) : {amp: "&", lt: "<", gt: ">", quot: "\"", apos: "'"}[name.toLowerCase()]);
|
|
@@ -392,41 +430,14 @@ export function partScriptFonts(path, xml, plan, slideIndex) {
|
|
|
392
430
|
const LANGUAGE_TAG = /^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/;
|
|
393
431
|
const attribute = (xml, name) => new RegExp(`\\s${name}="([^"]*)"`).exec(xml)?.[1];
|
|
394
432
|
|
|
395
|
-
/**
|
|
396
|
-
* Match an OOXML run language to the OPF language to import. A catalog id is
|
|
397
|
-
* used when that record exports exactly this tag again: an exact BCP-47 tag,
|
|
398
|
-
* then a curated `ooxmlLang` (preferring the same primary language). A tag
|
|
399
|
-
* that core still resolves to a catalog record (for example en-NZ to English)
|
|
400
|
-
* is imported as the tag itself, so it round-trips. Without core's resolver,
|
|
401
|
-
* a record whose tag is the primary language alone is accepted too.
|
|
402
|
-
* Returns {language, record, shared} or null when nothing matches.
|
|
403
|
-
*/
|
|
404
|
-
export function matchCatalogLanguage(lang, catalogs) {
|
|
405
|
-
const records = Array.isArray(catalogs?.languages) ? catalogs.languages : [];
|
|
406
|
-
const key = lang.toLowerCase(), primary = key.split("-")[0];
|
|
407
|
-
const lower = value => typeof value === "string" ? value.toLowerCase() : undefined;
|
|
408
|
-
const shared = records.filter(record => lower(record.bcp47) === key || lower(record.ooxmlLang) === key);
|
|
409
|
-
const exports = record => !resolver || resolver({language: record.id}).lang.toLowerCase() === key;
|
|
410
|
-
const record = shared.find(record => lower(record.bcp47) === key && exports(record))
|
|
411
|
-
?? shared.find(record => lower(record.bcp47)?.split("-")[0] === primary && exports(record))
|
|
412
|
-
?? shared.find(exports);
|
|
413
|
-
if (record) return {language: record.id, record, shared};
|
|
414
|
-
if (resolver) {
|
|
415
|
-
const resolved = resolver({language: lang});
|
|
416
|
-
// A tag core cannot resolve falls back to its default language; that is no match.
|
|
417
|
-
const matched = resolved.languageSource !== "default" && resolved.languageId && records.find(candidate => candidate.id === resolved.languageId);
|
|
418
|
-
return matched ? {language: lang, record: matched, shared: []} : null;
|
|
419
|
-
}
|
|
420
|
-
const byPrimary = records.find(candidate => lower(candidate.bcp47) === primary);
|
|
421
|
-
return byPrimary ? {language: byPrimary.id, record: byPrimary, shared: []} : null;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
433
|
/**
|
|
425
434
|
* Observe the presentation language in run `lang`. `slides` and `theme` are
|
|
426
|
-
* XML strings. `language` is
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
435
|
+
* XML strings. `language` is the dominant run tag, imported as a BCP-47 tag
|
|
436
|
+
* string (OPF 0.15: language is an engine vocabulary, not a catalog), or
|
|
437
|
+
* undefined when the runs carry no language; `lang` is the same tag.
|
|
438
|
+
* `catalogs` are the host's registered catalogs, for the font schemes core
|
|
439
|
+
* resolves. Nothing is reported here: languageDiagnostics() reports against
|
|
440
|
+
* the final imported document, after FF-32 provenance.
|
|
430
441
|
*/
|
|
431
442
|
export function observeLanguage({slides, theme, themePath, slideThemes, catalogs}) {
|
|
432
443
|
const counts = new Map();
|
|
@@ -441,8 +452,7 @@ export function observeLanguage({slides, theme, themePath, slideThemes, catalogs
|
|
|
441
452
|
}
|
|
442
453
|
const ranked = [...counts].sort((left, right) => right[1] - left[1] || (left[0] < right[0] ? -1 : left[0] > right[0] ? 1 : 0));
|
|
443
454
|
const lang = ranked[0]?.[0];
|
|
444
|
-
|
|
445
|
-
return {lang, language: lang === undefined ? undefined : match?.language ?? lang, match, ranked, rtlParagraphs, theme, themePath, slideThemes};
|
|
455
|
+
return {lang, language: lang, ranked, rtlParagraphs, theme, themePath, slideThemes, catalogs};
|
|
446
456
|
}
|
|
447
457
|
|
|
448
458
|
/**
|
|
@@ -454,32 +464,30 @@ export function observeLanguage({slides, theme, themePath, slideThemes, catalogs
|
|
|
454
464
|
*/
|
|
455
465
|
export function reconcileLanguage(groups, observed, report) {
|
|
456
466
|
const index = groups.findIndex(item => item.field === "language");
|
|
457
|
-
if (index < 0 || observed.lang === undefined
|
|
467
|
+
if (index < 0 || observed.lang === undefined) return groups;
|
|
458
468
|
const stored = groups[index].ops.find(op => op.path?.length === 1 && op.path[0] === "language")?.value;
|
|
459
469
|
if (stored === undefined) return groups;
|
|
460
|
-
const resolved = resolver({language: stored});
|
|
470
|
+
const resolved = resolver({language: stored}, {}, observed.catalogs);
|
|
461
471
|
if (resolved.languageSource === "default" || resolved.lang.toLowerCase() === observed.lang.toLowerCase()) return groups;
|
|
462
472
|
report?.({code: "metadata-reference-changed", path: "language",
|
|
463
473
|
message: `Runs now use ${observed.lang}, not ${resolved.lang} of the stored language, so the stored language was not restored; the imported language follows the runs.`});
|
|
464
474
|
return groups.filter((_, position) => position !== index);
|
|
465
475
|
}
|
|
466
476
|
|
|
467
|
-
/**
|
|
468
|
-
* Diagnostics for the final imported document: several run languages, a run
|
|
469
|
-
* tag that maps ambiguously or to no catalog record (only when that observed
|
|
470
|
-
* language was kept), right-to-left paragraphs under a left-to-right
|
|
471
|
-
* language, and theme East Asian/complex-script fonts the document does not
|
|
472
|
-
* reproduce.
|
|
473
|
-
*/
|
|
474
477
|
/** Whether the language observed in the runs is written right to left. */
|
|
475
478
|
export function observedRtl(observed) {
|
|
476
479
|
if (observed.lang === undefined) return false;
|
|
477
|
-
try { return resolver
|
|
480
|
+
try { return resolver({language: observed.language}, {}, observed.catalogs).rtl === true; } catch { return false; }
|
|
478
481
|
}
|
|
479
482
|
|
|
483
|
+
/**
|
|
484
|
+
* Diagnostics for the final imported document: several run languages,
|
|
485
|
+
* right-to-left paragraphs under a left-to-right language, and theme East
|
|
486
|
+
* Asian/complex-script fonts the document does not reproduce.
|
|
487
|
+
*/
|
|
480
488
|
export function languageDiagnostics(imported, observed, report) {
|
|
481
489
|
if (!report) return;
|
|
482
|
-
const {lang,
|
|
490
|
+
const {lang, ranked, rtlParagraphs, theme, catalogs} = observed;
|
|
483
491
|
if (lang === undefined) {
|
|
484
492
|
if (rtlParagraphs) report({code: "rtl-language-mismatch", path: "language", message: `${rtlParagraphs} right-to-left paragraph(s) carry no run language, so no presentation language was imported.`});
|
|
485
493
|
return;
|
|
@@ -488,22 +496,15 @@ export function languageDiagnostics(imported, observed, report) {
|
|
|
488
496
|
const languages = observedRtl(observed) ? ranked.filter(([tag]) => tag === lang || tag !== "en-US") : ranked;
|
|
489
497
|
if (languages.length > 1) {
|
|
490
498
|
report({code: "mixed-run-languages", path: "language",
|
|
491
|
-
message: `Runs use ${languages.length} languages (${languages.map(([tag, count]) => `${tag} x${count}`).join(", ")}). OPF has one presentation language, so ${lang} was imported.`});
|
|
492
|
-
}
|
|
493
|
-
const kept = imported.language === observed.language;
|
|
494
|
-
if (kept && !match) report({code: "language-uncatalogued", path: "language", message: `Run language ${lang} matches no languages catalog record; it was imported as a BCP-47 tag.`});
|
|
495
|
-
else if (kept && match.shared.length > 1 && !match.shared.some(record => record.bcp47?.toLowerCase() === lang.toLowerCase())) {
|
|
496
|
-
report({code: "language-ambiguous", path: "language",
|
|
497
|
-
message: `Run language ${lang} is the OOXML tag of ${match.shared.map(record => record.id).join(", ")}; ${match.language} was imported.`});
|
|
499
|
+
message: `Runs use ${languages.length} languages (${languages.map(([tag, count]) => `${tag} x${count}`).join(", ")}). OPF has one presentation language, so ${lang} was imported; a run in another language keeps it as its own lang where the text imports as rich text.`});
|
|
498
500
|
}
|
|
499
|
-
const resolved = resolver
|
|
500
|
-
|
|
501
|
-
if (rtlParagraphs && !rtl) {
|
|
501
|
+
const resolved = resolver(imported, {}, catalogs);
|
|
502
|
+
if (rtlParagraphs && !resolved.rtl) {
|
|
502
503
|
report({code: "rtl-language-mismatch", path: "language", message: `${rtlParagraphs} right-to-left paragraph(s) do not match the left-to-right language ${typeof imported.language === "string" ? imported.language : lang}; paragraph direction is not imported separately.`});
|
|
503
504
|
}
|
|
504
505
|
// The presentation theme carries the first slide's script fonts (planScriptFonts), so it is compared with that slide.
|
|
505
506
|
let themeResolved = resolved;
|
|
506
|
-
if (
|
|
507
|
+
if (imported.slides?.length) { try { themeResolved = resolver(imported, {slideIndex: 0}, catalogs); } catch { themeResolved = resolved; } }
|
|
507
508
|
if (theme && themeResolved) {
|
|
508
509
|
for (const {tag, slot, face} of themeScriptMismatches(theme, themeResolved)) {
|
|
509
510
|
report({code: "script-font-not-imported", path: "design.fontScheme",
|
|
@@ -511,11 +512,11 @@ export function languageDiagnostics(imported, observed, report) {
|
|
|
511
512
|
}
|
|
512
513
|
}
|
|
513
514
|
// opf-pptx#168: a slide on another slide master carries its own script fonts in that master's theme.
|
|
514
|
-
if (
|
|
515
|
+
if (Array.isArray(observed.slideThemes)) {
|
|
515
516
|
observed.slideThemes.forEach((slideTheme, index) => {
|
|
516
517
|
if (!slideTheme || slideTheme.path === observed.themePath) return;
|
|
517
518
|
let slideResolved;
|
|
518
|
-
try { slideResolved = resolver(imported, {slideIndex: index}); } catch { return; }
|
|
519
|
+
try { slideResolved = resolver(imported, {slideIndex: index}, catalogs); } catch { return; }
|
|
519
520
|
for (const {tag, slot, face} of themeScriptMismatches(slideTheme.xml, slideResolved)) {
|
|
520
521
|
report({code: "script-font-not-imported", path: `slides.${index}.design.fontScheme`,
|
|
521
522
|
message: `Slide ${index + 1}'s master theme ${tag} ${slot} font "${face}" differs from both its latin font and what the imported slide resolves, and is not represented in the imported OPF.`});
|
|
@@ -553,8 +554,8 @@ export function stripRunScriptFonts(xml) {
|
|
|
553
554
|
}
|
|
554
555
|
|
|
555
556
|
/**
|
|
556
|
-
*
|
|
557
|
-
* the paragraph end marks from reading an empty font (see themeScriptFonts).
|
|
557
|
+
* When the resolver throws for a deck (see planScriptFonts) the theme's East Asian slot still repeats the latin family of
|
|
558
|
+
* its font group, which keeps the paragraph end marks from reading an empty font (see themeScriptFonts).
|
|
558
559
|
*/
|
|
559
560
|
export function themeEastAsianFromLatin(xml) {
|
|
560
561
|
return xml.replace(/<a:(majorFont|minorFont)>[\s\S]*?<\/a:\1>/g, block => {
|
package/dist/theme-colors.js
CHANGED
|
@@ -36,7 +36,7 @@ const VENDOR_VALUES = new Set(['tx1', 'tx2', 'bg1', 'bg2', 'accent1', 'accent2',
|
|
|
36
36
|
const ROLE_SLOTS = Object.freeze({
|
|
37
37
|
primary: ['accent1'],
|
|
38
38
|
secondary: ['accent2'],
|
|
39
|
-
accent: ['
|
|
39
|
+
accent: ['accent3'],
|
|
40
40
|
text: ['dark1', 'light1'],
|
|
41
41
|
textSecondary: ['dark2', 'light2'],
|
|
42
42
|
surface: ['light2', 'dark2'],
|
|
@@ -213,9 +213,11 @@ function recordColors(record) {
|
|
|
213
213
|
|
|
214
214
|
/**
|
|
215
215
|
* Recover design.colorScheme from theme clrScheme colors.
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
216
|
+
* `records` are the colour schemes a bare id can name in the imported document
|
|
217
|
+
* (FA-23: the host default catalog's, each with its key as `id`; none when the
|
|
218
|
+
* host registers no catalog). An exact twelve-slot match yields that bare id
|
|
219
|
+
* (the clrScheme name breaks ties). Otherwise the colors are inline: relative
|
|
220
|
+
* to the record whose name the clrScheme carries, or as all readable slots.
|
|
219
221
|
*/
|
|
220
222
|
export function recoverColorScheme({colors, unreadable, name}, records) {
|
|
221
223
|
const complete = unreadable.length === 0;
|
|
@@ -245,19 +247,22 @@ export function presentationThemePath(presentationRoot, presentationRels, relati
|
|
|
245
247
|
}
|
|
246
248
|
|
|
247
249
|
/**
|
|
248
|
-
* Identify a catalog theme from the native theme name.
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
250
|
+
* Identify a catalog theme from the native theme name. `themes` are the theme
|
|
251
|
+
* records a bare id can name (as for recoverColorScheme, each with its key as
|
|
252
|
+
* `id`); `colorScheme(theme)` and `fontFamilies(theme)` resolve the theme's own
|
|
253
|
+
* references (null when they resolve nowhere). The name must equal a theme
|
|
254
|
+
* name, and the package must corroborate it with that theme's color scheme or
|
|
255
|
+
* its heading/body font pair, so a foreign deck that happens to use a theme
|
|
256
|
+
* called "Bold" is not bound to the OPF theme.
|
|
252
257
|
*/
|
|
253
|
-
export function recoverTheme({themeName, colors, majorFont, minorFont}, {themes,
|
|
258
|
+
export function recoverTheme({themeName, colors, majorFont, minorFont}, {themes, colorScheme, fontFamilies}) {
|
|
254
259
|
if (!themeName) return {};
|
|
255
260
|
const theme = themes.find(record => record.name === themeName);
|
|
256
261
|
if (!theme) return {};
|
|
257
|
-
const scheme =
|
|
262
|
+
const scheme = colorScheme(theme);
|
|
258
263
|
const schemeColors = scheme ? recordColors(scheme) : null;
|
|
259
264
|
const colorMatch = !!schemeColors && THEME_SLOTS.every(([slot]) => schemeColors[slot] === colors[slot]);
|
|
260
|
-
const fonts = fontFamilies(theme
|
|
265
|
+
const fonts = fontFamilies(theme);
|
|
261
266
|
const fontMatch = !!fonts && fonts.heading === majorFont && fonts.body === minorFont;
|
|
262
267
|
return colorMatch || fontMatch ? {id: theme.id} : {unverified: theme.id};
|
|
263
268
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {XMLParser} from 'fast-xml-parser';
|
|
2
|
-
import {
|
|
2
|
+
import {isValidFormat} from './format-check.js';
|
|
3
3
|
import {attachTextTags,decodeTextTag} from './code-provenance.js';
|
|
4
4
|
import {sourceLineParagraphs} from './text-provenance.js';
|
|
5
5
|
|
|
6
|
+
const STATUSES=['done','current','planned'];
|
|
6
7
|
const TAG='OPF_TIMELINE_V1',REL='http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags';
|
|
7
8
|
const parser=new XMLParser({ignoreAttributes:false,attributeNamePrefix:'',parseTagValue:false,trimValues:false});
|
|
8
9
|
const decoder=new TextDecoder('utf-8',{fatal:true}),array=value=>value===undefined?[]:Array.isArray(value)?value:[value];
|
|
@@ -49,16 +50,22 @@ export function importTimelineGroups(shapes,paragraphs,relationships,entries,rep
|
|
|
49
50
|
for(const group of groups.values())try{
|
|
50
51
|
const anchors=group.filter(item=>item.data.anchor!==undefined);if(anchors.length!==1)throw Error('Missing or duplicated timeline anchor.');
|
|
51
52
|
const anchor=anchors[0],manifest=anchor.data.anchor,count=validateManifest(manifest);
|
|
52
|
-
|
|
53
|
+
// A current event's ring is a second native ellipse with its own tag, so the group has one more shape per ring.
|
|
54
|
+
const total=count+manifest.eventCount+1+group.filter(item=>item.data.role==='ring').length;
|
|
53
55
|
if(anchor.data.role!=='text'||anchor.data.part!==0||anchor.data.line!==0||group.length!==total||new Set(group.map(item=>item.shape)).size!==total)throw Error('Incomplete or duplicated timeline elements.');
|
|
54
|
-
const ordered=manifest.parts.map(part=>Array(part.lines)),markers=new Set();let connectors=0;
|
|
56
|
+
const ordered=manifest.parts.map(part=>Array(part.lines)),markers=new Set(),rings=new Set(),statuses=new Map();let connectors=0;
|
|
55
57
|
for(const item of group){
|
|
56
58
|
if(item.ambiguous)throw Error('Multiple source identities on one timeline shape.');
|
|
57
|
-
if(item.data.role==='connector'||item.data.role==='marker'){
|
|
59
|
+
if(item.data.role==='connector'||item.data.role==='marker'||item.data.role==='ring'){
|
|
58
60
|
const expected=item.data.role==='connector'?'line':'ellipse';
|
|
59
61
|
if(item.shape['p:spPr']?.['a:prstGeom']?.prst!==expected||(paragraphs[item.index]??[]).some(paragraph=>paragraph.text||paragraph.bullet))throw Error('Timeline graphic now contains different geometry or native text.');
|
|
60
62
|
if(item.data.role==='connector'){if(++connectors>1)throw Error('Duplicated timeline connector.');}
|
|
61
|
-
else{
|
|
63
|
+
else{
|
|
64
|
+
const index=item.data.eventIndex,known=item.data.role==='marker'?markers:rings;
|
|
65
|
+
if(!Number.isSafeInteger(index)||index<0||index>=manifest.eventCount||known.has(index))throw Error('Ambiguous timeline marker.');
|
|
66
|
+
known.add(index);
|
|
67
|
+
if(item.data.role==='marker'&&item.data.status!==undefined){if(!STATUSES.includes(item.data.status))throw Error('Invalid timeline event status.');statuses.set(index,item.data.status);}
|
|
68
|
+
}
|
|
62
69
|
continue;
|
|
63
70
|
}
|
|
64
71
|
const {part,line}=item.data;
|
|
@@ -66,13 +73,17 @@ export function importTimelineGroups(shapes,paragraphs,relationships,entries,rep
|
|
|
66
73
|
ordered[part][line]=item;
|
|
67
74
|
}
|
|
68
75
|
if(connectors!==1||markers.size!==manifest.eventCount)throw Error('Incomplete timeline graphics.');
|
|
76
|
+
// Only a current event has a ring, and every current event has one.
|
|
77
|
+
for(const index of rings)if(statuses.get(index)!=='current')throw Error('Timeline ring without a current event.');
|
|
78
|
+
for(const [index,status] of statuses)if(status==='current'&&!rings.has(index))throw Error('Current timeline event without its ring.');
|
|
69
79
|
const result={events:Array.from({length:manifest.eventCount},()=>({}))};
|
|
70
80
|
for(const [index,part]of manifest.parts.entries()){
|
|
71
81
|
const text=sourceLineParagraphs(ordered[index],paragraphs)[0].text;
|
|
72
82
|
if(part.eventIndex===undefined)result[part.field]=text;else result.events[part.eventIndex][part.field]=text;
|
|
73
83
|
}
|
|
84
|
+
for(const [index,status] of statuses)result.events[index].status=status;
|
|
74
85
|
const timeline=manifest.shorthand?result.events:result;
|
|
75
|
-
if(!
|
|
86
|
+
if(!isValidFormat({slides:[{timeline}]}))throw Error('Current native text does not form a valid timeline.');
|
|
76
87
|
for(const item of group)consumed.add(item.shape);
|
|
77
88
|
items.push({shapes:group.map(item=>item.shape),payload:{type:'timeline',timeline}});
|
|
78
89
|
report({code:'timeline-import-reflow',message:'Timeline field order, source boundaries and current native text were recovered. Native formatting, positions, marker styling and font theme are not reconstructed; review the reflowed timeline.'});
|
|
@@ -7,7 +7,7 @@ import {unzipSync} from 'fflate';
|
|
|
7
7
|
// DrawingML `typeface` attributes (latin, ea, cs, sym, buFont, theme script
|
|
8
8
|
// supplements, embedded-font lists), SpreadsheetML font names (styles `name`,
|
|
9
9
|
// rich-text `rFont`) and the docProps/app.xml "Fonts Used" list.
|
|
10
|
-
//
|
|
10
|
+
// checkTypefaces() then applies the owner font policy: a package names only
|
|
11
11
|
// the fonts its author chose, plus theme references that resolve to them, empty
|
|
12
12
|
// theme script slots when no script font was selected (as in Office's themes; FF-49 checks a selected slot through options.themeScripts) and the documented theme script supplements.
|
|
13
13
|
|
|
@@ -33,7 +33,7 @@ export const THEME_SCRIPT_SUPPLEMENTS = Object.freeze({
|
|
|
33
33
|
minor: supplement(`Jpan=游ゴシック|Hans=等线|Arab=Arial|Hebr=Arial|Thai=Cordia New|Khmr=DaunPenh|Viet=Arial|${SHARED_SCRIPTS}`)
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
export function
|
|
36
|
+
export function inventoryTypefaces(input, options = {}) {
|
|
37
37
|
const typefaces = [];
|
|
38
38
|
const themes = {};
|
|
39
39
|
// Every theme part's fonts, and the theme each part of the presentation package resolves its references against.
|
|
@@ -90,15 +90,15 @@ export function packageFontsUsed(inventory) {
|
|
|
90
90
|
return [...used].sort(compareFontNames);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
export function
|
|
94
|
-
if (!Array.isArray(options.
|
|
95
|
-
throw new TypeError('
|
|
93
|
+
export function checkTypefaces(input, options = {}) {
|
|
94
|
+
if (!Array.isArray(options.families) || options.families.some(family => typeof family !== 'string' || !family)) {
|
|
95
|
+
throw new TypeError('checkTypefaces requires options.families, the non-empty family names the author chose.');
|
|
96
96
|
}
|
|
97
|
-
const chosen = new Set(options.
|
|
97
|
+
const chosen = new Set(options.families);
|
|
98
98
|
const monospace = new Set(options.monospace ?? []);
|
|
99
99
|
const allowEmpty = options.allowEmptyThemeScripts ?? true;
|
|
100
100
|
const supplements = options.scriptSupplements ?? THEME_SCRIPT_SUPPLEMENTS;
|
|
101
|
-
const inventory =
|
|
101
|
+
const inventory = inventoryTypefaces(input);
|
|
102
102
|
const violations = [];
|
|
103
103
|
const fail = (entry, reason, details = {}) => violations.push({reason, part: entry.part, element: entry.element, typeface: entry.typeface, ...details});
|
|
104
104
|
const emptyAllowed = entry => allowEmpty && entry.theme && (entry.element === 'ea' || entry.element === 'cs');
|
|
@@ -117,7 +117,7 @@ export function checkPptxTypefaces(input, options = {}) {
|
|
|
117
117
|
}
|
|
118
118
|
const reference = THEME_REFERENCE.exec(entry.typeface);
|
|
119
119
|
if (reference) {
|
|
120
|
-
if (entry.resolved === null) fail(entry, '
|
|
120
|
+
if (entry.resolved === null) fail(entry, 'theme-reference-unresolved');
|
|
121
121
|
else if (entry.resolved === '') { if (!allowEmpty || reference[2] === 'lt') fail(entry, 'empty-theme-reference'); }
|
|
122
122
|
else if (!chosen.has(entry.resolved)) fail(entry, 'foreign-theme-reference', {resolved: entry.resolved});
|
|
123
123
|
continue;
|
|
@@ -19,6 +19,8 @@ const canonical = value => JSON.stringify(value, (_key, item) => item && typeof
|
|
|
19
19
|
? Object.fromEntries(Object.keys(item).sort().filter(key => !(key === '#text' && typeof item[key] === 'string' && !item[key].trim())).map(key => [key, item[key]])) : item);
|
|
20
20
|
|
|
21
21
|
export const watermarkName = () => 'OPF watermark';
|
|
22
|
+
// FA-13: a text watermark is one native text box (a p:sp), not a picture.
|
|
23
|
+
export const watermarkTextName = () => 'OPF watermark text';
|
|
22
24
|
|
|
23
25
|
// The preview's frame: x/y at 30% and a 40% by 40% box, in the slide's units.
|
|
24
26
|
export const watermarkBox = (width, height) => ({x: width * .3, y: height * .3, w: width * .4, h: height * .4});
|
|
@@ -80,6 +82,7 @@ function attachTags(entries, manifests) {
|
|
|
80
82
|
const NS = 'http://schemas.openxmlformats.org/presentationml/2006/main', types = [];
|
|
81
83
|
let count = 0;
|
|
82
84
|
for (const [part, manifest] of manifests) {
|
|
85
|
+
const element = manifest.kind === 'text' ? 'sp' : 'pic', name = manifest.kind === 'text' ? watermarkTextName() : watermarkName();
|
|
83
86
|
const relPath = part.replace('/slides/', '/slides/_rels/') + '.rels';
|
|
84
87
|
const rels = decoder.decode(entries[relPath]);
|
|
85
88
|
const ids = new Set([...rels.matchAll(/\bId="([^"]+)"/g)].map(match => match[1]));
|
|
@@ -90,11 +93,11 @@ function attachTags(entries, manifests) {
|
|
|
90
93
|
types.push(`<Override PartName="/${tagPart}" ContentType="application/vnd.openxmlformats-officedocument.presentationml.tags+xml"/>`);
|
|
91
94
|
entries[relPath] = encoder.encode(rels.replace('</Relationships>', `<Relationship Id="${id}" Type="${REL}" Target="../tags/opfWatermark${count}.xml"/></Relationships>`));
|
|
92
95
|
let attached = false;
|
|
93
|
-
const xml = decoder.decode(entries[part]).replace(
|
|
94
|
-
if (attached || picture.match(/<p:cNvPr\b[^>]*\bname="([^"]+)"/)?.[1] !==
|
|
96
|
+
const xml = decoder.decode(entries[part]).replace(new RegExp(`<p:${element}>[\\s\\S]*?</p:${element}>`, 'g'), picture => {
|
|
97
|
+
if (attached || picture.match(/<p:cNvPr\b[^>]*\bname="([^"]+)"/)?.[1] !== name) return picture;
|
|
95
98
|
attached = true;
|
|
96
99
|
picture = picture.replace(/<p:nvPr\s*\/>/, '<p:nvPr></p:nvPr>');
|
|
97
|
-
if (!picture.includes('</p:nvPr>')) throw new Error('Generated watermark
|
|
100
|
+
if (!picture.includes('</p:nvPr>')) throw new Error('Generated watermark shape has no native application properties.');
|
|
98
101
|
return picture.replace('</p:nvPr>', `<p:custDataLst><p:tags r:id="${id}"/></p:custDataLst></p:nvPr>`);
|
|
99
102
|
});
|
|
100
103
|
entries[part] = encoder.encode(xml);
|
|
@@ -102,6 +105,28 @@ function attachTags(entries, manifests) {
|
|
|
102
105
|
entries['[Content_Types].xml'] = encoder.encode(decoder.decode(entries['[Content_Types].xml']).replace('</Types>', types.join('') + '</Types>'));
|
|
103
106
|
}
|
|
104
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Bind each generated text watermark box to its manifest with a native tag.
|
|
110
|
+
* marks: Map<slidePart, {slide, text, opacity}>. The manifest records the text and the exact native geometry
|
|
111
|
+
* (position, size and rotation), so an unchanged box imports back as design.watermark.
|
|
112
|
+
*/
|
|
113
|
+
export function tagTextWatermarks(entries, marks) {
|
|
114
|
+
if (!marks.size) return;
|
|
115
|
+
const manifests = new Map();
|
|
116
|
+
for (const part of Object.keys(entries).filter(path => /^ppt\/slides\/slide\d+\.xml$/.test(path))) {
|
|
117
|
+
const mark = marks.get(part);
|
|
118
|
+
if (!mark) continue;
|
|
119
|
+
let found = 0;
|
|
120
|
+
for (const shape of decoder.decode(entries[part]).match(/<p:sp>[\s\S]*?<\/p:sp>/g) ?? []) {
|
|
121
|
+
if (shape.match(/<p:cNvPr\b[^>]*\bname="([^"]+)"/)?.[1] !== watermarkTextName()) continue;
|
|
122
|
+
found++;
|
|
123
|
+
manifests.set(part, {v: 1, kind: 'text', slide: mark.slide, text: mark.text, opacity: mark.opacity, properties: parser.parse(shape)['p:sp']['p:spPr']});
|
|
124
|
+
}
|
|
125
|
+
if (found !== 1) throw new Error('Missing generated watermark text box.');
|
|
126
|
+
}
|
|
127
|
+
attachTags(entries, manifests);
|
|
128
|
+
}
|
|
129
|
+
|
|
105
130
|
function readTags(container, relationships, entries) {
|
|
106
131
|
const tags = [];
|
|
107
132
|
let unreadable = false;
|
|
@@ -113,6 +138,43 @@ function readTags(container, relationships, entries) {
|
|
|
113
138
|
return {tags, unreadable};
|
|
114
139
|
}
|
|
115
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Recover a text design.watermark from an unchanged tagged text box. paragraphs[index] are the native paragraphs of
|
|
143
|
+
* shapes[index]. Returns the consumed shape indexes and the slide design fields to restore. A box whose text, position,
|
|
144
|
+
* size or rotation changed stays an ordinary text box and is reported.
|
|
145
|
+
*/
|
|
146
|
+
export function importTextWatermark(shapes, paragraphs, relationships, entries, slideIndex, report) {
|
|
147
|
+
const consumed = new Set(), found = [];
|
|
148
|
+
for (const [index, shape] of shapes.entries()) {
|
|
149
|
+
const {tags, unreadable} = readTags(shape['p:nvSpPr']?.['p:nvPr']?.['p:custDataLst'], relationships, entries);
|
|
150
|
+
const own = tags.filter(tag => tag.name?.toUpperCase() === TAG);
|
|
151
|
+
if (!own.length) continue;
|
|
152
|
+
// The picture watermark has the same tag name; a picture's manifest has no kind and is read by importWatermark.
|
|
153
|
+
let kind;
|
|
154
|
+
try { kind = decodeTextTag(own[0].val)?.kind; } catch { kind = undefined; }
|
|
155
|
+
if (kind !== 'text') continue;
|
|
156
|
+
found.push({index, shape, own, unreadable, others: tags.some(tag => /^OPF_/i.test(tag.name) && tag.name.toUpperCase() !== TAG)});
|
|
157
|
+
}
|
|
158
|
+
if (!found.length) return {consumed};
|
|
159
|
+
const invalid = () => report({code: 'invalid-watermark-provenance', message: 'An edited, ambiguous or invalid tagged OPF watermark was imported as ordinary text. It is not reconstructed as design.watermark.'});
|
|
160
|
+
if (found.length > 1) { invalid(); return {consumed}; }
|
|
161
|
+
const [{index, shape, own, unreadable, others}] = found;
|
|
162
|
+
try {
|
|
163
|
+
if (unreadable || others || own.length !== 1) throw Error('Ambiguous watermark identity.');
|
|
164
|
+
const manifest = decodeTextTag(own[0].val);
|
|
165
|
+
if (manifest?.v !== 1 || manifest.kind !== 'text' || manifest.slide !== `slides.${slideIndex}` || typeof manifest.text !== 'string' || !manifest.text
|
|
166
|
+
|| typeof manifest.opacity !== 'number' || !(manifest.opacity >= 0 && manifest.opacity <= 1)) throw Error('Invalid watermark manifest.');
|
|
167
|
+
if (shape['p:nvSpPr']?.['p:cNvPr']?.name !== watermarkTextName()) throw Error('Watermark identity changed.');
|
|
168
|
+
if (canonical(shape['p:spPr']) !== canonical(manifest.properties)) throw Error('Watermark geometry changed.');
|
|
169
|
+
if ((paragraphs[index] ?? []).map(paragraph => paragraph.text).join('\n') !== manifest.text) throw Error('Watermark text changed.');
|
|
170
|
+
consumed.add(index);
|
|
171
|
+
return {consumed, design: {watermark: {text: manifest.text, opacity: manifest.opacity}}};
|
|
172
|
+
} catch {
|
|
173
|
+
invalid();
|
|
174
|
+
return {consumed};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
116
178
|
/**
|
|
117
179
|
* Recover design.watermark from an unchanged tagged picture. Returns the
|
|
118
180
|
* consumed picture indexes and the slide design fields to restore.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-pptx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Pure local OPF to PPTX export and PPTX to OPF import tooling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@node-projects/jszip": "4.3.0",
|
|
68
|
-
"@openpresentation/opf": "^0.
|
|
68
|
+
"@openpresentation/opf": "^0.15.1",
|
|
69
69
|
"@rgrove/parse-xml": "4.2.3",
|
|
70
70
|
"fast-xml-parser": "^5.8.0",
|
|
71
71
|
"fflate": "^0.8.3",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"sharp": "0.35.5"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
|
-
"@openpresentation/opf-render": "^0.
|
|
76
|
+
"@openpresentation/opf-render": "^0.15.0"
|
|
77
77
|
},
|
|
78
78
|
"peerDependenciesMeta": {
|
|
79
79
|
"@openpresentation/opf-render": {
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
84
|
"@expo-google-fonts/carlito": "0.4.1",
|
|
85
|
-
"@openpresentation/opf-render": "0.
|
|
85
|
+
"@openpresentation/opf-render": "0.15.0",
|
|
86
86
|
"esbuild": "0.28.2",
|
|
87
87
|
"playwright": "1.63.0"
|
|
88
88
|
},
|