Skip to content

Commit c3229fc

Browse files
committed
Update
1 parent 627b31a commit c3229fc

File tree

16 files changed

+1293
-49
lines changed

16 files changed

+1293
-49
lines changed

config.yml

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,25 @@ images:
55
- name: mkdocs
66
context: ./images/mkdocs
77
image: raineblog/mkdocs-docker
8+
89
- name: fragment
910
context: ./images/fragment
1011
image: raineblog/mkdocs-docker-fragment
1112
- name: rspress
1213
context: ./images/rspress
1314
image: raineblog/mkdocs-rspress-docker
15+
1416
- name: exporter
1517
context: ./images/exporter
1618
image: raineblog/mkdocs-exporter
17-
- name: exporter-puppeteer
18-
context: ./images/exporter-puppeteer
19-
image: raineblog/mkdocs-exporter-puppeteer
20-
- name: exporter-texlive
21-
context: ./images/exporter-texlive
22-
image: raineblog/mkdocs-exporter-texlive
19+
20+
- name: exporter-build
21+
context: ./images/exporter-build
22+
image: raineblog/mkdocs-exporter-build
23+
24+
# - name: exporter-puppeteer
25+
# context: ./images/exporter-puppeteer
26+
# image: raineblog/mkdocs-exporter-puppeteer
27+
# - name: exporter-texlive
28+
# context: ./images/exporter-texlive
29+
# image: raineblog/mkdocs-exporter-texlive

