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/lib/extractor.js CHANGED
@@ -2,19 +2,22 @@
2
2
  * ③ 实体提取器
3
3
  */
4
4
  const path = require('path');
5
- const { asArray, attr, child, textContent } = require('./xml-utils');
6
- const { boundsFromXfrm } = require('./utils/bounds');
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('./utils/color');
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('./utils/bounds').Bounds} bounds
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 = child(doc, 'p:sld');
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 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
- }
155
+ const bg = child(cSld, 'p:bg');
156
+ if (!bg) return null;
130
157
 
131
- function walkSpTree(spTree, ctx) {
132
- for (const sp of asArray(spTree['p:sp'])) {
133
- extractShape(sp, ctx);
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
- 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);
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
- 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 类型');
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 inner = child(grp, 'p:spTree');
164
- if (!inner) return;
165
- walkSpTree(inner, {
205
+ const chOff = readChOff(grpXfrm);
206
+
207
+ // p:grpSp 子节点直接挂在 grp 上,没有 p:spTree 包装
208
+ walkSpTree(grp, {
166
209
  ...ctx,
167
- offset: { x: ctx.offset.x + off.x, y: ctx.offset.y + off.y },
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 === 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;
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
- const shapeName = PRST_TO_SHAPE[prst];
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: { type: 'RECTANGLE', fill: fillColor, line: lineColor },
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: { type: shapeName, fill: fillColor, line: lineColor },
231
- degradeReason: fillDegraded ? '渐变填充退化为纯色' : undefined,
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 bounds = boundsFromXfrm(child(spPr, 'a:xfrm'), ctx.offset);
262
- const { color: lineColor } = resolveFillColor(child(spPr, 'a:ln'), ctx.scheme);
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: { type: 'LINE', fill: null, line: lineColor },
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
- * @param {object|null|undefined} pPr
427
+ * 组合内部坐标系原点(EMU)
428
+ * @param {object|null|undefined} xfrm
322
429
  */
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;
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
- if (options.noMedia) return [];
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
- const name = path.basename(entryPath); // TODO: 重名冲突时生成唯一文件名
33
- const dest = path.join(mediaDir, name);
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(path.join('media', name));
85
+ written.push(relPath);
37
86
  }
38
- return written;
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 = { packageMedia, writeConversionLog, writeOutputReadme };
123
+ module.exports = {
124
+ packageMedia,
125
+ applyMediaPathsToIR,
126
+ uniqueMediaFileName,
127
+ writeConversionLog,
128
+ writeOutputReadme,
129
+ };