@shbernal/ts-pptx 0.0.1
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/LICENSE +22 -0
- package/README.md +238 -0
- package/dist/autopage-tmfjRzL4.js +420 -0
- package/dist/autopage-tmfjRzL4.js.map +1 -0
- package/dist/browser.d.ts +23 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +103 -0
- package/dist/browser.js.map +1 -0
- package/dist/diagnostics-DYQC7xLV.js +0 -0
- package/dist/diagnostics-DYQC7xLV.js.map +1 -0
- package/dist/diagnostics-DzY9Tyfm.d.ts +693 -0
- package/dist/diagnostics-DzY9Tyfm.d.ts.map +1 -0
- package/dist/embedded-fonts-MaynAHa9.js +94 -0
- package/dist/embedded-fonts-MaynAHa9.js.map +1 -0
- package/dist/enums-D3aF4QGb.d.ts +452 -0
- package/dist/enums-D3aF4QGb.d.ts.map +1 -0
- package/dist/errors-DGE9zh3U.js +93 -0
- package/dist/errors-DGE9zh3U.js.map +1 -0
- package/dist/errors-DRLpOpgQ.d.ts +160 -0
- package/dist/errors-DRLpOpgQ.d.ts.map +1 -0
- package/dist/fit-CbxyHN69.js +0 -0
- package/dist/fit-CbxyHN69.js.map +1 -0
- package/dist/html-dom-wVa3TiG7.js +666 -0
- package/dist/html-dom-wVa3TiG7.js.map +1 -0
- package/dist/html.d.ts +26 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +60 -0
- package/dist/html.js.map +1 -0
- package/dist/image-size-C5Z1sKof.js +252 -0
- package/dist/image-size-C5Z1sKof.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +75 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect.d.ts +173 -0
- package/dist/inspect.d.ts.map +1 -0
- package/dist/inspect.js +285 -0
- package/dist/inspect.js.map +1 -0
- package/dist/math.d.ts +37 -0
- package/dist/math.d.ts.map +1 -0
- package/dist/math.js +106 -0
- package/dist/math.js.map +1 -0
- package/dist/measure.d.ts +223 -0
- package/dist/measure.d.ts.map +1 -0
- package/dist/measure.js +3 -0
- package/dist/node.d.ts +12 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +60 -0
- package/dist/node.js.map +1 -0
- package/dist/package-CaOLXSNi.d.ts +193 -0
- package/dist/package-CaOLXSNi.d.ts.map +1 -0
- package/dist/pres-5x1bjCzk.d.ts +202 -0
- package/dist/pres-5x1bjCzk.d.ts.map +1 -0
- package/dist/presentation-B0JWdTN1.js +4286 -0
- package/dist/presentation-B0JWdTN1.js.map +1 -0
- package/dist/presentation-RGOiun5P.d.ts +3249 -0
- package/dist/presentation-RGOiun5P.d.ts.map +1 -0
- package/dist/presentation-types-BVw0EknS.d.ts +538 -0
- package/dist/presentation-types-BVw0EknS.d.ts.map +1 -0
- package/dist/read.d.ts +73 -0
- package/dist/read.d.ts.map +1 -0
- package/dist/read.js +4 -0
- package/dist/script.d.ts +553 -0
- package/dist/script.d.ts.map +1 -0
- package/dist/script.js +0 -0
- package/dist/script.js.map +1 -0
- package/dist/shapes-BZU8UTW2.js +5365 -0
- package/dist/shapes-BZU8UTW2.js.map +1 -0
- package/dist/slide-B6mDaM-P.d.ts +3396 -0
- package/dist/slide-B6mDaM-P.d.ts.map +1 -0
- package/dist/text-WRUbOy2r.js +11238 -0
- package/dist/text-WRUbOy2r.js.map +1 -0
- package/dist/units-B-JUKwCS.js +144 -0
- package/dist/units-B-JUKwCS.js.map +1 -0
- package/dist/units-internal-7JcMfYti.js +723 -0
- package/dist/units-internal-7JcMfYti.js.map +1 -0
- package/dist/zip.d.ts +68 -0
- package/dist/zip.d.ts.map +1 -0
- package/dist/zip.js +163 -0
- package/dist/zip.js.map +1 -0
- package/package.json +214 -0
|
@@ -0,0 +1,4286 @@
|
|
|
1
|
+
import { i as PackageReadError, n as InvalidOptionError, o as UnsupportedFeatureError, t as InternalError } from "./errors-DGE9zh3U.js";
|
|
2
|
+
import { d as emuToInches } from "./units-B-JUKwCS.js";
|
|
3
|
+
import { i as FONT_REL_TYPE, n as FONT_DATA_CONTENT_TYPE, r as FONT_DATA_EXTENSION, t as EMBEDDED_FONT_SLOTS } from "./embedded-fonts-MaynAHa9.js";
|
|
4
|
+
import { ZipWriter, readZip } from "./zip.js";
|
|
5
|
+
import { $ as escapeXmlAttribute, A as resolveMasterColorContext, B as restyleSlide, D as readGradientFill, E as readPictureFill, F as flattenShape, G as relsPartNameFor, H as partNameExtension, I as flattenSlide, J as OOXML_NS, K as resolveRelativePartName, L as parseClrMap, M as resolveSlideColorContext, N as resolveSlideThemeParts, O as resolveColorElement, Q as createElement, R as parseClrScheme, T as TextFrame, U as partNameToZipPath, V as styleRefFill, W as relativePartName, X as boolValue, Y as attr, Z as childElements, at as intValue, b as Picture, c as wrapShapeElement, ct as removeAttr, dt as setAttr, et as firstChild, it as insertInOrder, j as resolveNotesColorContext, k as resolveLayoutColorContext, l as GraphicFrame, lt as removeChildrenByQName, n as buildShapes, nt as getElements, ot as ownerDocumentOf, q as zipPathToPartName, rt as getOrAddChild, st as parseXml, t as GroupShape, tt as firstChildElement, ut as serializeXml, x as AutoShape, z as remapLiteralColors } from "./shapes-BZU8UTW2.js";
|
|
6
|
+
//#region src/read/opc/content-types.ts
|
|
7
|
+
/**
|
|
8
|
+
* Overlay over `[Content_Types].xml`, used to resolve part content types at
|
|
9
|
+
* load and to register new ones when parts are added. While clean, the original
|
|
10
|
+
* bytes pass through verbatim on save (byte-identical); once a mutation marks it
|
|
11
|
+
* dirty, `OpcPackage.save()` writes `serialize()` instead.
|
|
12
|
+
*/
|
|
13
|
+
var ContentTypes = class ContentTypes {
|
|
14
|
+
#defaults = /* @__PURE__ */ new Map();
|
|
15
|
+
#overrides = /* @__PURE__ */ new Map();
|
|
16
|
+
#dirty = false;
|
|
17
|
+
static parse(xml) {
|
|
18
|
+
const contentTypes = new ContentTypes();
|
|
19
|
+
const types = parseXml(xml).documentElement;
|
|
20
|
+
if (!types || types.localName !== "Types") throw new PackageReadError("package/content-types-invalid-root", "[Content_Types].xml: expected <Types> root element");
|
|
21
|
+
for (const element of getElements(types, "ct:Default")) {
|
|
22
|
+
const extension = element.getAttribute("Extension");
|
|
23
|
+
const contentType = element.getAttribute("ContentType");
|
|
24
|
+
if (!extension || !contentType) throw new PackageReadError("package/content-types-entry-incomplete", "[Content_Types].xml: <Default> missing Extension or ContentType");
|
|
25
|
+
contentTypes.#defaults.set(extension.toLowerCase(), contentType);
|
|
26
|
+
}
|
|
27
|
+
for (const element of getElements(types, "ct:Override")) {
|
|
28
|
+
const partName = element.getAttribute("PartName");
|
|
29
|
+
const contentType = element.getAttribute("ContentType");
|
|
30
|
+
if (!partName || !contentType) throw new PackageReadError("package/content-types-entry-incomplete", "[Content_Types].xml: <Override> missing PartName or ContentType");
|
|
31
|
+
contentTypes.#overrides.set(partName, contentType);
|
|
32
|
+
}
|
|
33
|
+
return contentTypes;
|
|
34
|
+
}
|
|
35
|
+
/** Exact `Override` match first, else `Default` by lowercased extension. */
|
|
36
|
+
contentTypeFor(partName) {
|
|
37
|
+
return this.#overrides.get(partName) ?? this.#defaults.get(partNameExtension(partName));
|
|
38
|
+
}
|
|
39
|
+
/** True once a registration changed the overlay; `serialize()` is then authoritative. */
|
|
40
|
+
get isDirty() {
|
|
41
|
+
return this.#dirty;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Ensure `partName` resolves to `contentType`, adding the minimum needed.
|
|
45
|
+
* A no-op when an existing `Default`/`Override` already resolves it to the
|
|
46
|
+
* same type; otherwise an `Override` is added (always valid, never conflicts
|
|
47
|
+
* with a differing `Default`).
|
|
48
|
+
*/
|
|
49
|
+
ensureRegistered(partName, contentType) {
|
|
50
|
+
if (this.contentTypeFor(partName) === contentType) return;
|
|
51
|
+
this.#overrides.set(partName, contentType);
|
|
52
|
+
this.#dirty = true;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Drop the `Override` for `partName` if present (e.g. when a part is removed).
|
|
56
|
+
* `Default` rules by extension are left untouched — they may still serve other
|
|
57
|
+
* parts. Marks the overlay dirty when something was removed.
|
|
58
|
+
*/
|
|
59
|
+
removeOverride(partName) {
|
|
60
|
+
if (this.#overrides.delete(partName)) this.#dirty = true;
|
|
61
|
+
}
|
|
62
|
+
/** Register a `Default` content type for a (lowercased) extension if absent. */
|
|
63
|
+
ensureDefault(extension, contentType) {
|
|
64
|
+
const ext = extension.toLowerCase();
|
|
65
|
+
if (this.#defaults.get(ext) === contentType) return;
|
|
66
|
+
this.#defaults.set(ext, contentType);
|
|
67
|
+
this.#dirty = true;
|
|
68
|
+
}
|
|
69
|
+
serialize() {
|
|
70
|
+
const lines = ["<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>", "<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">"];
|
|
71
|
+
for (const [extension, contentType] of this.#defaults) lines.push(`<Default Extension="${escapeXmlAttribute(extension)}" ContentType="${escapeXmlAttribute(contentType)}"/>`);
|
|
72
|
+
for (const [partName, contentType] of this.#overrides) lines.push(`<Override PartName="${escapeXmlAttribute(partName)}" ContentType="${escapeXmlAttribute(contentType)}"/>`);
|
|
73
|
+
lines.push("</Types>");
|
|
74
|
+
return lines.join("");
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/read/opc/part.ts
|
|
79
|
+
const textDecoder$1 = new TextDecoder("utf-8");
|
|
80
|
+
const textEncoder$4 = new TextEncoder();
|
|
81
|
+
const XML_DECLARATION_RE = /^\uFEFF?<\?xml[^>]*\?>\s*/;
|
|
82
|
+
/**
|
|
83
|
+
* One OPC part. Holds the original bytes from the zip for its whole life;
|
|
84
|
+
* parses to a DOM only on demand; serializes back to the original bytes
|
|
85
|
+
* unless something mutated the DOM and called `markDirty()`.
|
|
86
|
+
*
|
|
87
|
+
* Untouched parts therefore round-trip byte-identically.
|
|
88
|
+
*/
|
|
89
|
+
var Part = class {
|
|
90
|
+
partName;
|
|
91
|
+
contentType;
|
|
92
|
+
#bytes;
|
|
93
|
+
#dom = null;
|
|
94
|
+
#dirty = false;
|
|
95
|
+
constructor(partName, contentType, bytes) {
|
|
96
|
+
this.partName = partName;
|
|
97
|
+
this.contentType = contentType;
|
|
98
|
+
this.#bytes = bytes;
|
|
99
|
+
}
|
|
100
|
+
/** Whether this part's body is XML (by content type). */
|
|
101
|
+
get isXmlPart() {
|
|
102
|
+
return this.contentType.endsWith("+xml") || this.contentType === "application/xml" || this.contentType === "text/xml";
|
|
103
|
+
}
|
|
104
|
+
/** True once the body has been materialized as a DOM. */
|
|
105
|
+
get isParsed() {
|
|
106
|
+
return this.#dom !== null;
|
|
107
|
+
}
|
|
108
|
+
/** The original bytes from the package. Do not mutate. */
|
|
109
|
+
get bytes() {
|
|
110
|
+
return this.#bytes;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Lazily parsed DOM for XML parts. Parsing alone does not mark the part
|
|
114
|
+
* dirty; call `markDirty()` after mutating the tree.
|
|
115
|
+
*/
|
|
116
|
+
get dom() {
|
|
117
|
+
if (!this.isXmlPart) throw new InvalidOptionError("part/not-xml", `Part ${this.partName} (${this.contentType}) is not an XML part and has no DOM`);
|
|
118
|
+
if (!this.#dom) this.#dom = parseXml(textDecoder$1.decode(this.#bytes));
|
|
119
|
+
return this.#dom;
|
|
120
|
+
}
|
|
121
|
+
/** Call after mutating the DOM so `serialize()` reserializes this part. */
|
|
122
|
+
markDirty() {
|
|
123
|
+
if (!this.isXmlPart) throw new InvalidOptionError("part/not-xml", `Part ${this.partName} (${this.contentType}) is not an XML part and cannot be marked dirty`);
|
|
124
|
+
this.#dirty = true;
|
|
125
|
+
}
|
|
126
|
+
get isDirty() {
|
|
127
|
+
return this.#dirty;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Bytes to write on save: the original bytes when clean (byte-identical),
|
|
131
|
+
* the serialized DOM when dirty (semantically equivalent, schema-valid).
|
|
132
|
+
*/
|
|
133
|
+
serialize() {
|
|
134
|
+
if (!this.#dirty) return this.#bytes;
|
|
135
|
+
let xml = serializeXml(this.dom);
|
|
136
|
+
if (!xml.startsWith("<?xml")) xml = (XML_DECLARATION_RE.exec(textDecoder$1.decode(this.#bytes))?.[0] ?? "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n") + xml;
|
|
137
|
+
return textEncoder$4.encode(xml);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
//#endregion
|
|
141
|
+
//#region src/read/opc/relationships.ts
|
|
142
|
+
/**
|
|
143
|
+
* Overlay over one `.rels` part, keyed by relationship id. While clean, the
|
|
144
|
+
* `.rels` bytes pass through verbatim on save (byte-identical); once `add()`
|
|
145
|
+
* (or another mutation) marks it dirty, `OpcPackage.save()` writes
|
|
146
|
+
* `serialize()` into the owning `.rels` part instead.
|
|
147
|
+
*/
|
|
148
|
+
var Relationships = class Relationships {
|
|
149
|
+
/** Partname of the part owning these relationships ('/' for package-level). */
|
|
150
|
+
sourcePartName;
|
|
151
|
+
#byId = /* @__PURE__ */ new Map();
|
|
152
|
+
#dirty = false;
|
|
153
|
+
constructor(sourcePartName) {
|
|
154
|
+
this.sourcePartName = sourcePartName;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* @param xml body of the `.rels` part
|
|
158
|
+
* @param sourcePartName partname of the owning part ('/' for `/_rels/.rels`)
|
|
159
|
+
*/
|
|
160
|
+
static parse(xml, sourcePartName) {
|
|
161
|
+
const relationships = new Relationships(sourcePartName);
|
|
162
|
+
const root = parseXml(xml).documentElement;
|
|
163
|
+
if (!root || root.localName !== "Relationships") throw new PackageReadError("package/relationships-invalid-root", `Relationships of ${sourcePartName}: expected <Relationships> root element`);
|
|
164
|
+
for (const element of getElements(root, "pr:Relationship")) {
|
|
165
|
+
const id = element.getAttribute("Id");
|
|
166
|
+
const type = element.getAttribute("Type");
|
|
167
|
+
const target = element.getAttribute("Target");
|
|
168
|
+
if (!id || !type || !target) throw new PackageReadError("package/relationship-incomplete", `Relationships of ${sourcePartName}: <Relationship> missing Id, Type, or Target`);
|
|
169
|
+
const targetModeAttribute = element.getAttribute("TargetMode");
|
|
170
|
+
const targetMode = targetModeAttribute === "Internal" || targetModeAttribute === "External" ? targetModeAttribute : void 0;
|
|
171
|
+
if (targetModeAttribute !== null && targetMode === void 0) throw new PackageReadError("package/relationship-invalid-target-mode", `Relationships of ${sourcePartName}: invalid TargetMode "${targetModeAttribute}" on ${id}`);
|
|
172
|
+
const relationship = {
|
|
173
|
+
id,
|
|
174
|
+
type,
|
|
175
|
+
target
|
|
176
|
+
};
|
|
177
|
+
if (targetMode) relationship.targetMode = targetMode;
|
|
178
|
+
if (relationships.#byId.has(id)) throw new PackageReadError("package/duplicate-relationship-id", `Relationships of ${sourcePartName}: duplicate relationship id ${id}`);
|
|
179
|
+
relationships.#byId.set(id, relationship);
|
|
180
|
+
}
|
|
181
|
+
return relationships;
|
|
182
|
+
}
|
|
183
|
+
/** Empty relationship set (for parts that have no `.rels`). */
|
|
184
|
+
static empty(sourcePartName) {
|
|
185
|
+
return new Relationships(sourcePartName);
|
|
186
|
+
}
|
|
187
|
+
get size() {
|
|
188
|
+
return this.#byId.size;
|
|
189
|
+
}
|
|
190
|
+
/** True once a relationship was added/changed; `serialize()` is then authoritative. */
|
|
191
|
+
get isDirty() {
|
|
192
|
+
return this.#dirty;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Add a relationship and return it. Allocates an id `rId<n>` with `n` one
|
|
196
|
+
* past the highest existing numeric id. `target` is relative to the source
|
|
197
|
+
* part's directory for internal targets (e.g. `../media/image1.png`), or an
|
|
198
|
+
* absolute URI for external ones. Marks this set dirty.
|
|
199
|
+
*/
|
|
200
|
+
add(type, target, targetMode) {
|
|
201
|
+
let max = 0;
|
|
202
|
+
for (const id of this.#byId.keys()) {
|
|
203
|
+
const match = /^rId(\d+)$/.exec(id);
|
|
204
|
+
if (match) max = Math.max(max, Number(match[1]));
|
|
205
|
+
}
|
|
206
|
+
const id = `rId${max + 1}`;
|
|
207
|
+
const relationship = {
|
|
208
|
+
id,
|
|
209
|
+
type,
|
|
210
|
+
target
|
|
211
|
+
};
|
|
212
|
+
if (targetMode) relationship.targetMode = targetMode;
|
|
213
|
+
this.#byId.set(id, relationship);
|
|
214
|
+
this.#dirty = true;
|
|
215
|
+
return relationship;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Add a relationship under a caller-chosen id and return it. Used when
|
|
219
|
+
* rebuilding a copied part's relationships across a package boundary: keeping
|
|
220
|
+
* each source id lets the copied part body's `r:id`/`r:embed` references stay
|
|
221
|
+
* valid without rewriting the body. Throws if the id is already present.
|
|
222
|
+
* Marks this set dirty.
|
|
223
|
+
*/
|
|
224
|
+
addWithId(id, type, target, targetMode) {
|
|
225
|
+
if (this.#byId.has(id)) throw new InvalidOptionError("relationship/duplicate-id", `Relationships of ${this.sourcePartName}: duplicate relationship id ${id}`);
|
|
226
|
+
const relationship = {
|
|
227
|
+
id,
|
|
228
|
+
type,
|
|
229
|
+
target
|
|
230
|
+
};
|
|
231
|
+
if (targetMode) relationship.targetMode = targetMode;
|
|
232
|
+
this.#byId.set(id, relationship);
|
|
233
|
+
this.#dirty = true;
|
|
234
|
+
return relationship;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Remove the relationship with `id` and return whether one was present. Used
|
|
238
|
+
* when a referenced part is deleted (e.g. removing a slide drops the
|
|
239
|
+
* presentation→slide relationship). Marks this set dirty when it removes one.
|
|
240
|
+
*/
|
|
241
|
+
remove(id) {
|
|
242
|
+
const removed = this.#byId.delete(id);
|
|
243
|
+
if (removed) this.#dirty = true;
|
|
244
|
+
return removed;
|
|
245
|
+
}
|
|
246
|
+
get(id) {
|
|
247
|
+
return this.#byId.get(id);
|
|
248
|
+
}
|
|
249
|
+
byType(type) {
|
|
250
|
+
return [...this.#byId.values()].filter((relationship) => relationship.type === type);
|
|
251
|
+
}
|
|
252
|
+
[Symbol.iterator]() {
|
|
253
|
+
return this.#byId.values();
|
|
254
|
+
}
|
|
255
|
+
/** Resolve an internal relationship's target to an absolute partname. */
|
|
256
|
+
resolveTarget(id) {
|
|
257
|
+
const relationship = this.#byId.get(id);
|
|
258
|
+
if (!relationship) throw new InvalidOptionError("relationship/not-found", `Relationships of ${this.sourcePartName}: no relationship with id ${id}`);
|
|
259
|
+
if (relationship.targetMode === "External") throw new InvalidOptionError("relationship/external-has-no-partname", `Relationships of ${this.sourcePartName}: ${id} is External (${relationship.target}) and has no partname`);
|
|
260
|
+
return resolveRelativePartName(this.sourcePartName, relationship.target);
|
|
261
|
+
}
|
|
262
|
+
serialize() {
|
|
263
|
+
const lines = ["<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>", "<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">"];
|
|
264
|
+
for (const { id, type, target, targetMode } of this.#byId.values()) {
|
|
265
|
+
const mode = targetMode ? ` TargetMode="${targetMode}"` : "";
|
|
266
|
+
lines.push(`<Relationship Id="${escapeXmlAttribute(id)}" Type="${escapeXmlAttribute(type)}" Target="${escapeXmlAttribute(target)}"${mode}/>`);
|
|
267
|
+
}
|
|
268
|
+
lines.push("</Relationships>");
|
|
269
|
+
return lines.join("");
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/read/opc/package.ts
|
|
274
|
+
const CONTENT_TYPES_ZIP_PATH = "[Content_Types].xml";
|
|
275
|
+
const RELATIONSHIPS_CONTENT_TYPE = "application/vnd.openxmlformats-package.relationships+xml";
|
|
276
|
+
const textDecoder = new TextDecoder("utf-8");
|
|
277
|
+
const textEncoder$3 = new TextEncoder();
|
|
278
|
+
/**
|
|
279
|
+
* An OPC package loaded from `.pptx` bytes.
|
|
280
|
+
*
|
|
281
|
+
* Fidelity contract: parts that are never mutated (`Part.markDirty()`) are
|
|
282
|
+
* written back byte-identically by `save()`. Whole-zip byte-identity is not
|
|
283
|
+
* promised — zip metadata and compression may differ — but part bodies, the
|
|
284
|
+
* part-name set, and part order are preserved.
|
|
285
|
+
*/
|
|
286
|
+
var OpcPackage = class OpcPackage {
|
|
287
|
+
/** All parts, keyed by partname (e.g. `/ppt/slides/slide1.xml`), in original zip order. */
|
|
288
|
+
#parts;
|
|
289
|
+
/** Content-type resolution + registration overlay. */
|
|
290
|
+
contentTypes;
|
|
291
|
+
#contentTypesBytes;
|
|
292
|
+
#relationshipsCache = /* @__PURE__ */ new Map();
|
|
293
|
+
constructor(parts, contentTypes, contentTypesBytes) {
|
|
294
|
+
this.#parts = parts;
|
|
295
|
+
this.contentTypes = contentTypes;
|
|
296
|
+
this.#contentTypesBytes = contentTypesBytes;
|
|
297
|
+
}
|
|
298
|
+
/** All parts, keyed by partname, in zip/add order. */
|
|
299
|
+
get parts() {
|
|
300
|
+
return this.#parts;
|
|
301
|
+
}
|
|
302
|
+
static async load(input) {
|
|
303
|
+
const entries = await readZip(input);
|
|
304
|
+
const contentTypesBytes = entries.get(CONTENT_TYPES_ZIP_PATH);
|
|
305
|
+
if (!contentTypesBytes) throw new PackageReadError("package/not-an-opc-package", "Not an OPC package: missing [Content_Types].xml");
|
|
306
|
+
const contentTypes = ContentTypes.parse(textDecoder.decode(contentTypesBytes));
|
|
307
|
+
const parts = /* @__PURE__ */ new Map();
|
|
308
|
+
for (const [zipPath, bytes] of entries) {
|
|
309
|
+
if (zipPath === CONTENT_TYPES_ZIP_PATH) continue;
|
|
310
|
+
const partName = zipPathToPartName(zipPath);
|
|
311
|
+
if (partName.startsWith("/[trash]/")) continue;
|
|
312
|
+
const contentType = contentTypes.contentTypeFor(partName);
|
|
313
|
+
if (!contentType) throw new PackageReadError("package/part-content-type-missing", `No content type for part ${partName}: [Content_Types].xml has no matching Override or Default`);
|
|
314
|
+
parts.set(partName, new Part(partName, contentType, bytes));
|
|
315
|
+
}
|
|
316
|
+
return new OpcPackage(parts, contentTypes, contentTypesBytes);
|
|
317
|
+
}
|
|
318
|
+
part(partName) {
|
|
319
|
+
return this.parts.get(partName);
|
|
320
|
+
}
|
|
321
|
+
partsByContentType(contentType) {
|
|
322
|
+
return [...this.parts.values()].filter((part) => part.contentType === contentType);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Relationships owned by a part ('/' for the package-level
|
|
326
|
+
* `/_rels/.rels`). Parts without a `.rels` part get an empty set.
|
|
327
|
+
*/
|
|
328
|
+
relationshipsFor(sourcePartName = "/") {
|
|
329
|
+
const cached = this.#relationshipsCache.get(sourcePartName);
|
|
330
|
+
if (cached) return cached;
|
|
331
|
+
const relsPart = this.parts.get(relsPartNameFor(sourcePartName));
|
|
332
|
+
const relationships = relsPart ? Relationships.parse(textDecoder.decode(relsPart.bytes), sourcePartName) : Relationships.empty(sourcePartName);
|
|
333
|
+
this.#relationshipsCache.set(sourcePartName, relationships);
|
|
334
|
+
return relationships;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Add a new part and register its content type (`ensureRegistered`), so the
|
|
338
|
+
* package stays consistent. Throws if the partname is already taken.
|
|
339
|
+
*/
|
|
340
|
+
addPart(partName, contentType, bytes) {
|
|
341
|
+
if (this.#parts.has(partName)) throw new InvalidOptionError("package/duplicate-part-name", `Cannot add part ${partName}: a part with that name already exists`);
|
|
342
|
+
this.contentTypes.ensureRegistered(partName, contentType);
|
|
343
|
+
const part = new Part(partName, contentType, bytes);
|
|
344
|
+
this.#parts.set(partName, part);
|
|
345
|
+
return part;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Remove a part and return whether it existed. Drops its `Override` content-type
|
|
349
|
+
* registration and any cached relationship set it owned, so a later `save()`
|
|
350
|
+
* neither re-emits it nor flushes a stale `.rels` for it. Low-level: it does not
|
|
351
|
+
* touch references *to* this part (dangling rels are the caller's concern) nor
|
|
352
|
+
* cascade to parts this one referenced — see {@link Presentation.removeSlide}
|
|
353
|
+
* for the slide-aware variant that unwires the presentation and prunes orphans.
|
|
354
|
+
*/
|
|
355
|
+
removePart(partName) {
|
|
356
|
+
if (!this.#parts.delete(partName)) return false;
|
|
357
|
+
this.contentTypes.removeOverride(partName);
|
|
358
|
+
this.#relationshipsCache.delete(partName);
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Reserve an unused media partname `/ppt/media/<base><n>.<ext>` (n one past
|
|
363
|
+
* the highest existing index for that base). Does not create the part.
|
|
364
|
+
*/
|
|
365
|
+
reserveMediaPartName(extension, base = "image") {
|
|
366
|
+
const ext = extension.toLowerCase().replace(/^\./, "");
|
|
367
|
+
const re = new RegExp(`^/ppt/media/${base}(\\d+)\\.${ext}$`, "i");
|
|
368
|
+
let max = 0;
|
|
369
|
+
for (const partName of this.#parts.keys()) {
|
|
370
|
+
const match = re.exec(partName);
|
|
371
|
+
if (match) max = Math.max(max, Number(match[1]));
|
|
372
|
+
}
|
|
373
|
+
return `/ppt/media/${base}${max + 1}.${ext}`;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Reserve an unused partname in the same directory and naming family as
|
|
377
|
+
* `templatePartName`, with a fresh index one past the highest existing one.
|
|
378
|
+
* The template's filename is split into a base (its stem minus any trailing
|
|
379
|
+
* digits) and an extension, e.g. `/ppt/slideLayouts/slideLayout1.xml` →
|
|
380
|
+
* `/ppt/slideLayouts/slideLayout<n>.xml`, `/ppt/media/image1.png` →
|
|
381
|
+
* `/ppt/media/image<n>.png`. Used when copying a part in from another package.
|
|
382
|
+
* Does not create the part.
|
|
383
|
+
*/
|
|
384
|
+
reservePartNameLike(templatePartName) {
|
|
385
|
+
const slash = templatePartName.lastIndexOf("/");
|
|
386
|
+
const dir = templatePartName.slice(0, slash);
|
|
387
|
+
const fileName = templatePartName.slice(slash + 1);
|
|
388
|
+
const dot = fileName.lastIndexOf(".");
|
|
389
|
+
const ext = dot > 0 ? fileName.slice(dot) : "";
|
|
390
|
+
const base = (dot > 0 ? fileName.slice(0, dot) : fileName).replace(/\d+$/, "");
|
|
391
|
+
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
392
|
+
const re = new RegExp(`^${escape(`${dir}/${base}`)}(\\d+)${escape(ext)}$`);
|
|
393
|
+
let max = 0;
|
|
394
|
+
for (const partName of this.#parts.keys()) {
|
|
395
|
+
const match = re.exec(partName);
|
|
396
|
+
if (match) max = Math.max(max, Number(match[1]));
|
|
397
|
+
}
|
|
398
|
+
return `${dir}/${base}${max + 1}${ext}`;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Re-emit the package. Clean parts are written from their original bytes;
|
|
402
|
+
* dirty parts from their DOM. Dirty relationship sets and content types are
|
|
403
|
+
* flushed first. Part order is preserved from load; added parts append.
|
|
404
|
+
*/
|
|
405
|
+
async save() {
|
|
406
|
+
this.#flushRelationships();
|
|
407
|
+
const zip = new ZipWriter();
|
|
408
|
+
zip.add(CONTENT_TYPES_ZIP_PATH, this.contentTypes.isDirty ? this.contentTypes.serialize() : this.#contentTypesBytes);
|
|
409
|
+
for (const part of this.#parts.values()) zip.add(partNameToZipPath(part.partName), part.serialize());
|
|
410
|
+
return zip.toBytes();
|
|
411
|
+
}
|
|
412
|
+
/** Write each dirty `Relationships` set back into its `.rels` part (creating it if new). */
|
|
413
|
+
#flushRelationships() {
|
|
414
|
+
for (const relationships of this.#relationshipsCache.values()) {
|
|
415
|
+
if (!relationships.isDirty) continue;
|
|
416
|
+
const relsPartName = relsPartNameFor(relationships.sourcePartName);
|
|
417
|
+
const bytes = textEncoder$3.encode(relationships.serialize());
|
|
418
|
+
this.#parts.set(relsPartName, new Part(relsPartName, RELATIONSHIPS_CONTENT_TYPE, bytes));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region src/read/api/slide-background.ts
|
|
424
|
+
/**
|
|
425
|
+
* Read a slide's effective background (`p:cSld/p:bg`), decoded into a typed union.
|
|
426
|
+
*
|
|
427
|
+
* A background lives on the slide itself, or is inherited from its slideLayout,
|
|
428
|
+
* else its slideMaster — the {@link SlideBackground.source} field records which.
|
|
429
|
+
* The writer authors solid-colour, gradient, and image backgrounds (all FAITHFUL);
|
|
430
|
+
* pattern and theme-indexed (`p:bgRef`) backgrounds are read-only for imported
|
|
431
|
+
* decks — surface them so a codegen pass can carry the part rather than fake it. A
|
|
432
|
+
* `themeRef` additionally resolves its `idx` through the slide theme's `fmtScheme`
|
|
433
|
+
* to a concrete {@link BackgroundFill} in `resolvedFill`, so a consumer can ask what
|
|
434
|
+
* colour/gradient a theme-indexed background actually renders as.
|
|
435
|
+
*/
|
|
436
|
+
/** The `p:cSld/p:bg` element of a slide/layout/master root, or `null`. */
|
|
437
|
+
function backgroundElementOf(root) {
|
|
438
|
+
const cSld = root && firstChild(root, "p:cSld");
|
|
439
|
+
return cSld ? firstChild(cSld, "p:bg") : null;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Decode a colour-bearing container (`p:bgPr`, or a synthetic wrapper around a
|
|
443
|
+
* `fmtScheme` fill entry) into a {@link BackgroundFill}. `a:noFill`, an empty
|
|
444
|
+
* container, or an unrecognized fill all read as `{ type: 'none' }`.
|
|
445
|
+
*/
|
|
446
|
+
function decodeBackgroundFill(container, ctx, rels) {
|
|
447
|
+
const solid = firstChild(container, "a:solidFill");
|
|
448
|
+
if (solid) return {
|
|
449
|
+
type: "solid",
|
|
450
|
+
color: resolveColorElement(firstChildElement(solid), ctx)
|
|
451
|
+
};
|
|
452
|
+
if (firstChild(container, "a:gradFill")) return {
|
|
453
|
+
type: "gradient",
|
|
454
|
+
gradient: readGradientFill(container, ctx)
|
|
455
|
+
};
|
|
456
|
+
const picture = readPictureFill(container, rels);
|
|
457
|
+
if (picture) return {
|
|
458
|
+
type: "image",
|
|
459
|
+
picture,
|
|
460
|
+
relId: picture.relId,
|
|
461
|
+
partName: picture.partName
|
|
462
|
+
};
|
|
463
|
+
const patt = firstChild(container, "a:pattFill");
|
|
464
|
+
if (patt) {
|
|
465
|
+
const wrapColor = (qname) => {
|
|
466
|
+
const wrap = firstChild(patt, qname);
|
|
467
|
+
const colorEl = wrap && firstChildElement(wrap);
|
|
468
|
+
return colorEl ? resolveColorElement(colorEl, ctx) : null;
|
|
469
|
+
};
|
|
470
|
+
return {
|
|
471
|
+
type: "pattern",
|
|
472
|
+
preset: attr(patt, "prst") ?? null,
|
|
473
|
+
foreground: wrapColor("a:fgClr"),
|
|
474
|
+
background: wrapColor("a:bgClr")
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return { type: "none" };
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Resolve a `p:bgRef`'s `idx` into the concrete {@link BackgroundFill} it renders
|
|
481
|
+
* as, through the slide theme's `fmtScheme`. A `p:bgRef` is a
|
|
482
|
+
* `CT_StyleMatrixReference` — the same shape as a shape's `a:fillRef` — so
|
|
483
|
+
* {@link styleRefFill} builds the indexed `bgFillStyleLst`/`fillStyleLst` entry with
|
|
484
|
+
* its `phClr` substituted by the bgRef's colour, and the resulting fill element is
|
|
485
|
+
* decoded like a `p:bgPr`. `null` when the theme has no `fmtScheme`, the entry is
|
|
486
|
+
* absent, or the colour cannot be resolved.
|
|
487
|
+
*
|
|
488
|
+
* `themeRels` — not the owning slide/layout/master's rels — is what an `a:blipFill`
|
|
489
|
+
* entry's `r:embed` resolves against: the fill element is lifted out of the *theme*
|
|
490
|
+
* part, so its relationship id is scoped to that part. Several stock Office themes
|
|
491
|
+
* (Ion, Facet, …) put a picture in the third `bgFillStyleLst` slot, so a deck using
|
|
492
|
+
* `p:bgRef idx="1003"` hits this.
|
|
493
|
+
*/
|
|
494
|
+
function resolveThemeRefFill(bgRef, ctx, themeRels) {
|
|
495
|
+
const fill = styleRefFill(bgRef, ctx);
|
|
496
|
+
if (!fill) return null;
|
|
497
|
+
const wrapper = createElement(ownerDocumentOf(bgRef), "p:bgPr");
|
|
498
|
+
wrapper.appendChild(fill);
|
|
499
|
+
return decodeBackgroundFill(wrapper, ctx, themeRels);
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Decode a `p:bg` element into a {@link SlideBackground}. `ctx` resolves colour
|
|
503
|
+
* tokens and (for a `themeRef`) the theme `fmtScheme`; `rels` (the *owning* part's
|
|
504
|
+
* relationships) resolves an image background's `r:embed` to an absolute part name.
|
|
505
|
+
*
|
|
506
|
+
* `themeRels` is the theme part's own relationships, used only for a `themeRef`'s
|
|
507
|
+
* {@link SlideBackground.resolvedFill}: that fill comes out of the theme part, so an
|
|
508
|
+
* image entry's `r:embed` is scoped to the theme, not to `rels`.
|
|
509
|
+
*/
|
|
510
|
+
function readSlideBackground(bg, source, ctx, rels, themeRels) {
|
|
511
|
+
const bgRef = firstChild(bg, "p:bgRef");
|
|
512
|
+
if (bgRef) return {
|
|
513
|
+
type: "themeRef",
|
|
514
|
+
source,
|
|
515
|
+
idx: intValue(attr(bgRef, "idx")),
|
|
516
|
+
color: resolveColorElement(firstChildElement(bgRef), ctx),
|
|
517
|
+
resolvedFill: resolveThemeRefFill(bgRef, ctx, themeRels)
|
|
518
|
+
};
|
|
519
|
+
const bgPr = firstChild(bg, "p:bgPr");
|
|
520
|
+
if (!bgPr) return {
|
|
521
|
+
type: "none",
|
|
522
|
+
source
|
|
523
|
+
};
|
|
524
|
+
return {
|
|
525
|
+
...decodeBackgroundFill(bgPr, ctx, rels),
|
|
526
|
+
source
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region src/read/api/notes.ts
|
|
531
|
+
/** One EMU coordinate from a placeholder's `p:spPr/a:xfrm` child, or `null` when absent. */
|
|
532
|
+
function xfrmEmu$1(sp, container, axis) {
|
|
533
|
+
const spPr = firstChild(sp, "p:spPr");
|
|
534
|
+
const xfrm = spPr && firstChild(spPr, "a:xfrm");
|
|
535
|
+
const el = xfrm && firstChild(xfrm, container);
|
|
536
|
+
return el ? intValue(attr(el, axis)) : null;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* One placeholder shape (`p:sp`) of a notes slide. A notes slide holds a fixed set:
|
|
540
|
+
* the slide thumbnail (`type="sldImg"`), the notes body (`type="body"`), and the
|
|
541
|
+
* slide number (`type="sldNum"`). Geometry getters read the placeholder's *own*
|
|
542
|
+
* `a:xfrm` and are `null` when it inherits geometry from the notesMaster (the case
|
|
543
|
+
* on an authored deck, whose `sldImg`/`sldNum` carry an empty `p:spPr`).
|
|
544
|
+
*/
|
|
545
|
+
var NotesPlaceholder = class {
|
|
546
|
+
sp;
|
|
547
|
+
part;
|
|
548
|
+
themeContext;
|
|
549
|
+
relationships;
|
|
550
|
+
constructor(sp, part, themeContext, relationships) {
|
|
551
|
+
this.sp = sp;
|
|
552
|
+
this.part = part;
|
|
553
|
+
this.themeContext = themeContext;
|
|
554
|
+
this.relationships = relationships;
|
|
555
|
+
}
|
|
556
|
+
#ph() {
|
|
557
|
+
const nvSpPr = firstChild(this.sp, "p:nvSpPr");
|
|
558
|
+
const nvPr = nvSpPr && firstChild(nvSpPr, "p:nvPr");
|
|
559
|
+
return nvPr ? firstChild(nvPr, "p:ph") : null;
|
|
560
|
+
}
|
|
561
|
+
#cNvPr() {
|
|
562
|
+
const nvSpPr = firstChild(this.sp, "p:nvSpPr");
|
|
563
|
+
return nvSpPr ? firstChild(nvSpPr, "p:cNvPr") : null;
|
|
564
|
+
}
|
|
565
|
+
/** Placeholder type (`p:ph/@type`: `sldImg` | `body` | `sldNum`), or `null` when absent. */
|
|
566
|
+
get type() {
|
|
567
|
+
const ph = this.#ph();
|
|
568
|
+
return ph ? attr(ph, "type") : null;
|
|
569
|
+
}
|
|
570
|
+
/** Placeholder index (`p:ph/@idx`), or `null` when unset. */
|
|
571
|
+
get idx() {
|
|
572
|
+
const ph = this.#ph();
|
|
573
|
+
return ph ? attr(ph, "idx") : null;
|
|
574
|
+
}
|
|
575
|
+
/** Shape name (`p:cNvPr/@name`), or `''` when unnamed. */
|
|
576
|
+
get name() {
|
|
577
|
+
const cNvPr = this.#cNvPr();
|
|
578
|
+
return (cNvPr && attr(cNvPr, "name")) ?? "";
|
|
579
|
+
}
|
|
580
|
+
/** Drawing id (`p:cNvPr/@id`), or `null` when absent. */
|
|
581
|
+
get id() {
|
|
582
|
+
const cNvPr = this.#cNvPr();
|
|
583
|
+
return cNvPr ? intValue(attr(cNvPr, "id")) : null;
|
|
584
|
+
}
|
|
585
|
+
/** Left edge in EMU (`a:off/@x`), or `null` when geometry is inherited (no own `a:xfrm`). */
|
|
586
|
+
get left() {
|
|
587
|
+
return xfrmEmu$1(this.sp, "a:off", "x");
|
|
588
|
+
}
|
|
589
|
+
/** Top edge in EMU (`a:off/@y`), or `null` when geometry is inherited. */
|
|
590
|
+
get top() {
|
|
591
|
+
return xfrmEmu$1(this.sp, "a:off", "y");
|
|
592
|
+
}
|
|
593
|
+
/** Width in EMU (`a:ext/@cx`), or `null` when geometry is inherited. */
|
|
594
|
+
get width() {
|
|
595
|
+
return xfrmEmu$1(this.sp, "a:ext", "cx");
|
|
596
|
+
}
|
|
597
|
+
/** Height in EMU (`a:ext/@cy`), or `null` when geometry is inherited. */
|
|
598
|
+
get height() {
|
|
599
|
+
return xfrmEmu$1(this.sp, "a:ext", "cy");
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* The placeholder's text as a navigable {@link TextFrame} (`p:txBody`), or `null`
|
|
603
|
+
* when it carries no text body (the `sldImg` thumbnail). The body placeholder's
|
|
604
|
+
* runs round-trip a slide's authored notes; the `sldNum` placeholder's frame
|
|
605
|
+
* holds the slide-number `a:fld`, whose value surfaces through `TextFrame.text`.
|
|
606
|
+
*/
|
|
607
|
+
get textFrame() {
|
|
608
|
+
const txBody = firstChild(this.sp, "p:txBody");
|
|
609
|
+
if (!txBody) return null;
|
|
610
|
+
const placeholder = this.type === "body" ? {
|
|
611
|
+
ph: {
|
|
612
|
+
type: this.type,
|
|
613
|
+
idx: this.idx ?? "0"
|
|
614
|
+
},
|
|
615
|
+
flatten: this.themeContext
|
|
616
|
+
} : void 0;
|
|
617
|
+
return new TextFrame(txBody, this.part, this.themeContext, placeholder, this.relationships);
|
|
618
|
+
}
|
|
619
|
+
/** The placeholder's flattened text (paragraphs joined by `\n`), or `''` when it has no text body. */
|
|
620
|
+
get text() {
|
|
621
|
+
return this.textFrame?.text ?? "";
|
|
622
|
+
}
|
|
623
|
+
/** Escape hatch: the underlying `p:sp` element. After mutating it call {@link markDirty}, or `save()` writes the original bytes. */
|
|
624
|
+
get element_() {
|
|
625
|
+
return this.sp;
|
|
626
|
+
}
|
|
627
|
+
/** Mark the owning notes-slide part dirty so `save()` reserializes it. Call after mutating {@link element_}. */
|
|
628
|
+
markDirty() {
|
|
629
|
+
this.part.markDirty();
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
/**
|
|
633
|
+
* A slide's speaker-notes slide (`notesSlideN.xml`) as a modeled object — the deep
|
|
634
|
+
* companion to {@link import('./slide.js').Slide.notesText}. Exposes the notes
|
|
635
|
+
* shape tree as {@link NotesPlaceholder}s ({@link body}/{@link slideImage}/
|
|
636
|
+
* {@link slideNumber}) plus the body {@link textFrame} the older getters flatten.
|
|
637
|
+
*
|
|
638
|
+
* The theme context (notesMaster → `theme2.xml`) is resolved once and shared across
|
|
639
|
+
* every placeholder's text frame.
|
|
640
|
+
*/
|
|
641
|
+
var NotesSlide = class {
|
|
642
|
+
opc;
|
|
643
|
+
part;
|
|
644
|
+
#themeContext;
|
|
645
|
+
constructor(opc, part) {
|
|
646
|
+
this.opc = opc;
|
|
647
|
+
this.part = part;
|
|
648
|
+
}
|
|
649
|
+
/** Partname of the notes slide part. */
|
|
650
|
+
get partName() {
|
|
651
|
+
return this.part.partName;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* The notes theme context, resolved through the notesMaster → `theme2.xml` chain
|
|
655
|
+
* and cached. Backs {@link NotesPlaceholder.textFrame}'s `Run.resolvedColor`.
|
|
656
|
+
*/
|
|
657
|
+
themeContext() {
|
|
658
|
+
return this.#themeContext ??= resolveNotesColorContext(this.opc, this.partName);
|
|
659
|
+
}
|
|
660
|
+
/** The placeholders (`p:sp`) in the notes shape tree, in document order. */
|
|
661
|
+
get placeholders() {
|
|
662
|
+
const spTree = this.#spTree();
|
|
663
|
+
if (!spTree) return [];
|
|
664
|
+
const ctx = this.themeContext();
|
|
665
|
+
const rels = this.opc.relationshipsFor(this.partName);
|
|
666
|
+
return getElements(spTree, "p:sp").map((sp) => new NotesPlaceholder(sp, this.part, ctx, rels));
|
|
667
|
+
}
|
|
668
|
+
/** The notes body placeholder (`p:ph type="body"`), or `null` when the notes slide has none. */
|
|
669
|
+
get body() {
|
|
670
|
+
return this.#byType("body");
|
|
671
|
+
}
|
|
672
|
+
/** The slide-thumbnail placeholder (`p:ph type="sldImg"`), or `null` when absent. */
|
|
673
|
+
get slideImage() {
|
|
674
|
+
return this.#byType("sldImg");
|
|
675
|
+
}
|
|
676
|
+
/** The slide-number placeholder (`p:ph type="sldNum"`, carrying the slide-number field), or `null`. */
|
|
677
|
+
get slideNumber() {
|
|
678
|
+
return this.#byType("sldNum");
|
|
679
|
+
}
|
|
680
|
+
/** The notes body as a navigable {@link TextFrame}, or `null` when there is no body text frame. */
|
|
681
|
+
get textFrame() {
|
|
682
|
+
return this.body?.textFrame ?? null;
|
|
683
|
+
}
|
|
684
|
+
/** The notes body text flattened (paragraphs joined by `\n`), or `''` when the body is empty/absent. */
|
|
685
|
+
get text() {
|
|
686
|
+
return this.body?.text ?? "";
|
|
687
|
+
}
|
|
688
|
+
#byType(type) {
|
|
689
|
+
return this.placeholders.find((ph) => ph.type === type) ?? null;
|
|
690
|
+
}
|
|
691
|
+
#spTree() {
|
|
692
|
+
const root = this.part.dom.documentElement;
|
|
693
|
+
const cSld = root && firstChild(root, "p:cSld");
|
|
694
|
+
return cSld ? firstChild(cSld, "p:spTree") : null;
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
//#endregion
|
|
698
|
+
//#region src/read/api/comments.ts
|
|
699
|
+
/** The slide → comments-part relationship type (legacy comments). */
|
|
700
|
+
const COMMENTS_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
|
|
701
|
+
/** The presentation → commentAuthors-part relationship type. */
|
|
702
|
+
const COMMENT_AUTHORS_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/commentAuthors";
|
|
703
|
+
/** Content type of a legacy per-slide comments part (`ppt/comments/commentN.xml`). */
|
|
704
|
+
const LEGACY_COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.presentationml.comments+xml";
|
|
705
|
+
/** The slide → modern-comments-part relationship type (2018 schema). */
|
|
706
|
+
const MODERN_COMMENTS_REL_TYPE = "http://schemas.microsoft.com/office/2018/10/relationships/comments";
|
|
707
|
+
/** The presentation → modern-authors-part relationship type (2018 schema). */
|
|
708
|
+
const MODERN_AUTHORS_REL_TYPE = "http://schemas.microsoft.com/office/2018/10/relationships/authors";
|
|
709
|
+
/** Content type of the modern per-slide comments part (`ppt/comments/modernComment_*.xml`). */
|
|
710
|
+
const MODERN_COMMENTS_CONTENT_TYPE = "application/vnd.ms-powerpoint.comments+xml";
|
|
711
|
+
/** Parse a `p:cmAuthorLst` root element into the author registry. */
|
|
712
|
+
function parseCommentAuthors(root) {
|
|
713
|
+
if (!root) return [];
|
|
714
|
+
return getElements(root, "p:cmAuthor").map((el) => ({
|
|
715
|
+
id: intValue(attr(el, "id")),
|
|
716
|
+
name: attr(el, "name") ?? "",
|
|
717
|
+
initials: attr(el, "initials") ?? "",
|
|
718
|
+
lastIdx: intValue(attr(el, "lastIdx")),
|
|
719
|
+
clrIdx: intValue(attr(el, "clrIdx"))
|
|
720
|
+
}));
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Read the deck-wide comment authors from `ppt/commentAuthors.xml`, resolved via the
|
|
724
|
+
* presentation part's `commentAuthors` relationship. `[]` when the deck has no comments.
|
|
725
|
+
*/
|
|
726
|
+
function readCommentAuthors(opc, presentationPartName) {
|
|
727
|
+
const rels = opc.relationshipsFor(presentationPartName);
|
|
728
|
+
const rel = rels.byType(COMMENT_AUTHORS_REL_TYPE)[0];
|
|
729
|
+
if (!rel) return [];
|
|
730
|
+
return parseCommentAuthors(opc.part(rels.resolveTarget(rel.id))?.dom.documentElement ?? null);
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Read one slide's legacy comments from its `comments/commentN.xml` part (resolved
|
|
734
|
+
* via the slide's `comments` relationship), resolving each comment's `@authorId`
|
|
735
|
+
* against `authors` (typically {@link readCommentAuthors}). `[]` when the slide has
|
|
736
|
+
* no comments part.
|
|
737
|
+
*/
|
|
738
|
+
function readSlideComments(opc, slidePart, authors) {
|
|
739
|
+
const rels = opc.relationshipsFor(slidePart.partName);
|
|
740
|
+
const rel = rels.byType(COMMENTS_REL_TYPE)[0];
|
|
741
|
+
if (!rel) return [];
|
|
742
|
+
const root = opc.part(rels.resolveTarget(rel.id))?.dom.documentElement;
|
|
743
|
+
if (!root) return [];
|
|
744
|
+
const byId = new Map(authors.map((a) => [a.id, a]));
|
|
745
|
+
return getElements(root, "p:cm").map((cm) => {
|
|
746
|
+
const authorId = intValue(attr(cm, "authorId"));
|
|
747
|
+
const author = authorId !== null ? byId.get(authorId) ?? null : null;
|
|
748
|
+
const pos = firstChild(cm, "p:pos");
|
|
749
|
+
const textEl = firstChild(cm, "p:text");
|
|
750
|
+
return {
|
|
751
|
+
author: author ? author.name : null,
|
|
752
|
+
authorInitials: author ? author.initials : null,
|
|
753
|
+
authorId,
|
|
754
|
+
idx: intValue(attr(cm, "idx")),
|
|
755
|
+
text: textEl?.textContent ?? "",
|
|
756
|
+
x: pos ? intValue(attr(pos, "x")) : null,
|
|
757
|
+
y: pos ? intValue(attr(pos, "y")) : null,
|
|
758
|
+
date: attr(cm, "dt") ?? null
|
|
759
|
+
};
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
/** Concatenate a `p188:txBody`'s paragraphs into plain text, joining paragraphs with `\n`. */
|
|
763
|
+
function modernCommentText(el) {
|
|
764
|
+
const txBody = firstChild(el, "p188:txBody");
|
|
765
|
+
if (!txBody) return "";
|
|
766
|
+
const paras = getElements(txBody, "a:p");
|
|
767
|
+
if (paras.length === 0) return txBody.textContent ?? "";
|
|
768
|
+
return paras.map((p) => p.textContent ?? "").join("\n");
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Decode one `p188:cm` (or `p188:reply`) element. Only the top-level comment
|
|
772
|
+
* carries a position and a reply list; replies pass `withThread: false` so they
|
|
773
|
+
* report `x`/`y` as `null` and never recurse (the schema forbids nested replies).
|
|
774
|
+
*/
|
|
775
|
+
function parseModernComment(el, byId, withThread) {
|
|
776
|
+
const authorId = attr(el, "authorId");
|
|
777
|
+
const author = authorId !== null ? byId.get(authorId) ?? null : null;
|
|
778
|
+
const pos = withThread ? firstChild(el, "p188:pos") : null;
|
|
779
|
+
const replyLst = withThread ? firstChild(el, "p188:replyLst") : null;
|
|
780
|
+
return {
|
|
781
|
+
id: attr(el, "id"),
|
|
782
|
+
author: author ? author.name : null,
|
|
783
|
+
authorInitials: author ? author.initials : null,
|
|
784
|
+
authorId,
|
|
785
|
+
created: attr(el, "created"),
|
|
786
|
+
text: modernCommentText(el),
|
|
787
|
+
x: pos ? intValue(attr(pos, "x")) : null,
|
|
788
|
+
y: pos ? intValue(attr(pos, "y")) : null,
|
|
789
|
+
replies: replyLst ? getElements(replyLst, "p188:reply").map((r) => parseModernComment(r, byId, false)) : []
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Read the deck-wide modern comment authors from `ppt/authors.xml`, resolved via
|
|
794
|
+
* the presentation part's 2018 `authors` relationship. `[]` when the deck has no
|
|
795
|
+
* modern authors part.
|
|
796
|
+
*/
|
|
797
|
+
function readModernCommentAuthors(opc, presentationPartName) {
|
|
798
|
+
const rels = opc.relationshipsFor(presentationPartName);
|
|
799
|
+
const rel = rels.byType(MODERN_AUTHORS_REL_TYPE)[0];
|
|
800
|
+
if (!rel) return [];
|
|
801
|
+
const root = opc.part(rels.resolveTarget(rel.id))?.dom.documentElement;
|
|
802
|
+
if (!root) return [];
|
|
803
|
+
return getElements(root, "p188:author").map((el) => ({
|
|
804
|
+
id: attr(el, "id"),
|
|
805
|
+
name: attr(el, "name") ?? "",
|
|
806
|
+
initials: attr(el, "initials") ?? "",
|
|
807
|
+
userId: attr(el, "userId"),
|
|
808
|
+
providerId: attr(el, "providerId")
|
|
809
|
+
}));
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Read one slide's modern comments from its `modernComment_*.xml` part (resolved
|
|
813
|
+
* via the slide's 2018 `comments` relationship), resolving each comment's and
|
|
814
|
+
* reply's `@authorId` GUID against `authors` (typically
|
|
815
|
+
* {@link readModernCommentAuthors}). Replies are nested, not flattened. `[]` when
|
|
816
|
+
* the slide has no modern comments part.
|
|
817
|
+
*/
|
|
818
|
+
function readModernSlideComments(opc, slidePart, authors) {
|
|
819
|
+
const rels = opc.relationshipsFor(slidePart.partName);
|
|
820
|
+
const rel = rels.byType(MODERN_COMMENTS_REL_TYPE)[0];
|
|
821
|
+
if (!rel) return [];
|
|
822
|
+
const root = opc.part(rels.resolveTarget(rel.id))?.dom.documentElement;
|
|
823
|
+
if (!root) return [];
|
|
824
|
+
const byId = new Map(authors.map((a) => [a.id, a]));
|
|
825
|
+
return getElements(root, "p188:cm").map((cm) => parseModernComment(cm, byId, true));
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Report which comment schema a deck uses: `'modern'` when it carries any 2018
|
|
829
|
+
* `modernComment_*` part, else `'legacy'` when it carries any classic
|
|
830
|
+
* `commentN.xml` part, else `'none'`. Lets a consumer pick the right accessor
|
|
831
|
+
* ({@link readModernSlideComments} vs {@link readSlideComments}) without probing
|
|
832
|
+
* both.
|
|
833
|
+
*/
|
|
834
|
+
function commentSchema(opc) {
|
|
835
|
+
if (opc.partsByContentType(MODERN_COMMENTS_CONTENT_TYPE).length > 0) return "modern";
|
|
836
|
+
if (opc.partsByContentType(LEGACY_COMMENTS_CONTENT_TYPE).length > 0) return "legacy";
|
|
837
|
+
return "none";
|
|
838
|
+
}
|
|
839
|
+
//#endregion
|
|
840
|
+
//#region src/read/api/tags.ts
|
|
841
|
+
/** The owner → tags-part relationship type. */
|
|
842
|
+
const TAGS_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags";
|
|
843
|
+
/**
|
|
844
|
+
* Read every tag referenced by `ownerPartName` (a slide part or the presentation
|
|
845
|
+
* part), resolving the owner's `tags` relationship(s) to their `ppt/tags/tagN.xml`
|
|
846
|
+
* part(s) and flattening each part's `p:tag` children into `{ name, val }`. `[]`
|
|
847
|
+
* when the owner references no tag part. Rel-driven rather than walking
|
|
848
|
+
* `p:custDataLst`, so it is robust to element ordering in the owner.
|
|
849
|
+
*/
|
|
850
|
+
function readTagsForPart(opc, ownerPartName) {
|
|
851
|
+
const rels = opc.relationshipsFor(ownerPartName);
|
|
852
|
+
const out = [];
|
|
853
|
+
for (const rel of rels.byType(TAGS_REL_TYPE)) {
|
|
854
|
+
const root = opc.part(rels.resolveTarget(rel.id))?.dom.documentElement;
|
|
855
|
+
if (!root) continue;
|
|
856
|
+
for (const tag of getElements(root, "p:tag")) out.push({
|
|
857
|
+
name: attr(tag, "name") ?? "",
|
|
858
|
+
val: attr(tag, "val") ?? ""
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
return out;
|
|
862
|
+
}
|
|
863
|
+
//#endregion
|
|
864
|
+
//#region src/read/api/chrome.ts
|
|
865
|
+
const SLIDE_MASTER_REL$4 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster";
|
|
866
|
+
const THEME_REL$1 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme";
|
|
867
|
+
const THEME_COLOR_SLOTS = [
|
|
868
|
+
"dk1",
|
|
869
|
+
"lt1",
|
|
870
|
+
"dk2",
|
|
871
|
+
"lt2",
|
|
872
|
+
"accent1",
|
|
873
|
+
"accent2",
|
|
874
|
+
"accent3",
|
|
875
|
+
"accent4",
|
|
876
|
+
"accent5",
|
|
877
|
+
"accent6",
|
|
878
|
+
"hlink",
|
|
879
|
+
"folHlink"
|
|
880
|
+
];
|
|
881
|
+
const COLOR_MAP_TOKENS = [
|
|
882
|
+
"bg1",
|
|
883
|
+
"tx1",
|
|
884
|
+
"bg2",
|
|
885
|
+
"tx2",
|
|
886
|
+
"accent1",
|
|
887
|
+
"accent2",
|
|
888
|
+
"accent3",
|
|
889
|
+
"accent4",
|
|
890
|
+
"accent5",
|
|
891
|
+
"accent6",
|
|
892
|
+
"hlink",
|
|
893
|
+
"folHlink"
|
|
894
|
+
];
|
|
895
|
+
/** Read `<a:latin>`/`<a:ea>`/`<a:cs>` typefaces of a theme font family; empty string ⇒ `null`. */
|
|
896
|
+
function readThemeFontFace(font) {
|
|
897
|
+
const face = (child) => {
|
|
898
|
+
const el = font && firstChild(font, child);
|
|
899
|
+
return el && attr(el, "typeface") || null;
|
|
900
|
+
};
|
|
901
|
+
return {
|
|
902
|
+
latin: face("a:latin"),
|
|
903
|
+
ea: face("a:ea"),
|
|
904
|
+
cs: face("a:cs")
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* A deck's theme part (`theme1.xml`) as a modeled object. Exposes the two property
|
|
909
|
+
* tiers a slide resolves against: the {@link colorScheme} (the 12 `a:clrScheme`
|
|
910
|
+
* slots as literal hex, `a:sysClr` resolved via its `lastClr`) and the
|
|
911
|
+
* {@link fontScheme} (major/minor Latin/EA/CS faces). Reachable from
|
|
912
|
+
* {@link SlideMaster.theme} and `Slide.theme`.
|
|
913
|
+
*/
|
|
914
|
+
var Theme = class {
|
|
915
|
+
part;
|
|
916
|
+
constructor(part) {
|
|
917
|
+
this.part = part;
|
|
918
|
+
}
|
|
919
|
+
/** Partname of the theme part. */
|
|
920
|
+
get partName() {
|
|
921
|
+
return this.part.partName;
|
|
922
|
+
}
|
|
923
|
+
/** Theme name (`a:theme/@name`, e.g. `Office Theme`), or `null` when unnamed. */
|
|
924
|
+
get name() {
|
|
925
|
+
const root = this.part.dom.documentElement;
|
|
926
|
+
return root ? attr(root, "name") : null;
|
|
927
|
+
}
|
|
928
|
+
/** Colour-scheme name (`a:clrScheme/@name`, e.g. `Office`), or `null`. */
|
|
929
|
+
get colorSchemeName() {
|
|
930
|
+
const clrScheme = this.#clrScheme();
|
|
931
|
+
return clrScheme ? attr(clrScheme, "name") : null;
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* The theme colour scheme: the 12 {@link ThemeColorSlot}s resolved to a 6-hex RGB
|
|
935
|
+
* (an `a:sysClr` slot resolves through its `lastClr`). A slot the theme does not
|
|
936
|
+
* define reads `null`. Use {@link color} to look one slot up.
|
|
937
|
+
*/
|
|
938
|
+
get colorScheme() {
|
|
939
|
+
const parsed = parseClrScheme(this.#clrScheme());
|
|
940
|
+
const out = {};
|
|
941
|
+
for (const slot of THEME_COLOR_SLOTS) out[slot] = parsed.get(slot) ?? null;
|
|
942
|
+
return out;
|
|
943
|
+
}
|
|
944
|
+
/** The 6-hex RGB of one colour-scheme slot, or `null` when the theme does not define it. */
|
|
945
|
+
color(slot) {
|
|
946
|
+
return parseClrScheme(this.#clrScheme()).get(slot) ?? null;
|
|
947
|
+
}
|
|
948
|
+
/** The theme's `a:fontScheme` (major/minor font families), or `null` when the theme declares none. */
|
|
949
|
+
get fontScheme() {
|
|
950
|
+
const themeElements = this.#themeElements();
|
|
951
|
+
const fontScheme = themeElements && firstChild(themeElements, "a:fontScheme");
|
|
952
|
+
if (!fontScheme) return null;
|
|
953
|
+
return {
|
|
954
|
+
name: attr(fontScheme, "name"),
|
|
955
|
+
major: readThemeFontFace(firstChild(fontScheme, "a:majorFont")),
|
|
956
|
+
minor: readThemeFontFace(firstChild(fontScheme, "a:minorFont"))
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
/** Escape hatch: the underlying `a:theme` element. After mutating it call {@link markDirty}, or `save()` writes the original bytes. */
|
|
960
|
+
get element_() {
|
|
961
|
+
return this.part.dom.documentElement;
|
|
962
|
+
}
|
|
963
|
+
/** Mark the theme part dirty so `save()` reserializes it. Call after mutating {@link element_}. */
|
|
964
|
+
markDirty() {
|
|
965
|
+
this.part.markDirty();
|
|
966
|
+
}
|
|
967
|
+
#themeElements() {
|
|
968
|
+
const root = this.part.dom.documentElement;
|
|
969
|
+
return root ? firstChild(root, "a:themeElements") : null;
|
|
970
|
+
}
|
|
971
|
+
#clrScheme() {
|
|
972
|
+
const themeElements = this.#themeElements();
|
|
973
|
+
return themeElements ? firstChild(themeElements, "a:clrScheme") : null;
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
/** One EMU coordinate from a placeholder's `p:spPr/a:xfrm` child, or `null` when absent. */
|
|
977
|
+
function xfrmEmu(sp, container, axis) {
|
|
978
|
+
const spPr = firstChild(sp, "p:spPr");
|
|
979
|
+
const xfrm = spPr && firstChild(spPr, "a:xfrm");
|
|
980
|
+
const el = xfrm && firstChild(xfrm, container);
|
|
981
|
+
return el ? intValue(attr(el, axis)) : null;
|
|
982
|
+
}
|
|
983
|
+
/** The `p:ph` element of a shape (`p:sp/p:nvSpPr/p:nvPr/p:ph`), or `null` when it is not a placeholder. */
|
|
984
|
+
function placeholderElement(sp) {
|
|
985
|
+
const nvSpPr = firstChild(sp, "p:nvSpPr");
|
|
986
|
+
const nvPr = nvSpPr && firstChild(nvSpPr, "p:nvPr");
|
|
987
|
+
return nvPr ? firstChild(nvPr, "p:ph") : null;
|
|
988
|
+
}
|
|
989
|
+
/** The placeholder shapes (`p:sp` carrying a `p:ph`) of a master/layout `p:spTree`, in document order. */
|
|
990
|
+
function placeholderShapes(spTree) {
|
|
991
|
+
if (!spTree) return [];
|
|
992
|
+
return getElements(spTree, "p:sp").filter((sp) => placeholderElement(sp) !== null);
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* One placeholder shape (`p:sp` with a `p:ph`) of a slide master or slide layout —
|
|
996
|
+
* the definition a slide's placeholder of the same `type`/`idx` inherits geometry
|
|
997
|
+
* and text style from. Geometry getters read the placeholder's *own* `a:xfrm`,
|
|
998
|
+
* which a master/layout placeholder carries explicitly (so it round-trips, unlike a
|
|
999
|
+
* notes placeholder's inherited geometry).
|
|
1000
|
+
*/
|
|
1001
|
+
var Placeholder = class {
|
|
1002
|
+
sp;
|
|
1003
|
+
part;
|
|
1004
|
+
themeContext;
|
|
1005
|
+
relationships;
|
|
1006
|
+
constructor(sp, part, themeContext, relationships) {
|
|
1007
|
+
this.sp = sp;
|
|
1008
|
+
this.part = part;
|
|
1009
|
+
this.themeContext = themeContext;
|
|
1010
|
+
this.relationships = relationships;
|
|
1011
|
+
}
|
|
1012
|
+
#cNvPr() {
|
|
1013
|
+
const nvSpPr = firstChild(this.sp, "p:nvSpPr");
|
|
1014
|
+
return nvSpPr ? firstChild(nvSpPr, "p:cNvPr") : null;
|
|
1015
|
+
}
|
|
1016
|
+
/** Placeholder type (`p:ph/@type`: `title` | `body` | `sldNum` | …), or `null` when absent (a body placeholder). */
|
|
1017
|
+
get type() {
|
|
1018
|
+
const ph = placeholderElement(this.sp);
|
|
1019
|
+
return ph ? attr(ph, "type") : null;
|
|
1020
|
+
}
|
|
1021
|
+
/** Placeholder index (`p:ph/@idx`), or `null` when unset. */
|
|
1022
|
+
get idx() {
|
|
1023
|
+
const ph = placeholderElement(this.sp);
|
|
1024
|
+
return ph ? attr(ph, "idx") : null;
|
|
1025
|
+
}
|
|
1026
|
+
/** Shape name (`p:cNvPr/@name`), or `''` when unnamed. */
|
|
1027
|
+
get name() {
|
|
1028
|
+
const cNvPr = this.#cNvPr();
|
|
1029
|
+
return (cNvPr && attr(cNvPr, "name")) ?? "";
|
|
1030
|
+
}
|
|
1031
|
+
/** Drawing id (`p:cNvPr/@id`), or `null` when absent. */
|
|
1032
|
+
get id() {
|
|
1033
|
+
const cNvPr = this.#cNvPr();
|
|
1034
|
+
return cNvPr ? intValue(attr(cNvPr, "id")) : null;
|
|
1035
|
+
}
|
|
1036
|
+
/** Left edge in EMU (`a:off/@x`), or `null` when the placeholder carries no own `a:xfrm`. */
|
|
1037
|
+
get left() {
|
|
1038
|
+
return xfrmEmu(this.sp, "a:off", "x");
|
|
1039
|
+
}
|
|
1040
|
+
/** Top edge in EMU (`a:off/@y`), or `null` when the placeholder carries no own `a:xfrm`. */
|
|
1041
|
+
get top() {
|
|
1042
|
+
return xfrmEmu(this.sp, "a:off", "y");
|
|
1043
|
+
}
|
|
1044
|
+
/** Width in EMU (`a:ext/@cx`), or `null` when the placeholder carries no own `a:xfrm`. */
|
|
1045
|
+
get width() {
|
|
1046
|
+
return xfrmEmu(this.sp, "a:ext", "cx");
|
|
1047
|
+
}
|
|
1048
|
+
/** Height in EMU (`a:ext/@cy`), or `null` when the placeholder carries no own `a:xfrm`. */
|
|
1049
|
+
get height() {
|
|
1050
|
+
return xfrmEmu(this.sp, "a:ext", "cy");
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* The placeholder's text as a navigable {@link TextFrame} (`p:txBody`), or `null`
|
|
1054
|
+
* when it carries no text body. Threaded with the owning master/layout theme
|
|
1055
|
+
* context so a run's own `schemeClr` resolves through `Run.resolvedColor`.
|
|
1056
|
+
*/
|
|
1057
|
+
get textFrame() {
|
|
1058
|
+
const txBody = firstChild(this.sp, "p:txBody");
|
|
1059
|
+
return txBody ? new TextFrame(txBody, this.part, this.themeContext, void 0, this.relationships) : null;
|
|
1060
|
+
}
|
|
1061
|
+
/** Escape hatch: the underlying `p:sp` element. After mutating it call {@link markDirty}, or `save()` writes the original bytes. */
|
|
1062
|
+
get element_() {
|
|
1063
|
+
return this.sp;
|
|
1064
|
+
}
|
|
1065
|
+
/** Mark the owning master/layout part dirty so `save()` reserializes it. Call after mutating {@link element_}. */
|
|
1066
|
+
markDirty() {
|
|
1067
|
+
this.part.markDirty();
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
/**
|
|
1071
|
+
* A deck's slide master (`slideMasterN.xml`) as a modeled object. Exposes the
|
|
1072
|
+
* property tiers a bound slide inherits: the {@link colorMap} (token → theme slot),
|
|
1073
|
+
* its {@link theme}, its {@link placeholders} (with geometry), its own
|
|
1074
|
+
* {@link background}, and the {@link layouts} that build on it. Reachable from
|
|
1075
|
+
* {@link SlideLayout.master} and `Slide.master`.
|
|
1076
|
+
*/
|
|
1077
|
+
var SlideMaster = class {
|
|
1078
|
+
opc;
|
|
1079
|
+
part;
|
|
1080
|
+
#themeContext;
|
|
1081
|
+
constructor(opc, part) {
|
|
1082
|
+
this.opc = opc;
|
|
1083
|
+
this.part = part;
|
|
1084
|
+
}
|
|
1085
|
+
/** Partname of the master part. */
|
|
1086
|
+
get partName() {
|
|
1087
|
+
return this.part.partName;
|
|
1088
|
+
}
|
|
1089
|
+
/** The master's authoring name (`p:cSld/@name`), or `''` when unnamed (the writer's default). */
|
|
1090
|
+
get name() {
|
|
1091
|
+
const cSld = this.#cSld();
|
|
1092
|
+
return (cSld && attr(cSld, "name")) ?? "";
|
|
1093
|
+
}
|
|
1094
|
+
/** The master's theme (via its `theme` relationship), or `null` when absent. */
|
|
1095
|
+
get theme() {
|
|
1096
|
+
const themePart = this.#relTarget(THEME_REL$1);
|
|
1097
|
+
return themePart ? new Theme(themePart) : null;
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* The master's colour map (`p:clrMap`): each of the 12 {@link ColorMapToken}s
|
|
1101
|
+
* mapped to the {@link ThemeColorSlot} it resolves through. This is the indirection
|
|
1102
|
+
* that lets a slide's `schemeClr val="tx1"` reach a theme slot (e.g. `tx1` → `dk1`).
|
|
1103
|
+
* A token the map omits reads `null`.
|
|
1104
|
+
*/
|
|
1105
|
+
get colorMap() {
|
|
1106
|
+
const root = this.part.dom.documentElement;
|
|
1107
|
+
const parsed = parseClrMap(root ? firstChild(root, "p:clrMap") : null);
|
|
1108
|
+
const out = {};
|
|
1109
|
+
for (const token of COLOR_MAP_TOKENS) out[token] = parsed.get(token) ?? null;
|
|
1110
|
+
return out;
|
|
1111
|
+
}
|
|
1112
|
+
/** The master's placeholder shapes (`p:sp` carrying a `p:ph`), in document order. */
|
|
1113
|
+
get placeholders() {
|
|
1114
|
+
const ctx = this.themeContext();
|
|
1115
|
+
const rels = this.opc.relationshipsFor(this.partName);
|
|
1116
|
+
return placeholderShapes(this.#spTree()).map((sp) => new Placeholder(sp, this.part, ctx, rels));
|
|
1117
|
+
}
|
|
1118
|
+
/** The layouts built on this master (via `p:sldLayoutIdLst` → its relationships), in list order. */
|
|
1119
|
+
get layouts() {
|
|
1120
|
+
const root = this.part.dom.documentElement;
|
|
1121
|
+
const lst = root && firstChild(root, "p:sldLayoutIdLst");
|
|
1122
|
+
if (!lst) return [];
|
|
1123
|
+
const rels = this.opc.relationshipsFor(this.partName);
|
|
1124
|
+
const out = [];
|
|
1125
|
+
for (const entry of getElements(lst, "p:sldLayoutId")) {
|
|
1126
|
+
const relId = attr(entry, "r:id");
|
|
1127
|
+
const part = relId ? this.opc.part(rels.resolveTarget(relId)) : null;
|
|
1128
|
+
if (part) out.push(new SlideLayout(this.opc, part));
|
|
1129
|
+
}
|
|
1130
|
+
return out;
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* The master's *own* background (`p:cSld/p:bg`), decoded, or `null` when it defines
|
|
1134
|
+
* none. Scoped to the master's own element — a slide's *effective* background,
|
|
1135
|
+
* resolved through the slide → layout → master chain, is `Slide.background`.
|
|
1136
|
+
*/
|
|
1137
|
+
get background() {
|
|
1138
|
+
const bg = backgroundElementOf(this.part.dom.documentElement);
|
|
1139
|
+
if (!bg) return null;
|
|
1140
|
+
const themePart = this.#relTarget(THEME_REL$1);
|
|
1141
|
+
return readSlideBackground(bg, "master", this.themeContext(), this.opc.relationshipsFor(this.partName), themePart ? this.opc.relationshipsFor(themePart.partName) : null);
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* The master's colour/font context (its `p:clrMap` + theme), resolved once and
|
|
1145
|
+
* cached. Backs each {@link Placeholder}'s text frame.
|
|
1146
|
+
*/
|
|
1147
|
+
themeContext() {
|
|
1148
|
+
return this.#themeContext ??= resolveMasterColorContext(this.opc, this.partName);
|
|
1149
|
+
}
|
|
1150
|
+
#relTarget(type) {
|
|
1151
|
+
const rels = this.opc.relationshipsFor(this.partName);
|
|
1152
|
+
const rel = rels.byType(type)[0];
|
|
1153
|
+
return rel ? this.opc.part(rels.resolveTarget(rel.id)) ?? null : null;
|
|
1154
|
+
}
|
|
1155
|
+
#cSld() {
|
|
1156
|
+
const root = this.part.dom.documentElement;
|
|
1157
|
+
return root ? firstChild(root, "p:cSld") : null;
|
|
1158
|
+
}
|
|
1159
|
+
#spTree() {
|
|
1160
|
+
const cSld = this.#cSld();
|
|
1161
|
+
return cSld ? firstChild(cSld, "p:spTree") : null;
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
/**
|
|
1165
|
+
* A deck's slide layout (`slideLayoutN.xml`) as a modeled object — the gallery entry
|
|
1166
|
+
* a slide binds to. Exposes its {@link name}, its {@link type} (import-only; the
|
|
1167
|
+
* writer authors none), its {@link master} (and, through it, the {@link theme}), its
|
|
1168
|
+
* {@link placeholders} (with geometry), and its own {@link background}. Reachable
|
|
1169
|
+
* from `Slide.layout` and {@link SlideMaster.layouts}.
|
|
1170
|
+
*/
|
|
1171
|
+
var SlideLayout = class {
|
|
1172
|
+
opc;
|
|
1173
|
+
part;
|
|
1174
|
+
#themeContext;
|
|
1175
|
+
constructor(opc, part) {
|
|
1176
|
+
this.opc = opc;
|
|
1177
|
+
this.part = part;
|
|
1178
|
+
}
|
|
1179
|
+
/** Partname of the layout part. */
|
|
1180
|
+
get partName() {
|
|
1181
|
+
return this.part.partName;
|
|
1182
|
+
}
|
|
1183
|
+
/** The layout's authoring name (`p:cSld/@name`, e.g. `Title and Content`), or `''` when unnamed. */
|
|
1184
|
+
get name() {
|
|
1185
|
+
const cSld = this.#cSld();
|
|
1186
|
+
return (cSld && attr(cSld, "name")) ?? "";
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* The layout type (`p:sldLayout/@type`, e.g. `title` | `obj` | `blank`), or `null`
|
|
1190
|
+
* when the layout declares none. Import-only: the writer authors no `@type`, so an
|
|
1191
|
+
* authored deck reads `null`; an imported deck carries PowerPoint's value.
|
|
1192
|
+
*/
|
|
1193
|
+
get type() {
|
|
1194
|
+
const root = this.part.dom.documentElement;
|
|
1195
|
+
return root ? attr(root, "type") : null;
|
|
1196
|
+
}
|
|
1197
|
+
/** The master this layout is built on (via its `slideMaster` relationship), or `null` when absent. */
|
|
1198
|
+
get master() {
|
|
1199
|
+
const rels = this.opc.relationshipsFor(this.partName);
|
|
1200
|
+
const rel = rels.byType(SLIDE_MASTER_REL$4)[0];
|
|
1201
|
+
const part = rel ? this.opc.part(rels.resolveTarget(rel.id)) : null;
|
|
1202
|
+
return part ? new SlideMaster(this.opc, part) : null;
|
|
1203
|
+
}
|
|
1204
|
+
/** The theme this layout resolves against, via its {@link master}, or `null`. */
|
|
1205
|
+
get theme() {
|
|
1206
|
+
return this.master?.theme ?? null;
|
|
1207
|
+
}
|
|
1208
|
+
/** The layout's placeholder shapes (`p:sp` carrying a `p:ph`), in document order. */
|
|
1209
|
+
get placeholders() {
|
|
1210
|
+
const ctx = this.themeContext();
|
|
1211
|
+
const rels = this.opc.relationshipsFor(this.partName);
|
|
1212
|
+
return placeholderShapes(this.#spTree()).map((sp) => new Placeholder(sp, this.part, ctx, rels));
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* The layout's *own* background (`p:cSld/p:bg`), decoded, or `null` when it defines
|
|
1216
|
+
* none. A layout usually authors one — an explicit fill (`p:bgPr`) or a
|
|
1217
|
+
* theme-indexed reference (`p:bgRef`). The slide's *effective* background is
|
|
1218
|
+
* `Slide.background`.
|
|
1219
|
+
*/
|
|
1220
|
+
get background() {
|
|
1221
|
+
const bg = backgroundElementOf(this.part.dom.documentElement);
|
|
1222
|
+
if (!bg) return null;
|
|
1223
|
+
const themePart = this.master?.theme?.part ?? null;
|
|
1224
|
+
return readSlideBackground(bg, "layout", this.themeContext(), this.opc.relationshipsFor(this.partName), themePart ? this.opc.relationshipsFor(themePart.partName) : null);
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* The layout's colour/font context (walked layout → master → theme), resolved once
|
|
1228
|
+
* and cached. Backs each {@link Placeholder}'s text frame.
|
|
1229
|
+
*/
|
|
1230
|
+
themeContext() {
|
|
1231
|
+
return this.#themeContext ??= resolveLayoutColorContext(this.opc, this.partName);
|
|
1232
|
+
}
|
|
1233
|
+
#cSld() {
|
|
1234
|
+
const root = this.part.dom.documentElement;
|
|
1235
|
+
return root ? firstChild(root, "p:cSld") : null;
|
|
1236
|
+
}
|
|
1237
|
+
#spTree() {
|
|
1238
|
+
const cSld = this.#cSld();
|
|
1239
|
+
return cSld ? firstChild(cSld, "p:spTree") : null;
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
//#endregion
|
|
1243
|
+
//#region src/read/api/transition.ts
|
|
1244
|
+
/**
|
|
1245
|
+
* Typed read/edit model for a slide's `p:transition` (slide-show transition).
|
|
1246
|
+
*
|
|
1247
|
+
* The element sits in `CT_Slide` between `p:clrMapOvr` and `p:timing` and comes
|
|
1248
|
+
* in two shapes PowerPoint emits:
|
|
1249
|
+
*
|
|
1250
|
+
* - **bare** `<p:transition spd=… advClick=… advTm=…><p:TYPE …/></p:transition>`
|
|
1251
|
+
* when the duration matches a coarse `spd` speed bucket; and
|
|
1252
|
+
* - **wrapped** `<mc:AlternateContent>` carrying a `<mc:Choice Requires="p14">`
|
|
1253
|
+
* whose `p:transition` adds the precise `p14:dur` (milliseconds), plus a base
|
|
1254
|
+
* `<mc:Fallback>` without it — emitted for an off-bucket exact duration.
|
|
1255
|
+
*
|
|
1256
|
+
* The getter prefers the `p14` Choice (so `durationMs` is recovered); the setter
|
|
1257
|
+
* emits the `mc:AlternateContent` form when `durationMs` is set and the bare form
|
|
1258
|
+
* otherwise. See `docs/animations-and-transitions.md`.
|
|
1259
|
+
*/
|
|
1260
|
+
/** Child element names of `p:transition` that are not the transition-type choice. */
|
|
1261
|
+
const NON_TYPE_CHILDREN = /* @__PURE__ */ new Set(["sndAc", "extLst"]);
|
|
1262
|
+
/** The single transition-type child element (the choice), or `null`. */
|
|
1263
|
+
function typeElement(transition) {
|
|
1264
|
+
for (const child of childElements(transition)) if (!NON_TYPE_CHILDREN.has(child.localName ?? "")) return child;
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
/** Namespace prefix for a URI, defaulting to `p` for the base presentationml namespace. */
|
|
1268
|
+
function prefixFor(uri) {
|
|
1269
|
+
if (!uri) return "p";
|
|
1270
|
+
for (const [prefix, ns] of Object.entries(OOXML_NS)) if (ns === uri) return prefix;
|
|
1271
|
+
if (uri === "http://schemas.microsoft.com/office/powerpoint/2012/main") return "p15";
|
|
1272
|
+
if (uri === "http://schemas.microsoft.com/office/powerpoint/2015/9/main") return "p159";
|
|
1273
|
+
return "p";
|
|
1274
|
+
}
|
|
1275
|
+
/** Collect a transition-type element's non-namespace attributes by local name. */
|
|
1276
|
+
function variantAttrs(element) {
|
|
1277
|
+
const out = {};
|
|
1278
|
+
const attrs = element.attributes;
|
|
1279
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
1280
|
+
const a = attrs[i];
|
|
1281
|
+
if (!a || a.prefix === "xmlns" || a.name === "xmlns") continue;
|
|
1282
|
+
out[a.localName ?? a.name] = a.value;
|
|
1283
|
+
}
|
|
1284
|
+
return out;
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Locate the slide's transition under the root `p:sld`, returning the
|
|
1288
|
+
* `p:transition` element to decode (the `p14` Choice when wrapped) and the
|
|
1289
|
+
* outermost node that represents it (the bare element or the `mc:AlternateContent`
|
|
1290
|
+
* wrapper), or `null` when the slide has no transition.
|
|
1291
|
+
*/
|
|
1292
|
+
function findTransition(root) {
|
|
1293
|
+
const bare = firstChild(root, "p:transition");
|
|
1294
|
+
if (bare) return {
|
|
1295
|
+
transition: bare,
|
|
1296
|
+
outer: bare
|
|
1297
|
+
};
|
|
1298
|
+
const altContent = firstChild(root, "mc:AlternateContent");
|
|
1299
|
+
if (altContent) {
|
|
1300
|
+
const choice = firstChild(altContent, "mc:Choice");
|
|
1301
|
+
const transition = choice ? firstChild(choice, "p:transition") : null;
|
|
1302
|
+
if (transition) return {
|
|
1303
|
+
transition,
|
|
1304
|
+
outer: altContent
|
|
1305
|
+
};
|
|
1306
|
+
const fallback = firstChild(altContent, "mc:Fallback");
|
|
1307
|
+
const fbTransition = fallback ? firstChild(fallback, "p:transition") : null;
|
|
1308
|
+
if (fbTransition) return {
|
|
1309
|
+
transition: fbTransition,
|
|
1310
|
+
outer: altContent
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
return null;
|
|
1314
|
+
}
|
|
1315
|
+
/** Decode a `p:transition` element into a {@link TransitionInfo}, or `null` when malformed. */
|
|
1316
|
+
function parseTransition(root) {
|
|
1317
|
+
const found = findTransition(root);
|
|
1318
|
+
if (!found) return null;
|
|
1319
|
+
const { transition } = found;
|
|
1320
|
+
const type = typeElement(transition);
|
|
1321
|
+
if (!type) return null;
|
|
1322
|
+
const advClick = boolValue(attr(transition, "advClick"));
|
|
1323
|
+
const advTm = intValue(attr(transition, "advTm"));
|
|
1324
|
+
const spd = attr(transition, "spd");
|
|
1325
|
+
return {
|
|
1326
|
+
type: type.localName ?? "",
|
|
1327
|
+
namespace: prefixFor(type.namespaceURI),
|
|
1328
|
+
speed: spd === "slow" || spd === "med" || spd === "fast" ? spd : "fast",
|
|
1329
|
+
durationMs: intValue(attr(transition, "p14:dur")),
|
|
1330
|
+
advanceOnClick: advClick ?? true,
|
|
1331
|
+
advanceAfterMs: advTm,
|
|
1332
|
+
variant: variantAttrs(type),
|
|
1333
|
+
sound: parseSound(transition)
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
/** Decode the `p:sndAc` sound-action child of a transition into a {@link TransitionSoundInfo}, or `null`. */
|
|
1337
|
+
function parseSound(transition) {
|
|
1338
|
+
const sndAc = firstChild(transition, "p:sndAc");
|
|
1339
|
+
if (!sndAc) return null;
|
|
1340
|
+
const stSnd = firstChild(sndAc, "p:stSnd");
|
|
1341
|
+
if (stSnd) {
|
|
1342
|
+
const snd = firstChild(stSnd, "p:snd");
|
|
1343
|
+
return {
|
|
1344
|
+
form: "start",
|
|
1345
|
+
loop: boolValue(attr(stSnd, "loop")) ?? false,
|
|
1346
|
+
embedRid: snd ? attr(snd, "r:embed") : null,
|
|
1347
|
+
name: snd ? attr(snd, "name") : null
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
if (firstChild(sndAc, "p:endSnd")) return {
|
|
1351
|
+
form: "stop",
|
|
1352
|
+
loop: false,
|
|
1353
|
+
embedRid: null,
|
|
1354
|
+
name: null
|
|
1355
|
+
};
|
|
1356
|
+
return null;
|
|
1357
|
+
}
|
|
1358
|
+
/** Map an exact duration (ms) to PowerPoint's coarse speed bucket. */
|
|
1359
|
+
function speedForDuration(durationMs) {
|
|
1360
|
+
if (durationMs <= 500) return "fast";
|
|
1361
|
+
if (durationMs <= 1e3) return "med";
|
|
1362
|
+
return "slow";
|
|
1363
|
+
}
|
|
1364
|
+
/** Build a `p:transition` element (without the `p14:dur` attribute) from an input. */
|
|
1365
|
+
function buildTransitionElement(doc, input, speed, withDur) {
|
|
1366
|
+
const transition = createElement(doc, "p:transition");
|
|
1367
|
+
if (speed) setAttr(transition, "spd", speed);
|
|
1368
|
+
if (withDur && typeof input.durationMs === "number") setAttr(transition, "p14:dur", String(Math.round(input.durationMs)));
|
|
1369
|
+
if (input.advanceOnClick === false) setAttr(transition, "advClick", "0");
|
|
1370
|
+
if (typeof input.advanceAfterMs === "number") setAttr(transition, "advTm", String(Math.round(input.advanceAfterMs)));
|
|
1371
|
+
const type = createElement(doc, `${input.namespace ?? "p"}:${input.type}`);
|
|
1372
|
+
for (const [name, value] of Object.entries(input.variant ?? {})) setAttr(type, name, value);
|
|
1373
|
+
transition.appendChild(type);
|
|
1374
|
+
return transition;
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Build the DOM node for a transition: the bare `p:transition` when no exact
|
|
1378
|
+
* duration is requested, or an `mc:AlternateContent` wrapper (a `p14` Choice
|
|
1379
|
+
* carrying `p14:dur` plus a base `mc:Fallback`) when `durationMs` is set.
|
|
1380
|
+
*/
|
|
1381
|
+
function buildTransition(doc, input) {
|
|
1382
|
+
const hasDuration = typeof input.durationMs === "number";
|
|
1383
|
+
const speed = input.speed ?? (hasDuration ? speedForDuration(input.durationMs) : null);
|
|
1384
|
+
if (!hasDuration) return buildTransitionElement(doc, input, speed, false);
|
|
1385
|
+
const alt = createElement(doc, "mc:AlternateContent");
|
|
1386
|
+
const choice = createElement(doc, "mc:Choice");
|
|
1387
|
+
setAttr(choice, "Requires", "p14");
|
|
1388
|
+
choice.appendChild(buildTransitionElement(doc, input, speed, true));
|
|
1389
|
+
alt.appendChild(choice);
|
|
1390
|
+
const fallback = createElement(doc, "mc:Fallback");
|
|
1391
|
+
fallback.appendChild(buildTransitionElement(doc, input, speed, false));
|
|
1392
|
+
alt.appendChild(fallback);
|
|
1393
|
+
return alt;
|
|
1394
|
+
}
|
|
1395
|
+
/** Remove any existing transition node (bare or wrapped) from the slide root. */
|
|
1396
|
+
function removeTransition(root) {
|
|
1397
|
+
const found = findTransition(root);
|
|
1398
|
+
if (found) root.removeChild(found.outer);
|
|
1399
|
+
}
|
|
1400
|
+
//#endregion
|
|
1401
|
+
//#region src/read/api/animation.ts
|
|
1402
|
+
/**
|
|
1403
|
+
* Spid-aware, otherwise-opaque handling of a slide's build-animation tree
|
|
1404
|
+
* (`p:timing` / `p:bldLst`).
|
|
1405
|
+
*
|
|
1406
|
+
* The `p:timing` subtree is `CT_TimeNodeList` — deeply recursive and modeled
|
|
1407
|
+
* here as opaque DOM (an unmodified slide round-trips it byte-identically for
|
|
1408
|
+
* free). The only structured data we extract or rewrite is the set of `spid`
|
|
1409
|
+
* references — `<p:spTgt spid>` inside the timing tree and `<p:bldP spid>` in the
|
|
1410
|
+
* build list — so animations stay coherent when shape ids change. Three purely
|
|
1411
|
+
* structural operations: **enumerate**, **remap**, and **prune**. See
|
|
1412
|
+
* `docs/animations-and-transitions.md` ("spid-awareness").
|
|
1413
|
+
*/
|
|
1414
|
+
const P_NS = OOXML_NS.p;
|
|
1415
|
+
/** Every element carrying an animation `spid` reference, across the timing tree and build list. */
|
|
1416
|
+
function spidElements(root) {
|
|
1417
|
+
const out = [];
|
|
1418
|
+
for (const el of root.getElementsByTagNameNS(OOXML_NS.p, "spTgt")) out.push(el);
|
|
1419
|
+
for (const el of root.getElementsByTagNameNS(OOXML_NS.p, "bldP")) out.push(el);
|
|
1420
|
+
return out;
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Whether the slide carries build animations — a `<p:bldP>` entry or any timing
|
|
1424
|
+
* node with a `presetID` (distinguishing animation timing from the media-loop
|
|
1425
|
+
* timing that has neither).
|
|
1426
|
+
*/
|
|
1427
|
+
function hasAnimations(root) {
|
|
1428
|
+
if (root.getElementsByTagNameNS(OOXML_NS.p, "bldP").length > 0) return true;
|
|
1429
|
+
for (const cTn of root.getElementsByTagNameNS(OOXML_NS.p, "cTn")) if (attr(cTn, "presetID") !== null) return true;
|
|
1430
|
+
return false;
|
|
1431
|
+
}
|
|
1432
|
+
/** The sorted, de-duplicated set of `spid`s referenced by the slide's animations. */
|
|
1433
|
+
function enumerateSpids(root) {
|
|
1434
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1435
|
+
for (const element of spidElements(root)) {
|
|
1436
|
+
const spid = intValue(attr(element, "spid"));
|
|
1437
|
+
if (spid !== null) seen.add(spid);
|
|
1438
|
+
}
|
|
1439
|
+
return [...seen].sort((a, b) => a - b);
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Rewrite every animation `spid` according to `mapping` (old → new). Returns
|
|
1443
|
+
* `true` when at least one reference changed, so callers can `markDirty()` only
|
|
1444
|
+
* when needed.
|
|
1445
|
+
*/
|
|
1446
|
+
function remapSpids(root, mapping) {
|
|
1447
|
+
let changed = false;
|
|
1448
|
+
for (const element of spidElements(root)) {
|
|
1449
|
+
const spid = intValue(attr(element, "spid"));
|
|
1450
|
+
if (spid === null) continue;
|
|
1451
|
+
const next = mapping.get(spid);
|
|
1452
|
+
if (next !== void 0 && next !== spid) {
|
|
1453
|
+
setAttr(element, "spid", String(next));
|
|
1454
|
+
changed = true;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
return changed;
|
|
1458
|
+
}
|
|
1459
|
+
/** The nearest ancestor `<p:par>` whose `<p:cTn>` carries a `presetID` (the effect node), or `null`. */
|
|
1460
|
+
function effectParFor(spTgt) {
|
|
1461
|
+
for (let node = spTgt.parentNode; node; node = node.parentNode) if (node.localName === "par" && node.namespaceURI === OOXML_NS.p) {
|
|
1462
|
+
const cTn = firstChild(node, "p:cTn");
|
|
1463
|
+
if (cTn && attr(cTn, "presetID") !== null) return node;
|
|
1464
|
+
}
|
|
1465
|
+
return null;
|
|
1466
|
+
}
|
|
1467
|
+
/** Whether a `<p:par>` wrapper node has become empty (its `p:cTn/p:childTnLst` has no element children). */
|
|
1468
|
+
function isEmptyWrapperPar(par) {
|
|
1469
|
+
if (par.localName !== "par" || par.namespaceURI !== OOXML_NS.p) return false;
|
|
1470
|
+
const cTn = firstChild(par, "p:cTn");
|
|
1471
|
+
if (!cTn || attr(cTn, "presetID") !== null) return false;
|
|
1472
|
+
const childTnLst = firstChild(cTn, "p:childTnLst");
|
|
1473
|
+
return !childTnLst || childElements(childTnLst).length === 0;
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Remove all animation traces of the given `spid`s: each shape's `<p:bldP>` and
|
|
1477
|
+
* the effect-level `<p:par>` nodes whose `<p:spTgt>` targets it, then any wrapper
|
|
1478
|
+
* `<p:par>` left empty by the removal. Keeps the opaque timing tree free of
|
|
1479
|
+
* dangling references when shapes are deleted. Returns `true` when anything was
|
|
1480
|
+
* removed.
|
|
1481
|
+
*/
|
|
1482
|
+
function pruneSpids(root, spids) {
|
|
1483
|
+
const drop = /* @__PURE__ */ new Set();
|
|
1484
|
+
for (const s of spids) drop.add(s);
|
|
1485
|
+
if (drop.size === 0) return false;
|
|
1486
|
+
let changed = false;
|
|
1487
|
+
const wrappersToCheck = /* @__PURE__ */ new Set();
|
|
1488
|
+
const spTgts = Array.from(root.getElementsByTagNameNS(OOXML_NS.p, "spTgt"));
|
|
1489
|
+
const effectPars = /* @__PURE__ */ new Set();
|
|
1490
|
+
for (const spTgt of spTgts) {
|
|
1491
|
+
const spid = intValue(attr(spTgt, "spid"));
|
|
1492
|
+
if (spid === null || !drop.has(spid)) continue;
|
|
1493
|
+
const effectPar = effectParFor(spTgt);
|
|
1494
|
+
if (effectPar) effectPars.add(effectPar);
|
|
1495
|
+
}
|
|
1496
|
+
for (const effectPar of effectPars) {
|
|
1497
|
+
const parent = effectPar.parentNode;
|
|
1498
|
+
effectPar.parentNode?.removeChild(effectPar);
|
|
1499
|
+
changed = true;
|
|
1500
|
+
const wrapperPar = parent?.parentNode?.parentNode;
|
|
1501
|
+
if (wrapperPar) wrappersToCheck.add(wrapperPar);
|
|
1502
|
+
}
|
|
1503
|
+
let progress = true;
|
|
1504
|
+
while (progress) {
|
|
1505
|
+
progress = false;
|
|
1506
|
+
for (const wrapper of [...wrappersToCheck]) if (isEmptyWrapperPar(wrapper)) {
|
|
1507
|
+
const grandWrapper = wrapper.parentNode?.parentNode?.parentNode;
|
|
1508
|
+
wrapper.parentNode?.removeChild(wrapper);
|
|
1509
|
+
wrappersToCheck.delete(wrapper);
|
|
1510
|
+
if (grandWrapper) wrappersToCheck.add(grandWrapper);
|
|
1511
|
+
progress = true;
|
|
1512
|
+
changed = true;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
const bldPs = Array.from(root.getElementsByTagNameNS(OOXML_NS.p, "bldP"));
|
|
1516
|
+
for (const bldP of bldPs) {
|
|
1517
|
+
const spid = intValue(attr(bldP, "spid"));
|
|
1518
|
+
if (spid !== null && drop.has(spid)) {
|
|
1519
|
+
bldP.parentNode?.removeChild(bldP);
|
|
1520
|
+
changed = true;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
const bldLsts = Array.from(root.getElementsByTagNameNS(OOXML_NS.p, "bldLst"));
|
|
1524
|
+
for (const bldLst of bldLsts) if (childElements(bldLst).length === 0) bldLst.parentNode?.removeChild(bldLst);
|
|
1525
|
+
return changed;
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Flatten the whole slide to its final static state by removing the build
|
|
1529
|
+
* animation timeline: drop the `<p:timing>` block (which carries the `<p:bldLst>`
|
|
1530
|
+
* and the effect tree) so every shape renders at once with no click-through
|
|
1531
|
+
* staging. Gated on {@link hasAnimations}: a `<p:timing>` that is purely a
|
|
1532
|
+
* media loop (no `<p:bldP>` and no `presetID`-bearing node) is left untouched so
|
|
1533
|
+
* media playback survives. Returns `true` when a timing block was removed.
|
|
1534
|
+
*
|
|
1535
|
+
* This is the whole-slide counterpart to {@link pruneSpids} (which drops the
|
|
1536
|
+
* builds of specific shapes). It only removes staging; it never deletes shapes,
|
|
1537
|
+
* so a slide that animated alternating states over the same region will show
|
|
1538
|
+
* them all at once after flattening — that is "drop animations", distinct from
|
|
1539
|
+
* "remove staged/duplicate shapes".
|
|
1540
|
+
*/
|
|
1541
|
+
function flattenAnimations(root) {
|
|
1542
|
+
if (!hasAnimations(root)) return false;
|
|
1543
|
+
const timing = firstChild(root, "p:timing");
|
|
1544
|
+
if (!timing) return false;
|
|
1545
|
+
timing.parentNode?.removeChild(timing);
|
|
1546
|
+
return true;
|
|
1547
|
+
}
|
|
1548
|
+
const P_XMLNS = `xmlns:p="${P_NS}"`;
|
|
1549
|
+
/** A fresh `p:timing` carrying only an empty `tmRoot` (no effects yet). */
|
|
1550
|
+
const TIMING_SCAFFOLD = `<p:timing ${P_XMLNS}><p:tnLst><p:par><p:cTn id="1" dur="indefinite" restart="never" nodeType="tmRoot"><p:childTnLst/></p:cTn></p:par></p:tnLst></p:timing>`;
|
|
1551
|
+
/** A fresh `mainSeq` `p:seq` (its `cTn@id` is filled in by the caller). */
|
|
1552
|
+
const MAIN_SEQ_SCAFFOLD = `<p:seq ${P_XMLNS} concurrent="1" nextAc="seek"><p:cTn id="0" dur="indefinite" nodeType="mainSeq"><p:childTnLst/></p:cTn><p:prevCondLst><p:cond evt="onPrev" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:prevCondLst><p:nextCondLst><p:cond evt="onNext" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:nextCondLst></p:seq>`;
|
|
1553
|
+
/** Largest `<p:cTn @id>` in a subtree (0 when none). */
|
|
1554
|
+
function maxCTnId(scope) {
|
|
1555
|
+
let max = 0;
|
|
1556
|
+
for (const cTn of scope.getElementsByTagNameNS(P_NS, "cTn")) {
|
|
1557
|
+
const id = intValue(attr(cTn, "id"));
|
|
1558
|
+
if (id !== null && id > max) max = id;
|
|
1559
|
+
}
|
|
1560
|
+
return max;
|
|
1561
|
+
}
|
|
1562
|
+
/** Renumber every `<p:cTn @id>` in `node` (document order) starting at `start`; returns the next free id. */
|
|
1563
|
+
function renumberCTnIds(node, start) {
|
|
1564
|
+
let id = start;
|
|
1565
|
+
for (const cTn of node.getElementsByTagNameNS(P_NS, "cTn")) setAttr(cTn, "id", String(id++));
|
|
1566
|
+
return id;
|
|
1567
|
+
}
|
|
1568
|
+
/** Whether `par`'s subtree targets any of `spids` via a `<p:spTgt>`. */
|
|
1569
|
+
function targetsAnySpid(par, spids) {
|
|
1570
|
+
for (const spTgt of par.getElementsByTagNameNS(P_NS, "spTgt")) {
|
|
1571
|
+
const spid = intValue(attr(spTgt, "spid"));
|
|
1572
|
+
if (spid !== null && spids.has(spid)) return true;
|
|
1573
|
+
}
|
|
1574
|
+
return false;
|
|
1575
|
+
}
|
|
1576
|
+
/** Parse a namespaced scaffold string and import its root into `doc`. */
|
|
1577
|
+
function importScaffold(doc, xml) {
|
|
1578
|
+
return doc.importNode(parseXml(xml).documentElement, true);
|
|
1579
|
+
}
|
|
1580
|
+
/** The destination `mainSeq` `p:childTnLst` to append click groups into, creating the timing/seq scaffold as needed. */
|
|
1581
|
+
function getOrCreateMainSeqChildTnLst(root, doc) {
|
|
1582
|
+
let timing = firstChild(root, "p:timing");
|
|
1583
|
+
if (!timing) {
|
|
1584
|
+
timing = importScaffold(doc, TIMING_SCAFFOLD);
|
|
1585
|
+
insertInOrder(root, timing, ["p:extLst"]);
|
|
1586
|
+
}
|
|
1587
|
+
const existingSeq = timing.getElementsByTagNameNS(P_NS, "seq")[0];
|
|
1588
|
+
if (existingSeq) {
|
|
1589
|
+
const seqCTn = firstChild(existingSeq, "p:cTn");
|
|
1590
|
+
if (seqCTn) {
|
|
1591
|
+
let childLst = firstChild(seqCTn, "p:childTnLst");
|
|
1592
|
+
if (!childLst) {
|
|
1593
|
+
childLst = createElement(doc, "p:childTnLst");
|
|
1594
|
+
seqCTn.appendChild(childLst);
|
|
1595
|
+
}
|
|
1596
|
+
return childLst;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
let tmRootChildLst = timing.getElementsByTagNameNS(P_NS, "childTnLst")[0];
|
|
1600
|
+
if (!tmRootChildLst) {
|
|
1601
|
+
const freshTnLst = firstChild(importScaffold(doc, TIMING_SCAFFOLD), "p:tnLst");
|
|
1602
|
+
if (freshTnLst) insertInOrder(timing, freshTnLst, ["p:bldLst", "p:extLst"]);
|
|
1603
|
+
tmRootChildLst = timing.getElementsByTagNameNS(P_NS, "childTnLst")[0];
|
|
1604
|
+
}
|
|
1605
|
+
if (!tmRootChildLst) throw new InternalError("animation/timing-scaffold-failed", "getOrCreateMainSeqChildTnLst: failed to resolve or create the tmRoot child list");
|
|
1606
|
+
const seq = importScaffold(doc, MAIN_SEQ_SCAFFOLD);
|
|
1607
|
+
setAttr(firstChild(seq, "p:cTn"), "id", String(maxCTnId(timing) + 1));
|
|
1608
|
+
tmRootChildLst.insertBefore(seq, tmRootChildLst.firstChild);
|
|
1609
|
+
return firstChild(firstChild(seq, "p:cTn"), "p:childTnLst");
|
|
1610
|
+
}
|
|
1611
|
+
/** The destination `p:bldLst` (under `p:timing`), creating it before any `p:extLst`. */
|
|
1612
|
+
function getOrCreateBldLst(timing, doc) {
|
|
1613
|
+
let bldLst = firstChild(timing, "p:bldLst");
|
|
1614
|
+
if (!bldLst) {
|
|
1615
|
+
bldLst = createElement(doc, "p:bldLst");
|
|
1616
|
+
insertInOrder(timing, bldLst, ["p:extLst"]);
|
|
1617
|
+
}
|
|
1618
|
+
return bldLst;
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Carry the build animation of one or more copied shapes from `sourceRoot`'s
|
|
1622
|
+
* timing into `targetRoot`'s, remapping shape ids via `spidMap` (source id → new
|
|
1623
|
+
* id on the destination). For each mapped shape: its mainSeq click-group `<p:par>`
|
|
1624
|
+
* (the whole click step) and `<p:bldP>` are cloned, their `spid`s remapped, their
|
|
1625
|
+
* `<p:cTn>` ids renumbered to stay collision-free in the destination, and appended
|
|
1626
|
+
* after any existing build — matching how PowerPoint merges a pasted shape's
|
|
1627
|
+
* animation. Returns `true` when anything was carried (so the caller can
|
|
1628
|
+
* `markDirty()`), `false` when the source shapes have no animation.
|
|
1629
|
+
*/
|
|
1630
|
+
function carryShapeAnimations(sourceRoot, targetRoot, spidMap) {
|
|
1631
|
+
const sourceTiming = firstChild(sourceRoot, "p:timing");
|
|
1632
|
+
if (!sourceTiming) return false;
|
|
1633
|
+
const carriedSpids = new Set(spidMap.keys());
|
|
1634
|
+
const sourceSeq = sourceTiming.getElementsByTagNameNS(P_NS, "seq")[0];
|
|
1635
|
+
const groups = [];
|
|
1636
|
+
if (sourceSeq) {
|
|
1637
|
+
const seqCTn = firstChild(sourceSeq, "p:cTn");
|
|
1638
|
+
const seqChildLst = seqCTn ? firstChild(seqCTn, "p:childTnLst") : null;
|
|
1639
|
+
if (seqChildLst) {
|
|
1640
|
+
for (const par of childElements(seqChildLst)) if (targetsAnySpid(par, carriedSpids)) groups.push(par);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
const sourceBldLst = firstChild(sourceTiming, "p:bldLst");
|
|
1644
|
+
const bldPs = [];
|
|
1645
|
+
if (sourceBldLst) for (const bldP of childElements(sourceBldLst)) {
|
|
1646
|
+
const spid = intValue(attr(bldP, "spid"));
|
|
1647
|
+
if (spid !== null && carriedSpids.has(spid)) bldPs.push(bldP);
|
|
1648
|
+
}
|
|
1649
|
+
if (groups.length === 0 && bldPs.length === 0) return false;
|
|
1650
|
+
const doc = targetRoot.ownerDocument;
|
|
1651
|
+
const destChildLst = getOrCreateMainSeqChildTnLst(targetRoot, doc);
|
|
1652
|
+
const destTiming = firstChild(targetRoot, "p:timing");
|
|
1653
|
+
let nextId = maxCTnId(destTiming) + 1;
|
|
1654
|
+
for (const par of groups) {
|
|
1655
|
+
const clone = doc.importNode(par, true);
|
|
1656
|
+
remapSpids(clone, spidMap);
|
|
1657
|
+
nextId = renumberCTnIds(clone, nextId);
|
|
1658
|
+
destChildLst.appendChild(clone);
|
|
1659
|
+
}
|
|
1660
|
+
if (bldPs.length > 0) {
|
|
1661
|
+
const destBldLst = getOrCreateBldLst(destTiming, doc);
|
|
1662
|
+
for (const bldP of bldPs) {
|
|
1663
|
+
const clone = doc.importNode(bldP, true);
|
|
1664
|
+
const spid = intValue(attr(clone, "spid"));
|
|
1665
|
+
if (spid !== null && spidMap.has(spid)) setAttr(clone, "spid", String(spidMap.get(spid)));
|
|
1666
|
+
destBldLst.appendChild(clone);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
return true;
|
|
1670
|
+
}
|
|
1671
|
+
//#endregion
|
|
1672
|
+
//#region src/read/api/slide.ts
|
|
1673
|
+
const IMAGE_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
|
|
1674
|
+
const NOTES_SLIDE_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide";
|
|
1675
|
+
const SLIDE_LAYOUT_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout";
|
|
1676
|
+
/** Recognize a handful of common raster formats from their leading bytes. */
|
|
1677
|
+
function sniffImageType(bytes) {
|
|
1678
|
+
const b = bytes;
|
|
1679
|
+
if (b.length >= 8 && b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71) return {
|
|
1680
|
+
extension: "png",
|
|
1681
|
+
contentType: "image/png"
|
|
1682
|
+
};
|
|
1683
|
+
if (b.length >= 3 && b[0] === 255 && b[1] === 216 && b[2] === 255) return {
|
|
1684
|
+
extension: "jpeg",
|
|
1685
|
+
contentType: "image/jpeg"
|
|
1686
|
+
};
|
|
1687
|
+
if (b.length >= 4 && b[0] === 71 && b[1] === 73 && b[2] === 70 && b[3] === 56) return {
|
|
1688
|
+
extension: "gif",
|
|
1689
|
+
contentType: "image/gif"
|
|
1690
|
+
};
|
|
1691
|
+
if (b.length >= 2 && b[0] === 66 && b[1] === 77) return {
|
|
1692
|
+
extension: "bmp",
|
|
1693
|
+
contentType: "image/bmp"
|
|
1694
|
+
};
|
|
1695
|
+
if (b.length >= 4 && (b[0] === 73 && b[1] === 73 && b[2] === 42 && b[3] === 0 || b[0] === 77 && b[1] === 77 && b[2] === 0 && b[3] === 42)) return {
|
|
1696
|
+
extension: "tiff",
|
|
1697
|
+
contentType: "image/tiff"
|
|
1698
|
+
};
|
|
1699
|
+
if (b.length >= 12 && b[0] === 82 && b[1] === 73 && b[2] === 70 && b[3] === 70 && b[8] === 87 && b[9] === 69 && b[10] === 66 && b[11] === 80) return {
|
|
1700
|
+
extension: "webp",
|
|
1701
|
+
contentType: "image/webp"
|
|
1702
|
+
};
|
|
1703
|
+
return null;
|
|
1704
|
+
}
|
|
1705
|
+
var Slide = class {
|
|
1706
|
+
presentation;
|
|
1707
|
+
part;
|
|
1708
|
+
slideId;
|
|
1709
|
+
index;
|
|
1710
|
+
constructor(presentation, part, slideId, index) {
|
|
1711
|
+
this.presentation = presentation;
|
|
1712
|
+
this.part = part;
|
|
1713
|
+
this.slideId = slideId;
|
|
1714
|
+
this.index = index;
|
|
1715
|
+
}
|
|
1716
|
+
/** Partname of this slide's part. */
|
|
1717
|
+
get partName() {
|
|
1718
|
+
return this.part.partName;
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* Escape hatch: the underlying `p:sld` element. After mutating it call
|
|
1722
|
+
* {@link markDirty}, or `save()` writes the original bytes. Slide-level DOM
|
|
1723
|
+
* access is also reachable as `slide.part.dom`; this getter is the same node,
|
|
1724
|
+
* on the same rung of the ladder as `Shape.element_` and the rest.
|
|
1725
|
+
*/
|
|
1726
|
+
get element_() {
|
|
1727
|
+
const root = this.part.dom.documentElement;
|
|
1728
|
+
if (!root) throw new PackageReadError("package/part-has-no-root", `Slide ${this.partName} has no root <p:sld> element`);
|
|
1729
|
+
return root;
|
|
1730
|
+
}
|
|
1731
|
+
/** Mark this slide's part dirty so `save()` reserializes it. Call after mutating {@link element_}. */
|
|
1732
|
+
markDirty() {
|
|
1733
|
+
this.part.markDirty();
|
|
1734
|
+
}
|
|
1735
|
+
/** This slide part's relationships (image embeds, layout, hyperlinks, …). */
|
|
1736
|
+
get relationships() {
|
|
1737
|
+
return this.presentation.opc.relationshipsFor(this.partName);
|
|
1738
|
+
}
|
|
1739
|
+
#themeColors;
|
|
1740
|
+
/**
|
|
1741
|
+
* The slide's resolved theme context (`clrMap` + `clrScheme` + the theme
|
|
1742
|
+
* `fmtScheme` + `fontScheme`, plus the layout/master roots), walked once from
|
|
1743
|
+
* slide → layout → master → theme and cached on this proxy. Backs the read-model
|
|
1744
|
+
* resolved getters ({@link Shape.resolvedFill}, `Run.resolvedColor`,
|
|
1745
|
+
* `Run.resolvedSizePt`, `Run.resolvedFontFace`) so a `schemeClr` token — including
|
|
1746
|
+
* one delivered through a shape's `p:style` `fillRef`/`lnRef` — resolves to a
|
|
1747
|
+
* literal hex, and a placeholder run's inherited size/typeface (a `+mj-*`/`+mn-*`
|
|
1748
|
+
* font token included) resolves to a literal value. The maps/roots are empty when
|
|
1749
|
+
* the theme chain is incomplete, in which case tokens simply stay unresolved.
|
|
1750
|
+
*/
|
|
1751
|
+
themeContext() {
|
|
1752
|
+
return this.#themeColors ??= resolveSlideColorContext(this.presentation.opc, this.partName);
|
|
1753
|
+
}
|
|
1754
|
+
/** Authoring name of the slide (`p:cSld/@name`), or `null` if unnamed. */
|
|
1755
|
+
get name() {
|
|
1756
|
+
const cSld = this.#cSld();
|
|
1757
|
+
return cSld ? attr(cSld, "name") : null;
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Whether this slide is hidden (`p:sld/@show="0"`). The attribute is
|
|
1761
|
+
* `xsd:boolean` defaulting to `true`, so an absent attribute means shown.
|
|
1762
|
+
* Hidden slides are dropped from PowerPoint/LibreOffice presentations and
|
|
1763
|
+
* exported PDFs, so render order diverges from model order when any earlier
|
|
1764
|
+
* slide is hidden.
|
|
1765
|
+
*/
|
|
1766
|
+
get hidden() {
|
|
1767
|
+
const root = this.part.dom.documentElement;
|
|
1768
|
+
const show = root ? attr(root, "show") : null;
|
|
1769
|
+
return show === "0" || show === "false";
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Hide or show this slide. Hiding writes `p:sld/@show="0"`; showing removes
|
|
1773
|
+
* the attribute, restoring PowerPoint's canonical shown form (absent ⇒ shown).
|
|
1774
|
+
* Marks the slide part dirty.
|
|
1775
|
+
*/
|
|
1776
|
+
set hidden(value) {
|
|
1777
|
+
const root = this.part.dom.documentElement;
|
|
1778
|
+
if (!root) throw new PackageReadError("package/part-has-no-root", `Slide ${this.partName} has no root <p:sld> element`);
|
|
1779
|
+
if (value) setAttr(root, "show", "0");
|
|
1780
|
+
else removeAttr(root, "show");
|
|
1781
|
+
this.part.markDirty();
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* The slide's show transition (`p:transition`), decoded into a typed model, or
|
|
1785
|
+
* `null` when the slide has none. Handles both PowerPoint forms: the bare
|
|
1786
|
+
* `<p:transition>` and the `mc:AlternateContent` wrapper that carries the exact
|
|
1787
|
+
* `p14:dur` duration (the `p14` Choice is preferred so `durationMs` is recovered).
|
|
1788
|
+
*/
|
|
1789
|
+
get transition() {
|
|
1790
|
+
const root = this.part.dom.documentElement;
|
|
1791
|
+
return root ? parseTransition(root) : null;
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* Set or clear the slide's show transition. Assigning `null` removes it.
|
|
1795
|
+
* Writing a transition with `durationMs` emits the `mc:AlternateContent` form
|
|
1796
|
+
* (a `p14` Choice carrying `p14:dur` plus a base `mc:Fallback`); otherwise the
|
|
1797
|
+
* bare `<p:transition>` is written. The node is inserted at its schema slot —
|
|
1798
|
+
* after `p:clrMapOvr`, before `p:timing`/`p:extLst`. Marks the slide part dirty.
|
|
1799
|
+
*/
|
|
1800
|
+
set transition(value) {
|
|
1801
|
+
const root = this.part.dom.documentElement;
|
|
1802
|
+
if (!root) throw new PackageReadError("package/part-has-no-root", `Slide ${this.partName} has no root <p:sld> element`);
|
|
1803
|
+
removeTransition(root);
|
|
1804
|
+
if (value) {
|
|
1805
|
+
const doc = this.part.dom;
|
|
1806
|
+
insertInOrder(root, buildTransition(doc, value), ["p:timing", "p:extLst"]);
|
|
1807
|
+
}
|
|
1808
|
+
this.part.markDirty();
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Whether the slide carries build animations (`p:timing` with a `<p:bldP>` or a
|
|
1812
|
+
* `presetID`-bearing time node). The animation tree itself is preserved opaquely;
|
|
1813
|
+
* see {@link animationSpids}.
|
|
1814
|
+
*/
|
|
1815
|
+
get hasAnimations() {
|
|
1816
|
+
const root = this.part.dom.documentElement;
|
|
1817
|
+
return root ? hasAnimations(root) : false;
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* @internal The sorted, de-duplicated set of shape ids (`spid`) referenced by
|
|
1821
|
+
* the slide's animations (`<p:spTgt spid>` and `<p:bldP spid>`). Exposed for the
|
|
1822
|
+
* import paths' spid-coherence checks.
|
|
1823
|
+
*/
|
|
1824
|
+
animationSpids() {
|
|
1825
|
+
const root = this.part.dom.documentElement;
|
|
1826
|
+
return root ? enumerateSpids(root) : [];
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* @internal Rewrite every animation `spid` per `mapping` (old → new), keeping
|
|
1830
|
+
* the opaque timing tree coherent when shape ids are reassigned. Marks the slide
|
|
1831
|
+
* part dirty only when a reference changed.
|
|
1832
|
+
*/
|
|
1833
|
+
remapAnimationSpids(mapping) {
|
|
1834
|
+
const root = this.part.dom.documentElement;
|
|
1835
|
+
if (root && remapSpids(root, mapping)) this.part.markDirty();
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* @internal Remove the build animations targeting the given shape ids (their
|
|
1839
|
+
* `<p:bldP>` and effect nodes), so deleting a shape never leaves a dangling
|
|
1840
|
+
* `spid` reference. Marks the slide part dirty only when something was removed.
|
|
1841
|
+
*/
|
|
1842
|
+
pruneAnimationSpids(spids) {
|
|
1843
|
+
const root = this.part.dom.documentElement;
|
|
1844
|
+
if (root && pruneSpids(root, spids)) this.part.markDirty();
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Flatten the slide's build animations: remove the `<p:timing>` block so the
|
|
1848
|
+
* slide renders and edits as its final static state, with every shape shown at
|
|
1849
|
+
* once. Gated like {@link hasAnimations} — a `<p:timing>` that is purely a media
|
|
1850
|
+
* loop (no `<p:bldP>` or `presetID`) is preserved so media playback survives.
|
|
1851
|
+
* Marks the slide part dirty and returns `true` only when a timing block was
|
|
1852
|
+
* removed.
|
|
1853
|
+
*
|
|
1854
|
+
* This removes click-through staging only; it does not delete shapes. If a slide
|
|
1855
|
+
* animated alternating states over the same region, the flattened render shows
|
|
1856
|
+
* them all at once — keep that distinct from removing staged/duplicate shapes.
|
|
1857
|
+
*/
|
|
1858
|
+
flattenAnimations() {
|
|
1859
|
+
const root = this.part.dom.documentElement;
|
|
1860
|
+
if (root && flattenAnimations(root)) {
|
|
1861
|
+
this.part.markDirty();
|
|
1862
|
+
return true;
|
|
1863
|
+
}
|
|
1864
|
+
return false;
|
|
1865
|
+
}
|
|
1866
|
+
/** Top-level shapes in the slide's shape tree, in document order. */
|
|
1867
|
+
get shapes() {
|
|
1868
|
+
const spTree = this.#spTree();
|
|
1869
|
+
return spTree ? buildShapes(spTree, this) : [];
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* All text on the slide, flattened in document order — the read-model
|
|
1873
|
+
* counterpart to {@link TextFrame.text} one level up. Every text-bearing shape
|
|
1874
|
+
* contributes its text, recursing into groups ({@link GroupShape}) and reading
|
|
1875
|
+
* table cells ({@link GraphicFrame.table}); text-free shapes (pictures,
|
|
1876
|
+
* connectors, empty boxes) contribute nothing. Blocks are joined by `\n`; within
|
|
1877
|
+
* a table, cells in a row are joined by `\t` and rows by `\n`.
|
|
1878
|
+
*
|
|
1879
|
+
* This is deliberately scoped to the slide's own shape tree. It does **not**
|
|
1880
|
+
* include speaker notes (read those via {@link notesText}) or chart data labels
|
|
1881
|
+
* (read those via {@link GraphicFrame.chart}); folding those in would force
|
|
1882
|
+
* ordering and separator choices a caller is better placed to make. Extract a
|
|
1883
|
+
* whole deck with `deck.slides.map((s) => s.text)`.
|
|
1884
|
+
*/
|
|
1885
|
+
get text() {
|
|
1886
|
+
const blocks = [];
|
|
1887
|
+
const walk = (shapes) => {
|
|
1888
|
+
for (const shape of shapes) if (shape instanceof GroupShape) walk(shape.shapes);
|
|
1889
|
+
else if (shape instanceof GraphicFrame) {
|
|
1890
|
+
const table = shape.table;
|
|
1891
|
+
if (!table) continue;
|
|
1892
|
+
for (const row of table.rows) {
|
|
1893
|
+
const cells = row.cells.map((cell) => cell.text);
|
|
1894
|
+
if (cells.some((cell) => cell.length > 0)) blocks.push(cells.join(" "));
|
|
1895
|
+
}
|
|
1896
|
+
} else {
|
|
1897
|
+
const text = shape.text;
|
|
1898
|
+
if (text.length > 0) blocks.push(text);
|
|
1899
|
+
}
|
|
1900
|
+
};
|
|
1901
|
+
walk(this.shapes);
|
|
1902
|
+
return blocks.join("\n");
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* The slide's speaker-note text — the companion to {@link text} for the notes
|
|
1906
|
+
* that {@link text} deliberately excludes. Delegates to {@link notesSlide}: finds
|
|
1907
|
+
* the notes body placeholder (`p:ph` `type="body"`) and flattens its text frame
|
|
1908
|
+
* the same way {@link TextFrame.text} does (paragraphs joined by `\n`).
|
|
1909
|
+
*
|
|
1910
|
+
* Returns `null` when the slide has **no notes slide part at all** — distinct
|
|
1911
|
+
* from `''`, which means a notes slide exists but its body is empty (PowerPoint
|
|
1912
|
+
* often attaches an empty notes slide to every slide). Only the body placeholder
|
|
1913
|
+
* is read; the slide-thumbnail (`sldImg`) and slide-number (`sldNum`)
|
|
1914
|
+
* placeholders a notes slide also carries are read via {@link notesSlide}.
|
|
1915
|
+
*/
|
|
1916
|
+
get notesText() {
|
|
1917
|
+
const notes = this.notesSlide;
|
|
1918
|
+
return notes ? notes.text : null;
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* The slide's speaker-note body as a navigable {@link TextFrame} — the rich
|
|
1922
|
+
* companion to {@link notesText}, which flattens the same body to a plain string.
|
|
1923
|
+
* Walk `paragraphs → runs` to recover per-run formatting (bold/italic/underline,
|
|
1924
|
+
* colour, size, face) and any notes hyperlink that {@link notesText} discards.
|
|
1925
|
+
*
|
|
1926
|
+
* `null` when the slide has **no notes slide part** (the same boundary
|
|
1927
|
+
* {@link notesText} returns `null` at), and also when a notes part exists but
|
|
1928
|
+
* carries no body-placeholder text frame — there is then no frame to hand back
|
|
1929
|
+
* (whereas {@link notesText} reports `''` for that empty-body case). Convenience
|
|
1930
|
+
* for `notesSlide.textFrame`; see {@link notesSlide} for the whole modeled notes
|
|
1931
|
+
* slide (its `sldImg`/`sldNum` placeholders and their geometry).
|
|
1932
|
+
*/
|
|
1933
|
+
get notesTextFrame() {
|
|
1934
|
+
return this.notesSlide?.textFrame ?? null;
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* The slide's speaker-notes slide (`notesSlideN.xml`) as a modeled
|
|
1938
|
+
* {@link NotesSlide}, or `null` when the slide has no notes slide part. Beyond the
|
|
1939
|
+
* body text {@link notesText}/{@link notesTextFrame} surface, this exposes the
|
|
1940
|
+
* notes slide's three placeholders — the slide thumbnail (`slideImage`), the notes
|
|
1941
|
+
* body, and the slide-number field (`slideNumber`) — each with its geometry and
|
|
1942
|
+
* (where present) a navigable text frame.
|
|
1943
|
+
*
|
|
1944
|
+
* The writer authors all three placeholders but leaves `sldImg`/`sldNum` with an
|
|
1945
|
+
* empty `p:spPr`, so on an authored deck their geometry reads `null` while an
|
|
1946
|
+
* imported deck carries the notesMaster-derived geometry. The body text and the
|
|
1947
|
+
* slide-number field round-trip either way.
|
|
1948
|
+
*/
|
|
1949
|
+
get notesSlide() {
|
|
1950
|
+
const notesRel = this.relationships.byType(NOTES_SLIDE_REL_TYPE)[0];
|
|
1951
|
+
if (!notesRel) return null;
|
|
1952
|
+
const notesPart = this.presentation.opc.part(this.relationships.resolveTarget(notesRel.id));
|
|
1953
|
+
if (!notesPart) return null;
|
|
1954
|
+
return new NotesSlide(this.presentation.opc, notesPart);
|
|
1955
|
+
}
|
|
1956
|
+
/**
|
|
1957
|
+
* The slide layout this slide is bound to (`slideLayout` relationship), as a
|
|
1958
|
+
* modeled {@link SlideLayout}, or `null` when the slide has no layout. The layout
|
|
1959
|
+
* carries the placeholder geometry and background a slide inherits; walk on to
|
|
1960
|
+
* {@link master} and {@link theme} through it (`slide.layout.master.theme`).
|
|
1961
|
+
*/
|
|
1962
|
+
get layout() {
|
|
1963
|
+
const rel = this.relationships.byType(SLIDE_LAYOUT_REL_TYPE)[0];
|
|
1964
|
+
if (!rel) return null;
|
|
1965
|
+
const part = this.presentation.opc.part(this.relationships.resolveTarget(rel.id));
|
|
1966
|
+
return part ? new SlideLayout(this.presentation.opc, part) : null;
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* The slide master this slide resolves against, via its {@link layout}, as a
|
|
1970
|
+
* modeled {@link SlideMaster}, or `null` when the layout/master chain is broken.
|
|
1971
|
+
* The master owns the colour map (`schemeClr` token → theme slot) and the default
|
|
1972
|
+
* text styles a placeholder inherits.
|
|
1973
|
+
*/
|
|
1974
|
+
get master() {
|
|
1975
|
+
return this.layout?.master ?? null;
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* The theme this slide resolves colour and font tokens against, via its
|
|
1979
|
+
* {@link layout} → {@link master} → theme, as a modeled {@link Theme}, or `null`.
|
|
1980
|
+
* Read its {@link Theme.colorScheme}/{@link Theme.fontScheme} to see the literal
|
|
1981
|
+
* palette and faces a `schemeClr`/`+mj-*` token resolves to.
|
|
1982
|
+
*/
|
|
1983
|
+
get theme() {
|
|
1984
|
+
return this.layout?.master?.theme ?? null;
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* The slide's **legacy** review comments (`p:cm` in its `comments/commentN.xml`
|
|
1988
|
+
* part), `[]` when it has none. Each {@link Comment} carries its body text, marker
|
|
1989
|
+
* position (EMU), timestamp, and its author resolved through the deck-wide
|
|
1990
|
+
* {@link Presentation.commentAuthors} registry (`@authorId` → name/initials). These
|
|
1991
|
+
* are the comments the writer authors via `slide.addComment(...)`; the 2018 modern
|
|
1992
|
+
* comment parts (`p188:cm`) are a separate schema, preserved but not decoded.
|
|
1993
|
+
*/
|
|
1994
|
+
get comments() {
|
|
1995
|
+
return readSlideComments(this.presentation.opc, this.part, this.presentation.commentAuthors);
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* The slide's **modern** (2018) review comments (`p188:cm` in its
|
|
1999
|
+
* `modernComment_*.xml` part), `[]` when it has none. Each {@link ModernComment}
|
|
2000
|
+
* carries its body text, `created` timestamp, marker position (EMU), its author
|
|
2001
|
+
* resolved through the deck-wide {@link Presentation.modernCommentAuthors}
|
|
2002
|
+
* registry (`@authorId` GUID → name/initials), and its reply thread nested under
|
|
2003
|
+
* `replies`. Read-only — the writer authors the legacy schema; see
|
|
2004
|
+
* {@link Presentation.commentSchema} to tell which schema a deck uses.
|
|
2005
|
+
*/
|
|
2006
|
+
get modernComments() {
|
|
2007
|
+
return readModernSlideComments(this.presentation.opc, this.part, this.presentation.modernCommentAuthors);
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* The slide's programmatic tags (`p:custDataLst/p:tags` on the slide part,
|
|
2011
|
+
* resolved to `ppt/tags/tagN.xml`) as `{ name, val }` string pairs, `[]` when it
|
|
2012
|
+
* has none. Host/add-in metadata with no visible rendering and no writer —
|
|
2013
|
+
* read-only, preserved byte-for-byte on round-trip. Deck-level tags are
|
|
2014
|
+
* {@link Presentation.tags}.
|
|
2015
|
+
*/
|
|
2016
|
+
get tags() {
|
|
2017
|
+
return readTagsForPart(this.presentation.opc, this.partName);
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* The slide's effective background (`p:cSld/p:bg`), decoded into a typed
|
|
2021
|
+
* {@link SlideBackground}. Resolved through the inheritance chain: the slide's
|
|
2022
|
+
* own `p:bg` wins; failing that the slideLayout's, then the slideMaster's — the
|
|
2023
|
+
* result's `source` records which supplied it. `null` when nothing in the chain
|
|
2024
|
+
* defines a background.
|
|
2025
|
+
*
|
|
2026
|
+
* Colour tokens resolve against this slide's theme; an image background's
|
|
2027
|
+
* `r:embed` resolves to an absolute part name through the *owning* part's
|
|
2028
|
+
* relationships (the layout's rels for a layout-inherited image, etc.). Solid,
|
|
2029
|
+
* gradient, and image backgrounds are the three the writer authors and so
|
|
2030
|
+
* round-trip faithfully; `pattern`/`themeRef` are read-only for imported decks.
|
|
2031
|
+
*/
|
|
2032
|
+
get background() {
|
|
2033
|
+
const opc = this.presentation.opc;
|
|
2034
|
+
const parts = resolveSlideThemeParts(opc, this.partName);
|
|
2035
|
+
const ctx = this.themeContext();
|
|
2036
|
+
const candidates = [
|
|
2037
|
+
{
|
|
2038
|
+
root: parts.slideRoot,
|
|
2039
|
+
source: "slide",
|
|
2040
|
+
partName: this.partName
|
|
2041
|
+
},
|
|
2042
|
+
{
|
|
2043
|
+
root: parts.layoutRoot,
|
|
2044
|
+
source: "layout",
|
|
2045
|
+
partName: parts.layoutPartName
|
|
2046
|
+
},
|
|
2047
|
+
{
|
|
2048
|
+
root: parts.masterRoot,
|
|
2049
|
+
source: "master",
|
|
2050
|
+
partName: parts.masterPartName
|
|
2051
|
+
}
|
|
2052
|
+
];
|
|
2053
|
+
const themeRels = parts.themePartName ? opc.relationshipsFor(parts.themePartName) : null;
|
|
2054
|
+
for (const { root, source, partName } of candidates) {
|
|
2055
|
+
const bg = backgroundElementOf(root);
|
|
2056
|
+
if (!bg) continue;
|
|
2057
|
+
return readSlideBackground(bg, source, ctx, partName ? opc.relationshipsFor(partName) : null, themeRels);
|
|
2058
|
+
}
|
|
2059
|
+
return null;
|
|
2060
|
+
}
|
|
2061
|
+
/**
|
|
2062
|
+
* The slide's own slide-number placeholder (`p:sp` with `p:ph type="sldNum"`,
|
|
2063
|
+
* carrying an `<a:fld type="slidenum">`), or `null` when the slide shows no slide
|
|
2064
|
+
* number of its own. This is the concrete shape the writer emits for
|
|
2065
|
+
* `pptx.setSlideNumber(...)` on a slide's layout; its geometry and run formatting
|
|
2066
|
+
* read off the returned {@link AutoShape} the same as any placeholder.
|
|
2067
|
+
*
|
|
2068
|
+
* Scoped to the slide's *own* shape tree — a slide number inherited purely from
|
|
2069
|
+
* the master's `p:hf`/placeholder (with no shape on the slide) is a master-level
|
|
2070
|
+
* concern this getter does not resolve.
|
|
2071
|
+
*/
|
|
2072
|
+
get slideNumberPlaceholder() {
|
|
2073
|
+
return this.placeholder("sldNum") ?? null;
|
|
2074
|
+
}
|
|
2075
|
+
/** The first top-level shape with the given drawing id (`p:cNvPr/@id`), or `undefined`. */
|
|
2076
|
+
shapeById(id) {
|
|
2077
|
+
return this.shapes.find((shape) => shape.id === id);
|
|
2078
|
+
}
|
|
2079
|
+
/**
|
|
2080
|
+
* The first shape *anywhere* in the slide's shape tree with the given drawing id
|
|
2081
|
+
* (`p:cNvPr/@id`), descending into groups — unlike {@link shapeById}, which scans
|
|
2082
|
+
* only top-level shapes. Walked pre-order (a group is visited before its children),
|
|
2083
|
+
* matching how the writer allocates ids. `undefined` when no shape carries that id.
|
|
2084
|
+
*
|
|
2085
|
+
* Drawing ids are unique within a slide, so the first match is the only match; the
|
|
2086
|
+
* pre-order walk just fixes a deterministic order. This backs the connector-binding
|
|
2087
|
+
* resolution ({@link import('./shapes.js').Connector.startConnection}), which must
|
|
2088
|
+
* resolve a binding into a group that top-level {@link shapeById} cannot see.
|
|
2089
|
+
*/
|
|
2090
|
+
shapeByIdDeep(id) {
|
|
2091
|
+
const walk = (shapes) => {
|
|
2092
|
+
for (const shape of shapes) {
|
|
2093
|
+
if (shape.id === id) return shape;
|
|
2094
|
+
if (shape instanceof GroupShape) {
|
|
2095
|
+
const found = walk(shape.shapes);
|
|
2096
|
+
if (found) return found;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
return walk(this.shapes);
|
|
2101
|
+
}
|
|
2102
|
+
/** The first top-level shape with the given name (`p:cNvPr/@name`), or `undefined`. */
|
|
2103
|
+
shapeByName(name) {
|
|
2104
|
+
return this.shapes.find((shape) => shape.name === name);
|
|
2105
|
+
}
|
|
2106
|
+
/**
|
|
2107
|
+
* The first placeholder of the given type (`p:ph/@type`, e.g. `title`,
|
|
2108
|
+
* `ctrTitle`, `subTitle`, `body`), optionally narrowed by `idx`. Returns
|
|
2109
|
+
* `undefined` when none match. Only `p:sp` shapes can be placeholders, so the
|
|
2110
|
+
* result is an {@link AutoShape}.
|
|
2111
|
+
*/
|
|
2112
|
+
placeholder(type, idx) {
|
|
2113
|
+
return this.shapes.find((shape) => {
|
|
2114
|
+
const ph = shape instanceof AutoShape ? shape.placeholder : null;
|
|
2115
|
+
return ph !== null && ph.type === type && (idx === void 0 || ph.idx === idx);
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Append a text box (`p:sp` with `txBox="1"`) to the slide's shape tree and
|
|
2120
|
+
* return it. Geometry is required (EMU); width and height must be positive.
|
|
2121
|
+
* Allocates a drawing id unique within the slide. Marks the slide part dirty.
|
|
2122
|
+
*/
|
|
2123
|
+
addTextBox(options) {
|
|
2124
|
+
const { left, top, width, height } = options;
|
|
2125
|
+
requireFinite(left, "left");
|
|
2126
|
+
requireFinite(top, "top");
|
|
2127
|
+
requirePositive(width, "width");
|
|
2128
|
+
requirePositive(height, "height");
|
|
2129
|
+
const spTree = this.#spTree();
|
|
2130
|
+
if (!spTree) throw new PackageReadError("slide/no-shape-tree", `Slide ${this.partName} has no spTree to add a shape to`);
|
|
2131
|
+
const doc = spTree.ownerDocument;
|
|
2132
|
+
if (!doc) throw new InternalError("oxml/node-has-no-document", "Slide DOM has no owner document");
|
|
2133
|
+
const id = this.#nextShapeId();
|
|
2134
|
+
const sp = buildTextBox(doc, {
|
|
2135
|
+
id,
|
|
2136
|
+
name: options.name ?? `TextBox ${id}`,
|
|
2137
|
+
text: options.text ?? "",
|
|
2138
|
+
left: Math.round(left),
|
|
2139
|
+
top: Math.round(top),
|
|
2140
|
+
width: Math.round(width),
|
|
2141
|
+
height: Math.round(height)
|
|
2142
|
+
});
|
|
2143
|
+
this.#appendShape(spTree, sp);
|
|
2144
|
+
return new AutoShape(sp, this);
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* Add a picture (`p:pic`) from raw image bytes and return it. Creates a media
|
|
2148
|
+
* part under `/ppt/media/`, registers its content type, wires an `image`
|
|
2149
|
+
* relationship from this slide, and appends the picture to the shape tree.
|
|
2150
|
+
* Geometry is required (EMU); width and height must be positive. The image
|
|
2151
|
+
* format is sniffed from the bytes unless `extension`/`contentType` are given.
|
|
2152
|
+
*/
|
|
2153
|
+
addPicture(image, options) {
|
|
2154
|
+
const { left, top, width, height } = options;
|
|
2155
|
+
requireFinite(left, "left");
|
|
2156
|
+
requireFinite(top, "top");
|
|
2157
|
+
requirePositive(width, "width");
|
|
2158
|
+
requirePositive(height, "height");
|
|
2159
|
+
const sniffed = sniffImageType(image);
|
|
2160
|
+
const extension = (options.extension ?? sniffed?.extension)?.toLowerCase().replace(/^\./, "");
|
|
2161
|
+
const contentType = options.contentType ?? sniffed?.contentType;
|
|
2162
|
+
if (!extension || !contentType) throw new InvalidOptionError("image/undeterminable-type", "Could not determine image type; pass { extension, contentType } to addPicture");
|
|
2163
|
+
const spTree = this.#spTree();
|
|
2164
|
+
if (!spTree) throw new PackageReadError("slide/no-shape-tree", `Slide ${this.partName} has no spTree to add a picture to`);
|
|
2165
|
+
const doc = spTree.ownerDocument;
|
|
2166
|
+
if (!doc) throw new InternalError("oxml/node-has-no-document", "Slide DOM has no owner document");
|
|
2167
|
+
const opc = this.presentation.opc;
|
|
2168
|
+
const mediaPartName = opc.reserveMediaPartName(extension);
|
|
2169
|
+
opc.addPart(mediaPartName, contentType, image);
|
|
2170
|
+
const relId = this.relationships.add(IMAGE_REL_TYPE, relativePartName(this.partName, mediaPartName)).id;
|
|
2171
|
+
const id = this.#nextShapeId();
|
|
2172
|
+
const pic = buildPicture(doc, {
|
|
2173
|
+
id,
|
|
2174
|
+
name: options.name ?? `Picture ${id}`,
|
|
2175
|
+
relId,
|
|
2176
|
+
left: Math.round(left),
|
|
2177
|
+
top: Math.round(top),
|
|
2178
|
+
width: Math.round(width),
|
|
2179
|
+
height: Math.round(height)
|
|
2180
|
+
});
|
|
2181
|
+
this.#appendShape(spTree, pic);
|
|
2182
|
+
return new Picture(pic, this);
|
|
2183
|
+
}
|
|
2184
|
+
/**
|
|
2185
|
+
* @internal The slide's shape tree (`p:cSld/p:spTree`), or `null` if absent.
|
|
2186
|
+
* Exposed for cross-slide composition (`Presentation.importShape`).
|
|
2187
|
+
*/
|
|
2188
|
+
shapeTree() {
|
|
2189
|
+
return this.#spTree();
|
|
2190
|
+
}
|
|
2191
|
+
/**
|
|
2192
|
+
* @internal The smallest drawing id (`p:cNvPr/@id`) not already used on this
|
|
2193
|
+
* slide. Exposed so `Presentation.importShape` can give a lifted shape (and its
|
|
2194
|
+
* group children) collision-free ids on the host.
|
|
2195
|
+
*/
|
|
2196
|
+
nextShapeId() {
|
|
2197
|
+
return this.#nextShapeId();
|
|
2198
|
+
}
|
|
2199
|
+
/** Insert a shape after grpSpPr and before any trailing p:extLst on the tree; mark dirty. */
|
|
2200
|
+
#appendShape(spTree, shape) {
|
|
2201
|
+
spTree.insertBefore(shape, firstChild(spTree, "p:extLst"));
|
|
2202
|
+
this.part.markDirty();
|
|
2203
|
+
}
|
|
2204
|
+
/** The smallest drawing id (`p:cNvPr/@id`) not already used on the slide. */
|
|
2205
|
+
#nextShapeId() {
|
|
2206
|
+
const root = this.part.dom.documentElement;
|
|
2207
|
+
let max = 1;
|
|
2208
|
+
if (root) for (const cNvPr of root.getElementsByTagNameNS(OOXML_NS.p, "cNvPr")) {
|
|
2209
|
+
const id = intValue(attr(cNvPr, "id"));
|
|
2210
|
+
if (id !== null && id > max) max = id;
|
|
2211
|
+
}
|
|
2212
|
+
return max + 1;
|
|
2213
|
+
}
|
|
2214
|
+
#cSld() {
|
|
2215
|
+
const root = this.part.dom.documentElement;
|
|
2216
|
+
return root ? firstChild(root, "p:cSld") : null;
|
|
2217
|
+
}
|
|
2218
|
+
#spTree() {
|
|
2219
|
+
const cSld = this.#cSld();
|
|
2220
|
+
return cSld ? firstChild(cSld, "p:spTree") : null;
|
|
2221
|
+
}
|
|
2222
|
+
};
|
|
2223
|
+
function requireFinite(value, name) {
|
|
2224
|
+
if (!Number.isFinite(value)) throw new InvalidOptionError("coord/non-finite", `${name} must be a finite number of EMU, got ${value}`);
|
|
2225
|
+
}
|
|
2226
|
+
function requirePositive(value, name) {
|
|
2227
|
+
requireFinite(value, name);
|
|
2228
|
+
if (value <= 0) throw new InvalidOptionError("coord/not-positive", `${name} must be positive, got ${value}`);
|
|
2229
|
+
}
|
|
2230
|
+
/** Build a minimal, schema-valid text-box `p:sp` element (not yet attached). */
|
|
2231
|
+
function buildTextBox(doc, spec) {
|
|
2232
|
+
const make = (qname) => createElement(doc, qname);
|
|
2233
|
+
const append = (parent, qname) => {
|
|
2234
|
+
const child = make(qname);
|
|
2235
|
+
parent.appendChild(child);
|
|
2236
|
+
return child;
|
|
2237
|
+
};
|
|
2238
|
+
const sp = make("p:sp");
|
|
2239
|
+
const nvSpPr = append(sp, "p:nvSpPr");
|
|
2240
|
+
const cNvPr = append(nvSpPr, "p:cNvPr");
|
|
2241
|
+
setAttr(cNvPr, "id", String(spec.id));
|
|
2242
|
+
setAttr(cNvPr, "name", spec.name);
|
|
2243
|
+
setAttr(append(nvSpPr, "p:cNvSpPr"), "txBox", "1");
|
|
2244
|
+
append(nvSpPr, "p:nvPr");
|
|
2245
|
+
const spPr = append(sp, "p:spPr");
|
|
2246
|
+
const xfrm = append(spPr, "a:xfrm");
|
|
2247
|
+
const off = append(xfrm, "a:off");
|
|
2248
|
+
setAttr(off, "x", String(spec.left));
|
|
2249
|
+
setAttr(off, "y", String(spec.top));
|
|
2250
|
+
const ext = append(xfrm, "a:ext");
|
|
2251
|
+
setAttr(ext, "cx", String(spec.width));
|
|
2252
|
+
setAttr(ext, "cy", String(spec.height));
|
|
2253
|
+
const prstGeom = append(spPr, "a:prstGeom");
|
|
2254
|
+
setAttr(prstGeom, "prst", "rect");
|
|
2255
|
+
append(prstGeom, "a:avLst");
|
|
2256
|
+
const txBody = append(sp, "p:txBody");
|
|
2257
|
+
append(txBody, "a:bodyPr");
|
|
2258
|
+
append(txBody, "a:lstStyle");
|
|
2259
|
+
const p = append(txBody, "a:p");
|
|
2260
|
+
if (spec.text !== "") {
|
|
2261
|
+
const t = append(append(p, "a:r"), "a:t");
|
|
2262
|
+
t.textContent = spec.text;
|
|
2263
|
+
if (spec.text !== spec.text.trim()) setAttr(t, "xml:space", "preserve");
|
|
2264
|
+
}
|
|
2265
|
+
return sp;
|
|
2266
|
+
}
|
|
2267
|
+
/** Build a minimal, schema-valid `p:pic` element (not yet attached). */
|
|
2268
|
+
function buildPicture(doc, spec) {
|
|
2269
|
+
const make = (qname) => createElement(doc, qname);
|
|
2270
|
+
const append = (parent, qname) => {
|
|
2271
|
+
const child = make(qname);
|
|
2272
|
+
parent.appendChild(child);
|
|
2273
|
+
return child;
|
|
2274
|
+
};
|
|
2275
|
+
const pic = make("p:pic");
|
|
2276
|
+
const nvPicPr = append(pic, "p:nvPicPr");
|
|
2277
|
+
const cNvPr = append(nvPicPr, "p:cNvPr");
|
|
2278
|
+
setAttr(cNvPr, "id", String(spec.id));
|
|
2279
|
+
setAttr(cNvPr, "name", spec.name);
|
|
2280
|
+
setAttr(append(append(nvPicPr, "p:cNvPicPr"), "a:picLocks"), "noChangeAspect", "1");
|
|
2281
|
+
append(nvPicPr, "p:nvPr");
|
|
2282
|
+
const blipFill = append(pic, "p:blipFill");
|
|
2283
|
+
setAttr(append(blipFill, "a:blip"), "r:embed", spec.relId);
|
|
2284
|
+
append(append(blipFill, "a:stretch"), "a:fillRect");
|
|
2285
|
+
const spPr = append(pic, "p:spPr");
|
|
2286
|
+
const xfrm = append(spPr, "a:xfrm");
|
|
2287
|
+
const off = append(xfrm, "a:off");
|
|
2288
|
+
setAttr(off, "x", String(spec.left));
|
|
2289
|
+
setAttr(off, "y", String(spec.top));
|
|
2290
|
+
const ext = append(xfrm, "a:ext");
|
|
2291
|
+
setAttr(ext, "cx", String(spec.width));
|
|
2292
|
+
setAttr(ext, "cy", String(spec.height));
|
|
2293
|
+
const prstGeom = append(spPr, "a:prstGeom");
|
|
2294
|
+
setAttr(prstGeom, "prst", "rect");
|
|
2295
|
+
append(prstGeom, "a:avLst");
|
|
2296
|
+
return pic;
|
|
2297
|
+
}
|
|
2298
|
+
//#endregion
|
|
2299
|
+
//#region src/read/api/document-properties.ts
|
|
2300
|
+
/** Package-root rel to `/docProps/core.xml`. */
|
|
2301
|
+
const CORE_PROPS_REL_TYPE = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties";
|
|
2302
|
+
/** Content type of the core-properties part (fallback lookup when the rel is absent). */
|
|
2303
|
+
const CORE_PROPS_CONTENT_TYPE = "application/vnd.openxmlformats-package.core-properties+xml";
|
|
2304
|
+
/** Package-root rel to `/docProps/custom.xml`. */
|
|
2305
|
+
const CUSTOM_PROPS_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties";
|
|
2306
|
+
/** Content type of the custom-properties part (fallback lookup when the rel is absent). */
|
|
2307
|
+
const CUSTOM_PROPS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.custom-properties+xml";
|
|
2308
|
+
/** `dc:title`-style core-property fields, keyed by output field → element qname. */
|
|
2309
|
+
const CORE_FIELDS = [
|
|
2310
|
+
["title", "dc:title"],
|
|
2311
|
+
["subject", "dc:subject"],
|
|
2312
|
+
["creator", "dc:creator"],
|
|
2313
|
+
["keywords", "cp:keywords"],
|
|
2314
|
+
["description", "dc:description"],
|
|
2315
|
+
["lastModifiedBy", "cp:lastModifiedBy"],
|
|
2316
|
+
["revision", "cp:revision"],
|
|
2317
|
+
["category", "cp:category"],
|
|
2318
|
+
["contentStatus", "cp:contentStatus"],
|
|
2319
|
+
["created", "dcterms:created"],
|
|
2320
|
+
["modified", "dcterms:modified"],
|
|
2321
|
+
["lastPrinted", "cp:lastPrinted"]
|
|
2322
|
+
];
|
|
2323
|
+
/**
|
|
2324
|
+
* Read the deck's core document properties from `/docProps/core.xml`, resolved
|
|
2325
|
+
* via the package-root `core-properties` relationship (fallback: the part's
|
|
2326
|
+
* content type). Missing part → `{}` (all fields undefined). Present-but-empty
|
|
2327
|
+
* elements decode to the empty string.
|
|
2328
|
+
*/
|
|
2329
|
+
function readCoreProperties(opc) {
|
|
2330
|
+
const rels = opc.relationshipsFor("/");
|
|
2331
|
+
const rel = rels.byType(CORE_PROPS_REL_TYPE)[0];
|
|
2332
|
+
const root = (rel ? opc.part(rels.resolveTarget(rel.id)) : opc.partsByContentType(CORE_PROPS_CONTENT_TYPE)[0])?.dom.documentElement;
|
|
2333
|
+
if (!root) return {};
|
|
2334
|
+
const out = {};
|
|
2335
|
+
for (const [field, qname] of CORE_FIELDS) {
|
|
2336
|
+
const el = firstChild(root, qname);
|
|
2337
|
+
if (el) out[field] = el.textContent ?? "";
|
|
2338
|
+
}
|
|
2339
|
+
return out;
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Decode one `<property>`'s single `vt:` child into a typed value. String types
|
|
2343
|
+
* (`lpwstr`/`lpstr`/`bstr`) → string; integer types (`i1`..`i8`/`int`/`ui*`) →
|
|
2344
|
+
* number; real types (`r4`/`r8`/`decimal`) → number; `bool` → boolean;
|
|
2345
|
+
* `filetime`/`date` → the **raw string** (see {@link CoreProperties} on
|
|
2346
|
+
* timestamps). Any unrecognized `vt:` element falls back to its text content as a
|
|
2347
|
+
* string — lossy but never throws.
|
|
2348
|
+
*/
|
|
2349
|
+
function decodeValue(vt) {
|
|
2350
|
+
const text = vt.textContent ?? "";
|
|
2351
|
+
switch (vt.localName) {
|
|
2352
|
+
case "lpwstr":
|
|
2353
|
+
case "lpstr":
|
|
2354
|
+
case "bstr": return text;
|
|
2355
|
+
case "i1":
|
|
2356
|
+
case "i2":
|
|
2357
|
+
case "i4":
|
|
2358
|
+
case "i8":
|
|
2359
|
+
case "int":
|
|
2360
|
+
case "ui1":
|
|
2361
|
+
case "ui2":
|
|
2362
|
+
case "ui4":
|
|
2363
|
+
case "ui8":
|
|
2364
|
+
case "uint": return intValue(text) ?? text;
|
|
2365
|
+
case "r4":
|
|
2366
|
+
case "r8":
|
|
2367
|
+
case "decimal": {
|
|
2368
|
+
const n = Number.parseFloat(text);
|
|
2369
|
+
return Number.isFinite(n) ? n : text;
|
|
2370
|
+
}
|
|
2371
|
+
case "bool": return text.trim().toLowerCase() === "true" || text.trim() === "1";
|
|
2372
|
+
case "filetime":
|
|
2373
|
+
case "date": return text;
|
|
2374
|
+
default: return text;
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
/**
|
|
2378
|
+
* Read the deck's user-defined custom properties from `/docProps/custom.xml`,
|
|
2379
|
+
* resolved via the package-root `custom-properties` relationship (fallback: the
|
|
2380
|
+
* part's content type). Each `<property name=...>` maps to `{ name, value }`,
|
|
2381
|
+
* with `value` typed from its single `vt:` child. Missing part → `[]`. A
|
|
2382
|
+
* `<property>` without a `@name` or without a `vt:` child is skipped.
|
|
2383
|
+
*
|
|
2384
|
+
* The `vt:` child and `<property>` element sit in namespaces outside the read
|
|
2385
|
+
* model's qname registry, so children are matched by local name rather than the
|
|
2386
|
+
* `firstChild`/`getElements` helpers.
|
|
2387
|
+
*/
|
|
2388
|
+
function readCustomProperties(opc) {
|
|
2389
|
+
const rels = opc.relationshipsFor("/");
|
|
2390
|
+
const rel = rels.byType(CUSTOM_PROPS_REL_TYPE)[0];
|
|
2391
|
+
const root = (rel ? opc.part(rels.resolveTarget(rel.id)) : opc.partsByContentType(CUSTOM_PROPS_CONTENT_TYPE)[0])?.dom.documentElement;
|
|
2392
|
+
if (!root) return [];
|
|
2393
|
+
const out = [];
|
|
2394
|
+
for (const property of childElements(root)) {
|
|
2395
|
+
if (property.localName !== "property") continue;
|
|
2396
|
+
const name = attr(property, "name");
|
|
2397
|
+
const vt = firstChildElement(property);
|
|
2398
|
+
if (name === null || !vt) continue;
|
|
2399
|
+
out.push({
|
|
2400
|
+
name,
|
|
2401
|
+
value: decodeValue(vt)
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
return out;
|
|
2405
|
+
}
|
|
2406
|
+
//#endregion
|
|
2407
|
+
//#region src/read/api/slide-dom.ts
|
|
2408
|
+
/** The `p:cSld@name` of a slide/layout/master part (`''` when absent). */
|
|
2409
|
+
function cSldName(part) {
|
|
2410
|
+
const root = part?.dom.documentElement;
|
|
2411
|
+
const cSld = root && firstChild(root, "p:cSld");
|
|
2412
|
+
return (cSld && attr(cSld, "name")) ?? "";
|
|
2413
|
+
}
|
|
2414
|
+
/** Whether `el` is a `p:spTree`'s own group properties (`p:nvGrpSpPr`/`p:grpSpPr`), not a shape. */
|
|
2415
|
+
function isSpTreeProperty(el) {
|
|
2416
|
+
return el.namespaceURI === OOXML_NS.p && (el.localName === "nvGrpSpPr" || el.localName === "grpSpPr");
|
|
2417
|
+
}
|
|
2418
|
+
/** Whether a `p:spTree` child is a placeholder shape (its `*nvPr` carries a `p:ph`). */
|
|
2419
|
+
function isPlaceholderShape(shape) {
|
|
2420
|
+
const nv = firstChildElement(shape);
|
|
2421
|
+
const nvPr = nv && firstChild(nv, "p:nvPr");
|
|
2422
|
+
return !!(nvPr && firstChild(nvPr, "p:ph"));
|
|
2423
|
+
}
|
|
2424
|
+
/** The decorative shapes on a layout/master `p:spTree`: every shape child except placeholders. */
|
|
2425
|
+
function carriedDecorations(root) {
|
|
2426
|
+
if (!root) return [];
|
|
2427
|
+
const cSld = firstChild(root, "p:cSld");
|
|
2428
|
+
const spTree = cSld && firstChild(cSld, "p:spTree");
|
|
2429
|
+
if (!spTree) return [];
|
|
2430
|
+
const out = [];
|
|
2431
|
+
for (let node = spTree.firstChild; node; node = node.nextSibling) {
|
|
2432
|
+
if (node.nodeType !== 1) continue;
|
|
2433
|
+
const el = node;
|
|
2434
|
+
if (isSpTreeProperty(el) || isPlaceholderShape(el)) continue;
|
|
2435
|
+
out.push(el);
|
|
2436
|
+
}
|
|
2437
|
+
return out;
|
|
2438
|
+
}
|
|
2439
|
+
/**
|
|
2440
|
+
* The `n`-th shape child of a `p:spTree` in document (z-)order, skipping the
|
|
2441
|
+
* tree's own `nvGrpSpPr`/`grpSpPr` and any trailing `p:extLst`. Returns `null`
|
|
2442
|
+
* when `n` is past the last shape (the caller then appends).
|
|
2443
|
+
*/
|
|
2444
|
+
function nthShapeChild(spTree, n) {
|
|
2445
|
+
let i = 0;
|
|
2446
|
+
for (let node = spTree.firstChild; node; node = node.nextSibling) {
|
|
2447
|
+
if (node.nodeType !== 1) continue;
|
|
2448
|
+
const el = node;
|
|
2449
|
+
if (isSpTreeProperty(el)) continue;
|
|
2450
|
+
if (el.namespaceURI === OOXML_NS.p && el.localName === "extLst") continue;
|
|
2451
|
+
if (i === n) return el;
|
|
2452
|
+
i++;
|
|
2453
|
+
}
|
|
2454
|
+
return null;
|
|
2455
|
+
}
|
|
2456
|
+
/** The first shape child of a `p:spTree` (skipping `nvGrpSpPr`/`grpSpPr`), or `null`. */
|
|
2457
|
+
function firstShapeChild(spTree) {
|
|
2458
|
+
for (let node = spTree.firstChild; node; node = node.nextSibling) {
|
|
2459
|
+
if (node.nodeType !== 1) continue;
|
|
2460
|
+
const el = node;
|
|
2461
|
+
if (!isSpTreeProperty(el)) return el;
|
|
2462
|
+
}
|
|
2463
|
+
return null;
|
|
2464
|
+
}
|
|
2465
|
+
/** Collect `node` and all its descendant elements (document order) into `out`. */
|
|
2466
|
+
function collectElements(node, out) {
|
|
2467
|
+
out.push(node);
|
|
2468
|
+
for (let child = node.firstChild; child; child = child.nextSibling) if (child.nodeType === 1) collectElements(child, out);
|
|
2469
|
+
}
|
|
2470
|
+
//#endregion
|
|
2471
|
+
//#region src/read/api/rescale.ts
|
|
2472
|
+
/** Build the EMU transform mapping source-canvas coordinates onto the target canvas (see {@link ImportSlideOptions.rescale}). */
|
|
2473
|
+
function computeRescale(source, target, mode) {
|
|
2474
|
+
if (mode === "stretch") return {
|
|
2475
|
+
sx: target.widthEmu / source.widthEmu,
|
|
2476
|
+
sy: target.heightEmu / source.heightEmu,
|
|
2477
|
+
dx: 0,
|
|
2478
|
+
dy: 0
|
|
2479
|
+
};
|
|
2480
|
+
const scale = Math.min(target.widthEmu / source.widthEmu, target.heightEmu / source.heightEmu);
|
|
2481
|
+
return {
|
|
2482
|
+
sx: scale,
|
|
2483
|
+
sy: scale,
|
|
2484
|
+
dx: (target.widthEmu - source.widthEmu * scale) / 2,
|
|
2485
|
+
dy: (target.heightEmu - source.heightEmu * scale) / 2
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* Rescale every top-level transform on a `p:spTree`: shapes/pictures/connectors
|
|
2490
|
+
* (`p:spPr/a:xfrm`), groups (`p:grpSpPr/a:xfrm` — only the group's own off/ext, so
|
|
2491
|
+
* its children remap via the unchanged `chOff`/`chExt` rather than being recursed
|
|
2492
|
+
* into), and graphic frames (`p:graphicFrame/p:xfrm`, plus any inner table grid).
|
|
2493
|
+
* Placeholders that inherit geometry carry no `a:xfrm` and are left to inherit from
|
|
2494
|
+
* the (separately rescaled) layout/master.
|
|
2495
|
+
*/
|
|
2496
|
+
function rescaleSpTree(spTree, t) {
|
|
2497
|
+
for (let node = spTree.firstChild; node; node = node.nextSibling) {
|
|
2498
|
+
if (node.nodeType !== 1) continue;
|
|
2499
|
+
const el = node;
|
|
2500
|
+
if (el.namespaceURI !== OOXML_NS.p) continue;
|
|
2501
|
+
switch (el.localName) {
|
|
2502
|
+
case "sp":
|
|
2503
|
+
case "pic":
|
|
2504
|
+
case "cxnSp": {
|
|
2505
|
+
const spPr = firstChild(el, "p:spPr");
|
|
2506
|
+
rescaleXfrm(spPr && firstChild(spPr, "a:xfrm"), t);
|
|
2507
|
+
break;
|
|
2508
|
+
}
|
|
2509
|
+
case "grpSp": {
|
|
2510
|
+
const grpSpPr = firstChild(el, "p:grpSpPr");
|
|
2511
|
+
rescaleXfrm(grpSpPr && firstChild(grpSpPr, "a:xfrm"), t);
|
|
2512
|
+
break;
|
|
2513
|
+
}
|
|
2514
|
+
case "graphicFrame":
|
|
2515
|
+
rescaleXfrm(firstChild(el, "p:xfrm"), t);
|
|
2516
|
+
rescaleTableGrid(el, t);
|
|
2517
|
+
break;
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
/** Rewrite an `a:xfrm`/`p:xfrm`: `a:off` is repositioned (scale + translate); `a:ext` is resized (scale only). */
|
|
2522
|
+
function rescaleXfrm(xfrm, t) {
|
|
2523
|
+
if (!xfrm) return;
|
|
2524
|
+
const off = firstChild(xfrm, "a:off");
|
|
2525
|
+
if (off) {
|
|
2526
|
+
const x = intValue(attr(off, "x"));
|
|
2527
|
+
const y = intValue(attr(off, "y"));
|
|
2528
|
+
if (x !== null) setAttr(off, "x", String(Math.round(x * t.sx + t.dx)));
|
|
2529
|
+
if (y !== null) setAttr(off, "y", String(Math.round(y * t.sy + t.dy)));
|
|
2530
|
+
}
|
|
2531
|
+
const ext = firstChild(xfrm, "a:ext");
|
|
2532
|
+
if (ext) {
|
|
2533
|
+
const cx = intValue(attr(ext, "cx"));
|
|
2534
|
+
const cy = intValue(attr(ext, "cy"));
|
|
2535
|
+
if (cx !== null) setAttr(ext, "cx", String(Math.max(0, Math.round(cx * t.sx))));
|
|
2536
|
+
if (cy !== null) setAttr(ext, "cy", String(Math.max(0, Math.round(cy * t.sy))));
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
/**
|
|
2540
|
+
* Scale a graphic-frame table's intrinsic dimensions — column widths by `sx`, row
|
|
2541
|
+
* heights by `sy` — so the table resizes with the slide (PowerPoint derives table
|
|
2542
|
+
* geometry from the grid, not the frame `a:ext`). No-op for non-table frames.
|
|
2543
|
+
*/
|
|
2544
|
+
function rescaleTableGrid(graphicFrame, t) {
|
|
2545
|
+
const graphic = firstChild(graphicFrame, "a:graphic");
|
|
2546
|
+
const graphicData = graphic && firstChild(graphic, "a:graphicData");
|
|
2547
|
+
const tbl = graphicData && firstChild(graphicData, "a:tbl");
|
|
2548
|
+
if (!tbl) return;
|
|
2549
|
+
const grid = firstChild(tbl, "a:tblGrid");
|
|
2550
|
+
if (grid) for (const col of getElements(grid, "a:gridCol")) {
|
|
2551
|
+
const w = intValue(attr(col, "w"));
|
|
2552
|
+
if (w !== null) setAttr(col, "w", String(Math.max(0, Math.round(w * t.sx))));
|
|
2553
|
+
}
|
|
2554
|
+
for (const tr of getElements(tbl, "a:tr")) {
|
|
2555
|
+
const h = intValue(attr(tr, "h"));
|
|
2556
|
+
if (h !== null) setAttr(tr, "h", String(Math.max(0, Math.round(h * t.sy))));
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
//#endregion
|
|
2560
|
+
//#region src/read/api/table-styles.ts
|
|
2561
|
+
const TABLE_STYLES_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles";
|
|
2562
|
+
const TABLE_STYLES_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml";
|
|
2563
|
+
const TABLE_STYLES_DEFAULT_GUID = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}";
|
|
2564
|
+
const textEncoder$2 = new TextEncoder();
|
|
2565
|
+
/**
|
|
2566
|
+
* Copy any table style the restyled slide references from the source
|
|
2567
|
+
* `tableStyles.xml` into this deck's (the `remapLiterals` table leg). A restyled
|
|
2568
|
+
* table resolves its `@tableStyleId` against the *destination* `tableStyles` and
|
|
2569
|
+
* silently falls back when that id is absent; the source `<a:tblStyle>` is itself
|
|
2570
|
+
* symbolic (scheme colours), so copying it verbatim under the **same** id lets the
|
|
2571
|
+
* table keep its structure while re-branding to this deck's theme — no table-XML
|
|
2572
|
+
* rewrite needed. Idempotent: an id this deck already defines is left as-is, and a
|
|
2573
|
+
* referenced id with no source definition is skipped. Creates and wires a
|
|
2574
|
+
* `tableStyles.xml` part if this deck has none.
|
|
2575
|
+
*/
|
|
2576
|
+
function copySourceTableStyles(dest, sourceOpc, slideRoot) {
|
|
2577
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2578
|
+
for (const idEl of slideRoot.getElementsByTagNameNS(OOXML_NS.a, "tableStyleId")) {
|
|
2579
|
+
const id = idEl.textContent?.trim();
|
|
2580
|
+
if (id) ids.add(id);
|
|
2581
|
+
}
|
|
2582
|
+
if (ids.size === 0) return;
|
|
2583
|
+
const sourceList = tableStyleList(sourceOpc);
|
|
2584
|
+
if (!sourceList) return;
|
|
2585
|
+
const sourceStyles = new Map(getElements(sourceList, "a:tblStyle").map((st) => [attr(st, "styleId"), st]));
|
|
2586
|
+
const destPart = ensureTableStylesPart(dest);
|
|
2587
|
+
const destList = destPart.dom.documentElement;
|
|
2588
|
+
if (!destList) return;
|
|
2589
|
+
const referenced = [];
|
|
2590
|
+
for (const id of ids) {
|
|
2591
|
+
const src = sourceStyles.get(id);
|
|
2592
|
+
if (src) referenced.push(src);
|
|
2593
|
+
}
|
|
2594
|
+
if (addTableStyles(destList, referenced)) destPart.markDirty();
|
|
2595
|
+
}
|
|
2596
|
+
/**
|
|
2597
|
+
* Append each source `<a:tblStyle>` this deck does not already define, keyed by
|
|
2598
|
+
* `styleId` — the shared core of {@link copySourceTableStyles} (which offers only
|
|
2599
|
+
* the styles a restyled slide references) and {@link carryTableStyles} (which
|
|
2600
|
+
* offers the source's whole list). Destination-wins per id: a style this deck
|
|
2601
|
+
* already defines is left untouched, so a re-call adds nothing. Returns whether
|
|
2602
|
+
* anything was added.
|
|
2603
|
+
*/
|
|
2604
|
+
function addTableStyles(destList, styles) {
|
|
2605
|
+
const present = new Set(getElements(destList, "a:tblStyle").map((st) => attr(st, "styleId")));
|
|
2606
|
+
let added = false;
|
|
2607
|
+
for (const style of styles) {
|
|
2608
|
+
const id = attr(style, "styleId");
|
|
2609
|
+
if (!id || present.has(id)) continue;
|
|
2610
|
+
destList.appendChild(ownerDocumentOf(destList).importNode(style, true));
|
|
2611
|
+
present.add(id);
|
|
2612
|
+
added = true;
|
|
2613
|
+
}
|
|
2614
|
+
return added;
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Copy the source deck's **whole** `tableStyles.xml` into this deck: every
|
|
2618
|
+
* `<a:tblStyle>` it defines, plus its `a:tblStyleLst@def` (the default table
|
|
2619
|
+
* style). Presentation-level, so a separate traversal from the master/layout copy
|
|
2620
|
+
* chain — and whole-deck, since `tableStyles.xml` does not record which style
|
|
2621
|
+
* belongs to which master. See `ImportSlideMastersOptions.tableStyles`.
|
|
2622
|
+
*
|
|
2623
|
+
* Styles union by `styleId` (destination-wins per id, via {@link addTableStyles}),
|
|
2624
|
+
* but `def` is **source-wins**: a caller asking for the source's table styling
|
|
2625
|
+
* wants its default too, and the destination's `def` is typically a generator stub
|
|
2626
|
+
* rather than a deliberate choice. Carrying the styles without the `def` would be
|
|
2627
|
+
* the worse half-measure — the standard default GUID is one most templates also
|
|
2628
|
+
* define, so a new table would silently resolve to the wrong style rather than
|
|
2629
|
+
* visibly to none. Idempotent: a re-call re-adds nothing and re-sets `def` to the
|
|
2630
|
+
* same value.
|
|
2631
|
+
*/
|
|
2632
|
+
function carryTableStyles(dest, sourceOpc) {
|
|
2633
|
+
const sourceList = tableStyleList(sourceOpc);
|
|
2634
|
+
if (!sourceList) return;
|
|
2635
|
+
const destPart = ensureTableStylesPart(dest);
|
|
2636
|
+
const destList = destPart.dom.documentElement;
|
|
2637
|
+
if (!destList) return;
|
|
2638
|
+
let changed = addTableStyles(destList, getElements(sourceList, "a:tblStyle"));
|
|
2639
|
+
const sourceDef = attr(sourceList, "def");
|
|
2640
|
+
if (sourceDef && attr(destList, "def") !== sourceDef) {
|
|
2641
|
+
destList.setAttribute("def", sourceDef);
|
|
2642
|
+
changed = true;
|
|
2643
|
+
}
|
|
2644
|
+
if (changed) destPart.markDirty();
|
|
2645
|
+
}
|
|
2646
|
+
/** The `a:tblStyleLst` root of a package's `tableStyles.xml`, or `null` when it has none. */
|
|
2647
|
+
function tableStyleList(opc) {
|
|
2648
|
+
const part = opc.partsByContentType(TABLE_STYLES_CONTENT_TYPE)[0];
|
|
2649
|
+
return part ? part.dom.documentElement : null;
|
|
2650
|
+
}
|
|
2651
|
+
/**
|
|
2652
|
+
* The deck's `tableStyles.xml` part, creating an empty one (and wiring its
|
|
2653
|
+
* `presentation.xml` relationship + content type) when the deck has none.
|
|
2654
|
+
*/
|
|
2655
|
+
function ensureTableStylesPart(dest) {
|
|
2656
|
+
const existing = dest.opc.partsByContentType(TABLE_STYLES_CONTENT_TYPE)[0];
|
|
2657
|
+
if (existing) return existing;
|
|
2658
|
+
const partName = dest.opc.reservePartNameLike("/ppt/tableStyles.xml");
|
|
2659
|
+
const xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\r\n<a:tblStyleLst xmlns:a="${OOXML_NS.a}" def="${TABLE_STYLES_DEFAULT_GUID}"/>`;
|
|
2660
|
+
const part = dest.opc.addPart(partName, TABLE_STYLES_CONTENT_TYPE, textEncoder$2.encode(xml));
|
|
2661
|
+
dest.opc.relationshipsFor(dest.presentationPart.partName).add(TABLE_STYLES_REL, relativePartName(dest.presentationPart.partName, partName));
|
|
2662
|
+
return part;
|
|
2663
|
+
}
|
|
2664
|
+
//#endregion
|
|
2665
|
+
//#region src/read/api/master-registry.ts
|
|
2666
|
+
/**
|
|
2667
|
+
* Slide-master bookkeeping in `presentation.xml` and in a master's own
|
|
2668
|
+
* `p:sldLayoutIdLst`: registering a master so renderers treat it as active,
|
|
2669
|
+
* promoting masters to the front of the id list, allocating ids out of the shared
|
|
2670
|
+
* master/layout id space, and emptying a freshly-copied master's layout list.
|
|
2671
|
+
*
|
|
2672
|
+
* These are the destination-side half of the import machinery -- they read and
|
|
2673
|
+
* write only this deck, never a source package and never the copy registry -- so
|
|
2674
|
+
* they take a structural {@link DeckTarget} rather than a `Presentation`. The
|
|
2675
|
+
* source-side half that drives most of them is `part-copy.ts`.
|
|
2676
|
+
*/
|
|
2677
|
+
const SLIDE_MASTER_REL$3 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster";
|
|
2678
|
+
const SLIDE_LAYOUT_REL$3 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout";
|
|
2679
|
+
const MIN_SLIDE_MASTER_ID = 2147483648;
|
|
2680
|
+
/**
|
|
2681
|
+
* Wire a freshly-copied slide master into `presentation.xml`: add a
|
|
2682
|
+
* presentation→master relationship and a `p:sldMasterId` entry in
|
|
2683
|
+
* `p:sldMasterIdLst`. A master that is reachable only through the
|
|
2684
|
+
* slide→layout→master rel chain but absent from `p:sldMasterIdLst` is treated
|
|
2685
|
+
* as inactive by renderers, so its background/graphics never paint. No-op when
|
|
2686
|
+
* the master is already registered (shared across imports from one source).
|
|
2687
|
+
*/
|
|
2688
|
+
function registerMaster(dest, masterPartName) {
|
|
2689
|
+
const presPart = dest.presentationPart;
|
|
2690
|
+
const presRels = dest.opc.relationshipsFor(presPart.partName);
|
|
2691
|
+
for (const rel of presRels.byType(SLIDE_MASTER_REL$3)) if (presRels.resolveTarget(rel.id) === masterPartName) return;
|
|
2692
|
+
const relId = presRels.add(SLIDE_MASTER_REL$3, relativePartName(presPart.partName, masterPartName)).id;
|
|
2693
|
+
const root = presPart.dom.documentElement;
|
|
2694
|
+
if (!root) throw new PackageReadError("package/part-has-no-root", "presentation.xml has no document element to register a master in");
|
|
2695
|
+
const lst = getOrAddChild(root, "p:sldMasterIdLst", [
|
|
2696
|
+
"p:notesMasterIdLst",
|
|
2697
|
+
"p:handoutMasterIdLst",
|
|
2698
|
+
"p:sldIdLst",
|
|
2699
|
+
"p:sldSz",
|
|
2700
|
+
"p:notesSz",
|
|
2701
|
+
"p:embeddedFontLst",
|
|
2702
|
+
"p:custShowLst",
|
|
2703
|
+
"p:photoAlbum",
|
|
2704
|
+
"p:custDataLst",
|
|
2705
|
+
"p:kinsoku",
|
|
2706
|
+
"p:defaultTextStyle",
|
|
2707
|
+
"p:modifyVerifier",
|
|
2708
|
+
"p:extLst"
|
|
2709
|
+
]);
|
|
2710
|
+
const entry = createElement(presPart.dom, "p:sldMasterId");
|
|
2711
|
+
setAttr(entry, "id", String(nextMasterLayoutId(dest)));
|
|
2712
|
+
setAttr(entry, "r:id", relId);
|
|
2713
|
+
lst.appendChild(entry);
|
|
2714
|
+
presPart.markDirty();
|
|
2715
|
+
}
|
|
2716
|
+
/**
|
|
2717
|
+
* Move the `p:sldMasterId` entries for `masterPartNames` to the front of
|
|
2718
|
+
* `p:sldMasterIdLst`, preserving their existing relative order, so the first of
|
|
2719
|
+
* them becomes the deck's `Designs(1)` — the theme PowerPoint's Design tab shows.
|
|
2720
|
+
* Reorders the id list only; relationships, ids, and all other parts are left as
|
|
2721
|
+
* they are. A no-op when the named masters already lead (idempotent re-call).
|
|
2722
|
+
*/
|
|
2723
|
+
function promoteMasters(dest, masterPartNames) {
|
|
2724
|
+
if (masterPartNames.length === 0) return;
|
|
2725
|
+
const presPart = dest.presentationPart;
|
|
2726
|
+
const root = presPart.dom.documentElement;
|
|
2727
|
+
const lst = root && firstChild(root, "p:sldMasterIdLst");
|
|
2728
|
+
if (!lst) return;
|
|
2729
|
+
const rels = dest.opc.relationshipsFor(presPart.partName);
|
|
2730
|
+
const promote = new Set(masterPartNames);
|
|
2731
|
+
const entries = getElements(lst, "p:sldMasterId");
|
|
2732
|
+
const isPromoted = (entry) => {
|
|
2733
|
+
const relId = attr(entry, "r:id");
|
|
2734
|
+
return relId ? promote.has(rels.resolveTarget(relId)) : false;
|
|
2735
|
+
};
|
|
2736
|
+
const desired = [...entries.filter(isPromoted), ...entries.filter((e) => !isPromoted(e))];
|
|
2737
|
+
if (desired.every((entry, i) => entry === entries[i])) return;
|
|
2738
|
+
for (const entry of desired) lst.appendChild(entry);
|
|
2739
|
+
presPart.markDirty();
|
|
2740
|
+
}
|
|
2741
|
+
/**
|
|
2742
|
+
* The next free id in the presentation-wide slide-master / slide-layout id space.
|
|
2743
|
+
*
|
|
2744
|
+
* `p:sldMasterId/@id` and every master's `p:sldLayoutId/@id` draw from ONE shared
|
|
2745
|
+
* id space (both ST_SlideMasterId/ST_SlideLayoutId, floor 2147483648) and must be
|
|
2746
|
+
* unique across the WHOLE presentation, not just within their own list. A duplicate
|
|
2747
|
+
* anywhere in this space makes PowerPoint report the file as corrupt on open
|
|
2748
|
+
* ("found a problem with content" / 0x80070570), even though LibreOffice silently
|
|
2749
|
+
* tolerates it — so an imported master AND each of its layouts must take an id past
|
|
2750
|
+
* the max of both the master-id list and every layout-id list. Recomputed per
|
|
2751
|
+
* allocation so ids appended earlier in the same import are counted.
|
|
2752
|
+
*/
|
|
2753
|
+
function nextMasterLayoutId(dest) {
|
|
2754
|
+
let max = MIN_SLIDE_MASTER_ID - 1;
|
|
2755
|
+
const presPart = dest.presentationPart;
|
|
2756
|
+
const root = presPart.dom.documentElement;
|
|
2757
|
+
const masterLst = root && firstChild(root, "p:sldMasterIdLst");
|
|
2758
|
+
if (masterLst) for (const entry of getElements(masterLst, "p:sldMasterId")) {
|
|
2759
|
+
const id = intValue(attr(entry, "id"));
|
|
2760
|
+
if (id !== null && id > max) max = id;
|
|
2761
|
+
}
|
|
2762
|
+
const presRels = dest.opc.relationshipsFor(presPart.partName);
|
|
2763
|
+
for (const rel of presRels.byType(SLIDE_MASTER_REL$3)) {
|
|
2764
|
+
const masterRoot = dest.opc.part(presRels.resolveTarget(rel.id))?.dom.documentElement;
|
|
2765
|
+
const layoutLst = masterRoot && firstChild(masterRoot, "p:sldLayoutIdLst");
|
|
2766
|
+
if (!layoutLst) continue;
|
|
2767
|
+
for (const entry of getElements(layoutLst, "p:sldLayoutId")) {
|
|
2768
|
+
const id = intValue(attr(entry, "id"));
|
|
2769
|
+
if (id !== null && id > max) max = id;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
return max + 1;
|
|
2773
|
+
}
|
|
2774
|
+
/** Empty a freshly-copied master's `p:sldLayoutIdLst`; copied layouts repopulate it. */
|
|
2775
|
+
function clearLayoutIdList(dest, masterPartName) {
|
|
2776
|
+
const masterPart = dest.opc.part(masterPartName);
|
|
2777
|
+
const root = masterPart?.dom.documentElement;
|
|
2778
|
+
const lst = root && firstChild(root, "p:sldLayoutIdLst");
|
|
2779
|
+
if (!masterPart || !lst) return;
|
|
2780
|
+
removeChildrenByQName(lst, ["p:sldLayoutId"]);
|
|
2781
|
+
masterPart.markDirty();
|
|
2782
|
+
}
|
|
2783
|
+
/**
|
|
2784
|
+
* Append a `p:sldLayoutId` entry for `layoutPartName` to `masterPartName`'s
|
|
2785
|
+
* `p:sldLayoutIdLst`, adding the master→layout relationship it points at. The
|
|
2786
|
+
* destination-side half of linking a copied layout into its copied master; the
|
|
2787
|
+
* caller resolves which master that is from the source-side rels.
|
|
2788
|
+
*/
|
|
2789
|
+
function addLayoutToMaster(dest, masterPartName, layoutPartName) {
|
|
2790
|
+
const masterPart = dest.opc.part(masterPartName);
|
|
2791
|
+
const root = masterPart?.dom.documentElement;
|
|
2792
|
+
if (!masterPart || !root) return;
|
|
2793
|
+
const relId = dest.opc.relationshipsFor(masterPartName).add(SLIDE_LAYOUT_REL$3, relativePartName(masterPartName, layoutPartName)).id;
|
|
2794
|
+
const lst = getOrAddChild(root, "p:sldLayoutIdLst", [
|
|
2795
|
+
"p:transition",
|
|
2796
|
+
"p:timing",
|
|
2797
|
+
"p:hf",
|
|
2798
|
+
"p:txStyles",
|
|
2799
|
+
"p:extLst"
|
|
2800
|
+
]);
|
|
2801
|
+
const entry = createElement(masterPart.dom, "p:sldLayoutId");
|
|
2802
|
+
setAttr(entry, "id", String(nextMasterLayoutId(dest)));
|
|
2803
|
+
setAttr(entry, "r:id", relId);
|
|
2804
|
+
lst.appendChild(entry);
|
|
2805
|
+
masterPart.markDirty();
|
|
2806
|
+
}
|
|
2807
|
+
//#endregion
|
|
2808
|
+
//#region src/read/api/part-copy.ts
|
|
2809
|
+
const SLIDE_LAYOUT_REL$2 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout";
|
|
2810
|
+
const SLIDE_MASTER_REL$2 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster";
|
|
2811
|
+
const NOTES_SLIDE_REL$2 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide";
|
|
2812
|
+
const SLIDE_MASTER_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml";
|
|
2813
|
+
const SLIDE_LAYOUT_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml";
|
|
2814
|
+
/**
|
|
2815
|
+
* Copy `sourcePartName` (and, recursively, every internal part it references)
|
|
2816
|
+
* from `ctx.source` into `ctx.dest`, returning the new partname. Idempotent
|
|
2817
|
+
* per source package via the copy registry. Relationship ids are preserved so
|
|
2818
|
+
* the copied part body's `r:id`/`r:embed` references stay valid; targets are
|
|
2819
|
+
* rewritten to the freshly-allocated partnames. Notes relationships are
|
|
2820
|
+
* dropped. A copied `slideMaster` does not drag in all its sibling layouts —
|
|
2821
|
+
* each imported `slideLayout` wires itself into the master instead (see
|
|
2822
|
+
* {@link linkLayoutIntoMaster}).
|
|
2823
|
+
*/
|
|
2824
|
+
function copyPart(ctx, sourcePartName) {
|
|
2825
|
+
const existing = ctx.registry.get(sourcePartName);
|
|
2826
|
+
if (existing) return existing;
|
|
2827
|
+
const sourcePart = ctx.source.part(sourcePartName);
|
|
2828
|
+
if (!sourcePart) throw new PackageReadError("package/part-missing", `importSlide: source package has no part ${sourcePartName}`);
|
|
2829
|
+
const newPartName = ctx.dest.opc.reservePartNameLike(sourcePartName);
|
|
2830
|
+
ctx.dest.opc.addPart(newPartName, sourcePart.contentType, sourcePart.bytes);
|
|
2831
|
+
ctx.registry.set(sourcePartName, newPartName);
|
|
2832
|
+
const isMaster = sourcePart.contentType === SLIDE_MASTER_CONTENT_TYPE;
|
|
2833
|
+
const sourceRels = ctx.source.relationshipsFor(sourcePartName);
|
|
2834
|
+
const targetRels = ctx.dest.opc.relationshipsFor(newPartName);
|
|
2835
|
+
for (const rel of sourceRels) {
|
|
2836
|
+
if (rel.type === NOTES_SLIDE_REL$2) continue;
|
|
2837
|
+
if (isMaster && rel.type === SLIDE_LAYOUT_REL$2) continue;
|
|
2838
|
+
if (rel.targetMode === "External") {
|
|
2839
|
+
targetRels.addWithId(rel.id, rel.type, rel.target, "External");
|
|
2840
|
+
continue;
|
|
2841
|
+
}
|
|
2842
|
+
const newTargetPartName = copyPart(ctx, sourceRels.resolveTarget(rel.id));
|
|
2843
|
+
targetRels.addWithId(rel.id, rel.type, relativePartName(newPartName, newTargetPartName));
|
|
2844
|
+
}
|
|
2845
|
+
if (isMaster) {
|
|
2846
|
+
clearLayoutIdList(ctx.dest, newPartName);
|
|
2847
|
+
registerMaster(ctx.dest, newPartName);
|
|
2848
|
+
}
|
|
2849
|
+
if (sourcePart.contentType === SLIDE_LAYOUT_CONTENT_TYPE) linkLayoutIntoMaster(ctx, sourceRels, newPartName);
|
|
2850
|
+
return newPartName;
|
|
2851
|
+
}
|
|
2852
|
+
/**
|
|
2853
|
+
* Wire a just-copied layout into its (already-copied) master. Resolves which
|
|
2854
|
+
* destination master that is by running the layout's *source* master rel through
|
|
2855
|
+
* the copy registry, then hands the destination-side wiring to
|
|
2856
|
+
* {@link addLayoutToMaster}. Called once per copied layout, so the master
|
|
2857
|
+
* accumulates exactly the imported layouts.
|
|
2858
|
+
*/
|
|
2859
|
+
function linkLayoutIntoMaster(ctx, layoutSourceRels, layoutPartName) {
|
|
2860
|
+
const masterRel = layoutSourceRels.byType(SLIDE_MASTER_REL$2)[0];
|
|
2861
|
+
if (!masterRel) return;
|
|
2862
|
+
const masterPartName = ctx.registry.get(layoutSourceRels.resolveTarget(masterRel.id));
|
|
2863
|
+
if (!masterPartName) return;
|
|
2864
|
+
addLayoutToMaster(ctx.dest, masterPartName, layoutPartName);
|
|
2865
|
+
}
|
|
2866
|
+
//#endregion
|
|
2867
|
+
//#region src/read/api/ops/embedded-fonts.ts
|
|
2868
|
+
/**
|
|
2869
|
+
* Merging embedded font faces into a deck's `p:embeddedFontLst`.
|
|
2870
|
+
*
|
|
2871
|
+
* Fonts arrive two ways — copied out of another `Presentation`, or handed over as raw bytes by a
|
|
2872
|
+
* generator during an append — and the two differ only in how the binary part comes into being.
|
|
2873
|
+
* That difference is confined to a per-face thunk, so both paths share one merge core: entries
|
|
2874
|
+
* join by typeface, faces de-dupe by slot, and the thunk runs only for a face actually being
|
|
2875
|
+
* added, so a face the deck already embeds never mints an orphan part.
|
|
2876
|
+
*/
|
|
2877
|
+
/**
|
|
2878
|
+
* `p:embeddedFontLst`'s document-order successors in `CT_Presentation` (index 7,
|
|
2879
|
+
* after `smartTags`): everything that may legally follow it, so a created list
|
|
2880
|
+
* lands in the right slot when the deck has none yet.
|
|
2881
|
+
*/
|
|
2882
|
+
const PRESENTATION_EMBEDDED_FONT_LST_SUCCESSORS = [
|
|
2883
|
+
"p:custShowLst",
|
|
2884
|
+
"p:photoAlbum",
|
|
2885
|
+
"p:custDataLst",
|
|
2886
|
+
"p:kinsoku",
|
|
2887
|
+
"p:defaultTextStyle",
|
|
2888
|
+
"p:modifyVerifier",
|
|
2889
|
+
"p:extLst"
|
|
2890
|
+
];
|
|
2891
|
+
/**
|
|
2892
|
+
* A face slot's document-order successors in `CT_EmbeddedFontListEntry`
|
|
2893
|
+
* (`font`, `regular`, `bold`, `italic`, `boldItalic`), so a newly-inserted face
|
|
2894
|
+
* keeps the schema's child order regardless of which slots already exist.
|
|
2895
|
+
*/
|
|
2896
|
+
const EMBEDDED_FONT_FACE_SUCCESSORS = {
|
|
2897
|
+
regular: [
|
|
2898
|
+
"p:bold",
|
|
2899
|
+
"p:italic",
|
|
2900
|
+
"p:boldItalic"
|
|
2901
|
+
],
|
|
2902
|
+
bold: ["p:italic", "p:boldItalic"],
|
|
2903
|
+
italic: ["p:boldItalic"],
|
|
2904
|
+
boldItalic: []
|
|
2905
|
+
};
|
|
2906
|
+
/**
|
|
2907
|
+
* Copy `source`'s embedded fonts into this deck and merge them into our
|
|
2908
|
+
* `p:embeddedFontLst`. Font binaries come across via {@link copyPart} (so the
|
|
2909
|
+
* per-source registry dedupes faces shared across repeated imports); entries are
|
|
2910
|
+
* merged by `typeface` + face slot, so a face this deck already embeds is reused
|
|
2911
|
+
* rather than duplicated. No-op when the source embeds no fonts. See
|
|
2912
|
+
* {@link ImportSlideOptions.embedFonts}.
|
|
2913
|
+
*/
|
|
2914
|
+
function carryEmbeddedFonts(dest, source, ctx) {
|
|
2915
|
+
const sourceRoot = source.presentationPart.dom.documentElement;
|
|
2916
|
+
const sourceLst = sourceRoot && firstChild(sourceRoot, "p:embeddedFontLst");
|
|
2917
|
+
const sourceEntries = sourceLst ? getElements(sourceLst, "p:embeddedFont") : [];
|
|
2918
|
+
if (sourceEntries.length === 0) return;
|
|
2919
|
+
const sourcePresRels = source.opc.relationshipsFor(source.presentationPart.partName);
|
|
2920
|
+
const incoming = [];
|
|
2921
|
+
for (const srcEntry of sourceEntries) {
|
|
2922
|
+
const srcFont = firstChild(srcEntry, "p:font");
|
|
2923
|
+
const typeface = srcFont ? attr(srcFont, "typeface") : null;
|
|
2924
|
+
if (!srcFont || !typeface) continue;
|
|
2925
|
+
const identity = [];
|
|
2926
|
+
for (const name of [
|
|
2927
|
+
"panose",
|
|
2928
|
+
"pitchFamily",
|
|
2929
|
+
"charset"
|
|
2930
|
+
]) {
|
|
2931
|
+
const value = attr(srcFont, name);
|
|
2932
|
+
if (value !== null) identity.push({
|
|
2933
|
+
name,
|
|
2934
|
+
value
|
|
2935
|
+
});
|
|
2936
|
+
}
|
|
2937
|
+
const faces = [];
|
|
2938
|
+
for (const slot of EMBEDDED_FONT_SLOTS) {
|
|
2939
|
+
const srcFace = firstChild(srcEntry, `p:${slot}`);
|
|
2940
|
+
const srcRid = srcFace && attr(srcFace, "r:id");
|
|
2941
|
+
if (!srcFace || !srcRid) continue;
|
|
2942
|
+
faces.push({
|
|
2943
|
+
slot,
|
|
2944
|
+
createPart: () => copyPart(ctx, sourcePresRels.resolveTarget(srcRid))
|
|
2945
|
+
});
|
|
2946
|
+
}
|
|
2947
|
+
incoming.push({
|
|
2948
|
+
typeface,
|
|
2949
|
+
identity,
|
|
2950
|
+
faces
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
mergeEmbeddedFontEntries(dest, incoming);
|
|
2954
|
+
}
|
|
2955
|
+
/**
|
|
2956
|
+
* Carry a generator's presentation-level embedded fonts ({@link ExtractedSlides.embeddedFonts},
|
|
2957
|
+
* from `pptx.embedFont`) into this deck during {@link Presentation.appendSlides}. Each face's raw bytes are
|
|
2958
|
+
* written as a fresh `/ppt/fonts/fontN.fntdata` part; merge/de-dupe by typeface + slot is shared
|
|
2959
|
+
* with {@link carryEmbeddedFonts} via {@link mergeEmbeddedFontEntries}, so appending the same
|
|
2960
|
+
* generator twice (or onto a deck that already embeds the face) carries each face once.
|
|
2961
|
+
*/
|
|
2962
|
+
function carryGeneratedEmbeddedFonts(dest, fonts) {
|
|
2963
|
+
const incoming = [];
|
|
2964
|
+
for (const font of fonts) {
|
|
2965
|
+
if (!font.typeface) continue;
|
|
2966
|
+
const identity = [];
|
|
2967
|
+
if (font.panose !== void 0) identity.push({
|
|
2968
|
+
name: "panose",
|
|
2969
|
+
value: font.panose
|
|
2970
|
+
});
|
|
2971
|
+
if (font.pitchFamily !== void 0) identity.push({
|
|
2972
|
+
name: "pitchFamily",
|
|
2973
|
+
value: String(font.pitchFamily)
|
|
2974
|
+
});
|
|
2975
|
+
if (font.charset !== void 0) identity.push({
|
|
2976
|
+
name: "charset",
|
|
2977
|
+
value: String(font.charset)
|
|
2978
|
+
});
|
|
2979
|
+
const faces = [];
|
|
2980
|
+
for (const slot of EMBEDDED_FONT_SLOTS) {
|
|
2981
|
+
const face = font.faces.find((f) => f.slot === slot);
|
|
2982
|
+
if (!face?.bytes) continue;
|
|
2983
|
+
const bytes = face.bytes;
|
|
2984
|
+
faces.push({
|
|
2985
|
+
slot,
|
|
2986
|
+
createPart: () => {
|
|
2987
|
+
const partName = dest.opc.reservePartNameLike("/ppt/fonts/font1.fntdata");
|
|
2988
|
+
dest.opc.addPart(partName, FONT_DATA_CONTENT_TYPE, bytes);
|
|
2989
|
+
return partName;
|
|
2990
|
+
}
|
|
2991
|
+
});
|
|
2992
|
+
}
|
|
2993
|
+
if (faces.length > 0) incoming.push({
|
|
2994
|
+
typeface: font.typeface,
|
|
2995
|
+
identity,
|
|
2996
|
+
faces
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
mergeEmbeddedFontEntries(dest, incoming);
|
|
3000
|
+
}
|
|
3001
|
+
/**
|
|
3002
|
+
* Merge normalized {@link IncomingEmbeddedFont} entries into this deck's
|
|
3003
|
+
* `p:embeddedFontLst` — the shared core of {@link carryEmbeddedFonts} (import-side)
|
|
3004
|
+
* and {@link carryGeneratedEmbeddedFonts} (append-side). Entries merge by `typeface`,
|
|
3005
|
+
* faces de-dupe by slot (a face this deck already embeds is left as is). For each newly
|
|
3006
|
+
* added face the `fntdata` Default is ensured, the binary part is created via the face's
|
|
3007
|
+
* `createPart` thunk, a `font` rel is added to presentation.xml, and the `p:<slot>` element
|
|
3008
|
+
* is inserted in schema child order. The list is created at CT_Presentation index 7 when
|
|
3009
|
+
* the deck has none yet. No-op for empty input.
|
|
3010
|
+
*/
|
|
3011
|
+
function mergeEmbeddedFontEntries(dest, entries) {
|
|
3012
|
+
if (entries.length === 0) return;
|
|
3013
|
+
const presPart = dest.presentationPart;
|
|
3014
|
+
const presRoot = presPart.dom.documentElement;
|
|
3015
|
+
if (!presRoot) throw new PackageReadError("package/part-has-no-root", "presentation.xml has no document element to carry embedded fonts into");
|
|
3016
|
+
const presRels = dest.opc.relationshipsFor(presPart.partName);
|
|
3017
|
+
const targetLst = getOrAddChild(presRoot, "p:embeddedFontLst", PRESENTATION_EMBEDDED_FONT_LST_SUCCESSORS);
|
|
3018
|
+
const targetByTypeface = /* @__PURE__ */ new Map();
|
|
3019
|
+
for (const entry of getElements(targetLst, "p:embeddedFont")) {
|
|
3020
|
+
const font = firstChild(entry, "p:font");
|
|
3021
|
+
const typeface = font && attr(font, "typeface");
|
|
3022
|
+
if (typeface) targetByTypeface.set(typeface, entry);
|
|
3023
|
+
}
|
|
3024
|
+
let copiedAny = false;
|
|
3025
|
+
for (const incoming of entries) {
|
|
3026
|
+
let targetEntry = targetByTypeface.get(incoming.typeface);
|
|
3027
|
+
if (!targetEntry) {
|
|
3028
|
+
targetEntry = createElement(presPart.dom, "p:embeddedFont");
|
|
3029
|
+
const targetFont = createElement(presPart.dom, "p:font");
|
|
3030
|
+
setAttr(targetFont, "typeface", incoming.typeface);
|
|
3031
|
+
for (const { name, value } of incoming.identity) setAttr(targetFont, name, value);
|
|
3032
|
+
targetEntry.appendChild(targetFont);
|
|
3033
|
+
targetLst.appendChild(targetEntry);
|
|
3034
|
+
targetByTypeface.set(incoming.typeface, targetEntry);
|
|
3035
|
+
}
|
|
3036
|
+
for (const face of incoming.faces) {
|
|
3037
|
+
if (firstChild(targetEntry, `p:${face.slot}`)) continue;
|
|
3038
|
+
dest.opc.contentTypes.ensureDefault(FONT_DATA_EXTENSION, FONT_DATA_CONTENT_TYPE);
|
|
3039
|
+
const newFontPart = face.createPart();
|
|
3040
|
+
const relId = presRels.add(FONT_REL_TYPE, relativePartName(presPart.partName, newFontPart)).id;
|
|
3041
|
+
const targetFace = createElement(presPart.dom, `p:${face.slot}`);
|
|
3042
|
+
setAttr(targetFace, "r:id", relId);
|
|
3043
|
+
insertInOrder(targetEntry, targetFace, EMBEDDED_FONT_FACE_SUCCESSORS[face.slot]);
|
|
3044
|
+
copiedAny = true;
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
if (copiedAny) presPart.markDirty();
|
|
3048
|
+
}
|
|
3049
|
+
//#endregion
|
|
3050
|
+
//#region src/read/api/ops/notes-master.ts
|
|
3051
|
+
/**
|
|
3052
|
+
* Carrying speaker notes and the deck's single notes master across an import or an append.
|
|
3053
|
+
*
|
|
3054
|
+
* A `notesSlide` is not self-contained: it back-references the slide it annotates and forward-
|
|
3055
|
+
* references the deck's notes master. `CT_NotesMasterIdList` allows at most one master per
|
|
3056
|
+
* presentation, so a deck that already has one keeps it — the destination's notes styling wins —
|
|
3057
|
+
* and a source master is copied only into a deck that has none. That single rule is why both the
|
|
3058
|
+
* `Presentation`-to-`Presentation` path and the generator-XML path funnel through
|
|
3059
|
+
* `registerNotesMaster`: they differ only in how the *part* comes into being.
|
|
3060
|
+
*/
|
|
3061
|
+
const SLIDE_REL$1 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
|
|
3062
|
+
const NOTES_SLIDE_REL$1 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide";
|
|
3063
|
+
const NOTES_MASTER_REL$1 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster";
|
|
3064
|
+
const THEME_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme";
|
|
3065
|
+
const NOTES_MASTER_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml";
|
|
3066
|
+
const THEME_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.theme+xml";
|
|
3067
|
+
const textEncoder$1 = new TextEncoder();
|
|
3068
|
+
/**
|
|
3069
|
+
* Carry the source slide's speaker notes onto the just-imported slide (the
|
|
3070
|
+
* `importNotes` option). The slide copy itself dropped the `notesSlide` rel, so
|
|
3071
|
+
* this copies the source `notesSlide` part into a fresh partname, wires a
|
|
3072
|
+
* `slide → notesSlide` rel on the new slide, and rebuilds the copied notesSlide's
|
|
3073
|
+
* own relationships:
|
|
3074
|
+
*
|
|
3075
|
+
* - its `slide` back-rel is repointed at the new slide (`newSlidePartName`) — the
|
|
3076
|
+
* source slide is *not* copied (that would be circular);
|
|
3077
|
+
* - its `notesMaster` rel is resolved through {@link ensureNotesMaster}, which
|
|
3078
|
+
* reuses this deck's notesMaster when it has one and copies the source's only
|
|
3079
|
+
* when it has none (a deck may have at most one notesMaster);
|
|
3080
|
+
* - any other internal target (media, etc.) is copied via {@link copyPart}.
|
|
3081
|
+
*
|
|
3082
|
+
* No-op when the source slide has no notes. Content-type registration for the
|
|
3083
|
+
* copied parts is handled by `addPart`/{@link copyPart}.
|
|
3084
|
+
*/
|
|
3085
|
+
function carryNotes(dest, source, ctx, sourceSlidePartName, newSlidePartName) {
|
|
3086
|
+
const sourceSlideRels = source.opc.relationshipsFor(sourceSlidePartName);
|
|
3087
|
+
const notesRel = sourceSlideRels.byType(NOTES_SLIDE_REL$1)[0];
|
|
3088
|
+
if (!notesRel) return;
|
|
3089
|
+
const sourceNotesPartName = sourceSlideRels.resolveTarget(notesRel.id);
|
|
3090
|
+
const sourceNotesPart = source.opc.part(sourceNotesPartName);
|
|
3091
|
+
if (!sourceNotesPart) return;
|
|
3092
|
+
const newNotesPartName = dest.opc.reservePartNameLike(sourceNotesPartName);
|
|
3093
|
+
dest.opc.addPart(newNotesPartName, sourceNotesPart.contentType, sourceNotesPart.bytes);
|
|
3094
|
+
dest.opc.relationshipsFor(newSlidePartName).add(NOTES_SLIDE_REL$1, relativePartName(newSlidePartName, newNotesPartName));
|
|
3095
|
+
const notesSourceRels = source.opc.relationshipsFor(sourceNotesPartName);
|
|
3096
|
+
const notesTargetRels = dest.opc.relationshipsFor(newNotesPartName);
|
|
3097
|
+
for (const rel of notesSourceRels) {
|
|
3098
|
+
if (rel.type === SLIDE_REL$1) {
|
|
3099
|
+
notesTargetRels.addWithId(rel.id, SLIDE_REL$1, relativePartName(newNotesPartName, newSlidePartName));
|
|
3100
|
+
continue;
|
|
3101
|
+
}
|
|
3102
|
+
if (rel.type === NOTES_MASTER_REL$1) {
|
|
3103
|
+
const notesMaster = ensureNotesMaster(dest, ctx, notesSourceRels.resolveTarget(rel.id));
|
|
3104
|
+
notesTargetRels.addWithId(rel.id, NOTES_MASTER_REL$1, relativePartName(newNotesPartName, notesMaster));
|
|
3105
|
+
continue;
|
|
3106
|
+
}
|
|
3107
|
+
if (rel.targetMode === "External") {
|
|
3108
|
+
notesTargetRels.addWithId(rel.id, rel.type, rel.target, "External");
|
|
3109
|
+
continue;
|
|
3110
|
+
}
|
|
3111
|
+
const newTarget = copyPart(ctx, notesSourceRels.resolveTarget(rel.id));
|
|
3112
|
+
notesTargetRels.addWithId(rel.id, rel.type, relativePartName(newNotesPartName, newTarget));
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
/**
|
|
3116
|
+
* Resolve the notesMaster an imported `notesSlide` should bind to, honouring the
|
|
3117
|
+
* single-notesMaster-per-presentation rule (`p:notesMasterIdLst` holds 0..1
|
|
3118
|
+
* `p:notesMasterId`). If this deck already has a notesMaster it is reused and the
|
|
3119
|
+
* source's is *not* copied (the destination's notes styling wins); otherwise the
|
|
3120
|
+
* source notesMaster (and, via {@link copyPart}, its theme) is copied and
|
|
3121
|
+
* registered in `presentation.xml`. Returns the destination notesMaster partname.
|
|
3122
|
+
*/
|
|
3123
|
+
function ensureNotesMaster(dest, ctx, sourceNotesMasterPartName) {
|
|
3124
|
+
const presPart = dest.presentationPart;
|
|
3125
|
+
const presRels = dest.opc.relationshipsFor(presPart.partName);
|
|
3126
|
+
const existing = presRels.byType(NOTES_MASTER_REL$1)[0];
|
|
3127
|
+
if (existing) return presRels.resolveTarget(existing.id);
|
|
3128
|
+
return registerNotesMaster(dest, copyPart(ctx, sourceNotesMasterPartName));
|
|
3129
|
+
}
|
|
3130
|
+
/**
|
|
3131
|
+
* Wire an already-added notesMaster part into `presentation.xml`: a `notesMaster`
|
|
3132
|
+
* relationship plus the single `p:notesMasterId` entry that `CT_NotesMasterIdList`
|
|
3133
|
+
* allows. Returns the partname, so callers can use it as a rel target.
|
|
3134
|
+
*
|
|
3135
|
+
* Split out of {@link ensureNotesMaster} because the two ways a notesMaster arrives
|
|
3136
|
+
* — copied from another `Presentation`, or authored by a generator and injected by
|
|
3137
|
+
* {@link Presentation.appendSlides} — differ only in how the *part* is created, not in how it is
|
|
3138
|
+
* registered.
|
|
3139
|
+
*/
|
|
3140
|
+
function registerNotesMaster(dest, notesMasterPartName) {
|
|
3141
|
+
const presPart = dest.presentationPart;
|
|
3142
|
+
const relId = dest.opc.relationshipsFor(presPart.partName).add(NOTES_MASTER_REL$1, relativePartName(presPart.partName, notesMasterPartName)).id;
|
|
3143
|
+
const root = presPart.dom.documentElement;
|
|
3144
|
+
if (!root) throw new PackageReadError("package/part-has-no-root", "presentation.xml has no document element to register a notes master in");
|
|
3145
|
+
const lst = getOrAddChild(root, "p:notesMasterIdLst", [
|
|
3146
|
+
"p:handoutMasterIdLst",
|
|
3147
|
+
"p:sldIdLst",
|
|
3148
|
+
"p:sldSz",
|
|
3149
|
+
"p:notesSz",
|
|
3150
|
+
"p:embeddedFontLst",
|
|
3151
|
+
"p:custShowLst",
|
|
3152
|
+
"p:photoAlbum",
|
|
3153
|
+
"p:custDataLst",
|
|
3154
|
+
"p:kinsoku",
|
|
3155
|
+
"p:defaultTextStyle",
|
|
3156
|
+
"p:modifyVerifier",
|
|
3157
|
+
"p:extLst"
|
|
3158
|
+
]);
|
|
3159
|
+
removeChildrenByQName(lst, ["p:notesMasterId"]);
|
|
3160
|
+
const entry = createElement(presPart.dom, "p:notesMasterId");
|
|
3161
|
+
setAttr(entry, "r:id", relId);
|
|
3162
|
+
lst.appendChild(entry);
|
|
3163
|
+
presPart.markDirty();
|
|
3164
|
+
return notesMasterPartName;
|
|
3165
|
+
}
|
|
3166
|
+
/**
|
|
3167
|
+
* Resolve the notesMaster an *appended* slide's notes should bind to. Same
|
|
3168
|
+
* single-notesMaster rule as {@link ensureNotesMaster}: this deck's own wins when it
|
|
3169
|
+
* has one, so the destination's notes styling is preserved and `master.xml` is
|
|
3170
|
+
* discarded. Otherwise the generator's notes master is installed, together with the
|
|
3171
|
+
* theme its `.rels` requires (the normal write path emits that as `theme2.xml`).
|
|
3172
|
+
*/
|
|
3173
|
+
function ensureNotesMasterFromXml(dest, master) {
|
|
3174
|
+
const presPart = dest.presentationPart;
|
|
3175
|
+
const presRels = dest.opc.relationshipsFor(presPart.partName);
|
|
3176
|
+
const existing = presRels.byType(NOTES_MASTER_REL$1)[0];
|
|
3177
|
+
if (existing) return presRels.resolveTarget(existing.id);
|
|
3178
|
+
const masterPartName = dest.opc.reservePartNameLike("/ppt/notesMasters/notesMaster1.xml");
|
|
3179
|
+
dest.opc.addPart(masterPartName, NOTES_MASTER_CONTENT_TYPE, textEncoder$1.encode(master.xml));
|
|
3180
|
+
const themePartName = dest.opc.reservePartNameLike("/ppt/theme/theme1.xml");
|
|
3181
|
+
dest.opc.addPart(themePartName, THEME_CONTENT_TYPE, textEncoder$1.encode(master.themeXml));
|
|
3182
|
+
dest.opc.relationshipsFor(masterPartName).add(THEME_REL, relativePartName(masterPartName, themePartName));
|
|
3183
|
+
return registerNotesMaster(dest, masterPartName);
|
|
3184
|
+
}
|
|
3185
|
+
//#endregion
|
|
3186
|
+
//#region src/read/api/ops/part-index.ts
|
|
3187
|
+
/**
|
|
3188
|
+
* Walking a deck's master/layout spine by partname.
|
|
3189
|
+
*
|
|
3190
|
+
* The `p:sldMasterIdLst` / `p:sldLayoutIdLst` entries hold relationship ids, not partnames, so
|
|
3191
|
+
* every traversal has to resolve through the owning part's relationships. These are the
|
|
3192
|
+
* source-side reads that do it — used against a *foreign* presentation as often as against this
|
|
3193
|
+
* one, which is why they take the deck rather than living on it.
|
|
3194
|
+
*/
|
|
3195
|
+
/** Master partnames in `p:sldMasterIdLst` order. */
|
|
3196
|
+
function slideMasterPartNames(pres) {
|
|
3197
|
+
const root = pres.presentationPart.dom.documentElement;
|
|
3198
|
+
const lst = root && firstChild(root, "p:sldMasterIdLst");
|
|
3199
|
+
if (!lst) return [];
|
|
3200
|
+
const rels = pres.opc.relationshipsFor(pres.presentationPart.partName);
|
|
3201
|
+
const out = [];
|
|
3202
|
+
for (const entry of getElements(lst, "p:sldMasterId")) {
|
|
3203
|
+
const relId = attr(entry, "r:id");
|
|
3204
|
+
if (relId) out.push(rels.resolveTarget(relId));
|
|
3205
|
+
}
|
|
3206
|
+
return out;
|
|
3207
|
+
}
|
|
3208
|
+
/** A master's layout partnames in `p:sldLayoutIdLst` order. */
|
|
3209
|
+
function layoutPartNamesOf(pres, masterPartName) {
|
|
3210
|
+
const root = pres.opc.part(masterPartName)?.dom.documentElement;
|
|
3211
|
+
const lst = root && firstChild(root, "p:sldLayoutIdLst");
|
|
3212
|
+
if (!lst) return [];
|
|
3213
|
+
const rels = pres.opc.relationshipsFor(masterPartName);
|
|
3214
|
+
const out = [];
|
|
3215
|
+
for (const entry of getElements(lst, "p:sldLayoutId")) {
|
|
3216
|
+
const relId = attr(entry, "r:id");
|
|
3217
|
+
if (relId) out.push(rels.resolveTarget(relId));
|
|
3218
|
+
}
|
|
3219
|
+
return out;
|
|
3220
|
+
}
|
|
3221
|
+
/** Resolve the single relationship of `type` owned by `partName`, or `null`. */
|
|
3222
|
+
function resolveSingleRel(opc, partName, type) {
|
|
3223
|
+
const rels = opc.relationshipsFor(partName);
|
|
3224
|
+
const rel = rels.byType(type)[0];
|
|
3225
|
+
return rel ? rels.resolveTarget(rel.id) : null;
|
|
3226
|
+
}
|
|
3227
|
+
//#endregion
|
|
3228
|
+
//#region src/read/api/ops/prune.ts
|
|
3229
|
+
/**
|
|
3230
|
+
* Orphan pruning after a part stops being referenced.
|
|
3231
|
+
*
|
|
3232
|
+
* Removing a slide can strand the parts only it used — its notes slide, media, chart parts. This
|
|
3233
|
+
* walks that fringe: a part is dropped when nothing left in the package (or the package root)
|
|
3234
|
+
* still resolves an internal relationship to it, and the parts *it* referenced are then examined
|
|
3235
|
+
* the same way. Shared chrome (masters, layouts, themes, the notes master) is exempt, so a deck
|
|
3236
|
+
* that momentarily has no slides does not lose its design.
|
|
3237
|
+
*/
|
|
3238
|
+
/**
|
|
3239
|
+
* Content types that are shared deck chrome: reachable through the
|
|
3240
|
+
* presentation → master → layout → theme graph, not owned by any one slide.
|
|
3241
|
+
* {@link Presentation.removeSlide} never prunes these as a removed slide's
|
|
3242
|
+
* orphan, even while momentarily unreferenced.
|
|
3243
|
+
*/
|
|
3244
|
+
const SHARED_CHROME_CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
3245
|
+
"application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml",
|
|
3246
|
+
"application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml",
|
|
3247
|
+
"application/vnd.openxmlformats-officedocument.theme+xml",
|
|
3248
|
+
"application/vnd.openxmlformats-officedocument.themeOverride+xml",
|
|
3249
|
+
"application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml",
|
|
3250
|
+
"application/vnd.openxmlformats-officedocument.presentationml.handoutMaster+xml",
|
|
3251
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presProps+xml",
|
|
3252
|
+
"application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml",
|
|
3253
|
+
"application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml",
|
|
3254
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"
|
|
3255
|
+
]);
|
|
3256
|
+
/**
|
|
3257
|
+
* Remove `partName` if it is neither shared chrome nor still referenced by any
|
|
3258
|
+
* remaining part, then recurse into the parts it referenced. The pruning a
|
|
3259
|
+
* removed slide triggers (notes/media/charts the slide alone used).
|
|
3260
|
+
*/
|
|
3261
|
+
function pruneIfOrphan(pres, partName) {
|
|
3262
|
+
const part = pres.opc.part(partName);
|
|
3263
|
+
if (!part || SHARED_CHROME_CONTENT_TYPES.has(part.contentType)) return;
|
|
3264
|
+
if (isReferenced(pres, partName)) return;
|
|
3265
|
+
const rels = pres.opc.relationshipsFor(partName);
|
|
3266
|
+
const childTargets = [...rels].filter((rel) => rel.targetMode !== "External").map((rel) => rels.resolveTarget(rel.id));
|
|
3267
|
+
pres.opc.removePart(relsPartNameFor(partName));
|
|
3268
|
+
pres.opc.removePart(partName);
|
|
3269
|
+
for (const child of childTargets) pruneIfOrphan(pres, child);
|
|
3270
|
+
}
|
|
3271
|
+
/** Whether any remaining part (or the package root) resolves an internal relationship to `partName`. */
|
|
3272
|
+
function isReferenced(pres, partName) {
|
|
3273
|
+
for (const owner of [...pres.opc.parts.keys(), "/"]) {
|
|
3274
|
+
if (owner.endsWith(".rels")) continue;
|
|
3275
|
+
const rels = pres.opc.relationshipsFor(owner);
|
|
3276
|
+
for (const rel of rels) {
|
|
3277
|
+
if (rel.targetMode === "External") continue;
|
|
3278
|
+
if (rels.resolveTarget(rel.id) === partName) return true;
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
return false;
|
|
3282
|
+
}
|
|
3283
|
+
//#endregion
|
|
3284
|
+
//#region src/read/api/ops/rescale-import.ts
|
|
3285
|
+
/**
|
|
3286
|
+
* Rescaling an imported slide's geometry onto the destination canvas.
|
|
3287
|
+
*
|
|
3288
|
+
* The memo of already-rescaled parts is the load-bearing part: a layout or master shared by
|
|
3289
|
+
* several imports from one source deck must be rescaled exactly once, or the second import
|
|
3290
|
+
* scales it again on top of the first. It is owned by the calling `Presentation` and threaded
|
|
3291
|
+
* through here, so it lives as long as the deck does rather than as long as one call.
|
|
3292
|
+
*/
|
|
3293
|
+
const SLIDE_LAYOUT_REL$1 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout";
|
|
3294
|
+
const SLIDE_MASTER_REL$1 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster";
|
|
3295
|
+
/**
|
|
3296
|
+
* Rescale an imported slide's geometry onto this deck's canvas (the `rescale`
|
|
3297
|
+
* option of {@link Presentation.importSlide}). Rewrites every top-level shape/group/
|
|
3298
|
+
* graphicFrame transform and table grid on the slide; in `copy` mode also
|
|
3299
|
+
* rescales the imported layout and master shape trees (resolved via the
|
|
3300
|
+
* slide → layout → master rel chain) so inherited placeholder/background geometry
|
|
3301
|
+
* stays aligned. `preserve`/`restyle` rebind to this deck's own master/layout —
|
|
3302
|
+
* already the destination size — so only the slide is touched. Geometry only:
|
|
3303
|
+
* font sizes and line widths are left as authored.
|
|
3304
|
+
*/
|
|
3305
|
+
function rescaleImportedGeometry(dest, rescaledParts, slidePartName, theme, source, target, mode) {
|
|
3306
|
+
const transform = computeRescale(source, target, mode);
|
|
3307
|
+
rescalePartGeometry(dest, rescaledParts, slidePartName, transform);
|
|
3308
|
+
if (theme === void 0 || theme === "copy") {
|
|
3309
|
+
const layout = resolveSingleRel(dest.opc, slidePartName, SLIDE_LAYOUT_REL$1);
|
|
3310
|
+
const master = layout ? resolveSingleRel(dest.opc, layout, SLIDE_MASTER_REL$1) : null;
|
|
3311
|
+
if (layout) rescalePartGeometry(dest, rescaledParts, layout, transform);
|
|
3312
|
+
if (master) rescalePartGeometry(dest, rescaledParts, master, transform);
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
/**
|
|
3316
|
+
* Rescale one part's `p:spTree` geometry in place. Idempotent per part
|
|
3317
|
+
* (`rescaledParts`), so a layout/master shared across repeated imports from one
|
|
3318
|
+
* source is rescaled exactly once.
|
|
3319
|
+
*/
|
|
3320
|
+
function rescalePartGeometry(dest, rescaledParts, partName, transform) {
|
|
3321
|
+
if (rescaledParts.has(partName)) return;
|
|
3322
|
+
rescaledParts.add(partName);
|
|
3323
|
+
const part = dest.opc.part(partName);
|
|
3324
|
+
const root = part?.dom.documentElement;
|
|
3325
|
+
const cSld = root && firstChild(root, "p:cSld");
|
|
3326
|
+
const spTree = cSld && firstChild(cSld, "p:spTree");
|
|
3327
|
+
if (!part || !spTree) return;
|
|
3328
|
+
rescaleSpTree(spTree, transform);
|
|
3329
|
+
part.markDirty();
|
|
3330
|
+
}
|
|
3331
|
+
//#endregion
|
|
3332
|
+
//#region src/read/api/presentation.ts
|
|
3333
|
+
/**
|
|
3334
|
+
* Read-model entry point: `Presentation` wraps an `OpcPackage` and exposes a
|
|
3335
|
+
* navigable, typed view of the deck (slides → shapes → text), backed by the
|
|
3336
|
+
* live DOM so the same nodes can later be mutated.
|
|
3337
|
+
*/
|
|
3338
|
+
const OFFICE_DOCUMENT_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
|
|
3339
|
+
const SLIDE_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
|
|
3340
|
+
const SLIDE_LAYOUT_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout";
|
|
3341
|
+
const SLIDE_MASTER_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster";
|
|
3342
|
+
const NOTES_SLIDE_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide";
|
|
3343
|
+
const NOTES_MASTER_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster";
|
|
3344
|
+
const IMAGE_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
|
|
3345
|
+
const HYPERLINK_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
|
|
3346
|
+
const CHART_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart";
|
|
3347
|
+
const PACKAGE_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/package";
|
|
3348
|
+
const AUDIO_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio";
|
|
3349
|
+
const VIDEO_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/video";
|
|
3350
|
+
const MS_MEDIA_REL = "http://schemas.microsoft.com/office/2007/relationships/media";
|
|
3351
|
+
const SLIDE_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml";
|
|
3352
|
+
const NOTES_SLIDE_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml";
|
|
3353
|
+
const CHART_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml";
|
|
3354
|
+
const XLSX_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
3355
|
+
/** Content type of the main part in an editable `.pptx` package. */
|
|
3356
|
+
const PRESENTATION_MAIN_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml";
|
|
3357
|
+
/** Content type of the main part in a `.potx` template package — flipped to {@link PRESENTATION_MAIN_CONTENT_TYPE} by {@link Presentation.fromTemplate}. */
|
|
3358
|
+
const PRESENTATION_TEMPLATE_MAIN_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.presentationml.template.main+xml";
|
|
3359
|
+
const textEncoder = new TextEncoder();
|
|
3360
|
+
/**
|
|
3361
|
+
* `p:sldIdLst`'s document-order successors in `CT_Presentation` (ECMA-376):
|
|
3362
|
+
* everything that may legally follow it, so an inserted `p:sldIdLst` lands in the
|
|
3363
|
+
* right position when a template omitted it (zero slides).
|
|
3364
|
+
*/
|
|
3365
|
+
const PRESENTATION_SLD_ID_LST_SUCCESSORS = [
|
|
3366
|
+
"p:sldSz",
|
|
3367
|
+
"p:notesSz",
|
|
3368
|
+
"p:smartTags",
|
|
3369
|
+
"p:embeddedFontLst",
|
|
3370
|
+
"p:custShowLst",
|
|
3371
|
+
"p:photoAlbum",
|
|
3372
|
+
"p:custDataLst",
|
|
3373
|
+
"p:kinsoku",
|
|
3374
|
+
"p:defaultTextStyle",
|
|
3375
|
+
"p:modifyVerifier",
|
|
3376
|
+
"p:extLst"
|
|
3377
|
+
];
|
|
3378
|
+
/** ST_SlideId minimum (ECMA-376): slide ids live in [256, 2147483647]. */
|
|
3379
|
+
const MIN_SLIDE_ID = 256;
|
|
3380
|
+
var Presentation = class Presentation {
|
|
3381
|
+
opc;
|
|
3382
|
+
#presentationPart;
|
|
3383
|
+
/**
|
|
3384
|
+
* Per-source copy registry for {@link importSlide}: source `OpcPackage` →
|
|
3385
|
+
* (source partname → partname allocated in this package). Lets parts shared
|
|
3386
|
+
* across imports from the same source deck (layout, master, theme, media) be
|
|
3387
|
+
* copied once and reused on later calls.
|
|
3388
|
+
*/
|
|
3389
|
+
#importRegistry = /* @__PURE__ */ new Map();
|
|
3390
|
+
/**
|
|
3391
|
+
* Parts whose geometry {@link importSlide}'s `rescale` has already rewritten, so a
|
|
3392
|
+
* layout/master shared across repeated imports from one source is not scaled twice.
|
|
3393
|
+
*/
|
|
3394
|
+
#rescaledParts = /* @__PURE__ */ new Set();
|
|
3395
|
+
constructor(opc) {
|
|
3396
|
+
this.opc = opc;
|
|
3397
|
+
}
|
|
3398
|
+
/** Open a `.pptx` from bytes and wrap it as a navigable `Presentation`. */
|
|
3399
|
+
static async load(input) {
|
|
3400
|
+
return new Presentation(await OpcPackage.load(input));
|
|
3401
|
+
}
|
|
3402
|
+
/** Wrap an already-loaded OPC package (e.g. from the lower-level API). */
|
|
3403
|
+
static fromPackage(opc) {
|
|
3404
|
+
return new Presentation(opc);
|
|
3405
|
+
}
|
|
3406
|
+
/**
|
|
3407
|
+
* Open a PowerPoint template (`.pptx` or `.potx`) and return it as an empty
|
|
3408
|
+
* deck shell ready to author onto: its slide masters, layouts, and theme are
|
|
3409
|
+
* kept **byte-identical**, while any sample slides the template carried are
|
|
3410
|
+
* stripped so only the shared chrome remains.
|
|
3411
|
+
*
|
|
3412
|
+
* Use it to build a fresh deck on a corporate template without rebuilding the
|
|
3413
|
+
* masters in code: discover the bindable layouts with {@link layouts}, then
|
|
3414
|
+
* author slides with a generator sized to match the template and graft them in
|
|
3415
|
+
* with {@link appendSlides} (which enforces an equal slide size). Saving yields
|
|
3416
|
+
* an editable `.pptx` that reuses the template's authored chrome verbatim.
|
|
3417
|
+
*
|
|
3418
|
+
* ```ts
|
|
3419
|
+
* const deck = await Presentation.fromTemplate(templateBytes) // .pptx or .potx
|
|
3420
|
+
* deck.layouts().map(l => l.name) // discover layouts
|
|
3421
|
+
* await deck.appendSlides(pptx, { layout: 'Title and Content' })
|
|
3422
|
+
* const out = await deck.save() // editable .pptx
|
|
3423
|
+
* ```
|
|
3424
|
+
*
|
|
3425
|
+
* A `.potx` package declares its main part with the template content type; by
|
|
3426
|
+
* default that override is flipped to the editable presentation type so the
|
|
3427
|
+
* saved output opens as a normal deck. Pass `keepTemplateContentType: true` to
|
|
3428
|
+
* preserve the template type. A `.pptx` input needs no flip, and a template
|
|
3429
|
+
* that already carries zero slides makes the strip a no-op.
|
|
3430
|
+
*/
|
|
3431
|
+
static async fromTemplate(input, options = {}) {
|
|
3432
|
+
const pres = new Presentation(await OpcPackage.load(input));
|
|
3433
|
+
if (!options.keepTemplateContentType) {
|
|
3434
|
+
const mainPart = pres.presentationPart;
|
|
3435
|
+
if (mainPart.contentType === PRESENTATION_TEMPLATE_MAIN_CONTENT_TYPE) pres.opc.contentTypes.ensureRegistered(mainPart.partName, PRESENTATION_MAIN_CONTENT_TYPE);
|
|
3436
|
+
}
|
|
3437
|
+
while (pres.slides.length > 0) pres.removeSlide(0);
|
|
3438
|
+
return pres;
|
|
3439
|
+
}
|
|
3440
|
+
/** The main presentation part (`/ppt/presentation.xml`), resolved via the package `officeDocument` relationship. */
|
|
3441
|
+
get presentationPart() {
|
|
3442
|
+
if (this.#presentationPart) return this.#presentationPart;
|
|
3443
|
+
const packageRels = this.opc.relationshipsFor("/");
|
|
3444
|
+
const officeDocument = packageRels.byType(OFFICE_DOCUMENT_REL);
|
|
3445
|
+
const officeDocumentRel = officeDocument[0];
|
|
3446
|
+
if (officeDocument.length !== 1 || !officeDocumentRel) throw new PackageReadError("package/office-document-relationship-invalid", `Expected exactly one officeDocument relationship, found ${officeDocument.length}`);
|
|
3447
|
+
const partName = packageRels.resolveTarget(officeDocumentRel.id);
|
|
3448
|
+
const part = this.opc.part(partName);
|
|
3449
|
+
if (!part) throw new PackageReadError("package/relationship-target-missing", `officeDocument relationship targets a missing part: ${partName}`);
|
|
3450
|
+
this.#presentationPart = part;
|
|
3451
|
+
return part;
|
|
3452
|
+
}
|
|
3453
|
+
/** The slides in presentation order (resolved from `p:sldIdLst` + the presentation's relationships). */
|
|
3454
|
+
get slides() {
|
|
3455
|
+
const root = this.presentationPart.dom.documentElement;
|
|
3456
|
+
const sldIdLst = root && firstChild(root, "p:sldIdLst");
|
|
3457
|
+
if (!sldIdLst) return [];
|
|
3458
|
+
const rels = this.opc.relationshipsFor(this.presentationPart.partName);
|
|
3459
|
+
const slides = [];
|
|
3460
|
+
let index = 0;
|
|
3461
|
+
for (const sldId of getElements(sldIdLst, "p:sldId")) {
|
|
3462
|
+
const relId = attr(sldId, "r:id");
|
|
3463
|
+
if (!relId) continue;
|
|
3464
|
+
const partName = rels.resolveTarget(relId);
|
|
3465
|
+
const part = this.opc.part(partName);
|
|
3466
|
+
if (!part) throw new PackageReadError("package/relationship-target-missing", `Slide relationship ${relId} targets a missing part: ${partName}`);
|
|
3467
|
+
slides.push(new Slide(this, part, intValue(attr(sldId, "id")) ?? 0, index++));
|
|
3468
|
+
}
|
|
3469
|
+
return slides;
|
|
3470
|
+
}
|
|
3471
|
+
/** Slide dimensions (`p:sldSz`), or `null` if the presentation declares none. */
|
|
3472
|
+
get slideSize() {
|
|
3473
|
+
const root = this.presentationPart.dom.documentElement;
|
|
3474
|
+
const sldSz = root && firstChild(root, "p:sldSz");
|
|
3475
|
+
if (!sldSz) return null;
|
|
3476
|
+
const widthEmu = intValue(attr(sldSz, "cx"));
|
|
3477
|
+
const heightEmu = intValue(attr(sldSz, "cy"));
|
|
3478
|
+
if (widthEmu === null || heightEmu === null) return null;
|
|
3479
|
+
return {
|
|
3480
|
+
widthEmu,
|
|
3481
|
+
heightEmu,
|
|
3482
|
+
widthIn: emuToInches(widthEmu),
|
|
3483
|
+
heightIn: emuToInches(heightEmu)
|
|
3484
|
+
};
|
|
3485
|
+
}
|
|
3486
|
+
/**
|
|
3487
|
+
* The deck's embedded font families (`p:embeddedFontLst` in `presentation.xml`),
|
|
3488
|
+
* `[]` when it embeds none. Each entry names the typeface and resolves every
|
|
3489
|
+
* embedded face's `r:id` to the absolute partname of its `.fntdata` binary — the
|
|
3490
|
+
* read counterpart to the write-side `pptx.embedFont` / `importSlide({ embedFonts })`
|
|
3491
|
+
* carry. An entry whose `p:font` has no `@typeface`, or a face whose `r:id` is
|
|
3492
|
+
* missing or dangling, is skipped (faithful degradation, no throw). Read-only.
|
|
3493
|
+
*/
|
|
3494
|
+
get embeddedFonts() {
|
|
3495
|
+
const root = this.presentationPart.dom.documentElement;
|
|
3496
|
+
const lst = root && firstChild(root, "p:embeddedFontLst");
|
|
3497
|
+
if (!lst) return [];
|
|
3498
|
+
const rels = this.opc.relationshipsFor(this.presentationPart.partName);
|
|
3499
|
+
const fonts = [];
|
|
3500
|
+
for (const entry of getElements(lst, "p:embeddedFont")) {
|
|
3501
|
+
const font = firstChild(entry, "p:font");
|
|
3502
|
+
const typeface = font && attr(font, "typeface");
|
|
3503
|
+
if (!typeface) continue;
|
|
3504
|
+
const faces = [];
|
|
3505
|
+
for (const slot of EMBEDDED_FONT_SLOTS) {
|
|
3506
|
+
const face = firstChild(entry, `p:${slot}`);
|
|
3507
|
+
const relId = face && attr(face, "r:id");
|
|
3508
|
+
if (!relId || !rels.get(relId)) continue;
|
|
3509
|
+
faces.push({
|
|
3510
|
+
slot,
|
|
3511
|
+
partName: rels.resolveTarget(relId)
|
|
3512
|
+
});
|
|
3513
|
+
}
|
|
3514
|
+
fonts.push({
|
|
3515
|
+
typeface,
|
|
3516
|
+
panose: font ? attr(font, "panose") : null,
|
|
3517
|
+
faces
|
|
3518
|
+
});
|
|
3519
|
+
}
|
|
3520
|
+
return fonts;
|
|
3521
|
+
}
|
|
3522
|
+
/**
|
|
3523
|
+
* The deck-wide **legacy** comment-author registry (`p:cmAuthorLst` in
|
|
3524
|
+
* `ppt/commentAuthors.xml`), `[]` when the deck has no comments. Each slide's
|
|
3525
|
+
* {@link Slide.comments} resolves its `@authorId` against this list. The 2018
|
|
3526
|
+
* modern comment authors (`ppt/authors.xml`) are a separate part, not decoded here.
|
|
3527
|
+
*/
|
|
3528
|
+
get commentAuthors() {
|
|
3529
|
+
return readCommentAuthors(this.opc, this.presentationPart.partName);
|
|
3530
|
+
}
|
|
3531
|
+
/**
|
|
3532
|
+
* The deck-wide **modern** (2018) comment-author registry (`p188:authorLst` in
|
|
3533
|
+
* `ppt/authors.xml`), `[]` when the deck carries no modern comments. Unlike the
|
|
3534
|
+
* legacy {@link commentAuthors}, each entry is keyed by a GUID `id` (plus
|
|
3535
|
+
* `userId`/`providerId`); each slide's {@link Slide.modernComments} resolves its
|
|
3536
|
+
* `@authorId` against this list. Read-only — the writer emits legacy comments.
|
|
3537
|
+
*/
|
|
3538
|
+
get modernCommentAuthors() {
|
|
3539
|
+
return readModernCommentAuthors(this.opc, this.presentationPart.partName);
|
|
3540
|
+
}
|
|
3541
|
+
/**
|
|
3542
|
+
* Which comment schema this deck uses: `'modern'` when it carries any 2018
|
|
3543
|
+
* `modernComment_*` part (read via {@link Slide.modernComments} /
|
|
3544
|
+
* {@link modernCommentAuthors}), `'legacy'` when it carries classic
|
|
3545
|
+
* `commentN.xml` parts (read via {@link Slide.comments} / {@link commentAuthors}),
|
|
3546
|
+
* or `'none'`. The two schemas do not coexist in practice, so this tells a
|
|
3547
|
+
* consumer which accessor to read without probing both.
|
|
3548
|
+
*/
|
|
3549
|
+
get commentSchema() {
|
|
3550
|
+
return commentSchema(this.opc);
|
|
3551
|
+
}
|
|
3552
|
+
/**
|
|
3553
|
+
* The deck's core document properties (`docProps/core.xml`): title, subject,
|
|
3554
|
+
* creator, keywords, revision, and the created/modified/lastPrinted timestamps
|
|
3555
|
+
* (kept as raw W3CDTF strings). `{}` when the deck carries no core-properties
|
|
3556
|
+
* part. The read counterpart of the write-side `pptx.title`/`subject`/`author`
|
|
3557
|
+
* (→ `creator`)/`revision` setters.
|
|
3558
|
+
*/
|
|
3559
|
+
get coreProperties() {
|
|
3560
|
+
return readCoreProperties(this.opc);
|
|
3561
|
+
}
|
|
3562
|
+
/**
|
|
3563
|
+
* The deck's user-defined custom document properties (`docProps/custom.xml`) as
|
|
3564
|
+
* `{ name, value }` pairs, each value typed from its `vt:` element (string,
|
|
3565
|
+
* number, boolean, or a raw filetime string). `[]` when the deck carries no
|
|
3566
|
+
* custom-properties part. The read counterpart of `pptx.setCustomProperty(...)`.
|
|
3567
|
+
*/
|
|
3568
|
+
get customProperties() {
|
|
3569
|
+
return readCustomProperties(this.opc);
|
|
3570
|
+
}
|
|
3571
|
+
/**
|
|
3572
|
+
* The deck-level programmatic tags (`p:custDataLst/p:tags` on `presentation.xml`,
|
|
3573
|
+
* resolved to `ppt/tags/tagN.xml`) as `{ name, val }` string pairs, `[]` when the
|
|
3574
|
+
* deck carries none. These are add-in/host metadata with no visible rendering and
|
|
3575
|
+
* no writer — read-only, preserved byte-for-byte on round-trip. Per-slide tags are
|
|
3576
|
+
* {@link Slide.tags}.
|
|
3577
|
+
*/
|
|
3578
|
+
get tags() {
|
|
3579
|
+
return readTagsForPart(this.opc, this.presentationPart.partName);
|
|
3580
|
+
}
|
|
3581
|
+
/**
|
|
3582
|
+
* Duplicate the slide at `index` and insert the copy at `options.at` (deck
|
|
3583
|
+
* order; `0` = first), defaulting to appending at the end when `at` is omitted
|
|
3584
|
+
* or out of range. Returns the new slide. The new slide part copies the source
|
|
3585
|
+
* bytes verbatim and shares the source's relationship targets (layout, images,
|
|
3586
|
+
* …) by copying its `.rels`; a new presentation→slide relationship and a
|
|
3587
|
+
* `p:sldId` entry are wired up. Marks the presentation part dirty.
|
|
3588
|
+
*
|
|
3589
|
+
* Note: relationships are copied as-is, so a source slide that owns a
|
|
3590
|
+
* one-to-one part (e.g. a notes slide) would end up shared with the clone.
|
|
3591
|
+
*/
|
|
3592
|
+
cloneSlide(index, options = {}) {
|
|
3593
|
+
const source = this.slides[index];
|
|
3594
|
+
if (!source) throw new InvalidOptionError("slide/index-out-of-range", `No slide at index ${index} to clone`);
|
|
3595
|
+
const opc = this.opc;
|
|
3596
|
+
const sourcePart = source.part;
|
|
3597
|
+
const newPartName = opc.reservePartNameLike(sourcePart.partName);
|
|
3598
|
+
const newPart = opc.addPart(newPartName, sourcePart.contentType, sourcePart.bytes);
|
|
3599
|
+
const sourceRels = opc.part(relsPartNameFor(sourcePart.partName));
|
|
3600
|
+
if (sourceRels) opc.addPart(relsPartNameFor(newPartName), sourceRels.contentType, sourceRels.bytes);
|
|
3601
|
+
return this.#insertSlidePart(newPart, options.at);
|
|
3602
|
+
}
|
|
3603
|
+
/**
|
|
3604
|
+
* Remove the slide at `index` (deck order) and return its former partname. The
|
|
3605
|
+
* `p:sldId` entry and the presentation→slide relationship are dropped, the slide
|
|
3606
|
+
* part and its `.rels` are deleted, and any part the slide *privately* owned
|
|
3607
|
+
* (its notes slide, slide-only media, charts/embeddings) that no remaining part
|
|
3608
|
+
* references is pruned too — recursively. Shared deck chrome (layout, master,
|
|
3609
|
+
* theme, …) is never pruned, so the deck stays renderable; removing every slide
|
|
3610
|
+
* leaves a valid master/layout-only package (a template shell).
|
|
3611
|
+
*
|
|
3612
|
+
* Untouched parts stay byte-identical, matching the package fidelity contract.
|
|
3613
|
+
* Throws when there is no slide at `index`.
|
|
3614
|
+
*/
|
|
3615
|
+
removeSlide(index) {
|
|
3616
|
+
const slide = this.slides[index];
|
|
3617
|
+
if (!slide) throw new InvalidOptionError("slide/index-out-of-range", `No slide at index ${index} to remove`);
|
|
3618
|
+
const partName = slide.partName;
|
|
3619
|
+
const slideRels = this.opc.relationshipsFor(partName);
|
|
3620
|
+
const formerTargets = [...slideRels].filter((rel) => rel.targetMode !== "External").map((rel) => slideRels.resolveTarget(rel.id));
|
|
3621
|
+
const presPart = this.presentationPart;
|
|
3622
|
+
const presRels = this.opc.relationshipsFor(presPart.partName);
|
|
3623
|
+
const root = presPart.dom.documentElement;
|
|
3624
|
+
const sldIdLst = root && firstChild(root, "p:sldIdLst");
|
|
3625
|
+
if (sldIdLst) for (const sldId of getElements(sldIdLst, "p:sldId")) {
|
|
3626
|
+
const relId = attr(sldId, "r:id");
|
|
3627
|
+
if (relId && presRels.get(relId) && presRels.resolveTarget(relId) === partName) {
|
|
3628
|
+
sldIdLst.removeChild(sldId);
|
|
3629
|
+
presRels.remove(relId);
|
|
3630
|
+
break;
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
presPart.markDirty();
|
|
3634
|
+
this.opc.removePart(relsPartNameFor(partName));
|
|
3635
|
+
this.opc.removePart(partName);
|
|
3636
|
+
for (const target of formerTargets) pruneIfOrphan(this, target);
|
|
3637
|
+
return partName;
|
|
3638
|
+
}
|
|
3639
|
+
/**
|
|
3640
|
+
* Append a copy of `source.slides[index]` to this presentation and return it.
|
|
3641
|
+
*
|
|
3642
|
+
* Unlike {@link cloneSlide} (same-deck duplicate), this copies a slide across
|
|
3643
|
+
* a package boundary: it brings the connected sub-graph the slide depends on —
|
|
3644
|
+
* its `slideLayout` → `slideMaster` → `theme`, plus any media, charts, and
|
|
3645
|
+
* embeddings — into this package under fresh partnames, rewriting every
|
|
3646
|
+
* partname, relationship id, and content-type registration so the result is a
|
|
3647
|
+
* self-consistent OPC package. Parts of this (target) package that are not
|
|
3648
|
+
* touched stay byte-identical, matching `cloneSlide`'s fidelity contract.
|
|
3649
|
+
*
|
|
3650
|
+
* Only the layout(s) actually used by imported slides are copied; the imported
|
|
3651
|
+
* master's `p:sldLayoutIdLst` is pruned to exactly those, mirroring how
|
|
3652
|
+
* PowerPoint's "Reuse Slides" brings a slide across. Parts shared by repeated
|
|
3653
|
+
* imports from the same source deck are copied once and reused.
|
|
3654
|
+
*
|
|
3655
|
+
* With `{ theme: 'preserve' }` the slide's source theme is instead *flattened*
|
|
3656
|
+
* into the slide XML and the slide is bound to this deck's existing
|
|
3657
|
+
* master/layout; with `{ theme: 'restyle' }` the slide is bound to this deck's
|
|
3658
|
+
* master/layout with its theme references left symbolic, so it re-brands to the
|
|
3659
|
+
* destination palette — see {@link ImportSlideOptions}.
|
|
3660
|
+
*
|
|
3661
|
+
* v1 limitations: by default the source slide size must equal this
|
|
3662
|
+
* presentation's — pass `{ rescale: 'fit' | 'stretch' }` to rescale the imported
|
|
3663
|
+
* geometry onto this deck's canvas instead (geometry only, not fonts/line
|
|
3664
|
+
* widths). Source notes are dropped unless you pass `{ importNotes: true }`.
|
|
3665
|
+
*/
|
|
3666
|
+
importSlide(source, index, options = {}) {
|
|
3667
|
+
const sourceSlide = source.slides[index];
|
|
3668
|
+
if (!sourceSlide) throw new InvalidOptionError("slide/index-out-of-range", `No slide at index ${index} to import`);
|
|
3669
|
+
const target = this.slideSize;
|
|
3670
|
+
const incoming = source.slideSize;
|
|
3671
|
+
const sizesDiffer = !target || !incoming || target.widthEmu !== incoming.widthEmu || target.heightEmu !== incoming.heightEmu;
|
|
3672
|
+
if (sizesDiffer && !options.rescale) {
|
|
3673
|
+
const fmt = (s) => s ? `${s.widthEmu}×${s.heightEmu} EMU` : "unknown";
|
|
3674
|
+
throw new InvalidOptionError("import/slide-size-mismatch", `importSlide requires equal slide sizes (pass { rescale: 'fit' | 'stretch' } to rescale); target is ${fmt(target)}, source is ${fmt(incoming)}`);
|
|
3675
|
+
}
|
|
3676
|
+
if (sizesDiffer && options.rescale && (!target || !incoming)) throw new InvalidOptionError("import/slide-size-unknown", "importSlide rescale requires both decks to declare a slide size (p:sldSz)");
|
|
3677
|
+
const newPartName = options.theme === "preserve" ? this.#importSlidePreserve(source, sourceSlide, options.carryMasterGraphics === true) : options.theme === "restyle" ? this.#importSlideRestyle(source, sourceSlide, options.carryMasterGraphics === true, options.remapLiterals === true) : copyPart(this.#importContext(source.opc), sourceSlide.partName);
|
|
3678
|
+
const newPart = this.opc.part(newPartName);
|
|
3679
|
+
if (!newPart) throw new InternalError("import/part-went-missing", `Imported slide part went missing: ${newPartName}`);
|
|
3680
|
+
if (sizesDiffer && options.rescale && target && incoming) rescaleImportedGeometry(this, this.#rescaledParts, newPartName, options.theme, incoming, target, options.rescale === true ? "fit" : options.rescale);
|
|
3681
|
+
const slide = this.#insertSlidePart(newPart, options.at);
|
|
3682
|
+
if (options.importNotes) carryNotes(this, source, this.#importContext(source.opc), sourceSlide.partName, newPartName);
|
|
3683
|
+
if (options.embedFonts) carryEmbeddedFonts(this, source, this.#importContext(source.opc));
|
|
3684
|
+
return slide;
|
|
3685
|
+
}
|
|
3686
|
+
/**
|
|
3687
|
+
* Graft slide master(s) from another open package into this one and return what
|
|
3688
|
+
* was copied. Unlike {@link importSlide} — which brings a master across only as
|
|
3689
|
+
* the dependency of an imported *slide* and prunes it to the one layout that
|
|
3690
|
+
* slide uses — this copies a master together with its **whole** layout family
|
|
3691
|
+
* and attaches it to no slide: the master and its layouts land in this deck's
|
|
3692
|
+
* layout gallery (PowerPoint's *Insert ▸ New Slide* / *Layout* picker) without
|
|
3693
|
+
* changing any existing slide.
|
|
3694
|
+
*
|
|
3695
|
+
* It is the "ship a brand template's layouts into a generated deck" capability,
|
|
3696
|
+
* kept brand-agnostic here: the caller supplies the source `.pptx`. Each grafted
|
|
3697
|
+
* master is wired into `p:sldMasterIdLst` (so renderers treat it as active) and
|
|
3698
|
+
* its `p:sldLayoutIdLst` is rebuilt to list exactly the copied layouts; the
|
|
3699
|
+
* connected theme/media/tag parts come across under fresh partnames, and parts
|
|
3700
|
+
* shared with earlier imports from the same source are reused (the copy
|
|
3701
|
+
* registry), so a re-call is idempotent. Untouched parts of this package stay
|
|
3702
|
+
* byte-identical, matching {@link importSlide}'s fidelity contract.
|
|
3703
|
+
*
|
|
3704
|
+
* `options.masters` / `options.layouts` narrow what is grafted; by default every
|
|
3705
|
+
* master and every layout comes across. The source and destination slide sizes
|
|
3706
|
+
* must match unless `options.requireEqualSize` is `false` (see
|
|
3707
|
+
* {@link ImportSlideMastersOptions}).
|
|
3708
|
+
*
|
|
3709
|
+
* Presentation-level parts are carried only on request: embedded fonts via
|
|
3710
|
+
* `options.embedFonts` and table styles via `options.tableStyles`. By default a
|
|
3711
|
+
* grafted master is appended after the deck's existing masters; `options.primary`
|
|
3712
|
+
* moves the grafted masters to the front so the deck presents as their theme (see
|
|
3713
|
+
* {@link ImportSlideMastersOptions}). The v1 limitation mirroring
|
|
3714
|
+
* {@link importSlide} is that geometry is not rescaled.
|
|
3715
|
+
*/
|
|
3716
|
+
importSlideMasters(source, options = {}) {
|
|
3717
|
+
if (options.requireEqualSize !== false) {
|
|
3718
|
+
const target = this.slideSize;
|
|
3719
|
+
const incoming = source.slideSize;
|
|
3720
|
+
if (!target || !incoming || target.widthEmu !== incoming.widthEmu || target.heightEmu !== incoming.heightEmu) {
|
|
3721
|
+
const fmt = (s) => s ? `${s.widthEmu}×${s.heightEmu} EMU` : "unknown";
|
|
3722
|
+
throw new InvalidOptionError("import/slide-size-mismatch", `importSlideMasters requires equal slide sizes (pass { requireEqualSize: false } to override); target is ${fmt(target)}, source is ${fmt(incoming)}`);
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
const pickMaster = options.masters ?? (() => true);
|
|
3726
|
+
const pickLayout = options.layouts ?? (() => true);
|
|
3727
|
+
const ctx = this.#importContext(source.opc);
|
|
3728
|
+
const imported = [];
|
|
3729
|
+
slideMasterPartNames(source).forEach((masterPartName, masterIndex) => {
|
|
3730
|
+
if (!pickMaster(cSldName(source.opc.part(masterPartName)), masterIndex)) return;
|
|
3731
|
+
const newMasterPartName = copyPart(ctx, masterPartName);
|
|
3732
|
+
const layoutPartNames = [];
|
|
3733
|
+
layoutPartNamesOf(source, masterPartName).forEach((layoutPartName, layoutIndex) => {
|
|
3734
|
+
if (!pickLayout(cSldName(source.opc.part(layoutPartName)), layoutIndex)) return;
|
|
3735
|
+
layoutPartNames.push(copyPart(ctx, layoutPartName));
|
|
3736
|
+
});
|
|
3737
|
+
imported.push({
|
|
3738
|
+
partName: newMasterPartName,
|
|
3739
|
+
layoutPartNames
|
|
3740
|
+
});
|
|
3741
|
+
});
|
|
3742
|
+
if (options.embedFonts) carryEmbeddedFonts(this, source, this.#importContext(source.opc));
|
|
3743
|
+
if (options.tableStyles) carryTableStyles(this, source.opc);
|
|
3744
|
+
if (options.primary) promoteMasters(this, imported.map((m) => m.partName));
|
|
3745
|
+
return imported;
|
|
3746
|
+
}
|
|
3747
|
+
/**
|
|
3748
|
+
* The deck's slide masters, in `p:sldMasterIdLst` order, as modeled
|
|
3749
|
+
* {@link SlideMaster}s — the typed read model over the shared chrome (each
|
|
3750
|
+
* master's colour map, theme, placeholders, and the layouts built on it). Walk
|
|
3751
|
+
* `pres.masters()[i].layouts` for the rich layout model, or use {@link layouts}
|
|
3752
|
+
* for the flat {@link LayoutHandle} gallery {@link appendSlides} binds to.
|
|
3753
|
+
* Read-only: it copies nothing and leaves the package byte-identical.
|
|
3754
|
+
*/
|
|
3755
|
+
masters() {
|
|
3756
|
+
return slideMasterPartNames(this).map((partName) => this.opc.part(partName)).filter((part) => part !== void 0).map((part) => new SlideMaster(this.opc, part));
|
|
3757
|
+
}
|
|
3758
|
+
/**
|
|
3759
|
+
* The deck's slide layouts, in master then layout order — the gallery a new
|
|
3760
|
+
* slide can bind to. Each {@link LayoutHandle} addresses one layout for
|
|
3761
|
+
* {@link appendSlides}; the `name` is its `p:cSld@name`. Read-only enumeration:
|
|
3762
|
+
* it copies nothing and leaves the package byte-identical.
|
|
3763
|
+
*/
|
|
3764
|
+
layouts() {
|
|
3765
|
+
const out = [];
|
|
3766
|
+
slideMasterPartNames(this).forEach((masterPartName, masterIndex) => {
|
|
3767
|
+
layoutPartNamesOf(this, masterPartName).forEach((layoutPartName, layoutIndex) => {
|
|
3768
|
+
out.push({
|
|
3769
|
+
partName: layoutPartName,
|
|
3770
|
+
name: cSldName(this.opc.part(layoutPartName)),
|
|
3771
|
+
masterPartName,
|
|
3772
|
+
masterIndex,
|
|
3773
|
+
layoutIndex
|
|
3774
|
+
});
|
|
3775
|
+
});
|
|
3776
|
+
});
|
|
3777
|
+
return out;
|
|
3778
|
+
}
|
|
3779
|
+
/**
|
|
3780
|
+
* Append generator-produced slides onto this deck, binding each to an existing
|
|
3781
|
+
* layout, and return the new {@link Slide}s. This is the hybrid
|
|
3782
|
+
* "generate-onto-existing" path: the deck's masters, layouts, theme — and every
|
|
3783
|
+
* other untouched part — stay **byte-identical** (only `presentation.xml`, its
|
|
3784
|
+
* `.rels`, `[Content_Types].xml`, and the freshly-added slide/media parts
|
|
3785
|
+
* change), because the existing chrome is never regenerated.
|
|
3786
|
+
*
|
|
3787
|
+
* `source` is any slide producer (a `TsPptx` instance); its authored slides
|
|
3788
|
+
* are serialized via {@link SlideSource.extractSlides} and spliced in under
|
|
3789
|
+
* fresh partnames, with each slide's `slideLayout` relationship pointed at the
|
|
3790
|
+
* layout named by `options.layout` and its image/hyperlink relationships rebuilt
|
|
3791
|
+
* (preserving the body's relationship ids). Insert position follows
|
|
3792
|
+
* `options.at` (see {@link AppendSlidesOptions}).
|
|
3793
|
+
*
|
|
3794
|
+
* Charts and internal slide-to-slide hyperlinks are carried across: chart parts
|
|
3795
|
+
* (chart XML + `.rels` + embedded workbook) are injected under fresh names, and a
|
|
3796
|
+
* `slide:N` link is repointed at the Nth appended slide's new partname.
|
|
3797
|
+
*
|
|
3798
|
+
* The generator's presentation-level embedded fonts (`pptx.embedFont`) are also
|
|
3799
|
+
* carried into this deck and merged into its `p:embeddedFontLst`, de-duped by
|
|
3800
|
+
* typeface + face slot — so author-side embedded fonts survive the append onto a
|
|
3801
|
+
* template that may itself already embed fonts.
|
|
3802
|
+
*
|
|
3803
|
+
* Embedded audio/video is carried too: the media part backs the ECMA audio/video rel
|
|
3804
|
+
* and the MS-2007 `media` rel sharing one Target, plus a separate preview image part,
|
|
3805
|
+
* with the media content type registered as a Default extension entry (what PowerPoint
|
|
3806
|
+
* authors). Online (external-link) video rides as two External rels over the link.
|
|
3807
|
+
*
|
|
3808
|
+
* Speaker notes ride across too: a slide authored with `addNotes` gets a `notesSlide`
|
|
3809
|
+
* part wired back to it, with its hyperlink rels preserved. A notes slide must bind to
|
|
3810
|
+
* a notes master, and a template commonly has none, so the generator's is installed
|
|
3811
|
+
* (with the theme its `.rels` needs) — but only when this deck has none of its own, so
|
|
3812
|
+
* an existing notes master and its styling always win.
|
|
3813
|
+
*
|
|
3814
|
+
* Limitations:
|
|
3815
|
+
* - An internal link to a source slide outside the appended batch throws (its
|
|
3816
|
+
* target has no counterpart in the destination).
|
|
3817
|
+
* - Appended slides are concrete absolute-positioned content with no placeholder
|
|
3818
|
+
* inheritance from the bound layout; the binding governs theme/`clrMap`
|
|
3819
|
+
* resolution and the "based on" link, not placeholder geometry. Author with
|
|
3820
|
+
* concrete colours — any `schemeClr` re-resolves against the destination theme.
|
|
3821
|
+
* - Source and destination slide sizes must match (no geometry rescale).
|
|
3822
|
+
*/
|
|
3823
|
+
async appendSlides(source, options) {
|
|
3824
|
+
const gallery = this.layouts();
|
|
3825
|
+
let target;
|
|
3826
|
+
if (typeof options.layout === "string") {
|
|
3827
|
+
const matches = gallery.filter((l) => l.name === options.layout);
|
|
3828
|
+
if (matches.length > 1) throw new InvalidOptionError("layout/ambiguous-name", `appendSlides: layout name ${JSON.stringify(options.layout)} is ambiguous (${matches.length} layouts share it); pass a LayoutHandle from layouts() instead`);
|
|
3829
|
+
const [only] = matches;
|
|
3830
|
+
if (!only) {
|
|
3831
|
+
const names = gallery.map((l) => JSON.stringify(l.name)).join(", ");
|
|
3832
|
+
throw new InvalidOptionError("layout/not-found", `appendSlides: no layout named ${JSON.stringify(options.layout)}; available: ${names || "(none)"}`);
|
|
3833
|
+
}
|
|
3834
|
+
target = only;
|
|
3835
|
+
} else {
|
|
3836
|
+
const handle = options.layout;
|
|
3837
|
+
if (!gallery.some((l) => l.partName === handle.partName)) throw new InvalidOptionError("layout/foreign-handle", `appendSlides: layout ${handle.partName} does not belong to this presentation`);
|
|
3838
|
+
target = handle;
|
|
3839
|
+
}
|
|
3840
|
+
const extracted = await source.extractSlides({ onMediaError: options.onMediaError });
|
|
3841
|
+
const size = this.slideSize;
|
|
3842
|
+
if (!size || size.widthEmu !== extracted.widthEmu || size.heightEmu !== extracted.heightEmu) {
|
|
3843
|
+
const fmt = (w, h) => `${w}×${h} EMU`;
|
|
3844
|
+
throw new InvalidOptionError("import/slide-size-mismatch", `appendSlides requires equal slide sizes; target is ${size ? fmt(size.widthEmu, size.heightEmu) : "unknown"}, source is ${fmt(extracted.widthEmu, extracted.heightEmu)}`);
|
|
3845
|
+
}
|
|
3846
|
+
const slideTemplate = this.slides[0]?.partName ?? "/ppt/slides/slide1.xml";
|
|
3847
|
+
const placed = extracted.slides.map((slide) => {
|
|
3848
|
+
const partName = this.opc.reservePartNameLike(slideTemplate);
|
|
3849
|
+
return {
|
|
3850
|
+
slide,
|
|
3851
|
+
part: this.opc.addPart(partName, SLIDE_CONTENT_TYPE, textEncoder.encode(slide.xml)),
|
|
3852
|
+
partName
|
|
3853
|
+
};
|
|
3854
|
+
});
|
|
3855
|
+
const partBySourceNumber = new Map(placed.map((p, i) => [i + 1, p.partName]));
|
|
3856
|
+
const added = [];
|
|
3857
|
+
placed.forEach(({ slide, part, partName }, i) => {
|
|
3858
|
+
const rels = this.opc.relationshipsFor(partName);
|
|
3859
|
+
for (const m of slide.media) {
|
|
3860
|
+
const mediaPartName = this.opc.reserveMediaPartName(m.extn);
|
|
3861
|
+
this.opc.addPart(mediaPartName, m.contentType, m.bytes);
|
|
3862
|
+
rels.addWithId(`rId${m.rId}`, IMAGE_REL, relativePartName(partName, mediaPartName));
|
|
3863
|
+
}
|
|
3864
|
+
for (const av of slide.avMedia) {
|
|
3865
|
+
const mediaPartName = this.opc.reserveMediaPartName(av.mediaExtn, "media");
|
|
3866
|
+
this.opc.contentTypes.ensureDefault(av.mediaExtn, av.mediaContentType);
|
|
3867
|
+
this.opc.addPart(mediaPartName, av.mediaContentType, av.mediaBytes);
|
|
3868
|
+
const mediaTarget = relativePartName(partName, mediaPartName);
|
|
3869
|
+
rels.addWithId(`rId${av.mediaRid}`, av.mtype === "audio" ? AUDIO_REL : VIDEO_REL, mediaTarget);
|
|
3870
|
+
rels.addWithId(`rId${av.msMediaRid}`, MS_MEDIA_REL, mediaTarget);
|
|
3871
|
+
const previewPartName = this.opc.reserveMediaPartName(av.previewExtn);
|
|
3872
|
+
this.opc.contentTypes.ensureDefault(av.previewExtn, av.previewContentType);
|
|
3873
|
+
this.opc.addPart(previewPartName, av.previewContentType, av.previewBytes);
|
|
3874
|
+
rels.addWithId(`rId${av.previewRid}`, IMAGE_REL, relativePartName(partName, previewPartName));
|
|
3875
|
+
}
|
|
3876
|
+
for (const ov of slide.onlineMedia) {
|
|
3877
|
+
rels.addWithId(`rId${ov.mediaRid}`, VIDEO_REL, ov.link, "External");
|
|
3878
|
+
rels.addWithId(`rId${ov.msMediaRid}`, MS_MEDIA_REL, ov.link, "External");
|
|
3879
|
+
}
|
|
3880
|
+
for (const h of slide.hyperlinks) rels.addWithId(`rId${h.rId}`, HYPERLINK_REL, h.target, "External");
|
|
3881
|
+
if (slide.notes) {
|
|
3882
|
+
const notesPartName = this.opc.reservePartNameLike("/ppt/notesSlides/notesSlide1.xml");
|
|
3883
|
+
this.opc.addPart(notesPartName, NOTES_SLIDE_CONTENT_TYPE, textEncoder.encode(slide.notes.xml));
|
|
3884
|
+
rels.add(NOTES_SLIDE_REL, relativePartName(partName, notesPartName));
|
|
3885
|
+
const notesRels = this.opc.relationshipsFor(notesPartName);
|
|
3886
|
+
const notesMasterPartName = extracted.notesMaster ? ensureNotesMasterFromXml(this, extracted.notesMaster) : null;
|
|
3887
|
+
if (notesMasterPartName) notesRels.addWithId("rId1", NOTES_MASTER_REL, relativePartName(notesPartName, notesMasterPartName));
|
|
3888
|
+
notesRels.addWithId("rId2", SLIDE_REL, relativePartName(notesPartName, partName));
|
|
3889
|
+
for (const h of slide.notes.hyperlinks) notesRels.addWithId(`rId${h.rId}`, HYPERLINK_REL, h.target, "External");
|
|
3890
|
+
}
|
|
3891
|
+
for (const c of slide.charts) {
|
|
3892
|
+
const chartPartName = this.opc.reservePartNameLike("/ppt/charts/chart1.xml");
|
|
3893
|
+
this.opc.addPart(chartPartName, CHART_CONTENT_TYPE, textEncoder.encode(c.chartXml));
|
|
3894
|
+
const embeddingPartName = this.opc.reservePartNameLike("/ppt/embeddings/Microsoft_Excel_Worksheet1.xlsx");
|
|
3895
|
+
this.opc.contentTypes.ensureDefault("xlsx", XLSX_CONTENT_TYPE);
|
|
3896
|
+
this.opc.addPart(embeddingPartName, XLSX_CONTENT_TYPE, c.embeddingBytes);
|
|
3897
|
+
this.opc.relationshipsFor(chartPartName).addWithId("rId1", PACKAGE_REL, relativePartName(chartPartName, embeddingPartName));
|
|
3898
|
+
rels.addWithId(`rId${c.rId}`, CHART_REL, relativePartName(partName, chartPartName));
|
|
3899
|
+
}
|
|
3900
|
+
for (const link of slide.slideLinks) {
|
|
3901
|
+
const targetPartName = partBySourceNumber.get(link.sourceSlideNumber);
|
|
3902
|
+
if (!targetPartName) throw new InvalidOptionError("import/unresolved-slide-link", `appendSlides: slide ${i} links to source slide ${link.sourceSlideNumber}, which is not among the appended slides`);
|
|
3903
|
+
rels.addWithId(`rId${link.rId}`, SLIDE_REL, relativePartName(partName, targetPartName));
|
|
3904
|
+
}
|
|
3905
|
+
rels.add(SLIDE_LAYOUT_REL, relativePartName(partName, target.partName));
|
|
3906
|
+
const at = options.at === void 0 ? void 0 : options.at + i;
|
|
3907
|
+
added.push(this.#insertSlidePart(part, at));
|
|
3908
|
+
});
|
|
3909
|
+
carryGeneratedEmbeddedFonts(this, extracted.embeddedFonts || []);
|
|
3910
|
+
return added;
|
|
3911
|
+
}
|
|
3912
|
+
/**
|
|
3913
|
+
* Copy one shape — an autoshape, picture, table/chart graphic frame, connector,
|
|
3914
|
+
* or group — from `source.shapes[shapeIndex]` onto `target`, returning the new
|
|
3915
|
+
* {@link Shape}. `target` must be a slide of *this* presentation; `source` may
|
|
3916
|
+
* belong to any open presentation.
|
|
3917
|
+
*
|
|
3918
|
+
* The lifted subtree is copied self-consistently: every media/chart/embedding it
|
|
3919
|
+
* depends on is dragged into this package (deduped against earlier imports from
|
|
3920
|
+
* the same source), its `r:embed`/`r:id`/… are rewritten to fresh host-slide
|
|
3921
|
+
* relationships, and its drawing ids (including a group's children) are reassigned
|
|
3922
|
+
* so they cannot collide with the host. With `theme: 'preserve'` (default) the
|
|
3923
|
+
* shape's theme references are baked to literals against the *source* theme so it
|
|
3924
|
+
* renders the same on a foreign host; `restyle` leaves them symbolic to re-brand;
|
|
3925
|
+
* `copy` brings the XML across untouched — see {@link ImportShapeOptions}.
|
|
3926
|
+
*
|
|
3927
|
+
* Differing slide sizes need `{ rescale }` (see {@link ImportShapeOptions.rescale});
|
|
3928
|
+
* a lifted `preserve` placeholder is baked self-contained and demoted to a plain
|
|
3929
|
+
* shape (see {@link ImportShapeOptions.theme}), so it neither re-inherits from nor
|
|
3930
|
+
* collides with the host. A shape's build animation lives in the slide-scoped
|
|
3931
|
+
* `p:timing`, not in its subtree, so the shape lands static unless
|
|
3932
|
+
* `{ carryAnimation: true }` opts in (see {@link ImportShapeOptions.carryAnimation}).
|
|
3933
|
+
*/
|
|
3934
|
+
importShape(target, source, shapeIndex, options = {}) {
|
|
3935
|
+
const [shape] = this.importShapes(target, source, [shapeIndex], options);
|
|
3936
|
+
if (!shape) throw new InvalidOptionError("shape/index-out-of-range", `importShape: source slide has no shape at index ${shapeIndex}`);
|
|
3937
|
+
return shape;
|
|
3938
|
+
}
|
|
3939
|
+
/**
|
|
3940
|
+
* Batch form of {@link importShape}: copy several shapes from one source slide
|
|
3941
|
+
* onto `target` in the given order. Media/chart/embedding parts shared by the
|
|
3942
|
+
* lifted shapes (and by earlier imports from the same source deck) are copied
|
|
3943
|
+
* once via the copy registry, and shared images resolve to a single host-slide
|
|
3944
|
+
* relationship. Returns the new {@link Shape}s in `shapeIndices` order.
|
|
3945
|
+
*/
|
|
3946
|
+
importShapes(target, source, shapeIndices, options = {}) {
|
|
3947
|
+
if (target.presentation !== this) throw new InvalidOptionError("slide/foreign-target", "importShape: target slide must belong to this presentation");
|
|
3948
|
+
const targetSize = this.slideSize;
|
|
3949
|
+
const sourceSize = source.presentation.slideSize;
|
|
3950
|
+
const sizesMatch = !!targetSize && !!sourceSize && targetSize.widthEmu === sourceSize.widthEmu && targetSize.heightEmu === sourceSize.heightEmu;
|
|
3951
|
+
let transform = null;
|
|
3952
|
+
if (!sizesMatch) {
|
|
3953
|
+
if (!options.rescale || !targetSize || !sourceSize) {
|
|
3954
|
+
const fmt = (s) => s ? `${s.widthEmu}×${s.heightEmu} EMU` : "unknown";
|
|
3955
|
+
throw new InvalidOptionError("import/slide-size-mismatch", `importShape requires equal slide sizes (or { rescale }); target is ${fmt(targetSize)}, source is ${fmt(sourceSize)}`);
|
|
3956
|
+
}
|
|
3957
|
+
transform = computeRescale(sourceSize, targetSize, options.rescale === "stretch" ? "stretch" : "fit");
|
|
3958
|
+
}
|
|
3959
|
+
const sourceShapes = source.shapes;
|
|
3960
|
+
const sourceElements = shapeIndices.map((i) => {
|
|
3961
|
+
const shape = sourceShapes[i];
|
|
3962
|
+
if (!shape) throw new InvalidOptionError("shape/index-out-of-range", `No shape at index ${i} on the source slide (it has ${sourceShapes.length})`);
|
|
3963
|
+
return shape.element_;
|
|
3964
|
+
});
|
|
3965
|
+
const spTree = target.shapeTree();
|
|
3966
|
+
if (!spTree) throw new PackageReadError("slide/no-shape-tree", `importShape: target slide ${target.partName} has no shape tree`);
|
|
3967
|
+
const targetDoc = spTree.ownerDocument;
|
|
3968
|
+
if (!targetDoc) throw new InternalError("oxml/node-has-no-document", "importShape: target slide DOM has no owner document");
|
|
3969
|
+
const theme = options.theme ?? "preserve";
|
|
3970
|
+
const sourceOpc = source.presentation.opc;
|
|
3971
|
+
const sourceRels = sourceOpc.relationshipsFor(source.partName);
|
|
3972
|
+
const targetRels = this.opc.relationshipsFor(target.partName);
|
|
3973
|
+
const relIdMap = /* @__PURE__ */ new Map();
|
|
3974
|
+
const ctx = theme === "preserve" ? this.#sourceFlattenContext(sourceOpc, source.partName) : null;
|
|
3975
|
+
const importCtx = this.#importContext(sourceOpc);
|
|
3976
|
+
const extLst = firstChild(spTree, "p:extLst");
|
|
3977
|
+
const anchor = options.at == null ? extLst : nthShapeChild(spTree, options.at) ?? extLst;
|
|
3978
|
+
const result = [];
|
|
3979
|
+
for (const shapeEl of sourceElements) {
|
|
3980
|
+
const imported = targetDoc.importNode(shapeEl, true);
|
|
3981
|
+
this.#rewriteCarriedRels(imported, importCtx, sourceRels, target.partName, targetRels, relIdMap);
|
|
3982
|
+
if (ctx) {
|
|
3983
|
+
const holder = createElement(targetDoc, "p:spTree");
|
|
3984
|
+
holder.appendChild(imported);
|
|
3985
|
+
flattenShape(holder, ctx);
|
|
3986
|
+
}
|
|
3987
|
+
if (transform) {
|
|
3988
|
+
const holder = createElement(targetDoc, "p:spTree");
|
|
3989
|
+
holder.appendChild(imported);
|
|
3990
|
+
rescaleSpTree(holder, transform);
|
|
3991
|
+
}
|
|
3992
|
+
let nextId = target.nextShapeId();
|
|
3993
|
+
const spidMap = /* @__PURE__ */ new Map();
|
|
3994
|
+
for (const cNvPr of imported.getElementsByTagNameNS(OOXML_NS.p, "cNvPr")) {
|
|
3995
|
+
const oldId = intValue(attr(cNvPr, "id"));
|
|
3996
|
+
if (oldId !== null) spidMap.set(oldId, nextId);
|
|
3997
|
+
setAttr(cNvPr, "id", String(nextId++));
|
|
3998
|
+
}
|
|
3999
|
+
spTree.insertBefore(imported, anchor);
|
|
4000
|
+
if (options.carryAnimation) {
|
|
4001
|
+
const sourceRoot = source.part.dom.documentElement;
|
|
4002
|
+
const targetRoot = target.part.dom.documentElement;
|
|
4003
|
+
if (sourceRoot && targetRoot) carryShapeAnimations(sourceRoot, targetRoot, spidMap);
|
|
4004
|
+
}
|
|
4005
|
+
const shape = wrapShapeElement(imported, target);
|
|
4006
|
+
if (!shape) throw new UnsupportedFeatureError("shape/element-unsupported", `importShape: unsupported shape element <${imported.localName}>`);
|
|
4007
|
+
if (options.left != null) shape.left = options.left;
|
|
4008
|
+
if (options.top != null) shape.top = options.top;
|
|
4009
|
+
if (options.width != null) shape.width = options.width;
|
|
4010
|
+
if (options.height != null) shape.height = options.height;
|
|
4011
|
+
result.push(shape);
|
|
4012
|
+
}
|
|
4013
|
+
target.part.markDirty();
|
|
4014
|
+
return result;
|
|
4015
|
+
}
|
|
4016
|
+
/**
|
|
4017
|
+
* Import a slide in `preserve` mode: rebind it to this deck's master/layout
|
|
4018
|
+
* (see {@link #importSlideRebind}), then flatten its source theme into the slide
|
|
4019
|
+
* XML (scheme colours + style-matrix fills baked to literals). Returns the new
|
|
4020
|
+
* partname.
|
|
4021
|
+
*
|
|
4022
|
+
* The flatten context is gathered from the *source* subgraph, so it can be read
|
|
4023
|
+
* before or after the rebind; the rebind injects any carried decorations before
|
|
4024
|
+
* we flatten, so a single sweep resolves the theme references on the slide's own
|
|
4025
|
+
* content and on the carried decorations together.
|
|
4026
|
+
*/
|
|
4027
|
+
#importSlidePreserve(source, sourceSlide, carryGraphics) {
|
|
4028
|
+
const ctx = this.#sourceFlattenContext(source.opc, sourceSlide.partName);
|
|
4029
|
+
const { newPartName, slideRoot, newPart } = this.#importSlideRebind(source, sourceSlide, carryGraphics);
|
|
4030
|
+
flattenSlide(slideRoot, ctx);
|
|
4031
|
+
newPart.markDirty();
|
|
4032
|
+
return newPartName;
|
|
4033
|
+
}
|
|
4034
|
+
/**
|
|
4035
|
+
* Import a slide in `restyle` mode: rebind it to this deck's master/layout (see
|
|
4036
|
+
* {@link #importSlideRebind}) and then {@link restyleSlide} it — drop its colour
|
|
4037
|
+
* map override but bake *nothing*, so its symbolic theme references re-resolve
|
|
4038
|
+
* against the destination theme and the slide re-brands. Returns the new
|
|
4039
|
+
* partname.
|
|
4040
|
+
*
|
|
4041
|
+
* The deliberate inverse of `preserve`: no flatten, no inherited-background
|
|
4042
|
+
* bake, no placeholder colour/size/geometry bake — every one of those would pin
|
|
4043
|
+
* the slide to its source look, the opposite of re-branding. Carried
|
|
4044
|
+
* decorations are left symbolic too, so they re-brand along with the slide.
|
|
4045
|
+
*
|
|
4046
|
+
* With `remapLiterals` it additionally force-remaps the slide's source-theme
|
|
4047
|
+
* literal colours back to symbolic scheme colours and copies any referenced
|
|
4048
|
+
* source table style into this deck — the two things plain `restyle` cannot
|
|
4049
|
+
* re-brand (see {@link ImportSlideOptions.remapLiterals}).
|
|
4050
|
+
*/
|
|
4051
|
+
#importSlideRestyle(source, sourceSlide, carryGraphics, remapLiterals) {
|
|
4052
|
+
const { newPartName, slideRoot, newPart } = this.#importSlideRebind(source, sourceSlide, carryGraphics);
|
|
4053
|
+
restyleSlide(slideRoot);
|
|
4054
|
+
if (remapLiterals) {
|
|
4055
|
+
const parts = resolveSlideThemeParts(source.opc, sourceSlide.partName);
|
|
4056
|
+
remapLiteralColors(slideRoot, {
|
|
4057
|
+
clrMap: parts.clrMap,
|
|
4058
|
+
clrScheme: parts.clrScheme
|
|
4059
|
+
});
|
|
4060
|
+
copySourceTableStyles(this, source.opc, slideRoot);
|
|
4061
|
+
}
|
|
4062
|
+
newPart.markDirty();
|
|
4063
|
+
return newPartName;
|
|
4064
|
+
}
|
|
4065
|
+
/**
|
|
4066
|
+
* The rebind shared by `preserve` and `restyle`: copy the slide bytes into a
|
|
4067
|
+
* fresh part, rebuild its relationships (drop notes, repoint the `slideLayout`
|
|
4068
|
+
* rel at this deck's existing layout, copy every other internal target —
|
|
4069
|
+
* media/charts — and pass externals through), and optionally bake the source
|
|
4070
|
+
* master/layout decorations onto the slide. Returns the new part, its name, and
|
|
4071
|
+
* its live root element for the caller's mode-specific pass (flatten vs restyle).
|
|
4072
|
+
*
|
|
4073
|
+
* This carries *no* theme baking of its own — not even the inherited background.
|
|
4074
|
+
* `preserve` adds that via {@link flattenSlide}'s context; `restyle` must not,
|
|
4075
|
+
* so the background stays symbolic and re-brands.
|
|
4076
|
+
*/
|
|
4077
|
+
#importSlideRebind(source, sourceSlide, carryGraphics) {
|
|
4078
|
+
const destLayout = this.#destinationLayoutPartName();
|
|
4079
|
+
const sourcePart = source.opc.part(sourceSlide.partName);
|
|
4080
|
+
if (!sourcePart) throw new PackageReadError("package/part-missing", `importSlide: source package has no part ${sourceSlide.partName}`);
|
|
4081
|
+
const newPartName = this.opc.reservePartNameLike(sourceSlide.partName);
|
|
4082
|
+
const newPart = this.opc.addPart(newPartName, sourcePart.contentType, sourcePart.bytes);
|
|
4083
|
+
const slideRoot = newPart.dom.documentElement;
|
|
4084
|
+
if (!slideRoot) throw new PackageReadError("package/part-has-no-root", `Imported slide ${newPartName} has no root element`);
|
|
4085
|
+
const ctx = this.#importContext(source.opc);
|
|
4086
|
+
const sourceRels = source.opc.relationshipsFor(sourceSlide.partName);
|
|
4087
|
+
const targetRels = this.opc.relationshipsFor(newPartName);
|
|
4088
|
+
for (const rel of sourceRels) {
|
|
4089
|
+
if (rel.type === NOTES_SLIDE_REL) continue;
|
|
4090
|
+
if (rel.type === SLIDE_LAYOUT_REL) {
|
|
4091
|
+
targetRels.addWithId(rel.id, SLIDE_LAYOUT_REL, relativePartName(newPartName, destLayout));
|
|
4092
|
+
continue;
|
|
4093
|
+
}
|
|
4094
|
+
if (rel.targetMode === "External") {
|
|
4095
|
+
targetRels.addWithId(rel.id, rel.type, rel.target, "External");
|
|
4096
|
+
continue;
|
|
4097
|
+
}
|
|
4098
|
+
const newTarget = copyPart(ctx, sourceRels.resolveTarget(rel.id));
|
|
4099
|
+
targetRels.addWithId(rel.id, rel.type, relativePartName(newPartName, newTarget));
|
|
4100
|
+
}
|
|
4101
|
+
if (carryGraphics) this.#carryMasterGraphics(ctx, slideRoot, newPartName, sourceSlide.partName);
|
|
4102
|
+
return {
|
|
4103
|
+
newPartName,
|
|
4104
|
+
slideRoot,
|
|
4105
|
+
newPart
|
|
4106
|
+
};
|
|
4107
|
+
}
|
|
4108
|
+
/**
|
|
4109
|
+
* Bake the source `slideLayout`/`slideMaster` shape-tree decorations onto the
|
|
4110
|
+
* imported slide (the `carryMasterGraphics` path). Every shape on those trees
|
|
4111
|
+
* *except* placeholders is deep-copied into the slide's `p:spTree` ahead of its
|
|
4112
|
+
* own content — master decorations first, then layout, then the slide's shapes —
|
|
4113
|
+
* so document (z-)order keeps the master furthest back. Each decoration's media
|
|
4114
|
+
* and other relationship targets are copied into this package and its
|
|
4115
|
+
* `r:embed`/`r:id`/… references rewritten to fresh slide-local ids. The injected
|
|
4116
|
+
* shapes are left for the caller's {@link flattenSlide} pass to resolve any
|
|
4117
|
+
* theme references they carry.
|
|
4118
|
+
*/
|
|
4119
|
+
#carryMasterGraphics(ctx, slideRoot, newPartName, slidePartName) {
|
|
4120
|
+
const sourceOpc = ctx.source;
|
|
4121
|
+
const layoutPartName = resolveSingleRel(sourceOpc, slidePartName, SLIDE_LAYOUT_REL);
|
|
4122
|
+
const masterPartName = layoutPartName ? resolveSingleRel(sourceOpc, layoutPartName, SLIDE_MASTER_REL) : null;
|
|
4123
|
+
const cSld = firstChild(slideRoot, "p:cSld");
|
|
4124
|
+
const spTree = cSld && firstChild(cSld, "p:spTree");
|
|
4125
|
+
if (!spTree) return;
|
|
4126
|
+
const doc = ownerDocumentOf(slideRoot);
|
|
4127
|
+
const slideRels = this.opc.relationshipsFor(newPartName);
|
|
4128
|
+
const relIdMap = /* @__PURE__ */ new Map();
|
|
4129
|
+
const anchor = firstShapeChild(spTree);
|
|
4130
|
+
for (const partName of [masterPartName, layoutPartName]) {
|
|
4131
|
+
if (!partName) continue;
|
|
4132
|
+
const decorations = carriedDecorations(sourceOpc.part(partName)?.dom.documentElement ?? null);
|
|
4133
|
+
if (decorations.length === 0) continue;
|
|
4134
|
+
const sourceRels = sourceOpc.relationshipsFor(partName);
|
|
4135
|
+
for (const deco of decorations) {
|
|
4136
|
+
const imported = doc.importNode(deco, true);
|
|
4137
|
+
this.#rewriteCarriedRels(imported, ctx, sourceRels, newPartName, slideRels, relIdMap);
|
|
4138
|
+
spTree.insertBefore(imported, anchor);
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
/**
|
|
4143
|
+
* Rewrite every relationship reference (`r:embed`, `r:id`, `r:link`, …) inside a
|
|
4144
|
+
* carried decoration so it points at a fresh slide-local relationship, copying
|
|
4145
|
+
* the referenced part into this package on first sight. `relIdMap` (keyed by
|
|
4146
|
+
* source part + source rel id) dedupes references shared within one import call.
|
|
4147
|
+
*/
|
|
4148
|
+
#rewriteCarriedRels(node, ctx, sourceRels, newPartName, slideRels, relIdMap) {
|
|
4149
|
+
const elements = [];
|
|
4150
|
+
collectElements(node, elements);
|
|
4151
|
+
for (const el of elements) {
|
|
4152
|
+
const refs = [];
|
|
4153
|
+
const attrs = el.attributes;
|
|
4154
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
4155
|
+
const a = attrs.item(i);
|
|
4156
|
+
if (!a || a.namespaceURI !== OOXML_NS.r || !a.value) continue;
|
|
4157
|
+
if (!sourceRels.get(a.value)) continue;
|
|
4158
|
+
refs.push({
|
|
4159
|
+
local: a.localName ?? a.name,
|
|
4160
|
+
id: a.value
|
|
4161
|
+
});
|
|
4162
|
+
}
|
|
4163
|
+
for (const { local, id } of refs) setAttr(el, `r:${local}`, this.#carryRel(ctx, sourceRels, id, newPartName, slideRels, relIdMap));
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
/** Resolve a carried decoration's source relationship to a fresh slide-local id, copying its internal target. */
|
|
4167
|
+
#carryRel(ctx, sourceRels, id, newPartName, slideRels, relIdMap) {
|
|
4168
|
+
const key = `${sourceRels.sourcePartName}|${id}`;
|
|
4169
|
+
const cached = relIdMap.get(key);
|
|
4170
|
+
if (cached) return cached;
|
|
4171
|
+
const rel = sourceRels.get(id);
|
|
4172
|
+
if (!rel) throw new InvalidOptionError("relationship/not-found", `Relationships of ${sourceRels.sourcePartName}: no relationship with id ${id}`);
|
|
4173
|
+
const newId = rel.targetMode === "External" ? slideRels.add(rel.type, rel.target, "External").id : slideRels.add(rel.type, relativePartName(newPartName, copyPart(ctx, sourceRels.resolveTarget(id)))).id;
|
|
4174
|
+
relIdMap.set(key, newId);
|
|
4175
|
+
return newId;
|
|
4176
|
+
}
|
|
4177
|
+
/**
|
|
4178
|
+
* The partname of the layout this deck's slides should attach to in `preserve`
|
|
4179
|
+
* mode: the first layout of the first slide master. Throws when the deck has no
|
|
4180
|
+
* master/layout to attach to (a deck ts-pptx always provides).
|
|
4181
|
+
*/
|
|
4182
|
+
#destinationLayoutPartName() {
|
|
4183
|
+
const presRels = this.opc.relationshipsFor(this.presentationPart.partName);
|
|
4184
|
+
const masterRel = presRels.byType(SLIDE_MASTER_REL)[0];
|
|
4185
|
+
if (!masterRel) throw new InvalidOptionError("import/destination-missing-master", "importSlide preserve mode requires a slide master in the destination deck");
|
|
4186
|
+
const masterPartName = presRels.resolveTarget(masterRel.id);
|
|
4187
|
+
const masterRels = this.opc.relationshipsFor(masterPartName);
|
|
4188
|
+
const layoutRel = masterRels.byType(SLIDE_LAYOUT_REL)[0];
|
|
4189
|
+
if (!layoutRel) throw new InvalidOptionError("import/destination-missing-layout", "importSlide preserve mode requires a slide layout in the destination deck");
|
|
4190
|
+
return masterRels.resolveTarget(layoutRel.id);
|
|
4191
|
+
}
|
|
4192
|
+
/**
|
|
4193
|
+
* Gather the flatten context for a source slide: walk slide → layout → master →
|
|
4194
|
+
* theme, reading the effective colour map (the slide's `clrMapOvr` override, or
|
|
4195
|
+
* the master `clrMap`), the theme `clrScheme`, and the theme `fmtScheme`.
|
|
4196
|
+
*/
|
|
4197
|
+
#sourceFlattenContext(sourceOpc, slidePartName) {
|
|
4198
|
+
const parts = resolveSlideThemeParts(sourceOpc, slidePartName);
|
|
4199
|
+
const themeElements = parts.themeElements;
|
|
4200
|
+
return {
|
|
4201
|
+
clrMap: parts.clrMap,
|
|
4202
|
+
clrScheme: parts.clrScheme,
|
|
4203
|
+
fmtScheme: themeElements ? firstChild(themeElements, "a:fmtScheme") : null,
|
|
4204
|
+
inheritedBackground: this.#effectiveBackground(sourceOpc, parts.slideRoot, parts.layoutPartName, parts.masterPartName),
|
|
4205
|
+
layoutRoot: parts.layoutRoot,
|
|
4206
|
+
masterRoot: parts.masterRoot
|
|
4207
|
+
};
|
|
4208
|
+
}
|
|
4209
|
+
/**
|
|
4210
|
+
* The background the slide effectively inherits from its source subgraph: the
|
|
4211
|
+
* layout's `p:bg`, else the master's. Returns `null` when the slide carries its
|
|
4212
|
+
* own `p:bg` (it stays on the slide and is flattened directly) or none exists.
|
|
4213
|
+
*/
|
|
4214
|
+
#effectiveBackground(sourceOpc, slideRoot, layoutPartName, masterPartName) {
|
|
4215
|
+
if (slideRoot && this.#backgroundOf(slideRoot)) return null;
|
|
4216
|
+
const layoutRoot = layoutPartName ? sourceOpc.part(layoutPartName)?.dom.documentElement ?? null : null;
|
|
4217
|
+
const masterRoot = masterPartName ? sourceOpc.part(masterPartName)?.dom.documentElement ?? null : null;
|
|
4218
|
+
return (layoutRoot && this.#backgroundOf(layoutRoot)) ?? (masterRoot && this.#backgroundOf(masterRoot)) ?? null;
|
|
4219
|
+
}
|
|
4220
|
+
/** The `p:cSld/p:bg` element of a slide/layout/master root, or `null`. */
|
|
4221
|
+
#backgroundOf(root) {
|
|
4222
|
+
const cSld = firstChild(root, "p:cSld");
|
|
4223
|
+
return cSld ? firstChild(cSld, "p:bg") : null;
|
|
4224
|
+
}
|
|
4225
|
+
/**
|
|
4226
|
+
* Open an import out of `source`: this deck as the destination, paired with the
|
|
4227
|
+
* copy registry for that package (created on first use). The registry is held on
|
|
4228
|
+
* the class rather than in the context because it must outlive any one call —
|
|
4229
|
+
* that is what makes a second import from the same source reuse the layout,
|
|
4230
|
+
* master, theme, and media it already copied instead of duplicating them.
|
|
4231
|
+
*/
|
|
4232
|
+
#importContext(source) {
|
|
4233
|
+
let registry = this.#importRegistry.get(source);
|
|
4234
|
+
if (!registry) {
|
|
4235
|
+
registry = /* @__PURE__ */ new Map();
|
|
4236
|
+
this.#importRegistry.set(source, registry);
|
|
4237
|
+
}
|
|
4238
|
+
return {
|
|
4239
|
+
dest: this,
|
|
4240
|
+
source,
|
|
4241
|
+
registry
|
|
4242
|
+
};
|
|
4243
|
+
}
|
|
4244
|
+
/**
|
|
4245
|
+
* Wire a new slide part into `p:sldIdLst` (rel + `p:sldId`) at zero-based
|
|
4246
|
+
* position `at` and return it. `p:sldIdLst` order *is* deck order, so the
|
|
4247
|
+
* insertion point is the only bookkeeping needed. An `at` that is omitted,
|
|
4248
|
+
* negative, or `>=` the current slide count appends (the prior behaviour).
|
|
4249
|
+
*/
|
|
4250
|
+
#insertSlidePart(newPart, at) {
|
|
4251
|
+
const presPart = this.presentationPart;
|
|
4252
|
+
const relId = this.opc.relationshipsFor(presPart.partName).add(SLIDE_REL, relativePartName(presPart.partName, newPart.partName)).id;
|
|
4253
|
+
const root = presPart.dom.documentElement;
|
|
4254
|
+
if (!root) throw new PackageReadError("package/part-has-no-root", "presentation.xml has no document element to append a slide to");
|
|
4255
|
+
const sldIdLst = getOrAddChild(root, "p:sldIdLst", PRESENTATION_SLD_ID_LST_SUCCESSORS);
|
|
4256
|
+
const existing = getElements(sldIdLst, "p:sldId");
|
|
4257
|
+
const newSlideId = this.#nextSlideId(existing);
|
|
4258
|
+
const sldId = createElement(presPart.dom, "p:sldId");
|
|
4259
|
+
setAttr(sldId, "id", String(newSlideId));
|
|
4260
|
+
setAttr(sldId, "r:id", relId);
|
|
4261
|
+
const inRange = at !== void 0 && at >= 0 && at < existing.length;
|
|
4262
|
+
const newIndex = inRange ? at : existing.length;
|
|
4263
|
+
const before = inRange ? existing[at] : null;
|
|
4264
|
+
if (before) sldIdLst.insertBefore(sldId, before);
|
|
4265
|
+
else sldIdLst.appendChild(sldId);
|
|
4266
|
+
presPart.markDirty();
|
|
4267
|
+
return new Slide(this, newPart, newSlideId, newIndex);
|
|
4268
|
+
}
|
|
4269
|
+
/** A slide id one past the highest existing, but at least ST_SlideId's minimum. */
|
|
4270
|
+
#nextSlideId(sldIds) {
|
|
4271
|
+
let max = MIN_SLIDE_ID - 1;
|
|
4272
|
+
for (const sldId of sldIds) {
|
|
4273
|
+
const id = intValue(attr(sldId, "id"));
|
|
4274
|
+
if (id !== null && id > max) max = id;
|
|
4275
|
+
}
|
|
4276
|
+
return max + 1;
|
|
4277
|
+
}
|
|
4278
|
+
/** Re-emit the package; untouched parts stay byte-identical (see `OpcPackage.save`). */
|
|
4279
|
+
async save() {
|
|
4280
|
+
return this.opc.save();
|
|
4281
|
+
}
|
|
4282
|
+
};
|
|
4283
|
+
//#endregion
|
|
4284
|
+
export { Placeholder as a, Theme as c, NotesSlide as d, OpcPackage as f, ContentTypes as h, Slide as i, readTagsForPart as l, Part as m, readCoreProperties as n, SlideLayout as o, Relationships as p, readCustomProperties as r, SlideMaster as s, Presentation as t, NotesPlaceholder as u };
|
|
4285
|
+
|
|
4286
|
+
//# sourceMappingURL=presentation-B0JWdTN1.js.map
|