@openpresentation/opf-pptx 0.20.3 → 0.22.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 +65 -3
- package/dist/annotation-provenance.js +11 -2
- package/dist/body-text-import.js +64 -7
- package/dist/chart-scheme-colors.js +47 -0
- package/dist/chart-workbook.js +7 -5
- package/dist/code-provenance.js +34 -4
- package/dist/errors.js +11 -0
- package/dist/furniture-fields.js +14 -1
- package/dist/furniture-provenance.js +13 -5
- package/dist/heading-provenance.js +3 -3
- package/dist/image-provenance.js +65 -7
- package/dist/index.d.ts +37 -1
- package/dist/index.js +145 -70
- package/dist/input-safety.js +115 -0
- package/dist/metric-provenance.js +4 -4
- package/dist/native-body.js +147 -0
- package/dist/native-footer-classification.js +104 -0
- package/dist/native-furniture.js +52 -17
- package/dist/native-text-style.js +7 -2
- package/dist/quote-provenance.js +3 -3
- package/dist/svg-image.js +9 -2
- package/dist/template-layouts.js +30 -14
- package/dist/text-provenance.js +3 -3
- package/dist/timeline-provenance.js +3 -3
- package/dist/typeface-inventory.js +5 -5
- package/package.json +4 -4
package/dist/image-provenance.js
CHANGED
|
@@ -32,6 +32,19 @@ export const IMAGE_TREATMENT_KEYS = Object.freeze(['fit', 'focus', 'shape', 'cor
|
|
|
32
32
|
/** An image background's own fields besides its source. */
|
|
33
33
|
export const BACKGROUND_IMAGE_KEYS = Object.freeze(['alt', 'fit', 'focus', 'opacity', 'recolor', 'overlay']);
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* RR-94: the authored `src` an image block records so a re-import can give it back instead of a data: URI: a relative path
|
|
37
|
+
* (no scheme, not absolute, no `..` segment) or an http(s) URL, as authored. Anything else (a data: URI, an `asset:` reference,
|
|
38
|
+
* a file: URL, an absolute or parent-relative path, which could name any file of the machine that re-exports it) is not recorded.
|
|
39
|
+
*/
|
|
40
|
+
export function recordableImageSource(value) {
|
|
41
|
+
const src = typeof value === 'string' ? value : object(value) && typeof value.src === 'string' ? value.src : undefined;
|
|
42
|
+
if (src === undefined || src.length === 0 || src.length > 2048 || /[\u0000-\u001f\u007f]/.test(src)) return undefined;
|
|
43
|
+
if (/^https?:\/\/[^/\s]+/i.test(src)) return src;
|
|
44
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(src) || /^[\\/]/.test(src) || src.split(/[\\/]/).includes('..')) return undefined;
|
|
45
|
+
return src;
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
export const imageName = number => `OPF image ${number}`;
|
|
36
49
|
export const imageOverlayName = pictureName => `${pictureName} overlay`;
|
|
37
50
|
export const backgroundImageName = slide => `OPF background ${slide}`;
|
|
@@ -78,6 +91,37 @@ function storedFocus({x, y}, orientation) {
|
|
|
78
91
|
|
|
79
92
|
const srcRectValue = value => Math.round(value * 100000) || 0;
|
|
80
93
|
|
|
94
|
+
/** Whether native dimensions can represent the raster's unchanged aspect. */
|
|
95
|
+
export function samePictureAspect(cx, cy, width, height) {
|
|
96
|
+
if (![cx, cy, width, height].every(value => Number.isSafeInteger(value) && value > 0)) return false;
|
|
97
|
+
// Native writers may round an aspect-preserving frame directly to EMUs.
|
|
98
|
+
if (Math.round(cx * height / width) === cy || Math.round(cy * width / height) === cx) return true;
|
|
99
|
+
// pictureFrame passes inches to core fitImage, whose roundBox rounds each
|
|
100
|
+
// dimension to 1e-6 input units, before placeImages rounds to whole EMUs.
|
|
101
|
+
// Invert that actual serialization, rather than allowing a visual tolerance.
|
|
102
|
+
const microinches = extent => {
|
|
103
|
+
// 914400 / 1e6 = 1143 / 1250. A rounded EMU k covers
|
|
104
|
+
// [1250*k-625, 1250*k+625) / 1143 integer microinches.
|
|
105
|
+
const scaled = BigInt(extent) * 1250n;
|
|
106
|
+
const lower = (scaled - 625n + 1142n) / 1143n;
|
|
107
|
+
const upper = (scaled + 625n + 1142n) / 1143n;
|
|
108
|
+
const result = [];
|
|
109
|
+
// Include adjacent integers for a floating-point half-EMU tie, then keep
|
|
110
|
+
// only dimensions that reproduce the writer's actual numeric operation.
|
|
111
|
+
for (let value = lower - 1n; value <= upper; value++) {
|
|
112
|
+
const numeric = Number(value);
|
|
113
|
+
if (numeric > 0 && Number.isSafeInteger(numeric) && Math.round(numeric / 1e6 * EMUS_PER_INCH) === extent) result.push(value);
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
};
|
|
117
|
+
const w = BigInt(width), h = BigInt(height);
|
|
118
|
+
// q=round(x*1e6) represents [q-.5,q+.5). These two half-open
|
|
119
|
+
// intervals must overlap at the raster's exact rational aspect. Strict
|
|
120
|
+
// overlap excludes a boundary where only one rounding cell contains it.
|
|
121
|
+
return microinches(cx).some(x => microinches(cy).some(y =>
|
|
122
|
+
(2n * x - 1n) * h < (2n * y + 1n) * w && (2n * y - 1n) * w < (2n * x + 1n) * h));
|
|
123
|
+
}
|
|
124
|
+
|
|
81
125
|
/**
|
|
82
126
|
* The native frame and crop of a picture: `box` (inches) is core's picture frame, `fit` cover | contain | stretch,
|
|
83
127
|
* `focus` core's focus point, `metadata` the embedded raster (width, height, orientation). `fitImage` is core's shared
|
|
@@ -107,7 +151,7 @@ export function pictureFrame(metadata, {box, fit, focus = {x: .5, y: .5}, shrink
|
|
|
107
151
|
* picture's objectName, or 'background' for the overlay of a native slide background fill; `overlay` (the authored value)
|
|
108
152
|
* is stored in OPF_IMAGE_OVERLAY_V1.
|
|
109
153
|
*/
|
|
110
|
-
export function placeImages(entries, images, overlays, {metadataFor, fail, fitImage}) {
|
|
154
|
+
export function placeImages(entries, images, overlays, {metadataFor, fail, fitImage, mediaKeyFor}) {
|
|
111
155
|
if (!images.size && !overlays.size) return;
|
|
112
156
|
const manifests = new Map(), overlayManifests = new Map(), placed = new Set(), shaped = new Set();
|
|
113
157
|
for (const part of Object.keys(entries).filter(path => /^ppt\/slides\/slide\d+\.xml$/.test(path))) {
|
|
@@ -135,7 +179,9 @@ export function placeImages(entries, images, overlays, {metadataFor, fail, fitIm
|
|
|
135
179
|
placed.add(name);
|
|
136
180
|
if (image.treatment !== undefined) {
|
|
137
181
|
const node = parser.parse(picture)['p:pic'];
|
|
138
|
-
|
|
182
|
+
const media = mediaKeyFor ? mediaKeyFor(part, embed) : undefined;
|
|
183
|
+
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']),
|
|
184
|
+
...(media !== undefined ? {media} : {}), ...(media !== undefined && image.src !== undefined ? {src: image.src} : {})});
|
|
139
185
|
}
|
|
140
186
|
return picture;
|
|
141
187
|
});
|
|
@@ -192,9 +238,9 @@ const pick = (value, keys) => Object.fromEntries(keys.filter(key => Object.hasOw
|
|
|
192
238
|
* readPicture(picture) returns the ordinary imported image payload; `valid(kind, value)` checks a recovered image block
|
|
193
239
|
* (`kind` 'block') or background (`kind` 'background') against the OPF format.
|
|
194
240
|
*/
|
|
195
|
-
export function importImages({pictures, shapes, relationships, entries, slideIndex, readPicture, valid, report}) {
|
|
241
|
+
export function importImages({pictures, shapes, relationships, entries, slideIndex, readPicture, valid, report, mediaKeyOf = () => undefined}) {
|
|
196
242
|
const slide = `slides.${slideIndex}`;
|
|
197
|
-
const blocks = new Map(), consumed = new Set(), consumedShapes = new Set();
|
|
243
|
+
const blocks = new Map(), sources = new Map(), consumed = new Set(), consumedShapes = new Set();
|
|
198
244
|
let background, backgroundOverlay;
|
|
199
245
|
const invalid = (path, message) => report({code: 'invalid-image-provenance', path, message});
|
|
200
246
|
// Overlay shapes first: each names the picture (or the slide background) it belongs to.
|
|
@@ -223,6 +269,10 @@ export function importImages({pictures, shapes, relationships, entries, slideInd
|
|
|
223
269
|
consumedShapes.add(found.index);
|
|
224
270
|
return found.overlay;
|
|
225
271
|
};
|
|
272
|
+
const restoredSource = (manifest, picture) => {
|
|
273
|
+
const key = typeof manifest.media === 'string' ? mediaKeyOf(picture) : undefined;
|
|
274
|
+
return key !== undefined && key === manifest.media && recordableImageSource(manifest.src) === manifest.src ? manifest.src : undefined;
|
|
275
|
+
};
|
|
226
276
|
const edges = new Set();
|
|
227
277
|
for (const [index, picture] of pictures.entries()) {
|
|
228
278
|
let manifest;
|
|
@@ -231,18 +281,24 @@ export function importImages({pictures, shapes, relationships, entries, slideInd
|
|
|
231
281
|
if (manifest === null) continue;
|
|
232
282
|
const name = picture['p:nvPicPr']?.['p:cNvPr']?.name;
|
|
233
283
|
const role = manifest?.role;
|
|
284
|
+
// RR-94: a plain block (no treatment) carries a tag only for its source and its crop to stay quiet. Once its bytes are another
|
|
285
|
+
// picture's (Change Picture), or it was moved or resized, the tag has nothing to give back: it reads as the ordinary picture.
|
|
286
|
+
const plain = role === 'block' && object(manifest?.treatment) && Object.keys(manifest.treatment).length === 0;
|
|
287
|
+
if (plain && typeof manifest.media === 'string' && mediaKeyOf(picture) !== manifest.media) continue;
|
|
234
288
|
const path = role === 'background' ? `${slide}.design.background` : slide;
|
|
235
289
|
try {
|
|
236
290
|
if (manifest?.v !== 1 || manifest.slide !== slide || !['block', 'background'].includes(role) || !object(manifest.treatment)) throw Error('Invalid image manifest.');
|
|
237
291
|
if (role === 'block' ? !/^OPF image \d+$/.test(name ?? '') : name !== backgroundImageName(slide)) throw Error('Image identity changed.');
|
|
238
292
|
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
293
|
} catch {
|
|
294
|
+
if (plain) continue;
|
|
240
295
|
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
296
|
continue;
|
|
242
297
|
}
|
|
243
298
|
if (role === 'background') {
|
|
244
299
|
const item = readPicture(picture);
|
|
245
|
-
|
|
300
|
+
// RR-94: an unchanged picture (its bytes still hash as exported) gives back the authored relative path or URL.
|
|
301
|
+
const src = restoredSource(manifest, picture) ?? item?.payload?.image?.src;
|
|
246
302
|
const fields = pick(manifest.treatment, BACKGROUND_IMAGE_KEYS.filter(key => key !== 'overlay'));
|
|
247
303
|
const overlay = takeOverlay(name, `${path}.overlay`);
|
|
248
304
|
const value = {type: 'image', src, ...fields, ...(overlay ? {overlay} : {})};
|
|
@@ -259,9 +315,11 @@ export function importImages({pictures, shapes, relationships, entries, slideInd
|
|
|
259
315
|
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
316
|
else if (edge !== undefined) edges.add(edge);
|
|
261
317
|
const item = readPicture(picture);
|
|
262
|
-
const
|
|
318
|
+
const source = restoredSource(manifest, picture);
|
|
319
|
+
const candidate = {...item?.payload, ...fields, ...(source !== undefined && object(item?.payload?.image) ? {image: {...item.payload.image, src: source}} : {})};
|
|
263
320
|
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
321
|
blocks.set(index, fields);
|
|
322
|
+
if (source !== undefined && object(item.payload.image)) sources.set(index, source);
|
|
265
323
|
}
|
|
266
324
|
// The overlay of a native slide background fill.
|
|
267
325
|
backgroundOverlay = takeOverlay('background', `${slide}.design.background.overlay`);
|
|
@@ -269,7 +327,7 @@ export function importImages({pictures, shapes, relationships, entries, slideInd
|
|
|
269
327
|
for (const [owner, found] of overlays) if (!found.edited && !consumedShapes.has(found.index) && owner !== 'background') {
|
|
270
328
|
invalid(`${slide}`, 'A tagged OPF image overlay no longer has its picture; it was imported as an ordinary shape.');
|
|
271
329
|
}
|
|
272
|
-
return {blocks, background, backgroundOverlay, backgroundOverlayShape, consumed, consumedShapes};
|
|
330
|
+
return {blocks, sources, background, backgroundOverlay, backgroundOverlayShape, consumed, consumedShapes};
|
|
273
331
|
}
|
|
274
332
|
|
|
275
333
|
// opf-pptx#221: the "Image unavailable" placeholder of an image block whose source could not be embedded (a remote URL is never
|
package/dist/index.d.ts
CHANGED
|
@@ -34,6 +34,13 @@ export interface ImageResolverContext {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export interface MediaProvenanceDiagnostic { code: "media-provenance-omitted"; path: string; message: string }
|
|
37
|
+
/** An opt-in native body could not represent this payload; current measured shapes were retained. */
|
|
38
|
+
export interface NativeBodyFallbackDiagnostic {
|
|
39
|
+
code: "native-body-fallback";
|
|
40
|
+
path: string;
|
|
41
|
+
reason: "complex-composition" | "multi-column" | "unsupported-list" | "annotated-text" | "unsupported-text" | "overflow";
|
|
42
|
+
message: string;
|
|
43
|
+
}
|
|
37
44
|
/** The chart data cannot be plotted, so a placeholder frame stands in for the chart. */
|
|
38
45
|
export interface ChartDataUnplottableDiagnostic { code: "chart-data-unplottable"; path: string; message: string; reason: "dataset-unknown" | "no-rows" | "no-columns" | "single-column-not-numeric" }
|
|
39
46
|
/** Content with no PowerPoint export (an empty table, an unsupported payload) is replaced by a plain-language placeholder frame. */
|
|
@@ -81,6 +88,27 @@ export interface VariableExampleUsedDiagnostic {
|
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
export interface ToPptxOptions {
|
|
91
|
+
/**
|
|
92
|
+
* How a footer or header text with words around `{{slide.number}}` (`{{slide.number}} / {{deck.slideCount}}`, `Page {{slide.number}}`)
|
|
93
|
+
* writes its number. "static" (default) writes the number as fixed text in an ordinary shape, so PowerPoint and Keynote both show
|
|
94
|
+
* `2 / 6`; the total is fixed either way (PowerPoint has no slide-count field). "live" keeps PowerPoint's slide-number field (in the
|
|
95
|
+
* Slide Number placeholder for the first footer one) with the words as fixed runs: Keynote replaces the whole paragraph that holds a
|
|
96
|
+
* slide-number field with the number alone, so it shows only `2`. A text that is only `{{slide.number}}` keeps its live field and
|
|
97
|
+
* placeholder in both modes. A fixed number does not follow slides moved or deleted in PowerPoint; the authored text returns on import (RR-91).
|
|
98
|
+
*/
|
|
99
|
+
slideNumberFields?: "static" | "live";
|
|
100
|
+
/**
|
|
101
|
+
* Prototype: "native" exports each supported paragraph or flat-bullet body (since 0.22.0 also a one-sequence numbered list) as
|
|
102
|
+
* one native body with native wrapping and no autofit: the first body of a slide is the Body placeholder, a further body region (since 0.22.0) is an
|
|
103
|
+
* ordinary native text box at its region. It retains selected fonts, effective alignment and region anchor,
|
|
104
|
+
* but native applications can choose different wraps and baselines from the measured preview. Unsupported groups,
|
|
105
|
+
* shared body regions, nested/description/restarted-numbered/multicolumn lists, annotations and complex text retain
|
|
106
|
+
* measured shapes with native-body-fallback diagnostics. Native paragraph separators normalize to LF on import.
|
|
107
|
+
* A plain-string title that wraps over several lines (no hard break, no overflow, no rich runs) is also one native text box
|
|
108
|
+
* with native wrap and no autofit, bound to the Title placeholder (since 0.22.0); an unwrapped title binds as before.
|
|
109
|
+
* Omitted or "measured" preserves the current measured-line export. No native application qualification is implied (Keynote 15.1.1 was checked for Title/Body controls and reflow, PowerPoint was not).
|
|
110
|
+
*/
|
|
111
|
+
bodyText?: "measured" | "native";
|
|
84
112
|
/** Default compatible converts WebP to a static PNG. Preserve embeds original WebP bytes. */
|
|
85
113
|
imageFormat?: "compatible" | "preserve";
|
|
86
114
|
/**
|
|
@@ -108,6 +136,14 @@ export interface ToPptxOptions {
|
|
|
108
136
|
* also written as PowerPoint's native section list whatever this option says.
|
|
109
137
|
*/
|
|
110
138
|
provenance?: "full" | "references-only" | false;
|
|
139
|
+
/**
|
|
140
|
+
* Literal footer text: "native" (default) keeps the first eligible one-line footer text bound to PowerPoint's Footer
|
|
141
|
+
* placeholder. "shape" omits only that slide-local ftr binding, retaining its measured editable text shape, locks,
|
|
142
|
+
* formatting, geometry, paint order and provenance. It stays on the slide, including when repeated. Live slide-number
|
|
143
|
+
* and date fields/placeholders, master/layout footer placeholders and flags remain unchanged. The literal shape then
|
|
144
|
+
* has no native Footer-dialog ownership; this does not guarantee viewer parity or restore tags another application strips.
|
|
145
|
+
*/
|
|
146
|
+
footerText?: "native" | "shape";
|
|
111
147
|
/**
|
|
112
148
|
* Values for the deck's template variables, keyed by variable id (core `resolveVariables`). A deck that uses content
|
|
113
149
|
* variables, or is marked `template: true`, is resolved to a concrete deck first; the PPTX holds the resolved text.
|
|
@@ -126,7 +162,7 @@ export interface ToPptxOptions {
|
|
|
126
162
|
/** Match preview/pagination clearance around supplied vector text outlines; default 1. */
|
|
127
163
|
textRasterPadding?: number;
|
|
128
164
|
/** 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. */
|
|
129
|
-
onDiagnostic?: (diagnostic: LayoutDiagnostic | UnresolvedReferenceDiagnostic | MediaProvenanceDiagnostic | ChartDataUnplottableDiagnostic | ChartDataAdaptedDiagnostic | ChartMapGeodataDiagnostic | ChartValueNotNumericDiagnostic | ChartMappingAdaptedDiagnostic | DataProvenanceOmittedDiagnostic | ContentPlaceholderDiagnostic | UnresolvedAssetDiagnostic | SvgSanitizedDiagnostic | SvgImageRasterizedDiagnostic | VariableExampleUsedDiagnostic) => void;
|
|
165
|
+
onDiagnostic?: (diagnostic: LayoutDiagnostic | UnresolvedReferenceDiagnostic | MediaProvenanceDiagnostic | NativeBodyFallbackDiagnostic | ChartDataUnplottableDiagnostic | ChartDataAdaptedDiagnostic | ChartMapGeodataDiagnostic | ChartValueNotNumericDiagnostic | ChartMappingAdaptedDiagnostic | DataProvenanceOmittedDiagnostic | ContentPlaceholderDiagnostic | UnresolvedAssetDiagnostic | SvgSanitizedDiagnostic | SvgImageRasterizedDiagnostic | VariableExampleUsedDiagnostic) => void;
|
|
130
166
|
baseDir?: string;
|
|
131
167
|
compressionLevel?: number;
|
|
132
168
|
imageResolver?: (src: string, context: ImageResolverContext) => ImageResolverResult | Promise<ImageResolverResult | null | undefined> | null | undefined;
|