@openpresentation/opf-pptx 0.11.8 → 0.11.9

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.
@@ -0,0 +1,297 @@
1
+ // Native PowerPoint Header & Footer objects (RR-11, docs/native-header-footer.md).
2
+ //
3
+ // PowerPoint's Insert > Header & Footer owns three slide placeholders: a date
4
+ // (`dt`), a footer text (`ftr`) and a slide number (`sldNum`). OPF footer parts
5
+ // that map onto one of those become real placeholders on the slide, with matching
6
+ // placeholders on the slide master and layouts and `p:hf` flags that say which of
7
+ // them the deck uses, so the dialog shows the right state and "Apply to All"
8
+ // works. Everything PowerPoint has no object for (header zones, organization,
9
+ // section, socials, images, a second text/date/number part, multi-line text)
10
+ // stays an ordinary tagged OPF furniture shape.
11
+ //
12
+ // Geometry never moves: a native slide placeholder carries the explicit `a:xfrm`
13
+ // core composed (exactly the box the tagged shape had), so preview, export and the
14
+ // parity harness agree. The master and layout placeholders are the defaults for
15
+ // footers a user adds natively; they sit where the first slide's native part sits,
16
+ // or where core would draw a default footer when no slide has one.
17
+
18
+ import {XMLParser} from 'fast-xml-parser';
19
+ import {DEFAULT_DATE_FORMAT, NATIVE_DATE_FIELDS, formatDate} from './furniture-fields.js';
20
+
21
+ const enc = new TextEncoder(), dec = new TextDecoder('utf-8', {fatal: true});
22
+ const parser = new XMLParser({ignoreAttributes: false, attributeNamePrefix: '', parseTagValue: false, trimValues: false});
23
+ const array = value => value === undefined ? [] : Array.isArray(value) ? value : [value];
24
+ const EMU = 9525;
25
+
26
+ // idx values are PowerPoint's own: layouts number the three footer placeholders
27
+ // 10, 11, 12 and the master 2, 3, 4; a slide placeholder points at its layout by idx.
28
+ export const NATIVE_PLACEHOLDERS = Object.freeze({
29
+ dt: {field: 'date', name: 'Date Placeholder', sz: 'half', idx: 10, masterIdx: 2, zone: 'left', align: 'l'},
30
+ ftr: {field: 'text', name: 'Footer Placeholder', sz: 'quarter', idx: 11, masterIdx: 3, zone: 'center', align: 'ctr'},
31
+ sldNum: {field: 'slideNumber', name: 'Slide Number Placeholder', sz: 'quarter', idx: 12, masterIdx: 4, zone: 'right', align: 'r'},
32
+ });
33
+ const ORDER = ['dt', 'ftr', 'sldNum'];
34
+ const phByField = {date: 'dt', text: 'ftr', slideNumber: 'sldNum'};
35
+ export const isNativePlaceholderType = value => Object.hasOwn(NATIVE_PLACEHOLDERS, value);
36
+ export const nativePlaceholderForField = field => phByField[field];
37
+ const ZONES = ['left', 'center', 'right'];
38
+ const ALIGN = {left: 'l', center: 'ctr', right: 'r'};
39
+
40
+ /**
41
+ * Which accepted core parts of one slide become native placeholders: the first
42
+ * footer text, the first footer date and the first footer slide number whose
43
+ * text fits one accepted line. Returns Map<partIndex, 'dt'|'ftr'|'sldNum'>.
44
+ */
45
+ export function nativeFurnitureParts(layout) {
46
+ const chosen = new Map(), taken = new Set();
47
+ for (const [index, part] of (layout?.parts ?? []).entries()) {
48
+ const ph = phByField[part.field];
49
+ if (part.kind !== 'footer' || part.type !== 'text' || !ph || taken.has(ph)) continue;
50
+ // One native shape holds one line; a part that wraps or breaks stays tagged shapes.
51
+ if (part.fit?.sourceLines?.length !== 1 || part.fit.lines?.length !== 1 || !part.text || part.links?.length) continue;
52
+ taken.add(ph);
53
+ chosen.set(index, ph);
54
+ }
55
+ return chosen;
56
+ }
57
+
58
+ /** Geometry of a default footer part for the master and layouts: its box and alignment in EMU. */
59
+ export function defaultPlaceholderGeometry(part) {
60
+ if (!part?.box) return undefined;
61
+ return {x: Math.round(part.box.x * EMU), y: Math.round(part.box.y * EMU), cx: Math.round(part.box.width * EMU),
62
+ cy: Math.round((part.fit?.lineHeight ?? part.box.height) * EMU), algn: ALIGN[part.alignment] ?? 'l', sz: Math.round((part.fit?.fontSize ?? 16) * .75 * 100)};
63
+ }
64
+
65
+ const xmlEscape = value => String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
66
+ const guid = (kind, n) => `{0F0F2701-${kind}-4000-8000-${n.toString(16).toUpperCase().padStart(12, '0')}}`;
67
+
68
+ /**
69
+ * Turn the recorded generated shapes of each slide into placeholders. `records`
70
+ * maps a generated object name to its placeholder type. Runs after the tags are
71
+ * attached: the shape keeps its name, geometry, runs, fields and tags, and gains
72
+ * `p:ph` (a placeholder is a text shape the layout can supply defaults for).
73
+ * Returns the placeholders in use and the first geometry/alignment seen per type.
74
+ */
75
+ export function attachNativePlaceholders(entries, records) {
76
+ const used = new Set(), first = new Map(), seen = new Set();
77
+ if (!records.size) return {used, first};
78
+ const slides = Object.keys(entries).filter(path => /^ppt\/slides\/slide\d+\.xml$/.test(path)).sort((a, b) => Number(a.match(/\d+/)[0]) - Number(b.match(/\d+/)[0]));
79
+ for (const path of slides) {
80
+ let changed = false;
81
+ const xml = dec.decode(entries[path]).replace(/<p:sp>[\s\S]*?<\/p:sp>/g, shape => {
82
+ const name = shape.match(/<p:cNvPr\b[^>]*\bname="([^"]+)"/)?.[1];
83
+ const ph = records.get(name);
84
+ if (!ph) return shape;
85
+ if (seen.has(name)) throw new Error('Duplicate generated native furniture shape.');
86
+ seen.add(name);
87
+ const properties = NATIVE_PLACEHOLDERS[ph];
88
+ if (!/<p:cNvSpPr\b[^>]*\/>/.test(shape)) throw new Error('Generated furniture shape has no plain shape properties.');
89
+ shape = shape.replace(/<p:cNvSpPr\b([^>]*)\/>/, '<p:cNvSpPr$1><a:spLocks noGrp="1"/></p:cNvSpPr>').replace(/<p:nvPr\s*\/>/, '<p:nvPr></p:nvPr>');
90
+ if (!shape.includes('<p:nvPr>')) throw new Error('Generated furniture shape has no native application properties.');
91
+ shape = shape.replace('<p:nvPr>', `<p:nvPr><p:ph type="${ph}" sz="${properties.sz}" idx="${properties.idx}"/>`);
92
+ used.add(ph);
93
+ if (!first.has(ph)) {
94
+ const geometry = shape.match(/<a:off x="(-?\d+)" y="(-?\d+)"\/><a:ext cx="(\d+)" cy="(\d+)"\/>/);
95
+ const algn = shape.match(/<a:pPr\b[^>]*\balgn="(\w+)"/)?.[1] ?? 'l';
96
+ if (geometry) first.set(ph, {x: Number(geometry[1]), y: Number(geometry[2]), cx: Number(geometry[3]), cy: Number(geometry[4]), algn});
97
+ }
98
+ changed = true;
99
+ return shape;
100
+ });
101
+ if (changed) entries[path] = enc.encode(xml);
102
+ }
103
+ if (seen.size !== records.size) throw new Error('Missing generated native furniture shapes.');
104
+ return {used, first};
105
+ }
106
+
107
+ // The text style a footer placeholder defaults to: the run properties of the deck's
108
+ // first native footer shape (else its first furniture shape), read from the finished
109
+ // slide (colors already theme references), so a footer added natively matches the
110
+ // exported ones. A hyperlink belongs to one slide's relationships and is dropped.
111
+ function furnitureRunStyle(slideXmls, names) {
112
+ for (const native of [true, false]) for (const xml of slideXmls) for (const [shape] of xml.matchAll(/<p:sp>[\s\S]*?<\/p:sp>/g)) {
113
+ const name = shape.match(/<p:cNvPr\b[^>]*\bname="(OPF furniture \d+ part \d+ line \d+)"/)?.[1];
114
+ if (!name || names.has(name) !== native) continue;
115
+ const run = shape.match(/<a:(?:r|fld)\b[^>]*>(<a:rPr\b[^>]*\/>|<a:rPr\b[^>]*>[\s\S]*?<\/a:rPr>)/)?.[1];
116
+ if (!run) continue;
117
+ const sz = run.match(/\bsz="(\d+)"/)?.[1];
118
+ const inner = (run.match(/^<a:rPr\b[^>]*>([\s\S]*)<\/a:rPr>$/)?.[1] ?? '')
119
+ .replace(/<a:hlink(Click|MouseOver)\b[^>]*\/>|<a:hlink(Click|MouseOver)\b[^>]*>[\s\S]*?<\/a:hlink(?:Click|MouseOver)>/g, '');
120
+ return {sz: sz ? Number(sz) : undefined, inner};
121
+ }
122
+ return undefined;
123
+ }
124
+
125
+ function placeholderGeometry(ph, info) {
126
+ const native = info.first.get(ph);
127
+ if (native) return native;
128
+ const fallback = info.defaults?.get(ph);
129
+ if (fallback) return fallback;
130
+ // No core geometry at all: PowerPoint's own proportions for a footer band.
131
+ const {width, height} = info.slideSize;
132
+ const band = Math.round(height * .0534), zone = Math.round(width * .26), left = Math.round(width * .07);
133
+ const at = {dt: left, ftr: Math.round(width * .37), sldNum: Math.round(width * .67)}[ph];
134
+ return {x: at, y: Math.round(height * .9), cx: zone, cy: band, algn: NATIVE_PLACEHOLDERS[ph].align};
135
+ }
136
+
137
+ function defRunProperties(style, geometry, extra) {
138
+ const sz = style?.sz ?? geometry.sz ?? 1200;
139
+ return `<a:defRPr sz="${sz}"${extra}>${style?.inner ?? ''}</a:defRPr>`;
140
+ }
141
+
142
+ function fieldRun(ph, id, text) {
143
+ if (ph === 'ftr') return '<a:endParaRPr lang="en-US"/>';
144
+ return `<a:fld id="${id}" type="${ph === 'dt' ? 'datetimeFigureOut' : 'slidenum'}"><a:rPr lang="en-US"/><a:t>${xmlEscape(text)}</a:t></a:fld><a:endParaRPr lang="en-US"/>`;
145
+ }
146
+
147
+ function masterShape(ph, order, info, style) {
148
+ const properties = NATIVE_PLACEHOLDERS[ph], geometry = placeholderGeometry(ph, info), id = order + 2;
149
+ return `<p:sp><p:nvSpPr><p:cNvPr id="${id}" name="${properties.name} ${id - 1}"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>` +
150
+ `<p:nvPr><p:ph type="${ph}" sz="${properties.sz}" idx="${properties.masterIdx}"/></p:nvPr></p:nvSpPr>` +
151
+ `<p:spPr><a:xfrm><a:off x="${geometry.x}" y="${geometry.y}"/><a:ext cx="${geometry.cx}" cy="${geometry.cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>` +
152
+ `<p:txBody><a:bodyPr vert="horz" lIns="0" tIns="0" rIns="0" bIns="0" rtlCol="0" anchor="t"/>` +
153
+ `<a:lstStyle><a:lvl1pPr algn="${geometry.algn}">${defRunProperties(style, geometry, '')}</a:lvl1pPr></a:lstStyle>` +
154
+ `<a:p>${fieldRun(ph, guid('0001', order + 1), ph === 'dt' ? info.dateText : '‹#›')}</a:p></p:txBody></p:sp>`;
155
+ }
156
+
157
+ function layoutShape(ph, order, info) {
158
+ const properties = NATIVE_PLACEHOLDERS[ph], id = order + 2;
159
+ return `<p:sp><p:nvSpPr><p:cNvPr id="${id}" name="${properties.name} ${id - 1}"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>` +
160
+ `<p:nvPr><p:ph type="${ph}" sz="${properties.sz}" idx="${properties.idx}"/></p:nvPr></p:nvSpPr><p:spPr/>` +
161
+ `<p:txBody><a:bodyPr/><a:lstStyle/><a:p>${fieldRun(ph, guid('0002', order + 1), ph === 'dt' ? info.dateText : '‹#›')}</a:p></p:txBody></p:sp>`;
162
+ }
163
+
164
+ const flag = value => value ? '1' : '0';
165
+ const headerFooterFlags = used => `<p:hf sldNum="${flag(used.has('sldNum'))}" hdr="0" ftr="${flag(used.has('ftr'))}" dt="${flag(used.has('dt'))}"/>`;
166
+
167
+ function insertPlaceholders(xml, shapes, part) {
168
+ if (/<p:ph\b[^>]*\btype="(?:dt|ftr|sldNum)"/.test(xml)) throw new Error(`${part} already has footer placeholders.`);
169
+ const end = xml.indexOf('</p:spTree>');
170
+ if (end < 0) throw new Error(`${part} has no shape tree.`);
171
+ return xml.slice(0, end) + shapes + xml.slice(end);
172
+ }
173
+
174
+ /**
175
+ * The master, layout and notes master half of the native header/footer, written
176
+ * into the finished package. `read`/`write` access parts by path.
177
+ *
178
+ * - Slide master and layouts: `dt`, `ftr` and `sldNum` placeholders and a
179
+ * `p:hf` whose flags say which the deck uses (`hdr` is 0: slides have no
180
+ * header placeholder).
181
+ * - Notes master: PowerPoint's own placeholders stay; `p:hf` records that the
182
+ * notes pages carry a page number only, as the generated notes slides do.
183
+ */
184
+ export function writeNativeMasters(paths, read, write, info) {
185
+ const slideXmls = paths.filter(path => /^ppt\/slides\/slide\d+\.xml$/.test(path)).sort((a, b) => Number(a.match(/\d+/)[0]) - Number(b.match(/\d+/)[0])).map(path => dec.decode(read(path)));
186
+ const style = furnitureRunStyle(slideXmls, info.names);
187
+ const flags = headerFooterFlags(info.used);
188
+ for (const path of paths.filter(path => /^ppt\/slideMasters\/slideMaster\d+\.xml$/.test(path))) {
189
+ let xml = dec.decode(read(path));
190
+ xml = insertPlaceholders(xml, ORDER.map((ph, order) => masterShape(ph, order, info, style)).join(''), path);
191
+ xml = /<p:hf\b[^>]*\/>/.test(xml) ? xml.replace(/<p:hf\b[^>]*\/>/, flags) : xml.replace(/<p:txStyles\b/, `${flags}<p:txStyles`);
192
+ if (!xml.includes(flags)) throw new Error('Slide master header/footer flags could not be written.');
193
+ write(path, enc.encode(xml));
194
+ }
195
+ for (const path of paths.filter(path => /^ppt\/slideLayouts\/slideLayout\d+\.xml$/.test(path))) {
196
+ let xml = dec.decode(read(path));
197
+ xml = insertPlaceholders(xml, ORDER.map((ph, order) => layoutShape(ph, order, info)).join(''), path);
198
+ if (!xml.includes('</p:clrMapOvr>')) throw new Error('Slide layout has no color map override.');
199
+ xml = /<p:hf\b[^>]*\/>/.test(xml) ? xml.replace(/<p:hf\b[^>]*\/>/, flags) : xml.replace('</p:clrMapOvr>', `</p:clrMapOvr>${flags}`);
200
+ write(path, enc.encode(xml));
201
+ }
202
+ for (const path of paths.filter(path => /^ppt\/notesMasters\/notesMaster\d+\.xml$/.test(path))) {
203
+ let xml = dec.decode(read(path));
204
+ const notes = '<p:hf hdr="0" ftr="0" dt="0"/>';
205
+ xml = /<p:hf\b[^>]*\/>/.test(xml) ? xml.replace(/<p:hf\b[^>]*\/>/, notes) : xml.replace(/(<p:clrMap\b[^>]*\/>)/, `$1${notes}`);
206
+ if (!xml.includes(notes)) throw new Error('Notes master header/footer flags could not be written.');
207
+ write(path, enc.encode(xml));
208
+ }
209
+ }
210
+
211
+ export const nativeDateText = date => formatDate(date ?? '', DEFAULT_DATE_FORMAT) ?? formatDate('2026-01-01', DEFAULT_DATE_FORMAT);
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // Import
215
+ // ---------------------------------------------------------------------------
216
+
217
+ const dateFormatForField = Object.fromEntries(Object.entries(NATIVE_DATE_FIELDS).map(([format, type]) => [type, format]));
218
+ const REL_LAYOUT = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout';
219
+ const REL_MASTER = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster';
220
+
221
+ function xfrmBounds(xfrm) {
222
+ const x = Number(xfrm?.['a:off']?.x), y = Number(xfrm?.['a:off']?.y), cx = Number(xfrm?.['a:ext']?.cx), cy = Number(xfrm?.['a:ext']?.cy);
223
+ return [x, y, cx, cy].every(Number.isFinite) && cx > 0 ? {x, y, cx, cy} : undefined;
224
+ }
225
+
226
+ // A placeholder without its own xfrm inherits it: slide -> layout (same idx, else same
227
+ // type) -> master (same type). Parsed layout/master trees are cached per package read.
228
+ function inheritedBounds(shape, ph, slidePath, relationships, readPart) {
229
+ const own = xfrmBounds(shape['p:spPr']?.['a:xfrm']);
230
+ if (own) return own;
231
+ const index = shape['p:nvSpPr']?.['p:nvPr']?.['p:ph']?.idx;
232
+ const find = (path, byIdx) => {
233
+ const tree = readPart(path)?.[path.includes('slideMaster') ? 'p:sldMaster' : 'p:sldLayout']?.['p:cSld']?.['p:spTree'];
234
+ const candidates = array(tree?.['p:sp']).filter(candidate => candidate['p:nvSpPr']?.['p:nvPr']?.['p:ph']?.type === ph);
235
+ const match = (byIdx && index !== undefined ? candidates.find(candidate => candidate['p:nvSpPr']['p:nvPr']['p:ph'].idx === index) : undefined) ?? candidates[0];
236
+ return match ? xfrmBounds(match['p:spPr']?.['a:xfrm']) : undefined;
237
+ };
238
+ const layout = [...relationships(slidePath).values()].find(relationship => relationship.type === REL_LAYOUT)?.path;
239
+ if (!layout) return undefined;
240
+ const fromLayout = find(layout, true);
241
+ if (fromLayout) return fromLayout;
242
+ const master = [...relationships(layout).values()].find(relationship => relationship.type === REL_MASTER)?.path;
243
+ return master ? find(master, false) : undefined;
244
+ }
245
+
246
+ function zoneOf(bounds, slideWidth, fallback) {
247
+ if (!bounds || !(slideWidth > 0)) return fallback;
248
+ const center = (bounds.x + bounds.cx / 2) / slideWidth;
249
+ return center < 1 / 3 ? 'left' : center > 2 / 3 ? 'right' : 'center';
250
+ }
251
+
252
+ /**
253
+ * The footer one slide's native placeholders describe. `claimed` is the set of
254
+ * shape indexes OPF provenance already consumed or reserved. Returns
255
+ * {fields: [{zone, field, value, settings, index}], skipped: [index]} where `value`
256
+ * is the OPF field value and `settings` the zone's `dateFormat`/`slideNumberFormat`.
257
+ */
258
+ export function readNativePlaceholders(context, claimed, relationships, readPart) {
259
+ const fields = [];
260
+ for (const [index, shape] of context.shapes.entries()) {
261
+ const ph = shape['p:nvSpPr']?.['p:nvPr']?.['p:ph']?.type;
262
+ if (!isNativePlaceholderType(ph) || claimed.has(index)) continue;
263
+ const paragraphs = context.paragraphs[index] ?? [];
264
+ const text = paragraphs.map(paragraph => paragraph.text).join('\n');
265
+ const nativeFields = paragraphs.flatMap(paragraph => paragraph.fields ?? []);
266
+ const zone = zoneOf(inheritedBounds(shape, ph, context.path, relationships, readPart), context.slideWidth, NATIVE_PLACEHOLDERS[ph].zone);
267
+ const properties = NATIVE_PLACEHOLDERS[ph];
268
+ if (ph === 'sldNum') {
269
+ const live = nativeFields.filter(field => field.type === 'slidenum');
270
+ if (live.length !== 1) continue;
271
+ const at = text.indexOf(live[0].text);
272
+ const format = at < 0 ? undefined : text.slice(0, at) + '{current}' + text.slice(at + live[0].text.length);
273
+ fields.push({index, zone, field: properties.field, value: true, settings: format && format !== '{current}' ? {slideNumberFormat: format} : {}});
274
+ } else if (ph === 'dt') {
275
+ if (!text.trim()) continue;
276
+ const pattern = nativeFields.length === 1 && nativeFields[0].text === text ? dateFormatForField[nativeFields[0].type] : undefined;
277
+ if (pattern) fields.push({index, zone, field: properties.field, value: true, settings: pattern === DEFAULT_DATE_FORMAT ? {} : {dateFormat: pattern}});
278
+ // A fixed date, or a field with no OPF pattern (a time, the master's datetimeFigureOut), keeps its current words.
279
+ else fields.push({index, zone, field: properties.field, value: text, settings: {}});
280
+ } else {
281
+ if (!text.trim()) continue;
282
+ fields.push({index, zone, field: properties.field, value: text, settings: {}});
283
+ }
284
+ }
285
+ return fields;
286
+ }
287
+
288
+ /** The OPF footer value for native placeholder fields: zones keyed left/center/right. */
289
+ export function footerValue(fields) {
290
+ const value = {};
291
+ for (const item of fields) {
292
+ value[item.zone] ??= {};
293
+ value[item.zone][item.field] = item.value;
294
+ Object.assign(value[item.zone], item.settings);
295
+ }
296
+ return Object.fromEntries(ZONES.filter(zone => value[zone]).map(zone => [zone, value[zone]]));
297
+ }
@@ -1,4 +1,5 @@
1
1
  import {XMLParser} from 'fast-xml-parser';