example/export-20260304/1.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
2+
3+
4+
这是一个非常典型且高级的自动化排版需求。将“现代前端网页的排版能力”与“传统 LaTeX 的精美框架”结合,并通过 Python 进行精细的胶水层处理,是目前生成高质量、复杂定制化 PDF 的最佳实践之一。
5+
6+
针对你的两个问题,我为你进行详细的梳理和解答:
7+
8+
### 问题一:Python 操控 PDF 用什么库?
9+
10+
在 Python 生态中,处理 PDF 的库有很多,但针对你**“合并页面”**以及**“提取并精确定位目录(精确到垂直高度)”**的需求,各个库的表现差异很大。
11+
12+
#### 1. 各个库的优缺点评估
13+
14+
| 库名称 | 优点 | 缺点 | 适用场景 |
15+
| :--- | :--- | :--- | :--- |
16+
| **pypdf**<br>(原 PyPDF2) | 纯 Python 编写,无需 C 依赖,安装极其方便;基本的合并、提取页面非常稳定。 | 提取和写入目录(Outline/Bookmarks)的能力较弱,**很难精准读取或写入包含特定 X/Y 坐标(垂直高度)的目录项**| 简单的 PDF 页面拆分与合并。 |
17+
| **pikepdf** | 基于底层 C++ 库 QPDF,极其稳定,不会破坏源 PDF 的内部结构;处理损坏的 PDF 能力最强。 | API 比较底层,操作目录(Outlines)需要直接操作 PDF 的字典对象,学习曲线非常陡峭,不够直观。 | 复杂的 PDF 结构修复、加密/解密、底层元数据修改。 |
18+
| **PyMuPDF**<br>(fitz) | 基于强大的 MuPDF (C++) 引擎,**速度极快**;提供非常高级且直观的 API;**对目录(TOC/Bookmarks)的支持极其完美,可以直接读取和写入包含精确坐标(目的地坐标)的目录**;支持页面覆盖(Overlay)。 | 采用 AGPL 协议(如果是内部 CI 工具或开源项目无所谓,闭源商业分发需注意);体积较大。 | **高级排版、精确目录操控、页面重组、文本高亮。** |
19+
20+
#### 🏆 强烈推荐:**PyMuPDF (fitz)**
21+
22+
**理由:完全契合你的“精确到垂直高度”需求。**
23+
在 Chrome 打印出的 PDF 中,目录不仅仅是一个页码,它是一个带有 `Destination`(目的地)的链接,包含了跳转时的 X 和 Y 坐标(这就是为什么点击目录会滚动到 H2 的准确位置,而不是页面最顶端)。
24+
25+
使用 PyMuPDF,你可以通过一行代码提取出包含精确坐标的目录:
26+
```python
27+
import fitz
28+
doc = fitz.open("chrome_output.pdf")
29+
# detailed=True 是关键!它不仅返回页码,还会返回精确的跳转坐标点 (to.x, to.y)
30+
toc = doc.get_toc(detailed=True)
31+
32+
# toc 的数据结构类似于:
33+
# [
34+
#[1, "第一章大标题", 1, {"kind": 1, "to": fitz.Point(0.0, 120.5)}],
35+
#[2, "某个 H2 标题", 1, {"kind": 1, "to": fitz.Point(0.0, 450.8)}]
36+
# ]
37+
```
38+
当你合并 PDF 后,只需要将 `toc` 列表中的页码加上偏移量(Offset),保留原有的 `fitz.Point(x, y)` 垂直高度坐标,然后再用 `doc.set_toc(toc)` 重新植入到最终合并的 PDF 中,就能**完美保留 H2 在页面中间的精准定位**
39+
40+
---
41+
42+
### 问题二:XeLaTeX 的构建策略(如何与网页 PDF 结合)
43+
44+
你提出了三种思路:单独构建若干小 PDF、构建合在一起的 PDF 后插入、构建带空页的 PDF 然后覆盖。
45+
46+
#### 🏆 最佳策略:构建包含若干空页(或占位页)的 PDF,然后在 Python 中替换/覆盖。
47+
48+
这个策略(你提到的第三种)在自动化排版领域被称为 **"Placeholder 占位法"**,它是绝对的最佳实践,原因如下:
49+
50+
**为什么另外两种不好?**
51+
1. **一个一个页面构建,再在 Python 中合并**:XeLaTeX 最大的优势是它的自动化排版和交叉引用系统。如果你把封面、扉页、目录、各章首页全部分开编译,**LaTeX 就无法自动为你生成准确的“总目录页”(Visual TOC,即打印在纸上的目录)**,也无法处理全局的页码编排(比如罗马数字前言转阿拉伯数字正文)。
52+
2. **构建一个连在一起的,然后在 Python 中插入**:同理,如果你只是在 LaTeX 里写了各章首页,而没有把网页 PDF 的页数算进去,LaTeX 生成的总目录页上的页码就是全错的(比如第一章是第 3 页,第二章变成第 4 页,而实际上第一章有 20 页 Chrome 网页内容)。
53+
54+
**为什么“空页占位法”最好?**
55+
因为你不仅需要 PDF 左侧的“书签目录”(Bookmarks),你还需要书籍前面的**“物理目录页”**(打在纸上的目录)。
56+
通过 Node.js 获取 Chrome 打印的每一章 PDF 的页数,并将这个数字传递给 LaTeX,LaTeX 就能完美地把整本书的骨架和页码计算清楚。
57+
58+
#### 具体实施工作流:
59+
60+
**Step 1: Node 端准备数据**
61+
用 Node.js 调用 Chrome 打印各章网页为 `chapter1.pdf`, `chapter2.pdf` 等。
62+
获取它们的页数(例如第一章 10 页,第二章 15 页),并生成一个中间文件(如 `pages_config.tex`):
63+
```latex
64+
\def\chapOnePages{10}
65+
\def\chapTwoPages{15}
66+
```
67+
68+
**Step 2: XeLaTeX 构建完整骨架(包含占位)**
69+
你的 `.tex` 主文件引入上面的配置,生成包含精确页码的总目录,并使用循环生成空白页占位:
70+
```latex
71+
\input{pages_config.tex}
72+
73+
% 封面、扉页...
74+
\tableofcontents % LaTeX 自动生成精准的物理目录
75+
76+
\chapter{第一章大标题} % 生成章首页
77+
% 根据 Node 传来的页数,生成对应数量的空白页占位(确保后续章节页码正确)
78+
\newcount\myloop
79+
\myloop=0
80+
\loop
81+
\ifnum\myloop<\chapOnePages
82+
\null\newpage
83+
\advance\myloop by 1
84+
\repeat
85+
86+
\chapter{第二章大标题}
87+
% ...
88+
```
89+
*编译结果:你会得到一个包含封面、真实目录、章首页以及几十张空白页的完整 `skeleton.pdf`。它的页码、页眉页脚都已经完美排好。*
90+
91+
**Step 3: Python 端 (PyMuPDF) 实施魔法**
92+
利用 Python,将 `skeleton.pdf` 和各种 `chapter.pdf` 结合,提取并调整书签:
93+
94+
1. **合并/覆盖页面**:找到 `skeleton.pdf` 中的空白页索引,使用 PyMuPDF 的页面覆盖功能(Overlay)或直接替换(Replace)。
95+
* *进阶玩法*:如果你希望网页 PDF 的页面上也带有 LaTeX 生成的页眉、页脚和页码!你可以不删除空白页,而是用 `page.show_pdf_page(rect, chrome_doc, pno)` 把 Chrome 生成的 PDF **叠印(Overlay)**在 LaTeX 生成的带页眉页脚的空白页上!这就实现了“现代网页排版内容 + 传统 LaTeX 优雅页眉页脚”。
96+
2. **处理目录(书签)并保持垂直高度**
97+
* 提取 `skeleton.pdf` 的书签(获取 LaTeX 生成的章级别书签)。
98+
* 提取各个 `chapterX.pdf` 的详细书签 `get_toc(detailed=True)`
99+
* 遍历 Chrome 的书签,将其所在页码加上该章在 Skeleton 中的起始页码偏移量。
100+
* 因为 `detailed=True` 保留了类似 `{'kind': 1, 'to': Point(0.0, 450.8)}` 的数据,**你什么都不用改,直接把字典塞回去**
101+
* 将合并后的总目录写回最终的 PDF `doc.set_toc(combined_toc)`
102+
103+
### 总结建议
104+
105+
1. 使用 **PyMuPDF (fitz)** 作为你的 Python 处理库。
106+
2. 放弃在 LaTeX 中直接 `\includepdf`,改为:**Node 计算页数 -> XeLaTeX 使用空页占位生成完整带页码的骨架 -> Python(PyMuPDF) 替换或叠印页面,并重新计算拼接带有精确 X/Y 坐标的书签目录。**
107+
108+
这种架构极其灵活:前端网页只管内容排版,LaTeX 只管书籍装帧和物理页码计算,Python 作为大脑精准无误地把两者在像素和数据结构级别缝合。未来任何部分的修改(换前端主题、换 LaTeX 封面)都互不干扰。

