@qvac/harness 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -1
- package/NOTICE.md +16 -0
- package/dist-lib/lib/capabilities/completion.d.ts +1 -0
- package/dist-lib/lib/capabilities/completion.js +28 -8
- package/dist-lib/lib/capabilities/tool-turns.d.ts +3 -0
- package/dist-lib/lib/capabilities/tool-turns.js +7 -0
- package/dist-lib/lib/context-window.js +2 -2
- package/dist-lib/lib/helpers/run-outcome.d.ts +4 -1
- package/dist-lib/lib/helpers/run-outcome.js +14 -8
- package/dist-lib/lib/helpers/tool-call-markup.js +4 -4
- package/dist-lib/lib/hooks/built-in/arg-repair.js +7 -0
- package/dist-lib/lib/office/engine/codes.d.ts +62 -0
- package/dist-lib/lib/office/engine/codes.js +61 -0
- package/dist-lib/lib/office/engine/docx/document.d.ts +216 -0
- package/dist-lib/lib/office/engine/docx/document.js +1152 -0
- package/dist-lib/lib/office/engine/docx/index.d.ts +7 -0
- package/dist-lib/lib/office/engine/docx/index.js +8 -0
- package/dist-lib/lib/office/engine/docx/styles.d.ts +31 -0
- package/dist-lib/lib/office/engine/docx/styles.js +173 -0
- package/dist-lib/lib/office/engine/docx/template.d.ts +18 -0
- package/dist-lib/lib/office/engine/docx/template.js +133 -0
- package/dist-lib/lib/office/engine/docx/text.d.ts +55 -0
- package/dist-lib/lib/office/engine/docx/text.js +538 -0
- package/dist-lib/lib/office/engine/drawingml/chart.d.ts +63 -0
- package/dist-lib/lib/office/engine/drawingml/chart.js +539 -0
- package/dist-lib/lib/office/engine/drawingml/text.d.ts +35 -0
- package/dist-lib/lib/office/engine/drawingml/text.js +352 -0
- package/dist-lib/lib/office/engine/errors.d.ts +6 -0
- package/dist-lib/lib/office/engine/errors.js +12 -0
- package/dist-lib/lib/office/engine/image.d.ts +8 -0
- package/dist-lib/lib/office/engine/image.js +48 -0
- package/dist-lib/lib/office/engine/index.d.ts +12 -0
- package/dist-lib/lib/office/engine/index.js +7 -0
- package/dist-lib/lib/office/engine/opc/content-types.d.ts +17 -0
- package/dist-lib/lib/office/engine/opc/content-types.js +74 -0
- package/dist-lib/lib/office/engine/opc/media.d.ts +13 -0
- package/dist-lib/lib/office/engine/opc/media.js +11 -0
- package/dist-lib/lib/office/engine/opc/package.d.ts +51 -0
- package/dist-lib/lib/office/engine/opc/package.js +324 -0
- package/dist-lib/lib/office/engine/opc/paths.d.ts +7 -0
- package/dist-lib/lib/office/engine/opc/paths.js +46 -0
- package/dist-lib/lib/office/engine/opc/properties.d.ts +7 -0
- package/dist-lib/lib/office/engine/opc/properties.js +75 -0
- package/dist-lib/lib/office/engine/opc/relationships.d.ts +23 -0
- package/dist-lib/lib/office/engine/opc/relationships.js +65 -0
- package/dist-lib/lib/office/engine/opc/zip.d.ts +15 -0
- package/dist-lib/lib/office/engine/opc/zip.js +264 -0
- package/dist-lib/lib/office/engine/pptx/fit.d.ts +12 -0
- package/dist-lib/lib/office/engine/pptx/fit.js +37 -0
- package/dist-lib/lib/office/engine/pptx/index.d.ts +8 -0
- package/dist-lib/lib/office/engine/pptx/index.js +8 -0
- package/dist-lib/lib/office/engine/pptx/inherit.d.ts +29 -0
- package/dist-lib/lib/office/engine/pptx/inherit.js +155 -0
- package/dist-lib/lib/office/engine/pptx/notes.d.ts +5 -0
- package/dist-lib/lib/office/engine/pptx/notes.js +85 -0
- package/dist-lib/lib/office/engine/pptx/presentation.d.ts +54 -0
- package/dist-lib/lib/office/engine/pptx/presentation.js +749 -0
- package/dist-lib/lib/office/engine/pptx/read.d.ts +9 -0
- package/dist-lib/lib/office/engine/pptx/read.js +66 -0
- package/dist-lib/lib/office/engine/pptx/shapes.d.ts +52 -0
- package/dist-lib/lib/office/engine/pptx/shapes.js +276 -0
- package/dist-lib/lib/office/engine/pptx/slides.d.ts +27 -0
- package/dist-lib/lib/office/engine/pptx/slides.js +123 -0
- package/dist-lib/lib/office/engine/pptx/template.d.ts +20 -0
- package/dist-lib/lib/office/engine/pptx/template.js +391 -0
- package/dist-lib/lib/office/engine/pptx/types.d.ts +120 -0
- package/dist-lib/lib/office/engine/pptx/types.js +1 -0
- package/dist-lib/lib/office/engine/units.d.ts +3 -0
- package/dist-lib/lib/office/engine/units.js +12 -0
- package/dist-lib/lib/office/engine/utf8.d.ts +2 -0
- package/dist-lib/lib/office/engine/utf8.js +103 -0
- package/dist-lib/lib/office/engine/xlsx/formula.d.ts +49 -0
- package/dist-lib/lib/office/engine/xlsx/formula.js +381 -0
- package/dist-lib/lib/office/engine/xlsx/index.d.ts +8 -0
- package/dist-lib/lib/office/engine/xlsx/index.js +8 -0
- package/dist-lib/lib/office/engine/xlsx/refs.d.ts +22 -0
- package/dist-lib/lib/office/engine/xlsx/refs.js +82 -0
- package/dist-lib/lib/office/engine/xlsx/reshape.d.ts +21 -0
- package/dist-lib/lib/office/engine/xlsx/reshape.js +424 -0
- package/dist-lib/lib/office/engine/xlsx/sheet.d.ts +108 -0
- package/dist-lib/lib/office/engine/xlsx/sheet.js +662 -0
- package/dist-lib/lib/office/engine/xlsx/strings.d.ts +15 -0
- package/dist-lib/lib/office/engine/xlsx/strings.js +93 -0
- package/dist-lib/lib/office/engine/xlsx/styles.d.ts +38 -0
- package/dist-lib/lib/office/engine/xlsx/styles.js +369 -0
- package/dist-lib/lib/office/engine/xlsx/template.d.ts +7 -0
- package/dist-lib/lib/office/engine/xlsx/template.js +54 -0
- package/dist-lib/lib/office/engine/xlsx/workbook.d.ts +161 -0
- package/dist-lib/lib/office/engine/xlsx/workbook.js +850 -0
- package/dist-lib/lib/office/engine/xlsx/write.d.ts +2 -0
- package/dist-lib/lib/office/engine/xlsx/write.js +42 -0
- package/dist-lib/lib/office/engine/xml.d.ts +45 -0
- package/dist-lib/lib/office/engine/xml.js +258 -0
- package/dist-lib/lib/tools/exec/scope.d.ts +2 -0
- package/dist-lib/lib/tools/exec/scope.js +33 -2
- package/dist-lib/lib/tools/pdf/build.d.ts +43 -0
- package/dist-lib/lib/tools/pdf/build.js +150 -0
- package/dist-lib/lib/tools/pdf/images.d.ts +7 -0
- package/dist-lib/lib/tools/pdf/images.js +82 -0
- package/dist-lib/lib/tools/pdf/request.d.ts +63 -0
- package/dist-lib/lib/tools/pdf/request.js +335 -0
- package/dist-lib/lib/tools/pdf.d.ts +6 -0
- package/dist-lib/lib/tools/pdf.js +302 -0
- package/dist-lib/lib/tools/skill.js +14 -0
- package/package.json +25 -1
- package/worker/harness-sidecar.mjs +3 -1
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
import { Package } from '../opc/package.js';
|
|
2
|
+
import * as xml from '../xml.js';
|
|
3
|
+
import * as text from '../drawingml/text.js';
|
|
4
|
+
import * as chart from '../drawingml/chart.js';
|
|
5
|
+
import * as media from '../opc/media.js';
|
|
6
|
+
import * as properties from '../opc/properties.js';
|
|
7
|
+
import { probe } from '../image.js';
|
|
8
|
+
import { DocsError } from '../errors.js';
|
|
9
|
+
import { isRecord } from '../../../guards.js';
|
|
10
|
+
import * as inherit from './inherit.js';
|
|
11
|
+
import * as fit from './fit.js';
|
|
12
|
+
import * as template from './template.js';
|
|
13
|
+
import * as slides from './slides.js';
|
|
14
|
+
import * as notes from './notes.js';
|
|
15
|
+
import { pick, describe, layoutName, layoutType, BODY_TYPES } from './read.js';
|
|
16
|
+
import { SHAPE_ORDER, shapesOf, placeholderFrom, pictureElement, textBoxElement, tableElement, chartFrameElement, setBackground, nextShapeId } from './shapes.js';
|
|
17
|
+
const { a: A, p: P, r: R, p14: P14 } = xml.ns;
|
|
18
|
+
const STRICT_REL = 'http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument';
|
|
19
|
+
const REL_BASE = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/';
|
|
20
|
+
const REL = {
|
|
21
|
+
officeDocument: REL_BASE + 'officeDocument',
|
|
22
|
+
slide: REL_BASE + 'slide',
|
|
23
|
+
notesSlide: REL_BASE + 'notesSlide',
|
|
24
|
+
slideLayout: REL_BASE + 'slideLayout',
|
|
25
|
+
image: REL_BASE + 'image',
|
|
26
|
+
chart: REL_BASE + 'chart',
|
|
27
|
+
hyperlink: REL_BASE + 'hyperlink'
|
|
28
|
+
};
|
|
29
|
+
const SLIDE_TYPE = 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml';
|
|
30
|
+
const MAIN_TYPES = new Set([
|
|
31
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml',
|
|
32
|
+
'application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml',
|
|
33
|
+
'application/vnd.openxmlformats-officedocument.presentationml.template.main+xml',
|
|
34
|
+
'application/vnd.ms-powerpoint.presentation.macroEnabled.main+xml',
|
|
35
|
+
'application/vnd.ms-powerpoint.slideshow.macroEnabled.main+xml',
|
|
36
|
+
'application/vnd.ms-powerpoint.template.macroEnabled.main+xml'
|
|
37
|
+
]);
|
|
38
|
+
const SKIPPED_PLACEHOLDERS = new Set(['dt', 'ftr', 'sldNum']);
|
|
39
|
+
const TITLE_TYPES = new Set(['title', 'ctrTitle']);
|
|
40
|
+
const SVG_BLIP_EXT = '{96DAC541-7B7A-43D3-8B79-37D633B846F1}';
|
|
41
|
+
const BOX_KEYS = ['x', 'y', 'width', 'height'];
|
|
42
|
+
const EMU_PER_PIXEL = 9525;
|
|
43
|
+
const EMPTY_SLIDE = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
44
|
+
<p:sld xmlns:a="${A}" xmlns:r="${R}" xmlns:p="${P}"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr></p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sld>`;
|
|
45
|
+
function checkPlacement(opts) {
|
|
46
|
+
for (const key of BOX_KEYS) {
|
|
47
|
+
const value = opts[key];
|
|
48
|
+
if (value !== undefined &&
|
|
49
|
+
!(typeof value === 'number' && Number.isInteger(value) && value >= 0)) {
|
|
50
|
+
throw new DocsError('INVALID_BOX', `${key} must be a non-negative whole number of EMU, got ${String(value)}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function withLevels(paragraphs, before) {
|
|
55
|
+
const list = paragraphs.length === 0 ? [{ text: '' }] : paragraphs;
|
|
56
|
+
return list.map((p, i) => ({
|
|
57
|
+
text: p.text,
|
|
58
|
+
level: p.level ?? (before[Math.min(i, before.length - 1)] ?? { level: 0 }).level
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
function sameParagraphs(a, b) {
|
|
62
|
+
return a.length === b.length && a.every((p, i) => p.text === b[i].text && p.level === b[i].level);
|
|
63
|
+
}
|
|
64
|
+
function applyAutofit(txBody, estimate) {
|
|
65
|
+
const a = xml.builder(xml.owner(txBody), A);
|
|
66
|
+
let bodyPr = xml.child(txBody, A, 'bodyPr');
|
|
67
|
+
if (!bodyPr) {
|
|
68
|
+
bodyPr = a('bodyPr');
|
|
69
|
+
txBody.insertBefore(bodyPr, txBody.firstChild);
|
|
70
|
+
}
|
|
71
|
+
for (const local of ['noAutofit', 'spAutoFit', 'normAutofit']) {
|
|
72
|
+
for (const el of xml.children(bodyPr, A, local)) {
|
|
73
|
+
xml.remove(el);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const autofit = a('normAutofit', {
|
|
77
|
+
fontScale: estimate.fontScale < 1 ? Math.round(estimate.fontScale * 100000) : undefined,
|
|
78
|
+
lnSpcReduction: estimate.lineReduction > 0 ? Math.round(estimate.lineReduction * 100000) : undefined
|
|
79
|
+
});
|
|
80
|
+
const after = xml
|
|
81
|
+
.children(bodyPr, A)
|
|
82
|
+
.find((el) => ['scene3d', 'sp3d', 'flatTx', 'extLst'].includes(el.localName ?? ''));
|
|
83
|
+
bodyPr.insertBefore(autofit, after ?? null);
|
|
84
|
+
}
|
|
85
|
+
function textBodies(found) {
|
|
86
|
+
if (found.kind === 'table') {
|
|
87
|
+
return xml.descendants(found.el, A, 'txBody');
|
|
88
|
+
}
|
|
89
|
+
const own = xml.child(found.el, P, 'txBody');
|
|
90
|
+
return own ? [own] : [];
|
|
91
|
+
}
|
|
92
|
+
function findShape(shapes, ref) {
|
|
93
|
+
if (ref === 'title') {
|
|
94
|
+
return shapes.find((s) => s.placeholder !== null && TITLE_TYPES.has(s.placeholder));
|
|
95
|
+
}
|
|
96
|
+
if (ref === 'subtitle') {
|
|
97
|
+
return shapes.find((s) => s.placeholder === 'subTitle');
|
|
98
|
+
}
|
|
99
|
+
if (ref === 'body') {
|
|
100
|
+
return shapes.find((s) => s.placeholder !== null && BODY_TYPES.has(s.placeholder) && s.el.localName === 'sp');
|
|
101
|
+
}
|
|
102
|
+
return shapes.find((s) => String(s.id) === String(ref));
|
|
103
|
+
}
|
|
104
|
+
function dropSvgBlip(blip) {
|
|
105
|
+
for (const ext of xml.descendants(blip, A, 'ext')) {
|
|
106
|
+
if (ext.getAttribute('uri') === SVG_BLIP_EXT) {
|
|
107
|
+
xml.remove(ext);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const extLst = xml.child(blip, A, 'extLst');
|
|
111
|
+
if (extLst && !xml.children(extLst).length) {
|
|
112
|
+
xml.remove(extLst);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function dropCrop(blipFill) {
|
|
116
|
+
for (const srcRect of xml.children(blipFill, A, 'srcRect')) {
|
|
117
|
+
xml.remove(srcRect);
|
|
118
|
+
}
|
|
119
|
+
for (const fillRect of xml.descendants(blipFill, A, 'fillRect')) {
|
|
120
|
+
for (const side of ['l', 't', 'r', 'b']) {
|
|
121
|
+
fillRect.removeAttribute(side);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function shapeNotFound(slide, ref, shapes) {
|
|
126
|
+
const listing = shapes.map((s) => `${s.id} ${s.kind}${s.placeholder ? ` (${s.placeholder})` : ''} "${s.name}"`);
|
|
127
|
+
return new DocsError('SHAPE_NOT_FOUND', `slide ${String(slide)} has no shape ${JSON.stringify(ref)}; its shapes are: ${listing.join(', ') || 'none'}`);
|
|
128
|
+
}
|
|
129
|
+
function slideFromLayout(layoutDoc) {
|
|
130
|
+
const doc = xml.parse(EMPTY_SLIDE);
|
|
131
|
+
const spTree = xml.descendants(doc, P, 'spTree')[0];
|
|
132
|
+
let nextId = 2;
|
|
133
|
+
for (const source of shapesOf(layoutDoc)) {
|
|
134
|
+
const skip = source.el.localName !== 'sp' ||
|
|
135
|
+
!source.placeholder ||
|
|
136
|
+
SKIPPED_PLACEHOLDERS.has(source.placeholder);
|
|
137
|
+
if (!skip) {
|
|
138
|
+
spTree.appendChild(placeholderFrom(doc, source, nextId++));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return doc;
|
|
142
|
+
}
|
|
143
|
+
function checkSlideText(doc, at, opts) {
|
|
144
|
+
const shapes = shapesOf(doc);
|
|
145
|
+
for (const [ref, value] of [
|
|
146
|
+
['title', opts.title],
|
|
147
|
+
['body', opts.body]
|
|
148
|
+
]) {
|
|
149
|
+
if (value === undefined)
|
|
150
|
+
continue;
|
|
151
|
+
text.normalizeParagraphs(value);
|
|
152
|
+
if (!findShape(shapes, ref)) {
|
|
153
|
+
throw shapeNotFound(at, ref, shapes);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function fitted(info, current) {
|
|
158
|
+
const scale = Math.min(current.width / info.width, current.height / info.height);
|
|
159
|
+
const width = Math.round(info.width * scale);
|
|
160
|
+
const height = Math.round(info.height * scale);
|
|
161
|
+
return {
|
|
162
|
+
x: current.x + Math.round((current.width - width) / 2),
|
|
163
|
+
y: current.y + Math.round((current.height - height) / 2),
|
|
164
|
+
width,
|
|
165
|
+
height
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function tableBody(tc) {
|
|
169
|
+
const existing = xml.child(tc, A, 'txBody');
|
|
170
|
+
if (existing) {
|
|
171
|
+
return existing;
|
|
172
|
+
}
|
|
173
|
+
const a = xml.builder(xml.owner(tc), A);
|
|
174
|
+
const txBody = a('txBody');
|
|
175
|
+
for (const local of ['bodyPr', 'lstStyle', 'p']) {
|
|
176
|
+
txBody.appendChild(a(local));
|
|
177
|
+
}
|
|
178
|
+
tc.insertBefore(txBody, tc.firstChild);
|
|
179
|
+
return txBody;
|
|
180
|
+
}
|
|
181
|
+
function ensureXfrm(el) {
|
|
182
|
+
const existing = inherit.xfrmOf(el);
|
|
183
|
+
if (existing) {
|
|
184
|
+
return existing;
|
|
185
|
+
}
|
|
186
|
+
const doc = xml.owner(el);
|
|
187
|
+
if (el.localName === 'graphicFrame') {
|
|
188
|
+
const frameXfrm = xml.builder(doc, P)('xfrm');
|
|
189
|
+
el.insertBefore(frameXfrm, xml.children(el)[1] ?? null);
|
|
190
|
+
return frameXfrm;
|
|
191
|
+
}
|
|
192
|
+
const propsName = el.localName === 'grpSp' ? 'grpSpPr' : 'spPr';
|
|
193
|
+
let props = xml.child(el, P, propsName);
|
|
194
|
+
if (!props) {
|
|
195
|
+
props = xml.builder(doc, P)(propsName);
|
|
196
|
+
el.insertBefore(props, xml.children(el)[1] ?? null);
|
|
197
|
+
}
|
|
198
|
+
const made = xml.builder(doc, A)('xfrm');
|
|
199
|
+
props.insertBefore(made, props.firstChild);
|
|
200
|
+
return made;
|
|
201
|
+
}
|
|
202
|
+
function writeXfrm(el, box) {
|
|
203
|
+
const xfrm = ensureXfrm(el);
|
|
204
|
+
const a = xml.builder(xml.owner(el), A);
|
|
205
|
+
let off = xml.child(xfrm, A, 'off');
|
|
206
|
+
if (!off) {
|
|
207
|
+
off = a('off');
|
|
208
|
+
xfrm.insertBefore(off, xfrm.firstChild);
|
|
209
|
+
}
|
|
210
|
+
let ext = xml.child(xfrm, A, 'ext');
|
|
211
|
+
if (!ext) {
|
|
212
|
+
ext = a('ext');
|
|
213
|
+
xfrm.insertBefore(ext, off.nextSibling);
|
|
214
|
+
}
|
|
215
|
+
xml.setAttributes(off, { x: box.x, y: box.y });
|
|
216
|
+
xml.setAttributes(ext, { cx: box.width, cy: box.height });
|
|
217
|
+
}
|
|
218
|
+
function checkBox(box) {
|
|
219
|
+
if (!isRecord(box)) {
|
|
220
|
+
throw new DocsError('INVALID_BOX', 'box must be { x, y, width, height }');
|
|
221
|
+
}
|
|
222
|
+
const keys = Object.keys(box);
|
|
223
|
+
if (!keys.length || keys.some((k) => !['x', 'y', 'width', 'height'].includes(k))) {
|
|
224
|
+
throw new DocsError('INVALID_BOX', 'box takes x, y, width and height in EMU');
|
|
225
|
+
}
|
|
226
|
+
const out = {};
|
|
227
|
+
for (const key of BOX_KEYS) {
|
|
228
|
+
const value = box[key];
|
|
229
|
+
if (value === undefined) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (!(typeof value === 'number' && Number.isInteger(value) && value >= 0)) {
|
|
233
|
+
throw new DocsError('INVALID_BOX', `${key} must be a non-negative whole number of EMU, got ${String(value)}`);
|
|
234
|
+
}
|
|
235
|
+
out[key] = value;
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
function shapeIds(el) {
|
|
240
|
+
const own = el.localName === 'cNvPr' ? el : xml.descendants(el, P, 'cNvPr')[0];
|
|
241
|
+
const all = [own, ...xml.descendants(el, P, 'cNvPr')].filter((node) => node !== undefined);
|
|
242
|
+
return new Set(all.map((node) => xml.attr(node, 'id')));
|
|
243
|
+
}
|
|
244
|
+
export class Presentation {
|
|
245
|
+
package;
|
|
246
|
+
main;
|
|
247
|
+
slidesChanged;
|
|
248
|
+
touchedSlides;
|
|
249
|
+
constructor(pkg) {
|
|
250
|
+
this.package = pkg;
|
|
251
|
+
this.slidesChanged = false;
|
|
252
|
+
this.touchedSlides = new Set();
|
|
253
|
+
const main = pkg.related('/', REL.officeDocument)[0];
|
|
254
|
+
if (!main || !MAIN_TYPES.has(pkg.contentType(main) ?? '')) {
|
|
255
|
+
if (pkg.rels('/').byType(STRICT_REL).length) {
|
|
256
|
+
throw new DocsError('UNSUPPORTED_PPTX', 'the presentation uses Strict Open XML, which is not supported');
|
|
257
|
+
}
|
|
258
|
+
throw new DocsError('NOT_PPTX', 'the file is not a PowerPoint presentation');
|
|
259
|
+
}
|
|
260
|
+
this.main = main;
|
|
261
|
+
const top = pkg.root(main);
|
|
262
|
+
if (top.namespaceURI !== P || top.localName !== 'presentation') {
|
|
263
|
+
throw new DocsError('UNSUPPORTED_PPTX', 'the presentation uses Strict Open XML, which is not supported');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
static open(bytes, opts) {
|
|
267
|
+
return new Presentation(Package.open(bytes, opts));
|
|
268
|
+
}
|
|
269
|
+
static create(opts = {}) {
|
|
270
|
+
const pkg = Package.create();
|
|
271
|
+
template.build(pkg, opts);
|
|
272
|
+
return new Presentation(pkg);
|
|
273
|
+
}
|
|
274
|
+
get changed() {
|
|
275
|
+
return this.package.changed;
|
|
276
|
+
}
|
|
277
|
+
get slideCount() {
|
|
278
|
+
return slides.entries(this.deck).length;
|
|
279
|
+
}
|
|
280
|
+
get slideSize() {
|
|
281
|
+
const sz = xml.child(this.package.root(this.main), P, 'sldSz');
|
|
282
|
+
return sz
|
|
283
|
+
? { width: Number(sz.getAttribute('cx')), height: Number(sz.getAttribute('cy')) }
|
|
284
|
+
: { width: 9144000, height: 6858000 };
|
|
285
|
+
}
|
|
286
|
+
layouts() {
|
|
287
|
+
return slides.allLayouts(this.deck).map(({ part }) => ({
|
|
288
|
+
name: layoutName(this.package.xml(part)),
|
|
289
|
+
type: layoutType(this.package.xml(part))
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
read(opts = {}) {
|
|
293
|
+
const all = slides.entries(this.deck);
|
|
294
|
+
const wanted = pick(all.length, opts);
|
|
295
|
+
return all.flatMap((entry, index) => (wanted(index) ? [this.slideInfo(entry, index)] : []));
|
|
296
|
+
}
|
|
297
|
+
setText(target, value, opts = {}) {
|
|
298
|
+
if (opts.fit !== undefined && opts.fit !== 'shrink') {
|
|
299
|
+
throw new DocsError('INVALID_OPTIONS', 'fit must be "shrink" or left out');
|
|
300
|
+
}
|
|
301
|
+
const { entry, found } = this.locate(target);
|
|
302
|
+
if (found.kind === 'table') {
|
|
303
|
+
throw new DocsError('NOT_TEXT', `shape ${found.id} is a table; set its cells with setCellText`);
|
|
304
|
+
}
|
|
305
|
+
if (found.el.localName !== 'sp') {
|
|
306
|
+
throw new DocsError('NOT_TEXT', `shape ${found.id} is a ${found.kind} and holds no text`);
|
|
307
|
+
}
|
|
308
|
+
const txBody = text.ensureTextBody(found.el, SHAPE_ORDER);
|
|
309
|
+
const result = this.write({ part: entry.part, txBody }, value);
|
|
310
|
+
const estimate = this.estimate(entry.part, found.el, txBody);
|
|
311
|
+
if (!estimate) {
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
314
|
+
result.overflow = estimate.overflow;
|
|
315
|
+
if (opts.fit === 'shrink') {
|
|
316
|
+
applyAutofit(txBody, estimate);
|
|
317
|
+
this.package.touch(entry.part);
|
|
318
|
+
result.fontScale = estimate.fontScale;
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
setCellText(cell, value) {
|
|
323
|
+
const { entry, found } = this.locate(cell);
|
|
324
|
+
if (found.kind !== 'table') {
|
|
325
|
+
throw new DocsError('NOT_TABLE', `shape ${found.id} is a ${found.kind}, not a table`);
|
|
326
|
+
}
|
|
327
|
+
const { row, column } = cell;
|
|
328
|
+
const rows = xml.descendants(found.el, A, 'tr');
|
|
329
|
+
const tr = rows[row];
|
|
330
|
+
if (!tr) {
|
|
331
|
+
throw new DocsError('CELL_NOT_FOUND', `row ${row} does not exist; the table has ${rows.length} (0 to ${rows.length - 1})`);
|
|
332
|
+
}
|
|
333
|
+
const cells = xml.children(tr, A, 'tc');
|
|
334
|
+
const tc = cells[column];
|
|
335
|
+
if (!tc) {
|
|
336
|
+
throw new DocsError('CELL_NOT_FOUND', `column ${column} does not exist; the row has ${cells.length} (0 to ${cells.length - 1})`);
|
|
337
|
+
}
|
|
338
|
+
if (tc.getAttribute('hMerge') === '1' || tc.getAttribute('vMerge') === '1') {
|
|
339
|
+
throw new DocsError('CELL_MERGED', `cell ${row},${column} is covered by a merged cell; write the merge's top-left cell`);
|
|
340
|
+
}
|
|
341
|
+
return this.write({ part: entry.part, txBody: tableBody(tc) }, value);
|
|
342
|
+
}
|
|
343
|
+
setStyle(target, style, opts = {}) {
|
|
344
|
+
const { entry, found } = this.locate(target);
|
|
345
|
+
const bodies = textBodies(found);
|
|
346
|
+
if (bodies.length === 0) {
|
|
347
|
+
throw new DocsError('NOT_TEXT', `shape ${found.id} is a ${found.kind} and holds no text`);
|
|
348
|
+
}
|
|
349
|
+
if (opts.paragraph !== undefined && bodies.length > 1) {
|
|
350
|
+
throw new DocsError('INVALID_STYLE', 'paragraph cannot be used on a table; style the whole table instead');
|
|
351
|
+
}
|
|
352
|
+
const runs = bodies.reduce((n, txBody) => n + text.applyStyle(txBody, style, opts), 0);
|
|
353
|
+
this.package.touch(entry.part);
|
|
354
|
+
return { runs };
|
|
355
|
+
}
|
|
356
|
+
format(target, style, opts) {
|
|
357
|
+
return this.setStyle(target, style, opts);
|
|
358
|
+
}
|
|
359
|
+
addSlide(opts = {}) {
|
|
360
|
+
const all = slides.entries(this.deck);
|
|
361
|
+
const at = opts.at ?? all.length;
|
|
362
|
+
if (!Number.isInteger(at) || at < 0 || at > all.length) {
|
|
363
|
+
throw new DocsError('SLIDE_NOT_FOUND', `at must be a position from 0 to ${all.length}, got ${String(opts.at)}`);
|
|
364
|
+
}
|
|
365
|
+
const layout = slides.pickLayout(this.deck, opts);
|
|
366
|
+
const layoutDoc = this.package.xml(layout);
|
|
367
|
+
const doc = slideFromLayout(layoutDoc);
|
|
368
|
+
checkSlideText(doc, at, opts);
|
|
369
|
+
this.insertSlide({ doc, layout, at, ids: all.map((e) => e.id), before: all[at]?.el ?? null });
|
|
370
|
+
if (opts.title !== undefined) {
|
|
371
|
+
this.setText({ slide: at, shape: 'title' }, opts.title);
|
|
372
|
+
}
|
|
373
|
+
if (opts.body !== undefined) {
|
|
374
|
+
this.setText({ slide: at, shape: 'body' }, opts.body);
|
|
375
|
+
}
|
|
376
|
+
return { index: at, layout: layoutName(layoutDoc), shapes: shapesOf(doc).map(describe) };
|
|
377
|
+
}
|
|
378
|
+
addPicture(slide, bytes, opts = {}) {
|
|
379
|
+
const entry = slides.slideAt(this.deck, slide);
|
|
380
|
+
const box = this.pictureBox(probe(bytes), opts);
|
|
381
|
+
const { rId } = media.addImage(this.package, bytes, { source: entry.part, dir: '../media' });
|
|
382
|
+
const doc = this.package.xml(entry.part);
|
|
383
|
+
const id = nextShapeId(doc);
|
|
384
|
+
this.spTree(entry.part).appendChild(pictureElement(doc, { id, rId, box, name: opts.name, description: opts.description }));
|
|
385
|
+
this.package.touch(entry.part);
|
|
386
|
+
return { id, ...box };
|
|
387
|
+
}
|
|
388
|
+
addTextBox(slide, opts = {}) {
|
|
389
|
+
const entry = slides.slideAt(this.deck, slide);
|
|
390
|
+
const doc = this.package.xml(entry.part);
|
|
391
|
+
const id = nextShapeId(doc);
|
|
392
|
+
const box = this.box(opts);
|
|
393
|
+
const el = textBoxElement(doc, {
|
|
394
|
+
id,
|
|
395
|
+
box,
|
|
396
|
+
text: opts.text,
|
|
397
|
+
style: opts.style,
|
|
398
|
+
fill: opts.fill,
|
|
399
|
+
name: opts.name
|
|
400
|
+
});
|
|
401
|
+
this.spTree(entry.part).appendChild(el);
|
|
402
|
+
this.package.touch(entry.part);
|
|
403
|
+
return { id, ...box };
|
|
404
|
+
}
|
|
405
|
+
addTable(slide, opts) {
|
|
406
|
+
const entry = slides.slideAt(this.deck, slide);
|
|
407
|
+
const doc = this.package.xml(entry.part);
|
|
408
|
+
const id = nextShapeId(doc);
|
|
409
|
+
const box = this.box(opts);
|
|
410
|
+
const el = tableElement(doc, {
|
|
411
|
+
id,
|
|
412
|
+
box,
|
|
413
|
+
rows: opts.rows,
|
|
414
|
+
header: opts.header,
|
|
415
|
+
name: opts.name,
|
|
416
|
+
tableStyle: template.TABLE_STYLE
|
|
417
|
+
});
|
|
418
|
+
this.spTree(entry.part).appendChild(el);
|
|
419
|
+
this.package.touch(entry.part);
|
|
420
|
+
return { id, ...box };
|
|
421
|
+
}
|
|
422
|
+
addChart(slide, spec) {
|
|
423
|
+
const entry = slides.slideAt(this.deck, slide);
|
|
424
|
+
const box = this.box(spec);
|
|
425
|
+
const part = this.package.freeName('/ppt/charts/chart%d.xml');
|
|
426
|
+
chart.createChart({ pkg: this.package, part }, spec);
|
|
427
|
+
const rId = this.package.rels(entry.part).add(REL.chart, part);
|
|
428
|
+
const doc = this.package.xml(entry.part);
|
|
429
|
+
const id = nextShapeId(doc);
|
|
430
|
+
this.spTree(entry.part).appendChild(chartFrameElement(doc, { id, box, rId, name: spec.name }));
|
|
431
|
+
this.package.touch(entry.part);
|
|
432
|
+
return { id, ...box };
|
|
433
|
+
}
|
|
434
|
+
setChartData(target, data) {
|
|
435
|
+
return chart.setChartData({ pkg: this.package, part: this.chartOf(target) }, data);
|
|
436
|
+
}
|
|
437
|
+
setChartTitle(target, title) {
|
|
438
|
+
const value = title;
|
|
439
|
+
if (!(value === null || typeof value === 'string' || Array.isArray(value))) {
|
|
440
|
+
throw new DocsError('INVALID_TEXT', 'title must be text, or null to remove it');
|
|
441
|
+
}
|
|
442
|
+
const ref = { pkg: this.package, part: this.chartOf(target) };
|
|
443
|
+
if (value === null || typeof value === 'string') {
|
|
444
|
+
return chart.setChartTitle(ref, value);
|
|
445
|
+
}
|
|
446
|
+
return chart.setChartTitle(ref, text.normalizeParagraphs(value));
|
|
447
|
+
}
|
|
448
|
+
setBackground(slide, color) {
|
|
449
|
+
const entry = slides.slideAt(this.deck, slide);
|
|
450
|
+
setBackground(this.package.xml(entry.part), color);
|
|
451
|
+
this.package.touch(entry.part);
|
|
452
|
+
return { changed: true };
|
|
453
|
+
}
|
|
454
|
+
deleteShape(target) {
|
|
455
|
+
const { entry, found } = this.locate(target);
|
|
456
|
+
const doc = this.package.xml(entry.part);
|
|
457
|
+
const ids = shapeIds(found.el);
|
|
458
|
+
const result = { deleted: found.id };
|
|
459
|
+
const timing = xml.child(xml.root(doc), P, 'timing');
|
|
460
|
+
if (timing && xml.descendants(timing, P, 'spTgt').some((t) => ids.has(xml.attr(t, 'spid')))) {
|
|
461
|
+
xml.remove(timing);
|
|
462
|
+
result.removed = { animations: true };
|
|
463
|
+
}
|
|
464
|
+
for (const local of ['stCxn', 'endCxn']) {
|
|
465
|
+
for (const link of xml.descendants(doc, A, local)) {
|
|
466
|
+
if (ids.has(xml.attr(link, 'id'))) {
|
|
467
|
+
xml.remove(link);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
xml.remove(found.el);
|
|
472
|
+
this.touchSlide(entry.part);
|
|
473
|
+
return result;
|
|
474
|
+
}
|
|
475
|
+
setBox(target, input) {
|
|
476
|
+
const { entry, found } = this.locate(target);
|
|
477
|
+
const box = checkBox(input);
|
|
478
|
+
const current = inherit.box({ pkg: this.package, slide: entry.part, el: found.el });
|
|
479
|
+
const merged = { ...current, ...box };
|
|
480
|
+
const { x, y, width, height } = merged;
|
|
481
|
+
if (x === undefined || y === undefined || width === undefined || height === undefined) {
|
|
482
|
+
throw new DocsError('INVALID_BOX', `shape ${found.id} has no position to start from; give x, y, width and height`);
|
|
483
|
+
}
|
|
484
|
+
if (width === 0 || height === 0) {
|
|
485
|
+
throw new DocsError('INVALID_BOX', 'the shape would have no visible size');
|
|
486
|
+
}
|
|
487
|
+
const next = { x, y, width, height };
|
|
488
|
+
writeXfrm(found.el, next);
|
|
489
|
+
this.touchSlide(entry.part);
|
|
490
|
+
return { id: found.id ?? 0, ...next };
|
|
491
|
+
}
|
|
492
|
+
replacePicture(target, bytes, opts = {}) {
|
|
493
|
+
const { entry, found } = this.locate(target);
|
|
494
|
+
if (found.kind !== 'picture') {
|
|
495
|
+
throw new DocsError('NOT_PICTURE', `shape ${found.id} is a ${found.kind}, not a picture`);
|
|
496
|
+
}
|
|
497
|
+
const info = probe(bytes);
|
|
498
|
+
const blip = xml.descendants(found.el, A, 'blip')[0];
|
|
499
|
+
if (!blip) {
|
|
500
|
+
throw new DocsError('NOT_PICTURE', `shape ${found.id} has no image to replace`);
|
|
501
|
+
}
|
|
502
|
+
const { rId } = media.addImage(this.package, bytes, { source: entry.part, dir: '../media' });
|
|
503
|
+
xml.setNsAttribute(blip, { namespace: R, local: 'embed' }, rId);
|
|
504
|
+
dropSvgBlip(blip);
|
|
505
|
+
const blipFill = blip.parentNode;
|
|
506
|
+
if (xml.isElement(blipFill)) {
|
|
507
|
+
dropCrop(blipFill);
|
|
508
|
+
}
|
|
509
|
+
const current = inherit.box({ pkg: this.package, slide: entry.part, el: found.el });
|
|
510
|
+
let result = { id: found.id };
|
|
511
|
+
if (current) {
|
|
512
|
+
const next = opts.stretch ? current : fitted(info, current);
|
|
513
|
+
result = this.setBox({ slide: target.slide, shape: found.id ?? target.shape }, next);
|
|
514
|
+
}
|
|
515
|
+
this.touchSlide(entry.part);
|
|
516
|
+
return result;
|
|
517
|
+
}
|
|
518
|
+
setNotes(slide, value) {
|
|
519
|
+
const entry = slides.slideAt(this.deck, slide);
|
|
520
|
+
notes.writeNotes(this.deck, entry.part, value);
|
|
521
|
+
return { changed: true };
|
|
522
|
+
}
|
|
523
|
+
deleteSlide(slide) {
|
|
524
|
+
const entry = slides.slideAt(this.deck, slide);
|
|
525
|
+
const pkg = this.package;
|
|
526
|
+
this.refuseLinked(entry, slide);
|
|
527
|
+
const top = pkg.root(this.main);
|
|
528
|
+
for (const sld of xml.descendants(top, P, 'sld')) {
|
|
529
|
+
if (sld.getAttributeNS(R, 'id') === entry.rId) {
|
|
530
|
+
xml.remove(sld);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
for (const ref of xml.descendants(top, P14, 'sldId')) {
|
|
534
|
+
if (ref.getAttribute('id') === entry.id) {
|
|
535
|
+
xml.remove(ref);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
xml.remove(entry.el);
|
|
539
|
+
pkg.rels(this.main).remove(entry.rId);
|
|
540
|
+
pkg.remove(entry.part);
|
|
541
|
+
pkg.touch(this.main);
|
|
542
|
+
this.slidesChanged = true;
|
|
543
|
+
return { slides: this.slideCount };
|
|
544
|
+
}
|
|
545
|
+
moveSlide(slide, opts) {
|
|
546
|
+
slides.slideAt(this.deck, slide);
|
|
547
|
+
const all = slides.entries(this.deck);
|
|
548
|
+
const entry = all[slide];
|
|
549
|
+
const to = isRecord(opts) ? opts.to : undefined;
|
|
550
|
+
if (typeof to !== 'number' || !Number.isInteger(to) || to < 0 || to >= all.length) {
|
|
551
|
+
throw new DocsError('SLIDE_NOT_FOUND', `to must be a position from 0 to ${all.length - 1}, got ${String(to)}`);
|
|
552
|
+
}
|
|
553
|
+
if (slide === to) {
|
|
554
|
+
return { moved: false };
|
|
555
|
+
}
|
|
556
|
+
const list = entry.el.parentNode;
|
|
557
|
+
xml.remove(entry.el);
|
|
558
|
+
const rest = all.filter((e) => e !== entry);
|
|
559
|
+
list?.insertBefore(entry.el, rest[to]?.el ?? null);
|
|
560
|
+
slides.placeInSection(this.deck, entry.id);
|
|
561
|
+
this.package.touch(this.main);
|
|
562
|
+
this.slidesChanged = true;
|
|
563
|
+
return { moved: true };
|
|
564
|
+
}
|
|
565
|
+
save(opts) {
|
|
566
|
+
for (const part of this.touchedSlides) {
|
|
567
|
+
if (this.package.has(part)) {
|
|
568
|
+
this.package.dropUnreferenced(part, [REL.image, REL.chart, REL.hyperlink]);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
this.touchedSlides = new Set();
|
|
572
|
+
if (this.slidesChanged) {
|
|
573
|
+
properties.refreshSummary(this.package, { Slides: this.slideCount });
|
|
574
|
+
this.slidesChanged = false;
|
|
575
|
+
}
|
|
576
|
+
properties.applySaveOptions(this.package, opts);
|
|
577
|
+
return this.package.save();
|
|
578
|
+
}
|
|
579
|
+
get deck() {
|
|
580
|
+
return { pkg: this.package, main: this.main };
|
|
581
|
+
}
|
|
582
|
+
spTree(part) {
|
|
583
|
+
const tree = xml.descendants(this.package.xml(part), P, 'spTree')[0];
|
|
584
|
+
if (!tree) {
|
|
585
|
+
throw new DocsError('BROKEN_PPTX', `${part} has no shape tree`);
|
|
586
|
+
}
|
|
587
|
+
return tree;
|
|
588
|
+
}
|
|
589
|
+
slideInfo(entry, index) {
|
|
590
|
+
const doc = this.package.xml(entry.part);
|
|
591
|
+
const layout = slides.layoutOf(this.package, entry.part);
|
|
592
|
+
const info = {
|
|
593
|
+
index,
|
|
594
|
+
layout: layout ? layoutName(this.package.xml(layout)) : null,
|
|
595
|
+
shapes: shapesOf(doc).map((shape) => this.shapeInfo(entry.part, shape))
|
|
596
|
+
};
|
|
597
|
+
const noteText = notes.notesText(this.deck, entry.part);
|
|
598
|
+
if (noteText) {
|
|
599
|
+
info.notes = noteText;
|
|
600
|
+
}
|
|
601
|
+
return info;
|
|
602
|
+
}
|
|
603
|
+
shapeInfo(part, shape) {
|
|
604
|
+
const out = describe(shape);
|
|
605
|
+
const box = inherit.box({ pkg: this.package, slide: part, el: shape.el });
|
|
606
|
+
if (box) {
|
|
607
|
+
out.box = box;
|
|
608
|
+
}
|
|
609
|
+
const chartPart = shape.kind === 'chart' ? this.chartPart(part, shape.el) : null;
|
|
610
|
+
if (chartPart) {
|
|
611
|
+
out.chart = chart.readChart(this.package.xml(chartPart));
|
|
612
|
+
}
|
|
613
|
+
return out;
|
|
614
|
+
}
|
|
615
|
+
insertSlide(spec) {
|
|
616
|
+
const pkg = this.package;
|
|
617
|
+
const part = pkg.freeName('/ppt/slides/slide%d.xml');
|
|
618
|
+
pkg.add(part, spec.doc, SLIDE_TYPE);
|
|
619
|
+
pkg.rels(part).add(REL.slideLayout, spec.layout);
|
|
620
|
+
const rId = pkg.rels(this.main).add(REL.slide, part);
|
|
621
|
+
const list = slides.ensureSlideIdList(this.deck);
|
|
622
|
+
const id = Math.max(255, ...spec.ids.map(Number)) + 1;
|
|
623
|
+
if (id >= 2147483648) {
|
|
624
|
+
throw new DocsError('TOO_MANY_SLIDES', 'the deck has run out of slide ids');
|
|
625
|
+
}
|
|
626
|
+
const el = xml.builder(xml.owner(list), P)('sldId', { id });
|
|
627
|
+
xml.setNsAttribute(el, { namespace: R, local: 'id' }, rId);
|
|
628
|
+
list.insertBefore(el, spec.before);
|
|
629
|
+
slides.placeInSection(this.deck, String(id));
|
|
630
|
+
pkg.touch(this.main);
|
|
631
|
+
this.slidesChanged = true;
|
|
632
|
+
}
|
|
633
|
+
refuseLinked(entry, slide) {
|
|
634
|
+
const pkg = this.package;
|
|
635
|
+
const noteParts = pkg.related(entry.part, REL.notesSlide);
|
|
636
|
+
const linkers = pkg
|
|
637
|
+
.names()
|
|
638
|
+
.filter((name) => name !== this.main && name !== entry.part && !noteParts.includes(name))
|
|
639
|
+
.filter((name) => pkg.rels(name).targeting(entry.part).length > 0);
|
|
640
|
+
if (!linkers.length) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const all = slides.entries(this.deck);
|
|
644
|
+
const where = linkers.map((name) => {
|
|
645
|
+
const i = all.findIndex((s) => s.part === name);
|
|
646
|
+
return i === -1 ? name : `slide ${i}`;
|
|
647
|
+
});
|
|
648
|
+
throw new DocsError('SLIDE_IS_LINKED', `slide ${slide} is the target of a link on ${where.join(', ')}; remove the link first`);
|
|
649
|
+
}
|
|
650
|
+
touchSlide(part) {
|
|
651
|
+
this.touchedSlides.add(part);
|
|
652
|
+
this.package.touch(part);
|
|
653
|
+
}
|
|
654
|
+
locate(target) {
|
|
655
|
+
if (!isRecord(target)) {
|
|
656
|
+
throw new DocsError('SHAPE_NOT_FOUND', 'a shape is addressed as { slide, shape }');
|
|
657
|
+
}
|
|
658
|
+
const entry = slides.slideAt(this.deck, target.slide);
|
|
659
|
+
const shapes = shapesOf(this.package.xml(entry.part));
|
|
660
|
+
const found = findShape(shapes, target.shape);
|
|
661
|
+
if (found) {
|
|
662
|
+
return { entry, found };
|
|
663
|
+
}
|
|
664
|
+
throw shapeNotFound(target.slide, target.shape, shapes);
|
|
665
|
+
}
|
|
666
|
+
write(at, value) {
|
|
667
|
+
const before = text.readParagraphs(at.txBody);
|
|
668
|
+
if (sameParagraphs(before, withLevels(text.normalizeParagraphs(value), before))) {
|
|
669
|
+
return { paragraphs: before.length, changed: false };
|
|
670
|
+
}
|
|
671
|
+
const removed = text.losses(at.txBody);
|
|
672
|
+
const count = text.writeParagraphs(at.txBody, value);
|
|
673
|
+
this.package.touch(at.part);
|
|
674
|
+
const result = { paragraphs: count, changed: true };
|
|
675
|
+
if (Object.keys(removed).length) {
|
|
676
|
+
result.removed = removed;
|
|
677
|
+
}
|
|
678
|
+
return result;
|
|
679
|
+
}
|
|
680
|
+
estimate(part, el, txBody) {
|
|
681
|
+
const shape = { pkg: this.package, slide: part, el };
|
|
682
|
+
const box = inherit.box(shape);
|
|
683
|
+
if (!box) {
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
const paragraphs = xml.children(txBody, A, 'p');
|
|
687
|
+
const sized = text.readParagraphs(txBody).map((p, i) => {
|
|
688
|
+
const pEl = paragraphs[i];
|
|
689
|
+
const run = pEl ? xml.descendants(pEl, A, 'rPr').find((r) => r.hasAttribute('sz')) : undefined;
|
|
690
|
+
const size = run ? Number(run.getAttribute('sz')) / 100 : inherit.fontSize(shape, p.level);
|
|
691
|
+
return { text: p.text, level: p.level, size };
|
|
692
|
+
});
|
|
693
|
+
return fit.estimate(sized, box, inherit.insets(shape));
|
|
694
|
+
}
|
|
695
|
+
chartOf(target) {
|
|
696
|
+
const { entry, found } = this.locate(target);
|
|
697
|
+
if (found.kind !== 'chart') {
|
|
698
|
+
throw new DocsError('NOT_CHART', `shape ${found.id} is a ${found.kind}, not a chart`);
|
|
699
|
+
}
|
|
700
|
+
const part = this.chartPart(entry.part, found.el);
|
|
701
|
+
if (!part) {
|
|
702
|
+
throw new DocsError('BROKEN_PPTX', `chart ${found.id} points at a missing part`);
|
|
703
|
+
}
|
|
704
|
+
return part;
|
|
705
|
+
}
|
|
706
|
+
chartPart(slidePart, el) {
|
|
707
|
+
const ref = xml.descendants(el, chart.C, 'chart')[0];
|
|
708
|
+
const rel = ref ? this.package.rels(slidePart).get(ref.getAttributeNS(R, 'id')) : null;
|
|
709
|
+
return rel && !rel.external && rel.part && this.package.has(rel.part) ? rel.part : null;
|
|
710
|
+
}
|
|
711
|
+
box(opts) {
|
|
712
|
+
checkPlacement(opts);
|
|
713
|
+
const slide = this.slideSize;
|
|
714
|
+
const box = {
|
|
715
|
+
x: opts.x ?? Math.round(slide.width * 0.07),
|
|
716
|
+
y: opts.y ?? Math.round(slide.height * 0.25),
|
|
717
|
+
width: opts.width ?? Math.round(slide.width * 0.86),
|
|
718
|
+
height: opts.height ?? Math.round(slide.height * 0.65)
|
|
719
|
+
};
|
|
720
|
+
if (box.width === 0 || box.height === 0) {
|
|
721
|
+
throw new DocsError('INVALID_BOX', 'the shape would have no visible size');
|
|
722
|
+
}
|
|
723
|
+
return box;
|
|
724
|
+
}
|
|
725
|
+
pictureBox(info, opts) {
|
|
726
|
+
checkPlacement(opts);
|
|
727
|
+
const slide = this.slideSize;
|
|
728
|
+
const ratio = info.height / info.width;
|
|
729
|
+
let { width, height } = opts;
|
|
730
|
+
if (width === undefined && height === undefined) {
|
|
731
|
+
const natural = { width: info.width * EMU_PER_PIXEL, height: info.height * EMU_PER_PIXEL };
|
|
732
|
+
const scale = Math.min(1, (slide.width * 0.8) / natural.width, (slide.height * 0.8) / natural.height);
|
|
733
|
+
width = Math.round(natural.width * scale);
|
|
734
|
+
height = Math.round(natural.height * scale);
|
|
735
|
+
}
|
|
736
|
+
else if (height === undefined) {
|
|
737
|
+
height = Math.round((width ?? 0) * ratio);
|
|
738
|
+
}
|
|
739
|
+
else if (width === undefined) {
|
|
740
|
+
width = Math.round(height / ratio);
|
|
741
|
+
}
|
|
742
|
+
if (!width || !height) {
|
|
743
|
+
throw new DocsError('INVALID_BOX', 'the picture would have no visible size');
|
|
744
|
+
}
|
|
745
|
+
const x = opts.x ?? Math.max(0, Math.round((slide.width - width) / 2));
|
|
746
|
+
const y = opts.y ?? Math.max(0, Math.round((slide.height - height) / 2));
|
|
747
|
+
return { x, y, width, height };
|
|
748
|
+
}
|
|
749
|
+
}
|