---
name: pdf-reportlab
description: 用 ReportLab 生成专业中文 PDF 研究报告。集中样式管理、紧凑分页（widows/orphans/keepWithNext）、颜色层次、封面保密标识、页眉页脚简洁化。基于 9MW1911 综合战略报告的编排参考。
---

# ReportLab 中文 PDF 模板使用指南（v0.5）

## 一、为什么是 ReportLab

- **完全可控**：每个字号、行距、颜色由代码说了算，不被引擎意外改动
- **中文字体一次搞定**：`pdfmetrics.registerFont` 注册后全局可用，子集嵌入 PDF，无授权问题
- **分页规则精确**：支持 widows/orphans、keepWithNext、splitByRow 等避免孤行/寡行
- **图表嵌入高质量**：matplotlib 300 DPI PNG，速度快
- **样式集中在 StyleSheet**：避免字号不一

---

## 二、模板入口

脚本：`.opencode/templates/report-template.py`

调用：
```bash
uv run python .opencode/templates/report-template.py \
    --input projects/<slug>/phase4/final.md \
    --manifest projects/<slug>/manifest.json \
    --output projects/<slug>/phase4/final.pdf \
    --fonts-dir .opencode/templates/fonts
```

首次使用前：
```bash
bash .opencode/templates/fonts/download-fonts.sh
```

---

## 三、字体注册（7 个字重）

```
SrcSerif-Regular   思源宋体 Regular   正文
SrcSerif-Bold      思源宋体 Bold      正文粗体
SrcSans-Light      思源黑体 Light     页眉页脚
SrcSans-Medium     思源黑体 Medium    三级标题/图表
SrcSans-Bold       思源黑体 Bold      一二级标题
SrcSans-Heavy      思源黑体 Heavy     封面大标题
Kai                霞鹜文楷 Regular   摘要/引文
```

---

## 四、样式表（StyleSheet，集中管理）

| 样式名 | 字体 | 字号 | 行高 | 颜色 | 备注 |
|---|---|---|---|---|---|
| `body` | SrcSerif | 10.5 | 18 | `#1a1a1a` | 正文，首行缩进 21pt |
| `body-bold` | SrcSerif-Bold | 10.5 | 18 | `#1a1a1a` | 行内加粗 |
| `h1` | SrcSans-Bold | 18 | 28 | `#1e3a8a` | 章标题，前强制分页 |
| `h2` | SrcSans-Bold | 14 | 22 | `#2c5282` | 节标题 |
| `h3` | SrcSans-Medium | 12 | 18 | `#374151` | 小节标题 |
| `quote` | Kai | 10.5 | 18 | `#4b5563` | 引文 |
| `caption` | SrcSans-Medium | 9 | 13 | `#6b7280` | 图表标题 |
| `footnote` | SrcSerif | 9 | 13 | `#374151` | 脚注/参考文献 |
| `header-footer` | SrcSans-Light | 8 | 12 | `#9ca3af` | 页眉页脚 |
| `cover-title` | SrcSans-Heavy | 28 | 40 | `#0f172a` | 封面主标题 |
| `cover-subtitle` | SrcSans-Medium | 15 | 24 | `#475569` | 封面副标题 |
| `cover-confidential` | SrcSans-Bold | 11 | 16 | `#dc2626` | 封面保密标识（红色）|
| `cover-meta` | SrcSerif | 11 | 18 | `#334155` | 封面元信息 |
| `summary` | SrcSerif | 11 | 20 | `#1a1a1a` | 执行摘要 |

---

## 五、分页规则（关键升级点）

### 章（h1）

- `pageBreakBefore=1`（每章新起一页）
- `keepWithNext=1`（标题和下一段不分离）

### 节（h2）

- **禁止单独触发分页**
- `keepWithNext=1`（标题紧跟内容）

### 小节（h3）

- 同 h2，`keepWithNext=1`

### 段落

- `widows=2, orphans=2`（避免寡行/孤行）
- 每段之间 `spaceBefore=6, spaceAfter=6`

### 表格

- `TableStyle` 中启用 `splitByRow=True`（长表格按行分页）
- `repeatRows=1`（表头在分页后重复）
- 单元格 padding 统一 4-6pt

### 摘要/术语表/目录/参考文献

- 每个独占起始页（用 `PageBreak`）

---

## 六、封面模板（参考 9MW1911）

封面独立一页，无页眉页脚，布局：

```
（上 30% 空白）

     主标题（cover-title，黑色，居中）
     副标题（cover-subtitle，深灰，居中）

（中部 40% 空白）

     [机密 | 仅供 XX 内部决策使用]  （cover-confidential，红色，居中）

（下部 20%）

     类型：研究类
     作者：Deep Research 系统 / <公司名>
     编制日期：YYYY 年 M 月
     版本：v1.0
```

字段来源：
- `manifest.report_title` / `report_subtitle`
- `manifest.confidentiality` （如 "机密 | 仅供迈威生物内部决策使用"）
- `manifest.author` / `manifest.date` / `manifest.version`

---

## 七、页眉页脚（简洁化）

### 页眉

左边：报告简称（从 `manifest.report_title` 取前 15 字）
右边：章节名（动态，从当前 h1 内容取）
底线：`#e5e7eb` 浅灰分隔线

### 页脚

居中：页码（格式 `— X —`）
字号：8pt，`#9ca3af` 浅灰

### 特殊页

- 封面：无页眉页脚
- 免责声明：无页眉，仅页脚
- 其他（摘要/术语表/目录/正文/附录/参考文献）：有页眉页脚

