pptx2js 0.4.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/README.html +804 -0
- package/README.md +201 -0
- package/bin/pptx2js.js +75 -0
- package/lib/chart.js +245 -0
- package/lib/codegen.js +172 -0
- package/lib/convert.js +195 -0
- package/lib/extractor.js +394 -0
- package/lib/graphic.js +63 -0
- package/lib/index.js +7 -0
- package/lib/mapper.js +98 -0
- package/lib/packager.js +72 -0
- package/lib/placeholder.js +258 -0
- package/lib/presentation.js +70 -0
- package/lib/rels.js +117 -0
- package/lib/smartart.js +96 -0
- package/lib/table.js +138 -0
- package/lib/unpacker.js +40 -0
- package/lib/utils/bounds.js +34 -0
- package/lib/utils/color.js +128 -0
- package/lib/utils/emu.js +20 -0
- package/lib/xml-parser.js +25 -0
- package/lib/xml-utils.js +68 -0
- package/package.json +41 -0
package/lib/convert.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 转换流水线编排器
|
|
3
|
+
*/
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { parseXml } = require('./xml-parser');
|
|
7
|
+
const { unpack } = require('./unpacker');
|
|
8
|
+
const { buildRelationIndex } = require('./rels');
|
|
9
|
+
const { extractEntities } = require('./extractor');
|
|
10
|
+
const { mapToIR } = require('./mapper');
|
|
11
|
+
const { generateScript } = require('./codegen');
|
|
12
|
+
const {
|
|
13
|
+
packageMedia,
|
|
14
|
+
writeConversionLog,
|
|
15
|
+
writeOutputReadme,
|
|
16
|
+
} = require('./packager');
|
|
17
|
+
|
|
18
|
+
/** @typedef {object} ConvertOptions
|
|
19
|
+
* @property {string} [outputDir]
|
|
20
|
+
* @property {boolean} [noMedia]
|
|
21
|
+
* @property {boolean} [strictDegrade]
|
|
22
|
+
* @property {boolean} [strictSkip]
|
|
23
|
+
* @property {'minimal'|'info'|'verbose'} [logLevel]
|
|
24
|
+
* @property {number} [maxFileSize]
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** @typedef {object} ConvertResult
|
|
28
|
+
* @property {string} outputDir
|
|
29
|
+
* @property {string} scriptPath
|
|
30
|
+
* @property {string} logPath
|
|
31
|
+
* @property {object} log
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const DEFAULT_OUTPUT = './pptx2js-output';
|
|
35
|
+
const DEFAULT_MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} filePath
|
|
39
|
+
* @param {ConvertOptions} [options]
|
|
40
|
+
* @returns {Promise<ConvertResult>}
|
|
41
|
+
*/
|
|
42
|
+
async function convert(filePath, options = {}) {
|
|
43
|
+
const resolvedPath = path.resolve(filePath);
|
|
44
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
45
|
+
throw new Error(`源文件不存在: ${resolvedPath}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const stat = fs.statSync(resolvedPath);
|
|
49
|
+
if (stat.size > (options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE)) {
|
|
50
|
+
// 大文件流式解析 — 实现阶段补充
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const outputDir = path.resolve(options.outputDir ?? DEFAULT_OUTPUT);
|
|
54
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
55
|
+
|
|
56
|
+
const archive = await unpack(resolvedPath);
|
|
57
|
+
|
|
58
|
+
/** @type {Record<string, object>} */
|
|
59
|
+
const parsed = {};
|
|
60
|
+
for (const [entryPath, content] of archive.files) {
|
|
61
|
+
if (content === '__binary__') continue;
|
|
62
|
+
if (entryPath.endsWith('.xml') || entryPath.endsWith('.rels')) {
|
|
63
|
+
parsed[entryPath] = await parseXml(content);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const relIndex = buildRelationIndex(parsed);
|
|
68
|
+
const ctx = { relIndex, parsed, sourcePath: resolvedPath };
|
|
69
|
+
const entities = extractEntities(ctx);
|
|
70
|
+
const ir = mapToIR(entities, ctx);
|
|
71
|
+
const script = generateScript(ir, options);
|
|
72
|
+
|
|
73
|
+
const scriptPath = path.join(outputDir, 'output.js');
|
|
74
|
+
fs.writeFileSync(scriptPath, script, 'utf8');
|
|
75
|
+
|
|
76
|
+
await packageMedia({ archive, ir }, outputDir, {
|
|
77
|
+
noMedia: options.noMedia,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const log = buildConversionLog(resolvedPath, stat, entities, ir);
|
|
81
|
+
writeConversionLog(outputDir, log);
|
|
82
|
+
writeOutputReadme(outputDir, {
|
|
83
|
+
sourcePath: resolvedPath,
|
|
84
|
+
generatedAt: new Date().toISOString(),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (options.strictDegrade && log.statistics.degraded > 0) {
|
|
88
|
+
const err = new Error('存在退化项(--strict-degrade)');
|
|
89
|
+
err.code = 'STRICT_DEGRADE';
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
if (options.strictSkip) {
|
|
93
|
+
const errors = (log.omitted || []).filter((o) => o.severity === 'error');
|
|
94
|
+
if (errors.length > 0) {
|
|
95
|
+
const err = new Error('存在 error 级别跳过项(--strict-skip)');
|
|
96
|
+
err.code = 'STRICT_SKIP';
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
outputDir,
|
|
103
|
+
scriptPath,
|
|
104
|
+
logPath: path.join(outputDir, 'conversion.log'),
|
|
105
|
+
log,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} sourcePath
|
|
111
|
+
* @param {fs.Stats} stat
|
|
112
|
+
* @param {import('./extractor').SlideEntity[][]} entitiesBySlide
|
|
113
|
+
* @param {import('./mapper').IntermediateRepresentation} ir
|
|
114
|
+
*/
|
|
115
|
+
function buildConversionLog(sourcePath, stat, entitiesBySlide, ir) {
|
|
116
|
+
let full = 0;
|
|
117
|
+
let degraded = 0;
|
|
118
|
+
let skipped = 0;
|
|
119
|
+
|
|
120
|
+
/** @type {object[]} */
|
|
121
|
+
const slides = [];
|
|
122
|
+
/** @type {object[]} */
|
|
123
|
+
const degradedList = [];
|
|
124
|
+
/** @type {object[]} */
|
|
125
|
+
const omitted = [];
|
|
126
|
+
/** @type {object[]} */
|
|
127
|
+
const warnings = [];
|
|
128
|
+
|
|
129
|
+
entitiesBySlide.forEach((entities, slideIndex) => {
|
|
130
|
+
/** @type {object[]} */
|
|
131
|
+
const slideItems = [];
|
|
132
|
+
|
|
133
|
+
for (const entity of entities) {
|
|
134
|
+
const bounds = entity.bounds;
|
|
135
|
+
const base = {
|
|
136
|
+
slideIndex,
|
|
137
|
+
elementBounds: bounds,
|
|
138
|
+
kind: entity.kind,
|
|
139
|
+
decision: entity.decision,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (entity.decision === 'FULL') full++;
|
|
143
|
+
else if (entity.decision === 'DEGRADE') degraded++;
|
|
144
|
+
else skipped++;
|
|
145
|
+
|
|
146
|
+
slideItems.push(base);
|
|
147
|
+
|
|
148
|
+
if (entity.decision === 'DEGRADE' && entity.degradeReason) {
|
|
149
|
+
degradedList.push({
|
|
150
|
+
...base,
|
|
151
|
+
reason: entity.degradeReason,
|
|
152
|
+
severity: 'warn',
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (entity.decision === 'SKIP') {
|
|
157
|
+
omitted.push({
|
|
158
|
+
...base,
|
|
159
|
+
type: entity.kind,
|
|
160
|
+
reason: entity.skipReason,
|
|
161
|
+
severity: entity.skipReason?.includes('尚未支持') ? 'warn' : 'error',
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
slides.push({ slideIndex, items: slideItems });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (ir.slides.length === 0) {
|
|
170
|
+
warnings.push({
|
|
171
|
+
message: '未解析到任何幻灯片,请检查 presentation.xml 与关系文件',
|
|
172
|
+
severity: 'warn',
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
source: {
|
|
178
|
+
path: sourcePath,
|
|
179
|
+
size: stat.size,
|
|
180
|
+
slideCount: ir.slides.length,
|
|
181
|
+
},
|
|
182
|
+
statistics: {
|
|
183
|
+
slides: ir.slides.length,
|
|
184
|
+
full,
|
|
185
|
+
degraded,
|
|
186
|
+
skipped,
|
|
187
|
+
},
|
|
188
|
+
slides,
|
|
189
|
+
degraded: degradedList,
|
|
190
|
+
omitted,
|
|
191
|
+
warnings,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = { convert, DEFAULT_OUTPUT, DEFAULT_MAX_FILE_SIZE };
|
package/lib/extractor.js
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ③ 实体提取器
|
|
3
|
+
*/
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { asArray, attr, child, textContent } = require('./xml-utils');
|
|
6
|
+
const { boundsFromXfrm } = require('./utils/bounds');
|
|
7
|
+
const {
|
|
8
|
+
loadColorScheme,
|
|
9
|
+
resolveFillColor,
|
|
10
|
+
resolveColorFromContainer,
|
|
11
|
+
} = require('./utils/color');
|
|
12
|
+
const { getSlidePaths, getThemePath } = require('./presentation');
|
|
13
|
+
const {
|
|
14
|
+
buildSlideInheritance,
|
|
15
|
+
getEffectiveXfrm,
|
|
16
|
+
mergeTxBody,
|
|
17
|
+
resolvePlaceholderSps,
|
|
18
|
+
} = require('./placeholder');
|
|
19
|
+
const { isTableFrame, isChartFrame, isSmartArtFrame } = require('./graphic');
|
|
20
|
+
const { extractTable } = require('./table');
|
|
21
|
+
const { extractChart } = require('./chart');
|
|
22
|
+
const { extractSmartArt } = require('./smartart');
|
|
23
|
+
|
|
24
|
+
/** @typedef {'FULL' | 'DEGRADE' | 'SKIP'} ConversionDecision */
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {object} SlideEntity
|
|
28
|
+
* @property {number} slideIndex
|
|
29
|
+
* @property {string} slidePath
|
|
30
|
+
* @property {ConversionDecision} decision
|
|
31
|
+
* @property {string} kind
|
|
32
|
+
* @property {import('./utils/bounds').Bounds} bounds
|
|
33
|
+
* @property {object} [text]
|
|
34
|
+
* @property {object} [image]
|
|
35
|
+
* @property {object} [shape]
|
|
36
|
+
* @property {object} [table]
|
|
37
|
+
* @property {object} [chart]
|
|
38
|
+
* @property {string} [skipReason]
|
|
39
|
+
* @property {string} [degradeReason]
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
const PRST_TO_SHAPE = {
|
|
43
|
+
rect: 'RECTANGLE',
|
|
44
|
+
roundRect: 'ROUNDED_RECTANGLE',
|
|
45
|
+
ellipse: 'OVAL',
|
|
46
|
+
line: 'LINE',
|
|
47
|
+
triangle: 'TRIANGLE',
|
|
48
|
+
rtTriangle: 'RIGHT_TRIANGLE',
|
|
49
|
+
diamond: 'DIAMOND',
|
|
50
|
+
pentagon: 'PENTAGON',
|
|
51
|
+
hexagon: 'HEXAGON',
|
|
52
|
+
star5: 'STAR5',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @typedef {object} ExtractContext
|
|
57
|
+
* @property {Record<string, object>} parsed
|
|
58
|
+
* @property {import('./rels').RelationIndex} relIndex
|
|
59
|
+
* @property {string} [sourcePath]
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {ExtractContext} ctx
|
|
64
|
+
* @returns {SlideEntity[][]}
|
|
65
|
+
*/
|
|
66
|
+
function extractEntities(ctx) {
|
|
67
|
+
const { parsed, relIndex } = ctx;
|
|
68
|
+
const themePath = getThemePath(parsed, relIndex);
|
|
69
|
+
const scheme = loadColorScheme(parsed, themePath);
|
|
70
|
+
const slidePaths = getSlidePaths(parsed, relIndex);
|
|
71
|
+
|
|
72
|
+
return slidePaths.map((slidePath, slideIndex) =>
|
|
73
|
+
extractSlide(slidePath, slideIndex, ctx, scheme)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} slidePath
|
|
79
|
+
* @param {number} slideIndex
|
|
80
|
+
* @param {ExtractContext} ctx
|
|
81
|
+
* @param {Record<string, string>} scheme
|
|
82
|
+
*/
|
|
83
|
+
function extractSlide(slidePath, slideIndex, ctx, scheme) {
|
|
84
|
+
const doc = ctx.parsed[slidePath];
|
|
85
|
+
const slide = child(doc, 'p:sld');
|
|
86
|
+
const cSld = child(slide, 'p:cSld');
|
|
87
|
+
const spTree = child(cSld, 'p:spTree');
|
|
88
|
+
if (!spTree) return [];
|
|
89
|
+
|
|
90
|
+
const inheritance = buildSlideInheritance(ctx.parsed, ctx.relIndex, slidePath);
|
|
91
|
+
|
|
92
|
+
/** @type {SlideEntity[]} */
|
|
93
|
+
const entities = [];
|
|
94
|
+
|
|
95
|
+
const bg = extractSlideBackground(cSld, scheme);
|
|
96
|
+
if (bg) {
|
|
97
|
+
entities.push({
|
|
98
|
+
slideIndex,
|
|
99
|
+
slidePath,
|
|
100
|
+
decision: bg.degraded ? 'DEGRADE' : 'FULL',
|
|
101
|
+
kind: 'background',
|
|
102
|
+
bounds: { x: 0, y: 0, w: 0, h: 0 },
|
|
103
|
+
shape: { type: 'background', fill: bg.color },
|
|
104
|
+
degradeReason: bg.degraded ? '渐变背景退化为纯色' : undefined,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const walkCtx = {
|
|
109
|
+
slideIndex,
|
|
110
|
+
slidePath,
|
|
111
|
+
relIndex: ctx.relIndex,
|
|
112
|
+
parsed: ctx.parsed,
|
|
113
|
+
scheme,
|
|
114
|
+
inheritance,
|
|
115
|
+
offset: { x: 0, y: 0 },
|
|
116
|
+
entities,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
walkSpTree(spTree, walkCtx);
|
|
120
|
+
return entities;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function extractSlideBackground(cSld, scheme) {
|
|
124
|
+
const bgPr = child(child(cSld, 'p:bg'), 'p:bgPr');
|
|
125
|
+
if (!bgPr) return null;
|
|
126
|
+
const { color, degraded } = resolveFillColor(bgPr, scheme);
|
|
127
|
+
if (!color) return null;
|
|
128
|
+
return { color, degraded };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function walkSpTree(spTree, ctx) {
|
|
132
|
+
for (const sp of asArray(spTree['p:sp'])) {
|
|
133
|
+
extractShape(sp, ctx);
|
|
134
|
+
}
|
|
135
|
+
for (const pic of asArray(spTree['p:pic'])) {
|
|
136
|
+
extractPicture(pic, ctx);
|
|
137
|
+
}
|
|
138
|
+
for (const grp of asArray(spTree['p:grpSp'])) {
|
|
139
|
+
flattenGroup(grp, ctx);
|
|
140
|
+
}
|
|
141
|
+
for (const frame of asArray(spTree['p:graphicFrame'])) {
|
|
142
|
+
if (isTableFrame(frame)) {
|
|
143
|
+
const entity = extractTable(frame, ctx);
|
|
144
|
+
if (entity) ctx.entities.push(entity);
|
|
145
|
+
} else if (isChartFrame(frame)) {
|
|
146
|
+
const entity = extractChart(frame, ctx);
|
|
147
|
+
if (entity) ctx.entities.push(entity);
|
|
148
|
+
} else if (isSmartArtFrame(frame)) {
|
|
149
|
+
const entity = extractSmartArt(frame, ctx);
|
|
150
|
+
if (entity) ctx.entities.push(entity);
|
|
151
|
+
} else {
|
|
152
|
+
skipElement(frame, ctx, '不支持的 graphicFrame 类型');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const cxn of asArray(spTree['p:cxnSp'])) {
|
|
156
|
+
extractConnector(cxn, ctx);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function flattenGroup(grp, ctx) {
|
|
161
|
+
const grpXfrm = child(child(grp, 'p:grpSpPr'), 'a:xfrm');
|
|
162
|
+
const off = readOffset(grpXfrm);
|
|
163
|
+
const inner = child(grp, 'p:spTree');
|
|
164
|
+
if (!inner) return;
|
|
165
|
+
walkSpTree(inner, {
|
|
166
|
+
...ctx,
|
|
167
|
+
offset: { x: ctx.offset.x + off.x, y: ctx.offset.y + off.y },
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function extractShape(sp, ctx) {
|
|
172
|
+
const xfrm = getEffectiveXfrm(sp, ctx.inheritance);
|
|
173
|
+
const bounds = boundsFromXfrm(xfrm, ctx.offset);
|
|
174
|
+
|
|
175
|
+
const { layoutSp, masterSp } = resolvePlaceholderSps(sp, ctx.inheritance);
|
|
176
|
+
const txBodyRaw = child(sp, 'p:txBody');
|
|
177
|
+
|
|
178
|
+
if (txBodyRaw) {
|
|
179
|
+
const txBody = mergeTxBody(txBodyRaw, layoutSp, masterSp);
|
|
180
|
+
const runs = extractTextRuns(txBody, ctx.scheme, {
|
|
181
|
+
relIndex: ctx.relIndex,
|
|
182
|
+
slidePath: ctx.slidePath,
|
|
183
|
+
});
|
|
184
|
+
if (runs.length === 0) return;
|
|
185
|
+
|
|
186
|
+
const hasGradient = runs.some((r) => r.degraded);
|
|
187
|
+
ctx.entities.push({
|
|
188
|
+
slideIndex: ctx.slideIndex,
|
|
189
|
+
slidePath: ctx.slidePath,
|
|
190
|
+
decision: hasGradient ? 'DEGRADE' : 'FULL',
|
|
191
|
+
kind: 'text',
|
|
192
|
+
bounds,
|
|
193
|
+
text: { runs: runs.map(({ text, options }) => ({ text, options })) },
|
|
194
|
+
degradeReason: hasGradient ? '文本渐变退化为纯色' : undefined,
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const spPr = child(sp, 'p:spPr');
|
|
200
|
+
const prst = attr(child(spPr, 'a:prstGeom'), 'prst');
|
|
201
|
+
if (!prst) return;
|
|
202
|
+
|
|
203
|
+
const shapeName = PRST_TO_SHAPE[prst];
|
|
204
|
+
const { color: fillColor, degraded: fillDegraded } = resolveFillColor(
|
|
205
|
+
spPr,
|
|
206
|
+
ctx.scheme
|
|
207
|
+
);
|
|
208
|
+
const ln = child(spPr, 'a:ln');
|
|
209
|
+
const lineColor = ln ? resolveFillColor(ln, ctx.scheme).color : null;
|
|
210
|
+
|
|
211
|
+
if (!shapeName) {
|
|
212
|
+
ctx.entities.push({
|
|
213
|
+
slideIndex: ctx.slideIndex,
|
|
214
|
+
slidePath: ctx.slidePath,
|
|
215
|
+
decision: 'DEGRADE',
|
|
216
|
+
kind: 'shape',
|
|
217
|
+
bounds,
|
|
218
|
+
shape: { type: 'RECTANGLE', fill: fillColor, line: lineColor },
|
|
219
|
+
degradeReason: `未知预设形状 "${prst}",退化为矩形`,
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
ctx.entities.push({
|
|
225
|
+
slideIndex: ctx.slideIndex,
|
|
226
|
+
slidePath: ctx.slidePath,
|
|
227
|
+
decision: fillDegraded ? 'DEGRADE' : 'FULL',
|
|
228
|
+
kind: 'shape',
|
|
229
|
+
bounds,
|
|
230
|
+
shape: { type: shapeName, fill: fillColor, line: lineColor },
|
|
231
|
+
degradeReason: fillDegraded ? '渐变填充退化为纯色' : undefined,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function extractPicture(pic, ctx) {
|
|
236
|
+
const spPr = child(pic, 'p:spPr');
|
|
237
|
+
const bounds = boundsFromXfrm(child(spPr, 'a:xfrm'), ctx.offset);
|
|
238
|
+
|
|
239
|
+
const blip = child(child(pic, 'p:blipFill'), 'a:blip');
|
|
240
|
+
const embedId = attr(blip, 'r:embed');
|
|
241
|
+
if (!embedId) return;
|
|
242
|
+
|
|
243
|
+
const mediaPath = ctx.relIndex.resolve(ctx.slidePath, embedId);
|
|
244
|
+
if (!mediaPath) return;
|
|
245
|
+
|
|
246
|
+
ctx.entities.push({
|
|
247
|
+
slideIndex: ctx.slideIndex,
|
|
248
|
+
slidePath: ctx.slidePath,
|
|
249
|
+
decision: 'FULL',
|
|
250
|
+
kind: 'image',
|
|
251
|
+
bounds,
|
|
252
|
+
image: {
|
|
253
|
+
zipPath: mediaPath,
|
|
254
|
+
fileName: path.posix.basename(mediaPath),
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function extractConnector(cxn, ctx) {
|
|
260
|
+
const spPr = child(cxn, 'p:spPr');
|
|
261
|
+
const bounds = boundsFromXfrm(child(spPr, 'a:xfrm'), ctx.offset);
|
|
262
|
+
const { color: lineColor } = resolveFillColor(child(spPr, 'a:ln'), ctx.scheme);
|
|
263
|
+
|
|
264
|
+
ctx.entities.push({
|
|
265
|
+
slideIndex: ctx.slideIndex,
|
|
266
|
+
slidePath: ctx.slidePath,
|
|
267
|
+
decision: 'FULL',
|
|
268
|
+
kind: 'shape',
|
|
269
|
+
bounds,
|
|
270
|
+
shape: { type: 'LINE', fill: null, line: lineColor },
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function skipElement(_node, ctx, reason) {
|
|
275
|
+
ctx.entities.push({
|
|
276
|
+
slideIndex: ctx.slideIndex,
|
|
277
|
+
slidePath: ctx.slidePath,
|
|
278
|
+
decision: 'SKIP',
|
|
279
|
+
kind: 'skip',
|
|
280
|
+
bounds: { x: 0, y: 0, w: 0, h: 0 },
|
|
281
|
+
skipReason: reason,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function readOffset(xfrm) {
|
|
286
|
+
const off = child(xfrm, 'a:off');
|
|
287
|
+
return {
|
|
288
|
+
x: parseInt(attr(off, 'x') ?? '0', 10),
|
|
289
|
+
y: parseInt(attr(off, 'y') ?? '0', 10),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
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
|
+
/**
|
|
321
|
+
* @param {object|null|undefined} pPr
|
|
322
|
+
*/
|
|
323
|
+
function extractParaOptions(pPr) {
|
|
324
|
+
if (!pPr) return {};
|
|
325
|
+
const opts = {};
|
|
326
|
+
|
|
327
|
+
const algn = attr(pPr, 'algn');
|
|
328
|
+
if (algn && PARA_ALIGN_MAP[algn]) opts.align = PARA_ALIGN_MAP[algn];
|
|
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;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
module.exports = {
|
|
391
|
+
extractEntities,
|
|
392
|
+
PRST_TO_SHAPE,
|
|
393
|
+
boundsFromXfrm,
|
|
394
|
+
};
|
package/lib/graphic.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* graphicFrame 辅助:识别表格 / 图表等 graphicData URI
|
|
3
|
+
*/
|
|
4
|
+
const { attr, child } = require('./xml-utils');
|
|
5
|
+
|
|
6
|
+
const URI_TABLE =
|
|
7
|
+
'http://schemas.openxmlformats.org/drawingml/2006/table';
|
|
8
|
+
const URI_CHART =
|
|
9
|
+
'http://schemas.openxmlformats.org/drawingml/2006/chart';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {object} graphicFrame
|
|
13
|
+
* @returns {string|undefined}
|
|
14
|
+
*/
|
|
15
|
+
function getGraphicUri(graphicFrame) {
|
|
16
|
+
const graphicData = child(child(graphicFrame, 'a:graphic'), 'a:graphicData');
|
|
17
|
+
return attr(graphicData, 'uri');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {object} graphicFrame
|
|
22
|
+
* @returns {boolean}
|
|
23
|
+
*/
|
|
24
|
+
function isTableFrame(graphicFrame) {
|
|
25
|
+
const uri = getGraphicUri(graphicFrame);
|
|
26
|
+
return Boolean(uri && uri.includes('table'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {object} graphicFrame
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
function isChartFrame(graphicFrame) {
|
|
34
|
+
const uri = getGraphicUri(graphicFrame);
|
|
35
|
+
return Boolean(uri && uri.includes('chart'));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {object} graphicFrame
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
function isSmartArtFrame(graphicFrame) {
|
|
43
|
+
const uri = getGraphicUri(graphicFrame);
|
|
44
|
+
return Boolean(uri && uri.includes('diagram'));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {object} graphicFrame
|
|
49
|
+
* @returns {object|null} p:xfrm 节点
|
|
50
|
+
*/
|
|
51
|
+
function getGraphicXfrm(graphicFrame) {
|
|
52
|
+
return child(graphicFrame, 'p:xfrm');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
URI_TABLE,
|
|
57
|
+
URI_CHART,
|
|
58
|
+
getGraphicUri,
|
|
59
|
+
isTableFrame,
|
|
60
|
+
isChartFrame,
|
|
61
|
+
isSmartArtFrame,
|
|
62
|
+
getGraphicXfrm,
|
|
63
|
+
};
|