pptx2js 0.4.0 → 0.4.3
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.html +45 -37
- package/README.md +54 -34
- package/lib/{utils/bounds.js → bounds.js} +27 -4
- package/lib/chart.js +104 -30
- package/lib/codegen.js +66 -7
- package/lib/color.js +275 -0
- package/lib/convert.js +12 -8
- package/lib/extractor.js +200 -153
- package/lib/mapper.js +4 -0
- package/lib/packager.js +65 -8
- package/lib/placeholder.js +161 -29
- package/lib/presentation.js +5 -5
- package/lib/rels.js +19 -6
- package/lib/run-utils.js +181 -0
- package/lib/smartart.js +7 -15
- package/lib/table.js +33 -28
- package/lib/text-utils.js +235 -0
- package/lib/xml-parser.js +191 -15
- package/lib/xml-utils.js +82 -36
- package/package.json +4 -4
- package/lib/utils/color.js +0 -128
- package/lib/utils/emu.js +0 -20
package/lib/extractor.js
CHANGED
|
@@ -2,19 +2,22 @@
|
|
|
2
2
|
* ③ 实体提取器
|
|
3
3
|
*/
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const {
|
|
6
|
-
const { boundsFromXfrm } = require('./
|
|
5
|
+
const { attr, child, childNodes, children, documentRoot } = require('./xml-utils');
|
|
6
|
+
const { boundsFromXfrm } = require('./bounds');
|
|
7
7
|
const {
|
|
8
8
|
loadColorScheme,
|
|
9
9
|
resolveFillColor,
|
|
10
10
|
resolveColorFromContainer,
|
|
11
|
-
} = require('./
|
|
11
|
+
} = require('./color');
|
|
12
|
+
const { extractTextRuns } = require('./text-utils');
|
|
12
13
|
const { getSlidePaths, getThemePath } = require('./presentation');
|
|
13
14
|
const {
|
|
14
15
|
buildSlideInheritance,
|
|
15
16
|
getEffectiveXfrm,
|
|
17
|
+
getPlaceholderKey,
|
|
16
18
|
mergeTxBody,
|
|
17
19
|
resolvePlaceholderSps,
|
|
20
|
+
shouldShowMasterShapes,
|
|
18
21
|
} = require('./placeholder');
|
|
19
22
|
const { isTableFrame, isChartFrame, isSmartArtFrame } = require('./graphic');
|
|
20
23
|
const { extractTable } = require('./table');
|
|
@@ -29,7 +32,7 @@ const { extractSmartArt } = require('./smartart');
|
|
|
29
32
|
* @property {string} slidePath
|
|
30
33
|
* @property {ConversionDecision} decision
|
|
31
34
|
* @property {string} kind
|
|
32
|
-
* @property {import('./
|
|
35
|
+
* @property {import('./bounds').Bounds} bounds
|
|
33
36
|
* @property {object} [text]
|
|
34
37
|
* @property {object} [image]
|
|
35
38
|
* @property {object} [shape]
|
|
@@ -50,8 +53,17 @@ const PRST_TO_SHAPE = {
|
|
|
50
53
|
pentagon: 'PENTAGON',
|
|
51
54
|
hexagon: 'HEXAGON',
|
|
52
55
|
star5: 'STAR5',
|
|
56
|
+
leftBrace: 'LEFT_BRACE',
|
|
57
|
+
rightBracket: 'RIGHT_BRACKET',
|
|
58
|
+
rightArrow: 'RIGHT_ARROW',
|
|
59
|
+
downArrow: 'DOWN_ARROW',
|
|
60
|
+
curvedLeftArrow: 'LEFT_ARROW',
|
|
61
|
+
flowChartAlternateProcess: 'ROUNDED_RECTANGLE',
|
|
53
62
|
};
|
|
54
63
|
|
|
64
|
+
/** prst → 退化为 LINE 并附 degradeReason */
|
|
65
|
+
const PRST_DEGRADE_LINE = new Set(['bentConnector3']);
|
|
66
|
+
|
|
55
67
|
/**
|
|
56
68
|
* @typedef {object} ExtractContext
|
|
57
69
|
* @property {Record<string, object>} parsed
|
|
@@ -82,7 +94,7 @@ function extractEntities(ctx) {
|
|
|
82
94
|
*/
|
|
83
95
|
function extractSlide(slidePath, slideIndex, ctx, scheme) {
|
|
84
96
|
const doc = ctx.parsed[slidePath];
|
|
85
|
-
const slide =
|
|
97
|
+
const slide = documentRoot(doc, 'p:sld');
|
|
86
98
|
const cSld = child(slide, 'p:cSld');
|
|
87
99
|
const spTree = child(cSld, 'p:spTree');
|
|
88
100
|
if (!spTree) return [];
|
|
@@ -114,57 +126,91 @@ function extractSlide(slidePath, slideIndex, ctx, scheme) {
|
|
|
114
126
|
inheritance,
|
|
115
127
|
offset: { x: 0, y: 0 },
|
|
116
128
|
entities,
|
|
129
|
+
decorLayer: false,
|
|
117
130
|
};
|
|
118
131
|
|
|
132
|
+
if (
|
|
133
|
+
shouldShowMasterShapes(slide) &&
|
|
134
|
+
inheritance.masterSpTree &&
|
|
135
|
+
inheritance.masterPath
|
|
136
|
+
) {
|
|
137
|
+
walkSpTree(inheritance.masterSpTree, {
|
|
138
|
+
...walkCtx,
|
|
139
|
+
slidePath: inheritance.masterPath,
|
|
140
|
+
decorLayer: true,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (inheritance.layoutSpTree && inheritance.layoutPath) {
|
|
144
|
+
walkSpTree(inheritance.layoutSpTree, {
|
|
145
|
+
...walkCtx,
|
|
146
|
+
slidePath: inheritance.layoutPath,
|
|
147
|
+
decorLayer: true,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
119
150
|
walkSpTree(spTree, walkCtx);
|
|
120
151
|
return entities;
|
|
121
152
|
}
|
|
122
153
|
|
|
123
154
|
function extractSlideBackground(cSld, scheme) {
|
|
124
|
-
const
|
|
125
|
-
if (!
|
|
126
|
-
const { color, degraded } = resolveFillColor(bgPr, scheme);
|
|
127
|
-
if (!color) return null;
|
|
128
|
-
return { color, degraded };
|
|
129
|
-
}
|
|
155
|
+
const bg = child(cSld, 'p:bg');
|
|
156
|
+
if (!bg) return null;
|
|
130
157
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
158
|
+
const bgPr = child(bg, 'p:bgPr');
|
|
159
|
+
if (bgPr) {
|
|
160
|
+
const { color, degraded } = resolveFillColor(bgPr, scheme);
|
|
161
|
+
if (color) return { color, degraded };
|
|
134
162
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
163
|
+
|
|
164
|
+
const bgRef = child(bg, 'p:bgRef');
|
|
165
|
+
if (bgRef) {
|
|
166
|
+
const color = resolveColorFromContainer(bgRef, scheme);
|
|
167
|
+
if (color) return { color, degraded: false };
|
|
140
168
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
169
|
+
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function walkSpTree(spTree, ctx) {
|
|
174
|
+
for (const node of childNodes(spTree)) {
|
|
175
|
+
if (node.tag === 'p:sp') {
|
|
176
|
+
if (ctx.decorLayer && getPlaceholderKey(node)) continue;
|
|
177
|
+
extractShape(node, ctx);
|
|
178
|
+
} else if (node.tag === 'p:pic') {
|
|
179
|
+
extractPicture(node, ctx);
|
|
180
|
+
} else if (node.tag === 'p:grpSp') {
|
|
181
|
+
flattenGroup(node, ctx);
|
|
182
|
+
} else if (node.tag === 'p:graphicFrame') {
|
|
183
|
+
if (ctx.decorLayer) continue;
|
|
184
|
+
if (isTableFrame(node)) {
|
|
185
|
+
const entity = extractTable(node, ctx);
|
|
186
|
+
if (entity) ctx.entities.push(entity);
|
|
187
|
+
} else if (isChartFrame(node)) {
|
|
188
|
+
const entity = extractChart(node, ctx);
|
|
189
|
+
if (entity) ctx.entities.push(entity);
|
|
190
|
+
} else if (isSmartArtFrame(node)) {
|
|
191
|
+
const entity = extractSmartArt(node, ctx);
|
|
192
|
+
if (entity) ctx.entities.push(entity);
|
|
193
|
+
} else {
|
|
194
|
+
skipElement(node, ctx, '不支持的 graphicFrame 类型');
|
|
195
|
+
}
|
|
196
|
+
} else if (node.tag === 'p:cxnSp') {
|
|
197
|
+
extractConnector(node, ctx);
|
|
153
198
|
}
|
|
154
199
|
}
|
|
155
|
-
for (const cxn of asArray(spTree['p:cxnSp'])) {
|
|
156
|
-
extractConnector(cxn, ctx);
|
|
157
|
-
}
|
|
158
200
|
}
|
|
159
201
|
|
|
160
202
|
function flattenGroup(grp, ctx) {
|
|
161
203
|
const grpXfrm = child(child(grp, 'p:grpSpPr'), 'a:xfrm');
|
|
162
204
|
const off = readOffset(grpXfrm);
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
205
|
+
const chOff = readChOff(grpXfrm);
|
|
206
|
+
|
|
207
|
+
// p:grpSp 子节点直接挂在 grp 上,没有 p:spTree 包装
|
|
208
|
+
walkSpTree(grp, {
|
|
166
209
|
...ctx,
|
|
167
|
-
offset: {
|
|
210
|
+
offset: {
|
|
211
|
+
x: ctx.offset.x + off.x - chOff.x,
|
|
212
|
+
y: ctx.offset.y + off.y - chOff.y,
|
|
213
|
+
},
|
|
168
214
|
});
|
|
169
215
|
}
|
|
170
216
|
|
|
@@ -181,32 +227,46 @@ function extractShape(sp, ctx) {
|
|
|
181
227
|
relIndex: ctx.relIndex,
|
|
182
228
|
slidePath: ctx.slidePath,
|
|
183
229
|
});
|
|
184
|
-
if (runs.length
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
230
|
+
if (runs.length > 0) {
|
|
231
|
+
const hasGradient = runs.some((r) => r.degraded);
|
|
232
|
+
ctx.entities.push({
|
|
233
|
+
slideIndex: ctx.slideIndex,
|
|
234
|
+
slidePath: ctx.slidePath,
|
|
235
|
+
decision: hasGradient ? 'DEGRADE' : 'FULL',
|
|
236
|
+
kind: 'text',
|
|
237
|
+
bounds,
|
|
238
|
+
text: { runs: runs.map(({ text, options }) => ({ text, options })) },
|
|
239
|
+
degradeReason: hasGradient ? '文本渐变退化为纯色' : undefined,
|
|
240
|
+
});
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
// 无 a:r 文本(如 a:fld 页码)或空 txBody → 若有几何则继续按形状提取
|
|
197
244
|
}
|
|
198
245
|
|
|
199
246
|
const spPr = child(sp, 'p:spPr');
|
|
200
247
|
const prst = attr(child(spPr, 'a:prstGeom'), 'prst');
|
|
201
248
|
if (!prst) return;
|
|
202
249
|
|
|
203
|
-
|
|
250
|
+
let shapeName = PRST_TO_SHAPE[prst];
|
|
251
|
+
let prstDegradeReason;
|
|
252
|
+
if (PRST_DEGRADE_LINE.has(prst)) {
|
|
253
|
+
shapeName = 'LINE';
|
|
254
|
+
prstDegradeReason = `连接器 "${prst}" 退化为直线`;
|
|
255
|
+
} else if (prst === 'curvedLeftArrow') {
|
|
256
|
+
prstDegradeReason = 'curvedLeftArrow 退化为 LEFT_ARROW';
|
|
257
|
+
} else if (prst === 'flowChartAlternateProcess') {
|
|
258
|
+
prstDegradeReason = 'flowChartAlternateProcess 退化为圆角矩形';
|
|
259
|
+
}
|
|
260
|
+
|
|
204
261
|
const { color: fillColor, degraded: fillDegraded } = resolveFillColor(
|
|
205
262
|
spPr,
|
|
206
263
|
ctx.scheme
|
|
207
264
|
);
|
|
208
265
|
const ln = child(spPr, 'a:ln');
|
|
209
266
|
const lineColor = ln ? resolveFillColor(ln, ctx.scheme).color : null;
|
|
267
|
+
const lineWidth = extractLineWidthPt(ln);
|
|
268
|
+
const lineDash = extractLineDash(ln);
|
|
269
|
+
const flip = readFlip(xfrm);
|
|
210
270
|
|
|
211
271
|
if (!shapeName) {
|
|
212
272
|
ctx.entities.push({
|
|
@@ -215,23 +275,87 @@ function extractShape(sp, ctx) {
|
|
|
215
275
|
decision: 'DEGRADE',
|
|
216
276
|
kind: 'shape',
|
|
217
277
|
bounds,
|
|
218
|
-
shape: {
|
|
278
|
+
shape: {
|
|
279
|
+
type: 'RECTANGLE',
|
|
280
|
+
fill: fillColor,
|
|
281
|
+
line: lineColor,
|
|
282
|
+
lineWidth,
|
|
283
|
+
lineDash,
|
|
284
|
+
...flip,
|
|
285
|
+
},
|
|
219
286
|
degradeReason: `未知预设形状 "${prst}",退化为矩形`,
|
|
220
287
|
});
|
|
221
288
|
return;
|
|
222
289
|
}
|
|
223
290
|
|
|
291
|
+
const degradeReason =
|
|
292
|
+
prstDegradeReason ||
|
|
293
|
+
(fillDegraded ? '渐变/图案填充退化为纯色' : undefined);
|
|
294
|
+
|
|
224
295
|
ctx.entities.push({
|
|
225
296
|
slideIndex: ctx.slideIndex,
|
|
226
297
|
slidePath: ctx.slidePath,
|
|
227
|
-
decision: fillDegraded ? 'DEGRADE' : 'FULL',
|
|
298
|
+
decision: (prstDegradeReason || fillDegraded) ? 'DEGRADE' : 'FULL',
|
|
228
299
|
kind: 'shape',
|
|
229
300
|
bounds,
|
|
230
|
-
shape: {
|
|
231
|
-
|
|
301
|
+
shape: {
|
|
302
|
+
type: shapeName,
|
|
303
|
+
fill: fillColor,
|
|
304
|
+
line: lineColor,
|
|
305
|
+
lineWidth,
|
|
306
|
+
lineDash,
|
|
307
|
+
...flip,
|
|
308
|
+
},
|
|
309
|
+
degradeReason,
|
|
232
310
|
});
|
|
233
311
|
}
|
|
234
312
|
|
|
313
|
+
/**
|
|
314
|
+
* @param {object|null|undefined} xfrm
|
|
315
|
+
*/
|
|
316
|
+
function readFlip(xfrm) {
|
|
317
|
+
const flip = {};
|
|
318
|
+
if (attr(xfrm, 'flipH') === '1') flip.flipH = true;
|
|
319
|
+
if (attr(xfrm, 'flipV') === '1') flip.flipV = true;
|
|
320
|
+
return flip;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* @param {object|null|undefined} ln a:ln
|
|
325
|
+
* @returns {number|undefined}
|
|
326
|
+
*/
|
|
327
|
+
function extractLineWidthPt(ln) {
|
|
328
|
+
if (!ln) return undefined;
|
|
329
|
+
const w = parseInt(attr(ln, 'w') ?? '0', 10);
|
|
330
|
+
if (!w) return undefined;
|
|
331
|
+
return Math.max(1, Math.round(w / 12700));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** OOXML prstDash → PptxGenJS line.dashType */
|
|
335
|
+
const PRST_DASH_MAP = {
|
|
336
|
+
dash: 'dash',
|
|
337
|
+
dot: 'dot',
|
|
338
|
+
dashDot: 'dashDot',
|
|
339
|
+
lgDash: 'lgDash',
|
|
340
|
+
lgDashDot: 'lgDashDot',
|
|
341
|
+
lgDashDotDot: 'lgDashDotDot',
|
|
342
|
+
sysDash: 'sysDash',
|
|
343
|
+
sysDot: 'sysDot',
|
|
344
|
+
sysDashDot: 'sysDashDot',
|
|
345
|
+
sysDashDotDot: 'sysDashDotDot',
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* @param {object|null|undefined} ln a:ln
|
|
350
|
+
* @returns {string|undefined}
|
|
351
|
+
*/
|
|
352
|
+
function extractLineDash(ln) {
|
|
353
|
+
if (!ln) return undefined;
|
|
354
|
+
const prstDash = child(ln, 'a:prstDash');
|
|
355
|
+
const val = attr(prstDash, 'val');
|
|
356
|
+
return val ? PRST_DASH_MAP[val] : undefined;
|
|
357
|
+
}
|
|
358
|
+
|
|
235
359
|
function extractPicture(pic, ctx) {
|
|
236
360
|
const spPr = child(pic, 'p:spPr');
|
|
237
361
|
const bounds = boundsFromXfrm(child(spPr, 'a:xfrm'), ctx.offset);
|
|
@@ -258,8 +382,10 @@ function extractPicture(pic, ctx) {
|
|
|
258
382
|
|
|
259
383
|
function extractConnector(cxn, ctx) {
|
|
260
384
|
const spPr = child(cxn, 'p:spPr');
|
|
261
|
-
const
|
|
262
|
-
const
|
|
385
|
+
const xfrm = child(spPr, 'a:xfrm');
|
|
386
|
+
const bounds = boundsFromXfrm(xfrm, ctx.offset);
|
|
387
|
+
const ln = child(spPr, 'a:ln');
|
|
388
|
+
const { color: lineColor } = resolveFillColor(ln, ctx.scheme);
|
|
263
389
|
|
|
264
390
|
ctx.entities.push({
|
|
265
391
|
slideIndex: ctx.slideIndex,
|
|
@@ -267,7 +393,14 @@ function extractConnector(cxn, ctx) {
|
|
|
267
393
|
decision: 'FULL',
|
|
268
394
|
kind: 'shape',
|
|
269
395
|
bounds,
|
|
270
|
-
shape: {
|
|
396
|
+
shape: {
|
|
397
|
+
type: 'LINE',
|
|
398
|
+
fill: null,
|
|
399
|
+
line: lineColor,
|
|
400
|
+
lineWidth: extractLineWidthPt(ln),
|
|
401
|
+
lineDash: extractLineDash(ln),
|
|
402
|
+
...readFlip(xfrm),
|
|
403
|
+
},
|
|
271
404
|
});
|
|
272
405
|
}
|
|
273
406
|
|
|
@@ -290,105 +423,19 @@ function readOffset(xfrm) {
|
|
|
290
423
|
};
|
|
291
424
|
}
|
|
292
425
|
|
|
293
|
-
const PARA_ALIGN_MAP = {
|
|
294
|
-
l: 'left',
|
|
295
|
-
ctr: 'center',
|
|
296
|
-
r: 'right',
|
|
297
|
-
just: 'justify',
|
|
298
|
-
dist: 'justify',
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
function extractTextRuns(txBody, scheme, linkCtx) {
|
|
302
|
-
const runs = [];
|
|
303
|
-
for (const p of asArray(txBody['a:p'])) {
|
|
304
|
-
const pPr = child(p, 'a:pPr');
|
|
305
|
-
const bullet = extractBullet(pPr);
|
|
306
|
-
const paraOpts = extractParaOptions(pPr);
|
|
307
|
-
|
|
308
|
-
for (const r of asArray(p['a:r'])) {
|
|
309
|
-
const text = textContent(r['a:t']);
|
|
310
|
-
if (!text) continue;
|
|
311
|
-
const options = extractRunOptions(r['a:rPr'], scheme, linkCtx);
|
|
312
|
-
Object.assign(options, paraOpts);
|
|
313
|
-
if (bullet) Object.assign(options, bullet);
|
|
314
|
-
runs.push({ text, options, degraded: options._degraded });
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
return runs;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
426
|
/**
|
|
321
|
-
*
|
|
427
|
+
* 组合内部坐标系原点(EMU)
|
|
428
|
+
* @param {object|null|undefined} xfrm
|
|
322
429
|
*/
|
|
323
|
-
function
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const lvl = attr(pPr, 'lvl');
|
|
331
|
-
if (lvl) opts.indentLevel = parseInt(lvl, 10);
|
|
332
|
-
|
|
333
|
-
// 注:a:spcPct(百分比段前/段后距)暂不处理
|
|
334
|
-
const spcBef = attr(child(child(pPr, 'a:spcBef'), 'a:spcPts'), 'val');
|
|
335
|
-
if (spcBef) opts.paraSpaceBefore = parseInt(spcBef, 10) / 100;
|
|
336
|
-
|
|
337
|
-
// 注:a:spcPct(百分比段后距)暂不处理
|
|
338
|
-
const spcAft = attr(child(child(pPr, 'a:spcAft'), 'a:spcPts'), 'val');
|
|
339
|
-
if (spcAft) opts.paraSpaceAfter = parseInt(spcAft, 10) / 100;
|
|
340
|
-
|
|
341
|
-
// 注:a:spcPct(百分比行距)暂不处理
|
|
342
|
-
const lnSpc = attr(child(child(pPr, 'a:lnSpc'), 'a:spcPts'), 'val');
|
|
343
|
-
if (lnSpc) opts.lineSpacing = parseInt(lnSpc, 10) / 100;
|
|
344
|
-
|
|
345
|
-
return opts;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function extractBullet(pPr) {
|
|
349
|
-
if (!pPr) return null;
|
|
350
|
-
if (child(pPr, 'a:buChar')) return { bullet: true };
|
|
351
|
-
if (child(pPr, 'a:buAutoNum')) return { bullet: { type: 'number' } };
|
|
352
|
-
return null;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function extractRunOptions(rPr, scheme, linkCtx) {
|
|
356
|
-
const options = {};
|
|
357
|
-
let degraded = false;
|
|
358
|
-
if (!rPr) return options;
|
|
359
|
-
|
|
360
|
-
const sz = attr(rPr, 'sz');
|
|
361
|
-
if (sz) options.fontSize = parseInt(sz, 10) / 100;
|
|
362
|
-
if (attr(rPr, 'b') === '1') options.bold = true;
|
|
363
|
-
if (attr(rPr, 'i') === '1') options.italic = true;
|
|
364
|
-
|
|
365
|
-
const face = attr(child(rPr, 'a:latin'), 'typeface');
|
|
366
|
-
if (face) options.fontFace = face;
|
|
367
|
-
|
|
368
|
-
const solid = child(rPr, 'a:solidFill');
|
|
369
|
-
const grad = child(rPr, 'a:gradFill');
|
|
370
|
-
if (grad) {
|
|
371
|
-
degraded = true;
|
|
372
|
-
const g = resolveFillColor({ 'a:gradFill': grad }, scheme);
|
|
373
|
-
if (g.color) options.color = g.color;
|
|
374
|
-
} else if (solid) {
|
|
375
|
-
const c = resolveColorFromContainer(solid, scheme);
|
|
376
|
-
if (c) options.color = c;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const hlink = child(rPr, 'a:hlinkClick');
|
|
380
|
-
if (hlink && linkCtx) {
|
|
381
|
-
const relId = attr(hlink, 'r:id');
|
|
382
|
-
const url = relId && linkCtx.relIndex.resolve(linkCtx.slidePath, relId);
|
|
383
|
-
if (url) options.hyperlink = { url };
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (degraded) options._degraded = true;
|
|
387
|
-
return options;
|
|
430
|
+
function readChOff(xfrm) {
|
|
431
|
+
const chOff = child(xfrm, 'a:chOff');
|
|
432
|
+
return {
|
|
433
|
+
x: parseInt(attr(chOff, 'x') ?? '0', 10),
|
|
434
|
+
y: parseInt(attr(chOff, 'y') ?? '0', 10),
|
|
435
|
+
};
|
|
388
436
|
}
|
|
389
437
|
|
|
390
438
|
module.exports = {
|
|
391
439
|
extractEntities,
|
|
392
440
|
PRST_TO_SHAPE,
|
|
393
|
-
boundsFromXfrm,
|
|
394
441
|
};
|
package/lib/mapper.js
CHANGED
|
@@ -84,6 +84,10 @@ function mapToIR(entitiesBySlide, ctx) {
|
|
|
84
84
|
shape: entity.shape.type,
|
|
85
85
|
fill: entity.shape.fill,
|
|
86
86
|
line: entity.shape.line,
|
|
87
|
+
lineWidth: entity.shape.lineWidth,
|
|
88
|
+
lineDash: entity.shape.lineDash,
|
|
89
|
+
flipH: entity.shape.flipH,
|
|
90
|
+
flipV: entity.shape.flipV,
|
|
87
91
|
degradeReason: entity.degradeReason,
|
|
88
92
|
});
|
|
89
93
|
}
|
package/lib/packager.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ⑥ 资源打包器
|
|
3
3
|
* 复制 media/ 文件,写入退化生成图片。
|
|
4
|
-
* TODO: 重名冲突处理(不同 zip 路径、同名文件时避免覆盖)
|
|
5
4
|
*/
|
|
6
5
|
const fs = require('fs');
|
|
7
6
|
const path = require('path');
|
|
@@ -12,30 +11,82 @@ const path = require('path');
|
|
|
12
11
|
* @property {import('./mapper').IntermediateRepresentation} ir
|
|
13
12
|
*/
|
|
14
13
|
|
|
14
|
+
/**
|
|
15
|
+
* 为 zip 内媒体路径生成不冲突的输出文件名
|
|
16
|
+
* @param {string} entryPath 如 ppt/media/image1.png
|
|
17
|
+
* @param {Set<string>} usedDestNames
|
|
18
|
+
*/
|
|
19
|
+
function uniqueMediaFileName(entryPath, usedDestNames) {
|
|
20
|
+
const base = path.posix.basename(entryPath);
|
|
21
|
+
if (!usedDestNames.has(base)) {
|
|
22
|
+
usedDestNames.add(base);
|
|
23
|
+
return base;
|
|
24
|
+
}
|
|
25
|
+
const ext = path.posix.extname(base);
|
|
26
|
+
const stem = ext ? base.slice(0, -ext.length) : base;
|
|
27
|
+
let n = 2;
|
|
28
|
+
let candidate;
|
|
29
|
+
do {
|
|
30
|
+
candidate = `${stem}_${n}${ext}`;
|
|
31
|
+
n += 1;
|
|
32
|
+
} while (usedDestNames.has(candidate));
|
|
33
|
+
usedDestNames.add(candidate);
|
|
34
|
+
return candidate;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 将 IR 中图片元素的 mediaPath 对齐到实际写入的文件名
|
|
39
|
+
* @param {import('./mapper').IntermediateRepresentation} ir
|
|
40
|
+
* @param {Map<string, string>} zipToRel zip 路径 → media/xxx.png
|
|
41
|
+
*/
|
|
42
|
+
function applyMediaPathsToIR(ir, zipToRel) {
|
|
43
|
+
if (!ir?.slides) return;
|
|
44
|
+
for (const slide of ir.slides) {
|
|
45
|
+
for (const el of slide.elements) {
|
|
46
|
+
if (el.type === 'image' && el.zipPath && zipToRel.has(el.zipPath)) {
|
|
47
|
+
el.mediaPath = zipToRel.get(el.zipPath);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
15
53
|
/**
|
|
16
54
|
* @param {PackagerInput} input
|
|
17
55
|
* @param {string} outputDir
|
|
18
56
|
* @param {{ noMedia?: boolean }} options
|
|
19
|
-
* @returns {Promise<string[]>}
|
|
57
|
+
* @returns {Promise<{ written: string[], zipToRel: Map<string, string> }>}
|
|
20
58
|
*/
|
|
21
59
|
async function packageMedia(input, outputDir, options = {}) {
|
|
22
|
-
|
|
60
|
+
/** @type {Map<string, string>} */
|
|
61
|
+
const zipToRel = new Map();
|
|
62
|
+
|
|
63
|
+
if (options.noMedia) {
|
|
64
|
+
return { written: [], zipToRel };
|
|
65
|
+
}
|
|
23
66
|
|
|
24
67
|
const mediaDir = path.join(outputDir, 'media');
|
|
25
68
|
fs.mkdirSync(mediaDir, { recursive: true });
|
|
26
69
|
|
|
27
70
|
const written = [];
|
|
71
|
+
const usedDestNames = new Set();
|
|
28
72
|
const { zip } = input.archive;
|
|
73
|
+
|
|
29
74
|
for (const [entryPath, entry] of Object.entries(zip.files)) {
|
|
30
75
|
if (entry.dir) continue;
|
|
31
76
|
if (!entryPath.startsWith('ppt/media/')) continue;
|
|
32
|
-
|
|
33
|
-
const
|
|
77
|
+
|
|
78
|
+
const destName = uniqueMediaFileName(entryPath, usedDestNames);
|
|
79
|
+
const relPath = path.posix.join('media', destName);
|
|
80
|
+
zipToRel.set(entryPath, relPath);
|
|
81
|
+
|
|
82
|
+
const dest = path.join(outputDir, relPath);
|
|
34
83
|
const data = await entry.async('nodebuffer');
|
|
35
84
|
fs.writeFileSync(dest, data);
|
|
36
|
-
written.push(
|
|
85
|
+
written.push(relPath);
|
|
37
86
|
}
|
|
38
|
-
|
|
87
|
+
|
|
88
|
+
applyMediaPathsToIR(input.ir, zipToRel);
|
|
89
|
+
return { written, zipToRel };
|
|
39
90
|
}
|
|
40
91
|
|
|
41
92
|
/**
|
|
@@ -69,4 +120,10 @@ function writeOutputReadme(outputDir, meta) {
|
|
|
69
120
|
fs.writeFileSync(readmePath, content, 'utf8');
|
|
70
121
|
}
|
|
71
122
|
|
|
72
|
-
module.exports = {
|
|
123
|
+
module.exports = {
|
|
124
|
+
packageMedia,
|
|
125
|
+
applyMediaPathsToIR,
|
|
126
|
+
uniqueMediaFileName,
|
|
127
|
+
writeConversionLog,
|
|
128
|
+
writeOutputReadme,
|
|
129
|
+
};
|