---

## 八、报告结构（11 件套）

ReportLab 按以下顺序组装：

1. **封面**（cover PageTemplate）
2. **免责声明**（normal PageTemplate，单页）
3. **Executive Summary / 执行摘要**（summary PageTemplate，1-2 页）
4. **Abstract / 摘要**（normal，单页）
5. **Glossary / 术语表**（normal）
6. **Table of Contents / 目录**（TOC，自动生成）
7. **正文各章**（normal，每章 h1 强制分页）
8. **结论与建议**（正文的一部分）
9. **附录**（normal）
10. **参考文献**（bibliography PageTemplate，footnote 样式）
11. **版本信息**（normal，单页）

---

## 九、Markdown 支持范围

| Markdown | ReportLab 渲染 |
|---|---|
| `# 第 X 章 ...` | h1（新起一页，深蓝色）|
| `## X.Y ...` | h2（节，蓝色，不分页）|
| `### X.Y.Z ...` | h3（小节，深灰）|
| `**粗体**` | inline `<b>` |
| `*斜体*` | inline `<i>` |
| `` `代码` `` | 等宽字体 |
| `> 引文` | quote 样式（楷体，浅底色）|
| `- 项` / `1. 项` | 项目符号列表 |
| 表格 `\| \| \|` | Table，自动列宽 + splitByRow |
| `![caption](path)` | 图片 + caption 样式 |
| `[src_001]` | 上标引用 |
| `---` | 分页符 |

**不支持**：HTML 标签、数学公式、代码块高亮。

---

## 十、图表嵌入

不用 ReportLab 原生绘图，全部预渲染为 PNG：

```python
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm

font_path = '.opencode/templates/fonts/SourceHanSansSC-Medium.otf'
fm.fontManager.addfont(font_path)
plt.rcParams['font.family'] = 'Source Han Sans SC'
plt.rcParams['axes.unicode_minus'] = False

fig, ax = plt.subplots(figsize=(6, 4), dpi=300)
# ...
plt.savefig('projects/<slug>/phase4/figures/fig_01_XXX.png', dpi=300, bbox_inches='tight')
```

Markdown 中引用：`![图 1-1：2020-2025 GLP-1 市场规模（数据来源：[src_042]）](figures/fig_01_XXX.png)`

---

## 十一、manifest 必需字段

```json
{
  "slug": "...",
  "report_title": "自研 O-糖苷酶立项可行性研究报告",
  "report_subtitle": "对标 NEB 与 Merck 经典产品的技术路径、IP 壁垒与差异化战略",
  "confidentiality": "机密 | 仅供内部决策使用",
  "author": "Deep Research 系统",
  "date": "2026-04-21",
  "version": "1.0",
  "type": "研究类",
  "disclaimer": "本报告基于公开信息与 AI 辅助研究生成，仅供参考..."
}
```

---

## 十二、常见坑与对策

| 坑 | 对策 |
|---|---|
| 中文字号不一 | 集中 StyleSheet，不在 Paragraph 里 inline 改 fontSize |
| 行距太挤 | 行高 = 字号 × 1.5~1.7 |
| 换行断错 | `wordWrap='CJK'` 必设 |
| 表格被截断 | `splitByRow=True, repeatRows=1` |
| 标题孤行（页末一个标题后直接新页）| `keepWithNext=1` |
| 段落寡行（末段只剩一行在下页）| `widows=2, orphans=2` |
| 图片变形 | 先 matplotlib 出 300 DPI PNG，再 `Image(path, width=..., kind="proportional")` |
| 生成慢 | matplotlib 图表预渲染，不要在 PDF 生成阶段现算 |
| 参考文献丢失 | dr-reporter 出稿前检查：读 citations.md，确认内容已写入 final.md |

---

## 十三、dr-reporter 调用流程

```
1. 环境检查
   - 字体 ≥6 个 OTF 文件
   - final.md 存在
   - manifest.json 有必需字段

2. 回填参考文献（关键步骤，修复 v0.4 的 bug）
   - 读 citations.md 内容
   - 在 final.md 中找到"## 参考文献"段落
   - 如果段落内容是占位符（如 "[由 dr-reporter 自动生成]" 或为空）
     → 替换为 citations.md 的完整内容
   - 写回 final.md

3. 图表检查
   - 扫描 final.md 中所有 ![...](path) 图片引用
   - 验证每个 path 在 figures/ 目录下存在
   - 缺失图片 → 警告并继续，但汇报中注明

4. 生成 PDF
   uv run python .opencode/templates/report-template.py \
       --input projects/<slug>/phase4/final.md \
       --manifest projects/<slug>/manifest.json \
       --output projects/<slug>/phase4/final.pdf \
       --fonts-dir .opencode/templates/fonts

5. 生成 DOCX（可选）
   pandoc projects/<slug>/phase4/final.md \
       --from markdown --to docx \
       --output projects/<slug>/phase4/final.docx \
       --toc --toc-depth=3

6. 验证
   - PDF 文件大小 > 500KB（太小说明字体没嵌）
   - PDF 页数合理（按字数估算）
   - "参考文献"章节存在且非空

7. 汇报
```

---

## 十四、输出卫生检查（必跑）

生成 PDF 前：

```bash
# 加载 skill:output-hygiene
python3 .opencode/templates/hygiene_check.py projects/<slug>/phase4/final.md
```

禁止词残留 → 抛回 dr-polisher 再润色一轮。
