@openpresentation/opf-pptx 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEPENDENCY-NOTES.md +1 -1
- package/README.md +16 -7
- package/dist/chart-types.js +183 -0
- package/dist/chart-workbook.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +228 -95
- package/dist/media-dedupe.js +44 -0
- package/dist/theme-colors.js +92 -6
- package/dist/watermark-provenance.js +146 -0
- package/package.json +3 -3
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// PptxGenJS embeds one media part per picture, so a picture repeated on many
|
|
2
|
+
// slides repeats its bytes. Identical media parts collapse into the first one
|
|
3
|
+
// (in path order); every relationship that pointed at a removed part is
|
|
4
|
+
// re-targeted, so each slide still owns its own relationship and picture.
|
|
5
|
+
const hashBytes = bytes => {
|
|
6
|
+
// Bucket key only (cyrb53); equal buckets are confirmed byte for byte.
|
|
7
|
+
let h1 = 0xdeadbeef, h2 = 0x41c6ce57;
|
|
8
|
+
for (let index = 0; index < bytes.byteLength; index += 1) {
|
|
9
|
+
h1 = Math.imul(h1 ^ bytes[index], 2654435761);
|
|
10
|
+
h2 = Math.imul(h2 ^ bytes[index], 1597334677);
|
|
11
|
+
}
|
|
12
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
13
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
14
|
+
return (h2 >>> 0).toString(16) + (h1 >>> 0).toString(16);
|
|
15
|
+
};
|
|
16
|
+
const sameBytes = (a, b) => a.byteLength === b.byteLength && a.every((value, index) => value === b[index]);
|
|
17
|
+
const decoder = new TextDecoder(), encoder = new TextEncoder();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Remove duplicate ppt/media parts from entries (and their metadata), and
|
|
21
|
+
* re-target relationships. resolve(sourcePart, target) gives a package path.
|
|
22
|
+
* Returns the Map of removed path to the part that replaced it.
|
|
23
|
+
*/
|
|
24
|
+
export function dedupeMedia(entries, metadata, resolve) {
|
|
25
|
+
const canonical = new Map(), buckets = new Map();
|
|
26
|
+
for (const path of Object.keys(entries).filter(path => /^ppt\/media\/[^/]+$/.test(path)).sort()) {
|
|
27
|
+
const bytes = entries[path], key = `${bytes.byteLength}:${hashBytes(bytes)}`;
|
|
28
|
+
const match = (buckets.get(key) ?? []).find(candidate => sameBytes(entries[candidate], bytes));
|
|
29
|
+
if (match) canonical.set(path, match);
|
|
30
|
+
else buckets.set(key, [...(buckets.get(key) ?? []), path]);
|
|
31
|
+
}
|
|
32
|
+
if (!canonical.size) return canonical;
|
|
33
|
+
for (const part of Object.keys(entries).filter(path => path.endsWith('.rels'))) {
|
|
34
|
+
const source = part.replace(/(^|\/)_rels\/([^/]*)\.rels$/, '$1$2');
|
|
35
|
+
const xml = decoder.decode(entries[part]);
|
|
36
|
+
const next = xml.replace(/\bTarget="([^"]+)"/g, (attribute, target) => {
|
|
37
|
+
const replacement = canonical.get(resolve(source, target));
|
|
38
|
+
return replacement ? `Target="${target.slice(0, target.lastIndexOf('/') + 1)}${replacement.slice(replacement.lastIndexOf('/') + 1)}"` : attribute;
|
|
39
|
+
});
|
|
40
|
+
if (next !== xml) entries[part] = encoder.encode(next);
|
|
41
|
+
}
|
|
42
|
+
for (const path of canonical.keys()) { delete entries[path]; metadata?.delete(path); }
|
|
43
|
+
return canonical;
|
|
44
|
+
}
|
package/dist/theme-colors.js
CHANGED
|
@@ -102,7 +102,9 @@ function backgroundSlot(definition) {
|
|
|
102
102
|
* `vendor` limits the result to scheme values PptxGenJS 4.0.1 can emit.
|
|
103
103
|
*/
|
|
104
104
|
export function schemeColorValue(reference, hex, context, {vendor = false} = {}) {
|
|
105
|
-
|
|
105
|
+
// A slide with its own color scheme is pinned to literal colors: a theme edit
|
|
106
|
+
// in PowerPoint must not partially recolor it, even where a value coincides.
|
|
107
|
+
if (typeof reference !== 'string' || context.schemeOverride) return undefined;
|
|
106
108
|
const name = reference.trim();
|
|
107
109
|
const resolved = sixDigitHex(hex);
|
|
108
110
|
const deck = context.themeColors;
|
|
@@ -120,17 +122,73 @@ export function schemeColorValue(reference, hex, context, {vendor = false} = {})
|
|
|
120
122
|
return undefined;
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
function backgroundOpacity(definition) {
|
|
126
|
+
const opacity = typeof definition === 'object' && Number.isFinite(definition?.opacity) ? definition.opacity : 1;
|
|
127
|
+
return Math.max(0, Math.min(1, opacity));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Scheme value of a theme-slot slide background, or undefined when the background stays literal. */
|
|
131
|
+
export function schemeBackgroundValue(definition, context) {
|
|
132
|
+
const slot = backgroundSlot(definition);
|
|
133
|
+
return slot ? schemeColorValue(slot, context.colors?.background, context) : undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
123
136
|
/** Native slide background fill for a theme-slot background, or null when it must stay literal. */
|
|
124
137
|
export function schemeBackgroundFill(definition, context) {
|
|
125
|
-
const
|
|
126
|
-
if (!slot) return null;
|
|
127
|
-
const value = schemeColorValue(slot, context.colors?.background, context);
|
|
138
|
+
const value = schemeBackgroundValue(definition, context);
|
|
128
139
|
if (!value) return null;
|
|
129
|
-
const
|
|
130
|
-
const alpha = Math.round(Math.max(0, Math.min(1, opacity)) * 100000);
|
|
140
|
+
const alpha = Math.round(backgroundOpacity(definition) * 100000);
|
|
131
141
|
return `<a:solidFill>${alpha === 100000 ? `<a:schemeClr val="${value}"/>` : `<a:schemeClr val="${value}"><a:alpha val="${alpha}"/></a:schemeClr>`}</a:solidFill>`;
|
|
132
142
|
}
|
|
133
143
|
|
|
144
|
+
// The exporter picks default text for contrast with the slide background:
|
|
145
|
+
// light1 on a dark background, else the scheme text (dark1); muted text is
|
|
146
|
+
// dark2/light2. When the background itself is a theme reference, the matching
|
|
147
|
+
// text slot is the semantic pair, so a PowerPoint theme switch moves both.
|
|
148
|
+
const TEXT_PAIRS = Object.freeze({
|
|
149
|
+
light1: {text: 'dark1', muted: 'dark2'}, light2: {text: 'dark1', muted: 'dark2'},
|
|
150
|
+
dark1: {text: 'light1', muted: 'light2'}, dark2: {text: 'light1', muted: 'light2'},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Scheme values for the exporter's default text and muted text on this slide.
|
|
155
|
+
* A role gets a scheme value only when the slide background is an opaque theme
|
|
156
|
+
* reference to a light or dark slot, the slide has no scheme override, and the
|
|
157
|
+
* resolved default color is exactly the paired slot in the deck theme.
|
|
158
|
+
*/
|
|
159
|
+
export function defaultTextSchemeValues(context) {
|
|
160
|
+
const background = schemeBackgroundValue(context.backgroundDefinition, context);
|
|
161
|
+
if (!background || backgroundOpacity(context.backgroundDefinition) !== 1) return {};
|
|
162
|
+
const pair = TEXT_PAIRS[backgroundSlot(context.backgroundDefinition)];
|
|
163
|
+
if (!pair) return {};
|
|
164
|
+
const values = {};
|
|
165
|
+
for (const role of ['text', 'muted']) {
|
|
166
|
+
const hex = role === 'text' ? context.colors?.text : context.colors?.mutedText;
|
|
167
|
+
const value = schemeColorValue(pair[role], hex, context);
|
|
168
|
+
if (value) values[role] = value;
|
|
169
|
+
}
|
|
170
|
+
return values;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Scheme value for the exporter's default text in a table cell whose fill is a
|
|
175
|
+
* theme reference (`fillValue`, from schemeColorValue). A light or dark fill (bg1,
|
|
176
|
+
* bg2, tx1, tx2) pairs with the opposite text slot (dark1 on bg, light1 on tx).
|
|
177
|
+
* An accent fill has contrast-selected text, which stays a theme reference only
|
|
178
|
+
* when the selection is exactly the deck light1 or dark1. Anything else, and any
|
|
179
|
+
* fill that is not a theme reference, keeps literal text.
|
|
180
|
+
*/
|
|
181
|
+
export function tableTextSchemeValue(fillValue, hex, context) {
|
|
182
|
+
if (!fillValue) return undefined;
|
|
183
|
+
const light = /^bg[12]$/.test(fillValue), dark = /^tx[12]$/.test(fillValue);
|
|
184
|
+
const candidates = light ? ['dark1'] : dark ? ['light1'] : /^accent[1-6]$/.test(fillValue) ? ['light1', 'dark1'] : [];
|
|
185
|
+
for (const slot of candidates) {
|
|
186
|
+
const value = schemeColorValue(slot, hex, context, {vendor: true});
|
|
187
|
+
if (value) return value;
|
|
188
|
+
}
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
134
192
|
// ---------------------------------------------------------------------------
|
|
135
193
|
// Import
|
|
136
194
|
|
|
@@ -202,3 +260,31 @@ export function recoverTheme({themeName, colors, majorFont, minorFont}, {themes,
|
|
|
202
260
|
const fontMatch = !!fonts && fonts.heading === majorFont && fonts.body === minorFont;
|
|
203
261
|
return colorMatch || fontMatch ? {id: theme.id} : {unverified: theme.id};
|
|
204
262
|
}
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Slide master
|
|
266
|
+
|
|
267
|
+
/** a:solidFill child for a pptx color value: a scheme value or RRGGBB. */
|
|
268
|
+
export function solidColorXml(value) {
|
|
269
|
+
return /^(?:tx[12]|bg[12]|accent[1-6]|hlink|folHlink)$/.test(value) ? `<a:schemeClr val="${value}"/>` : `<a:srgbClr val="${value}"/>`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Give the slide master the deck's theme background and matching default text
|
|
274
|
+
* so slides added in PowerPoint follow the chosen theme. The vendored master
|
|
275
|
+
* has no background (its layout uses the bg1 background style) and tx1 text.
|
|
276
|
+
*/
|
|
277
|
+
export function writeMasterBackground(xml, {fill, text}) {
|
|
278
|
+
let result = xml.replace(/<p:bg>[\s\S]*?<\/p:bg>/, '');
|
|
279
|
+
result = result.replace(/<p:cSld\b([^>]*)>/, `<p:cSld$1><p:bg><p:bgPr>${fill}<a:effectLst/></p:bgPr></p:bg>`);
|
|
280
|
+
if (text && text !== 'tx1') {
|
|
281
|
+
result = result.replace(/<p:txStyles>[\s\S]*?<\/p:txStyles>/, styles =>
|
|
282
|
+
styles.split('<a:solidFill><a:schemeClr val="tx1"/></a:solidFill>').join(`<a:solidFill>${solidColorXml(text)}</a:solidFill>`));
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** The vendored layout's own bg1 background would hide the master background. */
|
|
288
|
+
export function inheritLayoutBackground(xml) {
|
|
289
|
+
return xml.replace(/<p:bg>[\s\S]*?<\/p:bg>/, '');
|
|
290
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {XMLParser} from 'fast-xml-parser';
|
|
2
|
+
import {decodeTextTag, encodeTextTag} from './code-provenance.js';
|
|
3
|
+
import {pictureTransform} from './image-geometry.js';
|
|
4
|
+
|
|
5
|
+
// design.watermark exports as one native picture per slide, placed first in the
|
|
6
|
+
// shape tree (beneath content) at the preview's frame: the centered 40% of the
|
|
7
|
+
// slide, fitted without cropping. Opacity is a native a:alphaModFix on the
|
|
8
|
+
// picture's blip. The tag records the exact native picture, so an unchanged
|
|
9
|
+
// picture imports back as design.watermark while an edited one stays ordinary.
|
|
10
|
+
const TAG = 'OPF_WATERMARK_V1', REL = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags';
|
|
11
|
+
const EMUS_PER_INCH = 914400;
|
|
12
|
+
export const WATERMARK_DEFAULT_OPACITY = 0.08;
|
|
13
|
+
const parser = new XMLParser({ignoreAttributes:false, attributeNamePrefix:'', parseTagValue:false, trimValues:false});
|
|
14
|
+
const decoder = new TextDecoder('utf-8', {fatal:true}), encoder = new TextEncoder();
|
|
15
|
+
const array = value => value === undefined ? [] : Array.isArray(value) ? value : [value];
|
|
16
|
+
const canonical = value => JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item)
|
|
17
|
+
? Object.fromEntries(Object.keys(item).sort().map(key => [key, item[key]])) : item);
|
|
18
|
+
|
|
19
|
+
export const watermarkName = () => 'OPF watermark';
|
|
20
|
+
|
|
21
|
+
// The preview's frame: x/y at 30% and a 40% by 40% box, in the slide's units.
|
|
22
|
+
export const watermarkBox = (width, height) => ({x: width * .3, y: height * .3, w: width * .4, h: height * .4});
|
|
23
|
+
|
|
24
|
+
// The preview reads opacity from an object form only; a string form is 0.08.
|
|
25
|
+
export function watermarkOpacity(value) {
|
|
26
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return WATERMARK_DEFAULT_OPACITY;
|
|
27
|
+
const opacity = value.opacity ?? WATERMARK_DEFAULT_OPACITY;
|
|
28
|
+
return typeof opacity === 'number' && Number.isFinite(opacity) ? Math.min(1, Math.max(0, opacity)) : WATERMARK_DEFAULT_OPACITY;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function blipFillIdentity(blipFill) {
|
|
32
|
+
const {['a:blip']: blip, ...rest} = blipFill ?? {};
|
|
33
|
+
const {['r:embed']: _embed, ...blipRest} = blip ?? {};
|
|
34
|
+
return {...rest, 'a:blip': blipRest};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Place each generated watermark picture at its frame, apply opacity, and bind
|
|
39
|
+
* the picture to its manifest with a native tag.
|
|
40
|
+
* marks: Map<slidePart, {slide, box (inches), opacity, path}>.
|
|
41
|
+
*/
|
|
42
|
+
export function placeWatermarks(entries, marks, metadataFor, fail) {
|
|
43
|
+
if (!marks.size) return;
|
|
44
|
+
const manifests = new Map();
|
|
45
|
+
for (const part of Object.keys(entries).filter(path => /^ppt\/slides\/slide\d+\.xml$/.test(path))) {
|
|
46
|
+
const mark = marks.get(part);
|
|
47
|
+
if (!mark) continue;
|
|
48
|
+
const xml = decoder.decode(entries[part]);
|
|
49
|
+
const emu = value => Math.round(value * EMUS_PER_INCH);
|
|
50
|
+
let found = 0;
|
|
51
|
+
const next = xml.replace(/<p:pic>[\s\S]*?<\/p:pic>/g, picture => {
|
|
52
|
+
if (picture.match(/<p:cNvPr\b[^>]*\bname="([^"]+)"/)?.[1] !== watermarkName()) return picture;
|
|
53
|
+
found++;
|
|
54
|
+
const embed = picture.match(/<a:blip\b[^>]*r:embed="([^"]+)"/)?.[1];
|
|
55
|
+
const metadata = metadataFor(part, embed);
|
|
56
|
+
if (!metadata) fail(mark.path);
|
|
57
|
+
const placed = pictureTransform(metadata, mark.box, 'fit');
|
|
58
|
+
const attributes = `${placed.rotation ? ` rot="${placed.rotation * 60000}"` : ''}${placed.flipH ? ' flipH="1"' : ''}${placed.flipV ? ' flipV="1"' : ''}`;
|
|
59
|
+
picture = picture.replace(/<a:xfrm\b[^>]*>[\s\S]*?<\/a:xfrm>/, `<a:xfrm${attributes}><a:off x="${emu(placed.x)}" y="${emu(placed.y)}"/><a:ext cx="${emu(placed.w)}" cy="${emu(placed.h)}"/></a:xfrm>`);
|
|
60
|
+
picture = picture.replace(/<a:srcRect\b[^>]*\/>/, '');
|
|
61
|
+
if (mark.opacity < 1) {
|
|
62
|
+
const effect = `<a:alphaModFix amt="${Math.round(mark.opacity * 100000)}"/>`;
|
|
63
|
+
picture = picture.replace(/<a:blip\b([^>]*?)(?:\/>|>([\s\S]*?)<\/a:blip>)/, (_, blipAttributes, inner = '') => `<a:blip${blipAttributes}>${effect}${inner}</a:blip>`);
|
|
64
|
+
}
|
|
65
|
+
const node = parser.parse(picture)['p:pic'];
|
|
66
|
+
manifests.set(part, {v: 1, slide: mark.slide, opacity: mark.opacity, properties: node['p:spPr'], blipFill: blipFillIdentity(node['p:blipFill'])});
|
|
67
|
+
return picture;
|
|
68
|
+
});
|
|
69
|
+
if (found !== 1) throw new Error('Missing generated watermark picture.');
|
|
70
|
+
entries[part] = encoder.encode(next);
|
|
71
|
+
}
|
|
72
|
+
attachTags(entries, manifests);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// One tag part per slide, related from the picture's application properties.
|
|
76
|
+
// (attachTextTags is keyed by shape name, and every slide's watermark shares one.)
|
|
77
|
+
function attachTags(entries, manifests) {
|
|
78
|
+
const NS = 'http://schemas.openxmlformats.org/presentationml/2006/main', types = [];
|
|
79
|
+
let count = 0;
|
|
80
|
+
for (const [part, manifest] of manifests) {
|
|
81
|
+
const relPath = part.replace('/slides/', '/slides/_rels/') + '.rels';
|
|
82
|
+
const rels = decoder.decode(entries[relPath]);
|
|
83
|
+
const ids = new Set([...rels.matchAll(/\bId="([^"]+)"/g)].map(match => match[1]));
|
|
84
|
+
const tagPart = `ppt/tags/opfWatermark${++count}.xml`;
|
|
85
|
+
let id = `rIdOpfWatermark${count}`;
|
|
86
|
+
while (ids.has(id)) id += '_';
|
|
87
|
+
entries[tagPart] = encoder.encode(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><p:tagLst xmlns:p="${NS}"><p:tag name="${TAG}" val="${encodeTextTag(manifest)}"/></p:tagLst>`);
|
|
88
|
+
types.push(`<Override PartName="/${tagPart}" ContentType="application/vnd.openxmlformats-officedocument.presentationml.tags+xml"/>`);
|
|
89
|
+
entries[relPath] = encoder.encode(rels.replace('</Relationships>', `<Relationship Id="${id}" Type="${REL}" Target="../tags/opfWatermark${count}.xml"/></Relationships>`));
|
|
90
|
+
let attached = false;
|
|
91
|
+
const xml = decoder.decode(entries[part]).replace(/<p:pic>[\s\S]*?<\/p:pic>/g, picture => {
|
|
92
|
+
if (attached || picture.match(/<p:cNvPr\b[^>]*\bname="([^"]+)"/)?.[1] !== watermarkName()) return picture;
|
|
93
|
+
attached = true;
|
|
94
|
+
picture = picture.replace(/<p:nvPr\s*\/>/, '<p:nvPr></p:nvPr>');
|
|
95
|
+
if (!picture.includes('</p:nvPr>')) throw new Error('Generated watermark picture has no native application properties.');
|
|
96
|
+
return picture.replace('</p:nvPr>', `<p:custDataLst><p:tags r:id="${id}"/></p:custDataLst></p:nvPr>`);
|
|
97
|
+
});
|
|
98
|
+
entries[part] = encoder.encode(xml);
|
|
99
|
+
}
|
|
100
|
+
entries['[Content_Types].xml'] = encoder.encode(decoder.decode(entries['[Content_Types].xml']).replace('</Types>', types.join('') + '</Types>'));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function readTags(container, relationships, entries) {
|
|
104
|
+
const tags = [];
|
|
105
|
+
let unreadable = false;
|
|
106
|
+
for (const link of array(container?.['p:tags'])) {
|
|
107
|
+
const rel = relationships.get(link['r:id']);
|
|
108
|
+
if (rel?.type !== REL || rel.targetMode === 'External' || !entries[rel.path]) { unreadable = true; continue; }
|
|
109
|
+
try { tags.push(...array(parser.parse(decoder.decode(entries[rel.path]))['p:tagLst']?.['p:tag'])); } catch { unreadable = true; }
|
|
110
|
+
}
|
|
111
|
+
return {tags, unreadable};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Recover design.watermark from an unchanged tagged picture. Returns the
|
|
116
|
+
* consumed picture indexes and the slide design fields to restore.
|
|
117
|
+
* readPicture(picture) returns the ordinary imported image payload.
|
|
118
|
+
*/
|
|
119
|
+
export function importWatermark(pictures, relationships, entries, slideIndex, readPicture, report) {
|
|
120
|
+
const consumed = new Set(), found = [];
|
|
121
|
+
for (const [index, picture] of pictures.entries()) {
|
|
122
|
+
const {tags, unreadable} = readTags(picture['p:nvPicPr']?.['p:nvPr']?.['p:custDataLst'], relationships, entries);
|
|
123
|
+
const own = tags.filter(tag => tag.name?.toUpperCase() === TAG);
|
|
124
|
+
if (!own.length) continue;
|
|
125
|
+
found.push({index, picture, own, unreadable, others: tags.some(tag => /^OPF_/i.test(tag.name) && tag.name.toUpperCase() !== TAG)});
|
|
126
|
+
}
|
|
127
|
+
if (!found.length) return {consumed};
|
|
128
|
+
const invalid = () => report({code: 'invalid-watermark-provenance', message: 'An edited, ambiguous or invalid tagged OPF watermark was imported as an ordinary picture. It is not reconstructed as design.watermark.'});
|
|
129
|
+
if (found.length > 1) { invalid(); return {consumed}; }
|
|
130
|
+
const [{index, picture, own, unreadable, others}] = found;
|
|
131
|
+
try {
|
|
132
|
+
if (unreadable || others || own.length !== 1) throw Error('Ambiguous watermark identity.');
|
|
133
|
+
const manifest = decodeTextTag(own[0].val);
|
|
134
|
+
if (manifest?.v !== 1 || manifest.slide !== `slides.${slideIndex}` || typeof manifest.opacity !== 'number' || !(manifest.opacity >= 0 && manifest.opacity <= 1)) throw Error('Invalid watermark manifest.');
|
|
135
|
+
if (picture['p:nvPicPr']?.['p:cNvPr']?.name !== watermarkName()) throw Error('Watermark identity changed.');
|
|
136
|
+
if (canonical(picture['p:spPr']) !== canonical(manifest.properties) || canonical(blipFillIdentity(picture['p:blipFill'])) !== canonical(manifest.blipFill)) throw Error('Watermark geometry or opacity changed.');
|
|
137
|
+
const item = readPicture(picture);
|
|
138
|
+
const src = item?.payload?.image?.src;
|
|
139
|
+
if (typeof src !== 'string') throw Error('Watermark bytes are unavailable.');
|
|
140
|
+
consumed.add(index);
|
|
141
|
+
return {consumed, design: {watermark: {src, opacity: manifest.opacity}}};
|
|
142
|
+
} catch {
|
|
143
|
+
invalid();
|
|
144
|
+
return {consumed};
|
|
145
|
+
}
|
|
146
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-pptx",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"description": "Pure local OPF to PPTX export and PPTX to OPF import tooling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"sideEffects": false,
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "node scripts/build.mjs",
|
|
40
|
-
"typecheck": "node --check src/index.js && node --check test/design-alignment.mjs && node --check src/color-ref.js && node --check src/theme-colors.js && node --check test/theme-colors.mjs && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check src/image-geometry.js && node --check src/slide-image-provenance.js && node --check test/slide-image.mjs && node --check test/slide-image-treatments.mjs && node --check test/image-fit.mjs && node --check test/image-orientation.mjs && node --check test/table-layout.mjs && node --check test/table-import.mjs && node --check test/object-ids.mjs && node --check test/export-corpus.mjs && node --check test/image-media-type.mjs && node --check src/image-fallback-node.js && node --check src/image-fallback-browser.js && node --check test/webp-fallback.mjs && node --check scripts/build-browser-check.mjs && node --check test/webp-fallback-browser.js && node --check test/image-fallback-boundary.mjs && node --check src/background.js && node --check test/background.mjs && node --check src/image-import.js && node --check test/image-import.mjs && node --check test/native-background.mjs && node --check src/background-import.js && node --check test/background-inheritance.mjs && node --check test/rich-table.mjs && node --check src/table-import.js && node --check test/rich-table-import.mjs && node --check test/rich-table-import-browser.js && node --check test/table-row-sizing.mjs && node --check test/table-styles.mjs && node --check test/table-styles-browser.js && node --check test/styled-table.mjs && node --check src/table-cell-import.js && node --check test/styled-table-import.mjs && node --check test/styled-table-import-browser.js && node --check src/table-border-import.js && node --check test/table-border-styles.mjs && node --check src/card-provenance.js && node --check test/content-cards.mjs && node --check src/chart-workbook.js && node --check test/chart-colors.mjs && node --check test/chart-workbook.mjs && node --check test/single-column-chart.mjs && node --check test/native-chart-colors.mjs && node --check src/heading-provenance.js && node --check test/accepted-text.mjs && node --check test/heading-provenance.mjs && node --check src/text-provenance.js && node --check test/plain-whitespace.mjs && node --check src/timeline-provenance.js && node --check test/shared-timeline.mjs && node --check test/shared-furniture.mjs && node --check src/furniture-provenance.js && node --check src/document-provenance.js && node --check test/document-provenance.mjs && node --check test/furniture-provenance.mjs && node --check src/furniture-fields.js && node --check test/furniture-fields.mjs && node --check test/furniture-wrapped-date.mjs && node --check test/native-picture.mjs && node --check test/compare-native-picture.mjs && node --check test/compare-native-picture-edit.mjs && node --check test/compare-native-picture-edit-controls.mjs && node --check test/native-font-embed-audit.mjs && node --check test/native-font-embed-controls.mjs && node --check test/native-font-embed-fixture-source.mjs && node --check test/native-font-edit-fixture.mjs && node --check test/native-mixed-edit-audit.mjs && node --check test/native-mixed-edit-controls.mjs && node --check test/master-bullet-font.mjs && node --check test/native-font-inventory-audit.mjs && node --check test/native-font-inventory-controls.mjs && node --check test/default-font-scheme.mjs && node --check test/export-chosen-fonts.mjs && node --check test/background-fills.mjs && node --check src/typeface-inventory.js && node --check src/package-fonts.js && node --check test/typeface-inventory.mjs && node --check src/script-fonts.js && node --check test/script-fonts.mjs && node --check test/powershell-scan.mjs && node --check test/powershell-scan-probes.mjs && node --check test/socials-furniture.mjs && node --check src/media-provenance.js && node --check test/layout-parity.mjs && node --check test/cover-centering.mjs && node --check test/media-placeholder.mjs && node --check test/image-placeholder.mjs && node --check test/list-shape-names.mjs && node --check test/layout-intent.mjs && node --check test/chart-cache-import.mjs && node --check test/zip-date.mjs && node --check test/native-text-whitespace.mjs && node --check test/native-underline.mjs && node --check src/native-text-style.js && node --check src/font-weights.js && node --check test/font-weight-import.mjs && node --check src/body-text-import.js && node --check test/native-body-rich-text.mjs && node --check test/native-body-rich-text-browser.js && node --check test/native-quote-import-contract.mjs && node --check test/native-quote-import-controls.mjs && node --check test/native-quote.mjs && node --check test/determinism.mjs && node --check test/determinism-fixtures.mjs && node --check test/table-frame-furniture-order.mjs && node --check test/shape-border-widths.mjs",
|
|
41
|
-
"test": "npm run build && node test/zip-date.mjs && node test/background-fills.mjs && npm run test:furniture && node test/slide-image.mjs && node test/slide-image-treatments.mjs && node test/document-provenance.mjs && node test/layout-intent.mjs && node test/master-bullet-font.mjs && node test/default-font-scheme.mjs && node test/script-fonts.mjs && node test/export-chosen-fonts.mjs && node test/typeface-inventory.mjs && node test/color-ref-export.mjs && node test/theme-colors.mjs && node test/plain-whitespace.mjs && node test/native-text-whitespace.mjs && node test/native-body-rich-text.mjs && node test/native-underline.mjs && node test/font-weight-import.mjs && node test/design-alignment.mjs && node test/accepted-text.mjs && node test/heading-provenance.mjs && node test/native-quote-import-controls.mjs && node test/chart-colors.mjs && node test/content-cards.mjs && node test/chart-workbook.mjs && node test/single-column-chart.mjs && node --test test/chart-cache-import.mjs && node test/dependency-boundary.mjs && npm run build:browser-check && npm run test:styled-table && node test/content-layout.mjs && node test/layout-parity.mjs && node test/cover-centering.mjs && node test/table-frame-furniture-order.mjs && node test/media-placeholder.mjs && node test/shape-border-widths.mjs && node test/image-placeholder.mjs && node test/list-shape-names.mjs && node test/shared-quote.mjs && npm run test:timeline && npm run test:determinism",
|
|
40
|
+
"typecheck": "node --check src/index.js && node --check src/chart-types.js && node --check test/chart-types.mjs && node --check test/design-alignment.mjs && node --check src/color-ref.js && node --check src/theme-colors.js && node --check test/theme-colors.mjs && node --check test/table-theme-colors.mjs && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check src/image-geometry.js && node --check src/slide-image-provenance.js && node --check test/slide-image.mjs && node --check test/slide-image-treatments.mjs && node --check src/watermark-provenance.js && node --check src/media-dedupe.js && node --check test/media-dedupe.mjs && node --check test/watermark.mjs && node --check test/image-fit.mjs && node --check test/image-orientation.mjs && node --check test/table-layout.mjs && node --check test/table-import.mjs && node --check test/object-ids.mjs && node --check test/export-corpus.mjs && node --check test/image-media-type.mjs && node --check src/image-fallback-node.js && node --check src/image-fallback-browser.js && node --check test/webp-fallback.mjs && node --check scripts/build-browser-check.mjs && node --check test/webp-fallback-browser.js && node --check test/image-fallback-boundary.mjs && node --check src/background.js && node --check test/background.mjs && node --check src/image-import.js && node --check test/image-import.mjs && node --check test/native-background.mjs && node --check src/background-import.js && node --check test/background-inheritance.mjs && node --check test/rich-table.mjs && node --check src/table-import.js && node --check test/rich-table-import.mjs && node --check test/rich-table-import-browser.js && node --check test/table-row-sizing.mjs && node --check test/table-styles.mjs && node --check test/table-styles-browser.js && node --check test/styled-table.mjs && node --check src/table-cell-import.js && node --check test/styled-table-import.mjs && node --check test/styled-table-import-browser.js && node --check src/table-border-import.js && node --check test/table-border-styles.mjs && node --check src/card-provenance.js && node --check test/content-cards.mjs && node --check src/chart-workbook.js && node --check test/chart-colors.mjs && node --check test/chart-workbook.mjs && node --check test/single-column-chart.mjs && node --check test/native-chart-colors.mjs && node --check src/heading-provenance.js && node --check test/accepted-text.mjs && node --check test/heading-provenance.mjs && node --check src/text-provenance.js && node --check test/plain-whitespace.mjs && node --check src/timeline-provenance.js && node --check test/shared-timeline.mjs && node --check test/shared-furniture.mjs && node --check src/furniture-provenance.js && node --check src/document-provenance.js && node --check test/document-provenance.mjs && node --check test/furniture-provenance.mjs && node --check src/furniture-fields.js && node --check test/furniture-fields.mjs && node --check test/furniture-wrapped-date.mjs && node --check test/native-picture.mjs && node --check test/compare-native-picture.mjs && node --check test/compare-native-picture-edit.mjs && node --check test/compare-native-picture-edit-controls.mjs && node --check test/native-font-embed-audit.mjs && node --check test/native-font-embed-controls.mjs && node --check test/native-font-embed-fixture-source.mjs && node --check test/native-font-edit-fixture.mjs && node --check test/native-mixed-edit-audit.mjs && node --check test/native-mixed-edit-controls.mjs && node --check test/master-bullet-font.mjs && node --check test/native-font-inventory-audit.mjs && node --check test/native-font-inventory-controls.mjs && node --check test/default-font-scheme.mjs && node --check test/export-chosen-fonts.mjs && node --check test/background-fills.mjs && node --check src/typeface-inventory.js && node --check src/package-fonts.js && node --check test/typeface-inventory.mjs && node --check src/script-fonts.js && node --check test/script-fonts.mjs && node --check test/powershell-scan.mjs && node --check test/powershell-scan-probes.mjs && node --check test/socials-furniture.mjs && node --check src/media-provenance.js && node --check test/layout-parity.mjs && node --check test/cover-centering.mjs && node --check test/media-placeholder.mjs && node --check test/image-placeholder.mjs && node --check test/list-shape-names.mjs && node --check test/layout-intent.mjs && node --check test/chart-cache-import.mjs && node --check test/zip-date.mjs && node --check test/native-text-whitespace.mjs && node --check test/native-underline.mjs && node --check src/native-text-style.js && node --check src/font-weights.js && node --check test/font-weight-import.mjs && node --check src/body-text-import.js && node --check test/native-body-rich-text.mjs && node --check test/native-body-rich-text-browser.js && node --check test/native-quote-import-contract.mjs && node --check test/native-quote-import-controls.mjs && node --check test/native-quote.mjs && node --check test/determinism.mjs && node --check test/determinism-fixtures.mjs && node --check test/table-frame-furniture-order.mjs && node --check test/shape-border-widths.mjs",
|
|
41
|
+
"test": "npm run build && node test/chart-types.mjs && node test/zip-date.mjs && node test/background-fills.mjs && npm run test:furniture && node test/slide-image.mjs && node test/slide-image-treatments.mjs && node test/watermark.mjs && node test/media-dedupe.mjs && node test/document-provenance.mjs && node test/layout-intent.mjs && node test/master-bullet-font.mjs && node test/default-font-scheme.mjs && node test/script-fonts.mjs && node test/export-chosen-fonts.mjs && node test/typeface-inventory.mjs && node test/color-ref-export.mjs && node test/theme-colors.mjs && node test/table-theme-colors.mjs && node test/plain-whitespace.mjs && node test/native-text-whitespace.mjs && node test/native-body-rich-text.mjs && node test/native-underline.mjs && node test/font-weight-import.mjs && node test/design-alignment.mjs && node test/accepted-text.mjs && node test/heading-provenance.mjs && node test/native-quote-import-controls.mjs && node test/chart-colors.mjs && node test/content-cards.mjs && node test/chart-workbook.mjs && node test/single-column-chart.mjs && node --test test/chart-cache-import.mjs && node test/dependency-boundary.mjs && npm run build:browser-check && npm run test:styled-table && node test/content-layout.mjs && node test/layout-parity.mjs && node test/cover-centering.mjs && node test/table-frame-furniture-order.mjs && node test/media-placeholder.mjs && node test/shape-border-widths.mjs && node test/image-placeholder.mjs && node test/list-shape-names.mjs && node test/shared-quote.mjs && npm run test:timeline && npm run test:determinism",
|
|
42
42
|
"validate": "node scripts/validate-package.mjs",
|
|
43
43
|
"test:packed": "node test/packed-install.mjs",
|
|
44
44
|
"test:compare-published": "node test/compare-published.mjs",
|