example/export-20260304/2.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
**(1)Python 操控 PDF 用什么库?推荐、优缺点对比**
2+
3+
你的核心需求是:**合并多个 PDF**(XeLaTeX 生成的首页/扉页/目录/章首页/尾页 + Node 打印的 content.pdf)、**精确插入/切片页面**(支持章节断点插入章首页)、**植入目录**(把 Node 已插入 content.pdf 的 h1~h6 大纲/书签 remap 到最终文件,并支持“精确到垂直高度”的定位,即 H2 不跳到页面顶部,而是保持源 PDF 中的原 y 坐标)、**动态插入空白页**、可能在可见目录页添加可点击链接。
4+
5+
我对比了 2025-2026 年主流库(基于最新文档、社区对比、实际用例),重点看**合并灵活性 + 大纲/书签 remap + 精确目的地(page + y 坐标)**能力:
6+
7+
|| 优点 | 缺点 | 是否支持精确垂直高度(y 坐标) | 推荐场景 | 许可/安装 |
8+
|------|------|------|--------------------------------|----------|----------|
9+
| **pypdf**(PyPDF2 继任者,纯 Python) | - 安装极简(`pip install pypdf`),纯 Python 无 C 依赖,CI 友好<br>- 合并/插入/拆分/旋转/元数据一流(`PdfWriter.merge()``insert_page()` 可精确位置)<br>- 大纲支持好(`add_outline_item()``add_outline_item_dict()`,支持层次 + FitH(y) 或 XYZ 目的地)<br>- 可读取 content.pdf 的 outline 并递归 remap 页码偏移<br>- 轻量、稳定、社区示例多 | - 精确坐标编辑弱(难在可见 TOC 页上自动找文本 bbox 加链接)<br>- 页面内容绘制/叠加/注解有限<br>- 大文件内存管理稍弱 | 支持(通过 Destination 的 FitH / XYZ),但需手动计算,不如 fitz 直观 | 纯合并 + 书签 remap(如果你只用侧边大纲,不需要可见 TOC 页上可点击链接) | MIT,零风险 |
10+
| **PyMuPDF (fitz)**(强烈推荐) | - **功能最全**:合并/切片(`insert_pdf()` 可指定范围)、动态创建新页/空白页、文本提取带精确 bbox(`get_text("dict")` 或 `search_for` 找 H2 位置)<br>- **大纲/书签完美**(`get_toc()` / `set_toc()`,轻松 remap 偏移)<br>- **精确链接**:`page.insert_link()` 支持 `LINK_GOTO` + `to=fitz.Point(x, y)`,直接指向 H2 原垂直高度<br>- 可在 LaTeX 生成的可见 TOC 页上自动搜索文本、加点击区域、叠加章首页<br>- 速度极快(C 后端),支持中文/Unicode,压缩/优化好 | - 非纯 Python(需 MuPDF 后端,安装稍大,但 wheels 齐全)<br>- 学习曲线略陡(但 API Pythonic)<br>- AGPL 许可(内部/CI 使用完全免费;若商用 SaaS 需商业许可,可联系 Artifex) | **最佳支持**:精确 Point(x,y) 或 /XYZ,配合 bbox 提取零误差 | **你的全部需求**(合并 + 精确 y 定位 + 动态章首页 + 可见目录链接 + 空页覆盖) | AGPL 或商业 |
11+
| **pikepdf**(基于 qpdf) | - 结构保真、压缩/修复/元数据/大纲极强<br>- 页面操作可靠,可与 pypdf 组合 | - 无文本坐标提取、无绘制/链接注解<br>- API 偏低级 | 间接支持(通过 outline,但无 y 精细控制) | 辅助优化最终 PDF 大小/修复 | MIT |
12+
| **ReportLab** | - 从零生成页面顶级(canvas 精确布局,适合动态漂亮 TOC) | - 编辑现有 PDF 弱(不适合改 content.pdf) | 支持(但需搭配其他库合并) | 只生成可见 TOC 页或章首页的辅助 | 商业/免费版限制 |
13+
14+
**我的推荐:首选 PyMuPDF (fitz)**
15+
理由:你反复强调“精确到垂直高度(H2 原位置)”和“灵活插入目录/章首页”,这正好是 fitz 的强项——它能直接读取 Node content.pdf 的 outline(或 headings bbox),在最终 PDF 中 remap 页码 + 保留/指定精确 y 坐标,甚至在 LaTeX 可见目录页上自动加可点击链接(无需手动硬编码坐标)。pypdf 够用但会卡在“可见目录精确链接”或未来叠加需求上。安装简单(`pip install pymupdf`),CI Docker 里几行就行。
16+
17+
如果你对 AGPL 有顾虑,或项目极简,先用 **pypdf** 起步(完全够合并 + 书签 remap),后期需要精确链接再切换 fitz(两者 API 有重叠,迁移成本低)。**避免单独用 ReportLab**,它更适合“从零生成”而非“操控现有 PDF”。
18+
19+
**(2)XeLaTeX 构建页面策略:哪个更好?**
20+
21+
你的三种方案对比(重点看**灵活性、维护性、精确插入目录 + 垂直高度**):
22+
23+
1. **一个一个页面构建若干 PDF → Python 合并****强烈推荐**
24+
XeLaTeX 编译多个独立小文件(cover.tex → cover.pdf、title.tex、toc.tex、每个章首页 opener_N.tex、end.tex)。
25+
Python(用 PyMuPDF)读取 Node 输出的 metadata(总页数、各章节起始页/页数/outline),动态组装:前页 + 可见目录 + (章首页 + content 对应章节切片) + 空页(动态 `new_page()`) + 尾页。
26+
**优点****最灵活、最好维护**。Python 全控逻辑(基于 JSON 数据驱动),改章首页样式只需改单个 .tex;内容页数变了自动适应;精确 remap outline 页码偏移 + 保留 H2 原 y;想加 duplex(奇数页开始)直接插空白页;未来加水印/页码/条件插入零成本。
27+
**缺点**:多文件管理(用 `BytesIO` 在内存处理即可)。
28+
**精确性**:content 章节页保持原结构,y 坐标不变;目录链接直接用 `Point(x, y)` 指向原位置。
29+
30+
2. **构建所有页面合在一起的单一 PDF → Python 插入页面**
31+
XeLaTeX 一次生成大骨架(所有装饰页 + 预留位置)。Python 再插入 content 页。
32+
**缺点**:LaTeX 无法预知动态章节断点和页数,插入中途章首页极麻烦;页数变化需重编 LaTeX;维护差(改一个装饰页要重跑全量)。不适合你的“精确插入”。
33+
34+
3. **构建含若干空页 → Python 覆盖源 PDF**(Node 输出页数 → LaTeX 预留空页)
35+
LaTeX 建 skeleton(装饰 + 精确数量空白),Python 用 `show_pdf_page()` 覆盖/替换空白为 content 页。
36+
**缺点****最不灵活**。内容长度变化就全崩;章首页插入逻辑复杂;空页预分配浪费;y 坐标虽能保但覆盖易出对齐/链接破坏问题;维护噩梦(每次内容变都要动态生成 .tex)。仅在“需要 LaTeX 作为透明背景叠加”时考虑。
37+
38+
**最终推荐:方案 1(分离组件 + Python 数据驱动合并)**
39+
这是最符合你“精确且灵活插入目录、精确垂直高度”的方案。它彻底解决当前 LuaLaTeX 的“不灵活、难维护”痛点——**样式归 LaTeX,逻辑/合并/链接归 Python**,两者解耦。改首页样式或加新章节类型,只动对应 .tex 或 Python 脚本,无需重构大文件。
40+
41+
**推荐实现路径(PyMuPDF + Node 配合,完整数据流)**
42+
1. **Node 端增强**(你已有 outline 插入):打印 content.pdf 同时输出 `metadata.json`(章节列表:level、title、content 内起始页、页数、可选 headings bbox/y)。Puppeteer 很容易加 `page.evaluate()` 获取。
43+
2. **XeLaTeX 端**
44+
- 静态:cover.tex、title.tex、end.tex。
45+
- 动态章首页:用 Jinja2 模板 + Python `subprocess.run(['xelatex', ...])` 生成 opener_1.pdf、opener_2.pdf 等(传入章节标题)。
46+
- 可见目录页(toc.tex):LaTeX 做排版样式(好看字体/间距),页码可先用占位,后 Python 更新或直接在 Python 绘制。
47+
3. **Python 主脚本(核心逻辑,伪代码框架)**
48+
```python
49+
import fitz # PyMuPDF
50+
import json
51+
52+
with open('metadata.json') as f:
53+
meta = json.load(f)
54+
content_doc = fitz.open('content.pdf')
55+
final = fitz.open() # 新文档
56+
57+
# 插入前页
58+
final.insert_pdf(fitz.open('cover.pdf'))
59+
final.insert_pdf(fitz.open('title.pdf'))
60+
offset = final.page_count # 前页偏移量
61+
62+
# 插入可见 TOC(LaTeX 生成)
63+
final.insert_pdf(fitz.open('toc.pdf'))
64+
65+
# 动态章节组装 + 精确目录
66+
for ch in meta['chapters']:
67+
# 插入章首页
68+
final.insert_pdf(fitz.open(f'opener_{ch["id"]}.pdf'))
69+
70+
# 插入 content 对应切片(精确页范围)
71+
start, end = ch['content_start'], ch['content_end']
72+
final.insert_pdf(content_doc, from_page=start, to_page=end)
73+
74+
# 动态加空页(e.g. 强制奇数页开始)
75+
if final.page_count % 2 == 1:
76+
final.new_page()
77+
78+
# 植入目录(remap 原 outline + 精确 y)
79+
toc_list = content_doc.get_toc() # [[level, title, page, ...], ...]
80+
for entry in toc_list:
81+
level, title, old_page, *dest = entry
82+
new_page = offset + old_page
83+
# 精确垂直高度(保留原 y,或从 bbox 取)
84+
y = dest[0] if dest else 0 # 原 y
85+
final_toc_entry = [level, title, new_page, {"kind": fitz.LINK_GOTO, "to": fitz.Point(0, y)}]
86+
# 添加到 final(或在可见 TOC 页加 annotation)
87+
final.set_toc(...) # 或手动 add_outline_item
88+
89+
# 尾页 + 保存
90+
final.insert_pdf(fitz.open('end.pdf'))
91+
final.save('final.pdf', deflate=True) # 压缩
92+
```
93+
- **精确垂直高度**:content 页不变,链接直接用原 y(PDF y 坐标 bottom-up,必要时 `page.rect.height - original_y` 转换)。
94+
- **空页覆盖**:如果你坚持用 skeleton,可改成 `page.show_pdf_page(rect, content_doc, page_num)` 精确放置(支持缩放/居中)。
95+
- **可见目录链接**:在 toc.pdf 对应页用 `search_for(title)` 找 bbox → `insert_link()` 加 GOTO 到精确 y。
96+
97+
**额外 nuance、边缘情况与维护考虑**
98+
- **页数动态**:全靠 Node JSON + Python 计算偏移,内容变长零问题。
99+
- **双面/奇偶页**:Python 轻松插空白页(比 LaTeX 预留可靠)。
100+
- **大文件/性能**:PyMuPDF 内存友好,用 `from_page/to_page` 范围操作。
101+
- **中文/字体**:XeLaTeX 用 `fontspec` 嵌入;Python 不影响。
102+
- **测试/维护**:每个组件 PDF 可单独预览;Python 脚本可加日志、单元测试;CI 一键跑(Node → metadata → xelatex 多进程 → Python)。
103+
- **如果只用书签(不需可见页链接)**:pypdf 版代码更简单(`add_outline_item` + 偏移)。
104+
- **迁移成本低**:先实现前页 + 整块 content + 尾页,逐步加章节切片和精确链接。
105+
106+
这个方案比你当前 LuaLaTeX 插入方式灵活 10 倍,维护成本大幅下降,同时完美实现“目录植入 + H2 原位置定位”。实际跑起来后,你会发现改任何装饰页或加新章节类型都只需动几行代码。
107+
108+
如果你需要完整可运行代码示例(带 pypdf 备选版)、Node Puppeteer 输出 metadata 的片段、Docker CI 配置、或针对“可见 TOC 页自动加链接”的深入实现,告诉我具体需求,我立刻给你!这样一步步迭代最稳。

0 commit comments

Comments
 (0)