dsh-ppt-studio 1.1.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/LICENSE +21 -0
- package/README.md +480 -0
- package/agent-presets/ppt/agent.cordis.yml +308 -0
- package/agent-presets/ppt/preset.yml +3 -0
- package/cordis.patch.yml +29 -0
- package/docs/00-/350/256/276/350/256/241/346/272/220.md +537 -0
- package/docs/01-/351/234/200/346/261/202/344/270/216/347/233/256/346/240/207.md +136 -0
- package/docs/02-/346/212/200/346/234/257/346/212/245/345/221/212.md +483 -0
- package/docs/03-/346/233/264/346/226/260/346/227/245/345/277/227.md +2898 -0
- package/docs/04-/350/267/257/347/272/277/345/233/276/344/270/216/351/207/214/347/250/213/347/242/221.md +113 -0
- package/docs/05-/350/277/255/344/273/243/346/265/201/347/250/213.md +210 -0
- package/docs/06-/350/257/204/345/256/241/344/270/216/346/265/213/350/257/225.md +296 -0
- package/docs/fidelity-report.md +225 -0
- package/examples/fx/fx-mini/deck.yaml +39 -0
- package/examples/fx/fx-mini/pages/01.yaml +6 -0
- package/examples/fx/fx-mini/pages/02.yaml +2 -0
- package/examples/fx/fx-mini/pages/03.yaml +6 -0
- package/examples/fx/fx-pro/deck.yaml +48 -0
- package/examples/fx/fx-pro/pages/01_cover.yaml +40 -0
- package/examples/fx/fx-pro/pages/02_arch.yaml +55 -0
- package/examples/fx/fx-pro/pages/03_table.yaml +20 -0
- package/examples/fx/fx-pro/pages/04_chart.yaml +40 -0
- package/examples/fx/fx-pro/pages/05_timeline.yaml +71 -0
- package/examples/fx/fx-pro/pages/06_longtext.yaml +13 -0
- package/examples/fx/fx-pro/pages/07_safearea.yaml +33 -0
- package/examples/fx/fx-pro/pages/08_contrast.yaml +22 -0
- package/examples/fx/fx-pro/pages/09_custgeom.yaml +32 -0
- package/examples/fx/fx-pro/pages/10_numbers.yaml +25 -0
- package/examples/fx/fx-pro/pages/11_section.yaml +20 -0
- package/examples/fx/fx-pro/pages/12_summary.yaml +57 -0
- package/examples/fx/fx-pro/preview/01_pages_01_cover.html +1 -0
- package/examples/fx/fx-pro/preview/02_pages_02_arch.html +3 -0
- package/examples/fx/fx-pro/preview/03_pages_03_table.html +1 -0
- package/examples/fx/fx-pro/preview/04_pages_04_chart.html +3 -0
- package/examples/fx/fx-pro/preview/05_pages_05_timeline.html +2 -0
- package/examples/fx/fx-pro/preview/06_pages_06_longtext.html +4 -0
- package/examples/fx/fx-pro/preview/07_pages_07_safearea.html +2 -0
- package/examples/fx/fx-pro/preview/08_pages_08_contrast.html +1 -0
- package/examples/fx/fx-pro/preview/09_pages_09_custgeom.html +1 -0
- package/examples/fx/fx-pro/preview/10_pages_10_numbers.html +1 -0
- package/examples/fx/fx-pro/preview/11_pages_11_section.html +1 -0
- package/examples/fx/fx-pro/preview/12_pages_12_summary.html +1 -0
- package/examples/fx/fx-pro/preview/deck.html +10 -0
- package/examples/fx/fx-pro/preview/layout.json +2325 -0
- package/examples/fx/fx-pro/preview/measured.json +814 -0
- package/examples/fx/fx-pro.pptx +0 -0
- package/examples/fx/ren-single/01.png +0 -0
- package/examples/fx/ren-spliced/01.png +0 -0
- package/examples/fx/ren-spliced/03.png +0 -0
- package/examples/fx/seed.pptx +0 -0
- package/examples/fx/single.pptx +0 -0
- package/examples/fx/spliced.pptx +0 -0
- package/examples/smoke/deck.yaml +26 -0
- package/examples/smoke/imported/deck.yaml +30 -0
- package/examples/smoke/imported/import-styles.json +126 -0
- package/examples/smoke/imported/pages/slide_01.yaml +31 -0
- package/examples/smoke/imported/pages/slide_02.yaml +38 -0
- package/examples/smoke/imported/pages/slide_03.yaml +55 -0
- package/examples/smoke/imported/preview-rt/01_pages_slide_01.html +1 -0
- package/examples/smoke/imported/preview-rt/02_pages_slide_02.html +1 -0
- package/examples/smoke/imported/preview-rt/03_pages_slide_03.html +1 -0
- package/examples/smoke/imported/preview-rt/deck.html +1 -0
- package/examples/smoke/imported/preview-rt/layout.json +434 -0
- package/examples/smoke/imported/reference/previews/01.png +0 -0
- package/examples/smoke/imported/reference/previews/02.png +0 -0
- package/examples/smoke/imported/reference/previews/03.png +0 -0
- package/examples/smoke/imported/source.pptx +0 -0
- package/examples/smoke/media-test.png +0 -0
- package/examples/smoke/out-smoke.pptx +0 -0
- package/examples/smoke/pages/01_cover.yaml +27 -0
- package/examples/smoke/pages/02_content.yaml +42 -0
- package/examples/smoke/pages/03_chart.yaml +35 -0
- package/examples/smoke/preview/01_pages_01_cover.html +1 -0
- package/examples/smoke/preview/02_pages_02_content.html +1 -0
- package/examples/smoke/preview/03_pages_03_chart.html +1 -0
- package/examples/smoke/preview/deck.html +1 -0
- package/examples/smoke/preview/layout.json +378 -0
- package/lib/autodeclare.js +40 -0
- package/lib/commands.js +161 -0
- package/lib/crosscheck.js +300 -0
- package/lib/imgmeta.js +55 -0
- package/lib/index.js +239 -0
- package/lib/measurement.js +209 -0
- package/lib/msrender.js +123 -0
- package/lib/pptd/export-pptx.js +686 -0
- package/lib/pptd/import-pptx.js +774 -0
- package/lib/pptd/layout.js +209 -0
- package/lib/pptd/render-html.js +254 -0
- package/lib/pptd/schema.js +518 -0
- package/lib/pptd/svgCharts.js +147 -0
- package/lib/pptxPy.js +205 -0
- package/lib/preset-delivery.js +195 -0
- package/lib/preview-server.js +164 -0
- package/lib/router.js +165 -0
- package/lib/scaffold.js +315 -0
- package/lib/skill.js +174 -0
- package/lib/splice.js +219 -0
- package/lib/state.js +88 -0
- package/lib/surgical.js +369 -0
- package/lib/templates.js +400 -0
- package/lib/tools.js +955 -0
- package/lib/verify.js +828 -0
- package/lib/xmljs.js +112 -0
- package/lib/zips.js +117 -0
- package/package.json +81 -0
- package/scripts/audit-manual-facts.mjs +294 -0
- package/scripts/audit-template-fidelity.mjs +162 -0
- package/scripts/bench-m2.mjs +48 -0
- package/scripts/build-templates.mjs +63 -0
- package/scripts/build.mjs +14 -0
- package/scripts/build.sh +7 -0
- package/scripts/check-lib-freshness.mjs +70 -0
- package/scripts/check-preset.mjs +189 -0
- package/scripts/check-tracked-lf.mjs +84 -0
- package/scripts/dbg-import.mjs +13 -0
- package/scripts/dbg-media.mjs +10 -0
- package/scripts/dbg-tpl-inventory.mjs +23 -0
- package/scripts/dump-workflow-baseline.mjs +38 -0
- package/scripts/e2e-1.0.mjs +129 -0
- package/scripts/eval-skills-blind.mjs +91 -0
- package/scripts/eval-skills-score.mjs +129 -0
- package/scripts/export-orig-templates.ps1 +17 -0
- package/scripts/fixtures/workflow-baseline.json +300 -0
- package/scripts/fixtures.mjs +501 -0
- package/scripts/import-user-templates.mjs +60 -0
- package/scripts/install-desktop.mjs +312 -0
- package/scripts/install.mjs +209 -0
- package/scripts/migrate-templates-dualtrack.mjs +71 -0
- package/scripts/preflight-1.0.mjs +198 -0
- package/scripts/probe-asset-freshness.mjs +57 -0
- package/scripts/probe-effect-semantics.mjs +69 -0
- package/scripts/probe-reparse-safety.mjs +32 -0
- package/scripts/rebuild-templates-c11.mjs +69 -0
- package/scripts/regression-real.mjs +84 -0
- package/scripts/release-sync.mjs +425 -0
- package/scripts/shot-templates.mjs +49 -0
- package/scripts/smoke.mjs +2512 -0
- package/scripts/verify-bundle-install.mjs +281 -0
- package/scripts/verify-fresh-install.mjs +260 -0
- package/skills/ppt-studio-copy/SKILL.md +139 -0
- package/skills/ppt-studio-craft/SKILL.md +171 -0
- package/skills/ppt-studio-data/SKILL.md +173 -0
- package/skills/ppt-studio-manual/SKILL.md +104 -0
- package/templates/academic-white/deck.yaml +24 -0
- package/templates/academic-white/pages/_chart.yaml +43 -0
- package/templates/academic-white/pages/_compare.yaml +37 -0
- package/templates/academic-white/pages/_content.yaml +35 -0
- package/templates/academic-white/pages/_cover.yaml +28 -0
- package/templates/academic-white/pages/_ending.yaml +28 -0
- package/templates/academic-white/pages/_flow.yaml +61 -0
- package/templates/academic-white/preview.png +0 -0
- package/templates/academic-white/template.yaml +14 -0
- package/templates/business-blue/deck.yaml +28 -0
- package/templates/business-blue/pages/_chart.yaml +36 -0
- package/templates/business-blue/pages/_compare.yaml +45 -0
- package/templates/business-blue/pages/_content.yaml +80 -0
- package/templates/business-blue/pages/_cover.yaml +39 -0
- package/templates/business-blue/pages/_ending.yaml +32 -0
- package/templates/business-blue/pages/_flow.yaml +87 -0
- package/templates/business-blue/preview.png +0 -0
- package/templates/business-blue/template.yaml +14 -0
- package/templates/pitch-bold/deck.yaml +23 -0
- package/templates/pitch-bold/pages/_chart.yaml +32 -0
- package/templates/pitch-bold/pages/_compare.yaml +36 -0
- package/templates/pitch-bold/pages/_content.yaml +58 -0
- package/templates/pitch-bold/pages/_cover.yaml +31 -0
- package/templates/pitch-bold/pages/_ending.yaml +27 -0
- package/templates/pitch-bold/pages/_flow.yaml +46 -0
- package/templates/pitch-bold/preview.png +0 -0
- package/templates/pitch-bold/template.yaml +14 -0
- package/templates/tech-dark/deck.yaml +24 -0
- package/templates/tech-dark/pages/_chart.yaml +27 -0
- package/templates/tech-dark/pages/_compare.yaml +38 -0
- package/templates/tech-dark/pages/_content.yaml +51 -0
- package/templates/tech-dark/pages/_cover.yaml +30 -0
- package/templates/tech-dark/pages/_ending.yaml +25 -0
- package/templates/tech-dark/pages/_flow.yaml +80 -0
- package/templates/tech-dark/preview.png +0 -0
- package/templates/tech-dark/template.yaml +14 -0
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PPTD → PPTX 导出器(主引擎):生成最小可打开的 OOXML。
|
|
3
|
+
* - 元素:text / shape(rect,ellipse,triangle) / line(箭头) / image / table
|
|
4
|
+
* / chart(矢量拼绘:bar=矩形、line=连接线+圆点、pie=饼形)
|
|
5
|
+
* - 文本自动 fit:与 verify 同一度量与阈值(overflow > 1px 才缩),按比例缩字号;
|
|
6
|
+
* 下限语义(2026-09-06 用户拍板):theme.minFontSize 显式 = 用户给出的字号下限(严格遵守,且不超过原字号);
|
|
7
|
+
* 未设置 = 无强制下限(仅 60% 原字号防荒谬保底),绝不升字;到下限仍溢出记 floorHit。
|
|
8
|
+
* 验证通过 ⇒ 导出不缩字(反馈 E2:双度量差已对齐)。
|
|
9
|
+
* - 1px = 1pt;EMU = pt × 12700。
|
|
10
|
+
* - 讲稿(2026-09-14 新增):页面 `notes:` 文本 → 标准 pptx 备注页(notesSlide + notesMaster)。
|
|
11
|
+
* 无 `notes:` 的工程**不产出任何备注部件**(产物与旧实现一致 ⇒ 老工程零变化);parity 里 notesExp/notesOut 自证。
|
|
12
|
+
*/
|
|
13
|
+
import { writeFile, readFile } from 'node:fs/promises'
|
|
14
|
+
import { join, isAbsolute } from 'node:path'
|
|
15
|
+
import { zipWrite } from '../zips.js'
|
|
16
|
+
import { normalizePage } from './layout.js'
|
|
17
|
+
import { chartData, chartColors, chartIssue } from './svgCharts.js'
|
|
18
|
+
|
|
19
|
+
const EMU = 12700
|
|
20
|
+
const xm = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
21
|
+
const emu = (v) => Math.round(v * EMU)
|
|
22
|
+
const hex = (c) => (c ?? '#000000').replace('#', '').toUpperCase().slice(0, 6).padEnd(6, '0')
|
|
23
|
+
|
|
24
|
+
let UID = 1
|
|
25
|
+
const nid = () => UID++
|
|
26
|
+
|
|
27
|
+
// A2 修复(反馈二):缺失媒体兜底——1×1 白色 PNG(页面引用但 media/ 无文件时不再 ENOENT 硬失败)
|
|
28
|
+
const TINY_PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64')
|
|
29
|
+
|
|
30
|
+
export async function exportPptx(ctx, { out = 'out.pptx', engine = 'pptd' } = {}) {
|
|
31
|
+
const report = { autoFit: [], warnings: [], chartInfos: [] }
|
|
32
|
+
const slides = []
|
|
33
|
+
// 连线自证(2026-09-14 真实反馈:预览对、PowerPoint 里线条镜像/×掉一条):
|
|
34
|
+
// straightConnector1 在 OOXML 里**只按包围盒的左上→右下**绘制,方向必须靠 flipH/flipV 表达;
|
|
35
|
+
// 漏写 → 反向斜率/镜像(cross1×cross2 会重合成一条线)。这里逐条从写出的 XML 反推端点自证。
|
|
36
|
+
const lineProof = { exp: 0, out: 0, wrong: [] }
|
|
37
|
+
UID = 1000
|
|
38
|
+
|
|
39
|
+
for (const page of ctx.pages) {
|
|
40
|
+
const els = normalizePage(page, ctx)
|
|
41
|
+
const shapes = []
|
|
42
|
+
const media = []
|
|
43
|
+
const mediaSeen = new Map()
|
|
44
|
+
const counts = { table: 0, image: 0, chart: 0 }
|
|
45
|
+
const addMedia = (srcPath) => {
|
|
46
|
+
if (mediaSeen.has(srcPath)) return mediaSeen.get(srcPath)
|
|
47
|
+
const rId = `rId${media.length + 2}`
|
|
48
|
+
mediaSeen.set(srcPath, rId)
|
|
49
|
+
media.push({ srcPath, rId })
|
|
50
|
+
return rId
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const el of els) {
|
|
54
|
+
switch (el.type) {
|
|
55
|
+
case 'text': shapes.push(textSp(el, report, ctx.minFontSize)); break
|
|
56
|
+
case 'shape': shapes.push(shapeSp(el)); break
|
|
57
|
+
case 'line': {
|
|
58
|
+
lineProof.exp++
|
|
59
|
+
shapes.push(connectorSp(el, lineProof))
|
|
60
|
+
break
|
|
61
|
+
}
|
|
62
|
+
case 'image': {
|
|
63
|
+
counts.image++
|
|
64
|
+
const rId = addMedia(el.src)
|
|
65
|
+
shapes.push(picSp(el, rId))
|
|
66
|
+
break
|
|
67
|
+
}
|
|
68
|
+
case 'table': counts.table++; shapes.push(tableFrame(el)); break
|
|
69
|
+
case 'chart': {
|
|
70
|
+
counts.chart++
|
|
71
|
+
const issue = chartIssue(el.chart)
|
|
72
|
+
if (issue) report.chartInfos.push(`第 ${page.index + 1} 页(${page.name})${el.id}: ${issue}`)
|
|
73
|
+
shapes.push(...chartSp(el))
|
|
74
|
+
break
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 页面背景(原生 p:bg:solid 或 image,背景图进入本页 media 列表)
|
|
80
|
+
const bg = resolveBg(page.page.background, ctx)
|
|
81
|
+
if (bg?.kind === 'image') {
|
|
82
|
+
bg.rId = addMedia(bg.src)
|
|
83
|
+
bg.xml = bg.xml.replace('BGPLACEHOLDER', bg.rId)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
slides.push({
|
|
87
|
+
page,
|
|
88
|
+
shapes,
|
|
89
|
+
media,
|
|
90
|
+
counts,
|
|
91
|
+
// 讲稿:只有非空字符串才产备注页(空/未写 = 与旧行为完全一致,不生成部件)
|
|
92
|
+
notes: typeof page.page.notes === 'string' && page.page.notes.trim() ? page.page.notes.trim() : null,
|
|
93
|
+
notesNo: 0,
|
|
94
|
+
spTree: `<p:spTree>${spTreeHeader()}${shapes.join('')}</p:spTree>`,
|
|
95
|
+
bg,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
// 备注页编号:只为有讲稿的页分配(notesSlide1..N;无讲稿的页没有关系,也没有部件)
|
|
99
|
+
let notesCount = 0
|
|
100
|
+
for (const s of slides) if (s.notes) s.notesNo = ++notesCount
|
|
101
|
+
|
|
102
|
+
const files = {}
|
|
103
|
+
files['[Content_Types].xml'] = contentTypes(slides)
|
|
104
|
+
files['_rels/.rels'] = rootRels()
|
|
105
|
+
files['docProps/core.xml'] = coreProps(ctx)
|
|
106
|
+
files['docProps/app.xml'] = appProps(ctx)
|
|
107
|
+
files['ppt/presentation.xml'] = presentationXml(ctx, slides)
|
|
108
|
+
files['ppt/_rels/presentation.xml.rels'] = presentationRels(slides)
|
|
109
|
+
files['ppt/slideMasters/slideMaster1.xml'] = slideMasterXml()
|
|
110
|
+
files['ppt/slideMasters/_rels/slideMaster1.xml.rels'] = slideMasterRels()
|
|
111
|
+
files['ppt/slideLayouts/slideLayout1.xml'] = slideLayoutXml()
|
|
112
|
+
files['ppt/slideLayouts/_rels/slideLayout1.xml.rels'] = slideLayoutRels()
|
|
113
|
+
files['ppt/theme/theme1.xml'] = themeXml()
|
|
114
|
+
if (notesCount > 0) {
|
|
115
|
+
files['ppt/notesMasters/notesMaster1.xml'] = notesMasterXml()
|
|
116
|
+
files['ppt/notesMasters/_rels/notesMaster1.xml.rels'] = notesMasterRels()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (let i = 0; i < slides.length; i++) {
|
|
120
|
+
const s = slides[i]
|
|
121
|
+
const n = i + 1
|
|
122
|
+
files[`ppt/slides/slide${n}.xml`] = slideXml(s)
|
|
123
|
+
// 备注页关系:rId 取媒体之后的下一个空闲号(媒体用 rId2..rId(1+media))
|
|
124
|
+
const notesRel = s.notes ? `<Relationship Id="rId${s.media.length + 2}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide" Target="../notesSlides/notesSlide${s.notesNo}.xml"/>` : ''
|
|
125
|
+
if (s.notes) {
|
|
126
|
+
files[`ppt/notesSlides/notesSlide${s.notesNo}.xml`] = notesSlideXml(s.notes)
|
|
127
|
+
// 关键:notesSlide **必须自带 rels**(→ notesMaster + 回指本页幻灯)。
|
|
128
|
+
// 2026-09-14 实测:缺这个 rels 时 python-pptx 能读,**PowerPoint 直接报"文件或目录损坏"**——
|
|
129
|
+
// 参照 python-pptx 默认模板(PowerPoint 原生产物)的结构:rId1=notesMaster、rId2=slide。
|
|
130
|
+
files[`ppt/notesSlides/_rels/notesSlide${s.notesNo}.xml.rels`] = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
|
131
|
+
+ '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster" Target="../notesMasters/notesMaster1.xml"/>'
|
|
132
|
+
+ `<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="../slides/slide${n}.xml"/>`
|
|
133
|
+
+ '</Relationships>'
|
|
134
|
+
}
|
|
135
|
+
const srels = [
|
|
136
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>',
|
|
137
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">',
|
|
138
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>',
|
|
139
|
+
...s.media.map((m) => `<Relationship Id="${m.rId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/${m.srcPath.split(/[\\/]/).pop()}"/>`),
|
|
140
|
+
notesRel,
|
|
141
|
+
'</Relationships>',
|
|
142
|
+
].join('')
|
|
143
|
+
files[`ppt/slides/_rels/slide${n}.xml.rels`] = srels
|
|
144
|
+
for (const m of s.media) {
|
|
145
|
+
const bn = m.srcPath.split(/[\\/]/).pop()
|
|
146
|
+
try {
|
|
147
|
+
files[`ppt/media/${bn}`] = await readFile(join(ctx.dir, m.srcPath))
|
|
148
|
+
} catch {
|
|
149
|
+
report.mediaPlaceholders = report.mediaPlaceholders ?? []
|
|
150
|
+
report.mediaPlaceholders.push(bn)
|
|
151
|
+
files[`ppt/media/${bn}`] = TINY_PNG
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// P1 防线(测试反馈 ★):导出产物 parity 回读——
|
|
157
|
+
// layout 元素计数 == OOXML 计数 + 结构不变量(graphicFrame 无嵌套 a:xfrm = PowerPoint 弃帧根源)
|
|
158
|
+
let tablesExp = 0, tablesOut = 0, imagesExp = 0, imagesOut = 0, illegal = 0
|
|
159
|
+
for (let i = 0; i < slides.length; i++) {
|
|
160
|
+
const s = slides[i]
|
|
161
|
+
tablesExp += s.counts.table
|
|
162
|
+
imagesExp += s.counts.image
|
|
163
|
+
const xml = files[`ppt/slides/slide${i + 1}.xml`]
|
|
164
|
+
const txt = typeof xml === 'string' ? xml : String(xml)
|
|
165
|
+
tablesOut += (txt.match(/graphicData uri="http:\/\/schemas\.openxmlformats\.org\/drawingml\/2006\/table"/g) ?? []).length
|
|
166
|
+
imagesOut += (txt.match(/<p:pic>/g) ?? []).length
|
|
167
|
+
if (txt.includes('<p:xfrm><a:xfrm>')) illegal++
|
|
168
|
+
}
|
|
169
|
+
// 备注 parity:页里有 notes 的页数 == 包里的 notesSlide 部件数;且每张 notesSlide 都被对应幻灯 rels 引用
|
|
170
|
+
const notesExp = slides.filter((s) => s.notes).length
|
|
171
|
+
const notesParts = Object.keys(files).filter((k) => /^ppt\/notesSlides\/notesSlide\d+\.xml$/.test(k))
|
|
172
|
+
const notesOut = notesParts.length
|
|
173
|
+
const notesRelOk = slides.filter((s) => s.notes).every((s) => String(files[`ppt/slides/_rels/slide${slides.indexOf(s) + 1}.xml.rels`]).includes(`notesSlide${s.notesNo}.xml`))
|
|
174
|
+
report.parity = {
|
|
175
|
+
tablesExp, tablesOut, imagesExp, imagesOut, illegalFrames: illegal,
|
|
176
|
+
linesExp: lineProof.exp, linesOut: lineProof.out, linesWrong: lineProof.wrong.length,
|
|
177
|
+
notesExp, notesOut,
|
|
178
|
+
ok: tablesExp === tablesOut && imagesExp === imagesOut && illegal === 0 &&
|
|
179
|
+
lineProof.exp === lineProof.out && lineProof.wrong.length === 0 &&
|
|
180
|
+
notesExp === notesOut && notesRelOk,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// out:绝对路径原样使用;相对路径相对 deck 目录(反馈 E1 ★)
|
|
184
|
+
const outPath = isAbsolute(out) ? out : join(ctx.dir, out)
|
|
185
|
+
await writeFile(outPath, zipWrite(files))
|
|
186
|
+
report.file = outPath
|
|
187
|
+
report.slides = slides.length
|
|
188
|
+
report.engine = engine
|
|
189
|
+
return report
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function spTreeHeader() {
|
|
193
|
+
return '<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>'
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function xfrm(x, y, w, h, rot = 0, flips = '') {
|
|
197
|
+
const r = rot ? ` rot="${Math.round(rot * 60000)}"` : ''
|
|
198
|
+
return '<a:xfrm' + flips + r + '><a:off x="' + emu(x) + '" y="' + emu(y) + '"/><a:ext cx="' + emu(w) + '" cy="' + emu(h) + '"/></a:xfrm>'
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 描边属性。`arrow` 的 `<a:tailEnd>` **必须写在 `<a:ln>` 内部**(2026-09-14 修复)——
|
|
203
|
+
* 旧实现把它拼在 `</a:ln>` 之后,PowerPoint 直接忽略 ⇒ 箭头全部消失(预览有、成品没有)。
|
|
204
|
+
* OOXML 子元素顺序:fill → prstDash → join → headEnd → tailEnd。
|
|
205
|
+
*/
|
|
206
|
+
function lineSpPr(line, arrow = false) {
|
|
207
|
+
const w = Math.round((line?.width ?? 1) * EMU)
|
|
208
|
+
const tail = arrow ? '<a:tailEnd type="triangle" w="med" len="med"/>' : ''
|
|
209
|
+
return '<a:ln w="' + w + '"><a:solidFill><a:srgbClr val="' + hex(line?.color ?? '#000000') + '"/></a:solidFill>' + tail + '</a:ln>'
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── text ─────────────────────────────────────────────────────────────────
|
|
213
|
+
const FIT_TOL = 1 // 与 verify 的 TOL 一致:verify 通过 ⇒ 导出不缩字(反馈 E2 ★★)
|
|
214
|
+
|
|
215
|
+
function textSp(el, report, minFontSize) {
|
|
216
|
+
const s = el.style
|
|
217
|
+
const m = el.metrics
|
|
218
|
+
const id = nid()
|
|
219
|
+
const origSize = s.fontSize
|
|
220
|
+
let fontSize = origSize
|
|
221
|
+
const boxH = el.bounds.h
|
|
222
|
+
// 缩字下限语义(2026-09-06 修订,用户拍板):下限 = 用户指令——
|
|
223
|
+
// ① 用户给出最小字号(theme.minFontSize 显式)→ 严格遵守(且不超过原字号,绝不升字);
|
|
224
|
+
// ② 未给出 → 不设强制(仅防荒谬:60% 原字号保底、最低 6pt);绝不高于原字号。
|
|
225
|
+
const floor = typeof minFontSize === 'number' && minFontSize > 0
|
|
226
|
+
? Math.min(origSize, minFontSize)
|
|
227
|
+
: Math.max(6, Math.round(origSize * 0.6))
|
|
228
|
+
if (s.wrap !== false && m.overflowY > FIT_TOL) {
|
|
229
|
+
const fit = Math.max(1, Math.floor(fontSize * Math.min(0.95, (boxH / Math.max(1, m.textH)) * 0.95)))
|
|
230
|
+
if (fit < fontSize) {
|
|
231
|
+
const floorHit = fit < floor
|
|
232
|
+
fontSize = floorHit ? floor : fit
|
|
233
|
+
report.autoFit.push({ id: el.id, from: origSize, to: fontSize, floorHit })
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const rPr =
|
|
237
|
+
'<a:rPr lang="zh-CN" sz="' + Math.round(fontSize * 100) + '"' + (s.bold ? ' b="1"' : '') + (s.italic ? ' i="1"' : '') +
|
|
238
|
+
'><a:solidFill><a:srgbClr val="' + hex(s.color ?? '#000000') + '"/></a:solidFill>' +
|
|
239
|
+
(s.fontFamily ? '<a:latin typeface="' + xm(s.fontFamily) + '"/><a:ea typeface="' + xm(s.fontFamily) + '"/>' : '') +
|
|
240
|
+
'</a:rPr>'
|
|
241
|
+
const body = String(el.content?.text ?? '').split(/\n/).map((line) => {
|
|
242
|
+
return '<a:p><a:pPr algn="' + alignOf(s.align) + '" lvl="0"/><a:r>' + rPr + '<a:t>' + xm(line) + '</a:t></a:r></a:p>'
|
|
243
|
+
}).join('')
|
|
244
|
+
const bodyPr = s.wrap === false
|
|
245
|
+
? '<a:bodyPr wrap="none" lIns="0" rIns="0" tIns="0" bIns="0"/>'
|
|
246
|
+
: '<a:bodyPr wrap="square" lIns="0" rIns="0" tIns="0" bIns="0"/>'
|
|
247
|
+
const sp = '<p:sp><p:nvSpPr><p:cNvPr id="' + id + '" name="' + xm(el.id) + '"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>'
|
|
248
|
+
return sp + '<p:spPr>' + xfrm(el.bounds.x, el.bounds.y, el.bounds.w, el.bounds.h) + '<a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>'
|
|
249
|
+
+ '<p:txBody>' + bodyPr + '<a:lstStyle/>' + body + '</p:txBody></p:sp>'
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function alignOf(a) { return a === 'right' ? 'r' : a === 'center' ? 'ctr' : 'l' }
|
|
253
|
+
|
|
254
|
+
// ── shapes ────────────────────────────────────────────────────────────────
|
|
255
|
+
function shapeSp(el) {
|
|
256
|
+
const id = nid()
|
|
257
|
+
// v0.9.1:kind 直通 OOXML prst(白名单 = schema SHAPE_KINDS);v0.11 候选 C:custGeom → custGeom 元素(几何零损失)
|
|
258
|
+
if (el.kind === 'custGeom') {
|
|
259
|
+
const fill = fillSpPr(el.fill)
|
|
260
|
+
const ln = el.line ? lineSpPr(el.line) : '<a:ln><a:noFill/></a:ln>'
|
|
261
|
+
return '<p:sp><p:nvSpPr><p:cNvPr id="' + id + '" name="' + xm(el.id) + '"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>'
|
|
262
|
+
+ '<p:spPr>' + xfrm(el.bounds.x, el.bounds.y, el.bounds.w, el.bounds.h, el.rotation ?? 0)
|
|
263
|
+
+ custGeomXml(el.path) + fill + ln + '</p:spPr>'
|
|
264
|
+
+ '<p:txBody><a:bodyPr rtlCol="0" anchor="ctr"/><a:lstStyle/><a:p/></p:txBody></p:sp>'
|
|
265
|
+
}
|
|
266
|
+
const prst = el.kind && /^[A-Za-z]+$/.test(el.kind) ? el.kind : 'rect'
|
|
267
|
+
const avLst = el.kind === 'roundRect' ? '<a:avLst><a:gd name="adj" fmla="val 8000"/></a:avLst>'
|
|
268
|
+
: el.kind === 'chevron' ? '<a:avLst><a:gd name="adj" fmla="val 25000"/></a:avLst>'
|
|
269
|
+
: el.kind === 'parallelogram' ? '<a:avLst><a:gd name="adj" fmla="val 25000"/></a:avLst>'
|
|
270
|
+
: '<a:avLst/>'
|
|
271
|
+
const fill = fillSpPr(el.fill)
|
|
272
|
+
const ln = el.line ? lineSpPr(el.line) : '<a:ln><a:noFill/></a:ln>'
|
|
273
|
+
return '<p:sp><p:nvSpPr><p:cNvPr id="' + id + '" name="' + xm(el.id) + '"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>'
|
|
274
|
+
+ '<p:spPr>' + xfrm(el.bounds.x, el.bounds.y, el.bounds.w, el.bounds.h, el.rotation ?? 0)
|
|
275
|
+
+ '<a:prstGeom prst="' + prst + '">' + avLst + '</a:prstGeom>' + fill + ln + '</p:spPr>'
|
|
276
|
+
+ '<p:txBody><a:bodyPr rtlCol="0" anchor="ctr"/><a:lstStyle/><a:p/></p:txBody></p:sp>'
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** v0.11 候选 C:PPTD path → custGeom XML(命令直通,几何零损失)。 */
|
|
280
|
+
function custGeomXml(path) {
|
|
281
|
+
const p = path ?? {}
|
|
282
|
+
const w = p.w ?? 100000
|
|
283
|
+
const h = p.h ?? 100000
|
|
284
|
+
const cmds = (p.commands ?? []).map((c) => {
|
|
285
|
+
if (c.cmd === 'close') return '<a:close/>'
|
|
286
|
+
if (c.cmd === 'arcTo') return '<a:arcTo wR="' + c.wR + '" hR="' + c.hR + '" stAng="' + c.stAng + '" swAng="' + c.swAng + '"/>'
|
|
287
|
+
const pts = (c.pts ?? []).map((pt) => '<a:pt x="' + pt[0] + '" y="' + pt[1] + '"/>').join('')
|
|
288
|
+
return '<a:' + c.cmd + '>' + pts + '</a:' + c.cmd + '>'
|
|
289
|
+
}).join('')
|
|
290
|
+
return '<a:custGeom><a:avLst/><a:gdLst/><a:ahLst/><a:cxnLst/><a:rect l="l" t="t" r="r" b="b"/>'
|
|
291
|
+
+ '<a:pathLst><a:path w="' + w + '" h="' + h + '">' + cmds + '</a:path></a:pathLst></a:custGeom>'
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** fill:'#hex' | {color, alpha?} | {type: gradient, stops: [{pos, color, alpha?}], angle} → OOXML fill。 */
|
|
295
|
+
function fillSpPr(fill) {
|
|
296
|
+
if (!fill) return '<a:noFill/>'
|
|
297
|
+
if (typeof fill === 'string') return '<a:solidFill>' + srgbClrXml(fill) + '</a:solidFill>'
|
|
298
|
+
if (fill.color) return '<a:solidFill>' + srgbClrXml(fill.color, fill.alpha) + '</a:solidFill>'
|
|
299
|
+
if (fill.type === 'gradient') {
|
|
300
|
+
const gs = (fill.stops ?? []).map((s) => {
|
|
301
|
+
const pos = Math.max(0, Math.min(100, Number(s.pos) || 0))
|
|
302
|
+
return '<a:gs pos="' + Math.round(pos * 1000) + '">' + srgbClrXml(s.color, s.alpha) + '</a:gs>'
|
|
303
|
+
}).join('')
|
|
304
|
+
if (!gs) return '<a:noFill/>'
|
|
305
|
+
const ang = fill.angle !== undefined ? Math.round(Math.max(0, ((Number(fill.angle) % 360) + 360) % 360) * 60000) : 5400000
|
|
306
|
+
return '<a:gradFill rotWithShape="1"><a:gsLst>' + gs + '</a:gsLst><a:lin ang="' + ang + '" scaled="1"/></a:gradFill>'
|
|
307
|
+
}
|
|
308
|
+
return '<a:noFill/>'
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** srgbClr(可选 alpha 0-100)→ OOXML(v0.11 透明度闭环)。 */
|
|
312
|
+
function srgbClrXml(color, alpha) {
|
|
313
|
+
return '<a:srgbClr val="' + hex(color) + '">' + (alpha !== undefined ? '<a:alpha val="' + Math.round(Number(alpha) * 1000) + '"/>' : '') + '</a:srgbClr>'
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ── line / connector ──────────────────────────────────────────────────────
|
|
317
|
+
/**
|
|
318
|
+
* 连线导出(2026-09-14 修复方向丢失):
|
|
319
|
+
* `straightConnector1` 只画包围盒的**左上→右下**,线的真实走向必须由 `flipH`/`flipV` 表达:
|
|
320
|
+
* p2 在 p1 左侧 → flipH;p2 在 p1 上方 → flipV。
|
|
321
|
+
* 漏写会镜像斜率;两条交叉线(如 × 的两笔)还会重叠成一条 —— 预览对、PowerPoint 里不对的根源。
|
|
322
|
+
* `tailEnd`(箭头)始终落在画出的"终点"= p2,所以定点翻转后箭头方向自动正确。
|
|
323
|
+
*/
|
|
324
|
+
function connectorSp(el, proof) {
|
|
325
|
+
const id = nid()
|
|
326
|
+
const p1 = el.points[0]
|
|
327
|
+
const p2 = el.points[1]
|
|
328
|
+
const x1 = p1[0]
|
|
329
|
+
const y1 = p1[1]
|
|
330
|
+
const x2 = p2[0]
|
|
331
|
+
const y2 = p2[1]
|
|
332
|
+
const minX = Math.min(x1, x2)
|
|
333
|
+
const minY = Math.min(y1, y2)
|
|
334
|
+
// 包围盒必须**精确**等于线段跨度(2026-09-14 第二修:不再 max(1,…) 兜底)——
|
|
335
|
+
// straightConnector1 是"包围盒对角",水平线给 cy=1 会被画成 1pt/12pt 的**假斜线**
|
|
336
|
+
// (预览平、PowerPoint 斜的根源)。cy=0 / cx=0 是合法表达(Office 自己也这么写)。
|
|
337
|
+
let wdt = Math.abs(x2 - x1)
|
|
338
|
+
let hgt = Math.abs(y2 - y1)
|
|
339
|
+
if (wdt === 0 && hgt === 0) { wdt = 1; hgt = 1 } // 零长线(退化点):给 1×1 以免被消费端丢弃
|
|
340
|
+
const flips = (x2 < x1 ? ' flipH="1"' : '') + (y2 < y1 ? ' flipV="1"' : '')
|
|
341
|
+
const xml = '<p:cxnSp><p:nvCxnSpPr><p:cNvPr id="' + id + '" name="' + xm(el.id) + '"/><p:cNvCxnSpPr/><p:nvPr/></p:nvCxnSpPr>'
|
|
342
|
+
+ '<p:spPr>' + xfrm(minX, minY, wdt, hgt, 0, flips) + '<a:prstGeom prst="straightConnector1"><a:avLst/></a:prstGeom>'
|
|
343
|
+
+ lineSpPr(el.line, el.arrow === true) + '</p:spPr></p:cxnSp>'
|
|
344
|
+
if (proof !== undefined && proof !== null) {
|
|
345
|
+
proof.out++
|
|
346
|
+
// 从写出的 XML 反推线段两端(OOXML 语义:本地 (0,0)→(w,h) 经 flip 映射回页面),必须与源 points 一致。
|
|
347
|
+
const back = connectorEndsFromXml(xml)
|
|
348
|
+
const same = back !== null &&
|
|
349
|
+
Math.abs(back[0][0] - x1) < 0.01 && Math.abs(back[1][0] - x2) < 0.01 &&
|
|
350
|
+
Math.abs(back[0][1] - y1) < 0.01 && Math.abs(back[1][1] - y2) < 0.01
|
|
351
|
+
if (!same) proof.wrong.push(`${el.id}: 源 [[${x1},${y1}],[${x2},${y2}]] → OOXML 还原 ${back === null ? '解析失败' : JSON.stringify(back.map((p) => p.map((v) => Math.round(v * 100) / 100)))}`)
|
|
352
|
+
// 箭头必须在 <a:ln> 内部(写在外面 PowerPoint 直接忽略)
|
|
353
|
+
if (el.arrow === true && !/<a:ln\b[^>]*>(?:(?!<\/a:ln>)[\s\S])*<a:tailEnd/.test(xml)) {
|
|
354
|
+
proof.wrong.push(`${el.id}: arrow=true 但 <a:tailEnd> 不在 <a:ln> 内(PowerPoint 会忽略箭头)`)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return xml
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** 从 connector XML 反推页面坐标端点(导出自证用;也供测试复用)。 */
|
|
361
|
+
export function connectorEndsFromXml(xml) {
|
|
362
|
+
const off = xml.match(/<a:off x="(-?\d+)" y="(-?\d+)"\/>/)
|
|
363
|
+
const ext = xml.match(/<a:ext cx="(\d+)" cy="(\d+)"\/>/)
|
|
364
|
+
if (!off || !ext) return null
|
|
365
|
+
const x = Number(off[1]) / EMU
|
|
366
|
+
const y = Number(off[2]) / EMU
|
|
367
|
+
const w = Number(ext[1]) / EMU
|
|
368
|
+
const h = Number(ext[2]) / EMU
|
|
369
|
+
const flipH = /flipH="1"/.test(xml)
|
|
370
|
+
const flipV = /flipV="1"/.test(xml)
|
|
371
|
+
const start = [x + (flipH ? w : 0), y + (flipV ? h : 0)]
|
|
372
|
+
const end = [x + (flipH ? 0 : w), y + (flipV ? 0 : h)]
|
|
373
|
+
return [start, end]
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ── image ─────────────────────────────────────────────────────────────────
|
|
377
|
+
function picSp(el, rId) {
|
|
378
|
+
const id = nid()
|
|
379
|
+
return '<p:pic><p:nvPicPr><p:cNvPr id="' + id + '" name="' + xm(el.id) + '"/><p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr>'
|
|
380
|
+
+ '<p:blipFill><a:blip r:embed="' + rId + '"/><a:stretch><a:fillRect/></a:stretch></p:blipFill>'
|
|
381
|
+
+ '<p:spPr>' + xfrm(el.bounds.x, el.bounds.y, el.bounds.w, el.bounds.h) + '<a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic>'
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── table ─────────────────────────────────────────────────────────────────
|
|
385
|
+
/** PowerPoint 默认表格样式(python-pptx/PowerPoint 落盘同一 GUID);无样式时部分版本丢弃表格(P1 事故)。 */
|
|
386
|
+
const TABLE_STYLE_ID = '{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}'
|
|
387
|
+
|
|
388
|
+
function tableFrame(el) {
|
|
389
|
+
const id = nid()
|
|
390
|
+
const rows = [el.header ? [...el.cols] : null, ...el.rows.map((r) => [...r])].filter(Boolean)
|
|
391
|
+
const ncols = Math.max(1, ...rows.map((r) => r.length))
|
|
392
|
+
const colW = Math.floor(emu(el.bounds.w) / ncols)
|
|
393
|
+
const header = el.header
|
|
394
|
+
const grid = '<a:tblGrid>' + Array.from({ length: ncols }, () => '<a:gridCol w="' + colW + '"/>').join('') + '</a:tblGrid>'
|
|
395
|
+
const trs = rows.map((r, ri) => {
|
|
396
|
+
const tds = Array.from({ length: ncols }, (_, ci) => {
|
|
397
|
+
const cell = r[ci]
|
|
398
|
+
const isH = header && ri === 0
|
|
399
|
+
const fill = isH ? '<a:solidFill><a:srgbClr val="DDEBF7"/></a:solidFill>' : '<a:noFill/>'
|
|
400
|
+
// 字号来自 normalizePage 的 fontPt(= max(11, theme.minFontSize)),不再硬编码 1100:
|
|
401
|
+
// 硬编码会让 audit 档"最小字号 ≥ minFontSize"的断言对含表工程恒假,且用户在 DSL 里无法修正。
|
|
402
|
+
const cellSz = Math.round((Number(el.fontPt) || 11) * 100)
|
|
403
|
+
const rPr = '<a:rPr lang="zh-CN" sz="' + cellSz + '"' + (isH ? ' b="1"' : '') + '><a:solidFill><a:srgbClr val="1F2937"/></a:solidFill></a:rPr>'
|
|
404
|
+
const tb = '<a:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r>' + rPr + '<a:t>' + xm(cell ?? '') + '</a:t></a:r></a:p></a:txBody>'
|
|
405
|
+
// P1 修复(测试反馈 ★):tcPr 子元素顺序须为 lnL..lnB → fill → marL(ECMA-376 CT_TableCellProperties);
|
|
406
|
+
// anchor 是 tcPr 的属性而非 marL 的属性。
|
|
407
|
+
return '<a:tc>' + tb + '<a:tcPr anchor="ctr">'
|
|
408
|
+
+ '<a:lnL><a:noFill/></a:lnL><a:lnR><a:noFill/></a:lnR><a:lnT><a:noFill/></a:lnT><a:lnB><a:noFill/></a:lnB>'
|
|
409
|
+
+ fill + '<a:marL l="45720" r="45720" t="22860" b="22860"/></a:tcPr></a:tc>'
|
|
410
|
+
}).join('')
|
|
411
|
+
return '<a:tr h="' + Math.floor(emu(el.bounds.h) / rows.length) + '">' + tds + '</a:tr>'
|
|
412
|
+
}).join('')
|
|
413
|
+
// P1 修复:graphicFrame 的 <p:xfrm> 直接含 <a:off>/<a:ext>(不能复用 spPr 的 xfrm(),
|
|
414
|
+
// 其嵌套 <a:xfrm> 属非法结构——PowerPoint 打开时静默丢弃整帧,HTML 预览不受影响,极具迷惑性)。
|
|
415
|
+
return '<p:graphicFrame><p:nvGraphicFramePr><p:cNvPr id="' + id + '" name="' + xm(el.id) + '"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr>'
|
|
416
|
+
+ '<p:xfrm><a:off x="' + emu(el.bounds.x) + '" y="' + emu(el.bounds.y) + '"/><a:ext cx="' + emu(el.bounds.w) + '" cy="' + emu(el.bounds.h) + '"/></p:xfrm>'
|
|
417
|
+
+ '<a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">'
|
|
418
|
+
+ '<a:tbl><a:tblPr firstRow="' + (header ? 1 : 0) + '" bandRow="0"><a:tableStyleId>' + TABLE_STYLE_ID + '</a:tableStyleId></a:tblPr>'
|
|
419
|
+
+ grid + trs + '</a:tbl>'
|
|
420
|
+
+ '</a:graphicData></a:graphic></p:graphicFrame>'
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── chart(矢量拼绘)───────────────────────────────────────────────────────
|
|
424
|
+
function chartSp(el) {
|
|
425
|
+
const x = el.bounds.x
|
|
426
|
+
const y = el.bounds.y
|
|
427
|
+
const w = el.bounds.w
|
|
428
|
+
const h = el.bounds.h
|
|
429
|
+
const data = chartData(el.chart)
|
|
430
|
+
const colors = chartColors(el.chart)
|
|
431
|
+
const out = []
|
|
432
|
+
const padT = 0.06 * h
|
|
433
|
+
const padR = 0.04 * w
|
|
434
|
+
const padB = el.chart.type === 'pie' ? 0.1 * h : 0.24 * h
|
|
435
|
+
const padL = 0.12 * w
|
|
436
|
+
const iw = w - padL - padR
|
|
437
|
+
const ih = h - padT - padB
|
|
438
|
+
const vals = data.series.flatMap((s) => s.values)
|
|
439
|
+
const maxV = Math.max(1e-9, ...vals)
|
|
440
|
+
const n = Math.max(1, data.categories.length)
|
|
441
|
+
const slot = iw / n
|
|
442
|
+
|
|
443
|
+
if (el.chart.type === 'bar') {
|
|
444
|
+
data.series.forEach((s, si) => {
|
|
445
|
+
s.values.forEach((v, i) => {
|
|
446
|
+
const bw = Math.max(2, (slot * 0.6) / data.series.length)
|
|
447
|
+
const bx = x + padL + slot * i + slot / 2 - bw / 2 + (si - (data.series.length - 1) / 2) * bw
|
|
448
|
+
const bh = Math.max(2, Math.abs(v / maxV) * ih)
|
|
449
|
+
const by = y + padT + ih - bh
|
|
450
|
+
out.push(shapeSp({ id: el.id + '-b' + i + '-' + si, kind: 'rect', bounds: { x: bx, y: by, w: bw, h: bh }, fill: colors[si % colors.length] }))
|
|
451
|
+
})
|
|
452
|
+
})
|
|
453
|
+
} else if (el.chart.type === 'line') {
|
|
454
|
+
data.series.forEach((s, si) => {
|
|
455
|
+
for (let i = 1; i < s.values.length; i++) {
|
|
456
|
+
const x1 = x + padL + slot * (i - 1) + slot / 2
|
|
457
|
+
const y1 = y + padT + ih - (s.values[i - 1] / maxV) * ih
|
|
458
|
+
const x2 = x + padL + slot * i + slot / 2
|
|
459
|
+
const y2 = y + padT + ih - (s.values[i] / maxV) * ih
|
|
460
|
+
out.push(connectorSp({ id: el.id + '-L' + i, points: [[x1, y1], [x2, y2]], line: { color: colors[si % colors.length], width: 2 } }))
|
|
461
|
+
}
|
|
462
|
+
s.values.forEach((v, i) => {
|
|
463
|
+
out.push(shapeSp({ id: el.id + '-d' + i + '-' + si, kind: 'ellipse', bounds: { x: x + padL + slot * i + slot / 2 - 3, y: y + padT + ih - (v / maxV) * ih - 3, w: 6, h: 6 }, fill: colors[si % colors.length] }))
|
|
464
|
+
})
|
|
465
|
+
})
|
|
466
|
+
} else if (el.chart.type === 'pie') {
|
|
467
|
+
const v = data.series[0].values
|
|
468
|
+
const total = v.reduce((a, b) => a + Math.max(0, b), 0) || 1
|
|
469
|
+
const r = Math.min(iw, ih) / 2
|
|
470
|
+
const cx = x + padL + iw / 2
|
|
471
|
+
const cy = y + padT + ih / 2
|
|
472
|
+
let ang = -90 * 60000
|
|
473
|
+
v.forEach((val, i) => {
|
|
474
|
+
if (val <= 0) return
|
|
475
|
+
const frac = val / total
|
|
476
|
+
const span = Math.round(frac * 360 * 60000)
|
|
477
|
+
const gd1 = Math.abs(ang % (360 * 60000))
|
|
478
|
+
const gd2 = Math.abs((ang + span) % (360 * 60000))
|
|
479
|
+
const gid = nid()
|
|
480
|
+
const oval = '<a:off x="' + Math.round(cx - r) + '" y="' + Math.round(cy - r) + '"/><a:ext cx="' + Math.round(r * 2) + '" cy="' + Math.round(r * 2) + '"/>'
|
|
481
|
+
out.push('<p:sp><p:nvSpPr><p:cNvPr id="' + gid + '" name="' + xm(el.id) + '-p' + i + '"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>'
|
|
482
|
+
+ '<p:spPr><a:xfrm>' + oval + '</a:xfrm>'
|
|
483
|
+
+ '<a:prstGeom prst="pie"><a:avLst><a:gd name="adj1" fmla="val ' + gd1 + '"/><a:gd name="adj2" fmla="val ' + gd2 + '"/></a:avLst></a:prstGeom>'
|
|
484
|
+
+ '<a:solidFill><a:srgbClr val="' + hex(colors[i % colors.length]) + '"/></a:solidFill><a:ln><a:noFill/></a:ln>'
|
|
485
|
+
+ '</p:spPr><p:txBody><a:bodyPr/><a:lstStyle/><a:p/></p:txBody></p:sp>')
|
|
486
|
+
ang += span
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
return out
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ── package parts ─────────────────────────────────────────────────────────
|
|
493
|
+
function contentTypes(slides) {
|
|
494
|
+
const notes = slides.filter((s) => s.notes)
|
|
495
|
+
const overrides = [
|
|
496
|
+
'<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>',
|
|
497
|
+
'<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>',
|
|
498
|
+
'<Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>',
|
|
499
|
+
'<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>',
|
|
500
|
+
'<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>',
|
|
501
|
+
'<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>',
|
|
502
|
+
...(notes.length ? ['<Override PartName="/ppt/notesMasters/notesMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml"/>'] : []),
|
|
503
|
+
...slides.map((_, i) => '<Override PartName="/ppt/slides/slide' + (i + 1) + '.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>'),
|
|
504
|
+
...notes.map((s) => '<Override PartName="/ppt/notesSlides/notesSlide' + s.notesNo + '.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml"/>'),
|
|
505
|
+
]
|
|
506
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
|
|
507
|
+
+ '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
|
|
508
|
+
+ '<Default Extension="xml" ContentType="application/xml"/>'
|
|
509
|
+
+ '<Default Extension="png" ContentType="image/png"/><Default Extension="jpeg" ContentType="image/jpeg"/>'
|
|
510
|
+
+ '<Default Extension="jpg" ContentType="image/jpeg"/><Default Extension="gif" ContentType="image/gif"/>'
|
|
511
|
+
+ '<Default Extension="webp" ContentType="image/webp"/>'
|
|
512
|
+
+ overrides.join('') + '</Types>'
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function rootRels() {
|
|
516
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
|
517
|
+
+ '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>'
|
|
518
|
+
+ '<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>'
|
|
519
|
+
+ '<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>'
|
|
520
|
+
+ '</Relationships>'
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function coreProps(ctx) {
|
|
524
|
+
const d = new Date().toISOString()
|
|
525
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/">'
|
|
526
|
+
+ '<dc:title>' + xm(ctx.deck.title ?? 'deck') + '</dc:title><dc:creator>dsh-ppt-studio</dc:creator><cp:lastModifiedBy>dsh-ppt-studio</cp:lastModifiedBy>'
|
|
527
|
+
+ '<dcterms:created xmlns:dcterms="http://purl.org/dc/terms/" xsi:type="dcterms:W3CDTF" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' + d + '</dcterms:created>'
|
|
528
|
+
+ '<dcterms:modified xmlns:dcterms="http://purl.org/dc/terms/" xsi:type="dcterms:W3CDTF" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' + d + '</dcterms:modified>'
|
|
529
|
+
+ '</cp:coreProperties>'
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function appProps(ctx) {
|
|
533
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">'
|
|
534
|
+
+ '<Application>dsh-ppt-studio</Application><Slides>' + ctx.pages.length + '</Slides></Properties>'
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function presentationXml(ctx, slides) {
|
|
538
|
+
const sldIdLst = slides.map((_, i) => '<p:sldId id="' + (256 + i) + '" r:id="rId' + (i + 2) + '"/>').join('')
|
|
539
|
+
// 注意:**不写 <p:notesMasterIdLst>**。备注母版只通过 presentation.xml.rels 里的 notesMaster 关系挂载
|
|
540
|
+
//(与 PowerPoint 原生模板 / python-pptx 产物同构)。2026-09-14 实测:多写这个元素,PowerPoint 直接报
|
|
541
|
+
// "文件或目录损坏"(0x80070570)——python-pptx 却照读不误。减量二分定位:撤掉它即恢复正常。
|
|
542
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">'
|
|
543
|
+
+ '<p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>'
|
|
544
|
+
+ '<p:sldIdLst>' + sldIdLst + '</p:sldIdLst>'
|
|
545
|
+
+ '<p:sldSz cx="' + emu(ctx.size.width) + '" cy="' + emu(ctx.size.height) + '"/><p:notesSz cx="6858000" cy="9144000"/>'
|
|
546
|
+
+ '<p:defaultTextStyle><a:lvl1pPr algn="l"><a:defRPr sz="1800"/></a:lvl1pPr><a:lvl2pPr><a:defRPr sz="1400"/></a:lvl2pPr><a:lvl3pPr><a:defRPr sz="1200"/></a:lvl3pPr><a:lvl4pPr><a:defRPr sz="1200"/></a:lvl4pPr><a:lvl5pPr><a:defRPr sz="1200"/></a:lvl5pPr></p:defaultTextStyle>'
|
|
547
|
+
+ '</p:presentation>'
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function presentationRels(slides) {
|
|
551
|
+
const r = slides.map((_, i) => '<Relationship Id="rId' + (i + 2) + '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide' + (i + 1) + '.xml"/>').join('')
|
|
552
|
+
const notesMaster = slides.some((s) => s.notes) ? `<Relationship Id="rId${slides.length + 2}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster" Target="notesMasters/notesMaster1.xml"/>` : ''
|
|
553
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
|
554
|
+
+ '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>'
|
|
555
|
+
+ r + notesMaster + '</Relationships>'
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function slideMasterXml() {
|
|
559
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">'
|
|
560
|
+
+ '<p:cSld><p:spTree>' + spTreeHeader() + '</p:spTree></p:cSld>'
|
|
561
|
+
+ '<p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/>'
|
|
562
|
+
+ '<p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst>'
|
|
563
|
+
+ '<p:txStyles><p:titleStyle><a:lvl1pPr algn="l"/></p:titleStyle><p:bodyStyle><a:lvl1pPr algn="l"/></p:bodyStyle><p:otherStyle><a:lvl1pPr algn="l"/></p:otherStyle></p:txStyles>'
|
|
564
|
+
+ '</p:sldMaster>'
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function slideMasterRels() {
|
|
568
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
|
569
|
+
+ '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>'
|
|
570
|
+
+ '<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>'
|
|
571
|
+
+ '</Relationships>'
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function slideLayoutXml() {
|
|
575
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" type="blank" preserve="1">'
|
|
576
|
+
+ '<p:cSld name="blank"><p:spTree>' + spTreeHeader() + '</p:spTree></p:cSld>'
|
|
577
|
+
+ '<p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sldLayout>'
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function slideLayoutRels() {
|
|
581
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
|
582
|
+
+ '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster1.xml"/>'
|
|
583
|
+
+ '</Relationships>'
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function slideXml(s) {
|
|
587
|
+
const bg = s.bg ? ['<p:bg>', s.bg.xml, '</p:bg>'].join('') : ''
|
|
588
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">'
|
|
589
|
+
+ '<p:cSld>' + bg + s.spTree + '</p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sld>'
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ── 备注页(讲稿):2026-09-14 新增 ────────────────────────────────────────────
|
|
593
|
+
// 结构对齐 PowerPoint/python-pptx 的实际产物:presentation → notesMasterIdLst + rels → notesMaster(含 theme rel),
|
|
594
|
+
// slide → rels(notesSlide)→ notesSlide(body 占位符 + 段落文本)。缺 notesMaster 时 PowerPoint 会在打开时自行补,
|
|
595
|
+
// 但产物就不是"标准包"了——所以宁可多两个小部件,保持与真实 pptx 同构。
|
|
596
|
+
const P_NS = 'xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"'
|
|
597
|
+
|
|
598
|
+
/** 讲稿文本 → notesSlide。
|
|
599
|
+
* 结构照 python-pptx 默认模板(PowerPoint 原生产物)复核过:
|
|
600
|
+
* ① notesSlide 的占位符 **必须能在 notesMaster 里找到对应项**(body 用 idx="3"、幻灯图 sldImg idx="2");
|
|
601
|
+
* ② notesSlide 必须自带 rels(→ notesMaster + 回指本页幻灯),见调用处。
|
|
602
|
+
* 2026-09-14 实测教训:占位符 idx 对不上 / 缺 rels 时 python-pptx 能读,**PowerPoint 报"文件或目录损坏"**。 */
|
|
603
|
+
function notesSlideXml(notes) {
|
|
604
|
+
const paras = String(notes).split(/\r?\n/)
|
|
605
|
+
.map((line) => '<a:p><a:r><a:rPr lang="zh-CN" dirty="0"/><a:t>' + xm(line) + '</a:t></a:r></a:p>')
|
|
606
|
+
.join('')
|
|
607
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><p:notes ' + P_NS + '>'
|
|
608
|
+
+ '<p:cSld><p:spTree>' + spTreeHeader()
|
|
609
|
+
+ '<p:sp><p:nvSpPr><p:cNvPr id="2" name="Slide Image Placeholder 1"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="sldImg" idx="2"/></p:nvPr></p:nvSpPr><p:spPr/></p:sp>'
|
|
610
|
+
+ '<p:sp><p:nvSpPr><p:cNvPr id="3" name="Notes Placeholder 2"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="body" sz="quarter" idx="3"/></p:nvPr></p:nvSpPr>'
|
|
611
|
+
+ '<p:spPr/><p:txBody><a:bodyPr/><a:lstStyle/>' + paras + '</p:txBody></p:sp>'
|
|
612
|
+
+ '</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:notes>'
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** 备注母版的占位符表(几何取自 PowerPoint 原生 notesMaster:notesSz = 6858000×9144000 EMU)。 */
|
|
616
|
+
const NOTES_PH = [
|
|
617
|
+
{ id: 2, name: 'Header Placeholder 1', ph: '<p:ph type="hdr" sz="quarter"/>', off: [0, 0], ext: [2971800, 457200], algn: 'l' },
|
|
618
|
+
{ id: 3, name: 'Date Placeholder 2', ph: '<p:ph type="dt" idx="1"/>', off: [3884613, 0], ext: [2971800, 457200], algn: 'r' },
|
|
619
|
+
{ id: 4, name: 'Slide Image Placeholder 3', ph: '<p:ph type="sldImg" idx="2"/>', off: [1143000, 685800], ext: [4572000, 3429000] },
|
|
620
|
+
{ id: 5, name: 'Notes Placeholder 4', ph: '<p:ph type="body" sz="quarter" idx="3"/>', off: [685800, 4343400], ext: [5486400, 4114800] },
|
|
621
|
+
{ id: 6, name: 'Footer Placeholder 5', ph: '<p:ph type="ftr" sz="quarter" idx="4"/>', off: [0, 8685213], ext: [2971800, 457200], algn: 'l' },
|
|
622
|
+
{ id: 7, name: 'Slide Number Placeholder 6', ph: '<p:ph type="sldNum" sz="quarter" idx="5"/>', off: [3884613, 8685213], ext: [2971800, 457200], algn: 'r' },
|
|
623
|
+
]
|
|
624
|
+
|
|
625
|
+
/** 最小但**完整**的 notesMaster(CT_NotesMaster 顺序:cSld → clrMap → notesStyle)。
|
|
626
|
+
* 占位符不能省:notesSlide 里的 ph 必须在此有对应项,否则 PowerPoint 判包损坏。 */
|
|
627
|
+
function notesMasterXml() {
|
|
628
|
+
const sp = (p) => '<p:sp><p:nvSpPr><p:cNvPr id="' + p.id + '" name="' + p.name + '"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr>' + p.ph + '</p:nvPr></p:nvSpPr>'
|
|
629
|
+
+ '<p:spPr><a:xfrm><a:off x="' + p.off[0] + '" y="' + p.off[1] + '"/><a:ext cx="' + p.ext[0] + '" cy="' + p.ext[1] + '"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>'
|
|
630
|
+
+ '<p:txBody><a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0"/><a:lstStyle>'
|
|
631
|
+
+ (p.algn ? '<a:lvl1pPr algn="' + p.algn + '"><a:defRPr sz="1200"/></a:lvl1pPr>' : '')
|
|
632
|
+
+ '</a:lstStyle><a:p><a:endParaRPr lang="zh-CN"/></a:p></p:txBody></p:sp>'
|
|
633
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><p:notesMaster ' + P_NS + '>'
|
|
634
|
+
+ '<p:cSld><p:spTree>' + spTreeHeader() + NOTES_PH.map(sp).join('') + '</p:spTree></p:cSld>'
|
|
635
|
+
+ '<p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/>'
|
|
636
|
+
+ '<p:notesStyle><a:lvl1pPr algn="l"><a:defRPr sz="1200"/></a:lvl1pPr></p:notesStyle>'
|
|
637
|
+
+ '</p:notesMaster>'
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function notesMasterRels() {
|
|
641
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
|
642
|
+
+ '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>'
|
|
643
|
+
+ '</Relationships>'
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** 页面背景 → 导出结构:{ kind:'solid', xml } | { kind:'image', src, rId, xml } | null */
|
|
647
|
+
function resolveBg(bg, ctx) {
|
|
648
|
+
if (!bg) return null
|
|
649
|
+
if (typeof bg === 'string') {
|
|
650
|
+
return { kind: 'solid', xml: '<p:bgPr><a:solidFill><a:srgbClr val="' + hex(ctx.resolveColor(bg)) + '"/></a:solidFill><a:effectLst/></p:bgPr>' }
|
|
651
|
+
}
|
|
652
|
+
if (bg.type === 'solid') {
|
|
653
|
+
return { kind: 'solid', xml: '<p:bgPr><a:solidFill><a:srgbClr val="' + hex(ctx.resolveColor(bg.color ?? '#FFFFFF')) + '"/></a:solidFill><a:effectLst/></p:bgPr>' }
|
|
654
|
+
}
|
|
655
|
+
if (bg.type === 'image') {
|
|
656
|
+
return {
|
|
657
|
+
kind: 'image',
|
|
658
|
+
src: bg.src,
|
|
659
|
+
rId: null, // 由调用方 addMedia 后回填
|
|
660
|
+
xml: '<p:bgPr><a:blipFill><a:blip r:embed="BGPLACEHOLDER"/><a:stretch><a:fillRect/></a:stretch></a:blipFill><a:effectLst/></p:bgPr>',
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return null
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function themeXml() {
|
|
667
|
+
const clr = [
|
|
668
|
+
['dk1', '000000'], ['lt1', 'FFFFFF'], ['dk2', '1F2937'], ['lt2', 'F3F4F6'],
|
|
669
|
+
['accent1', '2563EB'], ['accent2', 'F59E0B'], ['accent3', '10B981'], ['accent4', 'EF4444'],
|
|
670
|
+
['accent5', '8B5CF6'], ['accent6', '06B6D4'], ['hlink', '2563EB'], ['folHlink', '7C3AED'],
|
|
671
|
+
].map(([n, v]) => '<a:' + n + '><a:srgbClr val="' + v + '"/></a:' + n + '>').join('')
|
|
672
|
+
const fill = '<a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:fillStyleLst>'
|
|
673
|
+
const ln = '<a:lnStyleLst>'
|
|
674
|
+
+ '<a:ln w="6350" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln>'
|
|
675
|
+
+ '<a:ln w="12700" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln>'
|
|
676
|
+
+ '<a:ln w="19050" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln>'
|
|
677
|
+
+ '</a:lnStyleLst>'
|
|
678
|
+
const eff = '<a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst>'
|
|
679
|
+
const bg = '<a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:bgFillStyleLst>'
|
|
680
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Studio">'
|
|
681
|
+
+ '<a:themeElements><a:clrScheme name="Studio">' + clr + '</a:clrScheme>'
|
|
682
|
+
+ '<a:fontScheme name="Studio"><a:majorFont><a:latin typeface="Calibri"/><a:ea typeface="Microsoft YaHei"/><a:cs typeface=""/></a:majorFont>'
|
|
683
|
+
+ '<a:minorFont><a:latin typeface="Calibri"/><a:ea typeface="Microsoft YaHei"/><a:cs typeface=""/></a:minorFont></a:fontScheme>'
|
|
684
|
+
+ '<a:fmtScheme name="Studio">' + fill + ln + eff + bg + '</a:fmtScheme>'
|
|
685
|
+
+ '</a:themeElements></a:theme>'
|
|
686
|
+
}
|