修复svg2pptx转换器多项bug + 新增管线兼容性文档

svg2pptx.py:
- 修复image opacity不生效(通过OOXML alphaModFix)
- 修复环形图stroke渐变url(#id)引用不支持(fallback第一个stop颜色)
- 修复viewBox内缩放不传递(g组scale累积到所有子元素)
- 修复text baseline偏移(区分text-after-edge和auto)
- 修复text-anchor:middle/end的x坐标偏移
- 添加--html-dir参数支持

html2svg.py:
- 修复图片相对路径解析(以HTML文件所在目录为基准)
- 新增3种CSS兜底预处理(background-clip:text、text-fill-color、mask-image)

新增 references/pipeline-compat.md:
- HTML->SVG->PPTX管线兼容性规则文档
- CSS禁止清单、防偏移写法指南、防偏移checklist
- 已整合到SKILL.md和prompts.md中引用

prompts.md:
- 新增内联SVG防偏移约束(禁SVG text、用HTML叠加)

示例产物:
- ppt-output/ 包含SU7示例的完整HTML/SVG/PPTX产物
This commit is contained in:
sunbigfly
2026-03-21 03:57:23 +08:00
parent 7dcfe25603
commit e860485ec8
27 changed files with 7914 additions and 56 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
package-lock.json
package.json
.vscode/

View File

@@ -10,6 +10,23 @@
需求调研 → 资料搜集 → 大纲策划 → 策划稿 → 风格+配图+HTML设计稿 → 后处理(SVG+PPTX)
```
## 效果展示
> 以「新一代小米SU7发布」为主题的示例输出小米橙风格
| 封面页 | 配置对比页 |
|:---:|:---:|
| ![封面页](ppt-output/png/slide_01_cover.png) | ![配置对比](ppt-output/png/slide_02_models.png) |
| 动力续航页 | 智驾安全页 |
|:---:|:---:|
| ![动力续航](ppt-output/png/slide_03_power.png) | ![智驾安全](ppt-output/png/slide_04_smart.png) |
| 结束页 |
|:---:|
| ![结束页](ppt-output/png/slide_05_end.png) |
## 核心特性
| 特性 | 说明 |

View File

@@ -272,6 +272,7 @@ pip install python-pptx lxml Pillow 2>/dev/null
```
2. **SVG 转换** -- 运行 `html2svg.py`DOM 直接转 SVG保留 `<text>` 可编辑)
> **重要**HTML 设计稿必须遵守 `references/pipeline-compat.md` 中的管线兼容性规则,否则转换后会出现元素丢失、位置错位等问题。
```bash
python3 SKILL_DIR/scripts/html2svg.py OUTPUT_DIR/slides/ -o OUTPUT_DIR/svg/
```
@@ -316,7 +317,7 @@ ppt-output/
|------|-------|
| 内容 | 每页 >= 2 信息卡片 / >= 60% 内容页含数据 / 章节有递进 |
| 视觉 | 全局风格一致 / 配图风格统一 / 卡片不重叠 / 文字不溢出 |
| 技术 | CSS 变量统一 / SVG 友好约束遵守 / HTML 可被 Puppeteer 渲染 |
| 技术 | CSS 变量统一 / SVG 友好约束遵守 / HTML 可被 Puppeteer 渲染 / `pipeline-compat.md` 禁止清单检查 |
---
@@ -328,3 +329,4 @@ ppt-output/
| `references/style-system.md` | Step 5a | 8 种预置风格 + CSS 变量 + 风格 JSON 模型 |
| `references/bento-grid.md` | Step 5c | 7 种布局精确坐标 + 5 种卡片类型 + 决策矩阵 |
| `references/method.md` | 初次了解 | 核心理念与方法论 |
| `references/pipeline-compat.md` | **Step 5c 设计稿生成时** | CSS 禁止清单 + 图片路径 + 字号混排 + SVG text + 环形图 + svg2pptx 注意事项 |

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

View File

