pptx2js 0.4.1 → 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 +54 -18
- package/lib/codegen.js +5 -2
- package/lib/color.js +275 -0
- package/lib/convert.js +12 -8
- package/lib/extractor.js +188 -195
- package/lib/mapper.js +3 -0
- package/lib/packager.js +65 -8
- package/lib/placeholder.js +161 -29
- package/lib/presentation.js +5 -5
- package/lib/rels.js +4 -4
- package/lib/run-utils.js +3 -1
- package/lib/smartart.js +7 -15
- package/lib/table.js +7 -8
- package/lib/text-utils.js +235 -0
- package/lib/xml-parser.js +191 -15
- package/lib/xml-utils.js +82 -36
- package/package.json +2 -3
- package/lib/utils/color.js +0 -128
- package/lib/utils/emu.js +0 -20
package/lib/extractor.js
CHANGED
|
@@ -2,20 +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('./
|
|
12
|
-
const {
|
|
11
|
+
} = require('./color');
|
|
12
|
+
const { extractTextRuns } = require('./text-utils');
|
|
13
13
|
const { getSlidePaths, getThemePath } = require('./presentation');
|
|
14
14
|
const {
|
|
15
15
|
buildSlideInheritance,
|
|
16
16
|
getEffectiveXfrm,
|
|
17
|
+
getPlaceholderKey,
|
|
17
18
|
mergeTxBody,
|
|
18
19
|
resolvePlaceholderSps,
|
|
20
|
+
shouldShowMasterShapes,
|
|
19
21
|
} = require('./placeholder');
|
|
20
22
|
const { isTableFrame, isChartFrame, isSmartArtFrame } = require('./graphic');
|
|
21
23
|
const { extractTable } = require('./table');
|
|
@@ -30,7 +32,7 @@ const { extractSmartArt } = require('./smartart');
|
|
|
30
32
|
* @property {string} slidePath
|
|
31
33
|
* @property {ConversionDecision} decision
|
|
32
34
|
* @property {string} kind
|
|
33
|
-
* @property {import('./
|
|
35
|
+
* @property {import('./bounds').Bounds} bounds
|
|
34
36
|
* @property {object} [text]
|
|
35
37
|
* @property {object} [image]
|
|
36
38
|
* @property {object} [shape]
|
|
@@ -51,8 +53,17 @@ const PRST_TO_SHAPE = {
|
|
|
51
53
|
pentagon: 'PENTAGON',
|
|
52
54
|
hexagon: 'HEXAGON',
|
|
53
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',
|
|
54
62
|
};
|
|
55
63
|
|
|
64
|
+
/** prst → 退化为 LINE 并附 degradeReason */
|
|
65
|
+
const PRST_DEGRADE_LINE = new Set(['bentConnector3']);
|
|
66
|
+
|
|
56
67
|
/**
|
|
57
68
|
* @typedef {object} ExtractContext
|
|
58
69
|
* @property {Record<string, object>} parsed
|
|
@@ -83,7 +94,7 @@ function extractEntities(ctx) {
|
|
|
83
94
|
*/
|
|
84
95
|
function extractSlide(slidePath, slideIndex, ctx, scheme) {
|
|
85
96
|
const doc = ctx.parsed[slidePath];
|
|
86
|
-
const slide =
|
|
97
|
+
const slide = documentRoot(doc, 'p:sld');
|
|
87
98
|
const cSld = child(slide, 'p:cSld');
|
|
88
99
|
const spTree = child(cSld, 'p:spTree');
|
|
89
100
|
if (!spTree) return [];
|
|
@@ -115,57 +126,91 @@ function extractSlide(slidePath, slideIndex, ctx, scheme) {
|
|
|
115
126
|
inheritance,
|
|
116
127
|
offset: { x: 0, y: 0 },
|
|
117
128
|
entities,
|
|
129
|
+
decorLayer: false,
|
|
118
130
|
};
|
|
119
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
|
+
}
|
|
120
150
|
walkSpTree(spTree, walkCtx);
|
|
121
151
|
return entities;
|
|
122
152
|
}
|
|
123
153
|
|
|
124
154
|
function extractSlideBackground(cSld, scheme) {
|
|
125
|
-
const
|
|
126
|
-
if (!
|
|
127
|
-
const { color, degraded } = resolveFillColor(bgPr, scheme);
|
|
128
|
-
if (!color) return null;
|
|
129
|
-
return { color, degraded };
|
|
130
|
-
}
|
|
155
|
+
const bg = child(cSld, 'p:bg');
|
|
156
|
+
if (!bg) return null;
|
|
131
157
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
for (const pic of asArray(spTree['p:pic'])) {
|
|
137
|
-
extractPicture(pic, ctx);
|
|
158
|
+
const bgPr = child(bg, 'p:bgPr');
|
|
159
|
+
if (bgPr) {
|
|
160
|
+
const { color, degraded } = resolveFillColor(bgPr, scheme);
|
|
161
|
+
if (color) return { color, degraded };
|
|
138
162
|
}
|
|
139
|
-
|
|
140
|
-
|
|
163
|
+
|
|
164
|
+
const bgRef = child(bg, 'p:bgRef');
|
|
165
|
+
if (bgRef) {
|
|
166
|
+
const color = resolveColorFromContainer(bgRef, scheme);
|
|
167
|
+
if (color) return { color, degraded: false };
|
|
141
168
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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);
|
|
154
198
|
}
|
|
155
199
|
}
|
|
156
|
-
for (const cxn of asArray(spTree['p:cxnSp'])) {
|
|
157
|
-
extractConnector(cxn, ctx);
|
|
158
|
-
}
|
|
159
200
|
}
|
|
160
201
|
|
|
161
202
|
function flattenGroup(grp, ctx) {
|
|
162
203
|
const grpXfrm = child(child(grp, 'p:grpSpPr'), 'a:xfrm');
|
|
163
204
|
const off = readOffset(grpXfrm);
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
205
|
+
const chOff = readChOff(grpXfrm);
|
|
206
|
+
|
|
207
|
+
// p:grpSp 子节点直接挂在 grp 上,没有 p:spTree 包装
|
|
208
|
+
walkSpTree(grp, {
|
|
167
209
|
...ctx,
|
|
168
|
-
offset: {
|
|
210
|
+
offset: {
|
|
211
|
+
x: ctx.offset.x + off.x - chOff.x,
|
|
212
|
+
y: ctx.offset.y + off.y - chOff.y,
|
|
213
|
+
},
|
|
169
214
|
});
|
|
170
215
|
}
|
|
171
216
|
|
|
@@ -182,26 +227,37 @@ function extractShape(sp, ctx) {
|
|
|
182
227
|
relIndex: ctx.relIndex,
|
|
183
228
|
slidePath: ctx.slidePath,
|
|
184
229
|
});
|
|
185
|
-
if (runs.length
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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 → 若有几何则继续按形状提取
|
|
198
244
|
}
|
|
199
245
|
|
|
200
246
|
const spPr = child(sp, 'p:spPr');
|
|
201
247
|
const prst = attr(child(spPr, 'a:prstGeom'), 'prst');
|
|
202
248
|
if (!prst) return;
|
|
203
249
|
|
|
204
|
-
|
|
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
|
+
|
|
205
261
|
const { color: fillColor, degraded: fillDegraded } = resolveFillColor(
|
|
206
262
|
spPr,
|
|
207
263
|
ctx.scheme
|
|
@@ -209,6 +265,8 @@ function extractShape(sp, ctx) {
|
|
|
209
265
|
const ln = child(spPr, 'a:ln');
|
|
210
266
|
const lineColor = ln ? resolveFillColor(ln, ctx.scheme).color : null;
|
|
211
267
|
const lineWidth = extractLineWidthPt(ln);
|
|
268
|
+
const lineDash = extractLineDash(ln);
|
|
269
|
+
const flip = readFlip(xfrm);
|
|
212
270
|
|
|
213
271
|
if (!shapeName) {
|
|
214
272
|
ctx.entities.push({
|
|
@@ -217,23 +275,51 @@ function extractShape(sp, ctx) {
|
|
|
217
275
|
decision: 'DEGRADE',
|
|
218
276
|
kind: 'shape',
|
|
219
277
|
bounds,
|
|
220
|
-
shape: {
|
|
278
|
+
shape: {
|
|
279
|
+
type: 'RECTANGLE',
|
|
280
|
+
fill: fillColor,
|
|
281
|
+
line: lineColor,
|
|
282
|
+
lineWidth,
|
|
283
|
+
lineDash,
|
|
284
|
+
...flip,
|
|
285
|
+
},
|
|
221
286
|
degradeReason: `未知预设形状 "${prst}",退化为矩形`,
|
|
222
287
|
});
|
|
223
288
|
return;
|
|
224
289
|
}
|
|
225
290
|
|
|
291
|
+
const degradeReason =
|
|
292
|
+
prstDegradeReason ||
|
|
293
|
+
(fillDegraded ? '渐变/图案填充退化为纯色' : undefined);
|
|
294
|
+
|
|
226
295
|
ctx.entities.push({
|
|
227
296
|
slideIndex: ctx.slideIndex,
|
|
228
297
|
slidePath: ctx.slidePath,
|
|
229
|
-
decision: fillDegraded ? 'DEGRADE' : 'FULL',
|
|
298
|
+
decision: (prstDegradeReason || fillDegraded) ? 'DEGRADE' : 'FULL',
|
|
230
299
|
kind: 'shape',
|
|
231
300
|
bounds,
|
|
232
|
-
shape: {
|
|
233
|
-
|
|
301
|
+
shape: {
|
|
302
|
+
type: shapeName,
|
|
303
|
+
fill: fillColor,
|
|
304
|
+
line: lineColor,
|
|
305
|
+
lineWidth,
|
|
306
|
+
lineDash,
|
|
307
|
+
...flip,
|
|
308
|
+
},
|
|
309
|
+
degradeReason,
|
|
234
310
|
});
|
|
235
311
|
}
|
|
236
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
|
+
|
|
237
323
|
/**
|
|
238
324
|
* @param {object|null|undefined} ln a:ln
|
|
239
325
|
* @returns {number|undefined}
|
|
@@ -245,6 +331,31 @@ function extractLineWidthPt(ln) {
|
|
|
245
331
|
return Math.max(1, Math.round(w / 12700));
|
|
246
332
|
}
|
|
247
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
|
+
|
|
248
359
|
function extractPicture(pic, ctx) {
|
|
249
360
|
const spPr = child(pic, 'p:spPr');
|
|
250
361
|
const bounds = boundsFromXfrm(child(spPr, 'a:xfrm'), ctx.offset);
|
|
@@ -271,8 +382,10 @@ function extractPicture(pic, ctx) {
|
|
|
271
382
|
|
|
272
383
|
function extractConnector(cxn, ctx) {
|
|
273
384
|
const spPr = child(cxn, 'p:spPr');
|
|
274
|
-
const
|
|
275
|
-
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);
|
|
276
389
|
|
|
277
390
|
ctx.entities.push({
|
|
278
391
|
slideIndex: ctx.slideIndex,
|
|
@@ -280,7 +393,14 @@ function extractConnector(cxn, ctx) {
|
|
|
280
393
|
decision: 'FULL',
|
|
281
394
|
kind: 'shape',
|
|
282
395
|
bounds,
|
|
283
|
-
shape: {
|
|
396
|
+
shape: {
|
|
397
|
+
type: 'LINE',
|
|
398
|
+
fill: null,
|
|
399
|
+
line: lineColor,
|
|
400
|
+
lineWidth: extractLineWidthPt(ln),
|
|
401
|
+
lineDash: extractLineDash(ln),
|
|
402
|
+
...readFlip(xfrm),
|
|
403
|
+
},
|
|
284
404
|
});
|
|
285
405
|
}
|
|
286
406
|
|
|
@@ -303,146 +423,19 @@ function readOffset(xfrm) {
|
|
|
303
423
|
};
|
|
304
424
|
}
|
|
305
425
|
|
|
306
|
-
const PARA_ALIGN_MAP = {
|
|
307
|
-
l: 'left',
|
|
308
|
-
ctr: 'center',
|
|
309
|
-
r: 'right',
|
|
310
|
-
just: 'justify',
|
|
311
|
-
dist: 'justify',
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
function extractTextRuns(txBody, scheme, linkCtx) {
|
|
315
|
-
const runs = [];
|
|
316
|
-
const bodyParaOpts = extractBodyPrParaOptions(txBody);
|
|
317
|
-
const paragraphs = asArray(txBody['a:p']);
|
|
318
|
-
|
|
319
|
-
for (let pi = 0; pi < paragraphs.length; pi++) {
|
|
320
|
-
const p = paragraphs[pi];
|
|
321
|
-
// 同一段落内可有多个 a:pPr(PptxGenJS 为每个 run 内联段落属性)→ xml2js 解析为数组
|
|
322
|
-
const pPrs = asArray(p['a:pPr']);
|
|
323
|
-
const pRuns = asArray(p['a:r']);
|
|
324
|
-
const bullet0 = extractBullet(pPrs[0]);
|
|
325
|
-
|
|
326
|
-
pRuns.forEach((r, ri) => {
|
|
327
|
-
const pPr = pPrs[ri] ?? pPrs[0];
|
|
328
|
-
const paraOpts = { ...bodyParaOpts, ...extractParaOptions(pPr) };
|
|
329
|
-
const bullet = extractBullet(pPr) ?? bullet0;
|
|
330
|
-
|
|
331
|
-
const text = textContent(r['a:t']);
|
|
332
|
-
if (!text) return;
|
|
333
|
-
const options = extractRunOptions(r['a:rPr'], scheme, linkCtx);
|
|
334
|
-
Object.assign(options, paraOpts);
|
|
335
|
-
if (bullet) Object.assign(options, bullet);
|
|
336
|
-
runs.push({ text, options, degraded: options._degraded });
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
for (const _br of asArray(p['a:br'])) {
|
|
340
|
-
const pPr = pPrs[0];
|
|
341
|
-
const paraOpts = { ...bodyParaOpts, ...extractParaOptions(pPr) };
|
|
342
|
-
runs.push({ text: '\n', options: { ...paraOpts }, degraded: false });
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (pi < paragraphs.length - 1) {
|
|
346
|
-
const last = runs[runs.length - 1];
|
|
347
|
-
if (!last || !last.text.endsWith('\n')) {
|
|
348
|
-
const paraOpts = { ...bodyParaOpts, ...extractParaOptions(pPrs[0]) };
|
|
349
|
-
if (bullet0) delete paraOpts.bullet;
|
|
350
|
-
runs.push({ text: '\n', options: { ...paraOpts }, degraded: false });
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return compressTextRuns(runs);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
426
|
/**
|
|
358
|
-
*
|
|
359
|
-
* @param {object|null|undefined}
|
|
427
|
+
* 组合内部坐标系原点(EMU)
|
|
428
|
+
* @param {object|null|undefined} xfrm
|
|
360
429
|
*/
|
|
361
|
-
function
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* @param {object|null|undefined} pPr
|
|
370
|
-
*/
|
|
371
|
-
function extractParaOptions(pPr) {
|
|
372
|
-
if (!pPr) return {};
|
|
373
|
-
const node = Array.isArray(pPr) ? pPr[0] : pPr;
|
|
374
|
-
const opts = {};
|
|
375
|
-
|
|
376
|
-
const algn = attr(node, 'algn');
|
|
377
|
-
if (algn && PARA_ALIGN_MAP[algn]) opts.align = PARA_ALIGN_MAP[algn];
|
|
378
|
-
|
|
379
|
-
const lvl = attr(node, 'lvl');
|
|
380
|
-
if (lvl) opts.indentLevel = parseInt(lvl, 10);
|
|
381
|
-
|
|
382
|
-
// 注:a:spcPct(百分比段前/段后距)暂不处理
|
|
383
|
-
const spcBef = attr(child(child(node, 'a:spcBef'), 'a:spcPts'), 'val');
|
|
384
|
-
if (spcBef) opts.paraSpaceBefore = parseInt(spcBef, 10) / 100;
|
|
385
|
-
|
|
386
|
-
// 注:a:spcPct(百分比段后距)暂不处理
|
|
387
|
-
const spcAft = attr(child(child(node, 'a:spcAft'), 'a:spcPts'), 'val');
|
|
388
|
-
if (spcAft) opts.paraSpaceAfter = parseInt(spcAft, 10) / 100;
|
|
389
|
-
|
|
390
|
-
// 注:a:spcPct(百分比行距)暂不处理
|
|
391
|
-
const lnSpc = attr(child(child(node, 'a:lnSpc'), 'a:spcPts'), 'val');
|
|
392
|
-
if (lnSpc) opts.lineSpacing = parseInt(lnSpc, 10) / 100;
|
|
393
|
-
|
|
394
|
-
return opts;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function extractBullet(pPr) {
|
|
398
|
-
if (!pPr) return null;
|
|
399
|
-
const node = Array.isArray(pPr) ? pPr[0] : pPr;
|
|
400
|
-
if (child(node, 'a:buChar')) return { bullet: true };
|
|
401
|
-
if (child(node, 'a:buAutoNum')) return { bullet: { type: 'number' } };
|
|
402
|
-
return null;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function extractRunOptions(rPr, scheme, linkCtx) {
|
|
406
|
-
const options = {};
|
|
407
|
-
let degraded = false;
|
|
408
|
-
if (!rPr) return options;
|
|
409
|
-
|
|
410
|
-
const sz = attr(rPr, 'sz');
|
|
411
|
-
if (sz) options.fontSize = parseInt(sz, 10) / 100;
|
|
412
|
-
if (attr(rPr, 'b') === '1') options.bold = true;
|
|
413
|
-
if (attr(rPr, 'i') === '1') options.italic = true;
|
|
414
|
-
|
|
415
|
-
const u = attr(rPr, 'u');
|
|
416
|
-
if (u && u !== 'none') options.underline = { style: 'sng' };
|
|
417
|
-
|
|
418
|
-
const face = attr(child(rPr, 'a:latin'), 'typeface');
|
|
419
|
-
if (face) options.fontFace = face;
|
|
420
|
-
|
|
421
|
-
const solid = child(rPr, 'a:solidFill');
|
|
422
|
-
const grad = child(rPr, 'a:gradFill');
|
|
423
|
-
if (grad) {
|
|
424
|
-
degraded = true;
|
|
425
|
-
const g = resolveFillColor({ 'a:gradFill': grad }, scheme);
|
|
426
|
-
if (g.color) options.color = g.color;
|
|
427
|
-
} else if (solid) {
|
|
428
|
-
const c = resolveColorFromContainer(solid, scheme);
|
|
429
|
-
if (c) options.color = c;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const hlink = child(rPr, 'a:hlinkClick');
|
|
433
|
-
if (hlink && linkCtx) {
|
|
434
|
-
const relId = attr(hlink, 'r:id');
|
|
435
|
-
const url = relId && linkCtx.relIndex.resolve(linkCtx.slidePath, relId);
|
|
436
|
-
if (url) options.hyperlink = { url };
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (degraded) options._degraded = true;
|
|
440
|
-
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
|
+
};
|
|
441
436
|
}
|
|
442
437
|
|
|
443
438
|
module.exports = {
|
|
444
439
|
extractEntities,
|
|
445
|
-
extractTextRuns,
|
|
446
440
|
PRST_TO_SHAPE,
|
|
447
|
-
boundsFromXfrm,
|
|
448
441
|
};
|
package/lib/mapper.js
CHANGED
|
@@ -85,6 +85,9 @@ function mapToIR(entitiesBySlide, ctx) {
|
|
|
85
85
|
fill: entity.shape.fill,
|
|
86
86
|
line: entity.shape.line,
|
|
87
87
|
lineWidth: entity.shape.lineWidth,
|
|
88
|
+
lineDash: entity.shape.lineDash,
|
|
89
|
+
flipH: entity.shape.flipH,
|
|
90
|
+
flipV: entity.shape.flipV,
|
|
88
91
|
degradeReason: entity.degradeReason,
|
|
89
92
|
});
|
|
90
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
|
+
};
|