pptx-ts 3.7.0 → 4.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/README.md +116 -269
- package/dist/autopage-odPz7zZO.js +536 -0
- package/dist/autopage-odPz7zZO.js.map +1 -0
- package/dist/binary-string-CKVAYI3M.js +24 -0
- package/dist/binary-string-CKVAYI3M.js.map +1 -0
- package/dist/body-insets-Dw_BeWD_.js +54 -0
- package/dist/body-insets-Dw_BeWD_.js.map +1 -0
- package/dist/browser.d.ts +29 -9
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +41 -14
- package/dist/browser.js.map +1 -1
- package/dist/clip-Bl01BPcN.d.ts +67 -0
- package/dist/clip-Bl01BPcN.d.ts.map +1 -0
- package/dist/diagnostics-D8ot4Ei7.d.ts +244 -0
- package/dist/diagnostics-D8ot4Ei7.d.ts.map +1 -0
- package/dist/embedded-fonts-DarT9aoa.d.ts +20 -0
- package/dist/embedded-fonts-DarT9aoa.d.ts.map +1 -0
- package/dist/entry-compose-DCnvBZz2.d.ts +1046 -0
- package/dist/entry-compose-DCnvBZz2.d.ts.map +1 -0
- package/dist/{enums-CvRYniUA.d.ts → enums-BUKTUB6V.d.ts} +21 -13
- package/dist/{enums-CvRYniUA.d.ts.map → enums-BUKTUB6V.d.ts.map} +1 -1
- package/dist/{errors-Bvln0H-7.js → errors-C4oENPcu.js} +92 -2
- package/dist/errors-C4oENPcu.js.map +1 -0
- package/dist/families.d.ts +260 -0
- package/dist/families.d.ts.map +1 -0
- package/dist/families.js +57 -0
- package/dist/families.js.map +1 -0
- package/dist/{fit-BKs0zxMI.js → fit-Cb7naFj6.js} +249 -167
- package/dist/fit-Cb7naFj6.js.map +1 -0
- package/dist/font-metrics-v5fn7s7e.d.ts +613 -0
- package/dist/font-metrics-v5fn7s7e.d.ts.map +1 -0
- package/dist/grid-DikTjFey.js +187 -0
- package/dist/grid-DikTjFey.js.map +1 -0
- package/dist/{html-dom-B0I0QSbU.js → html-dom-Iuog2wdK.js} +121 -91
- package/dist/html-dom-Iuog2wdK.js.map +1 -0
- package/dist/html.d.ts +6 -6
- package/dist/html.d.ts.map +1 -1
- package/dist/html.js +7 -6
- package/dist/html.js.map +1 -1
- package/dist/image-size-CvN2iCA4.js +1611 -0
- package/dist/image-size-CvN2iCA4.js.map +1 -0
- package/dist/index.d.ts +24 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -11
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +40 -31
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +40 -48
- package/dist/inspect.js.map +1 -1
- package/dist/math.d.ts +7 -5
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +16 -7
- package/dist/math.js.map +1 -1
- package/dist/measure.d.ts +12 -225
- package/dist/measure.d.ts.map +1 -1
- package/dist/measure.js +3 -3
- package/dist/node.d.ts +22 -8
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +27 -7
- package/dist/node.js.map +1 -1
- package/dist/notes-Dh12JAbn.js +3264 -0
- package/dist/notes-Dh12JAbn.js.map +1 -0
- package/dist/options-internal-WqgMf720.js +43 -0
- package/dist/options-internal-WqgMf720.js.map +1 -0
- package/dist/{package-CaOLXSNi.d.ts → package-J49Ck6S2.d.ts} +27 -12
- package/dist/package-J49Ck6S2.d.ts.map +1 -0
- package/dist/playbtn-CUqQdoDf.js +22 -0
- package/dist/playbtn-CUqQdoDf.js.map +1 -0
- package/dist/{slide-ZqAdVvHV.d.ts → pres-C6qgOFAa.d.ts} +1021 -1423
- package/dist/pres-C6qgOFAa.d.ts.map +1 -0
- package/dist/{presentation-DrJtj3-C.js → presentation-DGYlAWhl.js} +3208 -2727
- package/dist/presentation-DGYlAWhl.js.map +1 -0
- package/dist/{presentation-types-CDjScsev.d.ts → presentation-types-8jLQHI4M.d.ts} +1070 -683
- package/dist/presentation-types-8jLQHI4M.d.ts.map +1 -0
- package/dist/read.d.ts +9 -9
- package/dist/read.d.ts.map +1 -1
- package/dist/read.js +4 -4
- package/dist/script.d.ts +80 -66
- package/dist/script.d.ts.map +1 -1
- package/dist/script.js +2399 -1139
- package/dist/script.js.map +1 -1
- package/dist/{shapes-BemroU-z.js → shapes-re0KyQk_.js} +2807 -1609
- package/dist/shapes-re0KyQk_.js.map +1 -0
- package/dist/slide-Dx2ZeVZ6.d.ts +1256 -0
- package/dist/slide-Dx2ZeVZ6.d.ts.map +1 -0
- package/dist/slide-size-BalTjFuv.js +88 -0
- package/dist/slide-size-BalTjFuv.js.map +1 -0
- package/dist/{units-internal-Dg95w_YD.js → table-cell-margins-EVg0EwxT.js} +235 -273
- package/dist/table-cell-margins-EVg0EwxT.js.map +1 -0
- package/dist/table-dom-BTDP3lEP.js +21 -0
- package/dist/table-dom-BTDP3lEP.js.map +1 -0
- package/dist/text-anchor-CrYXAfUu.js +176 -0
- package/dist/text-anchor-CrYXAfUu.js.map +1 -0
- package/dist/text-sN64Otf_.js +3980 -0
- package/dist/text-sN64Otf_.js.map +1 -0
- package/dist/transition-speed-kUEWDWTx.js +14 -0
- package/dist/transition-speed-kUEWDWTx.js.map +1 -0
- package/dist/units-internal-XJlf-hOl.js +649 -0
- package/dist/units-internal-XJlf-hOl.js.map +1 -0
- package/dist/zip-C0leZZMj.d.ts +67 -0
- package/dist/zip-C0leZZMj.d.ts.map +1 -0
- package/dist/zip-CKd8ihcf.js +177 -0
- package/dist/zip-CKd8ihcf.js.map +1 -0
- package/dist/zip.d.ts +3 -68
- package/dist/zip.js +3 -163
- package/dist/zoom-DPUHh85d.js +8865 -0
- package/dist/zoom-DPUHh85d.js.map +1 -0
- package/package.json +106 -19
- package/skills/ts-pptx-upstream/SKILL.md +58 -57
- package/dist/autopage-DkjYZUFD.js +0 -423
- package/dist/autopage-DkjYZUFD.js.map +0 -1
- package/dist/diagnostics-DfBdDHre.d.ts +0 -829
- package/dist/diagnostics-DfBdDHre.d.ts.map +0 -1
- package/dist/errors-Bvln0H-7.js.map +0 -1
- package/dist/errors-PflMPWBn.d.ts +0 -182
- package/dist/errors-PflMPWBn.d.ts.map +0 -1
- package/dist/fit-BKs0zxMI.js.map +0 -1
- package/dist/html-dom-B0I0QSbU.js.map +0 -1
- package/dist/namespaces-BVnNu7sg.js +0 -666
- package/dist/namespaces-BVnNu7sg.js.map +0 -1
- package/dist/notes-CJFJaG6M.js +0 -1724
- package/dist/notes-CJFJaG6M.js.map +0 -1
- package/dist/package-CaOLXSNi.d.ts.map +0 -1
- package/dist/pres-BcBR3CSM.d.ts +0 -202
- package/dist/pres-BcBR3CSM.d.ts.map +0 -1
- package/dist/presentation-DrJtj3-C.js.map +0 -1
- package/dist/presentation-types-CDjScsev.d.ts.map +0 -1
- package/dist/shapes-BemroU-z.js.map +0 -1
- package/dist/slide-ZqAdVvHV.d.ts.map +0 -1
- package/dist/text-B8SLEaZx.js +0 -11327
- package/dist/text-B8SLEaZx.js.map +0 -1
- package/dist/units-1kRzYShk.js +0 -231
- package/dist/units-1kRzYShk.js.map +0 -1
- package/dist/units-internal-Dg95w_YD.js.map +0 -1
- package/dist/zip.d.ts.map +0 -1
- package/dist/zip.js.map +0 -1
|
@@ -0,0 +1,3980 @@
|
|
|
1
|
+
import { l as warn, n as InvalidOptionError, r as MediaError, t as InternalError, u as warnOnce } from "./errors-C4oENPcu.js";
|
|
2
|
+
import { F as DEF_PRES_LAYOUT_NAME, J as SLDNUM_PLACEHOLDER_TEXT, P as DEF_PRES_LAYOUT, R as DEF_SLIDE_MARGIN_IN, U as LEVEL_MARGINS_EMU, V as LAYOUT_IDX_SERIES_BASE, X as MIN_SLIDE_MASTER_ID, a as convertRotationDegrees, n as clampRangedInput, q as SLDNUMFLDID, s as getSmartParseNumber, u as mapStated, v as resolveInsetsEmu } from "./units-internal-XJlf-hOl.js";
|
|
3
|
+
import { A as inchesToEmu, D as emuToInches, F as EMU_PER_INCH, T as STANDARD_LAYOUTS } from "./table-cell-margins-EVg0EwxT.js";
|
|
4
|
+
import { $t as THEME_REL, Bt as PRES_PROPS_CONTENT_TYPE, Dt as MS_MEDIA_REL, E as EMBEDDED_FONT_SLOTS, Ft as OFFICE_DOCUMENT_REL, Gt as SLIDE_LAYOUT_CONTENT_TYPE, Ht as RELATIONSHIPS_CONTENT_TYPE, Jt as SLIDE_MASTER_REL, Kt as SLIDE_LAYOUT_REL, Nt as NOTES_SLIDE_REL, Qt as THEME_CONTENT_TYPE, Vt as PRES_PROPS_REL, Wt as SLIDE_CONTENT_TYPE, Xt as TABLE_STYLES_CONTENT_TYPE, Yt as SLIDE_REL, Zt as TABLE_STYLES_REL, _t as CORE_PROPS_REL, a as textAlignToken, an as PML_ROOT_NS, at as CHARTEX_REL, bt as EXTENDED_PROPS_CONTENT_TYPE, c as checkEnumOrThrow, cn as decodeBase64ToBytes, d as xsdBool, dn as toMediaDataUri, en as VIDEO_REL, f as xsdBoolIfTrue, gt as CORE_PROPS_CONTENT_TYPE, h as stripHash, ht as COMMENT_AUTHORS_REL, in as OOXML_NS, jt as NOTES_MASTER_REL, l as checkEnumOrWarn, lt as CHART_REL, nn as VIEW_PROPS_REL, nt as TRANSITION_VARIANT_ATTRIBUTES, p as isHexColor, pt as COMMENTS_REL, qt as SLIDE_MASTER_CONTENT_TYPE, rt as AUDIO_REL, sn as bytesToBase64, tn as VIEW_PROPS_CONTENT_TYPE, tt as TRANSITION_TYPES, vt as CUSTOM_PROPS_CONTENT_TYPE, w as DEFAULT_COLOR_MAP, wt as IMAGE_REL, xt as EXTENDED_PROPS_REL, yt as CUSTOM_PROPS_REL, zt as PRESENTATION_MAIN_CONTENT_TYPE } from "./image-size-CvN2iCA4.js";
|
|
5
|
+
import { a as avContentType, i as audioExtensionForSubtype, o as dataUriMediaType, s as imageContentType } from "./text-anchor-CrYXAfUu.js";
|
|
6
|
+
import { At as getDuplicateObjectNames, C as targetBetweenPptSubparts, Ct as FONT_DATA_CONTENT_TYPE, D as cNvPrEl, Dt as serializeEmbeddedFontLst, Et as flattenEmbeddedFaces, G as clampParaMarginInchesEmu, It as preencodedPath, K as buAutoNumEl, N as spTreeEl, Nt as isHyperlinkRel, Ot as XML_DECL, Pt as isPreencodedPath, R as prstGeomRect, Rt as encodeXmlAttrValue, S as slidePath, St as voidEl, T as targetFromPresentation, Tt as FONT_REL_TYPE, U as clampFontSizeSz, W as clampParaIndentInchesEmu, _ as fontPath, at as fillNamesPaint, b as relsPath, bt as elOrVoid, c as themeFontDefRPr, f as relationshipsPart, g as commentPath, h as SLIDE_MASTER_PATH, ht as createColorElement, i as makeXmlNotesSlide, j as grpXfrmEl, jt as getNewRelId, kt as bracedGuid, l as externalHyperlinkRel, m as PRESENTATION_PATH, n as makeXmlNotesMaster, ot as genXmlColorSelection, p as NOTES_MASTER_PATH, q as buCharEl, s as lvlPPr, t as buildNotesSlideRels, u as relationshipEl, ut as STRETCH_FILL_RECT, v as notesSlidePath, vt as rejectEmptyColor, w as targetFromPptSubpart, wt as FONT_DATA_EXTENSION, x as slideLayoutPath, xt as raw, y as overrideName, yt as el } from "./notes-Dh12JAbn.js";
|
|
7
|
+
import { A as renderedSlideObjects, B as pushMediaRel, C as msMediaRid, D as findLayoutPlaceholder, E as placeholderOptionsForSlide, F as warnUnauthoredChild, I as SLIDE_METHOD_FAMILIES, L as composeFamilies, M as shapeIdCount, N as addChildDefinition, O as resolveTextAnchor, P as isGroupableObject, R as familyMethodUnavailable, S as alternateContentEl, T as addTextDefinition, V as resolveImageSource, _ as chartPartNames, a as mediaFamily, b as GROUP_SHAPE_LOCK_ATTRS, c as commentsFamily, d as textFamily, f as shapeFamily, g as makeXmlCore, h as groupFamily, i as model3dFamily, j as resolveObjectNameToId, k as collectSlideShapeIds, l as chartFamily, m as imageFamily, n as tableFamily, o as measureFamily, p as notesFamily, r as oleFamily, s as connectorFamily, t as zoomFamily, u as animationsFamily, v as FMT_SCHEME_XML, w as previewRid, x as genXmlObjectLock, y as resolveImageExtent, z as requirePresentationAuthor } from "./zoom-DPUHh85d.js";
|
|
8
|
+
import { n as setOrClear, t as pickDefined } from "./options-internal-WqgMf720.js";
|
|
9
|
+
import { b as isFontCollection, g as FontMetricsRegistry, t as applyMeasuredFit, y as parseFontMetrics } from "./fit-Cb7naFj6.js";
|
|
10
|
+
import { t as ZipWriter } from "./zip-CKd8ihcf.js";
|
|
11
|
+
import { t as transitionSpeedForDuration } from "./transition-speed-kUEWDWTx.js";
|
|
12
|
+
import { n as presentationFormatLabel, r as slideSizeType } from "./slide-size-BalTjFuv.js";
|
|
13
|
+
//#region src/gen/define/background.ts
|
|
14
|
+
/**
|
|
15
|
+
* ts-pptx: Background Definition
|
|
16
|
+
*
|
|
17
|
+
* Registers an `addBackground()` image as a slide media rel (color backgrounds carry no rel);
|
|
18
|
+
* the `<p:bg>` XML is emitted later at slide / layout serialize time.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Adds a background image or color to a slide definition.
|
|
22
|
+
*
|
|
23
|
+
* Only an image background does anything here: it is the one that needs a media relationship.
|
|
24
|
+
* A colour — object or the bare-string shorthand — carries no rel and is emitted straight from
|
|
25
|
+
* `slide.background` at serialize time.
|
|
26
|
+
*
|
|
27
|
+
* The rel id and the part name come from the shared allocators. This used to take the id as
|
|
28
|
+
* `_relsMedia.length + 1`, which a hyperlink or chart rel could already hold, and to name the part
|
|
29
|
+
* after the slide or layout title. Titles are caller-supplied and different ones sanitize alike,
|
|
30
|
+
* so "A B" and "A-B", or a layout titled "Slide 1" beside slide 1, wrote one part that both
|
|
31
|
+
* backgrounds pointed at. `nextMediaTarget` keys the name by slide, layout or master, never
|
|
32
|
+
* by title, so it is unique and needs no sanitizing.
|
|
33
|
+
*
|
|
34
|
+
* Called on every assignment, so it replaces what an earlier one registered. A new image takes
|
|
35
|
+
* over the earlier background's rel in place, keeping its id and its part name, rather than
|
|
36
|
+
* pushing a second one; replacing it by removal would shift the media count every later part name
|
|
37
|
+
* is read from. A colour clears the image rel id, which `slideBackgroundXml` checks before it looks
|
|
38
|
+
* at a colour, so an image replaced by a colour stops painting.
|
|
39
|
+
*
|
|
40
|
+
* The image's rel stays in `_relsMedia` for the same reason, so a package whose image background
|
|
41
|
+
* was replaced by a colour still carries the image part and an `image` relationship nothing
|
|
42
|
+
* references. That is valid OPC, only unused bytes. Dropping it would need either a tombstone the
|
|
43
|
+
* rel and part emitters skip, or media part names that do not depend on the count.
|
|
44
|
+
*
|
|
45
|
+
* An image whose `data` has no base64 header is dropped with a warning, as an image fill's is,
|
|
46
|
+
* and leaves any colour the background states to paint. It used to be written as a part holding
|
|
47
|
+
* the caller's text.
|
|
48
|
+
* @param {BackgroundOption} props - a bare colour, or an object with a colour or image definition
|
|
49
|
+
* @param {SlideLayoutInternal} target - slide or layout that the background is set on
|
|
50
|
+
*/
|
|
51
|
+
function addBackgroundDefinition(props, target) {
|
|
52
|
+
if (!(props && typeof props === "object" && (props.path || props.data))) {
|
|
53
|
+
delete target._bkgdImgRid;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const source = resolveImageSource(props);
|
|
57
|
+
if (typeof source === "string") {
|
|
58
|
+
warn("background/missing-base64-header", "background `data` value lacks a base64 header (ex: 'image/png;base64,...'); ignoring the background image.");
|
|
59
|
+
delete target._bkgdImgRid;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let strImgExtn = source.extn;
|
|
63
|
+
if (strImgExtn === "jpg") strImgExtn = "jpeg";
|
|
64
|
+
const type = imageContentType(strImgExtn);
|
|
65
|
+
const previous = target._relsMedia.findIndex((rel) => rel.rId === target._bkgdImgRid);
|
|
66
|
+
const replaced = target._relsMedia[previous];
|
|
67
|
+
if (replaced) {
|
|
68
|
+
target._relsMedia[previous] = {
|
|
69
|
+
path: source.path || preencodedPath(strImgExtn),
|
|
70
|
+
type,
|
|
71
|
+
extn: strImgExtn,
|
|
72
|
+
data: source.data,
|
|
73
|
+
rId: replaced.rId,
|
|
74
|
+
Target: replaced.Target.replace(/\.[^./]+$/, `.${strImgExtn}`)
|
|
75
|
+
};
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const slide = target;
|
|
79
|
+
const rId = getNewRelId(slide);
|
|
80
|
+
pushMediaRel(slide, {
|
|
81
|
+
kind: "image",
|
|
82
|
+
extn: strImgExtn,
|
|
83
|
+
type,
|
|
84
|
+
path: source.path,
|
|
85
|
+
data: source.data,
|
|
86
|
+
rId
|
|
87
|
+
});
|
|
88
|
+
target._bkgdImgRid = rId;
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/slide.ts
|
|
92
|
+
/**
|
|
93
|
+
* Project one authored render-object onto the public {@link SlideObjectInfo}, recursing into a
|
|
94
|
+
* group's children.
|
|
95
|
+
*
|
|
96
|
+
* The name is the stored one, which is the caller's own spelling: only `cNvPrOpen` escapes it. So
|
|
97
|
+
* a name read here resolves when it is handed back to `groupObjects()`.
|
|
98
|
+
*
|
|
99
|
+
* Every `add*Definition` also *generates* a name when the caller passed none, so the fallback below
|
|
100
|
+
* is unreachable through the public API. It is there because the field is typed non-optional
|
|
101
|
+
* internally only by convention, and an empty name would otherwise read back as `undefined` from a
|
|
102
|
+
* surface that promises a string.
|
|
103
|
+
*/
|
|
104
|
+
function toSlideObjectInfo(obj) {
|
|
105
|
+
const stored = obj.options?.objectName;
|
|
106
|
+
return {
|
|
107
|
+
type: obj._type,
|
|
108
|
+
objectName: typeof stored === "string" && stored.length > 0 ? stored : "",
|
|
109
|
+
isPlaceholder: !!obj.options?.placeholder,
|
|
110
|
+
canGroup: isGroupableObject(obj),
|
|
111
|
+
children: (obj._groupObjects ?? []).map(toSlideObjectInfo)
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
var SlideBuilder = class {
|
|
115
|
+
_setSlideNum;
|
|
116
|
+
addSlide;
|
|
117
|
+
getSlide;
|
|
118
|
+
getSections;
|
|
119
|
+
_name;
|
|
120
|
+
_presLayout;
|
|
121
|
+
_rels;
|
|
122
|
+
_relsChart;
|
|
123
|
+
_relsMedia;
|
|
124
|
+
_rId;
|
|
125
|
+
_slideId;
|
|
126
|
+
_slideLayout;
|
|
127
|
+
_slideNum;
|
|
128
|
+
_slideNumberProps;
|
|
129
|
+
_slideObjects;
|
|
130
|
+
_comments = [];
|
|
131
|
+
_newAutoPagedSlides = [];
|
|
132
|
+
_animations = [];
|
|
133
|
+
/**
|
|
134
|
+
* The child-descriptor authors this presentation was composed with, for the group family: a
|
|
135
|
+
* group's children resolve through the same table a slide master's `objects` do, so a group can
|
|
136
|
+
* hold exactly the kinds this presentation can author.
|
|
137
|
+
*/
|
|
138
|
+
_childAuthors;
|
|
139
|
+
constructor(params) {
|
|
140
|
+
this.addSlide = params.addSlide;
|
|
141
|
+
this.getSlide = params.getSlide;
|
|
142
|
+
this.getSections = params.getSections ?? (() => []);
|
|
143
|
+
this._name = `Slide ${params.slideNumber}`;
|
|
144
|
+
this._presLayout = params.presLayout;
|
|
145
|
+
this._rId = params.slideRId;
|
|
146
|
+
this._rels = [];
|
|
147
|
+
this._relsChart = [];
|
|
148
|
+
this._relsMedia = [];
|
|
149
|
+
this._setSlideNum = params.setSlideNum;
|
|
150
|
+
this._slideId = params.slideId;
|
|
151
|
+
this._slideLayout = params.slideLayout || null;
|
|
152
|
+
this._slideNum = params.slideNumber;
|
|
153
|
+
this._slideObjects = [];
|
|
154
|
+
this._childAuthors = params.childAuthors;
|
|
155
|
+
this.#bindFamilyMethods(params.authors);
|
|
156
|
+
/** NOTE: Slide Numbers: In order for Slide Numbers to function they need to be in all 3 files: master/layout/slide
|
|
157
|
+
* `defineSlideMaster` and `addNewSlide.slideNumber` will add {slideNumber} to `this.masterSlide` and `this.slideLayouts`
|
|
158
|
+
* so, lastly, add to the Slide now.
|
|
159
|
+
*/
|
|
160
|
+
this._slideNumberProps = this._slideLayout?._slideNumberProps ? this._slideLayout._slideNumberProps : null;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Put this presentation's family methods on the slide.
|
|
164
|
+
*
|
|
165
|
+
* Per instance rather than on the prototype: the family set belongs to the presentation, not to
|
|
166
|
+
* the class, and two presentations composed differently in one process each get their own. A
|
|
167
|
+
* slide count is small enough that per-instance function properties are not a memory question.
|
|
168
|
+
*
|
|
169
|
+
* Chaining is applied here, once, rather than by each author: every `add*` answers the slide, so
|
|
170
|
+
* an author has nothing to say about the return value and cannot get it wrong.
|
|
171
|
+
*/
|
|
172
|
+
#bindFamilyMethods(authors) {
|
|
173
|
+
const slide = this;
|
|
174
|
+
for (const method of Object.keys(SLIDE_METHOD_FAMILIES)) {
|
|
175
|
+
const author = authors[method];
|
|
176
|
+
slide[method] = author ? (...args) => {
|
|
177
|
+
author(this, ...args);
|
|
178
|
+
return this;
|
|
179
|
+
} : () => familyMethodUnavailable(`slide.${method}`, SLIDE_METHOD_FAMILIES[method]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Background color or image
|
|
184
|
+
* @type {BackgroundProps}
|
|
185
|
+
* @example solid color `background: { color:'FF0000' }`
|
|
186
|
+
* @example color+trans `background: { color:'FF0000', transparency:0.5 }`
|
|
187
|
+
* @example base64 `background: { data:'image/png;base64,ABC[...]123' }`
|
|
188
|
+
* @example url `background: { path:'https://some.url/image.jpg'}`
|
|
189
|
+
*/
|
|
190
|
+
_background;
|
|
191
|
+
set background(props) {
|
|
192
|
+
this._background = props;
|
|
193
|
+
addBackgroundDefinition(props, this);
|
|
194
|
+
}
|
|
195
|
+
get background() {
|
|
196
|
+
return this._background;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Default font color
|
|
200
|
+
* @type {HexColor}
|
|
201
|
+
*/
|
|
202
|
+
_color;
|
|
203
|
+
set color(value) {
|
|
204
|
+
this._color = value;
|
|
205
|
+
}
|
|
206
|
+
get color() {
|
|
207
|
+
return this._color;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* @type {boolean}
|
|
211
|
+
*/
|
|
212
|
+
_hidden = false;
|
|
213
|
+
set hidden(value) {
|
|
214
|
+
this._hidden = value;
|
|
215
|
+
}
|
|
216
|
+
get hidden() {
|
|
217
|
+
return this._hidden;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Slide-show transition (`p:transition`) played when advancing to this slide.
|
|
221
|
+
* @type {TransitionProps}
|
|
222
|
+
*/
|
|
223
|
+
_transition;
|
|
224
|
+
set transition(value) {
|
|
225
|
+
this._transition = value;
|
|
226
|
+
}
|
|
227
|
+
get transition() {
|
|
228
|
+
return this._transition;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* @type {SlideNumberProps}
|
|
232
|
+
*/
|
|
233
|
+
set slideNumber(value) {
|
|
234
|
+
this._slideNumberProps = value;
|
|
235
|
+
this._setSlideNum(value);
|
|
236
|
+
}
|
|
237
|
+
get slideNumber() {
|
|
238
|
+
return this._slideNumberProps ?? void 0;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Every continuation slide this slide's tables have spilled onto, in call order, each once.
|
|
242
|
+
*
|
|
243
|
+
* Across calls, not per call: a slide can carry more than one `addTable`, and each auto-paging
|
|
244
|
+
* one appends whatever it spilled onto. The list was assigned rather than appended, so a second
|
|
245
|
+
* table erased the first table's report while its slides stayed in the deck. "Spilled onto"
|
|
246
|
+
* rather than "created" because a later table lands on the earlier one's continuations instead
|
|
247
|
+
* of making its own, and those are reported once.
|
|
248
|
+
*/
|
|
249
|
+
get newAutoPagedSlides() {
|
|
250
|
+
return this._newAutoPagedSlides;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* The objects authored on this slide so far, bottom-to-top in z-order.
|
|
254
|
+
*
|
|
255
|
+
* Built fresh on every access rather than cached: the underlying list is mutated in place by
|
|
256
|
+
* `addGroup`/`groupObjects` as well as appended to, so a cache would have to be invalidated from
|
|
257
|
+
* every authoring path and would go stale the first time one was added without remembering.
|
|
258
|
+
* Building it costs a walk of a list a slide-sized deck keeps in the dozens.
|
|
259
|
+
*/
|
|
260
|
+
get objects() {
|
|
261
|
+
return renderedSlideObjects(this._slideObjects).map(toSlideObjectInfo);
|
|
262
|
+
}
|
|
263
|
+
/** Slide width in inches (resolved from the active presentation layout). */
|
|
264
|
+
get width() {
|
|
265
|
+
return emuToInches(this._presLayout.width);
|
|
266
|
+
}
|
|
267
|
+
/** Slide height in inches (resolved from the active presentation layout). */
|
|
268
|
+
get height() {
|
|
269
|
+
return emuToInches(this._presLayout.height);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/font-source.ts
|
|
274
|
+
/**
|
|
275
|
+
* ts-pptx: getting font bytes out of whatever the caller handed over.
|
|
276
|
+
*
|
|
277
|
+
* `registerFontMetrics` and `embedFont` are two public methods on one class, forty lines apart,
|
|
278
|
+
* and each resolved its source itself. They agreed on `Uint8Array` and `ArrayBuffer` and
|
|
279
|
+
* disagreed about what a `string` means: `registerFontMetrics('X', someBase64)` handed the
|
|
280
|
+
* base64 to the runtime's file loader and failed to open it, while
|
|
281
|
+
* `embedFont({ data: someBase64 })` decoded it. Neither doc said which, because each was only
|
|
282
|
+
* describing its own half.
|
|
283
|
+
*
|
|
284
|
+
* The two really do want different string semantics -- `embedFont` names its path in `path` and
|
|
285
|
+
* its bytes in `data`, so a string in `data` cannot be a filename -- so the answer is one
|
|
286
|
+
* resolver with the difference stated as a flag rather than two resolvers with the difference
|
|
287
|
+
* left implicit.
|
|
288
|
+
*/
|
|
289
|
+
/**
|
|
290
|
+
* Resolve a caller's font source to bytes.
|
|
291
|
+
*
|
|
292
|
+
* A `Uint8Array` is used as it stands, an `ArrayBuffer` is wrapped, and a `string` is read
|
|
293
|
+
* according to `base64`: **false or absent** means a path or URL for the runtime's loader (what
|
|
294
|
+
* `registerFontMetrics(face, source)` and `embedFont({ path })` take), **true** means base64
|
|
295
|
+
* text (what `embedFont({ data })` takes, with or without a `data:` prefix).
|
|
296
|
+
* @param source - the caller's value
|
|
297
|
+
* @param runtime - the entry point's adapter, for the path/URL case
|
|
298
|
+
* @param opts - `base64: true` to read a string as base64 rather than as a path
|
|
299
|
+
* @param opts.base64 - see above
|
|
300
|
+
* @param label - how to name the offending argument if the source is unusable
|
|
301
|
+
*/
|
|
302
|
+
async function resolveFontBytes(source, runtime, opts, label) {
|
|
303
|
+
if (source instanceof Uint8Array) return source;
|
|
304
|
+
if (source instanceof ArrayBuffer) return new Uint8Array(source);
|
|
305
|
+
if (typeof source === "string") {
|
|
306
|
+
if (!opts?.base64) return await runtime.loadFontData(source);
|
|
307
|
+
const decoded = decodeBase64ToBytes(source);
|
|
308
|
+
if (!decoded) throw new InvalidOptionError("font/invalid-base64", `${label} is not valid base64`);
|
|
309
|
+
return decoded;
|
|
310
|
+
}
|
|
311
|
+
throw new InvalidOptionError("font/missing-source", `${label} must be a font path, raw bytes, or base64 data`);
|
|
312
|
+
}
|
|
313
|
+
//#endregion
|
|
314
|
+
//#region src/gen/define/master.ts
|
|
315
|
+
/**
|
|
316
|
+
* Transforms a slide definition to a slide object that is then passed to the XML transformation process.
|
|
317
|
+
* @param {SlideMasterProps} props - slide definition
|
|
318
|
+
* @param {PresSlideInternal|SlideLayoutInternal} target - empty slide object that should be updated by the passed definition
|
|
319
|
+
* @param {ChildAuthors} children - the child-descriptor authors this presentation was composed with
|
|
320
|
+
*/
|
|
321
|
+
function createSlideMaster(props, target, children) {
|
|
322
|
+
if (props.objects && Array.isArray(props.objects) && props.objects.length > 0) props.objects.forEach((object, idx) => {
|
|
323
|
+
const tgt = target;
|
|
324
|
+
if (addChildDefinition(tgt, object, children)) {} else if ("placeholder" in object) {
|
|
325
|
+
const placeholder = object.placeholder;
|
|
326
|
+
const { name, type, ...rawPlaceholderOptions } = placeholder.options;
|
|
327
|
+
const placeholderOptions = rawPlaceholderOptions;
|
|
328
|
+
placeholderOptions.placeholder = name;
|
|
329
|
+
placeholderOptions._placeholderType = type;
|
|
330
|
+
placeholderOptions._placeholderIdx = 100 + idx;
|
|
331
|
+
const placeholderRun = placeholder.text === void 0 ? {} : { text: placeholder.text };
|
|
332
|
+
addTextDefinition(tgt, [placeholderRun], placeholderOptions, true);
|
|
333
|
+
} else warnUnauthoredChild(object, "defineSlideMaster()");
|
|
334
|
+
});
|
|
335
|
+
if (props.slideNumber && typeof props.slideNumber === "object") target._slideNumberProps = props.slideNumber;
|
|
336
|
+
}
|
|
337
|
+
//#endregion
|
|
338
|
+
//#region src/gen/define/placeholder.ts
|
|
339
|
+
/**
|
|
340
|
+
* Adds placeholder objects to slide
|
|
341
|
+
* @param {PresSlideInternal} slide - slide object containing layouts
|
|
342
|
+
*/
|
|
343
|
+
function addPlaceholdersToSlideLayouts(slide) {
|
|
344
|
+
if (!slide._slideLayout) return;
|
|
345
|
+
(slide._slideLayout._slideObjects || []).forEach((slideLayoutObj) => {
|
|
346
|
+
if (slideLayoutObj._type === "placeholder") {
|
|
347
|
+
const slideLayoutOptions = slideLayoutObj.options || {};
|
|
348
|
+
if (!slide._slideObjects.some((slideObj) => slideObj.options && slideObj.options.placeholder === slideLayoutOptions.placeholder)) addTextDefinition(slide, [{ text: "" }], placeholderOptionsForSlide(slideLayoutOptions), true);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region src/media/placeholders.ts
|
|
354
|
+
/**
|
|
355
|
+
* The base64 placeholder images the library falls back to when a real asset is unavailable.
|
|
356
|
+
*
|
|
357
|
+
* Their own module because of where the bundler puts them, not because they group naturally.
|
|
358
|
+
* The chunker splits by module, so a constant is pulled into every chunk that reaches the module
|
|
359
|
+
* declaring it — and these lived in `constants-internal.ts` beside `XML_DECL`, `DEF_FONT_SIZE`
|
|
360
|
+
* and the rest of the vocabulary all of `src/gen/` imports. The rule that follows: a payload
|
|
361
|
+
* measured in kilobytes does not share a module with vocabulary, however tidy that grouping reads.
|
|
362
|
+
*
|
|
363
|
+
* The default video poster obeys the same rule one size up. At 14 kB it does not share a module
|
|
364
|
+
* with these two either: it lives alone in `src/media/playbtn.ts`, which is what lets the media
|
|
365
|
+
* pass reach it dynamically instead of linking it into every consumer's entry chunk.
|
|
366
|
+
*/
|
|
367
|
+
/** Shown in place of media that failed to load, under `onMediaError: 'placeholder'`. */
|
|
368
|
+
const IMG_BROKEN = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAB3CAYAAAD1oOVhAAAGAUlEQVR4Xu2dT0xcRRzHf7tAYSsc0EBSIq2xEg8mtTGebVzEqOVIolz0siRE4gGTStqKwdpWsXoyGhMuyAVJOHBgqyvLNgonDkabeCBYW/8kTUr0wsJC+Wfm0bfuvn37Znbem9mR9303mJnf/Pb7ed95M7PDI5JIJPYJV5EC7e3t1N/fT62trdqViQCIu+bVgpIHEo/Hqbe3V/sdYVKHyWSSZmZm8ilVA0oeyNjYmEnaVC2Xvr6+qg5fAOJAz4DU1dURGzFSqZRVqtMpAFIGyMjICC0vL9PExIRWKADiAYTNshYWFrRCARAOEFZcCKWtrY0GBgaUTYkBRACIE4rKZwqACALR5RQAqQCIDqcASIVAVDsFQCSAqHQKgEgCUeUUAPEBRIVTAMQnEBvK5OQkbW9vk991CoAEAMQJxc86BUACAhKUUwAkQCBBOAVAAgbi1ykAogCIH6cAiCIgsk4BEIVAZJwCIIqBVLqiBxANQFgXS0tLND4+zl08AogmIG5OSSQS1gGKwgtANAIRcQqAaAbCe6YASBWA2E6xDyeyDUl7+AKQMkDYYevm5mZHabA/Li4uUiaTsYLau8QA4gLE/hU7wajyYtv1hReDAiAOxQcHBymbzark4BkbQKom/X8dp9Npmpqasn4BIAYAYSnYp+4BBEAMUcCwNOCQsAKZnp62NtQOw8WmwT09PUo+ijaHsOMx7GppaaH6+nolH0Z10K2tLVpdXbW6UfV3mNqBdHd3U1NTk2rtlMRfW1uj2dlZAFGirkRQAJEQTWUTAFGprkRsAJEQTWUTAFGprkRsAJEQTWUTAFGprkRsAJEQTWUTAFGprkRsAJEQTWUTAFGprkRsAJEQTWUTAGHqrm8caPzQ0WC1logbeiC7X3xJm0PvUmRzh45cuki1588FAmVn9BO6P3yF9utrqGH0MtW82S8UN9RA9v/4k7InjhcJFTs/TLVXLwmJV67S7vD7tHF5pKi46fYdosdOcOOGG8j1OcqefbFEJD9Q3GCwDhqT31HklS4A8VRgfYM2Op6k3bt/BQJl58J7lPvwg5JYNccepaMry0LPqFA7hCm39+NNyp2J0172b19QysGINj5CsRtpij57musOViH0QPJQXn6J9u7dlYJSFkbrMYolrwvDAJAC+WWdEpQz7FTgECeUCpzi6YxvvqXoM6eEhqnCSgDikEzUKUE7Aw7xuHctKB5OYU3dZlNR9syQdAaAcAYTC0pXF+39c09o2Ik+3EqxVKqiB7hbYAxZkk4pbBaEM+AQofv+wTrFwylBOQNABIGwavdfe4O2pg5elO+86l99nY58/VUF0byrYsjiSFluNlXYrOHcBar7+EogUADEQ0YRGHbzoKAASBkg2+9cpM1rV0tK2QOcXW7bLEFAARAXIF4w2DrDWoeUWaf4hQIgDiA8GPZ2iNfi0Q8UACkAIgrDbrJ385eDxaPLLrEsFAB5oG6lMPJQPLZZZKAACBGVhcG2Q+bmuLu2nk55e4jqPv1IeEoceiBeX7s2zCa5MAqdstl91vfXwaEGsv/rb5TtOFk6tWXOuJGh6KmnhO9sayrMninPx103JBtXblHkice58cINZP4Hyr5wpkgkdiChEmc4FWazLzenNKa/p0jncwDiqcD6BuWePk07t1asatZGoYQzSqA4nFJ7soNiP/+EUyfc25GI2GG53dHPrKo1g/1Cw4pIXLrzO+1c+/wg7tBbFDle/EbQcjFCPWQJCau5EoBoFpzXHYDwFNJcDiCaBed1ByA8hTSXA4hmwXndAQhPIc3lAKJZcF53AMJTSHM5gGgWnNcdgPAU0lwOIJoF53UHIDyFNJcfSiCdnZ0Ui8U0SxlMd7lcjubn561gh+Y1scFIU/0o/3sgeLO12E2k7UXKYumgFoAYdg8ACIAYpoBh6cAhAGKYAoalA4cAiGEKGJYOHAIghilgWDpwCIAYpoBh6cAhAGKYAoalA4cAiGEKGJYOHAIghilgWDpwCIAYpoBh6ZQ4JB6PKzviYthnNy4d9h+1M5mMlVckkUjsG5dhiBMCEMPg/wuOfrZZ/RSywQAAAABJRU5ErkJggg==";
|
|
369
|
+
/** 1x1 transparent PNG, standing in for an SVG the runtime could not rasterize. */
|
|
370
|
+
const IMG_SVG_PLACEHOLDER = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
|
|
371
|
+
//#endregion
|
|
372
|
+
//#region src/gen/media.ts
|
|
373
|
+
/**
|
|
374
|
+
* ts-pptx: Media Methods
|
|
375
|
+
*/
|
|
376
|
+
function hasEncodingPath(rel) {
|
|
377
|
+
return typeof rel.path === "string" && rel.path.length > 0 && !isPreencodedPath(rel.path);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Write an SVG rel's PNG fallback, applying `onMediaError` to a preview that fails.
|
|
381
|
+
*
|
|
382
|
+
* The one place that policy is applied to a preview. The adapter has already stamped the
|
|
383
|
+
* placeholder and resolved with the failure, so only the PNG fallback is ever the placeholder:
|
|
384
|
+
* the SVG part keeps its bytes under either policy. A preview used to reject instead, and an SVG
|
|
385
|
+
* given inline then failed the whole write whatever `onMediaError` said, while one given by path
|
|
386
|
+
* was reported as `media/load-failed` and had its SVG bytes replaced with a broken image.
|
|
387
|
+
*/
|
|
388
|
+
async function writeSvgPreview(rel, runtime, onMediaError) {
|
|
389
|
+
const failure = await runtime.createSvgPngPreview(rel);
|
|
390
|
+
if (failure === null) return "done";
|
|
391
|
+
if (onMediaError === "placeholder") {
|
|
392
|
+
warn("media/svg-preview-failed", `${failure.message}; the SVG is kept and its PNG fallback is a placeholder.`);
|
|
393
|
+
return "done";
|
|
394
|
+
}
|
|
395
|
+
throw failure;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Encode Image/Audio/Video into base64
|
|
399
|
+
* @param {PresSlideInternal | SlideLayoutInternal | SlideMasterInternal} layout - slide, layout or master
|
|
400
|
+
* @param {RuntimeAdapter} runtime - runtime adapter (Node/browser media loader)
|
|
401
|
+
* @param {'throw' | 'placeholder'} onMediaError - failure policy: reject the export (default) or substitute a placeholder and warn
|
|
402
|
+
* @return {Promise} promise
|
|
403
|
+
*/
|
|
404
|
+
function encodeSlideMediaRels(layout, runtime, onMediaError = "throw") {
|
|
405
|
+
const imageProms = [];
|
|
406
|
+
const defaultCoverRels = layout._relsMedia.filter((rel) => rel.isDefaultCover && !rel.data);
|
|
407
|
+
if (defaultCoverRels.length > 0) imageProms.push((async () => {
|
|
408
|
+
const { IMG_PLAYBTN } = await import("./playbtn-CUqQdoDf.js");
|
|
409
|
+
for (const rel of defaultCoverRels) rel.data = IMG_PLAYBTN;
|
|
410
|
+
return "done";
|
|
411
|
+
})());
|
|
412
|
+
const candidateRels = layout._relsMedia.filter((rel) => rel.type !== "online" && !rel.data && hasEncodingPath(rel));
|
|
413
|
+
const unqPaths = [];
|
|
414
|
+
candidateRels.forEach((rel) => {
|
|
415
|
+
if (!unqPaths.includes(rel.path)) {
|
|
416
|
+
rel.isDuplicate = false;
|
|
417
|
+
unqPaths.push(rel.path);
|
|
418
|
+
} else rel.isDuplicate = true;
|
|
419
|
+
});
|
|
420
|
+
candidateRels.filter((rel) => !rel.isDuplicate).forEach((rel) => {
|
|
421
|
+
imageProms.push((async () => {
|
|
422
|
+
const dupes = candidateRels.filter((dupe) => dupe.isDuplicate && dupe.path === rel.path);
|
|
423
|
+
let data;
|
|
424
|
+
try {
|
|
425
|
+
data = toMediaDataUri(await runtime.loadMedia(rel), rel.type);
|
|
426
|
+
} catch (ex) {
|
|
427
|
+
if (Boolean(rel.oleRelType || rel.model3dRelType)) throw new MediaError("media/load-failed", `Failed to load the embedded payload "${rel.path}" during export; a placeholder picture cannot stand in for an OLE object or a 3D model.`, { cause: ex });
|
|
428
|
+
if (onMediaError === "placeholder") {
|
|
429
|
+
warn("media/load-failed", `Failed to load media "${rel.path}"; embedding a broken-image placeholder. (${String(ex)})`);
|
|
430
|
+
rel.data = IMG_BROKEN;
|
|
431
|
+
dupes.forEach((dupe) => dupe.data = IMG_BROKEN);
|
|
432
|
+
return "done";
|
|
433
|
+
}
|
|
434
|
+
throw new MediaError("media/load-failed", `Failed to load media "${rel.path}" during export.`, { cause: ex });
|
|
435
|
+
}
|
|
436
|
+
rel.data = data;
|
|
437
|
+
dupes.forEach((dupe) => dupe.data = data);
|
|
438
|
+
if (rel.isSvgPng) await writeSvgPreview(rel, runtime, onMediaError);
|
|
439
|
+
await Promise.all(dupes.filter((dupe) => dupe.isSvgPng).map((dupe) => writeSvgPreview(dupe, runtime, onMediaError)));
|
|
440
|
+
return "done";
|
|
441
|
+
})());
|
|
442
|
+
});
|
|
443
|
+
layout._relsMedia.filter((rel) => rel.isSvgPng && rel.data).forEach((rel) => {
|
|
444
|
+
imageProms.push(writeSvgPreview(rel, runtime, onMediaError));
|
|
445
|
+
});
|
|
446
|
+
return imageProms;
|
|
447
|
+
}
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region src/gen/prepare.ts
|
|
450
|
+
/**
|
|
451
|
+
* ts-pptx: the pre-serialization pass
|
|
452
|
+
*
|
|
453
|
+
* Everything that must happen to authored slide state *before* any XML is built. Two callers
|
|
454
|
+
* reach serialization and both need it:
|
|
455
|
+
*
|
|
456
|
+
* - `package/assemble.ts` — the normal `write`/`stream`/`writeFile` path, building a
|
|
457
|
+
* complete `.pptx`.
|
|
458
|
+
* - `PresentationCore.extractSlides()` — serializes slide bodies only, for splicing into
|
|
459
|
+
* an already-loaded deck via `Presentation.appendSlides()` (`pptx-ts/read`).
|
|
460
|
+
*
|
|
461
|
+
* The second used to reimplement the first inline, kept in step by a comment that said
|
|
462
|
+
* "exactly as ... does". Nothing failed when they drifted: `extractSlides` would simply emit
|
|
463
|
+
* slides that did not match what a normal write produces, and no test compares the two.
|
|
464
|
+
*
|
|
465
|
+
* Three steps rather than one combined pass, because the package path has work of its own that
|
|
466
|
+
* must land between the last two: it de-duplicates identical media across the deck (which needs
|
|
467
|
+
* the encoded bytes {@link encodeMediaForTargets} produces) and assigns package-unique chart part
|
|
468
|
+
* filenames, both before any text is measured. Exposing the steps lets `assemble` keep that work
|
|
469
|
+
* in place while still sharing the code.
|
|
470
|
+
*
|
|
471
|
+
* The order is load-bearing:
|
|
472
|
+
*
|
|
473
|
+
* 1. **Backfill placeholders first**, so a slide carries its layout's placeholder objects before
|
|
474
|
+
* anything reads its media. A seeded placeholder can register media of its own, an image fill,
|
|
475
|
+
* and when backfill ran after encoding that image was never loaded and its rel named a part
|
|
476
|
+
* that was never written.
|
|
477
|
+
* 2. **Media.** It is the only asynchronous step, and the sync XML pass reads the `rel.data` it
|
|
478
|
+
* populates.
|
|
479
|
+
* 3. **Bake measured fit last**, because it measures the text step 1 may have just added and
|
|
480
|
+
* writes the `fontScale` the sync XML pass then reads. The caller undoes it once that pass is
|
|
481
|
+
* done, so the next write measures from the authored values.
|
|
482
|
+
*
|
|
483
|
+
* The functions take different target sets on purpose: media lives on layouts and the master as
|
|
484
|
+
* well as on slides, while placeholder backfill and measured fit apply only to slides.
|
|
485
|
+
* `extractSlides` passes only its slides to all three, because it emits no layout or master
|
|
486
|
+
* parts.
|
|
487
|
+
*/
|
|
488
|
+
/**
|
|
489
|
+
* Before any step: refuse a link to a slide the deck does not have.
|
|
490
|
+
*
|
|
491
|
+
* A `hyperlink: { slide }` and a Slide Zoom's numeric `target` are checked for being slide numbers
|
|
492
|
+
* when they are authored, but whether the slide exists is only known once the deck is complete: a
|
|
493
|
+
* link to slide 3 can be authored before slide 3 is added. Both serializing callers check here,
|
|
494
|
+
* before anything is built. Otherwise the link is written as a relationship to a part the package
|
|
495
|
+
* never has.
|
|
496
|
+
*
|
|
497
|
+
* A layout and the master carry objects too, and an object given to `defineSlideLayout({ objects })`
|
|
498
|
+
* or `defineSlideMaster({ objects })` takes the same `hyperlink: { slide }`. This used to read the
|
|
499
|
+
* slides' relationships only, so a layout's number went unchecked and a link past the last slide
|
|
500
|
+
* reached the package from there. `extractSlides` emits no layout or master part and so passes
|
|
501
|
+
* neither, the same way it passes only slides to the three preparation steps below.
|
|
502
|
+
* @param slides - every slide in the deck, in order
|
|
503
|
+
* @param layouts - every slide layout, in order; omitted when no layout part is written
|
|
504
|
+
* @param master - the deck's slide master; omitted when no master part is written
|
|
505
|
+
*/
|
|
506
|
+
function requireSlideLinksInDeck(slides, layouts = [], master) {
|
|
507
|
+
const sources = [
|
|
508
|
+
...slides.map((slide) => ({
|
|
509
|
+
label: `Slide ${slide._slideNum}`,
|
|
510
|
+
part: slide
|
|
511
|
+
})),
|
|
512
|
+
...layouts.map((layout, idx) => ({
|
|
513
|
+
label: `Slide layout ${idx + 1}`,
|
|
514
|
+
part: layout
|
|
515
|
+
})),
|
|
516
|
+
...master ? [{
|
|
517
|
+
label: "The slide master",
|
|
518
|
+
part: master
|
|
519
|
+
}] : []
|
|
520
|
+
];
|
|
521
|
+
for (const { label, part } of sources) for (const rel of part._rels) {
|
|
522
|
+
if (!isHyperlinkRel(rel) || rel.data !== "slide") continue;
|
|
523
|
+
if (!(Number(rel.Target) > slides.length)) continue;
|
|
524
|
+
throw new InvalidOptionError("slide/link-past-last-slide", `${label} links to slide ${String(rel.Target)}, but the deck has ${slides.length} slide${slides.length === 1 ? "" : "s"}.`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Step 2: load and base64-encode every media rel on the given targets, populating `rel.data`.
|
|
529
|
+
* @param {ReadonlyArray<PresSlideInternal | SlideLayoutInternal | SlideMasterInternal>} targets - media-bearing targets (slides, layouts, master)
|
|
530
|
+
* @param {RuntimeAdapter} runtime - runtime adapter used to load media
|
|
531
|
+
* @param {'throw' | 'placeholder'} onMediaError - failure policy for unloadable media
|
|
532
|
+
*/
|
|
533
|
+
async function encodeMediaForTargets(targets, runtime, onMediaError) {
|
|
534
|
+
const promises = [];
|
|
535
|
+
for (const target of targets) promises.push(...encodeSlideMediaRels(target, runtime, onMediaError));
|
|
536
|
+
await Promise.all(promises);
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Step 1: seed each slide with the layout placeholders it leaves empty.
|
|
540
|
+
* @param {PresSlideInternal[]} slides - the slides to prepare
|
|
541
|
+
*/
|
|
542
|
+
function backfillPlaceholders(slides) {
|
|
543
|
+
for (const slide of slides) if (slide._slideLayout) addPlaceholdersToSlideLayouts(slide);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Step 3: bake measured text fit.
|
|
547
|
+
* @param {PresSlideInternal[]} slides - the slides to prepare
|
|
548
|
+
* @param {FontMetricsRegistry} fontMetrics - registered font metrics; an empty registry skips the fit bake
|
|
549
|
+
* @returns the step that restores the authored values; call it once the slide XML is built
|
|
550
|
+
*/
|
|
551
|
+
function bakeMeasuredFit(slides, fontMetrics) {
|
|
552
|
+
return applyMeasuredFit(slides, fontMetrics);
|
|
553
|
+
}
|
|
554
|
+
//#endregion
|
|
555
|
+
//#region src/gen/anim/animation.ts
|
|
556
|
+
/**
|
|
557
|
+
* Resolved animation target's shape id (`spid`), or `null` when it cannot be resolved — in which
|
|
558
|
+
* case the effect is dropped, so every `null` warns rather than leaving the animation silently
|
|
559
|
+
* missing from the deck.
|
|
560
|
+
*
|
|
561
|
+
* `objectName` resolves through `shapeIds`, which covers group children: they are `<p:cNvPr>`-named
|
|
562
|
+
* on the slide and animate like any other shape, but are not in `_slideObjects`, so the old lookup
|
|
563
|
+
* there dropped every animation targeting one. The name goes in raw, and `resolveObjectNameToId`
|
|
564
|
+
* compares it with the raw name the slide object stores, so the warning below quotes the same
|
|
565
|
+
* spelling the caller passed.
|
|
566
|
+
*
|
|
567
|
+
* `shapeIndex` is a 0-based index into the top-level objects that RENDER, which is the same
|
|
568
|
+
* sequence `collectSlideShapeIds` allocates along; group children take ids past that range. It
|
|
569
|
+
* counted `_slideObjects` itself, and four of that array's members draw nothing — so `addNotes`
|
|
570
|
+
* before the first shape shifted every index by one and `shapeIndex: 0` addressed the notes.
|
|
571
|
+
* An index outside `[0, renderedCount)` would emit a `<p:spTgt spid>` naming no shape on the
|
|
572
|
+
* slide — a dangling spid PowerPoint reports as a repair (0x80070570) — so it warns and drops,
|
|
573
|
+
* exactly like an unresolvable `objectName` does.
|
|
574
|
+
* @param shapeIds - the slide's shape ids, from `collectSlideShapeIds`
|
|
575
|
+
* @param slideObjects - the slide's top-level objects, which a `shapeIndex` addresses
|
|
576
|
+
* @param anim - the animation to resolve
|
|
577
|
+
* @returns the target's `<p:cNvPr>` id, or `null`
|
|
578
|
+
*/
|
|
579
|
+
function resolveAnimationSpid(shapeIds, slideObjects, anim) {
|
|
580
|
+
if (typeof anim.shapeIndex === "number") {
|
|
581
|
+
const targets = renderedSlideObjects(slideObjects);
|
|
582
|
+
const target = targets[anim.shapeIndex];
|
|
583
|
+
if (target) return shapeIds.get(target) ?? null;
|
|
584
|
+
warn("animation/target-index-out-of-range", `addAnimation: shapeIndex ${anim.shapeIndex} is out of range (slide has ${targets.length} top-level shape(s)), so its "${anim.preset}" effect was dropped.`);
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
if (anim.objectName) {
|
|
588
|
+
const id = resolveObjectNameToId(shapeIds, anim.objectName);
|
|
589
|
+
if (id !== null) return id;
|
|
590
|
+
warn("animation/target-not-found", `addAnimation: no object named "${anim.objectName}" on the slide, so its "${anim.preset}" effect was dropped.`);
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
warn("animation/target-missing", `addAnimation: the "${anim.preset}" effect names no target (pass shapeIndex or objectName), so it was dropped.`);
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Whether the effect names a preset this module has a template for. The `preset` type admits only
|
|
598
|
+
* those, but a JavaScript caller can pass any string, so an unknown one warns and the effect is
|
|
599
|
+
* dropped before its target is resolved, like an effect whose target cannot be found.
|
|
600
|
+
* @param anim - the animation to check
|
|
601
|
+
* @returns `true` when the preset can be written
|
|
602
|
+
*/
|
|
603
|
+
function hasKnownAnimationPreset(anim) {
|
|
604
|
+
if (Object.hasOwn(ANIM_PRESETS, anim.preset)) return true;
|
|
605
|
+
warn("animation/unknown-preset", `addAnimation: "${String(anim.preset)}" is not a preset this library writes (${Object.keys(ANIM_PRESETS).join(", ")}), so the effect was dropped.`);
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
const ANIM_SET_VISIBLE = (spid, next) => el("p:set", null, [raw(el("p:cBhvr", null, [
|
|
609
|
+
raw(el("p:cTn", {
|
|
610
|
+
id: next(),
|
|
611
|
+
dur: 1,
|
|
612
|
+
fill: "hold"
|
|
613
|
+
}, raw(el("p:stCondLst", null, raw(voidEl("p:cond", { delay: 0 })))))),
|
|
614
|
+
raw(el("p:tgtEl", null, raw(voidEl("p:spTgt", { spid })))),
|
|
615
|
+
raw(el("p:attrNameLst", null, raw(el("p:attrName", null, "style.visibility"))))
|
|
616
|
+
])), raw(el("p:to", null, raw(voidEl("p:strVal", { val: "visible" }))))]);
|
|
617
|
+
const ANIM_SET_HIDDEN = (spid, dur, next) => el("p:set", null, [raw(el("p:cBhvr", null, [
|
|
618
|
+
raw(el("p:cTn", {
|
|
619
|
+
id: next(),
|
|
620
|
+
dur: 1,
|
|
621
|
+
fill: "hold"
|
|
622
|
+
}, raw(el("p:stCondLst", null, raw(voidEl("p:cond", { delay: Math.max(0, dur - 1) })))))),
|
|
623
|
+
raw(el("p:tgtEl", null, raw(voidEl("p:spTgt", { spid })))),
|
|
624
|
+
raw(el("p:attrNameLst", null, raw(el("p:attrName", null, "style.visibility"))))
|
|
625
|
+
])), raw(el("p:to", null, raw(voidEl("p:strVal", { val: "hidden" }))))]);
|
|
626
|
+
/** `p:animEffect` filter transition (fade/wipe(down)/…), shared by entrance and exit presets. */
|
|
627
|
+
const ANIM_EFFECT = (transition, filter, spid, dur, next) => el("p:animEffect", {
|
|
628
|
+
transition,
|
|
629
|
+
filter
|
|
630
|
+
}, raw(el("p:cBhvr", null, [raw(voidEl("p:cTn", {
|
|
631
|
+
id: next(),
|
|
632
|
+
dur
|
|
633
|
+
})), raw(el("p:tgtEl", null, raw(voidEl("p:spTgt", { spid }))))])));
|
|
634
|
+
const ANIM_FADE = (transition, spid, dur, next) => ANIM_EFFECT(transition, "fade", spid, dur, next);
|
|
635
|
+
/**
|
|
636
|
+
* One `ppt_x`/`ppt_y` motion `<p:anim>` for Fly In (entrance) / Fly Out (exit).
|
|
637
|
+
* PowerPoint authors the two directions differently (captured in
|
|
638
|
+
* `slide-animation-presets.oracle.json`): the entrance form references the
|
|
639
|
+
* hashed run-time variables (`#ppt_x`) and holds (`fill="hold"`) starting
|
|
640
|
+
* off-screen and ending in place; the exit form uses the bare variables
|
|
641
|
+
* (`ppt_x`, no `#`), omits `fill="hold"`, and ends off-screen.
|
|
642
|
+
*/
|
|
643
|
+
const ANIM_FLY_AXIS = (direction, axis, spid, dur, next) => {
|
|
644
|
+
const h = direction === "in" ? "#" : "";
|
|
645
|
+
let from;
|
|
646
|
+
let to;
|
|
647
|
+
if (axis === "x") {
|
|
648
|
+
from = `${h}ppt_x`;
|
|
649
|
+
to = `${h}ppt_x`;
|
|
650
|
+
} else if (direction === "in") {
|
|
651
|
+
from = `1+${h}ppt_h/2`;
|
|
652
|
+
to = `${h}ppt_y`;
|
|
653
|
+
} else {
|
|
654
|
+
from = `${h}ppt_y`;
|
|
655
|
+
to = `1+${h}ppt_h/2`;
|
|
656
|
+
}
|
|
657
|
+
const fill = direction === "in" ? "hold" : null;
|
|
658
|
+
return el("p:anim", {
|
|
659
|
+
calcmode: "lin",
|
|
660
|
+
valueType: "num"
|
|
661
|
+
}, [raw(el("p:cBhvr", { additive: "base" }, [
|
|
662
|
+
raw(voidEl("p:cTn", {
|
|
663
|
+
id: next(),
|
|
664
|
+
dur,
|
|
665
|
+
fill
|
|
666
|
+
})),
|
|
667
|
+
raw(el("p:tgtEl", null, raw(voidEl("p:spTgt", { spid })))),
|
|
668
|
+
raw(el("p:attrNameLst", null, raw(el("p:attrName", null, `ppt_${axis}`))))
|
|
669
|
+
])), raw(el("p:tavLst", null, [raw(el("p:tav", { tm: 0 }, raw(el("p:val", null, raw(voidEl("p:strVal", { val: from })))))), raw(el("p:tav", { tm: 1e5 }, raw(el("p:val", null, raw(voidEl("p:strVal", { val: to }))))))]))]);
|
|
670
|
+
};
|
|
671
|
+
const ANIM_PRESETS = {
|
|
672
|
+
fadeIn: {
|
|
673
|
+
presetID: 10,
|
|
674
|
+
presetClass: "entr",
|
|
675
|
+
presetSubtype: 0,
|
|
676
|
+
defaultDurationMs: 500,
|
|
677
|
+
behaviors: (spid, dur, next) => ANIM_SET_VISIBLE(spid, next) + ANIM_FADE("in", spid, dur, next)
|
|
678
|
+
},
|
|
679
|
+
flyIn: {
|
|
680
|
+
presetID: 2,
|
|
681
|
+
presetClass: "entr",
|
|
682
|
+
presetSubtype: 4,
|
|
683
|
+
defaultDurationMs: 500,
|
|
684
|
+
behaviors: (spid, dur, next) => ANIM_SET_VISIBLE(spid, next) + ANIM_FLY_AXIS("in", "x", spid, dur, next) + ANIM_FLY_AXIS("in", "y", spid, dur, next)
|
|
685
|
+
},
|
|
686
|
+
appear: {
|
|
687
|
+
presetID: 1,
|
|
688
|
+
presetClass: "entr",
|
|
689
|
+
presetSubtype: 0,
|
|
690
|
+
defaultDurationMs: 500,
|
|
691
|
+
behaviors: (spid, _dur, next) => ANIM_SET_VISIBLE(spid, next)
|
|
692
|
+
},
|
|
693
|
+
wipe: {
|
|
694
|
+
presetID: 22,
|
|
695
|
+
presetClass: "entr",
|
|
696
|
+
presetSubtype: 4,
|
|
697
|
+
defaultDurationMs: 500,
|
|
698
|
+
behaviors: (spid, dur, next) => ANIM_SET_VISIBLE(spid, next) + ANIM_EFFECT("in", "wipe(down)", spid, dur, next)
|
|
699
|
+
},
|
|
700
|
+
grow: {
|
|
701
|
+
presetID: 6,
|
|
702
|
+
presetClass: "emph",
|
|
703
|
+
presetSubtype: 0,
|
|
704
|
+
defaultDurationMs: 2e3,
|
|
705
|
+
behaviors: (spid, dur, next) => el("p:animScale", null, [raw(el("p:cBhvr", null, [raw(voidEl("p:cTn", {
|
|
706
|
+
id: next(),
|
|
707
|
+
dur,
|
|
708
|
+
fill: "hold"
|
|
709
|
+
})), raw(el("p:tgtEl", null, raw(voidEl("p:spTgt", { spid }))))])), raw(voidEl("p:by", {
|
|
710
|
+
x: 15e4,
|
|
711
|
+
y: 15e4
|
|
712
|
+
}))])
|
|
713
|
+
},
|
|
714
|
+
spin: {
|
|
715
|
+
presetID: 8,
|
|
716
|
+
presetClass: "emph",
|
|
717
|
+
presetSubtype: 0,
|
|
718
|
+
defaultDurationMs: 2e3,
|
|
719
|
+
behaviors: (spid, dur, next) => el("p:animRot", { by: 216e5 }, [raw(el("p:cBhvr", null, [
|
|
720
|
+
raw(voidEl("p:cTn", {
|
|
721
|
+
id: next(),
|
|
722
|
+
dur,
|
|
723
|
+
fill: "hold"
|
|
724
|
+
})),
|
|
725
|
+
raw(el("p:tgtEl", null, raw(voidEl("p:spTgt", { spid })))),
|
|
726
|
+
raw(el("p:attrNameLst", null, raw(el("p:attrName", null, "r"))))
|
|
727
|
+
]))])
|
|
728
|
+
},
|
|
729
|
+
fadeOut: {
|
|
730
|
+
presetID: 10,
|
|
731
|
+
presetClass: "exit",
|
|
732
|
+
presetSubtype: 0,
|
|
733
|
+
defaultDurationMs: 500,
|
|
734
|
+
behaviors: (spid, dur, next) => ANIM_FADE("out", spid, dur, next) + ANIM_SET_HIDDEN(spid, dur, next)
|
|
735
|
+
},
|
|
736
|
+
flyOut: {
|
|
737
|
+
presetID: 2,
|
|
738
|
+
presetClass: "exit",
|
|
739
|
+
presetSubtype: 4,
|
|
740
|
+
defaultDurationMs: 500,
|
|
741
|
+
behaviors: (spid, dur, next) => ANIM_FLY_AXIS("out", "x", spid, dur, next) + ANIM_FLY_AXIS("out", "y", spid, dur, next) + ANIM_SET_HIDDEN(spid, dur, next)
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
const ANIM_NODE_TYPE = {
|
|
745
|
+
onClick: "clickEffect",
|
|
746
|
+
withPrevious: "withEffect",
|
|
747
|
+
afterPrevious: "afterEffect"
|
|
748
|
+
};
|
|
749
|
+
/**
|
|
750
|
+
* Assemble the `mainSeq` from preset effects. Effects are grouped into click
|
|
751
|
+
* steps: an `onClick` effect opens a new click group; `afterPrevious` opens a new
|
|
752
|
+
* sub-step within it, starting when the sub-step before it ends; `withPrevious`
|
|
753
|
+
* joins the current sub-step. This reproduces PowerPoint's interactive build tree.
|
|
754
|
+
*
|
|
755
|
+
* A sub-step's delay counts from the click, not from the sub-step before it, and that sub-step
|
|
756
|
+
* ends with its longest effect: PowerPoint writes 0, 500, 1250 and 3250 ms for effects of 500,
|
|
757
|
+
* 750 and 1000 ms, a 2000 ms `withPrevious` beside the third, then a fourth.
|
|
758
|
+
*/
|
|
759
|
+
function buildAnimationSeq(animations, next) {
|
|
760
|
+
const groups = [];
|
|
761
|
+
for (const entry of animations) {
|
|
762
|
+
const meta = ANIM_PRESETS[entry.anim.preset];
|
|
763
|
+
if (!meta) continue;
|
|
764
|
+
const trigger = entry.anim.trigger ?? "onClick";
|
|
765
|
+
const duration = typeof entry.anim.durationMs === "number" ? entry.anim.durationMs : meta.defaultDurationMs;
|
|
766
|
+
const subs = groups[groups.length - 1]?.subs;
|
|
767
|
+
const lastSub = subs?.[subs.length - 1];
|
|
768
|
+
if (trigger === "onClick" || !subs || !lastSub) groups.push({ subs: [{
|
|
769
|
+
delay: 0,
|
|
770
|
+
longest: duration,
|
|
771
|
+
effects: [entry]
|
|
772
|
+
}] });
|
|
773
|
+
else if (trigger === "afterPrevious") subs.push({
|
|
774
|
+
delay: lastSub.delay + lastSub.longest,
|
|
775
|
+
longest: duration,
|
|
776
|
+
effects: [entry]
|
|
777
|
+
});
|
|
778
|
+
else {
|
|
779
|
+
lastSub.effects.push(entry);
|
|
780
|
+
lastSub.longest = Math.max(lastSub.longest, duration);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
const emitEffect = (entry) => {
|
|
784
|
+
const meta = ANIM_PRESETS[entry.anim.preset];
|
|
785
|
+
if (!meta) return "";
|
|
786
|
+
const nodeType = ANIM_NODE_TYPE[entry.anim.trigger ?? "onClick"];
|
|
787
|
+
const duration = typeof entry.anim.durationMs === "number" ? entry.anim.durationMs : meta.defaultDurationMs;
|
|
788
|
+
const effectId = next();
|
|
789
|
+
const behaviors = meta.behaviors(entry.spid, duration, next);
|
|
790
|
+
return el("p:par", null, raw(el("p:cTn", {
|
|
791
|
+
id: effectId,
|
|
792
|
+
presetID: meta.presetID,
|
|
793
|
+
presetClass: meta.presetClass,
|
|
794
|
+
presetSubtype: meta.presetSubtype,
|
|
795
|
+
fill: "hold",
|
|
796
|
+
grpId: 0,
|
|
797
|
+
nodeType
|
|
798
|
+
}, [raw(el("p:stCondLst", null, raw(voidEl("p:cond", { delay: 0 })))), raw(el("p:childTnLst", null, raw(behaviors)))])));
|
|
799
|
+
};
|
|
800
|
+
const emitSub = (sub) => {
|
|
801
|
+
const subId = next();
|
|
802
|
+
const effects = sub.effects.map(emitEffect).join("");
|
|
803
|
+
return el("p:par", null, raw(el("p:cTn", {
|
|
804
|
+
id: subId,
|
|
805
|
+
fill: "hold"
|
|
806
|
+
}, [raw(el("p:stCondLst", null, raw(voidEl("p:cond", { delay: sub.delay })))), raw(el("p:childTnLst", null, raw(effects)))])));
|
|
807
|
+
};
|
|
808
|
+
const emitGroup = (group) => {
|
|
809
|
+
const groupId = next();
|
|
810
|
+
const subs = group.subs.map(emitSub).join("");
|
|
811
|
+
return el("p:par", null, raw(el("p:cTn", {
|
|
812
|
+
id: groupId,
|
|
813
|
+
fill: "hold"
|
|
814
|
+
}, [raw(el("p:stCondLst", null, raw(voidEl("p:cond", { delay: "indefinite" })))), raw(el("p:childTnLst", null, raw(subs)))])));
|
|
815
|
+
};
|
|
816
|
+
const clickGroups = groups.map(emitGroup).join("");
|
|
817
|
+
return el("p:seq", {
|
|
818
|
+
concurrent: 1,
|
|
819
|
+
nextAc: "seek"
|
|
820
|
+
}, [
|
|
821
|
+
raw(el("p:cTn", {
|
|
822
|
+
id: 2,
|
|
823
|
+
dur: "indefinite",
|
|
824
|
+
nodeType: "mainSeq"
|
|
825
|
+
}, raw(el("p:childTnLst", null, raw(clickGroups))))),
|
|
826
|
+
raw(el("p:prevCondLst", null, raw(el("p:cond", {
|
|
827
|
+
evt: "onPrev",
|
|
828
|
+
delay: 0
|
|
829
|
+
}, raw(el("p:tgtEl", null, raw(voidEl("p:sldTgt")))))))),
|
|
830
|
+
raw(el("p:nextCondLst", null, raw(el("p:cond", {
|
|
831
|
+
evt: "onNext",
|
|
832
|
+
delay: 0
|
|
833
|
+
}, raw(el("p:tgtEl", null, raw(voidEl("p:sldTgt"))))))))
|
|
834
|
+
]);
|
|
835
|
+
}
|
|
836
|
+
/** One `<p:bldP>` per animated shape, in order of first appearance. */
|
|
837
|
+
function buildBldList(animations) {
|
|
838
|
+
const seen = /* @__PURE__ */ new Set();
|
|
839
|
+
const bldPs = [];
|
|
840
|
+
for (const { spid } of animations) {
|
|
841
|
+
if (seen.has(spid)) continue;
|
|
842
|
+
seen.add(spid);
|
|
843
|
+
bldPs.push(voidEl("p:bldP", {
|
|
844
|
+
spid,
|
|
845
|
+
grpId: 0
|
|
846
|
+
}));
|
|
847
|
+
}
|
|
848
|
+
return el("p:bldLst", null, raw(bldPs.join("")));
|
|
849
|
+
}
|
|
850
|
+
//#endregion
|
|
851
|
+
//#region src/gen/anim/timing.ts
|
|
852
|
+
/**
|
|
853
|
+
* Build the slide-level `<p:timing>` tree that makes embedded media loop.
|
|
854
|
+
* - PowerPoint stores playback looping as `repeatCount` on the media node's `<p:cTn>`
|
|
855
|
+
* (`indefinite` for "Loop until Stopped", or `N*1000` for a finite N plays), inside
|
|
856
|
+
* the slide timing tree rather than on the `<p:pic>` itself.
|
|
857
|
+
* - A slide has at most one `<p:timing>`; all looping media share its `tmRoot` node.
|
|
858
|
+
* - Audio loops via `<p:audio>`, video via `<p:video>` (both `CT_TLCommonMediaNodeData`).
|
|
859
|
+
* - The media node targets the picture by `spid` (its `<p:cNvPr>` id = slide-object index + 2).
|
|
860
|
+
* @param {PresSlideInternal} slide - the slide to inspect for looping media
|
|
861
|
+
* @returns {string} the `<p:timing>` XML, or `''` when no media loops
|
|
862
|
+
*/
|
|
863
|
+
function slideTimingToXml(slide) {
|
|
864
|
+
const loopMedia = slide._slideObjects.filter((obj) => obj._type === "media" && obj.mtype !== "online" && typeof obj.mediaRid === "number" && (obj.loop === true || typeof obj.loopCount === "number" && obj.loopCount > 0));
|
|
865
|
+
const shapeIds = collectSlideShapeIds(slide._slideObjects);
|
|
866
|
+
const animations = (slide._animations ?? []).filter((anim) => hasKnownAnimationPreset(anim)).map((anim) => ({
|
|
867
|
+
anim,
|
|
868
|
+
spid: resolveAnimationSpid(shapeIds, slide._slideObjects, anim)
|
|
869
|
+
})).filter((entry) => entry.spid !== null);
|
|
870
|
+
const mediaNode = (obj, nodeId) => {
|
|
871
|
+
const spid = shapeIds.get(obj) ?? 2;
|
|
872
|
+
const repeatCount = obj.loop === true ? "indefinite" : String(Math.round(obj.loopCount * 1e3));
|
|
873
|
+
const mediaTag = obj.mtype === "audio" ? "p:audio" : "p:video";
|
|
874
|
+
return el(mediaTag, null, raw(el("p:cMediaNode", null, [raw(el("p:cTn", {
|
|
875
|
+
id: nodeId,
|
|
876
|
+
repeatCount,
|
|
877
|
+
fill: "hold",
|
|
878
|
+
display: 0
|
|
879
|
+
}, raw(el("p:stCondLst", null, raw(voidEl("p:cond", { delay: "indefinite" })))))), raw(el("p:tgtEl", null, raw(voidEl("p:spTgt", { spid }))))])));
|
|
880
|
+
};
|
|
881
|
+
if (animations.length === 0) {
|
|
882
|
+
if (loopMedia.length === 0) return "";
|
|
883
|
+
let nodeId = 1;
|
|
884
|
+
const mediaNodes = loopMedia.map((obj) => mediaNode(obj, nodeId += 1)).join("");
|
|
885
|
+
return el("p:timing", null, raw(el("p:tnLst", null, raw(el("p:par", null, raw(el("p:cTn", {
|
|
886
|
+
id: 1,
|
|
887
|
+
dur: "indefinite",
|
|
888
|
+
restart: "never",
|
|
889
|
+
nodeType: "tmRoot"
|
|
890
|
+
}, raw(el("p:childTnLst", null, raw(mediaNodes))))))))));
|
|
891
|
+
}
|
|
892
|
+
let id = 2;
|
|
893
|
+
const next = () => id += 1;
|
|
894
|
+
const seq = buildAnimationSeq(animations, next);
|
|
895
|
+
const mediaNodes = loopMedia.map((obj) => mediaNode(obj, next())).join("");
|
|
896
|
+
const bldLst = buildBldList(animations);
|
|
897
|
+
return el("p:timing", null, [raw(el("p:tnLst", null, raw(el("p:par", null, raw(el("p:cTn", {
|
|
898
|
+
id: 1,
|
|
899
|
+
dur: "indefinite",
|
|
900
|
+
restart: "never",
|
|
901
|
+
nodeType: "tmRoot"
|
|
902
|
+
}, raw(el("p:childTnLst", null, raw(seq + mediaNodes))))))))), raw(bldLst)]);
|
|
903
|
+
}
|
|
904
|
+
//#endregion
|
|
905
|
+
//#region src/gen/anim/transition.ts
|
|
906
|
+
/**
|
|
907
|
+
* The attributes of a transition's type element, from the caller's `variant`.
|
|
908
|
+
*
|
|
909
|
+
* Each `variant` key is written as an attribute name, so a key the element does not declare would
|
|
910
|
+
* be an attribute the schema does not allow there, and a key that is not a name at all would be
|
|
911
|
+
* markup. Only the attributes the element declares are written, and the rest warn.
|
|
912
|
+
*/
|
|
913
|
+
function transitionVariantAttrs(type, variant) {
|
|
914
|
+
if (!variant) return null;
|
|
915
|
+
const allowed = TRANSITION_VARIANT_ATTRIBUTES[type];
|
|
916
|
+
const attrs = {};
|
|
917
|
+
for (const [name, value] of Object.entries(variant)) {
|
|
918
|
+
if (allowed.includes(name)) {
|
|
919
|
+
attrs[name] = value;
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
warnOnce("transition/unknown-variant", `slide transition \`${type}\` variant \`${name}\` is not an attribute of <p:${type}> and is ignored — ${allowed.length > 0 ? `use ${allowed.join(", ")}` : "it takes none"}.`, {
|
|
923
|
+
received: name,
|
|
924
|
+
valid: allowed
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
return attrs;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Build the `p:sndAc` sound-action child of `p:transition` (positioned after the
|
|
931
|
+
* transition-type element). A start sound is `<p:stSnd [loop="1"]><p:snd r:embed
|
|
932
|
+
* name/></p:stSnd>` referencing the embedded audio by `soundRId`; the stop-previous form is
|
|
933
|
+
* `<p:endSnd/>` (no rel). Returns `''` when the transition has no sound, or a start sound was
|
|
934
|
+
* registered under no relationship on this slide.
|
|
935
|
+
*/
|
|
936
|
+
function transitionSoundToXml(transition, soundRId) {
|
|
937
|
+
const sound = transition.sound;
|
|
938
|
+
if (!sound) return "";
|
|
939
|
+
if (sound.stopPrevious) return el("p:sndAc", null, raw(voidEl("p:endSnd")));
|
|
940
|
+
if (soundRId === void 0) return "";
|
|
941
|
+
return el("p:sndAc", null, raw(el("p:stSnd", { loop: xsdBoolIfTrue(sound.loop) }, raw(voidEl("p:snd", {
|
|
942
|
+
"r:embed": `rId${soundRId}`,
|
|
943
|
+
name: sound.name || null
|
|
944
|
+
})))));
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Build the slide-show transition tree (`p:transition`), positioned in `CT_Slide`
|
|
948
|
+
* between `p:clrMapOvr` and `p:timing`. Emits PowerPoint's `mc:AlternateContent`
|
|
949
|
+
* form (a `p14` Choice carrying the exact `p14:dur`, plus a base `mc:Fallback`)
|
|
950
|
+
* when `durationMs` is set, and the bare `<p:transition>` otherwise. See
|
|
951
|
+
* `docs/contributing/design/animations.md`.
|
|
952
|
+
* @param slide - the slide whose transition is emitted
|
|
953
|
+
* @param soundRId - the relationship id this slide's start sound was registered under, if one was.
|
|
954
|
+
* It is the slide's, not the transition's: one transition object can sit on several slides, each
|
|
955
|
+
* declaring the sound under an id of its own.
|
|
956
|
+
* @returns {string} the transition XML, or `''` when the slide has no transition the writer can emit
|
|
957
|
+
*/
|
|
958
|
+
function slideTransitionToXml(slide, soundRId) {
|
|
959
|
+
const transition = slide.transition;
|
|
960
|
+
if (!transition?.type) return "";
|
|
961
|
+
const type = checkEnumOrWarn(transition.type, TRANSITION_TYPES, "transition/unknown-type", "slide transition `type`");
|
|
962
|
+
if (!type) return "";
|
|
963
|
+
const typeEl = voidEl(`p:${type}`, transitionVariantAttrs(type, transition.variant));
|
|
964
|
+
const sndAc = transitionSoundToXml(transition, soundRId);
|
|
965
|
+
const hasDuration = typeof transition.durationMs === "number" && Number.isFinite(transition.durationMs);
|
|
966
|
+
const baseAttrs = {
|
|
967
|
+
spd: (transition.speed ?? (hasDuration ? transitionSpeedForDuration(transition.durationMs) : null)) || null,
|
|
968
|
+
advClick: transition.advanceOnClick === false ? "0" : null,
|
|
969
|
+
advTm: typeof transition.advanceAfterMs === "number" ? Math.round(transition.advanceAfterMs) : null
|
|
970
|
+
};
|
|
971
|
+
if (!hasDuration) return el("p:transition", baseAttrs, [raw(typeEl), raw(sndAc)]);
|
|
972
|
+
const dur = Math.round(transition.durationMs);
|
|
973
|
+
return alternateContentEl({
|
|
974
|
+
requires: {
|
|
975
|
+
prefix: "p14",
|
|
976
|
+
uri: OOXML_NS.p14
|
|
977
|
+
},
|
|
978
|
+
choice: el("p:transition", {
|
|
979
|
+
...baseAttrs,
|
|
980
|
+
"p14:dur": dur
|
|
981
|
+
}, [raw(typeEl), raw(sndAc)]),
|
|
982
|
+
fallback: el("p:transition", baseAttrs, [raw(typeEl), raw(sndAc)])
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
//#endregion
|
|
986
|
+
//#region src/gen/slide/object.ts
|
|
987
|
+
/** The four axes that make up an explicit group frame. All or nothing — see `givenGroupFrameAxes`. */
|
|
988
|
+
const GROUP_FRAME_AXES = [
|
|
989
|
+
"x",
|
|
990
|
+
"y",
|
|
991
|
+
"w",
|
|
992
|
+
"h"
|
|
993
|
+
];
|
|
994
|
+
/**
|
|
995
|
+
* Which of `x`/`y`/`w`/`h` a group's caller actually supplied.
|
|
996
|
+
*
|
|
997
|
+
* A group frame is explicit only when **all four** are given; anything less falls back to
|
|
998
|
+
* auto-bounds on every axis. Partial frames used to be honoured per-axis, which let the unset ones
|
|
999
|
+
* take the shared per-object defaults (`x=0`, `y=0`, `cx=75%` of the layout width, `cy=0`) — so
|
|
1000
|
+
* `addGroup([rect], { x: 5, y: 2 })` emitted a zero-height group whose width was a silent
|
|
1001
|
+
* slide-width fraction, and every child of it then re-read as `null` through the read path's
|
|
1002
|
+
* degenerate-`chExt` guard.
|
|
1003
|
+
*
|
|
1004
|
+
* Per-axis fallback was the other candidate, but it cannot mean what it reads like: the writer keeps
|
|
1005
|
+
* an identity child space (`chOff/chExt == off/ext`), so a group's frame never moves or scales its
|
|
1006
|
+
* children — it only places the selection handle and the rotate pivot. `{ x: 5 }` would leave the
|
|
1007
|
+
* children where they were and put the group's box somewhere they are not. Falling back whole, with
|
|
1008
|
+
* a warning, keeps the group box around its content and says so out loud.
|
|
1009
|
+
* @param options - the group object's options
|
|
1010
|
+
* @returns the supplied axis names, in `x`/`y`/`w`/`h` order
|
|
1011
|
+
*/
|
|
1012
|
+
const givenGroupFrameAxes = (options) => GROUP_FRAME_AXES.filter((axis) => typeof options[axis] !== "undefined");
|
|
1013
|
+
/**
|
|
1014
|
+
* Whether a group's frame is fully explicit (all four of `x`/`y`/`w`/`h`), and so should be used
|
|
1015
|
+
* verbatim instead of the children's bounding box. See `givenGroupFrameAxes`.
|
|
1016
|
+
* @param options - the group object's options
|
|
1017
|
+
* @returns true when every axis is supplied
|
|
1018
|
+
*/
|
|
1019
|
+
const hasCompleteGroupFrame = (options) => givenGroupFrameAxes(options).length === GROUP_FRAME_AXES.length;
|
|
1020
|
+
/**
|
|
1021
|
+
* Normalize one axis of a placement box so its extent is never negative.
|
|
1022
|
+
*
|
|
1023
|
+
* `<a:ext cx>`/`<a:ext cy>` are `ST_PositiveCoordinate` (ECMA-376 Part 1, `CT_PositiveSize2D`), so a
|
|
1024
|
+
* negative extent is schema-invalid. PowerPoint rejects the *whole* presentation with "The file or
|
|
1025
|
+
* directory is corrupted and unreadable" (0x80070570) and names no shape, part, or slide, while
|
|
1026
|
+
* LibreOffice renders the same package happily — so the defect is invisible until the deck reaches
|
|
1027
|
+
* PowerPoint.
|
|
1028
|
+
*
|
|
1029
|
+
* A signed delta is the natural way to write "draw from A to B" (`{ x: x0, y: y0, w: x1 - x0, h: y1 - y0 }`),
|
|
1030
|
+
* and it goes negative the moment the line runs leftward or upward. The correct encoding is the
|
|
1031
|
+
* normalized bounding box plus a flip, which is what `addConnector` already derives from its own
|
|
1032
|
+
* endpoints (`src/gen/define/connector.ts`); this gives every other shape kind the same treatment at
|
|
1033
|
+
* the one point where every `Coord` form ('50%', '2in', a number) has been resolved to EMU.
|
|
1034
|
+
*
|
|
1035
|
+
* The caller composes `flip` onto any `flipH`/`flipV` the author set, so an explicit flip plus a
|
|
1036
|
+
* negative extent cancel out rather than double-applying.
|
|
1037
|
+
* @param off - the axis origin in EMU (`a:off` `x` or `y`)
|
|
1038
|
+
* @param ext - the axis extent in EMU (`a:ext` `cx` or `cy`), possibly negative
|
|
1039
|
+
* @returns the min-corner origin, the absolute extent, and whether the axis was mirrored
|
|
1040
|
+
*/
|
|
1041
|
+
const normalizeAxisExtent = (off, ext) => ext < 0 ? {
|
|
1042
|
+
off: off + ext,
|
|
1043
|
+
ext: -ext,
|
|
1044
|
+
flip: true
|
|
1045
|
+
} : {
|
|
1046
|
+
off,
|
|
1047
|
+
ext,
|
|
1048
|
+
flip: false
|
|
1049
|
+
};
|
|
1050
|
+
/**
|
|
1051
|
+
* Resolve the frame a slide object is drawn in.
|
|
1052
|
+
*
|
|
1053
|
+
* The render pass and a group's bounding box both need it, and they used to resolve it apart. The
|
|
1054
|
+
* bounds pass defaulted a missing `w` or `h` to 0 and skipped placeholder inheritance, while the
|
|
1055
|
+
* render pass defaulted `cx` to 75% of the slide width and inherited the placeholder's frame, so a
|
|
1056
|
+
* group holding a text box with no `w` came out narrower than the text box.
|
|
1057
|
+
*
|
|
1058
|
+
* Each axis, most specific source first: what the caller stated on this object, else what the
|
|
1059
|
+
* layout placeholder it names states, else the default (`x`, `y` and `cy` 0, `cx` 75% of the slide
|
|
1060
|
+
* width). An explicit option beats an inherited one everywhere else in this library, and a
|
|
1061
|
+
* placeholder is an inherited one: applying it after the object's own values used to throw away a
|
|
1062
|
+
* stated frame with no diagnostic.
|
|
1063
|
+
*
|
|
1064
|
+
* Inheritance happens before normalization. A negative `w`/`h` becomes a min-corner origin, an
|
|
1065
|
+
* absolute extent and a flip, never a negative `<a:ext>`, which is out of range for
|
|
1066
|
+
* `ST_PositiveCoordinate` and costs the whole package (see `normalizeAxisExtent`). Doing it after
|
|
1067
|
+
* every `Coord` form has resolved to EMU means `'-2in'` and `'-25%'` normalize like a plain
|
|
1068
|
+
* negative number, and a placeholder's extents normalize with the object's own.
|
|
1069
|
+
* @param obj - the slide object
|
|
1070
|
+
* @param slide - the slide or layout holding it
|
|
1071
|
+
* @returns the object's frame
|
|
1072
|
+
*/
|
|
1073
|
+
function resolveObjectFrame(obj, slide) {
|
|
1074
|
+
const itemOpts = obj.options ?? {};
|
|
1075
|
+
const layout = slide._presLayout;
|
|
1076
|
+
const placeholder = findLayoutPlaceholder(slide._slideLayout, itemOpts.placeholder);
|
|
1077
|
+
const phOpts = placeholder?.options ?? {};
|
|
1078
|
+
const inherited = (own, ph) => own !== void 0 ? own : ph ?? void 0;
|
|
1079
|
+
const ownX = inherited(itemOpts.x, phOpts.x);
|
|
1080
|
+
const ownY = inherited(itemOpts.y, phOpts.y);
|
|
1081
|
+
const ownW = inherited(itemOpts.w, phOpts.w);
|
|
1082
|
+
const ownH = inherited(itemOpts.h, phOpts.h);
|
|
1083
|
+
const normX = normalizeAxisExtent(ownX !== void 0 ? getSmartParseNumber(ownX, "X", layout) : 0, ownW !== void 0 ? getSmartParseNumber(ownW, "X", layout) : getSmartParseNumber("75%", "X", layout));
|
|
1084
|
+
const normY = normalizeAxisExtent(ownY !== void 0 ? getSmartParseNumber(ownY, "Y", layout) : 0, ownH !== void 0 ? getSmartParseNumber(ownH, "Y", layout) : 0);
|
|
1085
|
+
return {
|
|
1086
|
+
x: normX.off,
|
|
1087
|
+
y: normY.off,
|
|
1088
|
+
cx: normX.ext,
|
|
1089
|
+
cy: normY.ext,
|
|
1090
|
+
flipX: normX.flip,
|
|
1091
|
+
flipY: normY.flip,
|
|
1092
|
+
placeholder
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Whether this part has a real slide number to cache in a `slidenum` field.
|
|
1097
|
+
*
|
|
1098
|
+
* Only a slide does. A master leaves `_slideNum` null and a layout carries the internal 1000+
|
|
1099
|
+
* counter that keeps layout media keys from colliding with slide media — neither is a page
|
|
1100
|
+
* number, and neither belongs in the field's cached text.
|
|
1101
|
+
*/
|
|
1102
|
+
function hasRealSlideNumber(slide) {
|
|
1103
|
+
return slide._slideNum != null && slide._slideNum < 1e3;
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Warn about duplicate Selection Pane identities on this slide.
|
|
1107
|
+
*
|
|
1108
|
+
* Unique `objectName` values are what consumers (e.g. semantic manifests) rely on, so
|
|
1109
|
+
* collisions are flagged loudly. Deliberately NOT `warnOnce`: this runs per emit, and two
|
|
1110
|
+
* `write()` calls are two separate exports — a caller capturing diagnostics around the second
|
|
1111
|
+
* would otherwise be told its deck is clean because the first one had already said otherwise. Groups are recursed into: a group's children are
|
|
1112
|
+
* `<p:cNvPr>`-named on this same slide, so a child colliding with a top-level object (or with a
|
|
1113
|
+
* child of another group) is a collision the Selection Pane shows, and checking only the top
|
|
1114
|
+
* level cannot see it.
|
|
1115
|
+
*/
|
|
1116
|
+
function warnDuplicateObjectNames(slide) {
|
|
1117
|
+
const collectObjectNames = (objects) => objects.flatMap((obj) => [...typeof obj.options?.objectName === "string" ? [obj.options.objectName] : [], ...collectObjectNames(obj._groupObjects || [])]);
|
|
1118
|
+
const duplicateObjectNames = getDuplicateObjectNames(collectObjectNames(slide._slideObjects));
|
|
1119
|
+
if (duplicateObjectNames.length > 0) warn("object-name/duplicate", `duplicate objectName value(s) emitted on a single slide: ${duplicateObjectNames.join(", ")}. Selection Pane identities should be unique.`);
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* The slide's `<p:bg>`, or `''` when it has none.
|
|
1123
|
+
*
|
|
1124
|
+
* Exactly one of the three forms is emitted: an image background (a rel the media pass resolved),
|
|
1125
|
+
* a colour or gradient, or the white default that only the built-in layout gets. They are
|
|
1126
|
+
* mutually exclusive because a master carrying both a `color` and a `path` must still produce a
|
|
1127
|
+
* single `<p:bg>`.
|
|
1128
|
+
*/
|
|
1129
|
+
function slideBackgroundXml(slide) {
|
|
1130
|
+
let strSlideXml = "";
|
|
1131
|
+
if (slide._bkgdImgRid) strSlideXml += el("p:bg", null, raw(el("p:bgPr", null, [raw(el("a:blipFill", {
|
|
1132
|
+
dpi: "0",
|
|
1133
|
+
rotWithShape: "1"
|
|
1134
|
+
}, [
|
|
1135
|
+
raw(el("a:blip", { "r:embed": `rId${slide._bkgdImgRid}` }, raw(voidEl("a:lum")))),
|
|
1136
|
+
raw(voidEl("a:srcRect")),
|
|
1137
|
+
raw(STRETCH_FILL_RECT)
|
|
1138
|
+
])), raw(voidEl("a:effectLst"))])));
|
|
1139
|
+
else if (fillNamesPaint(slide.background)) strSlideXml += el("p:bg", null, raw(el("p:bgPr", null, [raw(genXmlColorSelection(slide.background)), raw(voidEl("a:effectLst"))])));
|
|
1140
|
+
else if (!slide.background && slide._name && slide._name === "DEFAULT") strSlideXml += el("p:bg", null, raw(el("p:bgRef", { idx: "1001" }, raw(voidEl("a:schemeClr", { val: "bg1" })))));
|
|
1141
|
+
return strSlideXml;
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* The slide-number placeholder shape.
|
|
1145
|
+
*
|
|
1146
|
+
* Emitted after every authored object, so its `<p:cNvPr>` id can come from the same monotonic
|
|
1147
|
+
* counter the walk has been advancing — the caller allocates it and passes it in.
|
|
1148
|
+
*
|
|
1149
|
+
* @param slide - the slide, layout or master being emitted
|
|
1150
|
+
* @param snProps - the part's slide-number properties, read but never written: a serializer
|
|
1151
|
+
* does not normalize the authored model (see the contract on `RenderContext.itemOpts`)
|
|
1152
|
+
* @param slideNumberId - the `<p:cNvPr>` id to use, already allocated by the caller
|
|
1153
|
+
*/
|
|
1154
|
+
function slideNumberPlaceholderXml(slide, snProps, slideNumberId) {
|
|
1155
|
+
let strSlideXml = "";
|
|
1156
|
+
strSlideXml += "<p:sp>";
|
|
1157
|
+
strSlideXml += " <p:nvSpPr>";
|
|
1158
|
+
strSlideXml += voidEl("p:cNvPr", {
|
|
1159
|
+
id: slideNumberId,
|
|
1160
|
+
name: "Slide Number Placeholder 0"
|
|
1161
|
+
}, { openPrefix: " " }) + el("p:cNvSpPr", null, raw(voidEl("a:spLocks", { noGrp: "1" })));
|
|
1162
|
+
strSlideXml += el("p:nvPr", null, raw(voidEl("p:ph", {
|
|
1163
|
+
type: "sldNum",
|
|
1164
|
+
sz: "quarter",
|
|
1165
|
+
idx: "4294967295"
|
|
1166
|
+
})), { openPrefix: " " });
|
|
1167
|
+
strSlideXml += " </p:nvSpPr>";
|
|
1168
|
+
strSlideXml += " <p:spPr>";
|
|
1169
|
+
strSlideXml += el("a:xfrm", null, [raw(voidEl("a:off", {
|
|
1170
|
+
x: getSmartParseNumber(snProps.x, "X", slide._presLayout),
|
|
1171
|
+
y: getSmartParseNumber(snProps.y, "Y", slide._presLayout)
|
|
1172
|
+
})), raw(voidEl("a:ext", {
|
|
1173
|
+
cx: snProps.w ? getSmartParseNumber(snProps.w, "X", slide._presLayout) : "800000",
|
|
1174
|
+
cy: snProps.h ? getSmartParseNumber(snProps.h, "Y", slide._presLayout) : "300000"
|
|
1175
|
+
}))]) + prstGeomRect({ openPrefix: " " }) + el("a:extLst", null, raw(el("a:ext", { uri: "{C572A759-6A51-4108-AA02-DFA0A04FC94B}" }, raw(voidEl("ma14:wrappingTextBoxFlag", {
|
|
1176
|
+
val: "0",
|
|
1177
|
+
"xmlns:ma14": "http://schemas.microsoft.com/office/mac/drawingml/2011/main"
|
|
1178
|
+
})))), { openPrefix: " " }) + "</p:spPr>";
|
|
1179
|
+
strSlideXml += "<p:txBody>";
|
|
1180
|
+
const snInsets = resolveInsetsEmu(snProps.margin);
|
|
1181
|
+
strSlideXml += voidEl("a:bodyPr", {
|
|
1182
|
+
lIns: snInsets?.l ?? null,
|
|
1183
|
+
tIns: snInsets?.t ?? null,
|
|
1184
|
+
rIns: snInsets?.r ?? null,
|
|
1185
|
+
bIns: snInsets?.b ?? null,
|
|
1186
|
+
anchor: resolveTextAnchor(snProps.valign)
|
|
1187
|
+
});
|
|
1188
|
+
let defRPr = "";
|
|
1189
|
+
rejectEmptyColor(snProps.color, "slideNumber color");
|
|
1190
|
+
if (snProps.fontFace || snProps.fontSize || snProps.color) {
|
|
1191
|
+
const face = snProps.fontFace;
|
|
1192
|
+
defRPr = el("a:defRPr", { sz: clampFontSizeSz(snProps.fontSize || 12) }, [
|
|
1193
|
+
snProps.color ? raw(genXmlColorSelection(snProps.color)) : null,
|
|
1194
|
+
face ? raw(voidEl("a:latin", { typeface: face })) : null,
|
|
1195
|
+
face ? raw(voidEl("a:ea", { typeface: face })) : null,
|
|
1196
|
+
face ? raw(voidEl("a:cs", { typeface: face })) : null
|
|
1197
|
+
]);
|
|
1198
|
+
}
|
|
1199
|
+
strSlideXml += el("a:lstStyle", null, raw(el("a:lvl1pPr", null, raw(defRPr))), { openPrefix: " " });
|
|
1200
|
+
const snAlignRaw = snProps.align ?? "left";
|
|
1201
|
+
const snAlign = snAlignRaw.startsWith("c") ? "ctr" : snAlignRaw.startsWith("r") ? "r" : "l";
|
|
1202
|
+
strSlideXml += el("a:p", null, [
|
|
1203
|
+
raw(voidEl("a:pPr", { algn: snAlign })),
|
|
1204
|
+
raw(el("a:fld", {
|
|
1205
|
+
id: SLDNUMFLDID,
|
|
1206
|
+
type: "slidenum"
|
|
1207
|
+
}, [raw(voidEl("a:rPr", {
|
|
1208
|
+
b: xsdBool(snProps.bold),
|
|
1209
|
+
lang: "en-US"
|
|
1210
|
+
})), raw(el("a:t", null, hasRealSlideNumber(slide) ? String(slide._slideNum) : SLDNUM_PLACEHOLDER_TEXT))])),
|
|
1211
|
+
raw(voidEl("a:endParaRPr", { lang: "en-US" }))
|
|
1212
|
+
]);
|
|
1213
|
+
strSlideXml += "</p:txBody></p:sp>";
|
|
1214
|
+
return strSlideXml;
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Transforms a slide or slideLayout to resulting XML string - Creates `ppt/slide*.xml`
|
|
1218
|
+
* @param slide - slide object created within createSlideObject
|
|
1219
|
+
* @param renderers - which renderer emits each shape family; see {@link RendererTable} for why
|
|
1220
|
+
* this arrives as an argument rather than being imported here
|
|
1221
|
+
* @return XML string with `<p:cSld>` as the root
|
|
1222
|
+
*/
|
|
1223
|
+
function slideObjectToXml(slide, renderers) {
|
|
1224
|
+
let strSlideXml = slide._name ? "<p:cSld name=\"" + encodeXmlAttrValue(slide._name) + "\">" : "<p:cSld>";
|
|
1225
|
+
warnDuplicateObjectNames(slide);
|
|
1226
|
+
strSlideXml += slideBackgroundXml(slide);
|
|
1227
|
+
const spTreeChildren = [];
|
|
1228
|
+
const shapeIds = collectSlideShapeIds(slide._slideObjects);
|
|
1229
|
+
const resolveObjBounds = (obj) => {
|
|
1230
|
+
const o = obj.options || {};
|
|
1231
|
+
if (obj._type === "group" && !hasCompleteGroupFrame(o)) {
|
|
1232
|
+
const kids = (obj._groupObjects || []).map(resolveObjBounds);
|
|
1233
|
+
if (kids.length === 0) return {
|
|
1234
|
+
x: 0,
|
|
1235
|
+
y: 0,
|
|
1236
|
+
cx: 0,
|
|
1237
|
+
cy: 0
|
|
1238
|
+
};
|
|
1239
|
+
const minX = Math.min(...kids.map((b) => b.x));
|
|
1240
|
+
const minY = Math.min(...kids.map((b) => b.y));
|
|
1241
|
+
const maxX = Math.max(...kids.map((b) => b.x + b.cx));
|
|
1242
|
+
const maxY = Math.max(...kids.map((b) => b.y + b.cy));
|
|
1243
|
+
return {
|
|
1244
|
+
x: minX,
|
|
1245
|
+
y: minY,
|
|
1246
|
+
cx: maxX - minX,
|
|
1247
|
+
cy: maxY - minY
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
const frame = resolveObjectFrame(obj, slide);
|
|
1251
|
+
if (obj._type === "image") {
|
|
1252
|
+
const { drawnW, drawnH } = resolveImageExtent(obj, slide, frame);
|
|
1253
|
+
return {
|
|
1254
|
+
x: frame.x,
|
|
1255
|
+
y: frame.y,
|
|
1256
|
+
cx: drawnW,
|
|
1257
|
+
cy: drawnH
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
return {
|
|
1261
|
+
x: frame.x,
|
|
1262
|
+
y: frame.y,
|
|
1263
|
+
cx: frame.cx,
|
|
1264
|
+
cy: frame.cy
|
|
1265
|
+
};
|
|
1266
|
+
};
|
|
1267
|
+
const renderSlideObjectXml = (slideItemObj) => {
|
|
1268
|
+
const shapeId = shapeIds.get(slideItemObj) ?? 2;
|
|
1269
|
+
let strSlideXml = "";
|
|
1270
|
+
const itemOpts = slideItemObj.options ?? {};
|
|
1271
|
+
const { x, y, cx, cy, flipX, flipY, placeholder: placeholderObj } = resolveObjectFrame(slideItemObj, slide);
|
|
1272
|
+
const locationAttrs = {
|
|
1273
|
+
flipH: xsdBoolIfTrue(Boolean(itemOpts.flipH) !== flipX),
|
|
1274
|
+
flipV: xsdBoolIfTrue(Boolean(itemOpts.flipV) !== flipY),
|
|
1275
|
+
rot: mapStated(itemOpts.rotate, convertRotationDegrees) ?? null
|
|
1276
|
+
};
|
|
1277
|
+
const ctx = {
|
|
1278
|
+
obj: slideItemObj,
|
|
1279
|
+
shapeId,
|
|
1280
|
+
shapeIds,
|
|
1281
|
+
slide,
|
|
1282
|
+
frame: {
|
|
1283
|
+
x,
|
|
1284
|
+
y,
|
|
1285
|
+
cx,
|
|
1286
|
+
cy
|
|
1287
|
+
},
|
|
1288
|
+
placeholder: placeholderObj,
|
|
1289
|
+
locationAttrs,
|
|
1290
|
+
itemOpts
|
|
1291
|
+
};
|
|
1292
|
+
switch (slideItemObj._type) {
|
|
1293
|
+
case "chart":
|
|
1294
|
+
case "connector":
|
|
1295
|
+
case "image":
|
|
1296
|
+
case "media":
|
|
1297
|
+
case "model3d":
|
|
1298
|
+
case "oleObject":
|
|
1299
|
+
case "placeholder":
|
|
1300
|
+
case "table":
|
|
1301
|
+
case "text":
|
|
1302
|
+
case "zoom": {
|
|
1303
|
+
const render = renderers[slideItemObj._type];
|
|
1304
|
+
if (!render) throw new InternalError("slide/object-type-not-routed", `slideObjectToXml: no renderer registered for slide object type "${slideItemObj._type}"`);
|
|
1305
|
+
strSlideXml += render(ctx);
|
|
1306
|
+
break;
|
|
1307
|
+
}
|
|
1308
|
+
case "group": {
|
|
1309
|
+
const groupChildren = slideItemObj._groupObjects || [];
|
|
1310
|
+
let innerXml = "";
|
|
1311
|
+
groupChildren.forEach((child) => {
|
|
1312
|
+
innerXml += renderSlideObjectXml(child);
|
|
1313
|
+
});
|
|
1314
|
+
const givenAxes = givenGroupFrameAxes(itemOpts);
|
|
1315
|
+
if (givenAxes.length > 0 && givenAxes.length < GROUP_FRAME_AXES.length) {
|
|
1316
|
+
const missingAxes = GROUP_FRAME_AXES.filter((axis) => !givenAxes.includes(axis));
|
|
1317
|
+
warn("group/partial-frame", `addGroup: group "${itemOpts.objectName ?? ""}" has a partial frame (${givenAxes.join("/")} given, ${missingAxes.join("/")} missing); using auto-bounds (the bounding box of its children) instead. Pass all of x/y/w/h, or none.`);
|
|
1318
|
+
}
|
|
1319
|
+
const gb = hasCompleteGroupFrame(itemOpts) ? {
|
|
1320
|
+
x,
|
|
1321
|
+
y,
|
|
1322
|
+
cx,
|
|
1323
|
+
cy
|
|
1324
|
+
} : resolveObjBounds(slideItemObj);
|
|
1325
|
+
const gx = gb.x;
|
|
1326
|
+
const gy = gb.y;
|
|
1327
|
+
const gcx = gb.cx;
|
|
1328
|
+
const gcy = gb.cy;
|
|
1329
|
+
const grpLockXml = genXmlObjectLock("a:grpSpLocks", GROUP_SHAPE_LOCK_ATTRS, itemOpts.objectLock, itemOpts.objectName);
|
|
1330
|
+
strSlideXml += "<p:grpSp>";
|
|
1331
|
+
strSlideXml += el("p:nvGrpSpPr", null, [
|
|
1332
|
+
raw(cNvPrEl(shapeId, itemOpts.objectName, itemOpts.altText || "")),
|
|
1333
|
+
raw(elOrVoid("p:cNvGrpSpPr", null, grpLockXml)),
|
|
1334
|
+
raw(voidEl("p:nvPr"))
|
|
1335
|
+
]);
|
|
1336
|
+
strSlideXml += el("p:grpSpPr", null, raw(grpXfrmEl({
|
|
1337
|
+
x: gx,
|
|
1338
|
+
y: gy,
|
|
1339
|
+
cx: gcx,
|
|
1340
|
+
cy: gcy
|
|
1341
|
+
}, locationAttrs)));
|
|
1342
|
+
strSlideXml += innerXml;
|
|
1343
|
+
strSlideXml += "</p:grpSp>";
|
|
1344
|
+
break;
|
|
1345
|
+
}
|
|
1346
|
+
case "notes": break;
|
|
1347
|
+
case "tablecell": break;
|
|
1348
|
+
case "hyperlink": break;
|
|
1349
|
+
case "online": break;
|
|
1350
|
+
default: slideItemObj._type;
|
|
1351
|
+
}
|
|
1352
|
+
return strSlideXml;
|
|
1353
|
+
};
|
|
1354
|
+
slide._slideObjects.forEach((slideItemObj) => {
|
|
1355
|
+
spTreeChildren.push(renderSlideObjectXml(slideItemObj));
|
|
1356
|
+
});
|
|
1357
|
+
if (slide._slideNumberProps) {
|
|
1358
|
+
let maxShapeId = 1;
|
|
1359
|
+
for (const [obj, id] of shapeIds) {
|
|
1360
|
+
const lastId = id + shapeIdCount(obj) - 1;
|
|
1361
|
+
if (lastId > maxShapeId) maxShapeId = lastId;
|
|
1362
|
+
}
|
|
1363
|
+
spTreeChildren.push(slideNumberPlaceholderXml(slide, slide._slideNumberProps, maxShapeId + 1));
|
|
1364
|
+
}
|
|
1365
|
+
strSlideXml += spTreeEl(spTreeChildren);
|
|
1366
|
+
strSlideXml += "</p:cSld>";
|
|
1367
|
+
return strSlideXml;
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* The relationship id a part's default relationships are numbered from: one past the highest id
|
|
1371
|
+
* among its hyperlink, chart and media relationships.
|
|
1372
|
+
*
|
|
1373
|
+
* Exported because the master's `<p:sldLayoutId r:id>` values have to name the ids its `.rels` part
|
|
1374
|
+
* gives the layouts, and `makeXmlMaster` derived them as `_rels.length + idx + 1`. The two agreed
|
|
1375
|
+
* only while nothing registered a chart or media relationship on the master, since that count
|
|
1376
|
+
* ignores both.
|
|
1377
|
+
* @param slide - the slide, layout or master whose relationships are numbered
|
|
1378
|
+
*/
|
|
1379
|
+
function defaultRelIdStart(slide) {
|
|
1380
|
+
let lastRid = 0;
|
|
1381
|
+
for (const rel of slide._rels) lastRid = Math.max(lastRid, rel.rId);
|
|
1382
|
+
for (const rel of slide._relsChart || []) lastRid = Math.max(lastRid, rel.rId);
|
|
1383
|
+
for (const rel of slide._relsMedia || []) lastRid = Math.max(lastRid, rel.rId);
|
|
1384
|
+
return lastRid + 1;
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Transforms slide relations to XML string.
|
|
1388
|
+
* Extra relations that are not dynamic can be passed using the 2nd arg (e.g. theme relation in master file).
|
|
1389
|
+
* These relations are numbered from {@link defaultRelIdStart}.
|
|
1390
|
+
* `selfPath` is the emitting part's own path, and it is required because a relationship target
|
|
1391
|
+
* resolves against the part that states it. An internal slide link used to be written
|
|
1392
|
+
* `slide${n}.xml` whatever the part, which is right from `ppt/slides/` and names nothing from
|
|
1393
|
+
* `ppt/slideLayouts/` or `ppt/slideMasters/` — so a `hyperlink: { slide }` on an object given to
|
|
1394
|
+
* `defineSlideMaster({ objects })` wrote a dangling relationship.
|
|
1395
|
+
* @param {PresSlideInternal | SlideLayoutInternal | SlideMasterInternal} slide - slide, layout or master whose relations are being transformed
|
|
1396
|
+
* @param {{ target: string; type: string }[]} defaultRels - array of default relations
|
|
1397
|
+
* @param {string} selfPath - the emitting part's own path, e.g. `ppt/slideLayouts/slideLayout2.xml`
|
|
1398
|
+
* @return {string} XML
|
|
1399
|
+
*/
|
|
1400
|
+
function slideObjectRelationsToXml(slide, defaultRels, selfPath) {
|
|
1401
|
+
const rels = [];
|
|
1402
|
+
/**
|
|
1403
|
+
* The Targets emitted so far *from the media loop below*, which is the only place the
|
|
1404
|
+
* question is asked: a media item produces TWO rels sharing one Target, and the second is
|
|
1405
|
+
* told from the first by the first already being there.
|
|
1406
|
+
*
|
|
1407
|
+
* Two things about the shape. It holds raw Targets rather than escaped ones, because this
|
|
1408
|
+
* asks about the model and not about markup — the probe used to substring-scan the emitted
|
|
1409
|
+
* XML for ` Target="…"`, which forced the caller to escape by hand with the same escaper the
|
|
1410
|
+
* builder uses or the two drifted apart, and cost O(rels squared) in string scanning.
|
|
1411
|
+
*
|
|
1412
|
+
* And it is scoped to the media loop rather than to every rel. Scanning all of them meant a
|
|
1413
|
+
* hyperlink could answer a question about media, which is not a hypothetical: an online video
|
|
1414
|
+
* and a hyperlink to the same URL is an ordinary thing to author, and the hyperlink came
|
|
1415
|
+
* first, so the video pair came out as two MS-media rels with no ECMA `video` rel at all and
|
|
1416
|
+
* `<a:videoFile r:link>` pointing at the wrong type.
|
|
1417
|
+
*/
|
|
1418
|
+
const mediaTargets = /* @__PURE__ */ new Set();
|
|
1419
|
+
slide._rels.forEach((rel) => {
|
|
1420
|
+
if (isHyperlinkRel(rel)) {
|
|
1421
|
+
if (rel.data === "slide") rels.push(relationshipEl(rel.rId, SLIDE_REL, targetBetweenPptSubparts(selfPath, slidePath(Number(rel.Target)))));
|
|
1422
|
+
else rels.push(externalHyperlinkRel(rel.rId, rel.Target));
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1425
|
+
(slide._relsChart || []).forEach((rel) => {
|
|
1426
|
+
rels.push(relationshipEl(rel.rId, rel.isChartEx ? CHARTEX_REL : CHART_REL, rel.Target));
|
|
1427
|
+
});
|
|
1428
|
+
(slide._relsMedia || []).forEach((rel) => {
|
|
1429
|
+
const relType = rel.type.toLowerCase();
|
|
1430
|
+
const seen = mediaTargets.has(rel.Target);
|
|
1431
|
+
mediaTargets.add(rel.Target);
|
|
1432
|
+
const media = (type, targetMode) => relationshipEl(rel.rId, type, rel.Target, { targetMode });
|
|
1433
|
+
if (rel.oleRelType) rels.push(media(rel.oleRelType));
|
|
1434
|
+
else if (rel.model3dRelType) rels.push(media(rel.model3dRelType));
|
|
1435
|
+
else if (relType.includes("image")) rels.push(media(IMAGE_REL));
|
|
1436
|
+
else if (relType.includes("audio")) rels.push(seen ? media(MS_MEDIA_REL) : media(AUDIO_REL));
|
|
1437
|
+
else if (relType.includes("video")) rels.push(seen ? media(MS_MEDIA_REL) : media(VIDEO_REL));
|
|
1438
|
+
else if (relType.includes("online")) rels.push(seen ? media(MS_MEDIA_REL, "External") : media(VIDEO_REL, "External"));
|
|
1439
|
+
});
|
|
1440
|
+
const start = defaultRelIdStart(slide);
|
|
1441
|
+
defaultRels.forEach((rel, idx) => {
|
|
1442
|
+
rels.push(relationshipEl(start + idx, rel.type, rel.target));
|
|
1443
|
+
});
|
|
1444
|
+
return relationshipsPart(rels);
|
|
1445
|
+
}
|
|
1446
|
+
//#endregion
|
|
1447
|
+
//#region src/gen/slide/slide.ts
|
|
1448
|
+
/**
|
|
1449
|
+
* ts-pptx: slide part + slide/layout rels
|
|
1450
|
+
*
|
|
1451
|
+
* Emit a slide (`ppt/slides/slideN.xml`) and the relationship files for slides
|
|
1452
|
+
* and layouts (`slideN.xml.rels`, `slideLayoutN.xml.rels`).
|
|
1453
|
+
*/
|
|
1454
|
+
/**
|
|
1455
|
+
* Generates XML for the slide file (`ppt/slides/slide1.xml`)
|
|
1456
|
+
* @param slide - the slide object to transform into XML
|
|
1457
|
+
* @param renderers - which renderer emits each shape family, passed through to the shape walk
|
|
1458
|
+
* @param transitionSoundRId - the relationship id package assembly registered this slide's transition
|
|
1459
|
+
* start sound under, if it registered one
|
|
1460
|
+
* @return XML
|
|
1461
|
+
*/
|
|
1462
|
+
function makeXmlSlide(slide, renderers, transitionSoundRId) {
|
|
1463
|
+
return XML_DECL + "\r\n" + el("p:sld", {
|
|
1464
|
+
...PML_ROOT_NS,
|
|
1465
|
+
show: slide?.hidden ? "0" : null
|
|
1466
|
+
}, [
|
|
1467
|
+
raw(slideObjectToXml(slide, renderers)),
|
|
1468
|
+
raw(el("p:clrMapOvr", null, raw(voidEl("a:masterClrMapping")))),
|
|
1469
|
+
raw(slideTransitionToXml(slide, transitionSoundRId)),
|
|
1470
|
+
raw(slideTimingToXml(slide))
|
|
1471
|
+
]);
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Generates XML string for a slide layout relation file
|
|
1475
|
+
* @param {number} layoutNumber - 1-indexed number of a layout that relations are generated for
|
|
1476
|
+
* @param {SlideLayoutInternal[]} slideLayouts - Slide Layouts
|
|
1477
|
+
* @return {string} XML
|
|
1478
|
+
*/
|
|
1479
|
+
function makeXmlSlideLayoutRel(layoutNumber, slideLayouts) {
|
|
1480
|
+
const slideLayout = slideLayouts[layoutNumber - 1];
|
|
1481
|
+
if (!slideLayout) throw new InternalError("slide/rel-index-out-of-range", `makeXmlSlideLayoutRel: no slide layout at index ${layoutNumber - 1}`);
|
|
1482
|
+
return slideObjectRelationsToXml(slideLayout, [{
|
|
1483
|
+
target: "../slideMasters/slideMaster1.xml",
|
|
1484
|
+
type: SLIDE_MASTER_REL
|
|
1485
|
+
}], slideLayoutPath(layoutNumber));
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Creates `ppt/_rels/slide*.xml.rels`
|
|
1489
|
+
* @param {PresSlideInternal[]} slides
|
|
1490
|
+
* @param {SlideLayoutInternal[]} slideLayouts - Slide Layout(s)
|
|
1491
|
+
* @param {number} `slideNumber` 1-indexed number of a layout that relations are generated for
|
|
1492
|
+
* @return {string} XML
|
|
1493
|
+
*/
|
|
1494
|
+
function makeXmlSlideRel(slides, slideLayouts, slideNumber) {
|
|
1495
|
+
const slide = slides[slideNumber - 1];
|
|
1496
|
+
if (!slide) throw new InternalError("slide/rel-index-out-of-range", `makeXmlSlideRel: no slide at index ${slideNumber - 1}`);
|
|
1497
|
+
const defaultRels = [{
|
|
1498
|
+
target: targetFromPptSubpart(slideLayoutPath(getLayoutIdxForSlide(slides, slideLayouts, slideNumber))),
|
|
1499
|
+
type: SLIDE_LAYOUT_REL
|
|
1500
|
+
}, {
|
|
1501
|
+
target: targetFromPptSubpart(notesSlidePath(slideNumber)),
|
|
1502
|
+
type: NOTES_SLIDE_REL
|
|
1503
|
+
}];
|
|
1504
|
+
if ((slide._comments || []).length > 0) defaultRels.push({
|
|
1505
|
+
target: targetFromPptSubpart(commentPath(slideNumber)),
|
|
1506
|
+
type: COMMENTS_REL
|
|
1507
|
+
});
|
|
1508
|
+
return slideObjectRelationsToXml(slide, defaultRels, slidePath(slideNumber));
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* For the passed slide number, resolves name of a layout that is used for.
|
|
1512
|
+
* @param {PresSlideInternal[]} slides - srray of slides
|
|
1513
|
+
* @param {SlideLayoutInternal[]} slideLayouts - array of slideLayouts
|
|
1514
|
+
* @param {number} slideNumber
|
|
1515
|
+
* @return {number} slide number
|
|
1516
|
+
*/
|
|
1517
|
+
function getLayoutIdxForSlide(slides, slideLayouts, slideNumber) {
|
|
1518
|
+
for (let i = 0; i < slideLayouts.length; i++) if (slideLayouts[i]?._name === slides[slideNumber - 1]?._slideLayout?._name) return i + 1;
|
|
1519
|
+
return 1;
|
|
1520
|
+
}
|
|
1521
|
+
//#endregion
|
|
1522
|
+
//#region src/gen/pres/theme.ts
|
|
1523
|
+
/**
|
|
1524
|
+
* ts-pptx: `ppt/theme/theme1.xml`
|
|
1525
|
+
*
|
|
1526
|
+
* Emit the Office theme: the `<a:clrScheme>` (with any caller color overrides
|
|
1527
|
+
* over the default Office scheme) plus the fixed font/format schemes.
|
|
1528
|
+
*/
|
|
1529
|
+
/**
|
|
1530
|
+
* Theme `<a:clrScheme>` slots in OOXML document order, with their default Office color child.
|
|
1531
|
+
* `dk1`/`lt1` default to `sysClr` (windowText/window); the rest are `srgbClr`. A user override
|
|
1532
|
+
* for any slot is emitted as `<a:srgbClr>` (see `buildThemeClrScheme`).
|
|
1533
|
+
*/
|
|
1534
|
+
const THEME_CLR_SCHEME_DEFAULTS = [
|
|
1535
|
+
["dk1", raw(voidEl("a:sysClr", {
|
|
1536
|
+
val: "windowText",
|
|
1537
|
+
lastClr: "000000"
|
|
1538
|
+
}))],
|
|
1539
|
+
["lt1", raw(voidEl("a:sysClr", {
|
|
1540
|
+
val: "window",
|
|
1541
|
+
lastClr: "FFFFFF"
|
|
1542
|
+
}))],
|
|
1543
|
+
["dk2", raw(voidEl("a:srgbClr", { val: "44546A" }))],
|
|
1544
|
+
["lt2", raw(voidEl("a:srgbClr", { val: "E7E6E6" }))],
|
|
1545
|
+
["accent1", raw(voidEl("a:srgbClr", { val: "4472C4" }))],
|
|
1546
|
+
["accent2", raw(voidEl("a:srgbClr", { val: "ED7D31" }))],
|
|
1547
|
+
["accent3", raw(voidEl("a:srgbClr", { val: "A5A5A5" }))],
|
|
1548
|
+
["accent4", raw(voidEl("a:srgbClr", { val: "FFC000" }))],
|
|
1549
|
+
["accent5", raw(voidEl("a:srgbClr", { val: "5B9BD5" }))],
|
|
1550
|
+
["accent6", raw(voidEl("a:srgbClr", { val: "70AD47" }))],
|
|
1551
|
+
["hlink", raw(voidEl("a:srgbClr", { val: "0563C1" }))],
|
|
1552
|
+
["folHlink", raw(voidEl("a:srgbClr", { val: "954F72" }))]
|
|
1553
|
+
];
|
|
1554
|
+
/**
|
|
1555
|
+
* Build the theme `<a:clrScheme>` block, applying any caller-supplied color overrides over the
|
|
1556
|
+
* default Office scheme. Invalid (non 6-digit-hex) overrides warn and keep the default rather
|
|
1557
|
+
* than emitting a degenerate color.
|
|
1558
|
+
* @param {ThemeColorScheme} [scheme] - per-slot hex overrides
|
|
1559
|
+
* @return {string} the `<a:clrScheme>...</a:clrScheme>` XML
|
|
1560
|
+
*/
|
|
1561
|
+
function buildThemeClrScheme(scheme) {
|
|
1562
|
+
return el("a:clrScheme", { name: "Office" }, THEME_CLR_SCHEME_DEFAULTS.map(([slot, defaultChild]) => {
|
|
1563
|
+
const override = scheme?.[slot];
|
|
1564
|
+
let child = defaultChild;
|
|
1565
|
+
if (typeof override === "string" && override.length > 0) {
|
|
1566
|
+
const hex = stripHash(override);
|
|
1567
|
+
if (isHexColor(hex)) child = raw(voidEl("a:srgbClr", { val: hex.toUpperCase() }));
|
|
1568
|
+
else warn("theme/invalid-color-override", `makeXmlTheme: colorScheme.${slot} "${override}" is not a 6-digit hex color; keeping the Office default.`);
|
|
1569
|
+
}
|
|
1570
|
+
return raw(el(`a:${slot}`, null, child));
|
|
1571
|
+
}));
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* The per-script theme font fallback table (`a:fontScheme`'s CT_TextFont lists), in OOXML
|
|
1575
|
+
* document order. Static Office defaults — no caller data, no escaping risk — unlike the
|
|
1576
|
+
* `<a:latin>`/`<a:ea>`/`<a:cs>` slots above.
|
|
1577
|
+
*
|
|
1578
|
+
* Forty-one of the forty-seven scripts take the same face in the major and minor lists, so the
|
|
1579
|
+
* shared table below is the whole thing and the two override maps hold the six-odd rows that
|
|
1580
|
+
* actually differ. Written out twice, ninety-four lines said less than these do, and one row
|
|
1581
|
+
* mistyped in one copy produces a theme that opens fine and paints a script in the wrong face —
|
|
1582
|
+
* a difference nobody reviewing a diff of two 47-row tables would see.
|
|
1583
|
+
*/
|
|
1584
|
+
const THEME_FONT_LIST = [
|
|
1585
|
+
["Jpan", "游ゴシック"],
|
|
1586
|
+
["Hang", "맑은 고딕"],
|
|
1587
|
+
["Hans", "等线"],
|
|
1588
|
+
["Hant", "新細明體"],
|
|
1589
|
+
["Arab", "Arial"],
|
|
1590
|
+
["Hebr", "Arial"],
|
|
1591
|
+
["Thai", "Cordia New"],
|
|
1592
|
+
["Ethi", "Nyala"],
|
|
1593
|
+
["Beng", "Vrinda"],
|
|
1594
|
+
["Gujr", "Shruti"],
|
|
1595
|
+
["Khmr", "DaunPenh"],
|
|
1596
|
+
["Knda", "Tunga"],
|
|
1597
|
+
["Guru", "Raavi"],
|
|
1598
|
+
["Cans", "Euphemia"],
|
|
1599
|
+
["Cher", "Plantagenet Cherokee"],
|
|
1600
|
+
["Yiii", "Microsoft Yi Baiti"],
|
|
1601
|
+
["Tibt", "Microsoft Himalaya"],
|
|
1602
|
+
["Thaa", "MV Boli"],
|
|
1603
|
+
["Deva", "Mangal"],
|
|
1604
|
+
["Telu", "Gautami"],
|
|
1605
|
+
["Taml", "Latha"],
|
|
1606
|
+
["Syrc", "Estrangelo Edessa"],
|
|
1607
|
+
["Orya", "Kalinga"],
|
|
1608
|
+
["Mlym", "Kartika"],
|
|
1609
|
+
["Laoo", "DokChampa"],
|
|
1610
|
+
["Sinh", "Iskoola Pota"],
|
|
1611
|
+
["Mong", "Mongolian Baiti"],
|
|
1612
|
+
["Viet", "Arial"],
|
|
1613
|
+
["Uigh", "Microsoft Uighur"],
|
|
1614
|
+
["Geor", "Sylfaen"],
|
|
1615
|
+
["Armn", "Arial"],
|
|
1616
|
+
["Bugi", "Leelawadee UI"],
|
|
1617
|
+
["Bopo", "Microsoft JhengHei"],
|
|
1618
|
+
["Java", "Javanese Text"],
|
|
1619
|
+
["Lisu", "Segoe UI"],
|
|
1620
|
+
["Mymr", "Myanmar Text"],
|
|
1621
|
+
["Nkoo", "Ebrima"],
|
|
1622
|
+
["Olck", "Nirmala UI"],
|
|
1623
|
+
["Osma", "Ebrima"],
|
|
1624
|
+
["Phag", "Phagspa"],
|
|
1625
|
+
["Syrn", "Estrangelo Edessa"],
|
|
1626
|
+
["Syrj", "Estrangelo Edessa"],
|
|
1627
|
+
["Syre", "Estrangelo Edessa"],
|
|
1628
|
+
["Sora", "Nirmala UI"],
|
|
1629
|
+
["Tale", "Microsoft Tai Le"],
|
|
1630
|
+
["Talu", "Microsoft New Tai Lue"],
|
|
1631
|
+
["Tfng", "Ebrima"]
|
|
1632
|
+
];
|
|
1633
|
+
/**
|
|
1634
|
+
* The seven scripts whose *heading* face differs from the body one. Latin and CJK headings take
|
|
1635
|
+
* the Light weight; the scripts with no Light cut take a serif (or, for Khmer, a different face
|
|
1636
|
+
* entirely) where the body takes a sans. Every other script in {@link THEME_FONT_LIST} uses one
|
|
1637
|
+
* face for both.
|
|
1638
|
+
*/
|
|
1639
|
+
const MAJOR_FONT_OVERRIDES = {
|
|
1640
|
+
Jpan: "游ゴシック Light",
|
|
1641
|
+
Hans: "等线 Light",
|
|
1642
|
+
Arab: "Times New Roman",
|
|
1643
|
+
Hebr: "Times New Roman",
|
|
1644
|
+
Thai: "Angsana New",
|
|
1645
|
+
Khmr: "MoolBoran",
|
|
1646
|
+
Viet: "Times New Roman"
|
|
1647
|
+
};
|
|
1648
|
+
/** The major (heading) and minor (body) lists, both derived from the one table. */
|
|
1649
|
+
const MAJOR_FONT_LIST = THEME_FONT_LIST.map(([script, face]) => [script, MAJOR_FONT_OVERRIDES[script] ?? face]);
|
|
1650
|
+
const MINOR_FONT_LIST = THEME_FONT_LIST;
|
|
1651
|
+
function fontListChildren(scriptFaces) {
|
|
1652
|
+
return scriptFaces.map(([script, typeface]) => raw(voidEl("a:font", {
|
|
1653
|
+
script,
|
|
1654
|
+
typeface
|
|
1655
|
+
})));
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Creates `ppt/theme/theme1.xml`
|
|
1659
|
+
* @return {string} XML
|
|
1660
|
+
*/
|
|
1661
|
+
function makeXmlTheme(pres) {
|
|
1662
|
+
const majorLatin = pres.theme?.headFontFace ? raw(voidEl("a:latin", { typeface: pres.theme.headFontFace })) : raw(voidEl("a:latin", {
|
|
1663
|
+
typeface: "Calibri Light",
|
|
1664
|
+
panose: "020F0302020204030204"
|
|
1665
|
+
}));
|
|
1666
|
+
const minorLatin = pres.theme?.bodyFontFace ? raw(voidEl("a:latin", { typeface: pres.theme.bodyFontFace })) : raw(voidEl("a:latin", {
|
|
1667
|
+
typeface: "Calibri",
|
|
1668
|
+
panose: "020F0502020204030204"
|
|
1669
|
+
}));
|
|
1670
|
+
const majorEa = raw(voidEl("a:ea", { typeface: pres.theme?.headFontFaceEA ?? "" }));
|
|
1671
|
+
const minorEa = raw(voidEl("a:ea", { typeface: pres.theme?.bodyFontFaceEA ?? "" }));
|
|
1672
|
+
const majorCs = raw(voidEl("a:cs", { typeface: pres.theme?.headFontFaceCS ?? "" }));
|
|
1673
|
+
const minorCs = raw(voidEl("a:cs", { typeface: pres.theme?.bodyFontFaceCS ?? "" }));
|
|
1674
|
+
const fontScheme = el("a:fontScheme", { name: "Office" }, [raw(el("a:majorFont", null, [
|
|
1675
|
+
majorLatin,
|
|
1676
|
+
majorEa,
|
|
1677
|
+
majorCs,
|
|
1678
|
+
...fontListChildren(MAJOR_FONT_LIST)
|
|
1679
|
+
])), raw(el("a:minorFont", null, [
|
|
1680
|
+
minorLatin,
|
|
1681
|
+
minorEa,
|
|
1682
|
+
minorCs,
|
|
1683
|
+
...fontListChildren(MINOR_FONT_LIST)
|
|
1684
|
+
]))]);
|
|
1685
|
+
const themeElements = el("a:themeElements", null, [
|
|
1686
|
+
raw(buildThemeClrScheme(pres.theme?.colorScheme)),
|
|
1687
|
+
raw(fontScheme),
|
|
1688
|
+
raw(FMT_SCHEME_XML)
|
|
1689
|
+
]);
|
|
1690
|
+
const extLst = el("a:extLst", null, raw(el("a:ext", { uri: "{05A4C25C-085E-4340-85A3-A5531E510DB2}" }, raw(voidEl("thm15:themeFamily", {
|
|
1691
|
+
"xmlns:thm15": "http://schemas.microsoft.com/office/thememl/2012/main",
|
|
1692
|
+
name: "Office Theme",
|
|
1693
|
+
id: "{62F939B6-93AF-4DB8-9C6B-D6C7DFDC589F}",
|
|
1694
|
+
vid: "{4A3C46E8-61CC-4603-A589-7422A47A8E4A}"
|
|
1695
|
+
})))));
|
|
1696
|
+
return XML_DECL + el("a:theme", {
|
|
1697
|
+
"xmlns:a": OOXML_NS.a,
|
|
1698
|
+
name: "Office Theme"
|
|
1699
|
+
}, [
|
|
1700
|
+
raw(themeElements),
|
|
1701
|
+
raw(voidEl("a:objectDefaults")),
|
|
1702
|
+
raw(voidEl("a:extraClrSchemeLst")),
|
|
1703
|
+
raw(extLst)
|
|
1704
|
+
]);
|
|
1705
|
+
}
|
|
1706
|
+
//#endregion
|
|
1707
|
+
//#region src/gen/extract-slides.ts
|
|
1708
|
+
/**
|
|
1709
|
+
* Embedded audio/video. addMedia (`gen/define/media.ts`) pushes three consecutive
|
|
1710
|
+
* _relsMedia entries per item off `mediaRid`: the ECMA audio/video rel
|
|
1711
|
+
* (rId=mediaRid), the MS-2007 `media` rel (mediaRid+1, sharing one Target),
|
|
1712
|
+
* and the preview image rel (mediaRid+2). The body references all three; we
|
|
1713
|
+
* surface the descriptor so appendSlides can reproduce the rel graph. Online
|
|
1714
|
+
* media (external link) is excluded — it has a different rel shape and no part.
|
|
1715
|
+
*/
|
|
1716
|
+
function avMediaOf(slide, relByRid) {
|
|
1717
|
+
return slide._slideObjects.filter((obj) => obj._type === "media" && obj.mtype !== "online" && typeof obj.mediaRid === "number").map((obj) => {
|
|
1718
|
+
const mtype = obj.mtype === "audio" ? "audio" : "video";
|
|
1719
|
+
const mediaRid = obj.mediaRid;
|
|
1720
|
+
const mediaRel = relByRid.get(mediaRid);
|
|
1721
|
+
const previewRel = relByRid.get(previewRid(mediaRid));
|
|
1722
|
+
if (!mediaRel || !previewRel) return null;
|
|
1723
|
+
const mediaBytes = decodeBase64ToBytes(typeof mediaRel.data === "string" ? mediaRel.data : "");
|
|
1724
|
+
const previewBytes = decodeBase64ToBytes(typeof previewRel.data === "string" ? previewRel.data : "");
|
|
1725
|
+
if (!mediaBytes || !previewBytes) return null;
|
|
1726
|
+
const mediaExtn = relExtension(mediaRel, "mp4");
|
|
1727
|
+
const previewExtn = relExtension(previewRel, "png");
|
|
1728
|
+
return {
|
|
1729
|
+
mtype,
|
|
1730
|
+
mediaRid,
|
|
1731
|
+
msMediaRid: msMediaRid(mediaRid),
|
|
1732
|
+
previewRid: previewRid(mediaRid),
|
|
1733
|
+
mediaBytes,
|
|
1734
|
+
mediaExtn,
|
|
1735
|
+
mediaContentType: avContentType(mediaExtn, mtype),
|
|
1736
|
+
previewBytes,
|
|
1737
|
+
previewExtn,
|
|
1738
|
+
previewContentType: imageContentType(previewExtn)
|
|
1739
|
+
};
|
|
1740
|
+
}).filter((m) => m !== null);
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* A media rel's lowercased file extension: its own `extn` when it has one, else the suffix of
|
|
1744
|
+
* its `Target`, else `fallback`.
|
|
1745
|
+
* @param rel - the media relationship
|
|
1746
|
+
* @param fallback - the extension to assume when neither source names one
|
|
1747
|
+
*/
|
|
1748
|
+
function relExtension(rel, fallback) {
|
|
1749
|
+
return (rel.extn || rel.Target.split(".").pop() || fallback).toLowerCase();
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Plain image media, as decoded bytes keyed by the `rId` the body uses.
|
|
1753
|
+
*
|
|
1754
|
+
* `avPreviewRids` are the poster frames of {@link avMediaOf}'s items: they live in
|
|
1755
|
+
* `_relsMedia` as image rels too, and their descriptor already carries them, so they are
|
|
1756
|
+
* excluded here rather than surfaced twice.
|
|
1757
|
+
*/
|
|
1758
|
+
function imageMediaOf(slide, avPreviewRids) {
|
|
1759
|
+
return slide._relsMedia.filter((rel) => rel.type.toLowerCase().includes("image") && !avPreviewRids.has(rel.rId)).map((rel) => {
|
|
1760
|
+
const data = rel.data && typeof rel.data === "string" ? rel.data : "";
|
|
1761
|
+
const bytes = decodeBase64ToBytes(data);
|
|
1762
|
+
const extn = relExtension(rel, "png");
|
|
1763
|
+
return bytes ? {
|
|
1764
|
+
rId: rel.rId,
|
|
1765
|
+
bytes,
|
|
1766
|
+
extn,
|
|
1767
|
+
contentType: imageContentType(extn)
|
|
1768
|
+
} : null;
|
|
1769
|
+
}).filter((m) => m !== null);
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Online (external-link) video. addMedia type:'online' pushes three rels off
|
|
1773
|
+
* mediaRid: the ECMA video rel (mediaRid, External), the MS-2007 media rel
|
|
1774
|
+
* (mediaRid+1, External, sharing the link Target), and the poster image rel
|
|
1775
|
+
* (mediaRid+2, carried by {@link imageMediaOf} as a normal image). No media binary
|
|
1776
|
+
* part exists; appendSlides reproduces only the two external rels + poster.
|
|
1777
|
+
*/
|
|
1778
|
+
function onlineMediaOf(slide, relByRid) {
|
|
1779
|
+
return slide._slideObjects.filter((obj) => obj._type === "media" && obj.mtype === "online" && typeof obj.mediaRid === "number").map((obj) => {
|
|
1780
|
+
const mediaRid = obj.mediaRid;
|
|
1781
|
+
const videoRel = relByRid.get(mediaRid);
|
|
1782
|
+
if (!videoRel || typeof videoRel.Target !== "string" || !videoRel.Target) return null;
|
|
1783
|
+
return {
|
|
1784
|
+
mediaRid,
|
|
1785
|
+
msMediaRid: msMediaRid(mediaRid),
|
|
1786
|
+
link: videoRel.Target
|
|
1787
|
+
};
|
|
1788
|
+
}).filter((m) => m !== null);
|
|
1789
|
+
}
|
|
1790
|
+
/** External hyperlink rels, keyed by the `rId` the body uses. */
|
|
1791
|
+
function hyperlinksOf(slide) {
|
|
1792
|
+
return slide._rels.filter((rel) => isHyperlinkRel(rel) && rel.data !== "slide").map((rel) => ({
|
|
1793
|
+
rId: rel.rId,
|
|
1794
|
+
target: rel.Target
|
|
1795
|
+
}));
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* Internal slide-to-slide links: `rel.data === 'slide'`, and `rel.Target` is the 1-based
|
|
1799
|
+
* source slide number (see addText's hyperlink.slide handling).
|
|
1800
|
+
*
|
|
1801
|
+
* A Target that does not read as a slide number throws rather than passing `NaN` on to the
|
|
1802
|
+
* append path's rel-graph rebuild, where it surfaces as "links to source slide NaN" a whole
|
|
1803
|
+
* subsystem later. `addText` writes this Target itself, so the condition is a defect on this
|
|
1804
|
+
* side rather than bad input — an {@link InternalError}, like the chart router's own.
|
|
1805
|
+
*/
|
|
1806
|
+
function slideLinksOf(slide) {
|
|
1807
|
+
return slide._rels.filter((rel) => isHyperlinkRel(rel) && rel.data === "slide").map((rel) => {
|
|
1808
|
+
const sourceSlideNumber = Number(rel.Target);
|
|
1809
|
+
if (!Number.isInteger(sourceSlideNumber) || sourceSlideNumber < 1) throw new InternalError("slide/link-target-not-a-number", `Internal slide link rId${rel.rId} targets "${String(rel.Target)}", which is not a slide number`);
|
|
1810
|
+
return {
|
|
1811
|
+
rId: rel.rId,
|
|
1812
|
+
sourceSlideNumber
|
|
1813
|
+
};
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* Speaker notes, or `undefined` when the slide has none. The body and the hyperlinks come from one
|
|
1818
|
+
* resolution of the notes, because the two must agree on every hyperlink rId. Notes rels reserve
|
|
1819
|
+
* rId1=notesMaster and rId2=slide, both of which appendSlides wires itself.
|
|
1820
|
+
*/
|
|
1821
|
+
function notesOf(slide) {
|
|
1822
|
+
if (!slide._slideObjects.some((obj) => obj._type === "notes")) return void 0;
|
|
1823
|
+
const notes = buildNotesSlideRels(slide);
|
|
1824
|
+
return {
|
|
1825
|
+
xml: makeXmlNotesSlide(slide, notes),
|
|
1826
|
+
hyperlinks: notes.rels.filter((rel) => typeof rel.Target === "string" && rel.Target).map((rel) => ({
|
|
1827
|
+
rId: rel.rId,
|
|
1828
|
+
target: rel.Target
|
|
1829
|
+
}))
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Serialize a deck's authored slides as injectable descriptors, without producing a package.
|
|
1834
|
+
* @param source - the deck state to read; see {@link ExtractSource}
|
|
1835
|
+
* @param opts.onMediaError - what a media source that cannot be read does, as on `write()`
|
|
1836
|
+
*/
|
|
1837
|
+
async function extractSlides(source, opts = {}) {
|
|
1838
|
+
const onMediaError = opts.onMediaError ?? "throw";
|
|
1839
|
+
const { presentation } = source;
|
|
1840
|
+
const deckSlides = presentation.slides;
|
|
1841
|
+
requireSlideLinksInDeck(deckSlides);
|
|
1842
|
+
backfillPlaceholders(deckSlides);
|
|
1843
|
+
await encodeMediaForTargets(deckSlides, source.runtime, onMediaError);
|
|
1844
|
+
const restoreAuthoredFit = bakeMeasuredFit(deckSlides, source.fontMetrics);
|
|
1845
|
+
let slides;
|
|
1846
|
+
try {
|
|
1847
|
+
slides = deckSlides.map((slide) => {
|
|
1848
|
+
const relByRid = new Map(slide._relsMedia.map((rel) => [rel.rId, rel]));
|
|
1849
|
+
const avMedia = avMediaOf(slide, relByRid);
|
|
1850
|
+
const notes = notesOf(slide);
|
|
1851
|
+
return {
|
|
1852
|
+
xml: makeXmlSlide(slide, source.renderers),
|
|
1853
|
+
media: imageMediaOf(slide, new Set(avMedia.map((item) => item.previewRid))),
|
|
1854
|
+
hyperlinks: hyperlinksOf(slide),
|
|
1855
|
+
charts: source.extract.charts?.(slide) ?? [],
|
|
1856
|
+
slideLinks: slideLinksOf(slide),
|
|
1857
|
+
avMedia,
|
|
1858
|
+
onlineMedia: onlineMediaOf(slide, relByRid),
|
|
1859
|
+
...notes ? { notes } : {}
|
|
1860
|
+
};
|
|
1861
|
+
});
|
|
1862
|
+
} finally {
|
|
1863
|
+
restoreAuthoredFit();
|
|
1864
|
+
}
|
|
1865
|
+
const notesMaster = slides.some((s) => s.notes) ? {
|
|
1866
|
+
xml: makeXmlNotesMaster(),
|
|
1867
|
+
themeXml: makeXmlTheme(presentation)
|
|
1868
|
+
} : void 0;
|
|
1869
|
+
return {
|
|
1870
|
+
widthEmu: presentation.presLayout.width,
|
|
1871
|
+
heightEmu: presentation.presLayout.height,
|
|
1872
|
+
slides,
|
|
1873
|
+
embeddedFonts: presentation.embeddedFonts,
|
|
1874
|
+
...notesMaster ? { notesMaster } : {}
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
//#endregion
|
|
1878
|
+
//#region src/gen/opc/app.ts
|
|
1879
|
+
/**
|
|
1880
|
+
* ts-pptx: `docProps/app.xml`
|
|
1881
|
+
*
|
|
1882
|
+
* Emit the extended-properties part (application, slide/notes counts, heading
|
|
1883
|
+
* pairs, titles of parts, company).
|
|
1884
|
+
*/
|
|
1885
|
+
/** This part is pretty-printed, one element per line, at three nesting depths. */
|
|
1886
|
+
const INDENT_1 = "\n ";
|
|
1887
|
+
const INDENT_2 = "\n ";
|
|
1888
|
+
const INDENT_3 = "\n ";
|
|
1889
|
+
const PROP = { openPrefix: INDENT_1 };
|
|
1890
|
+
const VECTOR = {
|
|
1891
|
+
openPrefix: INDENT_2,
|
|
1892
|
+
closePrefix: INDENT_2
|
|
1893
|
+
};
|
|
1894
|
+
const ITEM = { openPrefix: INDENT_3 };
|
|
1895
|
+
const NS = {
|
|
1896
|
+
xmlns: OOXML_NS.ep,
|
|
1897
|
+
"xmlns:vt": OOXML_NS.vt
|
|
1898
|
+
};
|
|
1899
|
+
/** One `<vt:variant>` of a heading pair: the section's name, then its count. */
|
|
1900
|
+
function headingPair(name, count) {
|
|
1901
|
+
return [el("vt:variant", null, raw(el("vt:lpstr", null, name)), ITEM), el("vt:variant", null, raw(el("vt:i4", null, count)), ITEM)];
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Creates `docProps/app.xml`
|
|
1905
|
+
*
|
|
1906
|
+
* `PresentationFormat` and `HiddenSlides` were constants -- `On-screen Show (16:9)` and `0` --
|
|
1907
|
+
* whatever the deck. PowerPoint rewrites both on its first save, so nothing broke, but until then
|
|
1908
|
+
* a 4:3 deck and a deck with hidden slides each stated something false to anything reading the
|
|
1909
|
+
* package without opening it. Both are known here, so both are reported.
|
|
1910
|
+
* @param {PresSlideInternal[]} slides - Presenation Slides
|
|
1911
|
+
* @param {string} company - "Company" metadata
|
|
1912
|
+
* @param {PresLayout} presLayout - the deck's slide size, which names its presentation format
|
|
1913
|
+
* @returns XML
|
|
1914
|
+
*/
|
|
1915
|
+
function makeXmlApp(slides, company, presLayout) {
|
|
1916
|
+
const headingPairs = el("vt:vector", {
|
|
1917
|
+
size: 6,
|
|
1918
|
+
baseType: "variant"
|
|
1919
|
+
}, [
|
|
1920
|
+
...headingPair("Fonts Used", 2),
|
|
1921
|
+
...headingPair("Theme", 1),
|
|
1922
|
+
...headingPair("Slide Titles", slides.length)
|
|
1923
|
+
].map(raw), VECTOR);
|
|
1924
|
+
const titlesOfParts = el("vt:vector", {
|
|
1925
|
+
size: slides.length + 1 + 2,
|
|
1926
|
+
baseType: "lpstr"
|
|
1927
|
+
}, [
|
|
1928
|
+
raw(el("vt:lpstr", null, "Arial", ITEM)),
|
|
1929
|
+
raw(el("vt:lpstr", null, "Calibri", ITEM)),
|
|
1930
|
+
raw(el("vt:lpstr", null, "Office Theme", ITEM)),
|
|
1931
|
+
raw(INDENT_3),
|
|
1932
|
+
...slides.map((_slideObj, idx) => raw(el("vt:lpstr", null, `Slide ${idx + 1}`)))
|
|
1933
|
+
], VECTOR);
|
|
1934
|
+
return XML_DECL + "\r\n" + el("Properties", NS, [
|
|
1935
|
+
raw(el("TotalTime", null, 0, PROP)),
|
|
1936
|
+
raw(el("Words", null, 0, PROP)),
|
|
1937
|
+
raw(el("Application", null, "Microsoft Office PowerPoint", PROP)),
|
|
1938
|
+
raw(el("PresentationFormat", null, presentationFormatLabel(presLayout.width, presLayout.height), PROP)),
|
|
1939
|
+
raw(el("Paragraphs", null, 0, PROP)),
|
|
1940
|
+
raw(el("Slides", null, slides.length, PROP)),
|
|
1941
|
+
raw(el("Notes", null, slides.length, PROP)),
|
|
1942
|
+
raw(el("HiddenSlides", null, slides.filter((slide) => slide.hidden).length, PROP)),
|
|
1943
|
+
raw(el("MMClips", null, 0, PROP)),
|
|
1944
|
+
raw(el("ScaleCrop", null, "false", PROP)),
|
|
1945
|
+
raw(el("HeadingPairs", null, raw(headingPairs), {
|
|
1946
|
+
...PROP,
|
|
1947
|
+
closePrefix: INDENT_1
|
|
1948
|
+
})),
|
|
1949
|
+
raw(el("TitlesOfParts", null, raw(titlesOfParts), {
|
|
1950
|
+
...PROP,
|
|
1951
|
+
closePrefix: INDENT_1
|
|
1952
|
+
})),
|
|
1953
|
+
raw(el("Company", null, company, PROP)),
|
|
1954
|
+
raw(el("LinksUpToDate", null, "false", PROP)),
|
|
1955
|
+
raw(el("SharedDoc", null, "false", PROP)),
|
|
1956
|
+
raw(el("HyperlinksChanged", null, "false", PROP)),
|
|
1957
|
+
raw(el("AppVersion", null, "16.0000", PROP))
|
|
1958
|
+
], { closePrefix: INDENT_1 });
|
|
1959
|
+
}
|
|
1960
|
+
//#endregion
|
|
1961
|
+
//#region src/gen/opc/content-types.ts
|
|
1962
|
+
/**
|
|
1963
|
+
* ts-pptx: `[Content_Types].xml`
|
|
1964
|
+
*
|
|
1965
|
+
* Emit the package content-types part: Default entries for the media extensions
|
|
1966
|
+
* actually used by the deck (plus the font default when faces are embedded) and
|
|
1967
|
+
* Override entries for every part the package skeleton writes.
|
|
1968
|
+
*
|
|
1969
|
+
* Entries that exist only because a construct family put a part in the package arrive as
|
|
1970
|
+
* {@link ContentTypeContributions} — already resolved to data and grouped by where they land.
|
|
1971
|
+
* The order below is a property of the format, not of whoever assembled that list, so a deck
|
|
1972
|
+
* written by a program that links three families and one that links ten emit the same bytes.
|
|
1973
|
+
*/
|
|
1974
|
+
/**
|
|
1975
|
+
* Some Override entries have always been emitted with a leading space. It is insignificant
|
|
1976
|
+
* whitespace, but it is in the bytes this library has shipped for years, so it is reproduced
|
|
1977
|
+
* verbatim rather than normalized (see AGENTS.md "Byte identity": whitespace diffs are a STOP).
|
|
1978
|
+
*/
|
|
1979
|
+
const LEADING_SPACE = { openPrefix: " " };
|
|
1980
|
+
/** Nothing beyond the skeleton — the shape a deck written with no construct families contributes. */
|
|
1981
|
+
const NO_CONTENT_TYPE_CONTRIBUTIONS = Object.freeze({
|
|
1982
|
+
defaults: [],
|
|
1983
|
+
presentation: [],
|
|
1984
|
+
perSlide: [],
|
|
1985
|
+
perLayout: [],
|
|
1986
|
+
trailing: []
|
|
1987
|
+
});
|
|
1988
|
+
function contentDefault(extension, contentType) {
|
|
1989
|
+
return voidEl("Default", {
|
|
1990
|
+
Extension: extension,
|
|
1991
|
+
ContentType: contentType
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
function override(partName, contentType, fmt) {
|
|
1995
|
+
return voidEl("Override", {
|
|
1996
|
+
PartName: partName,
|
|
1997
|
+
ContentType: contentType
|
|
1998
|
+
}, fmt);
|
|
1999
|
+
}
|
|
2000
|
+
/** Render a contributed Override, honouring its leading-space flag. */
|
|
2001
|
+
function contributedOverride(entry) {
|
|
2002
|
+
return override(entry.partName, entry.contentType, entry.leadingSpace ? LEADING_SPACE : void 0);
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Generate XML ContentType
|
|
2006
|
+
* @param opts - the deck's parts, plus whatever the construct families it was written with add
|
|
2007
|
+
* @returns XML
|
|
2008
|
+
*/
|
|
2009
|
+
function makeXmlContTypes(opts) {
|
|
2010
|
+
const { slides, slideLayouts, masterSlide, hasCustomProps, embeddedFonts } = opts;
|
|
2011
|
+
const contributions = opts.contributions ?? NO_CONTENT_TYPE_CONTRIBUTIONS;
|
|
2012
|
+
const parts = [contentDefault("xml", "application/xml"), contentDefault("rels", RELATIONSHIPS_CONTENT_TYPE)];
|
|
2013
|
+
const extnTypeMap = /* @__PURE__ */ new Map();
|
|
2014
|
+
const ctTargets = [];
|
|
2015
|
+
(slides || []).forEach((s) => ctTargets.push(s));
|
|
2016
|
+
(slideLayouts || []).forEach((l) => ctTargets.push(l));
|
|
2017
|
+
if (masterSlide) ctTargets.push(masterSlide);
|
|
2018
|
+
ctTargets.forEach((target) => {
|
|
2019
|
+
(target._relsMedia || []).forEach((rel) => {
|
|
2020
|
+
if (rel.type === "online" || !rel.extn || !rel.type) return;
|
|
2021
|
+
const contentType = rel.type.startsWith("audio/") ? avContentType(rel.extn, "audio") : rel.type.startsWith("video/") ? avContentType(rel.extn, "video") : rel.type;
|
|
2022
|
+
if (!extnTypeMap.has(rel.extn)) extnTypeMap.set(rel.extn, contentType);
|
|
2023
|
+
});
|
|
2024
|
+
});
|
|
2025
|
+
extnTypeMap.forEach((type, extn) => {
|
|
2026
|
+
parts.push(contentDefault(extn, type));
|
|
2027
|
+
});
|
|
2028
|
+
contributions.defaults.forEach((entry) => {
|
|
2029
|
+
if (extnTypeMap.has(entry.extension)) return;
|
|
2030
|
+
extnTypeMap.set(entry.extension, entry.contentType);
|
|
2031
|
+
parts.push(contentDefault(entry.extension, entry.contentType));
|
|
2032
|
+
});
|
|
2033
|
+
if ((embeddedFonts || []).some((font) => font.faces.some((face) => face.bytes))) parts.push(contentDefault(FONT_DATA_EXTENSION, FONT_DATA_CONTENT_TYPE));
|
|
2034
|
+
parts.push(override(overrideName(PRESENTATION_PATH), PRESENTATION_MAIN_CONTENT_TYPE));
|
|
2035
|
+
contributions.presentation.forEach((entry) => parts.push(contributedOverride(entry)));
|
|
2036
|
+
parts.push(override(overrideName(SLIDE_MASTER_PATH), SLIDE_MASTER_CONTENT_TYPE));
|
|
2037
|
+
slides.forEach((_slide, idx) => {
|
|
2038
|
+
parts.push(override(overrideName(slidePath(idx + 1)), SLIDE_CONTENT_TYPE));
|
|
2039
|
+
(contributions.perSlide[idx] || []).forEach((entry) => parts.push(contributedOverride(entry)));
|
|
2040
|
+
});
|
|
2041
|
+
parts.push(override("/ppt/presProps.xml", PRES_PROPS_CONTENT_TYPE));
|
|
2042
|
+
parts.push(override("/ppt/viewProps.xml", VIEW_PROPS_CONTENT_TYPE));
|
|
2043
|
+
parts.push(override("/ppt/theme/theme1.xml", THEME_CONTENT_TYPE));
|
|
2044
|
+
parts.push(override("/ppt/theme/theme2.xml", THEME_CONTENT_TYPE));
|
|
2045
|
+
parts.push(override("/ppt/tableStyles.xml", TABLE_STYLES_CONTENT_TYPE));
|
|
2046
|
+
slideLayouts.forEach((_layout, idx) => {
|
|
2047
|
+
parts.push(override(overrideName(slideLayoutPath(idx + 1)), SLIDE_LAYOUT_CONTENT_TYPE));
|
|
2048
|
+
(contributions.perLayout[idx] || []).forEach((entry) => parts.push(contributedOverride(entry)));
|
|
2049
|
+
});
|
|
2050
|
+
contributions.trailing.forEach((entry) => parts.push(contributedOverride(entry)));
|
|
2051
|
+
parts.push(override("/docProps/core.xml", CORE_PROPS_CONTENT_TYPE, LEADING_SPACE));
|
|
2052
|
+
parts.push(override("/docProps/app.xml", EXTENDED_PROPS_CONTENT_TYPE, LEADING_SPACE));
|
|
2053
|
+
if (hasCustomProps) parts.push(override("/docProps/custom.xml", CUSTOM_PROPS_CONTENT_TYPE, LEADING_SPACE));
|
|
2054
|
+
return XML_DECL + "\r\n" + el("Types", { xmlns: OOXML_NS.ct }, parts.map(raw));
|
|
2055
|
+
}
|
|
2056
|
+
//#endregion
|
|
2057
|
+
//#region src/gen/opc/custom-props.ts
|
|
2058
|
+
/**
|
|
2059
|
+
* ts-pptx: `docProps/custom.xml`
|
|
2060
|
+
*
|
|
2061
|
+
* Emit the custom-properties part from caller-supplied name/value pairs (bool,
|
|
2062
|
+
* date, number and string value types).
|
|
2063
|
+
*/
|
|
2064
|
+
const CUSTOM_PROPS_FMTID = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}";
|
|
2065
|
+
const PROPS_NS = {
|
|
2066
|
+
xmlns: "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties",
|
|
2067
|
+
"xmlns:vt": OOXML_NS.vt
|
|
2068
|
+
};
|
|
2069
|
+
/** Serialize one custom-property value to its typed `vt:` element. */
|
|
2070
|
+
function valueEl(value) {
|
|
2071
|
+
if (typeof value === "boolean") return el("vt:bool", null, String(value));
|
|
2072
|
+
if (value instanceof Date) return el("vt:filetime", null, value.toISOString().replace(/\.\d{3}Z$/, "Z"));
|
|
2073
|
+
if (typeof value === "number") return el(Number.isInteger(value) ? "vt:i4" : "vt:r8", null, value);
|
|
2074
|
+
return el("vt:lpwstr", null, String(value));
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Creates `docProps/custom.xml`
|
|
2078
|
+
* @param props - custom property name/value pairs
|
|
2079
|
+
* @returns XML
|
|
2080
|
+
*/
|
|
2081
|
+
function makeXmlCustomProperties(props) {
|
|
2082
|
+
const properties = props.map(({ name, value }, idx) => raw(el("property", {
|
|
2083
|
+
fmtid: CUSTOM_PROPS_FMTID,
|
|
2084
|
+
pid: idx + 2,
|
|
2085
|
+
name
|
|
2086
|
+
}, raw(valueEl(value)))));
|
|
2087
|
+
return XML_DECL + "\r\n" + el("Properties", PROPS_NS, properties);
|
|
2088
|
+
}
|
|
2089
|
+
//#endregion
|
|
2090
|
+
//#region src/gen/opc/root-rels.ts
|
|
2091
|
+
/**
|
|
2092
|
+
* ts-pptx: `_rels/.rels`
|
|
2093
|
+
*
|
|
2094
|
+
* Emit the package root relationships (app/core/presentation, plus custom-props
|
|
2095
|
+
* when present).
|
|
2096
|
+
*/
|
|
2097
|
+
/** Each entry carries its own indent; the `<Relationships>` wrapper adds none of its own. */
|
|
2098
|
+
const INDENTED = { fmt: { openPrefix: "\n " } };
|
|
2099
|
+
/**
|
|
2100
|
+
* Creates `_rels/.rels`
|
|
2101
|
+
* @returns XML
|
|
2102
|
+
*/
|
|
2103
|
+
function makeXmlRootRels(hasCustomProps) {
|
|
2104
|
+
const rels = [
|
|
2105
|
+
relationshipEl("rId1", EXTENDED_PROPS_REL, "docProps/app.xml", INDENTED),
|
|
2106
|
+
relationshipEl("rId2", CORE_PROPS_REL, "docProps/core.xml", INDENTED),
|
|
2107
|
+
relationshipEl("rId3", OFFICE_DOCUMENT_REL, "ppt/presentation.xml", INDENTED)
|
|
2108
|
+
];
|
|
2109
|
+
if (hasCustomProps) rels.push(relationshipEl("rId4", CUSTOM_PROPS_REL, "docProps/custom.xml", INDENTED));
|
|
2110
|
+
return relationshipsPart(rels, { closePrefix: "\n " });
|
|
2111
|
+
}
|
|
2112
|
+
//#endregion
|
|
2113
|
+
//#region src/gen/pres/presentation-rels.ts
|
|
2114
|
+
/**
|
|
2115
|
+
* Creates `ppt/_rels/presentation.xml.rels`
|
|
2116
|
+
* @param {PresSlideInternal[]} slides - Presenation Slides
|
|
2117
|
+
* @returns XML
|
|
2118
|
+
*/
|
|
2119
|
+
/**
|
|
2120
|
+
* The relationship ids `presentation.xml.rels` gives its fixed parts, and the first id free
|
|
2121
|
+
* after them.
|
|
2122
|
+
*
|
|
2123
|
+
* Layout: rId1 = slideMaster, rId2..(N+1) = N slides, then notesMaster/presProps/viewProps/
|
|
2124
|
+
* theme1/tableStyles, then commentAuthors when the deck has any comment, then the embedded
|
|
2125
|
+
* fonts.
|
|
2126
|
+
*
|
|
2127
|
+
* The rels writer and `makeXmlPresentation` both need these numbers — the presentation part
|
|
2128
|
+
* names the notesMaster and each embedded face by `r:id` — and each derived them separately,
|
|
2129
|
+
* one as `slides.length + 2` under a comment saying it copied the other's logic. Inserting a
|
|
2130
|
+
* relationship before `notesMaster` then broke the presentation's own reference with no
|
|
2131
|
+
* signal.
|
|
2132
|
+
* @param slides - the deck's slides, whose count and comments decide the layout
|
|
2133
|
+
*/
|
|
2134
|
+
function presentationFixedRelIds(slides) {
|
|
2135
|
+
const first = (slides?.length ?? 0) + 2;
|
|
2136
|
+
const hasComments = (slides || []).some((slide) => (slide._comments || []).length > 0);
|
|
2137
|
+
return {
|
|
2138
|
+
notesMaster: first,
|
|
2139
|
+
presProps: first + 1,
|
|
2140
|
+
viewProps: first + 2,
|
|
2141
|
+
theme: first + 3,
|
|
2142
|
+
tableStyles: first + 4,
|
|
2143
|
+
commentAuthors: hasComments ? first + 5 : null,
|
|
2144
|
+
fontStart: first + (hasComments ? 6 : 5)
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
/**
|
|
2148
|
+
* The first relationship id free for embedded-font rels — {@link presentationFixedRelIds}'
|
|
2149
|
+
* `fontStart`, kept as its own name because that is what the two font walks ask for.
|
|
2150
|
+
*/
|
|
2151
|
+
function presentationFontRelStart(slides) {
|
|
2152
|
+
return presentationFixedRelIds(slides).fontStart;
|
|
2153
|
+
}
|
|
2154
|
+
function makeXmlPresentationRels(slides, embeddedFonts) {
|
|
2155
|
+
const fixed = presentationFixedRelIds(slides);
|
|
2156
|
+
const rels = [relationshipEl(1, SLIDE_MASTER_REL, targetFromPresentation(SLIDE_MASTER_PATH))];
|
|
2157
|
+
for (let idx = 1; idx <= slides.length; idx++) rels.push(relationshipEl(idx + 1, SLIDE_REL, targetFromPresentation(slidePath(idx))));
|
|
2158
|
+
rels.push(relationshipEl(fixed.notesMaster, NOTES_MASTER_REL, targetFromPresentation(NOTES_MASTER_PATH)), relationshipEl(fixed.presProps, PRES_PROPS_REL, "presProps.xml"), relationshipEl(fixed.viewProps, VIEW_PROPS_REL, "viewProps.xml"), relationshipEl(fixed.theme, THEME_REL, "theme/theme1.xml"), relationshipEl(fixed.tableStyles, TABLE_STYLES_REL, "tableStyles.xml"));
|
|
2159
|
+
if (fixed.commentAuthors !== null) rels.push(relationshipEl(fixed.commentAuthors, COMMENT_AUTHORS_REL, "commentAuthors.xml"));
|
|
2160
|
+
for (const face of flattenEmbeddedFaces(embeddedFonts || [], presentationFontRelStart(slides))) rels.push(relationshipEl(face.rId, FONT_REL_TYPE, targetFromPresentation(fontPath(face.partIndex))));
|
|
2161
|
+
return relationshipsPart(rels);
|
|
2162
|
+
}
|
|
2163
|
+
//#endregion
|
|
2164
|
+
//#region src/gen/pres/presentation.ts
|
|
2165
|
+
/**
|
|
2166
|
+
* ts-pptx: `ppt/presentation.xml` + presProps/viewProps
|
|
2167
|
+
*
|
|
2168
|
+
* Emit the presentation part (masters/notes/slide id lists, sizes, embedded-font
|
|
2169
|
+
* list, default text styles, sections) and the small presProps/viewProps parts.
|
|
2170
|
+
*/
|
|
2171
|
+
function defaultTextStyleLevel(idy, marL) {
|
|
2172
|
+
return lvlPPr(idy, {
|
|
2173
|
+
marL,
|
|
2174
|
+
algn: "l"
|
|
2175
|
+
}, [raw(themeFontDefRPr("mn", {
|
|
2176
|
+
sz: 1800,
|
|
2177
|
+
kern: 1200
|
|
2178
|
+
}))]);
|
|
2179
|
+
}
|
|
2180
|
+
function sectionsExtLst(sections) {
|
|
2181
|
+
return el("p:extLst", null, [raw(el("p:ext", { uri: "{521415D9-36F7-43E2-AB2F-B90AF26B5E84}" }, raw(el("p14:sectionLst", { "xmlns:p14": OOXML_NS.p14 }, sections.map((sect) => raw(el("p14:section", {
|
|
2182
|
+
name: sect.title,
|
|
2183
|
+
id: sect._id
|
|
2184
|
+
}, raw(el("p14:sldIdLst", null, sect._slides.map((slide) => raw(voidEl("p14:sldId", { id: slide._slideId })))))))))))), raw(el("p:ext", { uri: "{EFAFB233-063F-42B5-8137-9DF3F51BA10A}" }, raw(voidEl("p15:sldGuideLst", { "xmlns:p15": OOXML_NS.p15 }))))]);
|
|
2185
|
+
}
|
|
2186
|
+
/**
|
|
2187
|
+
* Create presentation file (`ppt/presentation.xml`)
|
|
2188
|
+
* @see https://docs.microsoft.com/en-us/office/open-xml/structure-of-a-presentationml-document
|
|
2189
|
+
* @see http://www.datypic.com/sc/ooxml/t-p_CT_Presentation.html
|
|
2190
|
+
* @param {PresentationPropsInternal} pres - presentation
|
|
2191
|
+
* @return {string} XML
|
|
2192
|
+
*/
|
|
2193
|
+
function makeXmlPresentation(pres) {
|
|
2194
|
+
const openTag = `<p:presentation xmlns:a="${OOXML_NS.a}" xmlns:r="${OOXML_NS.r}" xmlns:p="${OOXML_NS.p}" ${pres.rtlMode ? "rtl=\"1\"" : ""} ${(pres.embeddedFonts || []).some((font) => font.faces.some((face) => face.bytes)) ? "embedTrueTypeFonts=\"1\" saveSubsetFonts=\"0\"" : "saveSubsetFonts=\"1\""} autoCompressPictures="0"${pres.firstSlideNum !== 1 ? ` firstSlideNum="${pres.firstSlideNum}"` : ""}>`;
|
|
2195
|
+
const sldMasterIdLst = el("p:sldMasterIdLst", null, raw(voidEl("p:sldMasterId", {
|
|
2196
|
+
id: MIN_SLIDE_MASTER_ID,
|
|
2197
|
+
"r:id": "rId1"
|
|
2198
|
+
})));
|
|
2199
|
+
const notesMasterIdLst = el("p:notesMasterIdLst", null, raw(voidEl("p:notesMasterId", { "r:id": `rId${presentationFixedRelIds(pres.slides).notesMaster}` })));
|
|
2200
|
+
const sldIdLst = el("p:sldIdLst", null, pres.slides.map((slide) => raw(voidEl("p:sldId", {
|
|
2201
|
+
id: slide._slideId,
|
|
2202
|
+
"r:id": `rId${slide._rId}`
|
|
2203
|
+
}))));
|
|
2204
|
+
const { width, height } = pres.presLayout;
|
|
2205
|
+
const sldSz = voidEl("p:sldSz", {
|
|
2206
|
+
cx: width,
|
|
2207
|
+
cy: height,
|
|
2208
|
+
type: slideSizeType(width, height)
|
|
2209
|
+
});
|
|
2210
|
+
const notesSz = voidEl("p:notesSz", {
|
|
2211
|
+
cx: pres.presLayout.height,
|
|
2212
|
+
cy: pres.presLayout.width
|
|
2213
|
+
});
|
|
2214
|
+
const fonts = pres.embeddedFonts || [];
|
|
2215
|
+
const flat = flattenEmbeddedFaces(fonts, presentationFontRelStart(pres.slides));
|
|
2216
|
+
const rIdOf = new Map(flat.map((face) => [`${face.fontIndex}:${face.slot}`, face.rId]));
|
|
2217
|
+
const embeddedFontLst = serializeEmbeddedFontLst(fonts, (fontIndex, slot) => rIdOf.get(`${fontIndex}:${slot}`));
|
|
2218
|
+
const defaultTextStyle = el("p:defaultTextStyle", null, LEVEL_MARGINS_EMU.map((marL, i) => raw(defaultTextStyleLevel(i + 1, marL))));
|
|
2219
|
+
const extLst = pres.sections && pres.sections.length > 0 ? sectionsExtLst(pres.sections) : "";
|
|
2220
|
+
return XML_DECL + "\r\n" + openTag + sldMasterIdLst + notesMasterIdLst + sldIdLst + sldSz + notesSz + embeddedFontLst + defaultTextStyle + extLst + "</p:presentation>";
|
|
2221
|
+
}
|
|
2222
|
+
/**
|
|
2223
|
+
* Create `ppt/presProps.xml`
|
|
2224
|
+
* @return {string} XML
|
|
2225
|
+
*/
|
|
2226
|
+
function makeXmlPresProps() {
|
|
2227
|
+
return XML_DECL + "\r\n" + voidEl("p:presentationPr", { ...PML_ROOT_NS });
|
|
2228
|
+
}
|
|
2229
|
+
/**
|
|
2230
|
+
* Creates `ppt/viewProps.xml`
|
|
2231
|
+
* @return {string} XML
|
|
2232
|
+
*/
|
|
2233
|
+
function makeXmlViewProps() {
|
|
2234
|
+
return XML_DECL + "\r\n" + el("p:viewPr", { ...PML_ROOT_NS }, [
|
|
2235
|
+
raw(el("p:normalViewPr", { horzBarState: "maximized" }, [raw(voidEl("p:restoredLeft", { sz: 15611 })), raw(voidEl("p:restoredTop", { sz: 94610 }))])),
|
|
2236
|
+
raw(el("p:slideViewPr", null, raw(el("p:cSldViewPr", {
|
|
2237
|
+
snapToGrid: 0,
|
|
2238
|
+
snapToObjects: 1
|
|
2239
|
+
}, [raw(el("p:cViewPr", { varScale: 1 }, [raw(el("p:scale", null, [raw(voidEl("a:sx", {
|
|
2240
|
+
n: 136,
|
|
2241
|
+
d: 100
|
|
2242
|
+
})), raw(voidEl("a:sy", {
|
|
2243
|
+
n: 136,
|
|
2244
|
+
d: 100
|
|
2245
|
+
}))])), raw(voidEl("p:origin", {
|
|
2246
|
+
x: 216,
|
|
2247
|
+
y: 312
|
|
2248
|
+
}))])), raw(voidEl("p:guideLst"))])))),
|
|
2249
|
+
raw(el("p:notesTextViewPr", null, raw(el("p:cViewPr", null, [raw(el("p:scale", null, [raw(voidEl("a:sx", {
|
|
2250
|
+
n: 1,
|
|
2251
|
+
d: 1
|
|
2252
|
+
})), raw(voidEl("a:sy", {
|
|
2253
|
+
n: 1,
|
|
2254
|
+
d: 1
|
|
2255
|
+
}))])), raw(voidEl("p:origin", {
|
|
2256
|
+
x: 0,
|
|
2257
|
+
y: 0
|
|
2258
|
+
}))])))),
|
|
2259
|
+
raw(voidEl("p:gridSpacing", {
|
|
2260
|
+
cx: 76200,
|
|
2261
|
+
cy: 76200
|
|
2262
|
+
}))
|
|
2263
|
+
]);
|
|
2264
|
+
}
|
|
2265
|
+
//#endregion
|
|
2266
|
+
//#region src/gen/pres/table-styles.ts
|
|
2267
|
+
/**
|
|
2268
|
+
* Create `ppt/tableStyles.xml`
|
|
2269
|
+
* @see: http://openxmldeveloper.org/discussions/formats/f/13/p/2398/8107.aspx
|
|
2270
|
+
* @return {string} XML
|
|
2271
|
+
*/
|
|
2272
|
+
function makeXmlTableStyles() {
|
|
2273
|
+
return XML_DECL + "\r\n" + voidEl("a:tblStyleLst", {
|
|
2274
|
+
"xmlns:a": OOXML_NS.a,
|
|
2275
|
+
def: "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2278
|
+
//#endregion
|
|
2279
|
+
//#region src/gen/slide/layout.ts
|
|
2280
|
+
/**
|
|
2281
|
+
* ts-pptx: slide-layout part
|
|
2282
|
+
*
|
|
2283
|
+
* Emit a slide layout (`ppt/slideLayouts/slideLayoutN.xml`) from a layout object.
|
|
2284
|
+
*/
|
|
2285
|
+
/**
|
|
2286
|
+
* Generates the XML layout resource from a layout object
|
|
2287
|
+
* @param layout - slide layout (master)
|
|
2288
|
+
* @param renderers - which renderer emits each shape family, passed through to the shape walk
|
|
2289
|
+
* @return XML
|
|
2290
|
+
*/
|
|
2291
|
+
function makeXmlLayout(layout, renderers) {
|
|
2292
|
+
return XML_DECL + el("p:sldLayout", {
|
|
2293
|
+
...PML_ROOT_NS,
|
|
2294
|
+
preserve: "1"
|
|
2295
|
+
}, [raw(slideObjectToXml(layout, renderers)), raw(el("p:clrMapOvr", null, raw(voidEl("a:masterClrMapping"))))], {
|
|
2296
|
+
openPrefix: "\n ",
|
|
2297
|
+
childPrefix: "\n "
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
//#endregion
|
|
2301
|
+
//#region src/gen/slide/master.ts
|
|
2302
|
+
/**
|
|
2303
|
+
* ts-pptx: slide-master parts
|
|
2304
|
+
*
|
|
2305
|
+
* The slide master (`slideMaster1.xml`) and its `.rels`, plus the master text
|
|
2306
|
+
* styles (`<p:txStyles>`): per-level default values mirroring the built-in
|
|
2307
|
+
* Office master, with any `MasterTextStyleProps` overrides layered on top.
|
|
2308
|
+
*/
|
|
2309
|
+
const MASTER_TITLE_DEFAULT = {
|
|
2310
|
+
marL: 0,
|
|
2311
|
+
algn: "ctr",
|
|
2312
|
+
spcBefPct: 0,
|
|
2313
|
+
bu: "none",
|
|
2314
|
+
sz: 4400,
|
|
2315
|
+
font: "mj"
|
|
2316
|
+
};
|
|
2317
|
+
const MASTER_BODY_DEFAULTS = [
|
|
2318
|
+
{
|
|
2319
|
+
marL: 342900,
|
|
2320
|
+
indent: -342900,
|
|
2321
|
+
algn: "l",
|
|
2322
|
+
spcBefPct: 2e4,
|
|
2323
|
+
bu: {
|
|
2324
|
+
char: "•",
|
|
2325
|
+
font: "Arial"
|
|
2326
|
+
},
|
|
2327
|
+
sz: 3200,
|
|
2328
|
+
font: "mn"
|
|
2329
|
+
},
|
|
2330
|
+
{
|
|
2331
|
+
marL: 742950,
|
|
2332
|
+
indent: -285750,
|
|
2333
|
+
algn: "l",
|
|
2334
|
+
spcBefPct: 2e4,
|
|
2335
|
+
bu: {
|
|
2336
|
+
char: "–",
|
|
2337
|
+
font: "Arial"
|
|
2338
|
+
},
|
|
2339
|
+
sz: 2800,
|
|
2340
|
+
font: "mn"
|
|
2341
|
+
},
|
|
2342
|
+
{
|
|
2343
|
+
marL: 1143e3,
|
|
2344
|
+
indent: -228600,
|
|
2345
|
+
algn: "l",
|
|
2346
|
+
spcBefPct: 2e4,
|
|
2347
|
+
bu: {
|
|
2348
|
+
char: "•",
|
|
2349
|
+
font: "Arial"
|
|
2350
|
+
},
|
|
2351
|
+
sz: 2400,
|
|
2352
|
+
font: "mn"
|
|
2353
|
+
},
|
|
2354
|
+
{
|
|
2355
|
+
marL: 1600200,
|
|
2356
|
+
indent: -228600,
|
|
2357
|
+
algn: "l",
|
|
2358
|
+
spcBefPct: 2e4,
|
|
2359
|
+
bu: {
|
|
2360
|
+
char: "–",
|
|
2361
|
+
font: "Arial"
|
|
2362
|
+
},
|
|
2363
|
+
sz: 2e3,
|
|
2364
|
+
font: "mn"
|
|
2365
|
+
},
|
|
2366
|
+
{
|
|
2367
|
+
marL: 2057400,
|
|
2368
|
+
indent: -228600,
|
|
2369
|
+
algn: "l",
|
|
2370
|
+
spcBefPct: 2e4,
|
|
2371
|
+
bu: {
|
|
2372
|
+
char: "»",
|
|
2373
|
+
font: "Arial"
|
|
2374
|
+
},
|
|
2375
|
+
sz: 2e3,
|
|
2376
|
+
font: "mn"
|
|
2377
|
+
},
|
|
2378
|
+
{
|
|
2379
|
+
marL: 2514600,
|
|
2380
|
+
indent: -228600,
|
|
2381
|
+
algn: "l",
|
|
2382
|
+
spcBefPct: 2e4,
|
|
2383
|
+
bu: {
|
|
2384
|
+
char: "•",
|
|
2385
|
+
font: "Arial"
|
|
2386
|
+
},
|
|
2387
|
+
sz: 2e3,
|
|
2388
|
+
font: "mn"
|
|
2389
|
+
},
|
|
2390
|
+
{
|
|
2391
|
+
marL: 2971800,
|
|
2392
|
+
indent: -228600,
|
|
2393
|
+
algn: "l",
|
|
2394
|
+
spcBefPct: 2e4,
|
|
2395
|
+
bu: {
|
|
2396
|
+
char: "•",
|
|
2397
|
+
font: "Arial"
|
|
2398
|
+
},
|
|
2399
|
+
sz: 2e3,
|
|
2400
|
+
font: "mn"
|
|
2401
|
+
},
|
|
2402
|
+
{
|
|
2403
|
+
marL: 3429e3,
|
|
2404
|
+
indent: -228600,
|
|
2405
|
+
algn: "l",
|
|
2406
|
+
spcBefPct: 2e4,
|
|
2407
|
+
bu: {
|
|
2408
|
+
char: "•",
|
|
2409
|
+
font: "Arial"
|
|
2410
|
+
},
|
|
2411
|
+
sz: 2e3,
|
|
2412
|
+
font: "mn"
|
|
2413
|
+
},
|
|
2414
|
+
{
|
|
2415
|
+
marL: 3886200,
|
|
2416
|
+
indent: -228600,
|
|
2417
|
+
algn: "l",
|
|
2418
|
+
spcBefPct: 2e4,
|
|
2419
|
+
bu: {
|
|
2420
|
+
char: "•",
|
|
2421
|
+
font: "Arial"
|
|
2422
|
+
},
|
|
2423
|
+
sz: 2e3,
|
|
2424
|
+
font: "mn"
|
|
2425
|
+
}
|
|
2426
|
+
];
|
|
2427
|
+
const MASTER_OTHER_DEFAULTS = LEVEL_MARGINS_EMU.map((marL) => ({
|
|
2428
|
+
marL,
|
|
2429
|
+
algn: "l",
|
|
2430
|
+
sz: 1800,
|
|
2431
|
+
font: "mn"
|
|
2432
|
+
}));
|
|
2433
|
+
function masterAlignAttr(align) {
|
|
2434
|
+
return align === void 0 ? "" : textAlignToken(align) ?? "";
|
|
2435
|
+
}
|
|
2436
|
+
/** Build the bullet element for a master level: caller override wins over the level default. */
|
|
2437
|
+
function masterBulletXml(bulletOverride, base) {
|
|
2438
|
+
if (bulletOverride === false) return voidEl("a:buNone");
|
|
2439
|
+
if (bulletOverride && typeof bulletOverride === "object") {
|
|
2440
|
+
const font = bulletOverride.fontFace ? voidEl("a:buFont", { typeface: bulletOverride.fontFace }) : "";
|
|
2441
|
+
if (bulletOverride.type === "number") {
|
|
2442
|
+
const startAt = typeof bulletOverride.numberStartAt === "number" ? bulletOverride.numberStartAt : void 0;
|
|
2443
|
+
return font + buAutoNumEl(bulletOverride.numberType || "arabicPeriod", startAt, "master textStyles bullet");
|
|
2444
|
+
}
|
|
2445
|
+
return (bulletOverride.fontFace ? font : voidEl("a:buFont", {
|
|
2446
|
+
typeface: "Arial",
|
|
2447
|
+
pitchFamily: 34,
|
|
2448
|
+
charset: 0
|
|
2449
|
+
})) + buCharEl(bulletOverride.characterCode, "•", "master textStyles bullet");
|
|
2450
|
+
}
|
|
2451
|
+
if (base === "none") return voidEl("a:buNone");
|
|
2452
|
+
if (base && typeof base === "object") return voidEl("a:buFont", {
|
|
2453
|
+
typeface: base.font,
|
|
2454
|
+
pitchFamily: 34,
|
|
2455
|
+
charset: 0
|
|
2456
|
+
}) + buCharEl(void 0, base.char, "master textStyles bullet");
|
|
2457
|
+
return "";
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* One length-valued override on a master text-style level, in EMU, or the level's default when the
|
|
2461
|
+
* caller stated something we cannot use.
|
|
2462
|
+
*
|
|
2463
|
+
* `marginLeft` and `indent` used to reach their clamps only when they were finite numbers and fall
|
|
2464
|
+
* back in silence otherwise, so `NaN`, `Infinity` and a string such as `'0.5in'` all became the
|
|
2465
|
+
* Office default with nothing said -- while `fontSize` on the very same level warned for exactly
|
|
2466
|
+
* those inputs. Silent coercion of invalid input is a footgun: the deck comes out wrong and the
|
|
2467
|
+
* caller has nothing to go on.
|
|
2468
|
+
*
|
|
2469
|
+
* `undefined` is the caller stating nothing, which is not a problem and is not reported.
|
|
2470
|
+
* @param stated - the caller's value, in inches
|
|
2471
|
+
* @param option - the option's name, as `MasterTextStyleLevel` spells it
|
|
2472
|
+
* @param fallback - the level's default, already in EMU
|
|
2473
|
+
* @param resolve - the clamp that converts a usable value to EMU
|
|
2474
|
+
*/
|
|
2475
|
+
function masterLevelLength(stated, option, fallback, resolve) {
|
|
2476
|
+
if (stated === void 0) return fallback;
|
|
2477
|
+
if (typeof stated === "number" && Number.isFinite(stated)) return resolve(stated);
|
|
2478
|
+
const kept = fallback === void 0 ? "the level default" : `${fallback / EMU_PER_INCH}in`;
|
|
2479
|
+
warn("master/invalid-text-style-length", `master textStyles ${option} "${String(stated)}" is invalid; keeping ${kept}.`);
|
|
2480
|
+
return fallback;
|
|
2481
|
+
}
|
|
2482
|
+
/** Serialize one `<a:lvlNpPr>` from its default, layering an optional caller override. */
|
|
2483
|
+
function masterLevelXml(levelNum, base, levelOverride = {}) {
|
|
2484
|
+
const marL = masterLevelLength(levelOverride.marginLeft, "marginLeft", base.marL, (inches) => clampParaMarginInchesEmu(inches, "master textStyles marginLeft"));
|
|
2485
|
+
const indentEmu = masterLevelLength(levelOverride.indent, "indent", base.indent, (inches) => clampParaIndentInchesEmu(inches, "master textStyles indent"));
|
|
2486
|
+
const algn = levelOverride.align && masterAlignAttr(levelOverride.align) || base.algn;
|
|
2487
|
+
let sz = base.sz;
|
|
2488
|
+
if (levelOverride.fontSize !== void 0) {
|
|
2489
|
+
if (typeof levelOverride.fontSize !== "number" || !Number.isFinite(levelOverride.fontSize) || levelOverride.fontSize <= 0) warn("master/invalid-text-style-font-size", `master textStyles fontSize "${String(levelOverride.fontSize)}" is invalid; keeping default ${base.sz / 100}pt.`);
|
|
2490
|
+
else sz = clampFontSizeSz(levelOverride.fontSize, "master textStyles fontSize");
|
|
2491
|
+
}
|
|
2492
|
+
rejectEmptyColor(levelOverride.color, "textStyles color");
|
|
2493
|
+
const colorXml = levelOverride.color ? createColorElement(levelOverride.color) : voidEl("a:schemeClr", { val: "tx1" });
|
|
2494
|
+
const latinXml = levelOverride.fontFace ? voidEl("a:latin", { typeface: levelOverride.fontFace }) : voidEl("a:latin", { typeface: `+${base.font}-lt` });
|
|
2495
|
+
return lvlPPr(levelNum, {
|
|
2496
|
+
marL,
|
|
2497
|
+
indent: indentEmu,
|
|
2498
|
+
algn
|
|
2499
|
+
}, [
|
|
2500
|
+
typeof base.spcBefPct === "number" ? raw(el("a:spcBef", null, raw(voidEl("a:spcPct", { val: base.spcBefPct })))) : null,
|
|
2501
|
+
raw(masterBulletXml(levelOverride.bullet, base.bu)),
|
|
2502
|
+
raw(themeFontDefRPr(base.font, {
|
|
2503
|
+
sz,
|
|
2504
|
+
b: xsdBoolIfTrue(levelOverride.bold),
|
|
2505
|
+
i: xsdBoolIfTrue(levelOverride.italic),
|
|
2506
|
+
kern: 1200
|
|
2507
|
+
}, colorXml, latinXml))
|
|
2508
|
+
]);
|
|
2509
|
+
}
|
|
2510
|
+
/** Clamp a caller-provided per-level override array to the 9 valid list levels, warning on overflow. */
|
|
2511
|
+
function masterLevelOverrides(levels, group) {
|
|
2512
|
+
if (!Array.isArray(levels)) return [];
|
|
2513
|
+
if (levels.length > 9) warn("master/too-many-text-style-levels", `master textStyles.${group} has ${levels.length} levels; only the first 9 are used.`);
|
|
2514
|
+
return levels.slice(0, 9);
|
|
2515
|
+
}
|
|
2516
|
+
/**
|
|
2517
|
+
* Build the `<p:txStyles>` block from caller overrides layered onto the Office master defaults.
|
|
2518
|
+
* Only invoked when `defineSlideMaster({ textStyles })` was set; the unconfigured deck keeps the
|
|
2519
|
+
* verbatim default literal in `makeXmlMaster` for byte-identical output.
|
|
2520
|
+
*/
|
|
2521
|
+
function makeXmlMasterTxStyles(textStyles) {
|
|
2522
|
+
const title = masterLevelXml(1, MASTER_TITLE_DEFAULT, textStyles.title);
|
|
2523
|
+
const bodyOverrides = masterLevelOverrides(textStyles.body, "body");
|
|
2524
|
+
const body = MASTER_BODY_DEFAULTS.map((base, i) => masterLevelXml(i + 1, base, bodyOverrides[i])).join("");
|
|
2525
|
+
const otherOverrides = masterLevelOverrides(textStyles.other, "other");
|
|
2526
|
+
const other = MASTER_OTHER_DEFAULTS.map((base, i) => masterLevelXml(i + 1, base, otherOverrides[i])).join("");
|
|
2527
|
+
return el("p:txStyles", null, [
|
|
2528
|
+
raw(el("p:titleStyle", null, raw(title))),
|
|
2529
|
+
raw(el("p:bodyStyle", null, raw(body))),
|
|
2530
|
+
raw(el("p:otherStyle", null, [raw(el("a:defPPr", null, raw(voidEl("a:defRPr", { lang: "en-US" })))), raw(other)]))
|
|
2531
|
+
]);
|
|
2532
|
+
}
|
|
2533
|
+
/**
|
|
2534
|
+
* The built-in Office master's `<p:txStyles>`, used verbatim when `defineSlideMaster({ textStyles })`
|
|
2535
|
+
* was never called. Deliberately NOT built via `masterLevelXml`: a configured title level always
|
|
2536
|
+
* carries `marL="0"` (from `MASTER_TITLE_DEFAULT.marL`), but the true unconfigured default titleStyle
|
|
2537
|
+
* has no `marL`/`indent` attribute at all — a pre-existing asymmetry between the two paths, preserved
|
|
2538
|
+
* exactly rather than unified.
|
|
2539
|
+
*/
|
|
2540
|
+
function makeXmlMasterDefaultTxStyles() {
|
|
2541
|
+
const defaultLevel = (n, attrs, bullet, sz, font, spcBefPct) => lvlPPr(n, attrs, [
|
|
2542
|
+
typeof spcBefPct === "number" ? raw(el("a:spcBef", null, raw(voidEl("a:spcPct", { val: spcBefPct })))) : null,
|
|
2543
|
+
raw(bullet),
|
|
2544
|
+
raw(themeFontDefRPr(font, {
|
|
2545
|
+
sz,
|
|
2546
|
+
kern: 1200
|
|
2547
|
+
}))
|
|
2548
|
+
]);
|
|
2549
|
+
const title = defaultLevel(1, { algn: "ctr" }, voidEl("a:buNone"), 4400, "mj", 0);
|
|
2550
|
+
const bodyLevels = MASTER_BODY_DEFAULTS.map((base, i) => {
|
|
2551
|
+
const bu = base.bu;
|
|
2552
|
+
return defaultLevel(i + 1, {
|
|
2553
|
+
marL: base.marL,
|
|
2554
|
+
indent: base.indent,
|
|
2555
|
+
algn: base.algn
|
|
2556
|
+
}, voidEl("a:buFont", {
|
|
2557
|
+
typeface: bu.font,
|
|
2558
|
+
pitchFamily: 34,
|
|
2559
|
+
charset: 0
|
|
2560
|
+
}) + voidEl("a:buChar", { char: bu.char }), base.sz, "mn", base.spcBefPct);
|
|
2561
|
+
});
|
|
2562
|
+
const otherLevels = MASTER_OTHER_DEFAULTS.map((base, i) => defaultLevel(i + 1, {
|
|
2563
|
+
marL: base.marL,
|
|
2564
|
+
algn: base.algn
|
|
2565
|
+
}, "", base.sz, "mn"));
|
|
2566
|
+
return el("p:txStyles", null, [
|
|
2567
|
+
raw(el("p:titleStyle", null, raw(title), {
|
|
2568
|
+
childPrefix: " ",
|
|
2569
|
+
closePrefix: " "
|
|
2570
|
+
})),
|
|
2571
|
+
raw(el("p:bodyStyle", null, bodyLevels.map(raw), {
|
|
2572
|
+
childPrefix: " ",
|
|
2573
|
+
closePrefix: " "
|
|
2574
|
+
})),
|
|
2575
|
+
raw(el("p:otherStyle", null, [raw(el("a:defPPr", null, raw(voidEl("a:defRPr", { lang: "en-US" })))), ...otherLevels.map(raw)], {
|
|
2576
|
+
childPrefix: " ",
|
|
2577
|
+
closePrefix: " "
|
|
2578
|
+
}))
|
|
2579
|
+
], { childPrefix: " " });
|
|
2580
|
+
}
|
|
2581
|
+
/**
|
|
2582
|
+
* Creates Slide Master 1 (`ppt/slideMasters/slideMaster1.xml`)
|
|
2583
|
+
* @param slide - slide object that represents master slide layout
|
|
2584
|
+
* @param layouts - slide layouts
|
|
2585
|
+
* @param renderers - which renderer emits each shape family, passed through to the shape walk
|
|
2586
|
+
* @return XML
|
|
2587
|
+
*/
|
|
2588
|
+
function makeXmlMaster(slide, layouts, renderers) {
|
|
2589
|
+
const layoutRidStart = defaultRelIdStart(slide);
|
|
2590
|
+
const layoutDefs = layouts.map((_layoutDef, idx) => voidEl("p:sldLayoutId", {
|
|
2591
|
+
id: LAYOUT_IDX_SERIES_BASE + idx,
|
|
2592
|
+
"r:id": `rId${layoutRidStart + idx}`
|
|
2593
|
+
})).join("");
|
|
2594
|
+
const clrMap = voidEl("p:clrMap", DEFAULT_COLOR_MAP);
|
|
2595
|
+
const hf = voidEl("p:hf", {
|
|
2596
|
+
sldNum: slide._slideNumberProps ? null : 0,
|
|
2597
|
+
hdr: 0,
|
|
2598
|
+
ftr: 0,
|
|
2599
|
+
dt: 0
|
|
2600
|
+
});
|
|
2601
|
+
const txStyles = slide._txStyles ? makeXmlMasterTxStyles(slide._txStyles) : makeXmlMasterDefaultTxStyles();
|
|
2602
|
+
return XML_DECL + "\r\n" + el("p:sldMaster", { ...PML_ROOT_NS }, [
|
|
2603
|
+
raw(slideObjectToXml(slide, renderers)),
|
|
2604
|
+
raw(clrMap),
|
|
2605
|
+
raw(el("p:sldLayoutIdLst", null, raw(layoutDefs))),
|
|
2606
|
+
raw(hf),
|
|
2607
|
+
raw(txStyles)
|
|
2608
|
+
]);
|
|
2609
|
+
}
|
|
2610
|
+
/**
|
|
2611
|
+
* Creates `ppt/slideMasters/_rels/slideMaster1.xml.rels`
|
|
2612
|
+
* @param {SlideMasterInternal} masterSlide - the deck's slide master
|
|
2613
|
+
* @param {SlideLayoutInternal[]} slideLayouts - Slide Layouts
|
|
2614
|
+
* @return {string} XML
|
|
2615
|
+
*/
|
|
2616
|
+
function makeXmlMasterRel(masterSlide, slideLayouts) {
|
|
2617
|
+
const defaultRels = slideLayouts.map((_layoutDef, idx) => ({
|
|
2618
|
+
target: targetFromPptSubpart(slideLayoutPath(idx + 1)),
|
|
2619
|
+
type: SLIDE_LAYOUT_REL
|
|
2620
|
+
}));
|
|
2621
|
+
defaultRels.push({
|
|
2622
|
+
target: "../theme/theme1.xml",
|
|
2623
|
+
type: THEME_REL
|
|
2624
|
+
});
|
|
2625
|
+
return slideObjectRelationsToXml(masterSlide, defaultRels, SLIDE_MASTER_PATH);
|
|
2626
|
+
}
|
|
2627
|
+
//#endregion
|
|
2628
|
+
//#region src/package/parts/shared.ts
|
|
2629
|
+
/**
|
|
2630
|
+
* The contributors in emission order. Sorting here rather than trusting the caller's array is the
|
|
2631
|
+
* whole point of {@link PartContributor.order}; the sort is stable, so two families that share a
|
|
2632
|
+
* rank keep their listed order rather than swapping unpredictably.
|
|
2633
|
+
*/
|
|
2634
|
+
function orderedContributors(contributors) {
|
|
2635
|
+
return [...contributors].sort((a, b) => a.order - b.order);
|
|
2636
|
+
}
|
|
2637
|
+
/**
|
|
2638
|
+
* Ask every contributor for its content-type entries and flatten them into the slot-keyed data
|
|
2639
|
+
* `makeXmlContTypes` emits. Takes the contributors already ordered — within a slot the entries
|
|
2640
|
+
* come out in the order they are collected here.
|
|
2641
|
+
*/
|
|
2642
|
+
function collectContentTypes(contributors, pres) {
|
|
2643
|
+
const defaults = [];
|
|
2644
|
+
const presentation = [];
|
|
2645
|
+
const trailing = [];
|
|
2646
|
+
const perSlide = pres.slides.map(() => []);
|
|
2647
|
+
const perLayout = pres.slideLayouts.map(() => []);
|
|
2648
|
+
for (const contributor of contributors) {
|
|
2649
|
+
const hooks = contributor.contentTypes;
|
|
2650
|
+
if (!hooks) continue;
|
|
2651
|
+
if (hooks.defaults) defaults.push(...hooks.defaults(pres));
|
|
2652
|
+
if (hooks.presentation) presentation.push(...hooks.presentation(pres));
|
|
2653
|
+
if (hooks.perSlide) {
|
|
2654
|
+
const perSlideHook = hooks.perSlide;
|
|
2655
|
+
pres.slides.forEach((slide, idx) => perSlide[idx]?.push(...perSlideHook(slide, idx + 1)));
|
|
2656
|
+
}
|
|
2657
|
+
if (hooks.perLayout) {
|
|
2658
|
+
const perLayoutHook = hooks.perLayout;
|
|
2659
|
+
pres.slideLayouts.forEach((layout, idx) => perLayout[idx]?.push(...perLayoutHook(layout, idx + 1)));
|
|
2660
|
+
}
|
|
2661
|
+
if (hooks.trailing) trailing.push(...hooks.trailing(pres));
|
|
2662
|
+
}
|
|
2663
|
+
return {
|
|
2664
|
+
defaults,
|
|
2665
|
+
presentation,
|
|
2666
|
+
perSlide,
|
|
2667
|
+
perLayout,
|
|
2668
|
+
trailing
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2671
|
+
//#endregion
|
|
2672
|
+
//#region src/package/assemble.ts
|
|
2673
|
+
/**
|
|
2674
|
+
* Package assembly: turn an authored presentation's internal state into the full set of
|
|
2675
|
+
* OOXML package parts and hand the bytes to the ZIP writer. This is the write-side
|
|
2676
|
+
* "packaging" layer — `[Content_Types].xml`, the `_rels` graph, docProps, theme, the
|
|
2677
|
+
* per-slide/layout/master parts, and media rels — split out of the authoring class
|
|
2678
|
+
* (`TsPptx`) so that class stays a façade over slide authoring. The parts a construct
|
|
2679
|
+
* family adds on top of that skeleton — charts, comments, notes — come from the part
|
|
2680
|
+
* contributors it is handed, so nothing here names a family.
|
|
2681
|
+
*
|
|
2682
|
+
* The entry point `writePackage` takes a structural {@link PackageSource} that the
|
|
2683
|
+
* authoring class satisfies; it does not depend on the class itself, so the same pipeline
|
|
2684
|
+
* can be driven from any assembled deck state.
|
|
2685
|
+
*/
|
|
2686
|
+
/**
|
|
2687
|
+
* Media extensions whose bytes are already entropy-coded, so running the ZIP's
|
|
2688
|
+
* DEFLATE pass over them costs CPU for a negligible size gain. For these we set
|
|
2689
|
+
* the per-entry ZIP compression to STORE while leaving XML parts on DEFLATE.
|
|
2690
|
+
* In image/video-heavy decks media dominates the byte count, so this is the
|
|
2691
|
+
* dominant cost when writing large presentations.
|
|
2692
|
+
* Formats that genuinely benefit from DEFLATE (bmp, wav, tiff, emf, wmf, svg)
|
|
2693
|
+
* are deliberately excluded so they keep inheriting the global compression.
|
|
2694
|
+
*/
|
|
2695
|
+
const ALREADY_COMPRESSED_MEDIA_EXTN = /* @__PURE__ */ new Set([
|
|
2696
|
+
"jpg",
|
|
2697
|
+
"jpeg",
|
|
2698
|
+
"png",
|
|
2699
|
+
"gif",
|
|
2700
|
+
"webp",
|
|
2701
|
+
"heic",
|
|
2702
|
+
"heif",
|
|
2703
|
+
"avif",
|
|
2704
|
+
"mp4",
|
|
2705
|
+
"m4v",
|
|
2706
|
+
"mov",
|
|
2707
|
+
"avi",
|
|
2708
|
+
"mpg",
|
|
2709
|
+
"mpeg",
|
|
2710
|
+
"wmv",
|
|
2711
|
+
"webm",
|
|
2712
|
+
"mkv",
|
|
2713
|
+
"mp3",
|
|
2714
|
+
"m4a",
|
|
2715
|
+
"aac",
|
|
2716
|
+
"ogg",
|
|
2717
|
+
"oga"
|
|
2718
|
+
]);
|
|
2719
|
+
/**
|
|
2720
|
+
* Extensions whose payload is itself a ZIP archive — the embedded OPC packages an OLE object can
|
|
2721
|
+
* carry (`addOleObject`). Deflating a zip inside a zip buys nothing, so these are STOREd like the
|
|
2722
|
+
* already-compressed media above. No media/image rel ever uses one of these extensions, so decks
|
|
2723
|
+
* without an OLE object are unaffected.
|
|
2724
|
+
*/
|
|
2725
|
+
const ZIP_CONTAINER_EXTN = /* @__PURE__ */ new Set([
|
|
2726
|
+
"xlsx",
|
|
2727
|
+
"xlsm",
|
|
2728
|
+
"docx",
|
|
2729
|
+
"docm",
|
|
2730
|
+
"pptx",
|
|
2731
|
+
"pptm"
|
|
2732
|
+
]);
|
|
2733
|
+
/**
|
|
2734
|
+
* Each slide's registered transition sound.
|
|
2735
|
+
*
|
|
2736
|
+
* Kept per slide, never on the transition: one transition object can be assigned to several slides,
|
|
2737
|
+
* and a relationship id means something only inside the slide part that declares it. Kept across
|
|
2738
|
+
* writes as well, so a deck written twice registers each sound once. An entry is reused only while
|
|
2739
|
+
* the slide still holds its relationship and still has the same sound assigned.
|
|
2740
|
+
*/
|
|
2741
|
+
const registeredTransitionSounds = /* @__PURE__ */ new WeakMap();
|
|
2742
|
+
/**
|
|
2743
|
+
* Register an audio media part + relationship for each slide-transition start sound
|
|
2744
|
+
* (`transition.sound` with `data`/`path`), and return the relationship id each slide's
|
|
2745
|
+
* `p:sndAc/p:snd r:embed` refers to. Runs before media encoding so the bytes are loaded. Re-export
|
|
2746
|
+
* is safe: a slide whose sound is already registered reuses that relationship. The stop-previous
|
|
2747
|
+
* form (`sound.stopPrevious`) needs no part and is skipped.
|
|
2748
|
+
*
|
|
2749
|
+
* Registered here, at write time, rather than when `slide.transition` is assigned. A slide's rel
|
|
2750
|
+
* ids and media part names are handed out in registration order, and a transition is often set
|
|
2751
|
+
* before the slide's pictures and media are added; registering the sound then would move every
|
|
2752
|
+
* later rel id and part name on the slide.
|
|
2753
|
+
*/
|
|
2754
|
+
function registerTransitionSounds(slides) {
|
|
2755
|
+
const soundRIds = /* @__PURE__ */ new Map();
|
|
2756
|
+
slides.forEach((slide) => {
|
|
2757
|
+
const transition = slide.transition;
|
|
2758
|
+
if (!transition || !TRANSITION_TYPES.includes(transition.type)) return;
|
|
2759
|
+
const sound = transition.sound;
|
|
2760
|
+
if (!sound || sound.stopPrevious) return;
|
|
2761
|
+
if (!sound.data && !sound.path) return;
|
|
2762
|
+
const registered = registeredTransitionSounds.get(slide);
|
|
2763
|
+
if (registered && slide._relsMedia.includes(registered.rel) && registered.data === sound.data && registered.path === sound.path) {
|
|
2764
|
+
soundRIds.set(slide, registered.rel.rId);
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
const dataMime = dataUriMediaType(sound.data ?? "");
|
|
2768
|
+
const pathFile = sound.path ? (sound.path.split("/").pop() ?? "").split("?")[0] ?? "" : "";
|
|
2769
|
+
const extn = dataMime?.startsWith("audio/") ? audioExtensionForSubtype(dataMime.slice(6)) : (pathFile.split(".").pop() ?? "wav").toLowerCase();
|
|
2770
|
+
const rId = getNewRelId(slide);
|
|
2771
|
+
const rel = pushMediaRel(slide, {
|
|
2772
|
+
kind: "audio",
|
|
2773
|
+
extn,
|
|
2774
|
+
type: `audio/${extn}`,
|
|
2775
|
+
path: sound.path,
|
|
2776
|
+
data: sound.data,
|
|
2777
|
+
rId
|
|
2778
|
+
});
|
|
2779
|
+
registeredTransitionSounds.set(slide, {
|
|
2780
|
+
rel,
|
|
2781
|
+
data: sound.data,
|
|
2782
|
+
path: sound.path
|
|
2783
|
+
});
|
|
2784
|
+
soundRIds.set(slide, rId);
|
|
2785
|
+
});
|
|
2786
|
+
return soundRIds;
|
|
2787
|
+
}
|
|
2788
|
+
/**
|
|
2789
|
+
* Write the media parts one target's rels point at. Images are how every deck carries a picture,
|
|
2790
|
+
* so this stays on the core path; the chart half of what used to be one function is a part
|
|
2791
|
+
* contributor (`parts/chart.ts`) and runs just before it, per target, so part order is unchanged.
|
|
2792
|
+
*
|
|
2793
|
+
* After media de-duplication many relationships share one `Target`, across targets as well as
|
|
2794
|
+
* within one: a logo on every slide is one part. Each used to be decoded and added once per
|
|
2795
|
+
* relationship. A path is now written the first time it decodes, and skipped after that. The zip
|
|
2796
|
+
* keys entries by path and a re-added key keeps its first position, and relationships sharing a
|
|
2797
|
+
* `Target` share its data and extension, so skipping cannot change a byte.
|
|
2798
|
+
* @param {PartTarget} slide - slide, layout or master carrying the rels
|
|
2799
|
+
* @param {ZipWriter} zip - zip writer
|
|
2800
|
+
* @param {Set<string>} written - the media paths already added, shared across every target of one package
|
|
2801
|
+
*/
|
|
2802
|
+
function createMediaParts(slide, zip, written) {
|
|
2803
|
+
slide._relsMedia.forEach((rel) => {
|
|
2804
|
+
if (rel.type !== "online" && rel.type !== "hyperlink") {
|
|
2805
|
+
const path = rel.Target.replace("..", "ppt");
|
|
2806
|
+
if (written.has(path)) return;
|
|
2807
|
+
const data = rel.data && typeof rel.data === "string" ? rel.data : "";
|
|
2808
|
+
const bytes = decodeBase64ToBytes(data);
|
|
2809
|
+
if (!bytes) return;
|
|
2810
|
+
const extn = (rel.extn || rel.Target.split(".").pop() || "").toLowerCase();
|
|
2811
|
+
zip.add(path, bytes, { store: ALREADY_COMPRESSED_MEDIA_EXTN.has(extn) || ZIP_CONTAINER_EXTN.has(extn) });
|
|
2812
|
+
written.add(path);
|
|
2813
|
+
}
|
|
2814
|
+
});
|
|
2815
|
+
}
|
|
2816
|
+
/**
|
|
2817
|
+
* Assemble every package part for `source` and return them in emission order, without zipping.
|
|
2818
|
+
* This runs the transition-sound registration, placeholder backfill, media encode, cross-deck
|
|
2819
|
+
* media de-dup, chart-part-id assignment, and measured-fit passes before the synchronous
|
|
2820
|
+
* XML pass reads slide state. The bytes are the same the ZIP writer would compress; splitting the
|
|
2821
|
+
* assembly from the zip lets the byte-identity harness (and a future parts API) read parts
|
|
2822
|
+
* directly. Only `onMediaError` is meaningful here — compression/output shape are zip concerns
|
|
2823
|
+
* handled by {@link zipPackageParts}.
|
|
2824
|
+
*/
|
|
2825
|
+
async function buildPackageParts(source, props) {
|
|
2826
|
+
const pres = source.presentation;
|
|
2827
|
+
const partPromises = [];
|
|
2828
|
+
const contributors = orderedContributors(source.partContributors);
|
|
2829
|
+
const zip = new ZipWriter();
|
|
2830
|
+
requireSlideLinksInDeck(pres.slides, pres.slideLayouts, pres.masterSlide);
|
|
2831
|
+
const transitionSoundRIds = registerTransitionSounds(pres.slides);
|
|
2832
|
+
backfillPlaceholders(pres.slides);
|
|
2833
|
+
const onMediaError = props.onMediaError ?? "throw";
|
|
2834
|
+
const mediaTargets = [
|
|
2835
|
+
...pres.slides,
|
|
2836
|
+
...pres.slideLayouts,
|
|
2837
|
+
pres.masterSlide
|
|
2838
|
+
];
|
|
2839
|
+
return await encodeMediaForTargets(mediaTargets, source.runtime, onMediaError).then(async () => {
|
|
2840
|
+
const canonicalMediaTargets = /* @__PURE__ */ new Map();
|
|
2841
|
+
for (const target of mediaTargets) for (const rel of target._relsMedia || []) {
|
|
2842
|
+
if (rel.type === "online" || rel.type === "hyperlink" || typeof rel.data !== "string" || !rel.data) continue;
|
|
2843
|
+
if (rel.oleRelType) continue;
|
|
2844
|
+
const key = (rel.extn || "") + "\0" + rel.data;
|
|
2845
|
+
const canonical = canonicalMediaTargets.get(key);
|
|
2846
|
+
if (canonical) rel.Target = canonical;
|
|
2847
|
+
else canonicalMediaTargets.set(key, rel.Target);
|
|
2848
|
+
}
|
|
2849
|
+
let chartPartIdx = 0;
|
|
2850
|
+
for (const target of mediaTargets) for (const rel of target._relsChart || []) {
|
|
2851
|
+
const chartId = ++chartPartIdx;
|
|
2852
|
+
rel.globalId = chartId;
|
|
2853
|
+
const { fileName, target } = chartPartNames(chartId, rel.isChartEx);
|
|
2854
|
+
rel.fileName = fileName;
|
|
2855
|
+
rel.Target = target;
|
|
2856
|
+
}
|
|
2857
|
+
const restoreAuthoredFit = bakeMeasuredFit(pres.slides, source.fontMetrics);
|
|
2858
|
+
try {
|
|
2859
|
+
const hasCustomProps = source.customProperties.length > 0;
|
|
2860
|
+
zip.add("[Content_Types].xml", makeXmlContTypes({
|
|
2861
|
+
slides: pres.slides,
|
|
2862
|
+
slideLayouts: pres.slideLayouts,
|
|
2863
|
+
masterSlide: pres.masterSlide,
|
|
2864
|
+
hasCustomProps,
|
|
2865
|
+
embeddedFonts: pres.embeddedFonts,
|
|
2866
|
+
contributions: collectContentTypes(contributors, pres)
|
|
2867
|
+
}));
|
|
2868
|
+
zip.add("_rels/.rels", makeXmlRootRels(hasCustomProps));
|
|
2869
|
+
zip.add("docProps/app.xml", makeXmlApp(pres.slides, pres.company, pres.presLayout));
|
|
2870
|
+
zip.add("docProps/core.xml", makeXmlCore(pres.title, pres.subject, pres.author, pres.revision));
|
|
2871
|
+
if (hasCustomProps) zip.add("docProps/custom.xml", makeXmlCustomProperties(source.customProperties));
|
|
2872
|
+
zip.add(relsPath(PRESENTATION_PATH), makeXmlPresentationRels(pres.slides, pres.embeddedFonts));
|
|
2873
|
+
for (const face of flattenEmbeddedFaces(pres.embeddedFonts, 1)) zip.add(fontPath(face.partIndex), face.bytes, { store: true });
|
|
2874
|
+
const themeXml = makeXmlTheme(pres);
|
|
2875
|
+
zip.add("ppt/theme/theme1.xml", themeXml);
|
|
2876
|
+
zip.add("ppt/theme/theme2.xml", themeXml);
|
|
2877
|
+
zip.add(PRESENTATION_PATH, makeXmlPresentation(pres));
|
|
2878
|
+
zip.add("ppt/presProps.xml", makeXmlPresProps());
|
|
2879
|
+
zip.add("ppt/tableStyles.xml", makeXmlTableStyles());
|
|
2880
|
+
zip.add("ppt/viewProps.xml", makeXmlViewProps());
|
|
2881
|
+
pres.slideLayouts.forEach((layout, idx) => {
|
|
2882
|
+
zip.add(slideLayoutPath(idx + 1), makeXmlLayout(layout, source.renderers));
|
|
2883
|
+
zip.add(relsPath(slideLayoutPath(idx + 1)), makeXmlSlideLayoutRel(idx + 1, pres.slideLayouts));
|
|
2884
|
+
});
|
|
2885
|
+
pres.slides.forEach((slide, idx) => {
|
|
2886
|
+
zip.add(slidePath(idx + 1), makeXmlSlide(slide, source.renderers, transitionSoundRIds.get(slide)));
|
|
2887
|
+
zip.add(relsPath(slidePath(idx + 1)), makeXmlSlideRel(pres.slides, pres.slideLayouts, idx + 1));
|
|
2888
|
+
contributors.forEach((contributor) => contributor.parts?.withEachSlide?.(slide, idx + 1, zip));
|
|
2889
|
+
});
|
|
2890
|
+
zip.add(SLIDE_MASTER_PATH, makeXmlMaster(pres.masterSlide, pres.slideLayouts, source.renderers));
|
|
2891
|
+
zip.add(relsPath(SLIDE_MASTER_PATH), makeXmlMasterRel(pres.masterSlide, pres.slideLayouts));
|
|
2892
|
+
contributors.forEach((contributor) => contributor.parts?.afterMaster?.(pres, zip));
|
|
2893
|
+
const writtenMediaPaths = /* @__PURE__ */ new Set();
|
|
2894
|
+
const addTargetRelParts = (target) => {
|
|
2895
|
+
contributors.forEach((contributor) => {
|
|
2896
|
+
const pending = contributor.parts?.fromTargetRels?.(target, zip);
|
|
2897
|
+
if (pending) partPromises.push(...pending);
|
|
2898
|
+
});
|
|
2899
|
+
createMediaParts(target, zip, writtenMediaPaths);
|
|
2900
|
+
};
|
|
2901
|
+
pres.slideLayouts.forEach(addTargetRelParts);
|
|
2902
|
+
pres.slides.forEach(addTargetRelParts);
|
|
2903
|
+
addTargetRelParts(pres.masterSlide);
|
|
2904
|
+
} finally {
|
|
2905
|
+
restoreAuthoredFit();
|
|
2906
|
+
}
|
|
2907
|
+
return await Promise.all(partPromises).then(() => zip.entries());
|
|
2908
|
+
});
|
|
2909
|
+
}
|
|
2910
|
+
/**
|
|
2911
|
+
* Zip an ordered list of package parts into the output shape `props.outputType` selects. Re-adds
|
|
2912
|
+
* each part to a fresh {@link ZipWriter} in order — preserving each part's `store` hint — so the
|
|
2913
|
+
* archive is byte-identical to assembling straight into one writer. Compression and output type
|
|
2914
|
+
* are the only zip-level knobs; part assembly happened in {@link buildPackageParts}.
|
|
2915
|
+
*/
|
|
2916
|
+
async function zipPackageParts(parts, props) {
|
|
2917
|
+
const zip = new ZipWriter();
|
|
2918
|
+
for (const part of parts) zip.add(part.path, part.data, { store: part.store });
|
|
2919
|
+
const compression = props.compression !== false;
|
|
2920
|
+
if (props.outputType) return await zip.generate(props.outputType, { compression });
|
|
2921
|
+
else return await zip.generate("blob", { compression });
|
|
2922
|
+
}
|
|
2923
|
+
/**
|
|
2924
|
+
* Assemble every package part for `source` and zip it into the output shape `props.outputType`
|
|
2925
|
+
* selects. Thin composition of {@link buildPackageParts} (the assembly pipeline) and
|
|
2926
|
+
* {@link zipPackageParts} (the zip pass), kept as the stable entry point the authoring class calls.
|
|
2927
|
+
*/
|
|
2928
|
+
async function writePackage(source, props) {
|
|
2929
|
+
return await zipPackageParts(await buildPackageParts(source, props), props);
|
|
2930
|
+
}
|
|
2931
|
+
//#endregion
|
|
2932
|
+
//#region src/presentation.ts
|
|
2933
|
+
/**
|
|
2934
|
+
* ts-pptx: the presentation core
|
|
2935
|
+
*
|
|
2936
|
+
* Home of {@link PresentationCore}, the class every entry point subclasses with a runtime
|
|
2937
|
+
* adapter (`index.ts`, `node.ts`, `browser.ts`). See the class doc below for the layout of
|
|
2938
|
+
* its body and where package assembly actually happens.
|
|
2939
|
+
*
|
|
2940
|
+
* Units: this library takes user coordinates in inches and converts to EMU on output — see
|
|
2941
|
+
* `units.ts` for the conversions and `STANDARD_LAYOUTS` for the built-in slide sizes.
|
|
2942
|
+
*
|
|
2943
|
+
* Derived from PptxGenJS (C) 2015-present Brent Ely (MIT), which in turn took some code from
|
|
2944
|
+
* the OfficeGen project (Copyright 2013 Ziv Barber). This project is likewise MIT; the full
|
|
2945
|
+
* license text is in `LICENSE` at the repository root.
|
|
2946
|
+
*/
|
|
2947
|
+
const VERSION = "4.0.1";
|
|
2948
|
+
function standardLayoutToPresLayout(layout) {
|
|
2949
|
+
return {
|
|
2950
|
+
name: layout.name,
|
|
2951
|
+
width: layout.widthEmu,
|
|
2952
|
+
height: layout.heightEmu
|
|
2953
|
+
};
|
|
2954
|
+
}
|
|
2955
|
+
/**
|
|
2956
|
+
* One side of a `defineLayout` definition, in inches, clamped to what `ST_SlideSizeCoordinate`
|
|
2957
|
+
* allows.
|
|
2958
|
+
*
|
|
2959
|
+
* `p:sldSz/@cx` and `@cy` are bounded 914400 to 51206400 EMU -- 1 to 56 inches -- and nothing
|
|
2960
|
+
* checked. Every arm of `defineLayout`'s guard chain is a `warn`, so
|
|
2961
|
+
* `{ name: 'Badge', width: 0.5, height: 0.5 }` produced no diagnostic at all (both values are
|
|
2962
|
+
* truthy and finite) and emitted a `sldSz` PowerPoint offers to repair; `width: -5` is truthy
|
|
2963
|
+
* too, so it reached the file as `cx="-4572000"`.
|
|
2964
|
+
*
|
|
2965
|
+
* `Number()` first, because the guard chain above deliberately accepts a numeric string as
|
|
2966
|
+
* advice rather than as an error, and `clampRangedInput` takes a number. A value with nothing
|
|
2967
|
+
* to coerce from is `NaN`, which it throws on -- as `coord/non-finite`, the code the unit
|
|
2968
|
+
* conversion used to raise one line later.
|
|
2969
|
+
* @param value - the caller's `width` or `height`
|
|
2970
|
+
* @param side - which one, for the diagnostic
|
|
2971
|
+
*/
|
|
2972
|
+
function layoutSideInches(value, side) {
|
|
2973
|
+
return clampRangedInput(Number(value), 1, 56, "layout/size-out-of-range", `defineLayout ${side} (inches)`, "coord/non-finite");
|
|
2974
|
+
}
|
|
2975
|
+
/**
|
|
2976
|
+
* Main presentation class and package export flow — the public entry point consumers
|
|
2977
|
+
* instantiate (`new TsPptx()`). Owns presentation-level state and metadata, collects
|
|
2978
|
+
* slides, and drives `write`/`writeFile`/`stream`. The actual OOXML string building is
|
|
2979
|
+
* delegated to the `gen-*` modules; runtime file/stream output goes through the injected
|
|
2980
|
+
* `RuntimeAdapter`. Enums (`AlignH`, `ChartType`, `ShapeType`, …) are imported from the
|
|
2981
|
+
* package entry, not read off the instance.
|
|
2982
|
+
*
|
|
2983
|
+
* Rough layout of the class body:
|
|
2984
|
+
* - Metadata accessors layout / author / company / title / theme / sections getters+setters
|
|
2985
|
+
* - Private slide helpers addNewSlide, getSlide, setSlideNumber
|
|
2986
|
+
* - Public export methods stream / write / writeFile — delegate packaging to `writePackage`
|
|
2987
|
+
* - Public authoring methods addSlide / defineLayout / defineSlideMaster / defineTableStyle
|
|
2988
|
+
*
|
|
2989
|
+
* Package assembly (`[Content_Types].xml`, the rels graph, per-part XML, the ZIP) lives in
|
|
2990
|
+
* `package/assemble.ts`; this class provides the deck state via {@link PresentationCore.packageSource}.
|
|
2991
|
+
* The live-DOM `tableToSlides()` is NOT here — it is added by the browser entry subclass
|
|
2992
|
+
* (`browser.ts`) so it stays out of the Node build and out of the core chunk.
|
|
2993
|
+
*/
|
|
2994
|
+
var PresentationCore = class {
|
|
2995
|
+
/**
|
|
2996
|
+
* Presentation layout name
|
|
2997
|
+
* Standard layouts:
|
|
2998
|
+
* - 'LAYOUT_4x3' (10" x 7.5")
|
|
2999
|
+
* - 'LAYOUT_16x9' (10" x 5.625")
|
|
3000
|
+
* - 'LAYOUT_16x10' (10" x 6.25")
|
|
3001
|
+
* - 'LAYOUT_WIDE' (13.333" x 7.5")
|
|
3002
|
+
* Custom layouts:
|
|
3003
|
+
* Use `pptx.defineLayout()` to create custom layouts (e.g.: 'A4')
|
|
3004
|
+
* @type {string}
|
|
3005
|
+
* @see https://support.office.com/en-us/article/Change-the-size-of-your-slides-040a811c-be43-40b9-8d04-0de5ed79987e
|
|
3006
|
+
*/
|
|
3007
|
+
_layout = DEF_PRES_LAYOUT;
|
|
3008
|
+
set layout(value) {
|
|
3009
|
+
const layoutKey = typeof value === "string" ? value : value?.layout;
|
|
3010
|
+
const newLayout = layoutKey ? this.LAYOUTS[layoutKey] : void 0;
|
|
3011
|
+
if (newLayout) {
|
|
3012
|
+
this._layout = layoutKey;
|
|
3013
|
+
this._presLayout = newLayout;
|
|
3014
|
+
} else throw new InvalidOptionError("layout/unknown", `Unknown layout ${JSON.stringify(layoutKey)}; pass a registered layout name (see LAYOUTS) or a STANDARD_LAYOUTS preset`);
|
|
3015
|
+
}
|
|
3016
|
+
get layout() {
|
|
3017
|
+
return this._layout;
|
|
3018
|
+
}
|
|
3019
|
+
/**
|
|
3020
|
+
* ts-pptx Library Version
|
|
3021
|
+
*/
|
|
3022
|
+
_version = VERSION;
|
|
3023
|
+
get version() {
|
|
3024
|
+
return this._version;
|
|
3025
|
+
}
|
|
3026
|
+
/**
|
|
3027
|
+
* @type {string}
|
|
3028
|
+
*/
|
|
3029
|
+
_author;
|
|
3030
|
+
set author(value) {
|
|
3031
|
+
this._author = value;
|
|
3032
|
+
}
|
|
3033
|
+
get author() {
|
|
3034
|
+
return this._author;
|
|
3035
|
+
}
|
|
3036
|
+
/**
|
|
3037
|
+
* @type {string}
|
|
3038
|
+
*/
|
|
3039
|
+
_company;
|
|
3040
|
+
set company(value) {
|
|
3041
|
+
this._company = value;
|
|
3042
|
+
}
|
|
3043
|
+
get company() {
|
|
3044
|
+
return this._company;
|
|
3045
|
+
}
|
|
3046
|
+
/**
|
|
3047
|
+
* @type {string}
|
|
3048
|
+
* @note the `revision` value must be a whole number only (without "." or "," - otherwise, PPT will throw errors upon opening!)
|
|
3049
|
+
*/
|
|
3050
|
+
_revision;
|
|
3051
|
+
set revision(value) {
|
|
3052
|
+
this._revision = value;
|
|
3053
|
+
}
|
|
3054
|
+
get revision() {
|
|
3055
|
+
return this._revision;
|
|
3056
|
+
}
|
|
3057
|
+
/**
|
|
3058
|
+
* @type {string}
|
|
3059
|
+
*/
|
|
3060
|
+
_subject;
|
|
3061
|
+
set subject(value) {
|
|
3062
|
+
this._subject = value;
|
|
3063
|
+
}
|
|
3064
|
+
get subject() {
|
|
3065
|
+
return this._subject;
|
|
3066
|
+
}
|
|
3067
|
+
/**
|
|
3068
|
+
* @type {ThemeProps}
|
|
3069
|
+
*/
|
|
3070
|
+
_theme = {};
|
|
3071
|
+
set theme(value) {
|
|
3072
|
+
this._theme = value;
|
|
3073
|
+
}
|
|
3074
|
+
get theme() {
|
|
3075
|
+
return this._theme;
|
|
3076
|
+
}
|
|
3077
|
+
/**
|
|
3078
|
+
* @type {string}
|
|
3079
|
+
*/
|
|
3080
|
+
_title;
|
|
3081
|
+
set title(value) {
|
|
3082
|
+
this._title = value;
|
|
3083
|
+
}
|
|
3084
|
+
get title() {
|
|
3085
|
+
return this._title;
|
|
3086
|
+
}
|
|
3087
|
+
/**
|
|
3088
|
+
* Slide number shown on the first slide (maps to firstSlideNum in presentation.xml)
|
|
3089
|
+
* @throws {InvalidOptionError} when the value is not an integer `xsd:int` can hold
|
|
3090
|
+
*/
|
|
3091
|
+
_firstSlideNum;
|
|
3092
|
+
set firstSlideNum(value) {
|
|
3093
|
+
if (!Number.isInteger(value) || value < -2147483648 || value > 2147483647) throw new InvalidOptionError("presentation/first-slide-num-not-an-integer", `firstSlideNum must be an integer, got ${String(value)}`);
|
|
3094
|
+
this._firstSlideNum = value;
|
|
3095
|
+
}
|
|
3096
|
+
get firstSlideNum() {
|
|
3097
|
+
return this._firstSlideNum;
|
|
3098
|
+
}
|
|
3099
|
+
/**
|
|
3100
|
+
* Whether Right-to-Left (RTL) mode is enabled
|
|
3101
|
+
* @type {boolean}
|
|
3102
|
+
*/
|
|
3103
|
+
_rtlMode;
|
|
3104
|
+
set rtlMode(value) {
|
|
3105
|
+
this._rtlMode = value;
|
|
3106
|
+
}
|
|
3107
|
+
get rtlMode() {
|
|
3108
|
+
return this._rtlMode;
|
|
3109
|
+
}
|
|
3110
|
+
/** the slide master the deck's master part is written from; not a `Slide`, so not handed out */
|
|
3111
|
+
_masterSlide;
|
|
3112
|
+
/** this Presentation's Slide objects */
|
|
3113
|
+
_slides;
|
|
3114
|
+
get slides() {
|
|
3115
|
+
return this._slides;
|
|
3116
|
+
}
|
|
3117
|
+
/** this Presentation's sections */
|
|
3118
|
+
_sections;
|
|
3119
|
+
get sections() {
|
|
3120
|
+
return this._sections;
|
|
3121
|
+
}
|
|
3122
|
+
/** custom document properties stored in docProps/custom.xml */
|
|
3123
|
+
_customProperties;
|
|
3124
|
+
/**
|
|
3125
|
+
* slide layout definition objects, used for generating slide layout files.
|
|
3126
|
+
* `protected` (not `private`) so the browser entry subclass can resolve
|
|
3127
|
+
* `tableToSlides({ masterTitle })` against them — that method lives on the
|
|
3128
|
+
* browser build only (it reads a live DOM), see `browser.ts`.
|
|
3129
|
+
*/
|
|
3130
|
+
_slideLayouts;
|
|
3131
|
+
get slideLayouts() {
|
|
3132
|
+
return this._slideLayouts;
|
|
3133
|
+
}
|
|
3134
|
+
get internalPresentation() {
|
|
3135
|
+
return {
|
|
3136
|
+
author: this.author,
|
|
3137
|
+
company: this.company,
|
|
3138
|
+
firstSlideNum: this.firstSlideNum,
|
|
3139
|
+
layout: this.layout,
|
|
3140
|
+
masterSlide: this._masterSlide,
|
|
3141
|
+
presLayout: this.presLayout,
|
|
3142
|
+
revision: this.revision,
|
|
3143
|
+
rtlMode: this.rtlMode,
|
|
3144
|
+
sections: this._sections,
|
|
3145
|
+
slideLayouts: this._slideLayouts,
|
|
3146
|
+
slides: this._slides,
|
|
3147
|
+
subject: this.subject,
|
|
3148
|
+
theme: this.theme,
|
|
3149
|
+
title: this.title,
|
|
3150
|
+
embeddedFonts: this._embeddedFonts
|
|
3151
|
+
};
|
|
3152
|
+
}
|
|
3153
|
+
LAYOUTS;
|
|
3154
|
+
/**
|
|
3155
|
+
* The built-in default layout, or a throw. `LAYOUTS` is a string-keyed map, so the lookup is
|
|
3156
|
+
* typed as possibly-missing at every call site even though the constructor registers it — and
|
|
3157
|
+
* the two callers that need it were spelling the same guard and the same message.
|
|
3158
|
+
*/
|
|
3159
|
+
#requireDefaultLayout() {
|
|
3160
|
+
const defLayout = this.LAYOUTS[DEF_PRES_LAYOUT];
|
|
3161
|
+
if (!defLayout) throw new InternalError("layout/default-not-registered", `Default presentation layout "${DEF_PRES_LAYOUT}" is not registered`);
|
|
3162
|
+
return defLayout;
|
|
3163
|
+
}
|
|
3164
|
+
_presLayout;
|
|
3165
|
+
get presLayout() {
|
|
3166
|
+
return this._presLayout;
|
|
3167
|
+
}
|
|
3168
|
+
_runtime;
|
|
3169
|
+
/** The construct families this presentation was composed with, flattened per seam. */
|
|
3170
|
+
#composition;
|
|
3171
|
+
/**
|
|
3172
|
+
* What a family's presentation-level author is handed. Assembled once: it carries the font
|
|
3173
|
+
* metrics registry, which is private to this class and therefore unreachable from a family
|
|
3174
|
+
* through the instance alone.
|
|
3175
|
+
*/
|
|
3176
|
+
#authorContext;
|
|
3177
|
+
/** Write-side font metrics for measured text fit (`fit:'shrink'`). @see registerFontMetrics */
|
|
3178
|
+
_fontMetrics = new FontMetricsRegistry();
|
|
3179
|
+
/** Author-side embedded font faces, accumulated by {@link embedFont} and emitted at write time. */
|
|
3180
|
+
_embeddedFonts = [];
|
|
3181
|
+
constructor(runtime, families) {
|
|
3182
|
+
this._runtime = runtime;
|
|
3183
|
+
this.#composition = composeFamilies(families);
|
|
3184
|
+
this.#authorContext = {
|
|
3185
|
+
pres: this,
|
|
3186
|
+
fontMetrics: this._fontMetrics
|
|
3187
|
+
};
|
|
3188
|
+
this.LAYOUTS = {
|
|
3189
|
+
LAYOUT_4x3: standardLayoutToPresLayout(STANDARD_LAYOUTS.LAYOUT_4x3),
|
|
3190
|
+
LAYOUT_16x9: standardLayoutToPresLayout(STANDARD_LAYOUTS.LAYOUT_16x9),
|
|
3191
|
+
LAYOUT_16x10: standardLayoutToPresLayout(STANDARD_LAYOUTS.LAYOUT_16x10),
|
|
3192
|
+
LAYOUT_WIDE: standardLayoutToPresLayout(STANDARD_LAYOUTS.LAYOUT_WIDE)
|
|
3193
|
+
};
|
|
3194
|
+
this._author = "ts-pptx";
|
|
3195
|
+
this._company = "ts-pptx";
|
|
3196
|
+
this._revision = "1";
|
|
3197
|
+
this._subject = "ts-pptx Presentation";
|
|
3198
|
+
this._title = "ts-pptx Presentation";
|
|
3199
|
+
const defLayout = this.#requireDefaultLayout();
|
|
3200
|
+
this._presLayout = {
|
|
3201
|
+
name: defLayout.name,
|
|
3202
|
+
width: defLayout.width,
|
|
3203
|
+
height: defLayout.height
|
|
3204
|
+
};
|
|
3205
|
+
this._firstSlideNum = 1;
|
|
3206
|
+
this._rtlMode = false;
|
|
3207
|
+
this._slideLayouts = [{
|
|
3208
|
+
_margin: DEF_SLIDE_MARGIN_IN,
|
|
3209
|
+
_name: DEF_PRES_LAYOUT_NAME,
|
|
3210
|
+
_presLayout: this._presLayout,
|
|
3211
|
+
_rels: [],
|
|
3212
|
+
_relsChart: [],
|
|
3213
|
+
_relsMedia: [],
|
|
3214
|
+
_slide: null,
|
|
3215
|
+
_slideNum: 1e3,
|
|
3216
|
+
_slideNumberProps: null,
|
|
3217
|
+
_slideObjects: []
|
|
3218
|
+
}];
|
|
3219
|
+
this._slides = [];
|
|
3220
|
+
this._sections = [];
|
|
3221
|
+
this._customProperties = [];
|
|
3222
|
+
this._masterSlide = {
|
|
3223
|
+
_presLayout: this._presLayout,
|
|
3224
|
+
_rels: [],
|
|
3225
|
+
_relsChart: [],
|
|
3226
|
+
_relsMedia: [],
|
|
3227
|
+
_slideNum: null,
|
|
3228
|
+
_slideNumberProps: null,
|
|
3229
|
+
_slideObjects: []
|
|
3230
|
+
};
|
|
3231
|
+
}
|
|
3232
|
+
/**
|
|
3233
|
+
* Provides an API for `addTableDefinition` to create slides as needed for auto-paging
|
|
3234
|
+
* @param layout - the layout of the slide the table is paged from
|
|
3235
|
+
* @return {Slide} new Slide
|
|
3236
|
+
*/
|
|
3237
|
+
addNewSlide = (layout) => {
|
|
3238
|
+
const nextOptions = {};
|
|
3239
|
+
if (layout && this._slideLayouts.includes(layout)) setOrClear(nextOptions, "masterTitle", layout._name);
|
|
3240
|
+
const lastSlide = this._slides[this._slides.length - 1];
|
|
3241
|
+
const sourceSection = this._sections.find((sect) => sect._slides.some((s) => s._slideNum === lastSlide?._slideNum));
|
|
3242
|
+
setOrClear(nextOptions, "sectionTitle", sourceSection?.title);
|
|
3243
|
+
return this.addSlide(nextOptions);
|
|
3244
|
+
};
|
|
3245
|
+
/**
|
|
3246
|
+
* Provides an API for `addTableDefinition` to get slide reference by number
|
|
3247
|
+
* @param {number} slideNum - slide number
|
|
3248
|
+
* @return {Slide} Slide
|
|
3249
|
+
*/
|
|
3250
|
+
getSlide = (slideNum) => this._slides.find((slide) => slide._slideNum === slideNum);
|
|
3251
|
+
/**
|
|
3252
|
+
* Provides the live section list to the `Slide` class so Section/Summary Zoom can resolve
|
|
3253
|
+
* a section title to its stable GUID and enumerate sections in order.
|
|
3254
|
+
* @return {SectionInternalProps[]} the presentation's sections
|
|
3255
|
+
*/
|
|
3256
|
+
getSections = () => this._sections;
|
|
3257
|
+
/**
|
|
3258
|
+
* Enables the `Slide` class to set ts-pptx [Presentation] master/layout slidenumbers
|
|
3259
|
+
* @param {SlideNumberProps} slideNum - slide number config
|
|
3260
|
+
*/
|
|
3261
|
+
setSlideNumber = (slideNum) => {
|
|
3262
|
+
this._masterSlide._slideNumberProps = slideNum;
|
|
3263
|
+
const defLayout = this._slideLayouts.find((layout) => layout._name === DEF_PRES_LAYOUT_NAME);
|
|
3264
|
+
if (defLayout) defLayout._slideNumberProps = slideNum;
|
|
3265
|
+
};
|
|
3266
|
+
/**
|
|
3267
|
+
* Assemble the slice of internal state the packaging layer reads. `PresentationCore` owns the
|
|
3268
|
+
* authored deck; `writePackage` (in `package/assemble.ts`) turns it into OOXML parts.
|
|
3269
|
+
*/
|
|
3270
|
+
packageSource() {
|
|
3271
|
+
return {
|
|
3272
|
+
runtime: this._runtime,
|
|
3273
|
+
presentation: this.internalPresentation,
|
|
3274
|
+
customProperties: this._customProperties,
|
|
3275
|
+
fontMetrics: this._fontMetrics,
|
|
3276
|
+
renderers: this.#composition.renderers,
|
|
3277
|
+
partContributors: this.#composition.partContributors
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3280
|
+
/**
|
|
3281
|
+
* Author + serialize this presentation's slides as injectable descriptors,
|
|
3282
|
+
* WITHOUT producing a `.pptx` package. Runs the same media-encode, placeholder
|
|
3283
|
+
* backfill, and measured-fit passes `write()` uses, then serializes each slide
|
|
3284
|
+
* body and resolves its image media to decoded bytes — so a loaded deck can
|
|
3285
|
+
* splice the slides in via `Presentation.appendSlides()` (see `pptx-ts/read`)
|
|
3286
|
+
* while keeping its own masters/layouts/theme byte-identical.
|
|
3287
|
+
*
|
|
3288
|
+
* Returns the deck's slide size (EMU, for the destination size check) and one
|
|
3289
|
+
* descriptor per authored slide. Charts, audio/video media, online media, and
|
|
3290
|
+
* internal slide-to-slide hyperlinks are surfaced as descriptors alongside the
|
|
3291
|
+
* serialized body — this method does not resolve them into a package; the append
|
|
3292
|
+
* path consumes each descriptor to reserve the parts and rebuild the rel graph.
|
|
3293
|
+
*/
|
|
3294
|
+
extractSlides = async (opts = {}) => extractSlides({
|
|
3295
|
+
runtime: this._runtime,
|
|
3296
|
+
presentation: this.internalPresentation,
|
|
3297
|
+
fontMetrics: this._fontMetrics,
|
|
3298
|
+
renderers: this.#composition.renderers,
|
|
3299
|
+
extract: this.#composition.extract
|
|
3300
|
+
}, opts);
|
|
3301
|
+
/**
|
|
3302
|
+
* One of this presentation's family-supplied methods, or the failure that names the family it
|
|
3303
|
+
* would have come from. The methods that call it stay on the class because their signatures are
|
|
3304
|
+
* the public contract; what they reach is what a family owns.
|
|
3305
|
+
*
|
|
3306
|
+
* `protected` for the browser entry, whose `tableToSlides` is one of these and is declared on
|
|
3307
|
+
* the subclass so that a Node build never links the DOM reader.
|
|
3308
|
+
*/
|
|
3309
|
+
familyAuthor(method) {
|
|
3310
|
+
const author = requirePresentationAuthor(this.#composition.presentationAuthors, method);
|
|
3311
|
+
return ((...args) => author(this.#authorContext, ...args));
|
|
3312
|
+
}
|
|
3313
|
+
/**
|
|
3314
|
+
* Register a font's metrics so `fit:'shrink'` and `fit:'resize'` text is measured at
|
|
3315
|
+
* export time: `'shrink'` bakes a real `fontScale` and `'resize'` a real box height, so
|
|
3316
|
+
* the text renders fitted in headless renderers and on plain file-open, with no manual
|
|
3317
|
+
* edit/resize.
|
|
3318
|
+
*
|
|
3319
|
+
* Without registered metrics, both write the bare flag (`<a:normAutofit/>` or
|
|
3320
|
+
* `<a:spAutoFit/>`) that only PowerPoint recomputes on edit. Register the same
|
|
3321
|
+
* face once per weight/style you use; bold/italic advances differ.
|
|
3322
|
+
*
|
|
3323
|
+
* **Font collections** (`.ttc`/`.otc`, how MS Gothic, Yu Gothic, SimSun, Microsoft
|
|
3324
|
+
* YaHei and Cambria ship on Windows) hold several fonts in one file, so one has to be
|
|
3325
|
+
* chosen. With no `font` option, `face` is used as the selector, which is usually what
|
|
3326
|
+
* you meant; a `face` that names no font in the collection throws rather than falling
|
|
3327
|
+
* back to the first, since measuring the wrong member is invisible downstream. Pass
|
|
3328
|
+
* `font` when the deck-side name differs from the name inside the file, and use
|
|
3329
|
+
* `listFontFaces` from `pptx-ts/measure` to see what a file holds.
|
|
3330
|
+
* @param {string} face - font family name as used in `fontFace` (e.g. 'Aptos')
|
|
3331
|
+
* @param {string | Uint8Array | ArrayBuffer} source - font file path/URL (Node/web) or raw TTF/OTF/TTC
|
|
3332
|
+
* bytes. A string is always a path or URL here; base64 text is `embedFont`'s `data`, not this.
|
|
3333
|
+
* @param {object} [opts] - variant flags (advances differ per weight/style) and collection selection
|
|
3334
|
+
* @param {boolean} [opts.bold] - these are the bold advances
|
|
3335
|
+
* @param {boolean} [opts.italic] - these are the italic advances
|
|
3336
|
+
* @param {number | string} [opts.font] - which font inside a collection: 0-based index, or a name
|
|
3337
|
+
* @example await pptx.registerFontMetrics('Aptos', '/usr/share/fonts/Aptos.ttf')
|
|
3338
|
+
* @example await pptx.registerFontMetrics('Aptos', aptosBoldBytes, { bold: true })
|
|
3339
|
+
* @example await pptx.registerFontMetrics('MS PGothic', 'C:/Windows/Fonts/msgothic.ttc')
|
|
3340
|
+
* @example await pptx.registerFontMetrics('Cambria Math', 'C:/Windows/Fonts/cambria.ttc', { font: 1 })
|
|
3341
|
+
*/
|
|
3342
|
+
async registerFontMetrics(face, source, opts) {
|
|
3343
|
+
const bytes = await resolveFontBytes(source, this._runtime, void 0, "registerFontMetrics: `source`");
|
|
3344
|
+
const font = opts?.font ?? (isFontCollection(bytes) ? face : void 0);
|
|
3345
|
+
const metrics = await parseFontMetrics(bytes, font === void 0 ? void 0 : { font });
|
|
3346
|
+
this._fontMetrics.set(face, metrics, pickDefined(opts ?? {}, ["bold", "italic"]));
|
|
3347
|
+
}
|
|
3348
|
+
/**
|
|
3349
|
+
* Embed a font face so the deck renders with it even on machines that do not
|
|
3350
|
+
* have it installed. The **whole** face is embedded (not glyph-subset) under
|
|
3351
|
+
* `/ppt/fonts/` and wired into `presentation.xml` (`p:embeddedFontLst` +
|
|
3352
|
+
* `embedTrueTypeFonts="1"`), exactly as PowerPoint's "Embed fonts in the file"
|
|
3353
|
+
* does. Call once per face/weight you use.
|
|
3354
|
+
*
|
|
3355
|
+
* The declared `typeface` MUST match the family name used in your run/`fontFace`
|
|
3356
|
+
* typefaces (e.g. `addText('hi', { fontFace: 'Silkscreen' })`) or PowerPoint
|
|
3357
|
+
* will not bind the embedded bytes to the text. Repeated calls with the same
|
|
3358
|
+
* `typeface` and different `style` accumulate into one `p:embeddedFont` entry.
|
|
3359
|
+
*
|
|
3360
|
+
* Font licensing is the caller's responsibility: the bytes are embedded as
|
|
3361
|
+
* handed over; `OS/2.fsType` embedding-permission bits are not enforced.
|
|
3362
|
+
* @param {object} opts - font source + identity
|
|
3363
|
+
* @param {string} [opts.path] - font file path (Node) or URL (web) to a `.ttf`/`.otf`
|
|
3364
|
+
* @param {ArrayBuffer | Uint8Array | string} [opts.data] - raw font bytes, or a base64 string (bare or a
|
|
3365
|
+
* whole `data:` URL), in lieu of `path`. Unlike `registerFontMetrics`, a string here is never a path.
|
|
3366
|
+
* @param {string} opts.typeface - family name as referenced by run/`fontFace` typefaces
|
|
3367
|
+
* @param {EmbeddedFontSlot} [opts.style] - face slot; defaults to `'regular'`
|
|
3368
|
+
* @example await pptx.embedFont({ path: '/fonts/Silkscreen-Regular.ttf', typeface: 'Silkscreen' })
|
|
3369
|
+
* @example await pptx.embedFont({ path: '/fonts/Silkscreen-Bold.ttf', typeface: 'Silkscreen', style: 'bold' })
|
|
3370
|
+
*/
|
|
3371
|
+
async embedFont(opts) {
|
|
3372
|
+
if (!opts || typeof opts.typeface !== "string" || opts.typeface.trim() === "") throw new InvalidOptionError("font/missing-typeface", "embedFont: `typeface` is required (the family name your runs reference)");
|
|
3373
|
+
const slot = opts.style ?? "regular";
|
|
3374
|
+
if (!EMBEDDED_FONT_SLOTS.includes(slot)) throw new InvalidOptionError("font/invalid-style-slot", `embedFont: invalid style "${slot}"; expected one of ${EMBEDDED_FONT_SLOTS.join(", ")}`);
|
|
3375
|
+
if (opts.path === void 0 && opts.data === void 0) throw new InvalidOptionError("font/missing-source", "embedFont: provide either `path` or `data`");
|
|
3376
|
+
const bytes = typeof opts.path === "string" ? await resolveFontBytes(opts.path, this._runtime, void 0, "embedFont: `path`") : await resolveFontBytes(opts.data, this._runtime, { base64: true }, "embedFont: `data` string");
|
|
3377
|
+
let font = this._embeddedFonts.find((f) => f.typeface === opts.typeface);
|
|
3378
|
+
if (!font) {
|
|
3379
|
+
font = {
|
|
3380
|
+
typeface: opts.typeface,
|
|
3381
|
+
faces: []
|
|
3382
|
+
};
|
|
3383
|
+
this._embeddedFonts.push(font);
|
|
3384
|
+
}
|
|
3385
|
+
const existing = font.faces.find((f) => f.slot === slot);
|
|
3386
|
+
if (existing) existing.bytes = bytes;
|
|
3387
|
+
else font.faces.push({
|
|
3388
|
+
slot,
|
|
3389
|
+
bytes
|
|
3390
|
+
});
|
|
3391
|
+
}
|
|
3392
|
+
/**
|
|
3393
|
+
* Measure how tall text wraps at a given width, using the **same** calibrated wrap
|
|
3394
|
+
* model the export-time autofit bake uses — so a layout-time prediction matches the
|
|
3395
|
+
* value `fit:'shrink'`/`'resize'` would bake. Synchronous: register the face's metrics
|
|
3396
|
+
* first with {@link TsPptx.registerFontMetrics} (lookup is sync).
|
|
3397
|
+
*
|
|
3398
|
+
* Lets a consumer size its own geometry before export — grow a card to fit its text,
|
|
3399
|
+
* reflow a grid, or detect overflow at layout time. Heights err **tall** (conservative),
|
|
3400
|
+
* so the returned height is ≥ what PowerPoint/LibreOffice render.
|
|
3401
|
+
* @param {string | TextProps[]} text - a string or run array (per-run options override the defaults in `opts`)
|
|
3402
|
+
* @param {MeasureTextOptions} opts - width (inches), font size/face (points), and spacing
|
|
3403
|
+
* @returns {TextMeasurement} laid-out height + line count + fit helpers
|
|
3404
|
+
* @example const m = pptx.measureText('Long heading…', { wIn: 3, fontSize: 18, fontFace: 'Aptos' })
|
|
3405
|
+
* @example if (pptx.measureText(runs, { wIn, fontSize, fontFace }).heightIn > cardHeightIn) growCard()
|
|
3406
|
+
*/
|
|
3407
|
+
measureText(text, opts) {
|
|
3408
|
+
return this.familyAuthor("measureText")(text, opts);
|
|
3409
|
+
}
|
|
3410
|
+
/**
|
|
3411
|
+
* Convenience overflow check: `true` if `text` does not fit a box of inner size
|
|
3412
|
+
* `wIn`×`hIn` (inches) at full size. Because the model errs tall, this is a
|
|
3413
|
+
* **conservative** (slightly over-reporting) check — appropriate for a build-time
|
|
3414
|
+
* warning, not a hard gate. An unmeasurable (unnamed theme-default) face reports
|
|
3415
|
+
* `false` (no overflow) so the linter does not false-positive on faces it cannot measure.
|
|
3416
|
+
* @param {string | TextProps[]} text - a string or run array
|
|
3417
|
+
* @param {OverflowBoxOptions} opts - {@link MeasureTextOptions} plus the box inner height `hIn`
|
|
3418
|
+
* @returns {boolean} true if the text overflows the box
|
|
3419
|
+
*/
|
|
3420
|
+
overflowsBox(text, opts) {
|
|
3421
|
+
return this.familyAuthor("overflowsBox")(text, opts);
|
|
3422
|
+
}
|
|
3423
|
+
/**
|
|
3424
|
+
* Compute the per-cell geometry of a table laid out at `opts.x`/`y`/`w`, without
|
|
3425
|
+
* adding it to a slide — so a consumer can place images or shapes precisely over
|
|
3426
|
+
* cells. Column widths (cell `x`/`w`) are exact, derived from the same logic the
|
|
3427
|
+
* writer uses. Row heights (`y`/`h`) are exact when pinned by `rowH` (array or
|
|
3428
|
+
* scalar) or table `h`; an auto-height row is estimated with the same conservative
|
|
3429
|
+
* (tall) text model as {@link TsPptx.measureText} and flagged `heightExact:false`
|
|
3430
|
+
* (register the cell font via {@link TsPptx.registerFontMetrics} for an exact
|
|
3431
|
+
* estimate). Geometry is for a single, un-paginated table — `autoPage` paging is
|
|
3432
|
+
* not modeled.
|
|
3433
|
+
* @param {TableRow[]} rows - the same `rows` passed to `slide.addTable`
|
|
3434
|
+
* @param {TableProps} opts - the same table options (`x`, `y`, `w`, `colW`, `rowH`, `h`, …)
|
|
3435
|
+
* @returns {TableLayoutResult} per-cell rectangles (inches) plus overall table bounds
|
|
3436
|
+
* @example
|
|
3437
|
+
* const g = pptx.tableLayout(rows, { x: 1, y: 1, w: 8, colW: [2, 3, 3] })
|
|
3438
|
+
* const c = g.cells.find(c => c.row === 0 && c.col === 2)
|
|
3439
|
+
* slide.addImage({ path: 'logo.png', x: c.xIn, y: c.yIn, w: c.wIn, h: c.hIn })
|
|
3440
|
+
*/
|
|
3441
|
+
tableLayout(rows, opts) {
|
|
3442
|
+
return this.familyAuthor("tableLayout")(rows, opts);
|
|
3443
|
+
}
|
|
3444
|
+
/**
|
|
3445
|
+
* Export the current presentation as portable ZIP bytes. Unlike the former
|
|
3446
|
+
* `stream()` method, this does not imply incremental output or convert the
|
|
3447
|
+
* archive to a Node `Buffer`: package assembly is synchronous and the returned
|
|
3448
|
+
* `Uint8Array` works unchanged in Node, browsers, and workers.
|
|
3449
|
+
* @param {WriteBaseProps} props - output properties
|
|
3450
|
+
* @returns {Promise<Uint8Array>} complete `.pptx` archive bytes
|
|
3451
|
+
*/
|
|
3452
|
+
async toBytes(props) {
|
|
3453
|
+
return await writePackage(this.packageSource(), {
|
|
3454
|
+
...pickDefined(props ?? {}, ["compression", "onMediaError"]),
|
|
3455
|
+
outputType: "uint8array"
|
|
3456
|
+
});
|
|
3457
|
+
}
|
|
3458
|
+
/**
|
|
3459
|
+
* Export the current Presentation as ZIP content with the selected type
|
|
3460
|
+
* @param {WriteProps} props output properties
|
|
3461
|
+
* @returns {Promise<string | ArrayBuffer | Blob | Uint8Array>} file content in selected type
|
|
3462
|
+
*/
|
|
3463
|
+
async write(props) {
|
|
3464
|
+
return await writePackage(this.packageSource(), pickDefined(props ?? {}, [
|
|
3465
|
+
"compression",
|
|
3466
|
+
"outputType",
|
|
3467
|
+
"onMediaError"
|
|
3468
|
+
]));
|
|
3469
|
+
}
|
|
3470
|
+
/**
|
|
3471
|
+
* Assemble the current Presentation into its OOXML package parts WITHOUT zipping — the raw
|
|
3472
|
+
* `path → bytes` contents `write()` would compress into the `.pptx`. Use this to stream parts
|
|
3473
|
+
* into a custom container, inspect individual parts, or feed a pipeline that does its own
|
|
3474
|
+
* archiving. To produce a real `.pptx` file, use {@link write} / {@link writeFile} instead.
|
|
3475
|
+
*
|
|
3476
|
+
* The returned parts are byte-identical, per part, to what `write()` emits for the same deck,
|
|
3477
|
+
* and their order is the package's emission order. Both are a stability-guaranteed observable
|
|
3478
|
+
* contract: adding a new part in a later release is backward-compatible as long as existing
|
|
3479
|
+
* part paths and their relative order do not shift; renaming or reordering an existing part is
|
|
3480
|
+
* a breaking change. Each call returns fresh `Uint8Array` views over per-call buffers (a new
|
|
3481
|
+
* assembly runs each time), so callers may retain or transfer them without defensive copying.
|
|
3482
|
+
* @param {PartsProps} props - assembly options (`onMediaError` only; compression/output shape
|
|
3483
|
+
* are zip concerns that do not apply to unzipped parts)
|
|
3484
|
+
* @returns {Promise<PackagePart[]>} the package parts in emission order
|
|
3485
|
+
*/
|
|
3486
|
+
async toParts(props) {
|
|
3487
|
+
return (await buildPackageParts(this.packageSource(), { onMediaError: props?.onMediaError })).map(({ path, data }) => ({
|
|
3488
|
+
path,
|
|
3489
|
+
data
|
|
3490
|
+
}));
|
|
3491
|
+
}
|
|
3492
|
+
/**
|
|
3493
|
+
* Export the current Presentation.
|
|
3494
|
+
* Write the generated presentation to disk (Node) or trigger a download (browser).
|
|
3495
|
+
* @param {WriteFileProps} props - output file properties
|
|
3496
|
+
* @returns {Promise<string>} the presentation name
|
|
3497
|
+
*/
|
|
3498
|
+
async writeFile(props) {
|
|
3499
|
+
const rawName = props?.fileName ?? "Presentation.pptx";
|
|
3500
|
+
const fileName = rawName.toLowerCase().endsWith(".pptx") ? rawName : `${rawName}.pptx`;
|
|
3501
|
+
const writeProps = pickDefined(props ?? {}, ["compression", "onMediaError"]);
|
|
3502
|
+
setOrClear(writeProps, "outputType", this._runtime.writeFileOutputType ?? void 0);
|
|
3503
|
+
const data = await writePackage(this.packageSource(), writeProps);
|
|
3504
|
+
return await this._runtime.writeFile(fileName, data);
|
|
3505
|
+
}
|
|
3506
|
+
/**
|
|
3507
|
+
* Set a custom document property stored in `docProps/custom.xml`.
|
|
3508
|
+
* Calling with the same name replaces the existing value.
|
|
3509
|
+
* @param name - property name
|
|
3510
|
+
* @param value - string, integer/float number, boolean, or Date
|
|
3511
|
+
*/
|
|
3512
|
+
setCustomProperty(name, value) {
|
|
3513
|
+
this._customProperties = this._customProperties.filter((p) => p.name !== name);
|
|
3514
|
+
this._customProperties.push({
|
|
3515
|
+
name,
|
|
3516
|
+
value
|
|
3517
|
+
});
|
|
3518
|
+
}
|
|
3519
|
+
/**
|
|
3520
|
+
* Add a new Section to Presentation
|
|
3521
|
+
* @param {SectionProps} section - section properties
|
|
3522
|
+
* @example pptx.addSection({ title:'Charts' });
|
|
3523
|
+
*/
|
|
3524
|
+
addSection(section) {
|
|
3525
|
+
if (!section) {
|
|
3526
|
+
warn("section/missing-argument", "addSection requires an argument");
|
|
3527
|
+
return;
|
|
3528
|
+
} else if (!section.title) {
|
|
3529
|
+
warn("section/missing-title", "addSection requires a title");
|
|
3530
|
+
return;
|
|
3531
|
+
} else if (this._sections.some((sect) => sect.title === section.title)) {
|
|
3532
|
+
warn("section/duplicate-title", `addSection: a section titled "${section.title}" already exists; ignoring duplicate`);
|
|
3533
|
+
return;
|
|
3534
|
+
}
|
|
3535
|
+
const newSection = {
|
|
3536
|
+
_type: "user",
|
|
3537
|
+
_slides: [],
|
|
3538
|
+
_id: bracedGuid(),
|
|
3539
|
+
title: section.title
|
|
3540
|
+
};
|
|
3541
|
+
if (section.order === void 0) {
|
|
3542
|
+
this._sections.push(newSection);
|
|
3543
|
+
return;
|
|
3544
|
+
}
|
|
3545
|
+
if (!Number.isInteger(section.order) || section.order < 1) {
|
|
3546
|
+
warn("section/invalid-order", `addSection: "order" must be a whole number of at least 1 (the first position); got ${String(section.order)}. Appending.`);
|
|
3547
|
+
this._sections.push(newSection);
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
3550
|
+
this._sections.splice(section.order - 1, 0, newSection);
|
|
3551
|
+
}
|
|
3552
|
+
/**
|
|
3553
|
+
* Add a new Slide to Presentation
|
|
3554
|
+
* @param {AddSlideProps} options - slide options
|
|
3555
|
+
* @returns {Slide} the new Slide
|
|
3556
|
+
*/
|
|
3557
|
+
addSlide(options) {
|
|
3558
|
+
const masterTitle = options?.masterTitle ?? "";
|
|
3559
|
+
let slideLayout = {
|
|
3560
|
+
_name: this.#requireDefaultLayout().name,
|
|
3561
|
+
_presLayout: this.presLayout,
|
|
3562
|
+
_rels: [],
|
|
3563
|
+
_relsChart: [],
|
|
3564
|
+
_relsMedia: [],
|
|
3565
|
+
_slideNum: this._slides.length + 1,
|
|
3566
|
+
_slideObjects: []
|
|
3567
|
+
};
|
|
3568
|
+
if (masterTitle) {
|
|
3569
|
+
const tmpLayout = this._slideLayouts.find((layout) => layout._name === masterTitle);
|
|
3570
|
+
if (tmpLayout) slideLayout = tmpLayout;
|
|
3571
|
+
else warn("slide/master-not-found", `addSlide: unable to find a slide master with title: "${masterTitle}"; using the default layout`);
|
|
3572
|
+
}
|
|
3573
|
+
const newSlide = new SlideBuilder({
|
|
3574
|
+
addSlide: this.addNewSlide,
|
|
3575
|
+
getSlide: this.getSlide,
|
|
3576
|
+
getSections: this.getSections,
|
|
3577
|
+
presLayout: this.presLayout,
|
|
3578
|
+
authors: this.#composition.authors,
|
|
3579
|
+
childAuthors: this.#composition.children,
|
|
3580
|
+
setSlideNum: this.setSlideNumber,
|
|
3581
|
+
slideId: this._slides.length + 256,
|
|
3582
|
+
slideRId: this._slides.length + 2,
|
|
3583
|
+
slideNumber: this._slides.length + 1,
|
|
3584
|
+
slideLayout
|
|
3585
|
+
});
|
|
3586
|
+
this._slides.push(newSlide);
|
|
3587
|
+
const namedSection = options?.sectionTitle ? this._sections.find((section) => section.title === options.sectionTitle) : void 0;
|
|
3588
|
+
if (options?.sectionTitle && !namedSection) warn("slide/section-not-found", `addSlide: unable to find section with title: "${options.sectionTitle}"`);
|
|
3589
|
+
if (namedSection) namedSection._slides.push(newSlide);
|
|
3590
|
+
else if (this._sections.length > 0) {
|
|
3591
|
+
const lastSect = this._sections[this._sections.length - 1];
|
|
3592
|
+
if (lastSect?._type === "default") lastSect._slides.push(newSlide);
|
|
3593
|
+
else this._sections.push({
|
|
3594
|
+
title: `Default-${this._sections.filter((sect) => sect._type === "default").length + 1}`,
|
|
3595
|
+
_type: "default",
|
|
3596
|
+
_id: bracedGuid(),
|
|
3597
|
+
_slides: [newSlide]
|
|
3598
|
+
});
|
|
3599
|
+
}
|
|
3600
|
+
return newSlide;
|
|
3601
|
+
}
|
|
3602
|
+
/**
|
|
3603
|
+
* Create a custom Slide Layout in any size
|
|
3604
|
+
* @param {PresLayout} layout - layout properties
|
|
3605
|
+
* @example pptx.defineLayout({ name:'A3', width:16.5, height:11.7 });
|
|
3606
|
+
*/
|
|
3607
|
+
defineLayout(layout) {
|
|
3608
|
+
if (!layout || typeof layout !== "object") throw new InvalidOptionError("layout/invalid-definition", "defineLayout requires an object `{ name, width, height }` with the dimensions in inches");
|
|
3609
|
+
if (!layout.name) throw new InvalidOptionError("layout/invalid-definition", "defineLayout requires a non-empty `name`: it is the key the layout is registered and selected under");
|
|
3610
|
+
if (layout.width === void 0) warn("layout/invalid-definition", "defineLayout requires `width`");
|
|
3611
|
+
else if (typeof layout.width !== "number") warn("layout/invalid-definition", "defineLayout `width` should be a number (inches)");
|
|
3612
|
+
if (layout.height === void 0) warn("layout/invalid-definition", "defineLayout requires `height`");
|
|
3613
|
+
else if (typeof layout.height !== "number") warn("layout/invalid-definition", "defineLayout `height` should be a number (inches)");
|
|
3614
|
+
this.LAYOUTS[layout.name] = {
|
|
3615
|
+
name: layout.name,
|
|
3616
|
+
width: inchesToEmu(layoutSideInches(layout.width, "width")),
|
|
3617
|
+
height: inchesToEmu(layoutSideInches(layout.height, "height"))
|
|
3618
|
+
};
|
|
3619
|
+
}
|
|
3620
|
+
/**
|
|
3621
|
+
* Create a new slide master [layout] for the Presentation
|
|
3622
|
+
* @param {SlideMasterProps} props - layout properties
|
|
3623
|
+
*/
|
|
3624
|
+
defineSlideMaster(props) {
|
|
3625
|
+
const propsClone = structuredClone(props);
|
|
3626
|
+
if (!propsClone.title) throw new InvalidOptionError("master/missing-title", "defineSlideMaster() object argument requires a `title` value.");
|
|
3627
|
+
const newLayout = {
|
|
3628
|
+
_margin: propsClone.margin || DEF_SLIDE_MARGIN_IN,
|
|
3629
|
+
_name: propsClone.title,
|
|
3630
|
+
_presLayout: this.presLayout,
|
|
3631
|
+
_rels: [],
|
|
3632
|
+
_relsChart: [],
|
|
3633
|
+
_relsMedia: [],
|
|
3634
|
+
_slide: null,
|
|
3635
|
+
_slideNum: 1e3 + this._slideLayouts.length + 1,
|
|
3636
|
+
_slideNumberProps: propsClone.slideNumber || null,
|
|
3637
|
+
_slideObjects: [],
|
|
3638
|
+
background: propsClone.background
|
|
3639
|
+
};
|
|
3640
|
+
createSlideMaster(propsClone, newLayout, this.#composition.children);
|
|
3641
|
+
if (propsClone.textStyles && typeof propsClone.textStyles === "object") this._masterSlide._txStyles = {
|
|
3642
|
+
...this._masterSlide._txStyles,
|
|
3643
|
+
...propsClone.textStyles
|
|
3644
|
+
};
|
|
3645
|
+
this._slideLayouts.push(newLayout);
|
|
3646
|
+
if (propsClone.background) addBackgroundDefinition(propsClone.background, newLayout);
|
|
3647
|
+
if (newLayout._slideNumberProps && !this._masterSlide._slideNumberProps) this._masterSlide._slideNumberProps = newLayout._slideNumberProps;
|
|
3648
|
+
}
|
|
3649
|
+
};
|
|
3650
|
+
//#endregion
|
|
3651
|
+
//#region src/families/core.ts
|
|
3652
|
+
/**
|
|
3653
|
+
* ts-pptx: the construct families every presentation carries
|
|
3654
|
+
*
|
|
3655
|
+
* The core tier. A composed presentation gets these whether it asks for them or not, so
|
|
3656
|
+
* `createPresentation()` with no families still authors text, shapes, images, groups and speaker
|
|
3657
|
+
* notes -- a deck, rather than a slide-shaped hole.
|
|
3658
|
+
*
|
|
3659
|
+
* The line is a judgement rather than a measurement: these five are what "a slide" means to most
|
|
3660
|
+
* callers, they are cheap, and a surface that made someone compose `text` before it could write a
|
|
3661
|
+
* word would be a worse default than one that costs a few kilobytes more. A family is core when
|
|
3662
|
+
* leaving it out would make the result something other than a deck; everything else is asked for.
|
|
3663
|
+
*/
|
|
3664
|
+
/** Every family a composed presentation carries without being asked. */
|
|
3665
|
+
const CORE_CONSTRUCT_FAMILIES = [
|
|
3666
|
+
textFamily,
|
|
3667
|
+
shapeFamily,
|
|
3668
|
+
imageFamily,
|
|
3669
|
+
groupFamily,
|
|
3670
|
+
notesFamily
|
|
3671
|
+
];
|
|
3672
|
+
//#endregion
|
|
3673
|
+
//#region src/entry-compose.ts
|
|
3674
|
+
/**
|
|
3675
|
+
* ts-pptx: composing a presentation from the construct families it actually needs
|
|
3676
|
+
*
|
|
3677
|
+
* The body behind each entry's `createPresentation`, and the types that describe what comes back.
|
|
3678
|
+
* It lives beside `entry-surface.ts` for the same reason that does: `index.ts`, `node.ts` and
|
|
3679
|
+
* `browser.ts` differ only in the runtime adapter they hand down, so the shared half is written
|
|
3680
|
+
* once and cannot drift between them.
|
|
3681
|
+
*
|
|
3682
|
+
* There is no second lean entry point, deliberately. A `pptx-ts/core` would need the same
|
|
3683
|
+
* three-condition treatment `.` gets, so three more source entries, three more `dist` files, three
|
|
3684
|
+
* more budget rows and a second place for the entries to diverge -- and it would buy nothing that
|
|
3685
|
+
* export-level shaking does not already give. `pptx-ts` is an intermediate module in a consumer's
|
|
3686
|
+
* graph: a program that imports only `createPresentation` drops the `TsPptx` class beside it and,
|
|
3687
|
+
* with it, every family that class names.
|
|
3688
|
+
*/
|
|
3689
|
+
/**
|
|
3690
|
+
* Build a presentation that carries the core tier plus `opts.use`, on the given runtime adapter.
|
|
3691
|
+
*
|
|
3692
|
+
* The cast is the one place this file is not type-safe, and it is unavoidable: the class is one
|
|
3693
|
+
* class whatever it was composed with, while the type says which of its methods are actually
|
|
3694
|
+
* there. The two are kept honest by the runtime, which raises `family/not-composed` naming the
|
|
3695
|
+
* family for any method the type has already refused.
|
|
3696
|
+
*/
|
|
3697
|
+
function composePresentation(runtime, opts) {
|
|
3698
|
+
return new PresentationCore(runtime, [...CORE_CONSTRUCT_FAMILIES, ...opts?.use ?? []]);
|
|
3699
|
+
}
|
|
3700
|
+
//#endregion
|
|
3701
|
+
//#region src/entry-families.ts
|
|
3702
|
+
/**
|
|
3703
|
+
* ts-pptx: the construct families the three main entries are composed with
|
|
3704
|
+
*
|
|
3705
|
+
* The full authoring surface, as a list: `index.ts`, `node.ts` and `browser.ts` each compose every
|
|
3706
|
+
* family, so `TsPptx` authors everything it ever has; the browser entry adds the table family's
|
|
3707
|
+
* live-DOM half on top, because that one needs a `document` to read. The families a *composed*
|
|
3708
|
+
* presentation gets without asking are the shorter list in `families/core.ts`.
|
|
3709
|
+
*
|
|
3710
|
+
* It lives beside `entry-surface.ts`, and for the same reason: the three entries publish one API
|
|
3711
|
+
* and differ only in the runtime adapter they hand down, so what they are composed with is one
|
|
3712
|
+
* list rather than three that can drift.
|
|
3713
|
+
*
|
|
3714
|
+
* Naming a family here is what puts its code in the graph -- a named import inside a reachable
|
|
3715
|
+
* function body is retained unconditionally. That is the whole point of the seam
|
|
3716
|
+
* (`families/shared.ts`): a program composed with fewer families links fewer of them.
|
|
3717
|
+
*/
|
|
3718
|
+
/**
|
|
3719
|
+
* Every construct family the library can author with: the core tier, plus everything a composed
|
|
3720
|
+
* presentation would have to ask for. Built from `CORE_CONSTRUCT_FAMILIES` rather than restating
|
|
3721
|
+
* it, so the two lists cannot disagree about whether a family exists.
|
|
3722
|
+
*/
|
|
3723
|
+
const ALL_CONSTRUCT_FAMILIES = /* @__PURE__ */ Object.freeze([
|
|
3724
|
+
textFamily,
|
|
3725
|
+
shapeFamily,
|
|
3726
|
+
imageFamily,
|
|
3727
|
+
groupFamily,
|
|
3728
|
+
notesFamily,
|
|
3729
|
+
animationsFamily,
|
|
3730
|
+
chartFamily,
|
|
3731
|
+
commentsFamily,
|
|
3732
|
+
connectorFamily,
|
|
3733
|
+
measureFamily,
|
|
3734
|
+
mediaFamily,
|
|
3735
|
+
model3dFamily,
|
|
3736
|
+
oleFamily,
|
|
3737
|
+
tableFamily,
|
|
3738
|
+
zoomFamily
|
|
3739
|
+
]);
|
|
3740
|
+
//#endregion
|
|
3741
|
+
//#region src/runtime/fetch-media.ts
|
|
3742
|
+
/**
|
|
3743
|
+
* The parts of a runtime adapter that are not runtime-specific.
|
|
3744
|
+
*
|
|
3745
|
+
* `fetch`, `btoa` and `TextEncoder` exist in Node, in every browser, and in the edge/worker
|
|
3746
|
+
* runtimes the neutral adapter serves, so the remote-load paths were the same code in all three
|
|
3747
|
+
* adapters — byte-identical in two of them and an inline branch in the third. What is genuinely
|
|
3748
|
+
* per-runtime stays in each adapter: `node:fs`, `FileReader`, the canvas rasterizer, the
|
|
3749
|
+
* `<a download>` writer.
|
|
3750
|
+
*
|
|
3751
|
+
* Nothing here touches a host global beyond `fetch`.
|
|
3752
|
+
*/
|
|
3753
|
+
/**
|
|
3754
|
+
* Whether `source` names something to `fetch` rather than something to read from a filesystem.
|
|
3755
|
+
*
|
|
3756
|
+
* A real scheme test, not `source.startsWith('http')`. That prefix matched a *relative path* —
|
|
3757
|
+
* `httpdocs/logo.png`, `http-icons/x.svg` — and sent it to `fetch`, which then failed with a
|
|
3758
|
+
* fetch error naming a file that exists on disk. It was also case-sensitive, so
|
|
3759
|
+
* `HTTPS://example.com/f.ttf` missed and went to `fs.readFile`.
|
|
3760
|
+
*
|
|
3761
|
+
* Only `http:` and `https:` count. A `file:` or `data:` URL falls through to the adapter's
|
|
3762
|
+
* local branch, where it fails — the same as before this was tightened, and deliberately left
|
|
3763
|
+
* that way: making either work is a feature with its own decisions (what a `file:` URL means
|
|
3764
|
+
* off Node, whether a `data:` URL should bypass loading entirely), not a side effect of fixing
|
|
3765
|
+
* a prefix test.
|
|
3766
|
+
*/
|
|
3767
|
+
function isRemote(source) {
|
|
3768
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(source)?.[1]?.toLowerCase();
|
|
3769
|
+
return scheme === "http" || scheme === "https";
|
|
3770
|
+
}
|
|
3771
|
+
/** Fetch a font file into raw bytes for `registerFontMetrics`. */
|
|
3772
|
+
async function fetchFontBytes(source) {
|
|
3773
|
+
const response = await fetch(source);
|
|
3774
|
+
if (!response.ok) throw new MediaError("font/fetch-failed", `Unable to load font (fetch): ${source}`);
|
|
3775
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
3776
|
+
}
|
|
3777
|
+
/**
|
|
3778
|
+
* Fetch a media file as raw base64 (no `data:` prefix) — see {@link RuntimeAdapter.loadMedia}
|
|
3779
|
+
* for why both encodings are accepted.
|
|
3780
|
+
*
|
|
3781
|
+
* Encodes through `bytesToBase64` rather than `Buffer`, so the same function serves a runtime
|
|
3782
|
+
* that has no `Buffer`. The Node adapter still uses `Buffer` on its *filesystem* branch, where
|
|
3783
|
+
* it is both available and faster on a large payload.
|
|
3784
|
+
*/
|
|
3785
|
+
async function fetchMediaBase64(path) {
|
|
3786
|
+
const response = await fetch(path);
|
|
3787
|
+
if (!response.ok) throw new MediaError("media/fetch-failed", `Unable to load image (fetch): ${path}`);
|
|
3788
|
+
return bytesToBase64(new Uint8Array(await response.arrayBuffer()));
|
|
3789
|
+
}
|
|
3790
|
+
/**
|
|
3791
|
+
* The SVG-preview fallback for a runtime with no canvas: stamp the placeholder PNG.
|
|
3792
|
+
*
|
|
3793
|
+
* An SVG placed on a slide needs a raster fallback beside it for viewers that will not render
|
|
3794
|
+
* the SVG. Rasterizing needs a canvas, which only the browser adapter has, so everywhere else
|
|
3795
|
+
* the fallback is a fixed placeholder image rather than a missing part. That is this runtime's
|
|
3796
|
+
* output rather than a failure, so it resolves `null`: reporting one would fail every SVG under
|
|
3797
|
+
* the default `onMediaError`.
|
|
3798
|
+
*/
|
|
3799
|
+
async function placeholderSvgPreview(rel) {
|
|
3800
|
+
rel.data = IMG_SVG_PLACEHOLDER;
|
|
3801
|
+
return null;
|
|
3802
|
+
}
|
|
3803
|
+
//#endregion
|
|
3804
|
+
//#region src/clip.ts
|
|
3805
|
+
/**
|
|
3806
|
+
* ts-pptx: named clip silhouettes for images
|
|
3807
|
+
*
|
|
3808
|
+
* A `ClipShape` is declarative data — a named silhouette plus its options — and {@link clipPath}
|
|
3809
|
+
* resolves it to the freeform `points` path `addImage` emits as the `<a:custGeom>` clip mask on
|
|
3810
|
+
* the `<p:pic>`. Paired with `sizing: { type: 'cover' }` (an `<a:srcRect>` crop) it reproduces
|
|
3811
|
+
* what a PowerPoint *picture placeholder* does — a layout `custGeom` clipping an inherited
|
|
3812
|
+
* blipFill — but standalone, with no placeholder involved.
|
|
3813
|
+
*
|
|
3814
|
+
* These are silhouettes recurring often enough in real decks to be worth naming; hand-authored
|
|
3815
|
+
* `points` remain available for anything else. Nothing here is theme- or brand-aware: which
|
|
3816
|
+
* silhouette to use, and which edge it sits flush to, belongs to the caller.
|
|
3817
|
+
*
|
|
3818
|
+
* ## The coordinate trap
|
|
3819
|
+
*
|
|
3820
|
+
* Coordinates come back in the image box's OWN inch space (`0..w`, `0..h`), because `custGeom`
|
|
3821
|
+
* points expressed as `%` resolve against the SLIDE, not the box. A box-relative silhouette must
|
|
3822
|
+
* therefore be emitted in inches already scaled to the box, which is why {@link clipPath} takes
|
|
3823
|
+
* the box size: the fractions below are multiplied by `w` / `h` at build time, so one silhouette
|
|
3824
|
+
* scales with whatever region it is handed. Authoring the same shape as percentages would put
|
|
3825
|
+
* the clip in the wrong place on every box that is not the full slide.
|
|
3826
|
+
*/
|
|
3827
|
+
const HALF_DISC = {
|
|
3828
|
+
deep: {
|
|
3829
|
+
topEdgeX: .31788,
|
|
3830
|
+
botEdgeX: .31788,
|
|
3831
|
+
tipTopX: .30061,
|
|
3832
|
+
tipTopY: .01078,
|
|
3833
|
+
tipBotX: .30061,
|
|
3834
|
+
tipBotY: .98922,
|
|
3835
|
+
apexY: .5,
|
|
3836
|
+
c1b: {
|
|
3837
|
+
x: .11594,
|
|
3838
|
+
y: .86833
|
|
3839
|
+
},
|
|
3840
|
+
c2b: {
|
|
3841
|
+
x: 0,
|
|
3842
|
+
y: .69392
|
|
3843
|
+
},
|
|
3844
|
+
c1t: {
|
|
3845
|
+
x: 0,
|
|
3846
|
+
y: .30609
|
|
3847
|
+
},
|
|
3848
|
+
c2t: {
|
|
3849
|
+
x: .11594,
|
|
3850
|
+
y: .13167
|
|
3851
|
+
}
|
|
3852
|
+
},
|
|
3853
|
+
shallow: {
|
|
3854
|
+
topEdgeX: .12611,
|
|
3855
|
+
botEdgeX: .11128,
|
|
3856
|
+
tipTopX: .09976,
|
|
3857
|
+
tipTopY: .05448,
|
|
3858
|
+
tipBotX: .09976,
|
|
3859
|
+
tipBotY: .97617,
|
|
3860
|
+
apexY: .51533,
|
|
3861
|
+
c1b: {
|
|
3862
|
+
x: .03522,
|
|
3863
|
+
y: .83247
|
|
3864
|
+
},
|
|
3865
|
+
c2b: {
|
|
3866
|
+
x: 0,
|
|
3867
|
+
y: .67725
|
|
3868
|
+
},
|
|
3869
|
+
c1t: {
|
|
3870
|
+
x: 0,
|
|
3871
|
+
y: .3534
|
|
3872
|
+
},
|
|
3873
|
+
c2t: {
|
|
3874
|
+
x: .03522,
|
|
3875
|
+
y: .19818
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
};
|
|
3879
|
+
/** The values each {@link ClipShape} field takes, for refusing one a JavaScript caller got wrong. */
|
|
3880
|
+
const CLIP_KINDS = ["half-disc"];
|
|
3881
|
+
const FLAT_SIDES = ["left", "right"];
|
|
3882
|
+
const HALF_DISC_PRESETS = Object.keys(HALF_DISC);
|
|
3883
|
+
/**
|
|
3884
|
+
* Half-disc clip path for a `w × h` box. Authored flat-right; `flat: 'left'` mirrors `x → 1 - x`.
|
|
3885
|
+
*/
|
|
3886
|
+
function halfDisc(w, h, flat, preset) {
|
|
3887
|
+
const s = HALF_DISC[preset];
|
|
3888
|
+
const fx = flat === "left" ? (x) => (1 - x) * w : (x) => x * w;
|
|
3889
|
+
const fy = (y) => y * h;
|
|
3890
|
+
return [
|
|
3891
|
+
{
|
|
3892
|
+
x: fx(s.topEdgeX),
|
|
3893
|
+
y: fy(0),
|
|
3894
|
+
moveTo: true
|
|
3895
|
+
},
|
|
3896
|
+
{
|
|
3897
|
+
x: fx(1),
|
|
3898
|
+
y: fy(0)
|
|
3899
|
+
},
|
|
3900
|
+
{
|
|
3901
|
+
x: fx(1),
|
|
3902
|
+
y: fy(1)
|
|
3903
|
+
},
|
|
3904
|
+
{
|
|
3905
|
+
x: fx(s.botEdgeX),
|
|
3906
|
+
y: fy(1)
|
|
3907
|
+
},
|
|
3908
|
+
{
|
|
3909
|
+
x: fx(s.tipBotX),
|
|
3910
|
+
y: fy(s.tipBotY)
|
|
3911
|
+
},
|
|
3912
|
+
{
|
|
3913
|
+
x: fx(0),
|
|
3914
|
+
y: fy(s.apexY),
|
|
3915
|
+
curve: {
|
|
3916
|
+
type: "cubic",
|
|
3917
|
+
x1: fx(s.c1b.x),
|
|
3918
|
+
y1: fy(s.c1b.y),
|
|
3919
|
+
x2: fx(s.c2b.x),
|
|
3920
|
+
y2: fy(s.c2b.y)
|
|
3921
|
+
}
|
|
3922
|
+
},
|
|
3923
|
+
{
|
|
3924
|
+
x: fx(s.tipTopX),
|
|
3925
|
+
y: fy(s.tipTopY),
|
|
3926
|
+
curve: {
|
|
3927
|
+
type: "cubic",
|
|
3928
|
+
x1: fx(s.c1t.x),
|
|
3929
|
+
y1: fy(s.c1t.y),
|
|
3930
|
+
x2: fx(s.c2t.x),
|
|
3931
|
+
y2: fy(s.c2t.y)
|
|
3932
|
+
}
|
|
3933
|
+
},
|
|
3934
|
+
{ close: true }
|
|
3935
|
+
];
|
|
3936
|
+
}
|
|
3937
|
+
/**
|
|
3938
|
+
* Resolve a {@link ClipShape} to a freeform `points` path filling a `w × h` box, ready to hand
|
|
3939
|
+
* to `addImage({ points })`.
|
|
3940
|
+
*
|
|
3941
|
+
* `w`/`h` are the image box's size **in inches** and must match the `w`/`h` the picture is drawn
|
|
3942
|
+
* at — the path is emitted in the box's own coordinate space, not normalized (see the module
|
|
3943
|
+
* note above).
|
|
3944
|
+
* @param {ClipShape} shape - the named silhouette and its options
|
|
3945
|
+
* @param {number} w - image box width (inches)
|
|
3946
|
+
* @param {number} h - image box height (inches)
|
|
3947
|
+
* @returns {GeometryPoint[]} the freeform path, for `addImage({ points })`
|
|
3948
|
+
* @throws {InvalidOptionError} `clip/invalid-shape` when `kind`, `flat` or `preset` is not one the
|
|
3949
|
+
* silhouette names. From JavaScript an unknown preset used to fail reading a field off
|
|
3950
|
+
* `undefined`, an unknown kind returned nothing and an unknown flat side traced the right-flat path.
|
|
3951
|
+
* @example
|
|
3952
|
+
* const w = 5.22, h = 7.5
|
|
3953
|
+
* slide.addImage({
|
|
3954
|
+
* path: 'cover.jpg', x: 0, y: 0, w, h,
|
|
3955
|
+
* points: clipPath({ kind: 'half-disc', flat: 'right' }, w, h),
|
|
3956
|
+
* sizing: { type: 'cover' }, // center-crop the photo into the "D"
|
|
3957
|
+
* })
|
|
3958
|
+
*/
|
|
3959
|
+
function clipPath(shape, w, h) {
|
|
3960
|
+
switch (checkEnumOrThrow(String(shape?.kind), CLIP_KINDS, "clipPath `kind`", "clip/invalid-shape")) {
|
|
3961
|
+
case "half-disc": return halfDisc(w, h, checkEnumOrThrow(shape.flat, FLAT_SIDES, "clipPath `flat`", "clip/invalid-shape"), checkEnumOrThrow(shape.preset ?? "deep", HALF_DISC_PRESETS, "clipPath `preset`", "clip/invalid-shape"));
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
//#endregion
|
|
3965
|
+
//#region src/types/text.ts
|
|
3966
|
+
/** Factory for a single inline text run. Prevents `as never` casts when building mixed-style run arrays. */
|
|
3967
|
+
function textRun(text, options) {
|
|
3968
|
+
return options !== void 0 ? {
|
|
3969
|
+
text,
|
|
3970
|
+
options
|
|
3971
|
+
} : { text };
|
|
3972
|
+
}
|
|
3973
|
+
/** Wraps a run array so TypeScript accepts it as `TextProps[]` without a cast. */
|
|
3974
|
+
function textRuns(runs) {
|
|
3975
|
+
return runs;
|
|
3976
|
+
}
|
|
3977
|
+
//#endregion
|
|
3978
|
+
export { fetchMediaBase64 as a, ALL_CONSTRUCT_FAMILIES as c, IMG_SVG_PLACEHOLDER as d, fetchFontBytes as i, composePresentation as l, textRuns as n, isRemote as o, clipPath as r, placeholderSvgPreview as s, textRun as t, PresentationCore as u };
|
|
3979
|
+
|
|
3980
|
+
//# sourceMappingURL=text-sN64Otf_.js.map
|