2
+ import {normalizeCrop, withoutSvgBlip} from './svg-image.js';
2
3
  import {attachTextTags, decodeTextTag} from './code-provenance.js';
3
4
  import {framedPictureTransform} from './image-geometry.js';
4
5
 
@@ -10,8 +11,9 @@ const EMUS_PER_INCH = 914400;
10
11
  const parser = new XMLParser({ignoreAttributes:false, attributeNamePrefix:'', parseTagValue:false, trimValues:false});
11
12
  const decoder = new TextDecoder('utf-8', {fatal:true}), encoder = new TextEncoder();
12
13
  const array = value => value === undefined ? [] : Array.isArray(value) ? value : [value];
14
+ // Whitespace-only text between elements is not identity: the vendored writer indents, PowerPoint drops it on save.
13
15
  const canonical = value => JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item)
14
- ? Object.fromEntries(Object.keys(item).sort().map(key => [key, item[key]])) : item);
16
+ ? Object.fromEntries(Object.keys(item).sort().filter(key => !(key === '#text' && typeof item[key] === 'string' && !item[key].trim())).map(key => [key, item[key]])) : item);
15
17
  const POSITIONS = ['background', 'top', 'bottom', 'left', 'right'];
16
18
 
17
19
  export const slideImageName = slidePath => `OPF slide image ${slidePath}`;
