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/lib/color.js ADDED
@@ -0,0 +1,275 @@
1
+ /**
2
+ * 颜色规范化(srgbClr、schemeClr、渐变首色标)
3
+ */
4
+ const { attr, child, children, documentRoot } = require('./xml-utils');
5
+
6
+ /** OOXML a:prstClr val → 6 位 RGB(源 PPT 主要使用 black / white) */
7
+ const PRST_COLOR_MAP = {
8
+ black: '000000',
9
+ white: 'FFFFFF',
10
+ red: 'FF0000',
11
+ green: '00FF00',
12
+ blue: '0000FF',
13
+ yellow: 'FFFF00',
14
+ gray: '808080',
15
+ grey: '808080',
16
+ silver: 'C0C0C0',
17
+ navy: '000080',
18
+ teal: '008080',
19
+ lime: '00FF00',
20
+ olive: '808000',
21
+ maroon: '800000',
22
+ aqua: '00FFFF',
23
+ fuchsia: 'FF00FF',
24
+ orange: 'FFA500',
25
+ purple: '800080',
26
+ brown: 'A52A2A',
27
+ pink: 'FFC0CB',
28
+ dkGreen: '006400',
29
+ dkBlue: '00008B',
30
+ dkRed: '8B0000',
31
+ dkGray: 'A9A9A9',
32
+ dkGrey: 'A9A9A9',
33
+ ltGray: 'D3D3D3',
34
+ ltGrey: 'D3D3D3',
35
+ };
36
+
37
+ /** @type {Record<string, string>} */
38
+ const DEFAULT_SCHEME = {
39
+ dk1: '000000',
40
+ lt1: 'FFFFFF',
41
+ dk2: '44546A',
42
+ lt2: 'E7E6E6',
43
+ accent1: '4472C4',
44
+ accent2: 'ED7D31',
45
+ accent3: 'A5A5A5',
46
+ accent4: 'FFC000',
47
+ accent5: '5B9BD5',
48
+ accent6: '70AD47',
49
+ hlink: '0563C1',
50
+ folHlink: '954F72',
51
+ };
52
+
53
+ /**
54
+ * @param {Record<string, object>|null} parsed
55
+ * @param {string|null} themePath
56
+ * @returns {Record<string, string>}
57
+ */
58
+ function loadColorScheme(parsed, themePath) {
59
+ if (!themePath || !parsed[themePath]) return { ...DEFAULT_SCHEME };
60
+
61
+ const theme = documentRoot(parsed[themePath], 'a:theme');
62
+ const clrScheme = child(child(theme, 'a:themeElements'), 'a:clrScheme');
63
+ if (!clrScheme) return { ...DEFAULT_SCHEME };
64
+
65
+ const scheme = { ...DEFAULT_SCHEME };
66
+ for (const name of Object.keys(DEFAULT_SCHEME)) {
67
+ const slot = child(clrScheme, `a:${name}`);
68
+ const rgb = extractRgbFromColorNode(slot);
69
+ if (rgb) scheme[name] = rgb;
70
+ }
71
+ return scheme;
72
+ }
73
+
74
+ /**
75
+ * @param {object|null|undefined} colorNode a:srgbClr / a:schemeClr 的父级
76
+ * @returns {string|null}
77
+ */
78
+ function extractRgbFromColorNode(colorNode) {
79
+ if (!colorNode) return null;
80
+
81
+ const srgb = child(colorNode, 'a:srgbClr');
82
+ if (srgb) {
83
+ const val = attr(srgb, 'val');
84
+ return val ? val.toUpperCase() : null;
85
+ }
86
+
87
+ const sys = child(colorNode, 'a:sysClr');
88
+ if (sys) {
89
+ const last = attr(sys, 'lastClr');
90
+ return last ? applyColorModifiers(last.toUpperCase(), sys) : null;
91
+ }
92
+
93
+ return null;
94
+ }
95
+
96
+ /**
97
+ * @param {object|null|undefined} fillNode a:solidFill | a:gradFill 等
98
+ * @param {Record<string, string>} scheme
99
+ * @returns {{ color: string|null, degraded: boolean }}
100
+ */
101
+ function resolveFillColor(fillNode, scheme) {
102
+ if (!fillNode) return { color: null, degraded: false };
103
+
104
+ const solid =
105
+ fillNode.tag === 'a:solidFill' ? fillNode : child(fillNode, 'a:solidFill');
106
+ if (solid) {
107
+ return { color: resolveColorFromContainer(solid, scheme), degraded: false };
108
+ }
109
+
110
+ const grad =
111
+ fillNode.tag === 'a:gradFill' ? fillNode : child(fillNode, 'a:gradFill');
112
+ if (grad) {
113
+ const gs = firstGradientStop(grad);
114
+ return {
115
+ color: gs ? resolveColorFromContainer(gs, scheme) : null,
116
+ degraded: true,
117
+ };
118
+ }
119
+
120
+ const ptn =
121
+ fillNode.tag === 'a:ptnFill' ? fillNode : child(fillNode, 'a:ptnFill');
122
+ if (ptn) {
123
+ const fgClr = child(ptn, 'a:fgClr');
124
+ return {
125
+ color: fgClr ? resolveColorFromContainer(fgClr, scheme) : null,
126
+ degraded: true,
127
+ };
128
+ }
129
+
130
+ return { color: null, degraded: false };
131
+ }
132
+
133
+ /**
134
+ * 渐变色标按 pos 升序,取最小 pos 对应色标(OOXML 0–100000)
135
+ * @param {object} grad a:gradFill
136
+ * @returns {object|null}
137
+ */
138
+ function firstGradientStop(grad) {
139
+ const gsLst = child(grad, 'a:gsLst');
140
+ if (!gsLst) return null;
141
+ const stops = children(gsLst, 'a:gs');
142
+ if (!stops.length) return null;
143
+ return [...stops].sort(
144
+ (a, b) => parseInt(attr(a, 'pos') ?? '0', 10) - parseInt(attr(b, 'pos') ?? '0', 10)
145
+ )[0];
146
+ }
147
+
148
+ /**
149
+ * @param {object|null|undefined} container 含 a:srgbClr / a:schemeClr
150
+ * @param {Record<string, string>} scheme
151
+ * @returns {string|null}
152
+ */
153
+ function resolveColorFromContainer(container, scheme) {
154
+ if (!container) return null;
155
+
156
+ const srgb = child(container, 'a:srgbClr');
157
+ if (srgb) {
158
+ const val = attr(srgb, 'val');
159
+ if (!val) return null;
160
+ return applyColorModifiers(val.toUpperCase(), srgb);
161
+ }
162
+
163
+ const schemeClr = child(container, 'a:schemeClr');
164
+ if (schemeClr) {
165
+ const name = attr(schemeClr, 'val');
166
+ const base = name ? scheme[name] ?? DEFAULT_SCHEME[name] ?? null : null;
167
+ return base ? applyColorModifiers(base, schemeClr) : null;
168
+ }
169
+
170
+ const prstClr = child(container, 'a:prstClr');
171
+ if (prstClr) {
172
+ const val = attr(prstClr, 'val');
173
+ const base = val ? PRST_COLOR_MAP[val] ?? PRST_COLOR_MAP[val.toLowerCase()] ?? '000000' : null;
174
+ return base ? applyColorModifiers(base, prstClr) : null;
175
+ }
176
+
177
+ const sysClr = child(container, 'a:sysClr');
178
+ if (sysClr) {
179
+ const last = attr(sysClr, 'lastClr');
180
+ return last ? applyColorModifiers(last.toUpperCase(), sysClr) : null;
181
+ }
182
+
183
+ return extractRgbFromColorNode(container);
184
+ }
185
+
186
+ /**
187
+ * @param {string} hex 6 位 RGB
188
+ */
189
+ function hexToRgb(hex) {
190
+ const h = String(hex).replace(/^#/, '');
191
+ if (h.length !== 6) return { r: 0, g: 0, b: 0 };
192
+ return {
193
+ r: parseInt(h.slice(0, 2), 16),
194
+ g: parseInt(h.slice(2, 4), 16),
195
+ b: parseInt(h.slice(4, 6), 16),
196
+ };
197
+ }
198
+
199
+ /**
200
+ * @param {{ r: number, g: number, b: number }} rgb
201
+ */
202
+ function rgbToHex(rgb) {
203
+ const clamp = (n) => Math.max(0, Math.min(255, Math.round(n)));
204
+ return [rgb.r, rgb.g, rgb.b]
205
+ .map((c) => clamp(c).toString(16).padStart(2, '0'))
206
+ .join('')
207
+ .toUpperCase();
208
+ }
209
+
210
+ /**
211
+ * srgbClr / schemeClr 上的 lumMod / shade / tint 等修饰(OOXML 比例 1/100000)
212
+ * @param {string} hex
213
+ * @param {object} clrNode
214
+ */
215
+ function applyColorModifiers(hex, clrNode) {
216
+ let { r, g, b } = hexToRgb(hex);
217
+
218
+ const lumMod = child(clrNode, 'a:lumMod');
219
+ if (lumMod) {
220
+ const pct = parseInt(attr(lumMod, 'val') ?? '100000', 10) / 100000;
221
+ r *= pct;
222
+ g *= pct;
223
+ b *= pct;
224
+ }
225
+
226
+ const lumOff = child(clrNode, 'a:lumOff');
227
+ if (lumOff) {
228
+ const pct = parseInt(attr(lumOff, 'val') ?? '0', 10) / 100000;
229
+ const delta = 255 * pct;
230
+ r += delta;
231
+ g += delta;
232
+ b += delta;
233
+ }
234
+
235
+ const shade = child(clrNode, 'a:shade');
236
+ if (shade) {
237
+ const pct = parseInt(attr(shade, 'val') ?? '0', 10) / 100000;
238
+ r *= pct;
239
+ g *= pct;
240
+ b *= pct;
241
+ }
242
+
243
+ const tint = child(clrNode, 'a:tint');
244
+ if (tint) {
245
+ const pct = parseInt(attr(tint, 'val') ?? '0', 10) / 100000;
246
+ r += (255 - r) * pct;
247
+ g += (255 - g) * pct;
248
+ b += (255 - b) * pct;
249
+ }
250
+
251
+ return rgbToHex({ r, g, b });
252
+ }
253
+
254
+ /** @deprecated 使用 applyColorModifiers */
255
+ const applySchemeClrModifiers = applyColorModifiers;
256
+
257
+ /**
258
+ * @param {Record<string, string>} scheme
259
+ * @param {object|null|undefined} clrNode
260
+ * @returns {string|null}
261
+ */
262
+ function resolveColor(scheme, clrNode) {
263
+ return resolveColorFromContainer(clrNode, scheme);
264
+ }
265
+
266
+ module.exports = {
267
+ DEFAULT_SCHEME,
268
+ loadColorScheme,
269
+ resolveFillColor,
270
+ resolveColor,
271
+ resolveColorFromContainer,
272
+ firstGradientStop,
273
+ applyColorModifiers,
274
+ applySchemeClrModifiers,
275
+ };
package/lib/convert.js CHANGED
@@ -46,8 +46,11 @@ async function convert(filePath, options = {}) {
46
46
  }
47
47
 
48
48
  const stat = fs.statSync(resolvedPath);
49
- if (stat.size > (options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE)) {
50
- // 大文件流式解析 实现阶段补充
49
+ const maxSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
50
+ if (stat.size > maxSize) {
51
+ throw new Error(
52
+ `源文件超过大小限制 (${stat.size} > ${maxSize} 字节)。请拆分 PPT 或使用 --max-file-size 提高上限。`
53
+ );
51
54
  }
52
55
 
53
56
  const outputDir = path.resolve(options.outputDir ?? DEFAULT_OUTPUT);
@@ -59,8 +62,8 @@ async function convert(filePath, options = {}) {
59
62
  const parsed = {};
60
63
  for (const [entryPath, content] of archive.files) {
61
64
  if (content === '__binary__') continue;
62
- if (entryPath.endsWith('.xml') || entryPath.endsWith('.rels')) {
63
- parsed[entryPath] = await parseXml(content);
65
+ if (/\.(xml|rels)$/i.test(entryPath)) {
66
+ parsed[entryPath] = parseXml(content);
64
67
  }
65
68
  }
66
69
 
@@ -68,15 +71,16 @@ async function convert(filePath, options = {}) {
68
71
  const ctx = { relIndex, parsed, sourcePath: resolvedPath };
69
72
  const entities = extractEntities(ctx);
70
73
  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
74
 
75
+ // packageMedia 会就地更新 ir 中的 mediaPath,generateScript 必须在其之后调用
76
76
  await packageMedia({ archive, ir }, outputDir, {
77
77
  noMedia: options.noMedia,
78
78
  });
79
79
 
80
+ const script = generateScript(ir, options);
81
+ const scriptPath = path.join(outputDir, 'output.js');
82
+ fs.writeFileSync(scriptPath, script, 'utf8');
83
+
80
84
  const log = buildConversionLog(resolvedPath, stat, entities, ir);
81
85
  writeConversionLog(outputDir, log);
82
86
  writeOutputReadme(outputDir, {