@openpresentation/opf-pptx 0.14.0 → 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 +25 -23
- package/dist/background-import.js +33 -17
- package/dist/background.js +28 -25
- package/dist/chart-types.js +7 -22
- package/dist/document-provenance.js +195 -128
- package/dist/image-provenance.js +273 -0
- package/dist/index.d.ts +32 -17
- package/dist/index.js +268 -189
- package/dist/script-fonts.js +34 -60
- package/dist/theme-colors.js +15 -10
- package/dist/typeface-inventory.js +1 -1
- package/package.json +4 -4
- package/dist/slide-image-provenance.js +0 -177
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import {XMLParser} from 'fast-xml-parser';
|
|
2
|
+
import {normalizeCrop, withoutSvgBlip} from './svg-image.js';
|
|
3
|
+
import {attachTextTags, decodeTextTag} from './code-provenance.js';
|
|
4
|
+
|
|
5
|
+
// FA-23 (OPF 0.15): image blocks and image backgrounds as native pictures.
|
|
6
|
+
//
|
|
7
|
+
// Every image block (core ComposedItem.image) exports as one native picture at core's frame. Its fit and focus are an
|
|
8
|
+
// a:srcRect; its treatments keep the DrawingML mapping 0.14's slide image used: the mask is a:prstGeom with core's guide
|
|
9
|
+
// values, the border an a:ln centered on the outline with a miter join, and the recolor and opacity a:grayscl or
|
|
10
|
+
// a:duotone then a:alphaModFix on the blip. An overlay is one native shape directly above its picture.
|
|
11
|
+
//
|
|
12
|
+
// An image background (core SlideComposition.backgroundImage) with alt text is a full-slide picture at the back of the
|
|
13
|
+
// slide (screen readers read its descr); without alt it is the native slide background fill (src/background.js), and its
|
|
14
|
+
// overlay is the first shape of the slide.
|
|
15
|
+
//
|
|
16
|
+
// OPF_IMAGE_V1 binds a picture to the image block's (or background's) own authored fields and the exact native geometry it
|
|
17
|
+
// was written with, so an unchanged picture imports back with its treatments and placement while an edited one stays an
|
|
18
|
+
// ordinary picture. OPF_IMAGE_OVERLAY_V1 does the same for an overlay shape: an edited overlay drops only the overlay.
|
|
19
|
+
export const IMAGE_TAG = 'OPF_IMAGE_V1', IMAGE_OVERLAY_TAG = 'OPF_IMAGE_OVERLAY_V1';
|
|
20
|
+
const REL = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags';
|
|
21
|
+
const EMUS_PER_INCH = 914400;
|
|
22
|
+
const parser = new XMLParser({ignoreAttributes:false, attributeNamePrefix:'', parseTagValue:false, trimValues:false});
|
|
23
|
+
const decoder = new TextDecoder('utf-8', {fatal:true}), encoder = new TextEncoder();
|
|
24
|
+
const array = value => value === undefined ? [] : Array.isArray(value) ? value : [value];
|
|
25
|
+
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
26
|
+
// Whitespace-only text between elements is not identity: the vendored writer indents, PowerPoint drops it on save.
|
|
27
|
+
const canonical = value => JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item)
|
|
28
|
+
? Object.fromEntries(Object.keys(item).sort().filter(key => !(key === '#text' && typeof item[key] === 'string' && !item[key].trim())).map(key => [key, item[key]])) : item);
|
|
29
|
+
|
|
30
|
+
/** An image block's own fields (everything but its type, asset, id, caption and extensions). */
|
|
31
|
+
export const IMAGE_TREATMENT_KEYS = Object.freeze(['fit', 'focus', 'shape', 'cornerRadius', 'border', 'opacity', 'recolor', 'overlay', 'aspectRatio', 'placement']);
|
|
32
|
+
/** An image background's own fields besides its source. */
|
|
33
|
+
export const BACKGROUND_IMAGE_KEYS = Object.freeze(['alt', 'fit', 'focus', 'opacity', 'recolor', 'overlay']);
|
|
34
|
+
|
|
35
|
+
export const imageName = number => `OPF image ${number}`;
|
|
36
|
+
export const imageOverlayName = pictureName => `${pictureName} overlay`;
|
|
37
|
+
export const backgroundImageName = slide => `OPF background ${slide}`;
|
|
38
|
+
export const backgroundOverlayName = slide => `OPF background overlay ${slide}`;
|
|
39
|
+
|
|
40
|
+
// DrawingML for the shared treatment: preset geometry (core guide values), a centered solid line, and blip
|
|
41
|
+
// recolor/alpha effects applied in that order.
|
|
42
|
+
const presetXml = shape => `<a:prstGeom prst="${shape?.preset ?? 'rect'}"><a:avLst>${Object.entries(shape?.adjust ?? {}).map(([name, value]) => `<a:gd name="${name}" fmla="val ${value}"/>`).join('')}</a:avLst></a:prstGeom>`;
|
|
43
|
+
const colorXml = ({hex, alpha}, extraAlpha = 1) => {
|
|
44
|
+
const value = Math.round(alpha * extraAlpha * 100000);
|
|
45
|
+
return `<a:srgbClr val="${hex}">${value < 100000 ? `<a:alpha val="${value}"/>` : ''}</a:srgbClr>`;
|
|
46
|
+
};
|
|
47
|
+
const lineXml = border => border ? `<a:ln w="${Math.round(border.width / 96 * EMUS_PER_INCH)}" algn="ctr"><a:solidFill>${colorXml(border)}</a:solidFill><a:miter lim="800000"/></a:ln>` : '';
|
|
48
|
+
function blipEffects(effects) {
|
|
49
|
+
let xml = '';
|
|
50
|
+
if (effects?.recolor?.type === 'grayscale') xml += '<a:grayscl/>';
|
|
51
|
+
if (effects?.recolor?.type === 'duotone') xml += `<a:duotone>${colorXml({...effects.recolor.dark, alpha: 1})}${colorXml({...effects.recolor.light, alpha: 1})}</a:duotone>`;
|
|
52
|
+
if (typeof effects?.opacity === 'number') xml += `<a:alphaModFix amt="${Math.round(effects.opacity * 100000)}"/>`;
|
|
53
|
+
return xml;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Native blip fill without its package-local relationship id.
|
|
57
|
+
function blipFillIdentity(blipFill) {
|
|
58
|
+
const {['a:blip']: blip, ...rest} = normalizeCrop(blipFill) ?? {};
|
|
59
|
+
const {['r:embed']: _embed, ...blipRest} = withoutSvgBlip(blip) ?? {};
|
|
60
|
+
return {...rest, 'a:blip': blipRest};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The focus point in the stored picture's own axes. DrawingML crops (a:srcRect) the picture before it flips and turns it
|
|
64
|
+
// (the EXIF orientation is written as the frame's rot/flipH/flipV), so a point the author placed on the displayed
|
|
65
|
+
// picture maps back through that orientation.
|
|
66
|
+
function storedFocus({x, y}, orientation) {
|
|
67
|
+
switch (orientation) {
|
|
68
|
+
case 2: return {x: 1 - x, y};
|
|
69
|
+
case 3: return {x: 1 - x, y: 1 - y};
|
|
70
|
+
case 4: return {x, y: 1 - y};
|
|
71
|
+
case 5: return {x: y, y: x};
|
|
72
|
+
case 6: return {x: y, y: 1 - x};
|
|
73
|
+
case 7: return {x: 1 - y, y: 1 - x};
|
|
74
|
+
case 8: return {x: 1 - y, y: x};
|
|
75
|
+
default: return {x, y};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const srcRectValue = value => Math.round(value * 100000) || 0;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The native frame and crop of a picture: `box` (inches) is core's picture frame, `fit` cover | contain | stretch,
|
|
83
|
+
* `focus` core's focus point, `metadata` the embedded raster (width, height, orientation). `fitImage` is core's shared
|
|
84
|
+
* fit math. A plain contain picture (`shrink`: a rectangle with no line or overlay) is framed by the picture itself, as
|
|
85
|
+
* 0.14 content images were; every other picture keeps the frame and pads with negative a:srcRect insets, so the mask,
|
|
86
|
+
* line and overlay apply to core's frame.
|
|
87
|
+
*/
|
|
88
|
+
export function pictureFrame(metadata, {box, fit, focus = {x: .5, y: .5}, shrink = false}, fitImage) {
|
|
89
|
+
const orientation = metadata.orientation ?? 1;
|
|
90
|
+
const target = orientation >= 5 ? {x: box.x + (box.w - box.h) / 2, y: box.y + (box.h - box.w) / 2, w: box.h, h: box.w} : box;
|
|
91
|
+
const rotation = [0, 0, 0, 180, 0, 90, 90, 90, 270][orientation];
|
|
92
|
+
const transform = {rotation, flipH: orientation === 2 || orientation === 7, flipV: orientation === 4 || orientation === 5};
|
|
93
|
+
// A tiled picture fills its frame with the picture at its intrinsic size; nothing is cropped.
|
|
94
|
+
if (fit === 'tile') return {...target, crop: null, ...transform};
|
|
95
|
+
const placed = fitImage({x: target.x, y: target.y, width: target.w, height: target.h}, fit, metadata.width / metadata.height, storedFocus(focus, orientation));
|
|
96
|
+
if (shrink && fit === 'contain') return {x: placed.image.x, y: placed.image.y, w: placed.image.width, h: placed.image.height, crop: null, ...transform};
|
|
97
|
+
const crop = {l: srcRectValue(placed.crop.left), t: srcRectValue(placed.crop.top), r: srcRectValue(placed.crop.right), b: srcRectValue(placed.crop.bottom)};
|
|
98
|
+
return {...target, crop: Object.values(crop).some(Boolean) ? crop : null, ...transform};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Place each generated image picture at its frame, write its treatment, place its overlay shape, then bind the tagged
|
|
103
|
+
* ones to their manifests. Runs after PptxGenJS embedded the bytes (the frame needs the raster's own size).
|
|
104
|
+
* images: Map<objectName, {slide, role: 'block' | 'background', path, box (inches), fit, focus, shrink, effects, treatment}>;
|
|
105
|
+
* `treatment` (the authored fields) is stored in OPF_IMAGE_V1 when present.
|
|
106
|
+
* overlays: Map<objectName, {slide, owner, box (reference px), shape, hex, alpha, opacity, overlay}>; `owner` is the
|
|
107
|
+
* picture's objectName, or 'background' for the overlay of a native slide background fill; `overlay` (the authored value)
|
|
108
|
+
* is stored in OPF_IMAGE_OVERLAY_V1.
|
|
109
|
+
*/
|
|
110
|
+
export function placeImages(entries, images, overlays, {metadataFor, fail, fitImage}) {
|
|
111
|
+
if (!images.size && !overlays.size) return;
|
|
112
|
+
const manifests = new Map(), overlayManifests = new Map(), placed = new Set(), shaped = new Set();
|
|
113
|
+
for (const part of Object.keys(entries).filter(path => /^ppt\/slides\/slide\d+\.xml$/.test(path))) {
|
|
114
|
+
const xml = decoder.decode(entries[part]);
|
|
115
|
+
let next = xml.replace(/<p:pic>[\s\S]*?<\/p:pic>/g, picture => {
|
|
116
|
+
const name = picture.match(/<p:cNvPr\b[^>]*\bname="([^"]+)"/)?.[1];
|
|
117
|
+
const image = images.get(name);
|
|
118
|
+
if (!image) return picture;
|
|
119
|
+
const embed = picture.match(/<a:blip\b[^>]*r:embed="([^"]+)"/)?.[1];
|
|
120
|
+
const metadata = metadataFor(part, embed);
|
|
121
|
+
if (!metadata) fail(image.path);
|
|
122
|
+
const frame = pictureFrame(metadata, image, fitImage);
|
|
123
|
+
const emu = value => Math.round(value * EMUS_PER_INCH);
|
|
124
|
+
const attributes = `${frame.rotation ? ` rot="${frame.rotation * 60000}"` : ''}${frame.flipH ? ' flipH="1"' : ''}${frame.flipV ? ' flipV="1"' : ''}`;
|
|
125
|
+
picture = picture.replace(/<a:xfrm\b[^>]*>[\s\S]*?<\/a:xfrm>/, `<a:xfrm${attributes}><a:off x="${emu(frame.x)}" y="${emu(frame.y)}"/><a:ext cx="${emu(frame.w)}" cy="${emu(frame.h)}"/></a:xfrm>`);
|
|
126
|
+
picture = picture.replace(/<a:srcRect\b[^>]*\/>/, '');
|
|
127
|
+
if (frame.crop) picture = picture.replace('<a:stretch>', `<a:srcRect l="${frame.crop.l}" t="${frame.crop.t}" r="${frame.crop.r}" b="${frame.crop.b}"/><a:stretch>`);
|
|
128
|
+
if (image.fit === 'tile') picture = picture.replace(/<a:stretch>[\s\S]*?<\/a:stretch>|<a:stretch\/>/, `<a:tile tx="0" ty="0" sx="${srcRectValue((metadata.dpiX ?? 96) / 96)}" sy="${srcRectValue((metadata.dpiY ?? 96) / 96)}" flip="none" algn="tl"/>`);
|
|
129
|
+
const effects = image.effects;
|
|
130
|
+
if (effects) {
|
|
131
|
+
picture = picture.replace(/<a:prstGeom\b[\s\S]*?<\/a:prstGeom>/, presetXml(effects.shape) + lineXml(effects.border));
|
|
132
|
+
const blip = blipEffects(effects);
|
|
133
|
+
if (blip) picture = picture.replace(/<a:blip\b([^>]*?)(?:\/>|>([\s\S]*?)<\/a:blip>)/, (_, attributes, inner = '') => `<a:blip${attributes}>${blip}${inner}</a:blip>`);
|
|
134
|
+
}
|
|
135
|
+
placed.add(name);
|
|
136
|
+
if (image.treatment !== undefined) {
|
|
137
|
+
const node = parser.parse(picture)['p:pic'];
|
|
138
|
+
manifests.set(name, {v: 1, role: image.role, slide: image.slide, path: image.path, treatment: image.treatment, properties: node['p:spPr'], blipFill: blipFillIdentity(node['p:blipFill'])});
|
|
139
|
+
}
|
|
140
|
+
return picture;
|
|
141
|
+
});
|
|
142
|
+
next = next.replace(/<p:sp>[\s\S]*?<\/p:sp>/g, shape => {
|
|
143
|
+
const name = shape.match(/<p:cNvPr\b[^>]*\bname="([^"]+)"/)?.[1];
|
|
144
|
+
const overlay = overlays.get(name);
|
|
145
|
+
if (!overlay) return shape;
|
|
146
|
+
const emu = value => Math.round(value / 96 * EMUS_PER_INCH);
|
|
147
|
+
shape = shape.replace(/<p:spPr>[\s\S]*?<\/p:spPr>/, `<p:spPr><a:xfrm><a:off x="${emu(overlay.box.x)}" y="${emu(overlay.box.y)}"/><a:ext cx="${emu(overlay.box.width)}" cy="${emu(overlay.box.height)}"/></a:xfrm>${presetXml(overlay.shape)}<a:solidFill>${colorXml(overlay, overlay.opacity)}</a:solidFill><a:ln><a:noFill/></a:ln></p:spPr>`);
|
|
148
|
+
const node = parser.parse(shape)['p:sp'];
|
|
149
|
+
overlayManifests.set(name, {v: 1, slide: overlay.slide, owner: overlay.owner, overlay: overlay.overlay, properties: node['p:spPr']});
|
|
150
|
+
shaped.add(name);
|
|
151
|
+
return shape;
|
|
152
|
+
});
|
|
153
|
+
if (next !== xml) entries[part] = encoder.encode(next);
|
|
154
|
+
}
|
|
155
|
+
if (placed.size !== images.size) throw new Error('Missing generated image picture.');
|
|
156
|
+
if (shaped.size !== overlays.size) throw new Error('Missing generated image overlay.');
|
|
157
|
+
attachTextTags(entries, manifests, IMAGE_TAG, 'opfImage', 'image', {pictures: true});
|
|
158
|
+
attachTextTags(entries, overlayManifests, IMAGE_OVERLAY_TAG, 'opfImageOverlay', 'image overlay');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Import
|
|
163
|
+
|
|
164
|
+
function readTags(container, relationships, entries) {
|
|
165
|
+
const tags = [];
|
|
166
|
+
let unreadable = false;
|
|
167
|
+
for (const link of array(container?.['p:tags'])) {
|
|
168
|
+
const rel = relationships.get(link['r:id']);
|
|
169
|
+
if (rel?.type !== REL || rel.targetMode === 'External' || !entries[rel.path]) { unreadable = true; continue; }
|
|
170
|
+
try { tags.push(...array(parser.parse(decoder.decode(entries[rel.path]))['p:tagLst']?.['p:tag'])); } catch { unreadable = true; }
|
|
171
|
+
}
|
|
172
|
+
return {tags, unreadable};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// The one tag of `name` on a shape, decoded, or null (absent); throws when it is ambiguous or sits beside another OPF tag.
|
|
176
|
+
function ownTag(container, relationships, entries, name) {
|
|
177
|
+
const {tags, unreadable} = readTags(container, relationships, entries);
|
|
178
|
+
const own = tags.filter(tag => tag.name?.toUpperCase() === name);
|
|
179
|
+
if (!own.length) return null;
|
|
180
|
+
if (unreadable || own.length !== 1 || tags.some(tag => /^OPF_/i.test(tag.name) && tag.name.toUpperCase() !== name)) throw Error('Ambiguous image identity.');
|
|
181
|
+
return decodeTextTag(own[0].val);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const pick = (value, keys) => Object.fromEntries(keys.filter(key => Object.hasOwn(value, key)).map(key => [key, value[key]]));
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Recover image blocks and an image background from unchanged tagged pictures and overlays of one slide.
|
|
188
|
+
* - `blocks`: Map<picture index, fields> to merge into the ordinary image block read from that picture;
|
|
189
|
+
* - `background`: the slide's image background from its tagged back picture, with the picture's index in `consumed`;
|
|
190
|
+
* - `backgroundOverlay`: the authored overlay of a native slide background fill;
|
|
191
|
+
* - `consumedShapes`: the overlay shapes recovered (they are no content).
|
|
192
|
+
* readPicture(picture) returns the ordinary imported image payload; `valid(kind, value)` checks a recovered image block
|
|
193
|
+
* (`kind` 'block') or background (`kind` 'background') against the OPF format.
|
|
194
|
+
*/
|
|
195
|
+
export function importImages({pictures, shapes, relationships, entries, slideIndex, readPicture, valid, report}) {
|
|
196
|
+
const slide = `slides.${slideIndex}`;
|
|
197
|
+
const blocks = new Map(), consumed = new Set(), consumedShapes = new Set();
|
|
198
|
+
let background, backgroundOverlay;
|
|
199
|
+
const invalid = (path, message) => report({code: 'invalid-image-provenance', path, message});
|
|
200
|
+
// Overlay shapes first: each names the picture (or the slide background) it belongs to.
|
|
201
|
+
const overlays = new Map();
|
|
202
|
+
for (const [index, shape] of shapes.entries()) {
|
|
203
|
+
let manifest;
|
|
204
|
+
try { manifest = ownTag(shape['p:nvSpPr']?.['p:nvPr']?.['p:custDataLst'], relationships, entries, IMAGE_OVERLAY_TAG); }
|
|
205
|
+
catch { invalid(slide, 'A tagged OPF image overlay is ambiguous; it was imported as an ordinary shape.'); continue; }
|
|
206
|
+
if (manifest === null) continue;
|
|
207
|
+
const name = shape['p:nvSpPr']?.['p:cNvPr']?.name;
|
|
208
|
+
const expected = manifest?.owner === 'background' ? backgroundOverlayName(slide) : typeof manifest?.owner === 'string' ? imageOverlayName(manifest.owner) : undefined;
|
|
209
|
+
if (manifest?.v !== 1 || manifest.slide !== slide || name !== expected || !object(manifest.overlay) || canonical(shape['p:spPr']) !== canonical(manifest.properties)) {
|
|
210
|
+
overlays.set(manifest?.owner, {index, edited: true});
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (overlays.has(manifest.owner)) { overlays.set(manifest.owner, {index, edited: true}); continue; }
|
|
214
|
+
overlays.set(manifest.owner, {index, overlay: manifest.overlay});
|
|
215
|
+
}
|
|
216
|
+
const takeOverlay = (owner, path) => {
|
|
217
|
+
const found = overlays.get(owner);
|
|
218
|
+
if (!found) return undefined;
|
|
219
|
+
if (found.edited) {
|
|
220
|
+
invalid(path, 'The tagged OPF image overlay is missing, edited or ambiguous, so the image is recovered without its overlay; the remaining native shape is imported as ordinary content.');
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
consumedShapes.add(found.index);
|
|
224
|
+
return found.overlay;
|
|
225
|
+
};
|
|
226
|
+
const edges = new Set();
|
|
227
|
+
for (const [index, picture] of pictures.entries()) {
|
|
228
|
+
let manifest;
|
|
229
|
+
try { manifest = ownTag(picture['p:nvPicPr']?.['p:nvPr']?.['p:custDataLst'], relationships, entries, IMAGE_TAG); }
|
|
230
|
+
catch { invalid(slide, 'A tagged OPF image is ambiguous; it was imported as an ordinary picture.'); continue; }
|
|
231
|
+
if (manifest === null) continue;
|
|
232
|
+
const name = picture['p:nvPicPr']?.['p:cNvPr']?.name;
|
|
233
|
+
const role = manifest?.role;
|
|
234
|
+
const path = role === 'background' ? `${slide}.design.background` : slide;
|
|
235
|
+
try {
|
|
236
|
+
if (manifest?.v !== 1 || manifest.slide !== slide || !['block', 'background'].includes(role) || !object(manifest.treatment)) throw Error('Invalid image manifest.');
|
|
237
|
+
if (role === 'block' ? !/^OPF image \d+$/.test(name ?? '') : name !== backgroundImageName(slide)) throw Error('Image identity changed.');
|
|
238
|
+
if (canonical(picture['p:spPr']) !== canonical(manifest.properties) || canonical(blipFillIdentity(picture['p:blipFill'])) !== canonical(normalizeCrop(manifest.blipFill))) throw Error('Image geometry or effects changed.');
|
|
239
|
+
} catch {
|
|
240
|
+
invalid(path, `An edited, ambiguous or invalid tagged OPF ${role === 'background' ? 'background picture' : 'image'} was imported as an ordinary picture; its OPF ${role === 'background' ? 'background' : 'treatment and placement'} are not reconstructed.`);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (role === 'background') {
|
|
244
|
+
const item = readPicture(picture);
|
|
245
|
+
const src = item?.payload?.image?.src;
|
|
246
|
+
const fields = pick(manifest.treatment, BACKGROUND_IMAGE_KEYS.filter(key => key !== 'overlay'));
|
|
247
|
+
const overlay = takeOverlay(name, `${path}.overlay`);
|
|
248
|
+
const value = {type: 'image', src, ...fields, ...(overlay ? {overlay} : {})};
|
|
249
|
+
if (typeof src !== 'string' || background || !valid('background', value)) { invalid(path, 'A tagged OPF background picture does not form a valid image background; it was imported as an ordinary picture.'); continue; }
|
|
250
|
+
background = value;
|
|
251
|
+
consumed.add(index);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const fields = pick(manifest.treatment, IMAGE_TREATMENT_KEYS.filter(key => key !== 'overlay'));
|
|
255
|
+
const overlay = takeOverlay(name, `${path}.overlay`);
|
|
256
|
+
if (overlay) fields.overlay = overlay;
|
|
257
|
+
// At most one placed block per edge: a copied tagged picture keeps its other fields and flows.
|
|
258
|
+
const edge = fields.placement?.edge;
|
|
259
|
+
if (edge !== undefined && edges.has(edge)) { delete fields.placement; invalid(path, `Two tagged OPF images are placed on the ${edge} edge; the second imports without its placement.`); }
|
|
260
|
+
else if (edge !== undefined) edges.add(edge);
|
|
261
|
+
const item = readPicture(picture);
|
|
262
|
+
const candidate = {...item?.payload, ...fields};
|
|
263
|
+
if (!item?.payload || !valid('block', candidate)) { invalid(path, 'A tagged OPF image does not form a valid image block; it was imported as an ordinary picture.'); if (edge !== undefined) edges.delete(edge); continue; }
|
|
264
|
+
blocks.set(index, fields);
|
|
265
|
+
}
|
|
266
|
+
// The overlay of a native slide background fill.
|
|
267
|
+
backgroundOverlay = takeOverlay('background', `${slide}.design.background.overlay`);
|
|
268
|
+
const backgroundOverlayShape = backgroundOverlay ? overlays.get('background').index : undefined;
|
|
269
|
+
for (const [owner, found] of overlays) if (!found.edited && !consumedShapes.has(found.index) && owner !== 'background') {
|
|
270
|
+
invalid(`${slide}`, 'A tagged OPF image overlay no longer has its picture; it was imported as an ordinary shape.');
|
|
271
|
+
}
|
|
272
|
+
return {blocks, background, backgroundOverlay, backgroundOverlayShape, consumed, consumedShapes};
|
|
273
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Catalog, Finding, Fonts, LayoutDiagnostic, UnresolvedReferenceDiagnostic } from "@openpresentation/opf";
|
|
2
2
|
export declare const packageName = "@openpresentation/opf-pptx";
|
|
3
3
|
|
|
4
4
|
export declare const releaseLane: Readonly<{
|
|
@@ -50,7 +50,7 @@ export interface ContentPlaceholderDiagnostic { code: "content-placeholder"; pat
|
|
|
50
50
|
export interface UnresolvedAssetDiagnostic { code: "unresolved-asset"; path: string; message: string; reason?: "unsupported-format" | "svg-malformed" | "svg-no-size" | "svg-too-large" | "svg-unsafe" | "svg-rasterizer-unavailable" | "svg-render-failed" | "svg-unreadable" }
|
|
51
51
|
/** An SVG picture had scripts, `foreignObject`, event handlers, references outside the file, `@import` rules or a DOCTYPE; they were removed from the embedded SVG (on import too). Nothing in an SVG is run or fetched. */
|
|
52
52
|
export interface SvgSanitizedDiagnostic { code: "svg-sanitized"; path: string; message: string }
|
|
53
|
-
/** An SVG
|
|
53
|
+
/** An SVG image block with a duotone recolor or a non-rectangular shape (or an SVG image background with the tile fit) exports as its PNG raster, not as a native SVG picture, so the effect applies as in the preview (PowerPoint applies opacity, grayscale and a border to an SVG picture, and those stay native). */
|
|
54
54
|
export interface SvgImageRasterizedDiagnostic { code: "svg-image-rasterized"; path: string; message: string }
|
|
55
55
|
/**
|
|
56
56
|
* The chart data was reshaped to export a native chart: a single value column was plotted against row numbers; a one-series construct
|
|
@@ -119,8 +119,8 @@ export interface ToPptxOptions {
|
|
|
119
119
|
fonts?: Fonts;
|
|
120
120
|
/** Match preview/pagination clearance around supplied vector text outlines; default 1. */
|
|
121
121
|
textRasterPadding?: number;
|
|
122
|
-
/** Layout diagnostics, `media-provenance-omitted` when video data cannot be stored,
|
|
123
|
-
onDiagnostic?: (diagnostic: LayoutDiagnostic |
|
|
122
|
+
/** Layout diagnostics, `media-provenance-omitted` when video data cannot be stored, and, from core's slide context, `unresolved-reference` (once per reference path) when a layout, theme, colour-scheme or font-scheme reference resolves nowhere (not embedded in the document's `catalogs` and not in a registered catalog): core's engine defaults are drawn instead, and a slide whose layout resolves nowhere, like one with no layout, composes automatically. None of them refuses the export unless `strictReferences` is set. */
|
|
123
|
+
onDiagnostic?: (diagnostic: LayoutDiagnostic | UnresolvedReferenceDiagnostic | MediaProvenanceDiagnostic | ChartDataUnplottableDiagnostic | ChartDataAdaptedDiagnostic | ChartMapGeodataDiagnostic | ChartValueNotNumericDiagnostic | ChartMappingAdaptedDiagnostic | DataProvenanceOmittedDiagnostic | ContentPlaceholderDiagnostic | UnresolvedAssetDiagnostic | SvgSanitizedDiagnostic | SvgImageRasterizedDiagnostic | VariableExampleUsedDiagnostic) => void;
|
|
124
124
|
baseDir?: string;
|
|
125
125
|
compressionLevel?: number;
|
|
126
126
|
imageResolver?: (src: string, context: ImageResolverContext) => ImageResolverResult | Promise<ImageResolverResult | null | undefined> | null | undefined;
|
|
@@ -134,6 +134,13 @@ export interface ToPptxOptions {
|
|
|
134
134
|
svgRasterizer?: (svg: string, size: { width: number; height: number; scale: number; text: boolean }) => Uint8Array | Promise<Uint8Array>;
|
|
135
135
|
seed?: number;
|
|
136
136
|
strictAssets?: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Strict reference policy (OPF 0.15). When true, a layout, theme, colour-scheme or font-scheme reference that resolves
|
|
139
|
+
* nowhere throws `OPFPptxError` with code `unresolved-reference` (its `path` is the first reference's, its `diagnostics`
|
|
140
|
+
* core's `unresolved-reference` diagnostics) instead of composing automatically or drawing with the engine default.
|
|
141
|
+
* Default: the value of `strictAssets`.
|
|
142
|
+
*/
|
|
143
|
+
strictReferences?: boolean;
|
|
137
144
|
timestamp?: string;
|
|
138
145
|
/**
|
|
139
146
|
* ZIP calendar timestamps, including embedded workbooks, use UTC fields at
|
|
@@ -150,21 +157,28 @@ export interface ToPptxOptions {
|
|
|
150
157
|
* PowerPoint updates on open. Without it a current date is reported as unresolved content.
|
|
151
158
|
*/
|
|
152
159
|
date?: string;
|
|
153
|
-
/**
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
160
|
+
/**
|
|
161
|
+
* The catalogs the host registered (core `Catalog[]`, as for opf-render and core's `resolveSlideContext`): each is matched
|
|
162
|
+
* against a document group's `catalogs.<group>.source`, and the first is the host default that bare ids use when the document
|
|
163
|
+
* omits `catalogs.default`. Never fetched. Every reference (slide layouts, themes, colour and font schemes) resolves in the
|
|
164
|
+
* document's embedded records first, then here; omitted or `[]`, only embedded records resolve. Register the gallery snapshot
|
|
165
|
+
* explicitly with `catalogs: [defaultCatalog]` from `@openpresentation/opf/catalog`.
|
|
166
|
+
*/
|
|
167
|
+
catalogs?: readonly Catalog[];
|
|
157
168
|
}
|
|
158
169
|
|
|
159
170
|
export interface FromPptxOptions {
|
|
160
|
-
/** 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 slide layout
|
|
171
|
+
/** 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`, and a footer section text that disagrees with PowerPoint's section list `section-reference-changed` (docs/document-roundtrip.md). */
|
|
161
172
|
onDiagnostic?: (diagnostic: {code: string; path: string; message: string}) => void;
|
|
162
173
|
fallbackName?: string;
|
|
163
174
|
schema?: string;
|
|
164
|
-
/**
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
175
|
+
/**
|
|
176
|
+
* The catalogs the host registered (core `Catalog[]`, as in ToPptxOptions; never fetched). Theme and colour-scheme recovery
|
|
177
|
+
* (FF-24) matches the package's theme against the first one, the host default: an exact colour-scheme match imports as its
|
|
178
|
+
* bare id and a corroborated theme name as `design.theme`. Without catalogs the colour scheme imports as an inline object and
|
|
179
|
+
* no theme id is recovered. A stored slide layout reference also resolves here.
|
|
180
|
+
*/
|
|
181
|
+
catalogs?: readonly Catalog[];
|
|
168
182
|
/**
|
|
169
183
|
* Opt in to raw per-shape signals (see PptxSignals). `true` uses the default limits; an object lowers or raises them.
|
|
170
184
|
* With it, `fromPptx` resolves to `{presentation, signals}`; without it (the default, also `false`/`null`) it resolves
|
|
@@ -178,8 +192,8 @@ export declare class OPFPptxError extends Error {
|
|
|
178
192
|
readonly details: Record<string, unknown>;
|
|
179
193
|
/** The `error` findings of the `format` check that refused the input or the imported presentation (`invalid-opf`, `invalid-import-opf`), or the variable errors (`unfilled-variables`, `invalid-variables`). */
|
|
180
194
|
readonly findings?: Finding[];
|
|
181
|
-
/** The layout diagnostics behind a `layout-overflow` error. */
|
|
182
|
-
readonly diagnostics?: LayoutDiagnostic[];
|
|
195
|
+
/** The layout diagnostics behind a `layout-overflow` error, or core's `unresolved-reference` diagnostics behind a strict-export `unresolved-reference` error. */
|
|
196
|
+
readonly diagnostics?: LayoutDiagnostic[] | UnresolvedReferenceDiagnostic[];
|
|
183
197
|
readonly path?: string;
|
|
184
198
|
constructor(code: string, message: string, details?: Record<string, unknown>);
|
|
185
199
|
}
|
|
@@ -390,7 +404,8 @@ export interface PptxOutlineSignal extends PptxColor {
|
|
|
390
404
|
export interface PptxOpfLink {
|
|
391
405
|
/**
|
|
392
406
|
* "block" fed a block (`path`, `blockType`); "title", "subtitle" and "tag" fed that slide field. Other roles name a shape the
|
|
393
|
-
* importer consumed without a block of its own: "furniture" (footer, date, number, section), "
|
|
407
|
+
* importer consumed without a block of its own: "furniture" (footer, date, number, section), "background" (the picture of an image background),
|
|
408
|
+
* "image-overlay" (the overlay shape of an image background or image block), "watermark", "logo",
|
|
394
409
|
* and the members of an OPF-tagged group ("code", "metric", "quote", "timeline", "media", "card-frame").
|
|
395
410
|
*/
|
|
396
411
|
role: string;
|
|
@@ -489,7 +504,7 @@ export type ThemeScriptSelection = Partial<Record<"major" | "minor", Partial<Rec
|
|
|
489
504
|
export type TypefaceViolationReason =
|
|
490
505
|
| "foreign-typeface"
|
|
491
506
|
| "foreign-theme-reference"
|
|
492
|
-
| "
|
|
507
|
+
| "theme-reference-unresolved"
|
|
493
508
|
| "empty-theme-reference"
|
|
494
509
|
| "empty-typeface"
|
|
495
510
|
| "foreign-script-supplement"
|