@@ -35,8 +37,8 @@ function blipEffects(effects) {
35
37
 
36
38
  // Native blip fill without its package-local relationship id.
37
39
  function blipFillIdentity(blipFill) {
38
- const {['a:blip']: blip, ...rest} = blipFill ?? {};
39
- const {['r:embed']: _embed, ...blipRest} = blip ?? {};
40
+ const {['a:blip']: blip, ...rest} = normalizeCrop(blipFill) ?? {};
41
+ const {['r:embed']: _embed, ...blipRest} = withoutSvgBlip(blip) ?? {};
40
42
  return {...rest, 'a:blip': blipRest};
41
43
  }
42
44
 
@@ -132,7 +134,7 @@ export function importSlideImage(pictures, shapes, relationships, entries, slide
132
134
  if (manifest?.v !== 1 || manifest.slide !== `slides.${slideIndex}` || !['crop', 'fit'].includes(manifest.fill) || !validTreatment(manifest.treatment, manifest.position)) throw Error('Invalid slide image manifest.');
133
135
  if (manifest.content !== undefined && manifest.content !== true) throw Error('Invalid slide image content flag.');
134
136
  if (picture['p:nvPicPr']?.['p:cNvPr']?.name !== slideImageName(manifest.slide)) throw Error('Slide image identity changed.');
135
- if (canonical(picture['p:spPr']) !== canonical(manifest.properties) || canonical(blipFillIdentity(picture['p:blipFill'])) !== canonical(manifest.blipFill)) throw Error('Slide image geometry or effects changed.');
137
+ if (canonical(picture['p:spPr']) !== canonical(manifest.properties) || canonical(blipFillIdentity(picture['p:blipFill'])) !== canonical(normalizeCrop(manifest.blipFill))) throw Error('Slide image geometry or effects changed.');
136
138
  const item = readPicture(picture);
137
139
  const src = item?.payload?.image?.src;
138
140
  if (typeof src !== 'string') throw Error('Slide image bytes are unavailable.');