@@ -0,0 +1,227 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
:root {
--bg-primary: #1A1A1A;
--bg-secondary: #111111;
--card-bg-from: #2A2A2A;
--card-bg-to: #1A1A1A;
--card-border: rgba(255,105,0,0.15);
--card-radius: 16px;
--text-primary: #FFFFFF;
--text-secondary: rgba(255,255,255,0.65);
--accent-1: #FF6900;
--accent-2: #FF8C00;
--accent-3: #FFFFFF;
--accent-4: #E0E0E0;
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
width: 1280px; height: 720px; overflow: hidden;
background: linear-gradient(135deg, var(--bg-primary), var(--bg-secondary));
font-family: 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif;
color: var(--text-primary);
position: relative;
}
/* 背景装饰 -- 大弧形光晕 */
.bg-glow {
position: absolute;
width: 900px; height: 900px;
border-radius: 50%;
background: radial-gradient(circle, rgba(255,105,0,0.08) 0%, transparent 70%);
top: -300px; right: -200px;
pointer-events: none;
}
.bg-glow-2 {
position: absolute;
width: 500px; height: 500px;
border-radius: 50%;
background: radial-gradient(circle, rgba(255,140,0,0.06) 0%, transparent 70%);
bottom: -150px; left: -100px;
pointer-events: none;
}
/* 左上角品牌标识区 */
.brand-area {
position: absolute; top: 40px; left: 60px;
display: flex; align-items: center; gap: 14px;
}
.brand-logo {
width: 36px; height: 36px;
border-radius: 8px;
background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
display: flex; align-items: center; justify-content: center;
}
.brand-logo svg { width: 20px; height: 20px; }
.brand-text {
font-size: 15px; font-weight: 600;
color: var(--text-secondary); letter-spacing: 2px;
}
/* 中央主标题 */
.hero {
position: absolute;
top: 50%; left: 60px;
transform: translateY(-55%);
max-width: 680px;
}
.hero-badge {
display: inline-block;
padding: 6px 18px;
border-radius: 20px;
border: 1px solid var(--accent-1);
color: var(--accent-1);
font-size: 13px; font-weight: 600;
letter-spacing: 1.5px;
margin-bottom: 24px;
}
.hero h1 {
font-size: 52px; font-weight: 800;
line-height: 1.2;
margin-bottom: 20px;
}
.hero h1 span {
color: var(--accent-1);
}
.hero p {
font-size: 18px;
color: var(--text-secondary);
line-height: 1.7;
max-width: 560px;
}
/* 右侧数据亮点卡片 */
.data-strip {
position: absolute;
right: 60px; top: 50%;
transform: translateY(-50%);
display: flex; flex-direction: column; gap: 16px;
}
.data-card {
width: 240px;
padding: 20px 24px;
border-radius: var(--card-radius);
background: linear-gradient(135deg, var(--card-bg-from), var(--card-bg-to));
border: 1px solid var(--card-border);
}
.data-card .number {
font-size: 36px; font-weight: 800;
color: var(--accent-1);
}
.data-card .unit {
font-size: 14px; color: var(--accent-1);
margin-left: 2px;
}
.data-card .label {
font-size: 13px; color: var(--text-secondary);
margin-top: 4px;
}
/* 底部信息条 */
.footer {
position: absolute; bottom: 30px; left: 60px; right: 60px;
display: flex; justify-content: space-between; align-items: center;
}
.footer-left {
font-size: 13px; color: var(--text-secondary);
}
.footer-right {
font-size: 12px; color: rgba(255,255,255,0.35);
display: flex; gap: 20px;
}
/* 装饰线条 */
.deco-line {
position: absolute;
width: 120px; height: 2px;
background: linear-gradient(90deg, var(--accent-1), transparent);
bottom: 80px; left: 60px;
}
/* 配图 -- 渐隐融合div遮罩实现SVG安全 */
.hero-image {
position: absolute;
bottom: 0; right: 0;
width: 55%;
height: 75%;
overflow: hidden;
pointer-events: none;
z-index: 0;
}
.hero-image img {
width: 100%; height: 100%;
object-fit: cover;
object-position: center;
opacity: 0.35;
}
/* 左侧渐隐遮罩 */
.hero-image .mask-left {
position: absolute; top: 0; left: 0;
width: 70%; height: 100%;
background: linear-gradient(90deg, var(--bg-primary) 0%, transparent 100%);
pointer-events: none;
}
/* 底部渐隐遮罩 */
.hero-image .mask-bottom {
position: absolute; bottom: 0; left: 0;
width: 100%; height: 50%;
background: linear-gradient(0deg, var(--bg-primary) 0%, transparent 100%);
pointer-events: none;
}
/* 确保内容在图片上方 */
.brand-area, .hero, .data-strip, .footer, .deco-line { z-index: 1; }
</style>
</head>
<body>
<div class="bg-glow"></div>
<div class="bg-glow-2"></div>
<div class="hero-image">
<img src="../images/su7_cover.png" alt="">
<div class="mask-left"></div>
<div class="mask-bottom"></div>
</div>
<div class="brand-area">
<div class="brand-logo">
<svg viewBox="0 0 24 24" fill="white"><path d="M2 8h20v8H2z" opacity="0.8"/><path d="M6 4h12v16H6z" opacity="0.6"/></svg>
</div>
<span class="brand-text">XIAOMI AUTO</span>
</div>
<div class="hero">
<div class="hero-badge">2026.03.19 NEW LAUNCH</div>
<h1>新一代<span>小米SU7</span><br>正式发布</h1>
<p>全系搭载V6s Plus超级电机全系标配激光雷达Pro版CLTC续航902km。上市34分钟锁单突破1.5万辆。</p>
</div>
<div class="data-strip">
<div class="data-card">
<div><span class="number">21.99</span><span class="unit">万元起</span></div>
<div class="label">标准版起售价</div>
</div>
<div class="data-card">
<div><span class="number">902</span><span class="unit">km</span></div>
<div class="label">Pro版CLTC续航</div>
</div>
<div class="data-card">
<div><span class="number">1.5</span><span class="unit">万+</span></div>
<div class="label">34分钟锁单量</div>
</div>
</div>
<div class="deco-line"></div>
<div class="footer">
<div class="footer-left">小米汽车 | 新一代SU7发布会</div>
<div class="footer-right">
<span>标准版 / Pro / Max</span>
<span>01 / 05</span>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,256 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
:root {
--bg-primary: #1A1A1A;
--bg-secondary: #111111;
--card-bg-from: #2A2A2A;
--card-bg-to: #1A1A1A;
--card-border: rgba(255,105,0,0.15);
--card-radius: 16px;
--text-primary: #FFFFFF;
--text-secondary: rgba(255,255,255,0.65);
--accent-1: #FF6900;
--accent-2: #FF8C00;
--accent-3: #FFFFFF;
--accent-4: #E0E0E0;
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
width: 1280px; height: 720px; overflow: hidden;
background: linear-gradient(135deg, var(--bg-primary), var(--bg-secondary));
font-family: 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif;
color: var(--text-primary);
position: relative;
}
/* 页面标题区 */
.page-title {
position: absolute; left: 40px; top: 20px;
width: 1200px; height: 50px;
display: flex; align-items: center; gap: 16px;
}
.title-accent {
width: 4px; height: 28px;
background: linear-gradient(180deg, var(--accent-1), var(--accent-2));
border-radius: 2px;
}
.page-title h2 {
font-size: 26px; font-weight: 700;
}
.page-title .subtitle {
font-size: 14px; color: var(--text-secondary);
margin-left: auto;
}
/* 内容区 -- 三栏等宽 */
.content-area {
position: absolute;
left: 40px; top: 80px;
width: 1200px; height: 580px;
display: grid;
grid-template: 1fr / repeat(3, 1fr);
gap: 20px;
}
.model-card {
border-radius: var(--card-radius);
background: linear-gradient(180deg, var(--card-bg-from), var(--card-bg-to));
border: 1px solid var(--card-border);
padding: 28px 24px;
display: flex; flex-direction: column;
position: relative;
overflow: hidden;
}
.model-card.featured {
border-color: rgba(255,105,0,0.4);
box-shadow: 0 0 30px rgba(255,105,0,0.08);
}
.model-card.featured .badge {
position: absolute; top: 16px; right: 16px;
padding: 4px 12px;
border-radius: 12px;
background: linear-gradient(90deg, var(--accent-1), var(--accent-2));
color: white; font-size: 11px; font-weight: 600;
}
.model-name {
font-size: 22px; font-weight: 700;
margin-bottom: 4px;
}
.model-price {
font-size: 32px; font-weight: 800;
margin-bottom: 20px;
}
.model-price span {
color: var(--accent-1);
}
.model-price .currency {
font-size: 16px; font-weight: 600;
color: var(--accent-1);
}
.spec-list {
flex: 1;
display: flex; flex-direction: column; gap: 12px;
}
.spec-item {
display: flex; justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.spec-item:last-child { border-bottom: none; }
.spec-label {
font-size: 13px; color: var(--text-secondary);
}
.spec-value {
font-size: 14px; font-weight: 600;
}
.spec-value.highlight {
color: var(--accent-1);
}
/* 底部对比条 */
.compare-bar {
margin-top: auto;
padding: 14px 0 0;
border-top: 1px solid rgba(255,255,255,0.08);
}
.compare-bar .bar-label {
font-size: 11px; color: var(--text-secondary);
margin-bottom: 6px;
}
.bar-track {
width: 100%; height: 6px;
background: rgba(255,255,255,0.06);
border-radius: 3px; overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: 3px;
background: linear-gradient(90deg, var(--accent-1), var(--accent-2));
}
/* 页脚 */
.footer {
position: absolute; bottom: 16px; left: 40px; right: 40px;
display: flex; justify-content: space-between;
font-size: 12px; color: rgba(255,255,255,0.3);
}
</style>
</head>
<body>
<div class="page-title">
<div class="title-accent"></div>
<h2>三款车型配置对比</h2>
<span class="subtitle">标准版 / Pro / Max -- 全系仅涨价4000元</span>
</div>
<div class="content-area">
<!-- 标准版 -->
<div class="model-card">
<div class="model-name">标准版</div>
<div class="model-price"><span>21.99</span><span class="currency"> 万元</span></div>
<div class="spec-list">
<div class="spec-item">
<span class="spec-label">电机</span>
<span class="spec-value">V6s Plus</span>
</div>
<div class="spec-item">
<span class="spec-label">功率</span>
<span class="spec-value">320hp</span>
</div>
<div class="spec-item">
<span class="spec-label">电压平台</span>
<span class="spec-value">752V</span>
</div>
<div class="spec-item">
<span class="spec-label">CLTC续航</span>
<span class="spec-value highlight">720km</span>
</div>
<div class="spec-item">
<span class="spec-label">激光雷达</span>
<span class="spec-value highlight">标配</span>
</div>
</div>
<div class="compare-bar">
<div class="bar-label">续航能力</div>
<div class="bar-track"><div class="bar-fill" style="width:80%"></div></div>
</div>
</div>
<!-- Pro版 -->
<div class="model-card featured">
<div class="badge">最长续航</div>
<div class="model-name">Pro</div>
<div class="model-price"><span>24.99</span><span class="currency"> 万元</span></div>
<div class="spec-list">
<div class="spec-item">
<span class="spec-label">电机</span>
<span class="spec-value">V6s Plus</span>
</div>
<div class="spec-item">
<span class="spec-label">功率</span>
<span class="spec-value">320hp</span>
</div>
<div class="spec-item">
<span class="spec-label">电压平台</span>
<span class="spec-value">752V</span>
</div>
<div class="spec-item">
<span class="spec-label">CLTC续航</span>
<span class="spec-value highlight">902km</span>
</div>
<div class="spec-item">
<span class="spec-label">空气悬挂</span>
<span class="spec-value highlight">标配</span>
</div>
</div>
<div class="compare-bar">
<div class="bar-label">续航能力</div>
<div class="bar-track"><div class="bar-fill" style="width:100%"></div></div>
</div>
</div>
<!-- Max版 -->
<div class="model-card">
<div class="model-name">Max</div>
<div class="model-price"><span>30.39</span><span class="currency"> 万元</span></div>
<div class="spec-list">
<div class="spec-item">
<span class="spec-label">电机</span>
<span class="spec-value">双电机四驱</span>
</div>
<div class="spec-item">
<span class="spec-label">功率</span>
<span class="spec-value highlight">690hp</span>
</div>
<div class="spec-item">
<span class="spec-label">电压平台</span>
<span class="spec-value highlight">897V</span>
</div>
<div class="spec-item">
<span class="spec-label">CLTC续航</span>
<span class="spec-value">835km</span>
</div>
<div class="spec-item">
<span class="spec-label">零百加速</span>
<span class="spec-value highlight">3.08s</span>
</div>
</div>
<div class="compare-bar">
<div class="bar-label">续航能力</div>
<div class="bar-track"><div class="bar-fill" style="width:93%"></div></div>
</div>
</div>
</div>
<div class="footer">
<span>新一代小米SU7 | 三款车型配置对比</span>
<span>02 / 05</span>
</div>
</body>
</html>

View File

@@ -0,0 +1,258 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
:root {
--bg-primary: #1A1A1A;
--bg-secondary: #111111;
--card-bg-from: #2A2A2A;
--card-bg-to: #1A1A1A;
--card-border: rgba(255,105,0,0.15);
--card-radius: 16px;
--text-primary: #FFFFFF;
--text-secondary: rgba(255,255,255,0.65);
--accent-1: #FF6900;
--accent-2: #FF8C00;
--accent-3: #FFFFFF;
--accent-4: #E0E0E0;
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
width: 1280px; height: 720px; overflow: hidden;
background: linear-gradient(135deg, var(--bg-primary), var(--bg-secondary));
font-family: 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif;
color: var(--text-primary);
position: relative;
}
.page-title {
position: absolute; left: 40px; top: 20px;
width: 1200px; height: 50px;
display: flex; align-items: center; gap: 16px;
}
.title-accent {
width: 4px; height: 28px;
background: linear-gradient(180deg, var(--accent-1), var(--accent-2));
border-radius: 2px;
}
.page-title h2 { font-size: 26px; font-weight: 700; }
.page-title .subtitle {
font-size: 14px; color: var(--text-secondary); margin-left: auto;
}
/* 内容区 -- 主次结合(大+两小) */
.content-area {
position: absolute;
left: 40px; top: 80px;
width: 1200px; height: 580px;
display: grid;
grid-template: 1fr 1fr / 2fr 1fr;
gap: 20px;
}
.card {
border-radius: var(--card-radius);
background: linear-gradient(180deg, var(--card-bg-from), var(--card-bg-to));
border: 1px solid var(--card-border);
padding: 28px;
position: relative;
overflow: hidden;
}
/* 主卡片氛围底图极低透明度无需遮罩SVG/PPTX安全 */
.main-card .card-bg-img {
position: absolute;
bottom: 0; right: 0;
width: 40%; height: 45%;
object-fit: cover;
opacity: 0.06;
pointer-events: none;
z-index: 0;
border-radius: 0 0 var(--card-radius) 0;
}
.main-card > *:not(.card-bg-img) { position: relative; z-index: 1; }
/* 主卡片 -- 跨两行 */
.main-card {
grid-row: 1 / -1;
}
.main-card h3 {
font-size: 20px; font-weight: 700; margin-bottom: 20px;
}
.main-card h3 svg {
width: 20px; height: 20px; vertical-align: middle;
margin-right: 8px; fill: var(--accent-1);
}
/* 续航对比图 -- SVG柱形图 */
.range-chart {
width: 100%; height: 340px;
margin-top: 12px;
}
.range-chart svg { width: 100%; height: 100%; }
.chart-bar { rx: 6; }
.chart-label { font-size: 13px; fill: var(--text-secondary); font-family: inherit; }
.chart-value { font-size: 16px; fill: var(--text-primary); font-weight: 700; font-family: inherit; }
.chart-model { font-size: 14px; fill: var(--text-secondary); font-family: inherit; }
/* 对比说明 */
.compare-note {
margin-top: 20px;
padding: 14px 18px;
border-radius: 10px;
background: rgba(255,105,0,0.06);
border-left: 3px solid var(--accent-1);
}
.compare-note p {
font-size: 13px; color: var(--text-secondary); line-height: 1.7;
}
.compare-note strong { color: var(--accent-1); font-weight: 600; }
/* 辅助卡片 */
.side-card h3 {
font-size: 18px; font-weight: 700; margin-bottom: 16px;
}
.kpi-row {
display: flex; align-items: baseline; gap: 4px;
}
.kpi-number {
font-size: 48px; font-weight: 800;
color: var(--accent-1);
line-height: 1;
}
.kpi-unit {
font-size: 18px; color: var(--accent-2);
font-weight: 600;
}
.kpi-desc {
font-size: 13px; color: var(--text-secondary);
margin-top: 10px; line-height: 1.6;
}
/* 快充进度环 -- 纯CSS+内联SVG兼容管线 */
.charge-ring {
width: 120px; height: 120px; margin: 12px auto;
position: relative;
}
.charge-ring svg { width: 100%; height: 100%; }
.charge-ring-text {
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.charge-ring-text .ring-val {
font-size: 22px; font-weight: 700; color: var(--text-primary);
display: block;
}
.charge-ring-text .ring-lbl {
font-size: 10px; color: var(--text-secondary);
display: block;
}
.footer {
position: absolute; bottom: 16px; left: 40px; right: 40px;
display: flex; justify-content: space-between;
font-size: 12px; color: rgba(255,255,255,0.3);
}
</style>
</head>
<body>
<div class="page-title">
<div class="title-accent"></div>
<h2>动力与续航全面升级</h2>
<span class="subtitle">V6s Plus超级电机 / 高压快充平台</span>
</div>
<div class="content-area">
<!-- 主卡片: 续航对比柱形图 -->
<div class="card main-card">
<img class="card-bg-img" src="../images/su7_power.png" alt="">
<h3>
<svg viewBox="0 0 24 24"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
CLTC续航里程对比 (km)
</h3>
<div class="range-chart">
<svg viewBox="0 0 660 340">
<!-- 初代 vs 新款 对比柱 -->
<!-- 标准版 -->
<text class="chart-model" x="70" y="330" text-anchor="middle">标准版</text>
<rect class="chart-bar" x="30" y="102" width="35" height="210" fill="rgba(255,255,255,0.1)"/>
<text class="chart-label" x="47" y="95" text-anchor="middle">700</text>
<rect class="chart-bar" x="70" y="96" width="35" height="216" fill="url(#barGrad)"/>
<text class="chart-value" x="87" y="89" text-anchor="middle">720</text>
<!-- Pro版 -->
<text class="chart-model" x="250" y="330" text-anchor="middle">Pro</text>
<rect class="chart-bar" x="210" y="63" width="35" height="249" fill="rgba(255,255,255,0.1)"/>
<text class="chart-label" x="227" y="56" text-anchor="middle">830</text>
<rect class="chart-bar" x="250" y="41" width="35" height="271" fill="url(#barGrad)"/>
<text class="chart-value" x="267" y="34" text-anchor="middle">902</text>
<!-- Max版 -->
<text class="chart-model" x="430" y="330" text-anchor="middle">Max</text>
<rect class="chart-bar" x="390" y="72" width="35" height="240" fill="rgba(255,255,255,0.1)"/>
<text class="chart-label" x="407" y="65" text-anchor="middle">800</text>
<rect class="chart-bar" x="430" y="62" width="35" height="250" fill="url(#barGrad)"/>
<text class="chart-value" x="447" y="55" text-anchor="middle">835</text>
<!-- 图例 -->
<rect x="520" y="20" width="14" height="14" rx="3" fill="rgba(255,255,255,0.1)"/>
<text class="chart-label" x="540" y="31">初代SU7</text>
<rect x="520" y="44" width="14" height="14" rx="3" fill="var(--accent-1)"/>
<text class="chart-label" x="540" y="55">新一代SU7</text>
<defs>
<linearGradient id="barGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#FF6900"/>
<stop offset="100%" stop-color="#FF8C00"/>
</linearGradient>
</defs>
</svg>
</div>
<div class="compare-note">
<p>Pro版续航从830km跃升至<strong>902km</strong>提升幅度达8.7%;全系搭载<strong>V6s Plus超级电机</strong>,标准版/Pro版功率提升至320hp。</p>
</div>
</div>
<!-- 辅助卡片1: 0-100加速 -->
<div class="card side-card">
<h3>Max版零百加速</h3>
<div class="kpi-row">
<span class="kpi-number">3.08</span>
<span class="kpi-unit">s</span>
</div>
<p class="kpi-desc">双电机四驱系统897V高压架构最大功率690hp。前双叉臂+后多连杆悬挂,配备闭式双腔空气弹簧。</p>
</div>
<!-- 辅助卡片2: 快充能力 -->
<div class="card side-card">
<h3>超级快充补能</h3>
<div class="charge-ring">
<svg viewBox="0 0 120 120">
<defs>
<linearGradient id="ringGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#FF6900"/>
<stop offset="100%" stop-color="#FF8C00"/>
</linearGradient>
</defs>
<circle cx="60" cy="60" r="50" fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="8"/>
<circle cx="60" cy="60" r="50" fill="none" stroke="url(#ringGrad)" stroke-width="8"
stroke-linecap="round" stroke-dasharray="235 314"
transform="rotate(-90 60 60)"/>
</svg>
<div class="charge-ring-text">
<span class="ring-val">15</span>
<span class="ring-lbl">分钟</span>
</div>
</div>
<p class="kpi-desc" style="text-align:center">Max版15分钟快充即可补充<strong style="color:var(--accent-1)">670km</strong>续航里程,补能效率行业领先。</p>
</div>
</div>
<div class="footer">
<span>新一代小米SU7 | 动力与续航</span>
<span>03 / 05</span>
</div>
</body>
</html>

View File

@@ -0,0 +1,240 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
:root {
--bg-primary: #1A1A1A;
--bg-secondary: #111111;
--card-bg-from: #2A2A2A;
--card-bg-to: #1A1A1A;
--card-border: rgba(255,105,0,0.15);
--card-radius: 16px;
--text-primary: #FFFFFF;
--text-secondary: rgba(255,255,255,0.65);
--accent-1: #FF6900;
--accent-2: #FF8C00;
--accent-3: #FFFFFF;
--accent-4: #E0E0E0;
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
width: 1280px; height: 720px; overflow: hidden;
background: linear-gradient(135deg, var(--bg-primary), var(--bg-secondary));
font-family: 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif;
color: var(--text-primary);
position: relative;
}
.page-title {
position: absolute; left: 40px; top: 20px;
width: 1200px; height: 50px;
display: flex; align-items: center; gap: 16px;
}
.title-accent {
width: 4px; height: 28px;
background: linear-gradient(180deg, var(--accent-1), var(--accent-2));
border-radius: 2px;
}
.page-title h2 { font-size: 26px; font-weight: 700; }
.page-title .subtitle {
font-size: 14px; color: var(--text-secondary); margin-left: auto;
}
/* 英雄式布局: 顶部横 + 下方3列 */
.content-area {
position: absolute;
left: 40px; top: 80px;
width: 1200px; height: 580px;
display: grid;
grid-template: auto 1fr / repeat(3, 1fr);
gap: 20px;
}
.card {
border-radius: var(--card-radius);
background: linear-gradient(180deg, var(--card-bg-from), var(--card-bg-to));
border: 1px solid var(--card-border);
padding: 24px;
overflow: hidden;
}
/* 英雄卡片 -- 横跨3列 */
.hero-card {
grid-column: 1 / -1;
display: flex; align-items: center; gap: 32px;
padding: 24px 32px;
}
.hero-icon {
width: 100px; height: 100px; flex-shrink: 0;
border-radius: 16px;
overflow: hidden;
}
.hero-icon img {
width: 100%; height: 100%;
object-fit: cover;
border-radius: 16px;
}
.hero-text h3 {
font-size: 20px; font-weight: 700; margin-bottom: 8px;
}
.hero-text p {
font-size: 14px; color: var(--text-secondary); line-height: 1.7;
}
.hero-text strong { color: var(--accent-1); }
/* 传感器阵列标签 */
.sensor-tags {
display: flex; flex-wrap: wrap; gap: 8px;
margin-top: 10px;
}
.sensor-tag {
padding: 4px 12px;
border-radius: 20px;
border: 1px solid rgba(255,105,0,0.25);
font-size: 12px; color: var(--accent-2);
background: rgba(255,105,0,0.04);
}
/* 子卡片 */
.sub-card h4 {
font-size: 16px; font-weight: 700; margin-bottom: 12px;
display: flex; align-items: center; gap: 10px;
}
.sub-card h4 svg {
width: 18px; height: 18px; fill: var(--accent-1);
}
.feature-list {
list-style: none;
display: flex; flex-direction: column; gap: 10px;
}
.feature-list li {
font-size: 13px; color: var(--text-secondary);
line-height: 1.6;
padding-left: 16px;
position: relative;
}
.feature-list li .dot {
position: absolute; left: 0; top: 7px;
width: 6px; height: 6px;
border-radius: 50%;
background: var(--accent-1);
}
/* 安全数据卡 */
.safety-data {
display: flex; flex-direction: column; gap: 14px;
margin-top: 4px;
}
.safety-item {
display: flex; align-items: center; gap: 12px;
}
.safety-num {
font-size: 28px; font-weight: 800;
color: var(--accent-1);
min-width: 80px;
}
.safety-num .unit {
font-size: 14px;
}
.safety-desc {
font-size: 12px; color: var(--text-secondary);
}
.footer {
position: absolute; bottom: 16px; left: 40px; right: 40px;
display: flex; justify-content: space-between;
font-size: 12px; color: rgba(255,255,255,0.3);
}
</style>
</head>
<body>
<div class="page-title">
<div class="title-accent"></div>
<h2>智能驾驶与安全配置</h2>
<span class="subtitle">全系标配激光雷达 / 700TOPS算力平台</span>
</div>
<div class="content-area">
<!-- 英雄卡: 智驾总览 -->
<div class="card hero-card">
<div class="hero-icon">
<img src="../images/su7_smart.png" alt="">
</div>
<div class="hero-text">
<h3>Xiaomi HAD 全场景智能驾驶</h3>
<p>全系标配<strong>700 TOPS</strong>高算力智能驾驶芯片,搭载<strong>XLA认知大模型</strong>全新架构,支持端到端全场景智驾。硬件从顶配下放至全系,实现真正的"智驾平权"。</p>
<div class="sensor-tags">
<span class="sensor-tag">128线激光雷达</span>
<span class="sensor-tag">4D毫米波雷达</span>
<span class="sensor-tag">11颗高清摄像头</span>
<span class="sensor-tag">12颗超声波雷达</span>
<span class="sensor-tag">XLA大模型</span>
</div>
</div>
</div>
<!-- 子卡1: 主动安全 -->
<div class="card sub-card">
<h4>
<svg viewBox="0 0 24 24"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/></svg>
被动安全
</h4>
<ul class="feature-list">
<li><span class="dot"></span>铠甲笼式钢铝混合车身</li>
<li><span class="dot"></span>2200MPa小米超强钢关键部位</li>
<li><span class="dot"></span>高强钢+铝合金占比90.3%</li>
<li><span class="dot"></span>全系9安全气囊+后排侧气囊)</li>
<li><span class="dot"></span>三重冗余应急开门设计</li>
</ul>
</div>
<!-- 子卡2: 智能安全 -->
<div class="card sub-card">
<h4>
<svg viewBox="0 0 24 24"><path d="M20.57 14.86L22 13.43 20.57 12 17 15.57 8.43 7 12 3.43 10.57 2 9.14 3.43 7.71 2 5.57 4.14 4.14 2.71 2.71 4.14l1.43 1.43L2 7.71l1.43 1.43L2 10.57 3.43 12 7 8.43 15.57 17 12 20.57 13.43 22l1.43-1.43L16.29 22l2.14-2.14 1.43 1.43 1.43-1.43-1.43-1.43L22 16.29z"/></svg>
主动安全
</h4>
<ul class="feature-list">
<li><span class="dot"></span>MAI 0速起步误加速抑制</li>
<li><span class="dot"></span>LAEB 前向低速防碰撞</li>
<li><span class="dot"></span>RAEB 后向低速防碰撞</li>
<li><span class="dot"></span>全速域主动安全防护</li>
<li><span class="dot"></span>智驾保障最高300万保额</li>
</ul>
</div>
<!-- 子卡3: 安全数据 -->
<div class="card sub-card">
<h4>
<svg viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z"/></svg>
关键安全数据
</h4>
<div class="safety-data">
<div class="safety-item">
<span class="safety-num">9<span class="unit"></span></span>
<span class="safety-desc">全系标配安全气囊数</span>
</div>
<div class="safety-item">
<span class="safety-num">2200<span class="unit">MPa</span></span>
<span class="safety-desc">超强钢最高强度</span>
</div>
<div class="safety-item">
<span class="safety-num">90.3<span class="unit">%</span></span>
<span class="safety-desc">高强度材料占比</span>
</div>
<div class="safety-item">
<span class="safety-num">300<span class="unit"></span></span>
<span class="safety-desc">智驾保障最高保额</span>
</div>
</div>
</div>
</div>
<div class="footer">
<span>新一代小米SU7 | 智能驾驶与安全</span>
<span>04 / 05</span>
</div>
</body>
</html>

View File

@@ -0,0 +1,216 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
:root {
--bg-primary: #1A1A1A;
--bg-secondary: #111111;
--card-bg-from: #2A2A2A;
--card-bg-to: #1A1A1A;
--card-border: rgba(255,105,0,0.15);
--card-radius: 16px;
--text-primary: #FFFFFF;
--text-secondary: rgba(255,255,255,0.65);
--accent-1: #FF6900;
--accent-2: #FF8C00;
--accent-3: #FFFFFF;
--accent-4: #E0E0E0;
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
width: 1280px; height: 720px; overflow: hidden;
background: linear-gradient(135deg, var(--bg-primary), var(--bg-secondary));
font-family: 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif;
color: var(--text-primary);
position: relative;
}
/* 背景装饰 -- 呼应封面 */
.bg-glow {
position: absolute;
width: 800px; height: 800px;
border-radius: 50%;
background: radial-gradient(circle, rgba(255,105,0,0.07) 0%, transparent 70%);
bottom: -300px; left: -200px;
pointer-events: none;
}
.bg-glow-2 {
position: absolute;
width: 500px; height: 500px;
border-radius: 50%;
background: radial-gradient(circle, rgba(255,140,0,0.05) 0%, transparent 70%);
top: -150px; right: -100px;
pointer-events: none;
}
/* 中央结束内容 */
.end-content {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
text-align: center;
width: 900px;
}
.end-badge {
display: inline-block;
padding: 6px 20px;
border-radius: 20px;
border: 1px solid rgba(255,105,0,0.3);
color: var(--accent-1);
font-size: 13px; font-weight: 600;
letter-spacing: 2px;
margin-bottom: 28px;
}
.end-title {
font-size: 44px; font-weight: 800;
line-height: 1.3;
margin-bottom: 32px;
}
.end-title span {
color: var(--accent-1);
}
/* 核心要点横排 */
.highlights {
display: flex; justify-content: center; gap: 40px;
margin-bottom: 40px;
}
.highlight-item {
text-align: center;
}
.highlight-num {
font-size: 36px; font-weight: 800;
color: var(--accent-1);
line-height: 1;
}
.highlight-unit {
font-size: 14px; color: var(--accent-2);
}
.highlight-label {
font-size: 13px; color: var(--text-secondary);
margin-top: 6px;
}
/* 分隔线 */
.divider {
width: 80px; height: 2px;
background: linear-gradient(90deg, transparent, var(--accent-1), transparent);
margin: 0 auto 28px;
}
/* 未来展望 */
.outlook {
font-size: 16px; color: var(--text-secondary);
line-height: 1.8;
max-width: 700px;
margin: 0 auto;
}
.outlook strong { color: var(--accent-1); }
/* 产品线标签 */
.product-line {
display: flex; justify-content: center; gap: 12px;
margin-top: 24px;
}
.product-tag {
padding: 6px 16px;
border-radius: 20px;
border: 1px solid rgba(255,255,255,0.12);
font-size: 12px; color: var(--text-secondary);
background: rgba(255,255,255,0.03);
}
.product-tag.active {
border-color: var(--accent-1);
color: var(--accent-1);
background: rgba(255,105,0,0.06);
}
/* 品牌底部 */
.brand-footer {
position: absolute; bottom: 30px; left: 0; right: 0;
text-align: center;
}
.brand-logo {
display: inline-flex; align-items: center; gap: 10px;
margin-bottom: 8px;
}
.brand-logo .logo-box {
width: 28px; height: 28px;
border-radius: 6px;
background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
display: flex; align-items: center; justify-content: center;
}
.brand-logo .logo-box svg { width: 16px; height: 16px; }
.brand-logo .logo-text {
font-size: 14px; font-weight: 600;
color: var(--text-secondary); letter-spacing: 1px;
}
.brand-copyright {
font-size: 11px; color: rgba(255,255,255,0.25);
}
.footer-page {
position: absolute; bottom: 16px; right: 40px;
font-size: 12px; color: rgba(255,255,255,0.3);
}
</style>
</head>
<body>
<div class="bg-glow"></div>
<div class="bg-glow-2"></div>
<div class="end-content">
<div class="end-badge">MARKET & OUTLOOK</div>
<div class="end-title">
市场热销 <span>未来可期</span>
</div>
<div class="highlights">
<div class="highlight-item">
<div class="highlight-num">1.5<span class="highlight-unit">万+</span></div>
<div class="highlight-label">34分钟锁单量</div>
</div>
<div class="highlight-item">
<div class="highlight-num">55<span class="highlight-unit">万辆</span></div>
<div class="highlight-label">2026年销量目标</div>
</div>
<div class="highlight-item">
<div class="highlight-num">41<span class="highlight-unit">万辆</span></div>
<div class="highlight-label">2025年实际交付</div>
</div>
<div class="highlight-item">
<div class="highlight-num">+34<span class="highlight-unit">%</span></div>
<div class="highlight-label">目标同比增幅</div>
</div>
</div>
<div class="divider"></div>
<p class="outlook">
2026年小米汽车规划<strong>四款新产品</strong>新款SU7、SU7行政版、以及两款全新增程式SUVYU8、YU9持续扩展产品矩阵<strong>年销55万辆</strong>目标全力冲刺。
</p>
<div class="product-line">
<span class="product-tag active">新款SU7</span>
<span class="product-tag">SU7行政版</span>
<span class="product-tag">YU8 (SUV)</span>
<span class="product-tag">YU9 (SUV)</span>
</div>
</div>
<div class="brand-footer">
<div class="brand-logo">
<div class="logo-box">
<svg viewBox="0 0 24 24" fill="white"><path d="M2 8h20v8H2z" opacity="0.8"/><path d="M6 4h12v16H6z" opacity="0.6"/></svg>
</div>
<span class="logo-text">XIAOMI AUTO</span>
</div>
<div class="brand-copyright">2026 Xiaomi Automobile Technology Co., Ltd.</div>
</div>
<div class="footer-page">05 / 05</div>
</body>
</html>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 64 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 48 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 155 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 120 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -0,0 +1,186 @@
# HTML -> SVG -> PPTX 管线兼容性规则
本文档汇总所有管线兼容性教训。**HTML 设计稿生成时必须遵守,在源头规避偏移问题。**
核心原则:**html2svg + svg2pptx 不是浏览器**,很多 CSS 特性和 SVG 属性在转换过程中会丢失或产生偏移。HTML 写法必须考虑到下游转换器的能力边界。
---
## 1. CSS 禁止清单
| 禁止特性 | 转换后现象 | 正确替代写法 |
|---------|---------|-----------|
| `background-clip: text` | 渐变变色块 + 白色文字 | `color: var(--accent-1)` 直接上色 |
| `-webkit-text-fill-color` | 文字颜色丢失 | 标准 `color` 属性 |
| `mask-image` / `-webkit-mask-image` | 图片完全消失 | `<div>` 遮罩层linear-gradient 背景) |
| `::before` / `::after`(视觉装饰用) | 内容消失 | 真实 `<div>` / `<span>` |
| `conic-gradient` | 不渲染 | 内联 SVG `<circle>` + stroke-dasharray |
| CSS border 三角形 (width:0 trick) | 形状丢失 | 内联 SVG `<polygon>` |
| `mix-blend-mode` | 不支持 | `opacity` 叠加 |
| `filter: blur()` | 光栅化变位图 | `opacity``box-shadow` |
| `content: '文字'` | 文字消失 | 真实 `<span>` |
| CSS `background-image: url(...)` | dom-to-svg 忽略 | `<img>` 标签 |
html2svg.py 兜底覆盖前3项 + 伪元素 + conic-gradient + border三角形共6种但兜底效果远不如正确写法。
---
## 2. 防偏移写法(关键章节)
svg2pptx 的文本定位基于 SVG text 元素的坐标,但 PPTX textbox 的坐标系与 SVG 不同SVG text y = baselinePPTX y = textbox 顶部)。以下写法可从 HTML 源头避免偏移:
### 2.1 内联 SVG 中的文本标注 -- 用 HTML 叠加替代 SVG text
**问题**:内联 SVG 中的 `<text>` 元素经过 dom-to-svg 转换后坐标是 viewBox 坐标系svg2pptx 在处理 baseline 偏移和 text-anchor 居中时有精度损失(约 +/- 3-5px导致标注位置偏移。
**HTML 防偏移写法**:把文字标注从 SVG `<text>` 移出来,用 HTML `<div>` 绝对定位叠加在 SVG 上方。HTML div 由 dom-to-svg 精确定位,不经过 viewBox 坐标转换,偏移风险为零。
```html
<!-- 正确HTML div 叠加标注,零偏移 -->
<div class="chart-container" style="position: relative;">
<svg viewBox="0 0 660 340" style="width:100%; height:100%;">
<!-- 只画柱子、线条等图形元素,不写 <text> -->
<rect x="80" y="100" width="60" height="200" fill="#FF6900"/>
</svg>
<!-- 标注用 HTML 绝对定位叠加 -->
<span style="position:absolute; left:12.5%; top:25%; font-size:14px; color:#fff;">720</span>
<span style="position:absolute; left:12.5%; bottom:5%; font-size:12px; color:rgba(255,255,255,0.6);">标准版</span>
</div>
```
```html
<!-- 禁止SVG text 在 PPTX 中会偏移 -->
<svg viewBox="0 0 660 340">
<rect x="80" y="100" width="60" height="200" fill="#FF6900"/>
<text x="110" y="90" text-anchor="middle" fill="#fff">720</text>
</svg>
```
### 2.2 不同字号混排 -- 必须用 flex 独立元素
**问题**:大小字号内嵌(`<div class="big">3.08<span class="small">s</span></div>`)经 dom-to-svg 转为独立 tspan 后svg2pptx 给每个 tspan 按各自字号做 baseline 偏移,小字会上移。
```html
<!-- 正确flex baseline 对齐 -->
<div style="display:flex; align-items:baseline; gap:4px;">
<span style="font-size:48px;">3.08</span>
<span style="font-size:18px;">s</span>
</div>
```
```html
<!-- 禁止:内嵌不同字号 span -->
<div class="big">3.08<span class="small">s</span></div>
```
### 2.3 环形图(圆弧进度条)-- SVG 画弧 + HTML 叠加文字
```html
<!-- 正确:环形图最佳实践 -->
<div class="ring-container" style="position: relative; width:120px; height:120px;">
<!-- SVG 只画圆环弧线 -->
<svg viewBox="0 0 120 120" style="width:100%; height:100%;">
<!-- 底圈 -->
<circle cx="60" cy="60" r="50" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="8"/>
<!-- 弧线:用 dasharray 两值格式,禁止 dashoffset -->
<circle cx="60" cy="60" r="50" fill="none" stroke="#FF6900" stroke-width="8"
stroke-dasharray="235 314" stroke-linecap="round"
transform="rotate(-90 60 60)"/>
</svg>
<!-- 中心文字用 HTML 叠加,不用 SVG text -->
<div style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); text-align:center;">
<div style="font-size:22px; font-weight:700; color:#fff;">15</div>
<div style="font-size:10px; color:rgba(255,255,255,0.6);">分钟</div>
</div>
</div>
```
### 2.4 图例标签 -- 用 HTML flex 布局
```html
<!-- 正确HTML flex 图例,不用 SVG text -->
<div style="display:flex; gap:16px; font-size:12px;">
<div style="display:flex; align-items:center; gap:4px;">
<span style="display:inline-block; width:12px; height:12px; background:#999; border-radius:2px;"></span>
<span style="color:rgba(255,255,255,0.6);">初代SU7</span>
</div>
<div style="display:flex; align-items:center; gap:4px;">
<span style="display:inline-block; width:12px; height:12px; background:#FF6900; border-radius:2px;"></span>
<span style="color:rgba(255,255,255,0.6);">新一代SU7</span>
</div>
</div>
```
### 2.5 x 轴标签(标准版/Pro/Max-- 用 HTML 容器
```html
<!-- 正确: x 轴标签用 HTML -->
<div style="display:flex; justify-content:space-around; padding:0 10%;">
<span style="font-size:13px; color:rgba(255,255,255,0.6);">标准版</span>
<span style="font-size:13px; color:rgba(255,255,255,0.6);">Pro</span>
<span style="font-size:13px; color:rgba(255,255,255,0.6);">Max</span>
</div>
```
---
## 3. 图片路径
| 场景 | 错误写法 | 正确写法 |
|------|---------|---------|
| img src 引用 | 依赖浏览器 resolve | html2svg 以 HTML 文件所在目录为基准 resolve 相对路径 |
| CSS background-image | 会被 dom-to-svg 忽略 | 用 `<img>` 标签 |
---
## 4. SVG circle 环形图属性
| 属性 | svg2pptx 支持 | 说明 |
|------|-------------|------|
| `stroke-dasharray="arc gap"` | 支持 | 用两个值:弧线长度 + 间隔长度 |
| `stroke-dashoffset` | **不支持** | 禁止使用,改用 dasharray 的两值格式 |
| `stroke-linecap="round"` | 支持 | 圆角弧端 |
| `transform="rotate(-90 cx cy)"` | 支持 | 从12点钟方向开始 |
正确弧线写法:`stroke-dasharray="235 314"` (弧长=235, 圆周=2*pi*50=314
---
## 5. 底层氛围图
| 项目 | 规则 |
|------|------|
| opacity | 0.05 - 0.10(卡片内)/ 0.25 - 0.40(封面页) |
| 尺寸 | 限制在容器 40-60%,不要全覆盖 |
| z-index | 必须为 0 或 -1 |
| 实现方式 | 极低 opacity直接 `<img>` + opacity |
| | 封面级渐隐:`<div>` 容器内 img + 遮罩 div |
| **禁止** | div 遮罩在 PPTX 中层叠不可靠时,回退到纯 opacity |
---
## 6. 配图技法管线安全等级
| 技法 | 管线安全 | 原因 |
|------|---------|------|
| 渐隐融合div遮罩 | 安全 | 真实 div + linear-gradient |
| 色调蒙版 | 安全 | 真实 div + 半透明背景 |
| 氛围底图 | 最安全 | 纯 opacity |
| 裁切视窗 | 安全 | overflow:hidden + div 渐变 |
| 圆形裁切 | 安全 | border-radius |
| ~~CSS mask-image~~ | **禁止** | dom-to-svg 不支持 |
---
## 7. 总结HTML 设计稿防偏移 checklist
生成每页 HTML 时,对照以下清单:
- [ ] CSS 禁止清单中的特性未使用
- [ ] 所有图片用 `<img>` 标签,不用 CSS background-image
- [ ] 内联 SVG 中**不含 `<text>` 元素**,所有文字标注用 HTML div 叠加
- [ ] 不同字号混排用 flex + 独立 span不用嵌套 span
- [ ] 环形图用 stroke-dasharray 两值格式,不用 dashoffset
- [ ] 图例、x轴标签、数据标注全部用 HTML 元素,不用 SVG text
- [ ] 底层配图用低 opacity `<img>` 或 div 遮罩
- [ ] 伪元素 `::before`/`::after` 装饰已用真实元素替代

View File

@@ -410,9 +410,10 @@
- 关键词: 用 <strong> 或 <span class="highlight"> 包裹(背景 accent-primary 10% 透明度)
### data数据卡片
- 核心数字: font-size=36-48px, font-weight=800, 使用 accent-primary 渐变色(background: linear-gradient; -webkit-background-clip 除外)
- SVG 友好替代: 直接用 color=accent-primary不要用 -webkit-background-clip: text
- 单位/标签: font-size=14-16px, color=text-secondary
- 核心数字: font-size=36-48px, font-weight=800, **直接用 `color: var(--accent-1)`**
- **禁止** `background-clip: text` + `-webkit-text-fill-color: transparent` 渐变文字SVG转换后变成橙色色块+白色文字)
- html2svg.py 有兜底自动修复,但会丢失渐变效果只保留主色
- 单位/标签: font-size=14-16px, color=text-secondary 或 color=accent-2
- 补充说明: font-size=13px, 在数字下方
### list列表卡片
@@ -520,7 +521,9 @@
**核心原则**:图片是**氛围的一部分**,不是独立的内容块。
#### 5 种融入技法(全部管线安全)
> **SVG 管线兼容警告**:所有渐隐/遮罩效果必须用 **真实 `<div>` 遮罩层** 实现(`linear-gradient` 背景的 div 叠加在图片上方)。**禁止使用 CSS `mask-image` / `-webkit-mask-image`**,该属性在 dom-to-svg 转换中完全丢失。html2svg.py 有兜底(自动降级为 opacity但效果远不如 div 遮罩精细。
#### 5 种融入技法(全部管线安全 -- 均使用 div 遮罩而非 mask-image
##### 1. 渐隐融合 -- 封面页/章节封面的首选
@@ -599,12 +602,25 @@
- 渐变遮罩用**真实 `<div>`**(禁用 ::before/::after
- `object-fit: cover``border-radius` 与容器一致
- 图片使用**绝对路径**(由 agent 生成图片后填入)
- 底层氛围图的 opacity 必须足够低0.05-0.15),尺寸限制在容器的 45-60%,避免遮挡前景内容
**禁止**
- 禁止使用 CSS `mask-image` / `-webkit-mask-image`SVG 转换后完全丢失,必须用 div 遮罩层替代)
- 禁止使用 `-webkit-background-clip: text`SVG 中渐变变色块,必须用 `color` 直接上色)
- 禁止使用 `-webkit-text-fill-color`SVG 不识别,必须用标准 `color` 属性)
- 禁止图片直接裸露在卡片角落(无融入效果)
- 禁止图片占据整个卡片且无蒙版(文字不可读)
- 禁止图片与背景色有明显的矩形边界线
#### 内联 SVG 防偏移约束(详见 `pipeline-compat.md` 第 2 章)
svg2pptx 对 SVG `<text>` 元素的 baseline/text-anchor 定位有精度损失(+/- 3-5px会导致文字标注在 PPTX 中偏移。以下规则从 HTML 源头避免偏移:
1. **内联 SVG 中禁止写 `<text>` 元素**。所有文字标注数据标注、x 轴标签、图例文字、环形图中心文字)必须用 HTML `<div>` / `<span>` 绝对定位叠加在 SVG 上方
2. **不同字号混排必须用 flex 独立元素**`display:flex; align-items:baseline; gap:4px`),禁止嵌套不同字号的 span
3. **环形图中心文字用 HTML position:absolute 叠加**,不写在 SVG `<text>`
4. **SVG circle 弧线用 `stroke-dasharray="弧长 间隔"` 两值格式**,禁止 `stroke-dashoffset`
## 对比度安全规则(必须遵守)
文字颜色必须与其直接背景形成足够对比度,否则用户看不清:
@@ -895,7 +911,7 @@ Step 3 -> 使用 Prompt #2大纲架构师
Step 4 -> 使用 Prompt #3内容分配与策划稿
Step 5a -> 使用 style-system.md 选择风格
Step 5b -> 如有 generate_image为每页生成配图
Step 5c -> 使用 Prompt #4HTML 设计稿),逐页生成
后处理 -> scripts/html_packager.py 合并预览 + scripts/html2svg.py 转 SVG
Step 5c -> 使用 Prompt #4HTML 设计稿),逐页生成。**必须遵守 `pipeline-compat.md` 中的 CSS 禁止清单和管线兼容性规则**
后处理 -> scripts/html_packager.py 合并预览 + scripts/html2svg.py 转 SVG + scripts/svg2pptx.py 转 PPTX
可选 -> 使用 Prompt #5演讲备注
```

View File

@@ -56,16 +56,24 @@ const path = require('path');
});
const imgDataMap = {};
const htmlDir = path.dirname(item.html); // HTML文件所在目录
for (const src of imgSrcs) {
if (!src) continue;
// 处理 file:// 和绝对路径
if (src.startsWith('data:')) continue; // 跳过已内联的
// 处理 file:// 和绝对/相对路径
let filePath = src;
if (filePath.startsWith('file://')) filePath = filePath.slice(7);
// 相对路径以HTML文件所在目录为基准resolve
if (!path.isAbsolute(filePath)) {
filePath = path.resolve(htmlDir, filePath);
}
if (fs.existsSync(filePath)) {
const data = fs.readFileSync(filePath);
const ext = path.extname(filePath).slice(1) || 'png';
const mime = ext === 'jpg' ? 'image/jpeg' : `image/${ext}`;
imgDataMap[src] = `data:${mime};base64,${data.toString('base64')}`;
} else {
console.warn('Image not found:', filePath, '(src:', src, ')');
}
}
@@ -287,6 +295,82 @@ const path = require('path');
svg.appendChild(polygon);
el.appendChild(svg);
}
// 4. 修复 background-clip: text 渐变文字
// dom-to-svg 不支持此特性,导致渐变背景变成色块、文字变白
for (const el of document.querySelectorAll('*')) {
const cs = getComputedStyle(el);
const bgClip = cs.webkitBackgroundClip || cs.backgroundClip || '';
if (bgClip !== 'text') continue;
// 提取渐变/背景中的主色作为文字颜色
const bgImage = cs.backgroundImage || '';
let mainColor = '#FF6900'; // fallback
const colorMatch = bgImage.match(/(#[0-9a-fA-F]{3,8}|rgb[a]?\([^)]+\))/);
if (colorMatch) mainColor = colorMatch[1];
// 清除渐变背景效果,改用直接 color
el.style.backgroundImage = 'none';
el.style.background = 'none';
el.style.webkitBackgroundClip = 'border-box';
el.style.backgroundClip = 'border-box';
el.style.webkitTextFillColor = 'unset';
el.style.color = mainColor;
console.warn('html2svg fallback: background-clip:text -> color:' + mainColor, el.tagName);
}
// 5. 修复 -webkit-text-fill-color非 background-clip:text 的独立使用)
for (const el of document.querySelectorAll('*')) {
const cs = getComputedStyle(el);
const fillColor = cs.webkitTextFillColor;
if (!fillColor || fillColor === cs.color) continue;
// 如果 text-fill-color 与 color 不同SVG 中会丢失
// 将 text-fill-color 值应用到 color
if (fillColor !== 'rgba(0, 0, 0, 0)' && fillColor !== 'transparent') {
el.style.color = fillColor;
el.style.webkitTextFillColor = 'unset';
}
}
// 6. 修复 mask-image / -webkit-mask-imageSVG 不支持)
// 根据元素层级智能降级:底层图片降透明度,前景元素直接移除蒙版
for (const el of document.querySelectorAll('*')) {
const cs = getComputedStyle(el);
const maskImg = cs.maskImage || cs.webkitMaskImage || '';
if (!maskImg || maskImg === 'none') continue;
// 清除 mask
el.style.maskImage = 'none';
el.style.webkitMaskImage = 'none';
// 判断是否为底层装饰图片(通过 z-index、pointer-events、opacity 推断)
const zIndex = parseInt(cs.zIndex) || 0;
const pointerEvents = cs.pointerEvents;
const isImg = el.tagName === 'IMG';
const currentOpacity = parseFloat(cs.opacity) || 1;
if (isImg || pointerEvents === 'none' || zIndex <= 0) {
// 底层氛围图:降低透明度 + 限制尺寸,不要遮挡内容
const newOpacity = Math.min(currentOpacity, 0.15);
el.style.opacity = String(newOpacity);
// 如果图片过大,限制为容器的合理比例
if (isImg) {
const parent = el.parentElement;
if (parent) {
const parentRect = parent.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
if (elRect.width > parentRect.width * 0.8) {
el.style.maxWidth = '60%';
el.style.maxHeight = '60%';
}
}
}
console.warn('html2svg fallback: mask-image -> opacity:' + newOpacity + ' (background layer)', el.tagName);
} else {
// 前景元素:只移除蒙版,保持原样
console.warn('html2svg fallback: mask-image removed (foreground)', el.tagName);
}
}
});
await new Promise(r => setTimeout(r, 300));

View File

@@ -429,7 +429,7 @@ class SvgConverter:
break
if sp_tree is None:
return
self._walk(root, sp_tree, 0, 0, 1.0, slide)
self._walk(root, sp_tree, 0, 0, 1.0, 1.0, slide)
def _parse_grads(self, root):
self.grads = {}
@@ -487,48 +487,49 @@ class SvgConverter:
sy = float(m.group(4))
return dx, dy, sx, sy
def _walk(self, el, sp, ox, oy, group_opacity, slide):
def _walk(self, el, sp, ox, oy, group_opacity, scale, slide):
tag = self._tag(el)
try:
if tag == 'rect':
self._rect(el, sp, ox, oy, group_opacity, slide)
self._rect(el, sp, ox, oy, group_opacity, scale, slide)
elif tag == 'text':
self._text(el, sp, ox, oy, group_opacity)
self._text(el, sp, ox, oy, group_opacity, scale)
elif tag == 'circle':
self._circle(el, sp, ox, oy, group_opacity)
self._circle(el, sp, ox, oy, group_opacity, scale)
elif tag == 'ellipse':
self._ellipse(el, sp, ox, oy, group_opacity)
self._ellipse(el, sp, ox, oy, group_opacity, scale)
elif tag == 'line':
self._line(el, sp, ox, oy)
self._line(el, sp, ox, oy, scale)
elif tag == 'path':
self._path(el, sp, ox, oy, group_opacity)
self._path(el, sp, ox, oy, group_opacity, scale)
elif tag == 'image':
self._image(el, sp, ox, oy, slide)
self._image(el, sp, ox, oy, group_opacity, scale, slide)
elif tag == 'g':
dx, dy, sx, sy = self._parse_transform(el)
el_opacity = float(el.get('opacity', '1'))
child_opacity = group_opacity * el_opacity
# scale 只应用于 delta不缩放父级偏移
new_ox = ox + dx
new_oy = oy + dy
# scale 累积父级scale * 当前g的scale
child_scale = scale * sx # 假设sx==sy等比缩放
new_ox = ox + dx * scale
new_oy = oy + dy * scale
for c in el:
self._walk(c, sp, new_ox, new_oy,
child_opacity, slide)
child_opacity, child_scale, slide)
elif tag in ('defs', 'style', 'linearGradient', 'radialGradient',
'stop', 'pattern', 'clipPath', 'filter', 'mask'):
pass # 跳过定义元素(不跳过被 mask 的内容元素)
pass
else:
for c in el:
self._walk(c, sp, ox, oy, group_opacity, slide)
self._walk(c, sp, ox, oy, group_opacity, scale, slide)
except Exception as e:
self.stats['errors'] += 1
print(f" Warning: {tag} element failed: {e}", file=sys.stderr)
def _rect(self, el, sp, ox, oy, opacity, slide):
x = float(el.get('x', 0)) + ox
y = float(el.get('y', 0)) + oy
w = float(el.get('width', 0))
h = float(el.get('height', 0))
def _rect(self, el, sp, ox, oy, opacity, scale, slide):
x = (float(el.get('x', 0)) * scale) + ox
y = (float(el.get('y', 0)) * scale) + oy
w = float(el.get('width', 0)) * scale
h = float(el.get('height', 0)) * scale
if w <= 0 or h <= 0:
return
@@ -573,13 +574,14 @@ class SvgConverter:
sp.append(shape)
self.stats['shapes'] += 1
def _text(self, el, sp, ox, oy, opacity):
def _text(self, el, sp, ox, oy, opacity, scale):
"""每个 tspan 保持独立文本框,保留精确 x/y 坐标。"""
fill_s = el.get('fill', el.get('color', ''))
fsz = el.get('font-size', '14px').replace('px', '')
fw = el.get('font-weight', '')
ff = el.get('font-family', '')
baseline = el.get('dominant-baseline', '')
anchor = el.get('text-anchor', 'start')
tspans = list(el.findall(f'{{{SVG_NS}}}tspan'))
@@ -588,22 +590,30 @@ class SvgConverter:
txt = ts.text
if not txt or not txt.strip():
continue
x = float(ts.get('x', 0)) + ox
y = float(ts.get('y', 0)) + oy
x = float(ts.get('x', 0)) * scale + ox
y = float(ts.get('y', 0)) * scale + oy
tlen = float(ts.get('textLength', 0))
ts_fsz = ts.get('font-size', fsz).replace('px', '')
ts_fw = ts.get('font-weight', fw)
ts_fill = ts.get('fill', fill_s)
ts_ff = ts.get('font-family', ff)
fh = float(ts_fsz)
# baseline偏移: text-after-edge -> y是底部减全高; auto -> y是baseline减85%
if 'after-edge' in baseline:
y -= fh
else:
y -= fh * 0.85
c = parse_color(ts_fill)
hex6 = c[0] if c and c[0] != 'grad' else '000000'
alpha = c[1] if c and c[0] != 'grad' else 100000
alpha = int(alpha * opacity)
cx_v = px(tlen) if tlen > 0 else px(len(txt) * float(ts_fsz) * 0.7)
cy_v = px(fh * 1.5)
# text-anchor 偏移: middle -> x减半宽, end -> x减全宽
if anchor == 'middle':
x -= cx_v / EMU_PX / 2
elif anchor == 'end':
x -= cx_v / EMU_PX
run = {
'text': txt.strip(), 'sz': font_sz(ts_fsz),
'bold': ts_fw in ('bold', '700', '800', '900'),
@@ -616,16 +626,25 @@ class SvgConverter:
self.stats['shapes'] += 1
elif el.text and el.text.strip():
x = float(el.get('x', 0)) + ox
y = float(el.get('y', 0)) + oy
x = float(el.get('x', 0)) * scale + ox
y = float(el.get('y', 0)) * scale + oy
fh = float(fsz)
# baseline偏移
if 'after-edge' in baseline:
y -= fh
else:
y -= fh * 0.85
c = parse_color(fill_s)
hex6 = c[0] if c and c[0] != 'grad' else '000000'
alpha = c[1] if c and c[0] != 'grad' else 100000
alpha = int(alpha * opacity)
txt = el.text.strip()
txt_w = len(txt) * float(fsz) * 0.7
# text-anchor 偏移
if anchor == 'middle':
x -= txt_w / 2
elif anchor == 'end':
x -= txt_w
run = {
'text': txt, 'sz': font_sz(fsz),
'bold': fw in ('bold', '700', '800', '900'),
@@ -638,10 +657,10 @@ class SvgConverter:
sp.append(shape)
self.stats['shapes'] += 1
def _circle(self, el, sp, ox, oy, opacity):
cx_v = float(el.get('cx', 0)) + ox
cy_v = float(el.get('cy', 0)) + oy
r = float(el.get('r', 0))
def _circle(self, el, sp, ox, oy, opacity, scale):
cx_v = float(el.get('cx', 0)) * scale + ox
cy_v = float(el.get('cy', 0)) * scale + oy
r = float(el.get('r', 0)) * scale
if r <= 0 or r < 2:
self.stats['skipped'] += 1
return
@@ -686,10 +705,25 @@ class SvgConverter:
av.append(_el('a:gd', {'name': 'adj2', 'fmla': f'val {adj2}'}))
geom.append(av)
# 描边颜色 = SVG 的 stroke 颜色
# 描边颜色 = SVG 的 stroke 颜色(支持渐变引用)
stroke_color = parse_color(stroke_s)
ln_children = []
if stroke_color and stroke_color[0] != 'grad':
if stroke_color and stroke_color[0] == 'grad':
# stroke 引用渐变 -> 提取渐变的第一个 stop 颜色作为实色
gdef = self.grads.get(stroke_color[1])
if gdef and gdef.get('stops'):
first_stop = gdef['stops'][0]
sc = parse_color(first_stop['color_str'])
if sc and sc[0] != 'grad':
ln_children.append(_el('a:solidFill', children=[
_srgb(sc[0], int(sc[1] * el_opacity))
]))
# 也尝试用渐变填充OOXML线条支持渐变
if not ln_children and gdef:
grad_fill = _make_grad(gdef)
if grad_fill is not None:
ln_children.append(grad_fill)
elif stroke_color and stroke_color[0] != 'grad':
ln_children.append(_el('a:solidFill', children=[
_srgb(stroke_color[0], int(stroke_color[1] * el_opacity))
]))
@@ -744,11 +778,11 @@ class SvgConverter:
preset='ellipse', fill_el=fill_el, line_el=line_el))
self.stats['shapes'] += 1
def _ellipse(self, el, sp, ox, oy, opacity):
cx_v = float(el.get('cx', 0)) + ox
cy_v = float(el.get('cy', 0)) + oy
rx = float(el.get('rx', 0))
ry = float(el.get('ry', 0))
def _ellipse(self, el, sp, ox, oy, opacity, scale):
cx_v = float(el.get('cx', 0)) * scale + ox
cy_v = float(el.get('cy', 0)) * scale + oy
rx = float(el.get('rx', 0)) * scale
ry = float(el.get('ry', 0)) * scale
if rx <= 0 or ry <= 0:
return
el_opacity = float(el.get('opacity', '1')) * opacity
@@ -758,11 +792,11 @@ class SvgConverter:
preset='ellipse', fill_el=fill_el))
self.stats['shapes'] += 1
def _line(self, el, sp, ox, oy):
x1 = float(el.get('x1', 0)) + ox
y1 = float(el.get('y1', 0)) + oy
x2 = float(el.get('x2', 0)) + ox
y2 = float(el.get('y2', 0)) + oy
def _line(self, el, sp, ox, oy, scale):
x1 = float(el.get('x1', 0)) * scale + ox
y1 = float(el.get('y1', 0)) * scale + oy
x2 = float(el.get('x2', 0)) * scale + ox
y2 = float(el.get('y2', 0)) * scale + oy
line_el = make_line(el.get('stroke', '#000'), el.get('stroke-width', '1'))
if line_el is None:
return
@@ -779,7 +813,7 @@ class SvgConverter:
sp.append(shape)
self.stats['shapes'] += 1
def _path(self, el, sp, ox, oy, opacity):
def _path(self, el, sp, ox, oy, opacity, scale):
"""SVG <path> -> OOXML custGeom 形状。"""
d = el.get('d', '')
if not d or 'nan' in d:
@@ -806,17 +840,20 @@ class SvgConverter:
line_el = make_line(el.get('stroke', ''), el.get('stroke-width', '1')) if el.get('stroke') else None
shape = make_shape(self._id(), f'P{self.sid}',
px(bx + ox), px(by + oy), px(bw), px(bh),
px((bx + ox) * scale) if scale != 1.0 else px(bx + ox),
px((by + oy) * scale) if scale != 1.0 else px(by + oy),
px(bw * scale), px(bh * scale),
fill_el=fill_el, line_el=line_el, geom_el=geom_el)
sp.append(shape)
self.stats['shapes'] += 1
def _image(self, el, sp, ox, oy, slide):
def _image(self, el, sp, ox, oy, opacity, scale, slide):
href = el.get(f'{{{XLINK_NS}}}href') or el.get('href', '')
x = float(el.get('x', 0)) + ox
y = float(el.get('y', 0)) + oy
w = float(el.get('width', 0))
h = float(el.get('height', 0))
x = float(el.get('x', 0)) * scale + ox
y = float(el.get('y', 0)) * scale + oy
w = float(el.get('width', 0)) * scale
h = float(el.get('height', 0)) * scale
el_opacity = float(el.get('opacity', '1')) * opacity
if not href or w <= 0 or h <= 0:
return
@@ -890,6 +927,19 @@ class SvgConverter:
pic.crop_top = crop_tb
pic.crop_bottom = crop_tb
# 应用透明度(通过 OOXML alphaModFix
if el_opacity < 0.99:
from pptx.oxml.ns import qn
sp_pr = pic._element.find(qn('p:spPr'))
if sp_pr is None:
sp_pr = pic._element.find(qn('pic:spPr'))
# 在 blipFill 的 blip 上设置 alphaModFix
blip = pic._element.find('.//' + qn('a:blip'))
if blip is not None:
alpha_val = int(el_opacity * 100000)
alpha_el = _el('a:alphaModFix', {'amt': str(alpha_val)})
blip.append(alpha_el)
self.stats['shapes'] += 1
@@ -934,6 +984,8 @@ def main():
parser = argparse.ArgumentParser(description="SVG to PPTX (native shapes)")
parser.add_argument('svg', help='SVG file or directory')
parser.add_argument('-o', '--output', default='presentation.pptx')
parser.add_argument('--html-dir', default=None,
help='HTML source directory (for future notes extraction)')
args = parser.parse_args()
convert(args.svg, args.output)