@openpresentation/opf-pptx 0.13.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -37
- package/dist/annotation-export.js +1 -1
- package/dist/annotation-provenance.js +4 -0
- package/dist/background-import.js +33 -17
- package/dist/background.js +30 -28
- package/dist/body-text-import.js +16 -4
- package/dist/chart-alt.js +54 -0
- package/dist/chart-data.js +29 -54
- package/dist/chart-highlight.js +113 -0
- package/dist/chart-options.js +46 -24
- package/dist/chart-types.js +61 -75
- package/dist/code-provenance.js +20 -4
- package/dist/color-ref.js +12 -6
- package/dist/data-provenance.js +18 -14
- package/dist/document-provenance.js +208 -129
- package/dist/format-check.js +10 -0
- package/dist/furniture-provenance.js +30 -3
- package/dist/heading-provenance.js +10 -3
- package/dist/image-fallback-node.js +4 -1
- package/dist/image-provenance.js +273 -0
- package/dist/index.d.ts +57 -35
- package/dist/index.js +724 -399
- package/dist/media-provenance.js +3 -3
- package/dist/metric-provenance.js +3 -3
- package/dist/native-text-style.js +2 -0
- package/dist/package-fonts.js +2 -2
- package/dist/quote-provenance.js +70 -26
- package/dist/rich-heading.js +119 -0
- package/dist/run-colors.js +3 -0
- package/dist/script-fonts.js +102 -101
- package/dist/theme-colors.js +16 -11
- package/dist/timeline-provenance.js +17 -6
- package/dist/typeface-inventory.js +8 -8
- package/dist/watermark-provenance.js +65 -3
- package/package.json +4 -4
- package/dist/slide-image-provenance.js +0 -177
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import {XMLParser} from 'fast-xml-parser';
|
|
2
|
-
import {normalizeCrop, withoutSvgBlip} from './svg-image.js';
|
|
3
|
-
import {attachTextTags, decodeTextTag} from './code-provenance.js';
|
|
4
|
-
import {framedPictureTransform} from './image-geometry.js';
|
|
5
|
-
|
|
6
|
-
// A slide-level image (design.slideImage) exports as one native picture. Its
|
|
7
|
-
// tag records the OPF placement and the exact native geometry, so an unchanged
|
|
8
|
-
// picture imports back as design.slideImage while an edited one stays ordinary.
|
|
9
|
-
const TAG = 'OPF_SLIDE_IMAGE_V1', OVERLAY_TAG = 'OPF_SLIDE_IMAGE_OVERLAY_V1', REL = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags';
|
|
10
|
-
const EMUS_PER_INCH = 914400;
|
|
11
|
-
const parser = new XMLParser({ignoreAttributes:false, attributeNamePrefix:'', parseTagValue:false, trimValues:false});
|
|
12
|
-
const decoder = new TextDecoder('utf-8', {fatal:true}), encoder = new TextEncoder();
|
|
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.
|
|
15
|
-
const canonical = value => JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(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);
|
|
17
|
-
const POSITIONS = ['background', 'top', 'bottom', 'left', 'right'];
|
|
18
|
-
|
|
19
|
-
export const slideImageName = slidePath => `OPF slide image ${slidePath}`;
|
|
20
|
-
export const slideImageOverlayName = slidePath => `OPF slide image overlay ${slidePath}`;
|
|
21
|
-
|
|
22
|
-
// DrawingML for the shared treatment: preset geometry (core guide values), a
|
|
23
|
-
// centered solid line, and blip recolor/alpha effects applied in that order.
|
|
24
|
-
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>`;
|
|
25
|
-
const colorXml = ({hex, alpha}, extraAlpha = 1) => {
|
|
26
|
-
const value = Math.round(alpha * extraAlpha * 100000);
|
|
27
|
-
return `<a:srgbClr val="${hex}">${value < 100000 ? `<a:alpha val="${value}"/>` : ''}</a:srgbClr>`;
|
|
28
|
-
};
|
|
29
|
-
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>` : '';
|
|
30
|
-
function blipEffects(effects) {
|
|
31
|
-
let xml = '';
|
|
32
|
-
if (effects?.recolor?.type === 'grayscale') xml += '<a:grayscl/>';
|
|
33
|
-
if (effects?.recolor?.type === 'duotone') xml += `<a:duotone>${colorXml({...effects.recolor.dark, alpha: 1})}${colorXml({...effects.recolor.light, alpha: 1})}</a:duotone>`;
|
|
34
|
-
if (typeof effects?.opacity === 'number') xml += `<a:alphaModFix amt="${Math.round(effects.opacity * 100000)}"/>`;
|
|
35
|
-
return xml;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Native blip fill without its package-local relationship id.
|
|
39
|
-
function blipFillIdentity(blipFill) {
|
|
40
|
-
const {['a:blip']: blip, ...rest} = normalizeCrop(blipFill) ?? {};
|
|
41
|
-
const {['r:embed']: _embed, ...blipRest} = withoutSvgBlip(blip) ?? {};
|
|
42
|
-
return {...rest, 'a:blip': blipRest};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Place each generated slide-image picture at its frame (crop/fit through
|
|
47
|
-
* a:srcRect), then bind the picture to its manifest with a native tag.
|
|
48
|
-
* images: Map<objectName, {slide, box (inches), fill, treatment, path}>.
|
|
49
|
-
*/
|
|
50
|
-
export function placeSlideImages(entries, images, metadataFor, fail) {
|
|
51
|
-
if (!images.size) return;
|
|
52
|
-
const manifests = new Map(), overlays = new Map();
|
|
53
|
-
for (const part of Object.keys(entries).filter(path => /^ppt\/slides\/slide\d+\.xml$/.test(path))) {
|
|
54
|
-
const xml = decoder.decode(entries[part]);
|
|
55
|
-
const next = xml.replace(/<p:pic>[\s\S]*?<\/p:pic>/g, picture => {
|
|
56
|
-
const name = picture.match(/<p:cNvPr\b[^>]*\bname="([^"]+)"/)?.[1];
|
|
57
|
-
const image = images.get(name);
|
|
58
|
-
if (!image) return picture;
|
|
59
|
-
const embed = picture.match(/<a:blip\b[^>]*r:embed="([^"]+)"/)?.[1];
|
|
60
|
-
const metadata = metadataFor(part, embed);
|
|
61
|
-
if (!metadata) fail(image.path);
|
|
62
|
-
const placed = framedPictureTransform(metadata, image.box, image.fill);
|
|
63
|
-
const emu = value => Math.round(value * EMUS_PER_INCH);
|
|
64
|
-
const attributes = `${placed.rotation ? ` rot="${placed.rotation * 60000}"` : ''}${placed.flipH ? ' flipH="1"' : ''}${placed.flipV ? ' flipV="1"' : ''}`;
|
|
65
|
-
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>`);
|
|
66
|
-
picture = picture.replace(/<a:srcRect\b[^>]*\/>/, '');
|
|
67
|
-
if (placed.crop) picture = picture.replace('<a:stretch>', `<a:srcRect l="${placed.crop.l}" t="${placed.crop.t}" r="${placed.crop.r}" b="${placed.crop.b}"/><a:stretch>`);
|
|
68
|
-
const effects = image.effects;
|
|
69
|
-
if (effects) {
|
|
70
|
-
picture = picture.replace(/<a:prstGeom\b[\s\S]*?<\/a:prstGeom>/, presetXml(effects.shape) + lineXml(effects.border));
|
|
71
|
-
const blip = blipEffects(effects);
|
|
72
|
-
if (blip) picture = picture.replace(/<a:blip\b([^>]*?)(?:\/>|>([\s\S]*?)<\/a:blip>)/, (_, attributes, inner = '') => `<a:blip${attributes}>${blip}${inner}</a:blip>`);
|
|
73
|
-
}
|
|
74
|
-
const node = parser.parse(picture)['p:pic'];
|
|
75
|
-
manifests.set(name, {v:1, slide:image.slide, position:image.treatment.position, fill:image.fill, treatment:image.treatment,
|
|
76
|
-
...(image.content === true ? {content:true} : {}),
|
|
77
|
-
properties:node['p:spPr'], blipFill:blipFillIdentity(node['p:blipFill'])});
|
|
78
|
-
return picture;
|
|
79
|
-
});
|
|
80
|
-
const shaped = next.replace(/<p:sp>[\s\S]*?<\/p:sp>/g, shape => {
|
|
81
|
-
const name = shape.match(/<p:cNvPr\b[^>]*\bname="([^"]+)"/)?.[1];
|
|
82
|
-
const image = [...images.values()].find(candidate => slideImageOverlayName(candidate.slide) === name);
|
|
83
|
-
const overlay = image?.effects?.overlay;
|
|
84
|
-
if (!overlay) return shape;
|
|
85
|
-
const emu = value => Math.round(value / 96 * EMUS_PER_INCH);
|
|
86
|
-
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>`);
|
|
87
|
-
const node = parser.parse(shape)['p:sp'];
|
|
88
|
-
overlays.set(name, {v:1, slide:image.slide, properties:node['p:spPr']});
|
|
89
|
-
return shape;
|
|
90
|
-
});
|
|
91
|
-
if (shaped !== xml) entries[part] = encoder.encode(shaped);
|
|
92
|
-
}
|
|
93
|
-
if (manifests.size !== images.size) throw new Error('Missing generated slide image picture.');
|
|
94
|
-
attachTextTags(entries, manifests, TAG, 'opfSlideImage', 'slide image', {pictures:true});
|
|
95
|
-
attachTextTags(entries, overlays, OVERLAY_TAG, 'opfSlideImageOverlay', 'slide image overlay');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function readTags(container, relationships, entries) {
|
|
99
|
-
const tags = [];
|
|
100
|
-
let unreadable = false;
|
|
101
|
-
for (const link of array(container?.['p:tags'])) {
|
|
102
|
-
const rel = relationships.get(link['r:id']);
|
|
103
|
-
if (rel?.type !== REL || rel.targetMode === 'External' || !entries[rel.path]) { unreadable = true; continue; }
|
|
104
|
-
try { tags.push(...array(parser.parse(decoder.decode(entries[rel.path]))['p:tagLst']?.['p:tag'])); } catch { unreadable = true; }
|
|
105
|
-
}
|
|
106
|
-
return {tags, unreadable};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function validTreatment(treatment, position) {
|
|
110
|
-
return treatment && typeof treatment === 'object' && !Array.isArray(treatment) && treatment.position === position
|
|
111
|
-
&& POSITIONS.includes(position) && treatment.src === undefined;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Recover design.slideImage 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 importSlideImage(pictures, shapes, relationships, entries, slideIndex, readPicture, report) {
|
|
120
|
-
const consumed = new Set(), consumedShapes = 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, consumedShapes};
|
|
128
|
-
const invalid = () => report({code:'invalid-slide-image-provenance', message:'An edited, ambiguous or invalid tagged OPF slide image was imported as an ordinary picture. Its native placement is not reconstructed as design.slideImage.'});
|
|
129
|
-
if (found.length > 1) { invalid(); return {consumed, consumedShapes}; }
|
|
130
|
-
const [{index, picture, own, unreadable, others}] = found;
|
|
131
|
-
try {
|
|
132
|
-
if (unreadable || others || own.length !== 1) throw Error('Ambiguous slide image identity.');
|
|
133
|
-
const manifest = decodeTextTag(own[0].val);
|
|
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.');
|
|
135
|
-
if (manifest.content !== undefined && manifest.content !== true) throw Error('Invalid slide image content flag.');
|
|
136
|
-
if (picture['p:nvPicPr']?.['p:cNvPr']?.name !== slideImageName(manifest.slide)) throw Error('Slide image identity 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.');
|
|
138
|
-
const item = readPicture(picture);
|
|
139
|
-
const src = item?.payload?.image?.src;
|
|
140
|
-
if (typeof src !== 'string') throw Error('Slide image bytes are unavailable.');
|
|
141
|
-
consumed.add(index);
|
|
142
|
-
const treatment = {...manifest.treatment};
|
|
143
|
-
if (treatment.overlay !== undefined) {
|
|
144
|
-
// The scrim is a separate native shape; recover it only while it is unchanged.
|
|
145
|
-
const overlay = findOverlay(shapes, relationships, entries, manifest.slide);
|
|
146
|
-
if (overlay === null) {
|
|
147
|
-
delete treatment.overlay;
|
|
148
|
-
report({code:'invalid-slide-image-provenance', message:'The tagged OPF slide image overlay is missing, edited or ambiguous. The slide image is recovered without its overlay; any remaining native shape is imported as ordinary content.'});
|
|
149
|
-
} else consumedShapes.add(overlay);
|
|
150
|
-
}
|
|
151
|
-
// FF-53: a picture that replaced the slide's root `image` (same source as the slide image) restores that
|
|
152
|
-
// payload too, so the round trip keeps `slide.image` with its alt text as well as design.slideImage.
|
|
153
|
-
const alt = item.payload.image.alt;
|
|
154
|
-
const content = manifest.content === true ? {image: {src, ...(typeof alt === 'string' && alt ? {alt} : {})}} : {};
|
|
155
|
-
return {consumed, consumedShapes, design: {slideImage: {...treatment, src}, ...(manifest.fill === 'fit' ? {imageFill: 'fit'} : {})}, ...content};
|
|
156
|
-
} catch {
|
|
157
|
-
invalid();
|
|
158
|
-
return {consumed, consumedShapes};
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function findOverlay(shapes, relationships, entries, slide) {
|
|
163
|
-
const matches = [];
|
|
164
|
-
for (const [index, shape] of shapes.entries()) {
|
|
165
|
-
const {tags, unreadable} = readTags(shape['p:nvSpPr']?.['p:nvPr']?.['p:custDataLst'], relationships, entries);
|
|
166
|
-
const own = tags.filter(tag => tag.name?.toUpperCase() === OVERLAY_TAG);
|
|
167
|
-
if (!own.length) continue;
|
|
168
|
-
try {
|
|
169
|
-
if (unreadable || own.length !== 1 || tags.some(tag => /^OPF_/i.test(tag.name) && tag.name.toUpperCase() !== OVERLAY_TAG)) throw Error('Ambiguous overlay identity.');
|
|
170
|
-
const manifest = decodeTextTag(own[0].val);
|
|
171
|
-
if (manifest?.v !== 1 || manifest.slide !== slide || shape['p:nvSpPr']?.['p:cNvPr']?.name !== slideImageOverlayName(slide)) throw Error('Invalid overlay identity.');
|
|
172
|
-
if (canonical(shape['p:spPr']) !== canonical(manifest.properties)) throw Error('Overlay changed.');
|
|
173
|
-
matches.push(index);
|
|
174
|
-
} catch { return null; }
|
|
175
|
-
}
|
|
176
|
-
return matches.length === 1 ? matches[0] : null;
|
|
177
|
-
}
|