@openpresentation/opf-pptx 0.16.1 → 0.18.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 +13 -11
- package/dist/code-provenance.js +1 -1
- package/dist/document-provenance.js +37 -40
- package/dist/furniture-fields.js +41 -9
- package/dist/furniture-provenance.js +242 -135
- package/dist/image-fallback-node.js +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +127 -44
- package/dist/logo-provenance.js +8 -8
- package/dist/master-furniture.js +326 -0
- package/dist/media-dedupe.js +2 -0
- package/dist/native-furniture.js +46 -25
- package/package.json +5 -5
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {XMLParser} from 'fast-xml-parser';
|
|
2
2
|
import {attachTextTags, decodeTextTag, encodeTextTag} from './code-provenance.js';
|
|
3
3
|
import {sourceLineParagraphs} from './text-provenance.js';
|
|
4
|
-
import {DEFAULT_DATE_FORMAT, NATIVE_DATE_FIELDS,
|
|
4
|
+
import {DEFAULT_DATE_FORMAT, NATIVE_DATE_FIELDS, isBuiltinTemplate, mentionsSlideToken, parseDate, slideNumberTemplate} from './furniture-fields.js';
|
|
5
5
|
import {schemas} from '@openpresentation/opf';
|
|
6
|
-
import {
|
|
6
|
+
import {resolveLogo} from '@openpresentation/opf/composition';
|
|
7
|
+
import {mediaKey} from './media-dedupe.js';
|
|
7
8
|
import {NATIVE_PLACEHOLDERS, footerValue, isNativePlaceholderType, readNativePlaceholders} from './native-furniture.js';
|
|
8
9
|
|
|
9
10
|
const TAG = 'OPF_FURNITURE_V1';
|
|
@@ -15,8 +16,8 @@ const enc = new TextEncoder(), dec = new TextDecoder('utf-8', {fatal: true});
|
|
|
15
16
|
const parser = new XMLParser({ignoreAttributes: false, attributeNamePrefix: '', parseTagValue: false, trimValues: false});
|
|
16
17
|
const array = value => value === undefined ? [] : Array.isArray(value) ? value : [value];
|
|
17
18
|
const kinds = ['header', 'footer'], zones = ['left', 'center', 'right'];
|
|
18
|
-
const fields = ['text', 'image', '
|
|
19
|
-
const settings = ['
|
|
19
|
+
const fields = ['text', 'image', 'socials', 'date'];
|
|
20
|
+
const settings = ['dateFormat'];
|
|
20
21
|
const dateFormatForField = Object.fromEntries(Object.entries(NATIVE_DATE_FIELDS).map(([format, type]) => [type, format]));
|
|
21
22
|
// Platform keys follow the Socials schema's propertyNames pattern exactly, so
|
|
22
23
|
// every key a valid document can carry re-imports (and nothing else does).
|
|
@@ -26,6 +27,8 @@ const platformId = new RegExp(schemas.presentation.$defs.Socials.propertyNames.p
|
|
|
26
27
|
const socialLines = part => part.links.map(link => ({platform: link.platform,
|
|
27
28
|
scheme: link.href && !/^[a-z][a-z0-9+.-]*:/i.test(link.text) ? 'https://' : ''}));
|
|
28
29
|
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
30
|
+
const LOGO_REFERENCE = /^var:organization(?:\.[A-Za-z0-9_-]{1,64}){1,3}$/;
|
|
31
|
+
const isLogoReference = value => typeof value === 'string' && value.length <= 200 && LOGO_REFERENCE.test(value);
|
|
29
32
|
const key = part => `${part.kind}.${part.zone}.${part.field}`;
|
|
30
33
|
// Key order is not meaning: a value merged from native placeholders equals the same value read from a manifest.
|
|
31
34
|
const canonical = value => JSON.stringify(value, (key, item) => object(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : item);
|
|
@@ -72,12 +75,13 @@ function unchangedStaticDate(marker, format, ordered, paragraphs) {
|
|
|
72
75
|
}), 'Current static date text or boundaries differ from the exported lines.');
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
// This manifest contains topology, identities, inactive flags
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
// This manifest contains topology, identities, inactive flags, format settings, the authored text of a zone whose `text` uses
|
|
79
|
+
// built-in variables (`templates`, FA-31; `authored` is the document as given to toPptx, before the deck-wide pass) and the
|
|
80
|
+
// organization logo reference of a zone `image` (`images`, RR-71: `{reference, media?, drawn?}` per `kind.zone`). Current native
|
|
81
|
+
// shapes supply all words, image bytes and alt text, even after edits; a recorded format, template or logo reference is kept
|
|
82
|
+
// only while the current text or picture still matches it exactly.
|
|
83
|
+
export function furnitureManifest(presentation, slide, layout, slideIndex, staticDates = new Map(), natives = new Map(), authored) {
|
|
84
|
+
const definitions = {}, formats = {}, templates = {}, images = {};
|
|
81
85
|
for (const kind of kinds) {
|
|
82
86
|
const local = slide.design?.[kind] !== undefined;
|
|
83
87
|
const source = local ? slide.design[kind] : presentation.design?.[kind];
|
|
@@ -85,50 +89,44 @@ export function furnitureManifest(presentation, slide, layout, slideIndex, stati
|
|
|
85
89
|
const value = source === false ? false : {};
|
|
86
90
|
if (value !== false) for (const zone of zones) if (source[zone] !== undefined) {
|
|
87
91
|
value[zone] = {};
|
|
92
|
+
// An organization logo reference (`var:organization.logo.icon`): core lays out an image part with `reference`; the manifest keeps
|
|
93
|
+
// the authored string. With no part (the organization has no logo) the reference is all there is, and import restores it.
|
|
94
|
+
const reference = isLogoReference(source[zone].image) ? source[zone].image : undefined;
|
|
95
|
+
const imagePart = layout.parts.find(part => part.kind === kind && part.zone === zone && part.type === 'image');
|
|
96
|
+
if (reference !== undefined) images[`${kind}.${zone}`] = imagePart ? {reference} : {reference, drawn: false};
|
|
88
97
|
for (const field of fields) if (source[zone][field] !== undefined) {
|
|
89
98
|
const current = source[zone][field];
|
|
99
|
+
if (field === 'image' && reference !== undefined && !imagePart) continue;
|
|
90
100
|
value[zone][field] = field === 'image' ? 'native' : typeof current === 'string' ? 'literal' : current;
|
|
91
101
|
}
|
|
92
102
|
const format = Object.fromEntries(settings.filter(setting => typeof source[zone][setting] === 'string').map(setting => [setting, source[zone][setting]]));
|
|
93
103
|
if (Object.keys(format).length) formats[`${kind}.${zone}`] = format;
|
|
104
|
+
// The authored text with its built-in tokens, so import restores them while the words still match this slide:
|
|
105
|
+
// {{slide.number}} is a native field; {{deck.slideCount}} and {{slide.section}} are fixed words; the deck-wide built-ins
|
|
106
|
+
// ({{organization.name}}) were resolved before export. A text that also uses a user variable is not stored (the filled deck
|
|
107
|
+
// round-trips); then only a slide-scoped token in the resolved text is.
|
|
108
|
+
const original = (local ? authored?.slide?.design?.[kind] : authored?.presentation?.design?.[kind])?.[zone]?.text;
|
|
109
|
+
if (isBuiltinTemplate(original)) templates[`${kind}.${zone}`] = original;
|
|
110
|
+
else if (mentionsSlideToken(source[zone].text)) templates[`${kind}.${zone}`] = source[zone].text;
|
|
94
111
|
}
|
|
95
112
|
definitions[kind] = {scope: local ? 'local' : 'global', value};
|
|
96
113
|
}
|
|
97
114
|
if (!Object.keys(definitions).length) return null;
|
|
98
115
|
const organizations = array(presentation.organization);
|
|
99
116
|
const organization = organizations.find(item => item.role === 'primary') ?? organizations[0];
|
|
100
|
-
const speaker = array(presentation.speaker)[0];
|
|
101
|
-
// Generated deck logos (logo: true) are listed beside the topology, never inside it: the part list and the field
|
|
102
|
-
// definitions are validated strictly by every released importer (0.11.6 and earlier reject an unknown field there),
|
|
103
|
-
// so they keep describing only what those importers know. `drawn` is false when no logo resolved at export.
|
|
104
|
-
const logos = [];
|
|
105
|
-
for (const kind of kinds) {
|
|
106
|
-
const source = (slide.design?.[kind] !== undefined ? slide.design : presentation.design)?.[kind];
|
|
107
|
-
for (const zone of zones) if (definitions[kind]?.value?.[zone] && source?.[zone]?.logo === true) {
|
|
108
|
-
logos.push({kind, zone, drawn: layout.parts.some(part => part.field === 'logo' && part.kind === kind && part.zone === zone)});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
117
|
return {v: 1, role: 'slide', group: String(slideIndex), definitions, ...(Object.keys(formats).length ? {formats} : {}),
|
|
112
|
-
...(
|
|
113
|
-
...(layout.parts.some(part => part.field === '
|
|
114
|
-
parts: layout.parts.
|
|
118
|
+
...(Object.keys(templates).length ? {templates} : {}), ...(Object.keys(images).length ? {images} : {}),
|
|
119
|
+
...(layout.parts.some(part => part.field === 'socials') ? {organizationId: organization?.id} : {}),
|
|
120
|
+
parts: layout.parts.map((part, index) => ({kind: part.kind, zone: part.zone, field: part.field,
|
|
115
121
|
type: part.type, count: part.type === 'image' ? 1 : part.fit.lines.length,
|
|
116
122
|
...(part.field === 'socials' ? {socials: socialLines(part)} : {}),
|
|
117
|
-
// The speaker part is one line, "Name, Title" or "Name": the name's length lets import split it again (lengths, never words).
|
|
118
|
-
...(part.field === 'speaker' ? {nameLength: speaker.name.length} : {}),
|
|
119
123
|
...(staticDates.has(index) ? {staticDate: staticDates.get(index)} : {}),
|
|
120
124
|
// RR-11: this part is a native PowerPoint placeholder (dt, ftr or sldNum); deleting it in PowerPoint's dialog is then intent.
|
|
121
|
-
...(natives.has(index) ? {ph: natives.get(index)} : {})}
|
|
122
|
-
...(logos.length ? {logos} : {})};
|
|
125
|
+
...(natives.has(index) ? {ph: natives.get(index)} : {})}))};
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
|
|
126
|
-
export const manifestPartIndex = (parts, index) => parts.slice(0, index).filter(part => part.field !== 'logo').length;
|
|
127
|
-
|
|
128
|
-
export function attachFurnitureTags(entries, records, manifests, logoRecords = new Map()) {
|
|
128
|
+
export function attachFurnitureTags(entries, records, manifests) {
|
|
129
129
|
attachTextTags(entries, records, TAG, 'opfFurniture', 'furniture', {pictures: true});
|
|
130
|
-
// A generated logo picture carries the logo tag (never the furniture tag): see logo-provenance.js.
|
|
131
|
-
attachTextTags(entries, logoRecords, LOGO_TAG, 'opfFurnitureLogo', 'furniture logo', {pictures: true});
|
|
132
130
|
const types = [];
|
|
133
131
|
for (const [path, manifest] of manifests) {
|
|
134
132
|
const part = `ppt/tags/opfFurnitureSlide${manifest.group}.xml`;
|
|
@@ -150,6 +148,49 @@ export function attachFurnitureTags(entries, records, manifests, logoRecords = n
|
|
|
150
148
|
if (types.length) entries['[Content_Types].xml'] = enc.encode(dec.decode(entries['[Content_Types].xml']).replace('</Types>', types.join('') + '</Types>'));
|
|
151
149
|
}
|
|
152
150
|
|
|
151
|
+
/**
|
|
152
|
+
* An organization `logo` value reduced to what logo resolution needs (RR-71): the shapes and tones that exist, each as a
|
|
153
|
+
* placeholder string (a stored picture, an asset reference and a path all count), or undefined when it has none.
|
|
154
|
+
*/
|
|
155
|
+
export function logoShape(value) {
|
|
156
|
+
if (typeof value === 'string') return value.trim() ? 'x' : undefined;
|
|
157
|
+
if (!object(value)) return undefined;
|
|
158
|
+
if (typeof value.src === 'string') return value.src.trim() ? 'x' : undefined;
|
|
159
|
+
if (typeof value.$opfMedia === 'string') return 'x';
|
|
160
|
+
const kept = Object.fromEntries(Object.entries(value).map(([name, item]) => [name, logoShape(item)]).filter(([, item]) => item !== undefined));
|
|
161
|
+
return Object.keys(kept).length ? kept : undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// The content key of the embedded bytes of a picture: its main blip's media part.
|
|
165
|
+
function pictureKey(picture, relationships, entries) {
|
|
166
|
+
const target = relationships.get(picture?.['p:blipFill']?.['a:blip']?.['r:embed'])?.path;
|
|
167
|
+
return target && entries[target] ? mediaKey(entries[target]) : undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Finish the `images` of each slide manifest once the package is final (SVG parts, format conversion and media de-duplication
|
|
172
|
+
* change what a picture embeds): the media key of every logo-reference picture, or `drawn: false` when the picture could not be
|
|
173
|
+
* embedded. Rewrites the slide's furniture tag part. `names` maps `<slide part>|<kind>.<zone>` to the picture's object name.
|
|
174
|
+
*/
|
|
175
|
+
export function stampFurnitureImageKeys(entries, manifests, names, relationshipsOf) {
|
|
176
|
+
for (const [path, manifest] of manifests) {
|
|
177
|
+
if (!manifest.images) continue;
|
|
178
|
+
const xml = dec.decode(entries[path]), relationships = relationshipsOf(entries, path);
|
|
179
|
+
const pictures = [...xml.matchAll(/<p:pic>[\s\S]*?<\/p:pic>/g)].map(match => match[0]);
|
|
180
|
+
for (const [slot, entry] of Object.entries(manifest.images)) {
|
|
181
|
+
if (entry.drawn === false) continue;
|
|
182
|
+
const name = names.get(`${path}|${slot}`);
|
|
183
|
+
const embed = name === undefined ? undefined : pictures.find(picture => picture.match(/<p:cNvPr\b[^>]*\bname="([^"]+)"/)?.[1] === name)?.match(/<a:blip\b[^>]*r:embed="([^"]+)"/)?.[1];
|
|
184
|
+
const target = embed === undefined ? undefined : relationships.get(embed)?.path;
|
|
185
|
+
if (target && entries[target]) entry.media = mediaKey(entries[target]);
|
|
186
|
+
else entry.drawn = false;
|
|
187
|
+
}
|
|
188
|
+
const part = `ppt/tags/opfFurnitureSlide${manifest.group}.xml`;
|
|
189
|
+
if (!entries[part]) throw Error('Missing furniture tag part.');
|
|
190
|
+
entries[part] = enc.encode(dec.decode(entries[part]).replace(/\bval="[^"]*"/, () => `val="${encodeTextTag(manifest)}"`));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
153
194
|
function readTags(container, relationships, entries) {
|
|
154
195
|
const tags = [];
|
|
155
196
|
let unreadable = false;
|
|
@@ -164,6 +205,29 @@ function readTags(container, relationships, entries) {
|
|
|
164
205
|
ambiguous: unreadable || tags.filter(tag => /^OPF_/i.test(tag.name) && tag.name.toUpperCase() !== SLIDE_TAG).length !== 1};
|
|
165
206
|
}
|
|
166
207
|
|
|
208
|
+
// RR-72: a part written once on the slide master or a layout is stored under `shared` (keyed by slot, with `on`, the definition's
|
|
209
|
+
// flag and its text template or logo reference), so importers that read only slide shapes ignore it. Put it back in place, as a
|
|
210
|
+
// part marked `on`, before the manifest is validated as one record.
|
|
211
|
+
function withSharedParts(manifest) {
|
|
212
|
+
if (manifest?.shared === undefined) return manifest;
|
|
213
|
+
check(object(manifest.shared) && object(manifest.definitions) && Array.isArray(manifest.parts), 'Invalid shared furniture.');
|
|
214
|
+
const result = {...manifest, definitions: structuredClone(manifest.definitions), parts: [...manifest.parts],
|
|
215
|
+
...(manifest.templates ? {templates: {...manifest.templates}} : {}), ...(manifest.images ? {images: {...manifest.images}} : {})};
|
|
216
|
+
delete result.shared;
|
|
217
|
+
for (const [slot, entry] of Object.entries(manifest.shared)) {
|
|
218
|
+
check(object(entry) && ['master', 'layout'].includes(entry.on) && slot === key(entry) && entry.ph === undefined, 'Invalid shared furniture part.');
|
|
219
|
+
const {on, flag, template, image, ...part} = entry;
|
|
220
|
+
const zone = result.definitions[entry.kind]?.value?.[entry.zone];
|
|
221
|
+
check(object(zone) && zone[entry.field] === undefined && flag !== undefined, 'Invalid shared furniture part.');
|
|
222
|
+
zone[entry.field] = flag;
|
|
223
|
+
const place = `${entry.kind}.${entry.zone}`;
|
|
224
|
+
if (template !== undefined) { check(entry.field === 'text' && result.templates?.[place] === undefined, 'Invalid shared furniture text.'); (result.templates ??= {})[place] = template; }
|
|
225
|
+
if (image !== undefined) { check(entry.field === 'image' && result.images?.[place] === undefined, 'Invalid shared furniture image.'); (result.images ??= {})[place] = image; }
|
|
226
|
+
result.parts.push({...part, on});
|
|
227
|
+
}
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
|
|
167
231
|
function validateManifest(manifest) {
|
|
168
232
|
check(manifest?.v === 1 && manifest.role === 'slide' && /^\d{1,10}$/.test(manifest.group), 'Invalid slide identity.');
|
|
169
233
|
check(object(manifest.definitions) && Object.keys(manifest.definitions).length > 0 && Object.keys(manifest.definitions).every(kind => kinds.includes(kind)), 'Invalid furniture definitions.');
|
|
@@ -184,14 +248,24 @@ function validateManifest(manifest) {
|
|
|
184
248
|
}
|
|
185
249
|
}
|
|
186
250
|
}
|
|
187
|
-
if (manifest.
|
|
188
|
-
check(
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
check(
|
|
193
|
-
|
|
194
|
-
|
|
251
|
+
if (manifest.images !== undefined) {
|
|
252
|
+
check(object(manifest.images), 'Invalid furniture images.');
|
|
253
|
+
for (const [slot, entry] of Object.entries(manifest.images)) {
|
|
254
|
+
const [kind, zone, extra] = slot.split('.');
|
|
255
|
+
const flag = manifest.definitions[kind]?.value?.[zone]?.image;
|
|
256
|
+
check(extra === undefined && object(manifest.definitions[kind]?.value?.[zone]) && object(entry) && Object.keys(entry).every(name => ['reference', 'media', 'drawn'].includes(name)) &&
|
|
257
|
+
isLogoReference(entry.reference) && (entry.media === undefined || (typeof entry.media === 'string' && /^\d{1,12}:[0-9a-f]{1,32}$/.test(entry.media))) &&
|
|
258
|
+
(entry.drawn === undefined || (entry.drawn === false && entry.media === undefined)) &&
|
|
259
|
+
// A reference with no part at all has no image flag; one with a part has the part's flag.
|
|
260
|
+
(flag === 'native' || (flag === undefined && entry.drawn === false)), 'Invalid furniture image.');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (manifest.templates !== undefined) {
|
|
264
|
+
check(object(manifest.templates), 'Invalid furniture texts.');
|
|
265
|
+
for (const [slot, template] of Object.entries(manifest.templates)) {
|
|
266
|
+
const [kind, zone, extra] = slot.split('.');
|
|
267
|
+
check(extra === undefined && manifest.definitions[kind]?.value?.[zone]?.text === 'literal' && typeof template === 'string' &&
|
|
268
|
+
template.length <= 100000 && template.includes('{{'), 'Invalid furniture text.');
|
|
195
269
|
}
|
|
196
270
|
}
|
|
197
271
|
if (manifest.formats !== undefined) {
|
|
@@ -207,34 +281,80 @@ function validateManifest(manifest) {
|
|
|
207
281
|
check(object(part) && expected.get(key(part)) === part.type, 'Ambiguous furniture part.');
|
|
208
282
|
check(Number.isSafeInteger(part.count) && part.count >= 1 && part.count <= 10000 && (part.type !== 'image' || part.count === 1), 'Invalid furniture line count.');
|
|
209
283
|
// RR-11: a footer part that is a native PowerPoint placeholder (dt, ftr, sldNum), unique per slide.
|
|
284
|
+
// RR-72: a part drawn once on the slide master or a layout.
|
|
285
|
+
check(part.on === undefined || (['master', 'layout'].includes(part.on) && part.ph === undefined), 'Invalid shared furniture part.');
|
|
210
286
|
if (part.ph !== undefined) check(isNativePlaceholderType(part.ph) && part.kind === 'footer' && part.type === 'text' && part.count === 1 &&
|
|
211
287
|
NATIVE_PLACEHOLDERS[part.ph].field === part.field && manifest.parts.filter(other => other.ph === part.ph).length === 1, 'Invalid native placeholder part.');
|
|
212
288
|
if (part.field === 'socials') check(Array.isArray(part.socials) && part.socials.length >= 1 && part.socials.length <= 100
|
|
213
289
|
&& part.socials.every(line => object(line) && platformId.test(line.platform) && schemes.includes(line.scheme))
|
|
214
290
|
&& new Set(part.socials.map(line => line.platform)).size === part.socials.length, 'Invalid social profile lines.');
|
|
215
291
|
else check(part.socials === undefined, 'Unexpected social profile lines.');
|
|
216
|
-
if (part.field === 'speaker') check(Number.isSafeInteger(part.nameLength) && part.nameLength >= 1 && part.nameLength <= 100000, 'Invalid speaker name length.');
|
|
217
|
-
else check(part.nameLength === undefined, 'Unexpected speaker name length.');
|
|
218
292
|
expected.delete(key(part));
|
|
219
293
|
}
|
|
220
|
-
if (manifest.parts.some(part => part.field === '
|
|
221
|
-
else check(manifest.speakerId === undefined, 'Unexpected speaker identity.');
|
|
222
|
-
if (manifest.parts.some(part => part.field === 'organization' || part.field === 'socials')) check(typeof manifest.organizationId === 'string' && /^[a-zA-Z0-9_-]+$/.test(manifest.organizationId), 'Invalid organization identity.');
|
|
294
|
+
if (manifest.parts.some(part => part.field === 'socials')) check(typeof manifest.organizationId === 'string' && /^[a-zA-Z0-9_-]+$/.test(manifest.organizationId), 'Invalid organization identity.');
|
|
223
295
|
}
|
|
224
296
|
|
|
225
|
-
// The
|
|
226
|
-
function
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
297
|
+
// The native fields of the lines of one tagged text part, with offsets into the text sourceLineParagraphs joins from them.
|
|
298
|
+
function orderedFields(ordered, paragraphs) {
|
|
299
|
+
const fields = [];
|
|
300
|
+
let base = 0;
|
|
301
|
+
for (const item of ordered) {
|
|
302
|
+
const current = paragraphs[item.index] ?? [];
|
|
303
|
+
let at = base;
|
|
304
|
+
for (const paragraph of current) {
|
|
305
|
+
for (const field of paragraph.fields ?? []) fields.push({...field, start: at + field.start});
|
|
306
|
+
at += paragraph.text.length + 1;
|
|
307
|
+
}
|
|
308
|
+
base += current.map(paragraph => paragraph.text).join('\n').length + item.data.separator.length;
|
|
309
|
+
}
|
|
310
|
+
return fields;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// A zone `text`: the stored template while it still draws exactly the words now on the slide (`position.draw` resolves its
|
|
314
|
+
// deck-wide built-ins from the document's own metadata and its slide-scoped tokens for this slide); otherwise the words as they
|
|
315
|
+
// stand, with each native slide-number field as `{{slide.number}}`.
|
|
316
|
+
function importedZoneText(text, nativeFields, template, position) {
|
|
317
|
+
if (template !== undefined) {
|
|
318
|
+
const drawn = position.draw(template, position);
|
|
319
|
+
if (typeof drawn !== 'string') return slideNumberTemplate(text, nativeFields);
|
|
320
|
+
// A section name is an XML attribute in the native list: tab, LF and CR come back as spaces (src/sections.js).
|
|
321
|
+
const spaced = value => value.replace(/[\t\n\r]/g, ' ');
|
|
322
|
+
if (drawn === text || (position.section !== undefined && spaced(drawn) === spaced(text))) return template;
|
|
323
|
+
}
|
|
324
|
+
return slideNumberTemplate(text, nativeFields);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// RR-72: the tagged furniture shapes of the slide's layout and master, by `on` and slot, and whether the slide shows them: a slide
|
|
328
|
+
// whose "Hide background graphics" is on (showMasterSp="0") shows neither; a layout with it hides the master's.
|
|
329
|
+
function inheritedRecords(context, entries) {
|
|
330
|
+
const inherited = context.inherited?.() ?? {};
|
|
331
|
+
const records = new Map(), damaged = {};
|
|
332
|
+
for (const on of ['layout', 'master']) {
|
|
333
|
+
const source = inherited[on];
|
|
334
|
+
if (!source) continue;
|
|
335
|
+
for (const [type, nodes, properties] of [['text', source.shapes, 'p:nvSpPr'], ['image', source.pictures, 'p:nvPicPr']]) {
|
|
336
|
+
for (const [index, node] of nodes.entries()) {
|
|
337
|
+
const container = node[properties]?.['p:nvPr']?.['p:custDataLst'];
|
|
338
|
+
const result = readTags(container, source.relationships, entries);
|
|
339
|
+
// A tag that cannot be read (a missing part, two identities) leaves the part's shape unknown: that is damage, not a removal.
|
|
340
|
+
if (array(container?.['p:tags']).length && result.tags.length !== 1 && result.ambiguous) damaged[on] = true;
|
|
341
|
+
for (const tag of result.tags) {
|
|
342
|
+
let data;
|
|
343
|
+
try { data = decodeTextTag(tag.val); } catch { damaged[on] = true; continue; }
|
|
344
|
+
if (result.ambiguous || data?.v !== 1 || typeof data.slot !== 'string' || data.role !== type) { damaged[on] = true; continue; }
|
|
345
|
+
const slot = `${on}|${data.slot}`;
|
|
346
|
+
records.set(slot, [...(records.get(slot) ?? []), {type, index, data}]);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
232
350
|
}
|
|
233
|
-
|
|
351
|
+
const hiddenSlide = context.root?.showMasterSp === '0' || context.root?.showMasterSp === 'false';
|
|
352
|
+
const hiddenLayout = inherited.layout?.root?.showMasterSp === '0' || inherited.layout?.root?.showMasterSp === 'false';
|
|
353
|
+
return {sources: inherited, records, damaged, hidden: {layout: hiddenSlide, master: hiddenSlide || hiddenLayout}};
|
|
234
354
|
}
|
|
235
355
|
|
|
236
|
-
function readSlide(context, entries, slideIndex, slideCount, report, taggedText) {
|
|
237
|
-
const {root, shapes,
|
|
356
|
+
function readSlide(context, entries, slideIndex, slideCount, report, taggedText, section, draw, logoOwner) {
|
|
357
|
+
const {root, shapes, pictures, relationships} = context;
|
|
238
358
|
const candidates = {};
|
|
239
359
|
const records = [];
|
|
240
360
|
let invalidRecord = false;
|
|
@@ -248,26 +368,19 @@ function readSlide(context, entries, slideIndex, slideCount, report, taggedText)
|
|
|
248
368
|
} catch { invalidRecord = true; }
|
|
249
369
|
}
|
|
250
370
|
}
|
|
251
|
-
// Generated logo pictures: identity only, the bytes come from design.logo.
|
|
252
|
-
const logoPictures = [];
|
|
253
|
-
for (const [index, node] of pictures.entries()) try {
|
|
254
|
-
for (const tag of readLogoTags(node['p:nvPicPr']?.['p:nvPr']?.['p:custDataLst'], relationships, entries)) {
|
|
255
|
-
const data = decodeTextTag(tag.val);
|
|
256
|
-
if (data?.role === 'furniture') logoPictures.push({index, data});
|
|
257
|
-
}
|
|
258
|
-
} catch { invalidRecord = true; }
|
|
259
371
|
const tags = readTags(root['p:cSld']?.['p:custDataLst'], relationships, entries);
|
|
260
|
-
if (!tags.tags.length && !records.length && !invalidRecord
|
|
372
|
+
if (!tags.tags.length && !records.length && !invalidRecord) return candidates;
|
|
261
373
|
let manifest;
|
|
262
374
|
try {
|
|
263
375
|
check(!tags.ambiguous && tags.tags.length === 1 && !invalidRecord, 'Missing or ambiguous furniture manifest/shape tags.');
|
|
264
|
-
manifest = decodeTextTag(tags.tags[0].val);
|
|
376
|
+
manifest = withSharedParts(decodeTextTag(tags.tags[0].val));
|
|
265
377
|
validateManifest(manifest);
|
|
266
378
|
for (const record of records) {
|
|
267
379
|
const data = record.data, part = manifest.parts[data?.part];
|
|
268
380
|
check(data?.v === 1 && data.group === manifest.group && Number.isSafeInteger(data.part) && part && data.role === record.type && part.type === record.type, 'Unmatched furniture identity.');
|
|
269
381
|
}
|
|
270
382
|
} catch (error) { report(slideIndex, error.message); return candidates; }
|
|
383
|
+
const inherited = manifest.parts.some(part => part.on) ? inheritedRecords(context, entries) : undefined;
|
|
271
384
|
for (const kind of kinds) {
|
|
272
385
|
const definition = manifest.definitions[kind];
|
|
273
386
|
if (!definition) continue;
|
|
@@ -277,25 +390,42 @@ function readSlide(context, entries, slideIndex, slideCount, report, taggedText)
|
|
|
277
390
|
if (value !== false) for (const zone of zones) if (definition.value[zone] !== undefined) {
|
|
278
391
|
value[zone] = Object.fromEntries(Object.entries(definition.value[zone]).filter(([, flag]) => flag === false));
|
|
279
392
|
}
|
|
280
|
-
const candidate = {scope: definition.scope, value, text: [], pictures: [],
|
|
281
|
-
// logo
|
|
282
|
-
for (const
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
if (found.length) { candidate.pictures.push(found[0].index); value[logo.zone].logo = true; }
|
|
286
|
-
else if (!logo.drawn) value[logo.zone].logo = true;
|
|
393
|
+
const candidate = {scope: definition.scope, value, text: [], pictures: [], socials: []};
|
|
394
|
+
// A logo reference with no part (the organization had no logo to draw) returns as the reference.
|
|
395
|
+
for (const [slot, entry] of Object.entries(manifest.images ?? {})) {
|
|
396
|
+
const [slotKind, slotZone] = slot.split('.');
|
|
397
|
+
if (slotKind === kind && entry.drawn === false && !definition.value?.[slotZone]?.image && value !== false) value[slotZone].image = entry.reference;
|
|
287
398
|
}
|
|
288
399
|
for (const [partIndex, part] of manifest.parts.entries()) {
|
|
289
400
|
if (part.kind !== kind) continue;
|
|
290
|
-
|
|
401
|
+
// A part on the master or a layout is read there; the slide's own shapes otherwise.
|
|
402
|
+
const source = part.on ? inherited.sources[part.on] : context;
|
|
403
|
+
const group = part.on ? (inherited.hidden[part.on] || !source ? [] : inherited.records.get(`${part.on}|${key(part)}`) ?? []) : records.filter(record => record.data.part === partIndex);
|
|
404
|
+
const {paragraphs, relationships} = source ?? context;
|
|
405
|
+
const logo = part.type === 'image' ? manifest.images?.[`${kind}.${part.zone}`] : undefined;
|
|
406
|
+
// The picture of a logo reference could not be embedded at export: the reference is all there is.
|
|
407
|
+
if (logo?.drawn === false && group.length === 0) { value[part.zone].image = logo.reference; continue; }
|
|
291
408
|
// A native placeholder PowerPoint's Header & Footer dialog removed is a deliberate removal, not damage.
|
|
292
409
|
if (part.ph !== undefined && group.length === 0) { removedNative = true; continue; }
|
|
410
|
+
// So is a master or layout part the slide no longer shows (deleted there, another layout, or the slide's background graphics hidden).
|
|
411
|
+
if (part.on !== undefined && group.length === 0) {
|
|
412
|
+
check(inherited.hidden[part.on] || !inherited.damaged[part.on], 'Damaged furniture tags on the slide master or layout.');
|
|
413
|
+
removedNative = true;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
293
417
|
check(group.length === part.count, 'Incomplete or duplicated furniture part.');
|
|
294
418
|
if (part.type === 'image') {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
419
|
+
// An unchanged picture of an organization logo returns as its reference (the organization is stored with its logo, and the
|
|
420
|
+
// picture still embeds the bytes export drew); an edited, replaced or foreign one is an ordinary image.
|
|
421
|
+
const picture = source.pictures[group[0].index];
|
|
422
|
+
if (logo && logoOwner(logo.reference) && logo.media !== undefined && logo.media === pictureKey(picture, relationships, entries)) value[part.zone].image = logo.reference;
|
|
423
|
+
else {
|
|
424
|
+
const current = source.readPicture(picture);
|
|
425
|
+
check(current?.kind === 'image', 'Furniture image is no longer recoverable.');
|
|
426
|
+
value[part.zone].image = current.payload.image;
|
|
427
|
+
}
|
|
428
|
+
if (!part.on) candidate.pictures.push(group[0].index);
|
|
299
429
|
} else {
|
|
300
430
|
const ordered = Array(part.count);
|
|
301
431
|
for (const record of group) {
|
|
@@ -305,22 +435,26 @@ function readSlide(context, entries, slideIndex, slideCount, report, taggedText)
|
|
|
305
435
|
}
|
|
306
436
|
const text = sourceLineParagraphs(ordered, paragraphs)[0].text;
|
|
307
437
|
const format = manifest.formats?.[`${kind}.${part.zone}`] ?? {};
|
|
308
|
-
if (part.field === 'slideNumber') {
|
|
309
|
-
// The live field renumbers in PowerPoint; any fixed text around it
|
|
310
|
-
// must still match the recorded format at this slide's position.
|
|
311
|
-
check(text === formatSlideNumber(format.slideNumberFormat ?? '{current}', slideIndex + 1, slideCount), 'Current slide number differs from its position; retain the visible number as ordinary text.');
|
|
312
|
-
if (format.slideNumberFormat !== undefined) value[part.zone].slideNumberFormat = format.slideNumberFormat;
|
|
313
|
-
}
|
|
314
|
-
if (part.field === 'section') candidate.sections.push(text);
|
|
315
|
-
if (part.field === 'organization') candidate.organizations.push({id: manifest.organizationId, name: text});
|
|
316
|
-
if (part.field === 'speaker') candidate.speakers.push(importedSpeaker(manifest.speakerId, text, part.nameLength));
|
|
317
438
|
if (part.field === 'socials') {
|
|
318
439
|
const lines = text.split('\n');
|
|
319
440
|
check(lines.length === part.socials.length && lines.every(line => line.trim()), 'Current social profile lines no longer match their platforms.');
|
|
320
441
|
candidate.socials.push({id: manifest.organizationId, socials: Object.fromEntries(part.socials.map((line, index) => [line.platform, line.scheme + lines[index]]))});
|
|
321
442
|
}
|
|
322
|
-
value[part.zone][part.field] = part.field === 'text' ? text : part.field === 'date' ? importedDate(definition.value[part.zone].date, format, text, ordered.flatMap(record => (paragraphs[record.index] ?? []).flatMap(paragraph => paragraph.fields ?? [])), value[part.zone], part.staticDate, ordered, paragraphs, message => report(slideIndex, `${kind}.${part.zone}.date: ${message}`)) : true;
|
|
323
|
-
candidate.text.push(...ordered.map(record => record.index));
|
|
443
|
+
value[part.zone][part.field] = part.field === 'text' ? importedZoneText(text, orderedFields(ordered, paragraphs), manifest.templates?.[`${kind}.${part.zone}`], {index: slideIndex, count: slideCount, section, draw}) : part.field === 'date' ? importedDate(definition.value[part.zone].date, format, text, ordered.flatMap(record => (paragraphs[record.index] ?? []).flatMap(paragraph => paragraph.fields ?? [])), value[part.zone], part.staticDate, ordered, paragraphs, message => report(slideIndex, `${kind}.${part.zone}.date: ${message}`)) : true;
|
|
444
|
+
if (!part.on) candidate.text.push(...ordered.map(record => record.index));
|
|
445
|
+
}
|
|
446
|
+
} catch (error) {
|
|
447
|
+
// A part on the slide master or a layout is never slide content: when its record no longer reads, its current words (or
|
|
448
|
+
// picture) stay the part's value, as literal text, and the damage is reported.
|
|
449
|
+
if (!part.on) throw error;
|
|
450
|
+
report(slideIndex, `${kind}.${part.zone}.${part.field}: ${error.message} The current ${part.type === 'image' ? 'picture' : 'words'} on the slide master or layout are kept.`);
|
|
451
|
+
if (part.type === 'image') {
|
|
452
|
+
const current = source.readPicture(source.pictures[group[0].index]);
|
|
453
|
+
if (current?.kind === 'image') value[part.zone].image = current.payload.image;
|
|
454
|
+
} else if (part.field !== 'socials') {
|
|
455
|
+
const lines = [...group].sort((a, b) => (Number.isSafeInteger(a.data.line) ? a.data.line : 0) - (Number.isSafeInteger(b.data.line) ? b.data.line : 0) || a.index - b.index);
|
|
456
|
+
value[part.zone][part.field] = lines.map(record => (paragraphs[record.index] ?? []).map(paragraph => paragraph.text).join('\n')).join('\n');
|
|
457
|
+
}
|
|
324
458
|
}
|
|
325
459
|
}
|
|
326
460
|
check(new Set(candidate.text).size === candidate.text.length && new Set(candidate.pictures).size === candidate.pictures.length, 'Repeated furniture shape identity.');
|
|
@@ -339,14 +473,6 @@ function readSlide(context, entries, slideIndex, slideCount, report, taggedText)
|
|
|
339
473
|
return candidates;
|
|
340
474
|
}
|
|
341
475
|
|
|
342
|
-
// The generated speaker line is "Name, Title" (or "Name"). The recorded name length splits it again; a line edited in
|
|
343
|
-
// PowerPoint that no longer has the separator at that position is the visible text as the name, with no title.
|
|
344
|
-
function importedSpeaker(id, text, nameLength) {
|
|
345
|
-
const rest = text.slice(nameLength);
|
|
346
|
-
if (text.length > nameLength && rest.startsWith(', ') && rest.length > 2) return {id, name: text.slice(0, nameLength), title: rest.slice(2)};
|
|
347
|
-
return {id, name: text};
|
|
348
|
-
}
|
|
349
|
-
|
|
350
476
|
// A current native date field wins. Full-mode static-wrap evidence can recover
|
|
351
477
|
// generated OPF intent without turning the existing PPTX text into a live field. A fixed
|
|
352
478
|
// date keeps its ISO value and pattern only when the current text round-trips.
|
|
@@ -374,37 +500,25 @@ function importedDate(flag, format, text, nativeFields, zone, marker, ordered, p
|
|
|
374
500
|
// Reconcile metadata before consuming any shapes. Global inheritance is promoted
|
|
375
501
|
// only when every slide has a valid definition/override and all inherited values
|
|
376
502
|
// agree. A missing tag must never cause another slide's furniture to reappear.
|
|
377
|
-
|
|
503
|
+
// `options.sections` are the native section names by slide index (a slide's `{{slide.section}}` is checked against them),
|
|
504
|
+
// `options.organizations` the stored organizations (OPF_DOCUMENT_V1) a socials part can name by id, and `options.draw(template,
|
|
505
|
+
// {index, count, section})` draws a stored zone text for a slide (its built-in variables resolved from the document's metadata).
|
|
506
|
+
export function importFurniture(contexts, entries, onDiagnostic, {sections = [], organizations = [], draw = () => undefined} = {}) {
|
|
507
|
+
// A logo reference returns only while the stored organizations still give it a logo (RR-71): `organizations` carry `logo`.
|
|
508
|
+
const logoOwner = reference => {
|
|
509
|
+
try { return resolveLogo({organization: organizations.filter(item => item.logo !== undefined)}, {}, {reference}) !== null; } catch { return false; }
|
|
510
|
+
};
|
|
378
511
|
const report = (index, message) => onDiagnostic?.({code: 'invalid-furniture-provenance', path: `slides.${index}.design`, message: `${message} Ordinary import retains current native content; no old source words are restored.`});
|
|
379
512
|
const taggedText = contexts.map(() => new Set());
|
|
380
|
-
const candidates = contexts.map((context, index) => readSlide(context, entries, index, contexts.length, report, taggedText[index]));
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
for (const [index, slide] of candidates.entries()) for (const kind of kinds) if (slide[kind]?.organizations.length) {
|
|
385
|
-
delete slide[kind]; report(index, `${kind}: Current organization metadata disagrees across repeated fields.`);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
const speakers = candidates.flatMap(slide => Object.values(slide).flatMap(candidate => candidate.speakers));
|
|
389
|
-
const speakerConflict = speakers.some(item => !same(item, speakers[0]));
|
|
390
|
-
if (speakerConflict) {
|
|
391
|
-
for (const [index, slide] of candidates.entries()) for (const kind of kinds) if (slide[kind]?.speakers.length) {
|
|
392
|
-
delete slide[kind]; report(index, `${kind}: Current speaker metadata disagrees across repeated fields.`);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
for (const [index, slide] of candidates.entries()) {
|
|
396
|
-
const sections = Object.values(slide).flatMap(candidate => candidate.sections);
|
|
397
|
-
if (sections.some(section => section !== sections[0])) for (const kind of kinds) if (slide[kind]?.sections.length) {
|
|
398
|
-
delete slide[kind]; report(index, `${kind}: Current section metadata disagrees on this slide.`);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
// Social profiles belong to the organization named by repeated furniture; without
|
|
402
|
-
// that name (or with disagreeing values) the lines stay ordinary current text.
|
|
403
|
-
const owner = candidates.flatMap(slide => Object.values(slide).flatMap(candidate => candidate.organizations))[0];
|
|
513
|
+
const candidates = contexts.map((context, index) => readSlide(context, entries, index, contexts.length, report, taggedText[index], sections[index], draw, logoOwner));
|
|
514
|
+
// Social profiles belong to the organization the manifest names by id, which only the stored document metadata can supply
|
|
515
|
+
// (no furniture part shows the organization's name any more); without it (or with disagreeing values) the lines stay
|
|
516
|
+
// ordinary current text.
|
|
404
517
|
const socials = candidates.flatMap(slide => Object.values(slide).flatMap(candidate => candidate.socials));
|
|
518
|
+
const owner = socials.length ? organizations.find(item => item.id === socials[0].id) : undefined;
|
|
405
519
|
if (socials.length && (!owner || socials.some(item => item.id !== owner.id || !same(item, socials[0])))) {
|
|
406
520
|
for (const [index, slide] of candidates.entries()) for (const kind of kinds) if (slide[kind]?.socials.length) {
|
|
407
|
-
delete slide[kind]; report(index, `${kind}: ${owner ? 'Current social profile metadata disagrees across repeated fields.' : 'Social profiles need
|
|
521
|
+
delete slide[kind]; report(index, `${kind}: ${owner ? 'Current social profile metadata disagrees across repeated fields.' : 'Social profiles need the stored organization metadata to rebuild organization.socials.'}`);
|
|
408
522
|
}
|
|
409
523
|
}
|
|
410
524
|
// RR-11: PowerPoint's own date, footer and slide-number placeholders that no OPF tag claims (a deck authored in
|
|
@@ -422,7 +536,7 @@ export function importFurniture(contexts, entries, onDiagnostic) {
|
|
|
422
536
|
}
|
|
423
537
|
if (!accepted.length) return;
|
|
424
538
|
const merged = footerValue(accepted), indexes = accepted.map(item => item.index);
|
|
425
|
-
if (!existing) slide.footer = {scope: 'native', value: merged, text: indexes, pictures: [],
|
|
539
|
+
if (!existing) slide.footer = {scope: 'native', value: merged, text: indexes, pictures: [], socials: []};
|
|
426
540
|
else {
|
|
427
541
|
if (existing.value === false) existing.value = {};
|
|
428
542
|
for (const zone of Object.keys(merged)) existing.value[zone] = {...existing.value[zone], ...merged[zone]};
|
|
@@ -430,20 +544,13 @@ export function importFurniture(contexts, entries, onDiagnostic) {
|
|
|
430
544
|
}
|
|
431
545
|
});
|
|
432
546
|
const result = {design: {}, slides: candidates.map((slide, index) => {
|
|
433
|
-
const values = Object.values(slide)
|
|
547
|
+
const values = Object.values(slide);
|
|
434
548
|
return {design: Object.fromEntries(Object.entries(slide).map(([kind, candidate]) => [kind, candidate.value])),
|
|
435
|
-
...(sections.length ? {section: sections[0]} : {}),
|
|
436
549
|
text: new Set(values.flatMap(candidate => candidate.text)), pictures: new Set(values.flatMap(candidate => candidate.pictures)), taggedText: taggedText[index]};
|
|
437
550
|
})};
|
|
438
|
-
|
|
439
|
-
if (validOrganizations.length) result.organization = validOrganizations[0];
|
|
551
|
+
// The organization the socials belong to: its stored identity, with the profiles the slides show now.
|
|
440
552
|
const validSocials = candidates.flatMap(slide => Object.values(slide).flatMap(candidate => candidate.socials));
|
|
441
|
-
if (
|
|
442
|
-
// Stored document metadata (FF-32) must not override disagreeing visible names.
|
|
443
|
-
if (organizationConflict) result.organizationConflict = true;
|
|
444
|
-
const validSpeakers = candidates.flatMap(slide => Object.values(slide).flatMap(candidate => candidate.speakers));
|
|
445
|
-
if (validSpeakers.length) result.speaker = validSpeakers[0];
|
|
446
|
-
if (speakerConflict) result.speakerConflict = true;
|
|
553
|
+
if (owner && validSocials.length) result.organization = {id: owner.id, name: owner.name, socials: validSocials[0].socials};
|
|
447
554
|
for (const kind of kinds) {
|
|
448
555
|
const inherited = candidates.map(slide => slide[kind]).filter(candidate => candidate?.scope === 'global');
|
|
449
556
|
if (inherited.length && candidates.every(slide => slide[kind]) && inherited.every(candidate => same(candidate.value, inherited[0].value))) {
|
|
@@ -13,12 +13,12 @@ export async function webpToPng(bytes) {
|
|
|
13
13
|
// not installed, and also when it is installed but its PNG converter (`@resvg/resvg-js`, an optional peer of opf-render
|
|
14
14
|
// 0.16) is not: opf-render's `converter-missing` error maps to the same code, with its install command on `install`.
|
|
15
15
|
//
|
|
16
|
-
// This is the one place the exporter calls the renderer. opf-render's
|
|
16
|
+
// This is the one place the exporter calls the renderer. opf-render's toPng(svg, {fonts, scale, background}) takes the
|
|
17
17
|
// fonts handle for the faces it may draw with: here only the bundled ones, never the system's.
|
|
18
18
|
export async function svgToPng(svg, {scale, text}) {
|
|
19
19
|
let render;
|
|
20
20
|
try {
|
|
21
|
-
({
|
|
21
|
+
({toPng: render} = await import('@openpresentation/opf-render'));
|
|
22
22
|
} catch (error) {
|
|
23
23
|
const unavailable = new Error('SVG pictures need the optional peer @openpresentation/opf-render (or an options.svgRasterizer).', {cause: error});
|
|
24
24
|
unavailable.code = 'svg-rasterizer-unavailable';
|
package/dist/index.d.ts
CHANGED
|
@@ -97,7 +97,7 @@ export interface ToPptxOptions {
|
|
|
97
97
|
/**
|
|
98
98
|
* OPF_DOCUMENT_V1 / OPF_SLIDE_V1 customer-data tags that let fromPptx restore
|
|
99
99
|
* catalog references, layout ids, authoring metadata (`filename` and `extensions`
|
|
100
|
-
* included), `design.logo
|
|
100
|
+
* included), `design.logo` (a logo reference or `false`), the organizations with their logos, the asset registry, slide `section`/`extensions` and
|
|
101
101
|
* the slide content structure: nested groups, promoted regions, the root payload
|
|
102
102
|
* form, block ids and extensions, group composition (docs/document-roundtrip.md).
|
|
103
103
|
* Tags are not shown in PowerPoint's UI. Default "full"; "references-only"
|
|
@@ -131,7 +131,7 @@ export interface ToPptxOptions {
|
|
|
131
131
|
/**
|
|
132
132
|
* Draws the PNG fallback of an SVG picture (the raster older viewers show; PowerPoint 2016 and Microsoft 365 draw the SVG itself). It receives the
|
|
133
133
|
* sanitized SVG text and the pixel size to draw (the SVG's own aspect, about 192 dpi of the displayed size) and returns PNG bytes with a transparent
|
|
134
|
-
* background. It must be deterministic for the export to be. Default in Node: opf-render's `
|
|
134
|
+
* background. It must be deterministic for the export to be. Default in Node: opf-render's `toPng` (resvg with the bundled fonts), loaded only when an
|
|
135
135
|
* SVG is exported (optional peer `@openpresentation/opf-render`). Without it, and without opf-render, an SVG picture exports as the "Image unavailable"
|
|
136
136
|
* placeholder with an `unresolved-asset` diagnostic (`reason: "svg-rasterizer-unavailable"`); a browser build has no default.
|
|
137
137
|
*/
|
|
@@ -172,7 +172,7 @@ export interface ToPptxOptions {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
export interface FromPptxOptions {
|
|
175
|
-
/** Reports native details that import cannot preserve, including code provenance fallback/reflow and grouped text transforms. Table paths identify native frame and row/cell indexes (including headers). Stored catalog references that no longer match the package report `design-reference-changed` / `layout-reference-changed` at the reference path; a stored slide layout reference that resolves neither in the stored document catalogs, nor in the slide's own stored record, nor in `catalogs` reports `unresolved-reference`; a slide whose imported blocks no longer fit the stored content structure reports `content-structure-changed` at `slides.N`, a block id repeated by a duplicated slide `duplicate-block-id
|
|
175
|
+
/** Reports native details that import cannot preserve, including code provenance fallback/reflow and grouped text transforms. Table paths identify native frame and row/cell indexes (including headers). Stored catalog references that no longer match the package report `design-reference-changed` / `layout-reference-changed` at the reference path; a stored slide layout reference that resolves neither in the stored document catalogs, nor in the slide's own stored record, nor in `catalogs` reports `unresolved-reference`; a slide whose imported blocks no longer fit the stored content structure reports `content-structure-changed` at `slides.N`, a block id repeated by a duplicated slide `duplicate-block-id` (docs/document-roundtrip.md). */
|
|
176
176
|
onDiagnostic?: (diagnostic: {code: string; path: string; message: string}) => void;
|
|
177
177
|
fallbackName?: string;
|
|
178
178
|
schema?: string;
|