用 Flask 从 0 到 1 实现一个本地版 WPS:PDF 合并拆分、Office 转 PDF、在线创建文档
一、为什么要做这个工具
日常办公里经常会遇到这些小需求:
- 多张图片合并成一个 PDF;
- 多个 PDF 合成一份文件;
- 扫描件或图片 PDF 需要删掉某几页;
- PDF 要拆分成多个文件;
- PDF 要加水印、加页码、压缩;
- Word、Excel、PPT 需要快速转成 PDF;
- 临时写一个 Word 文档、表格或 PDF 页面。
这些功能单独看都不复杂,但如果每次都打开不同软件,或者上传到在线网站处理,会比较麻烦。于是我做了一个本地版的轻量办公工具,界面上尽量模仿 WPS 的工具中心形式,把常用功能做成独立卡片。
最终效果如下:

二、项目功能概览
当前项目叫 Office Studio 文档工作台,主要分成两块:
1. 文档创建
支持在浏览器里新建三类文档:
- Word 文档:支持标题、正文、图片、表格,导出 .docx;
- Excel 表格:支持单元格编辑、增加行列,导出 .xlsx;
- PDF 画布:支持添加文本、图片、新增页面,导出 .pdf。
Word 编辑页面:

Excel 编辑页面:

PDF 创建页面:

2. PDF 工具中心
PDF 工具中心支持:
- 图片转 PDF;
- 多 PDF 合并;
- PDF 页面删除;
- PDF 页面拖拽重排;
- PDF 页面旋转;
- PDF 拆分;
- 提取指定页面;
- PDF 压缩;
- PDF 加文字水印;
- PDF 加页码;
- PDF 转图片;
- PDF 转 Word;
- Word 转 PDF;
- Excel 转 PDF;
- PPT 转 PDF;
- 图片格式批量转换;
- PDF 文本识别导出 Word。
PDF 工具中心页面如下:

三、技术选型
这个项目的目标是轻量、可本地运行、易扩展,所以没有引入很重的前端框架,整体技术栈如下:
| Web 服务 | Flask |
| PDF 读写 | pypdf |
| PDF 渲染预览 | pypdfium2 |
| 图片处理 | Pillow |
| 生成 PDF | ReportLab |
| Word 读写 | python-docx |
| Excel 读写 | openpyxl |
| PPTX 读取 | python-pptx |
| 前端 | HTML + CSS + 原生 JavaScript |
| 测试 | pytest |
依赖文件大致如下:
Flask==3.1.2
openpyxl==3.1.5
Pillow==11.3.0
pypdf==6.11.0
pypdfium2==5.2.0
python-docx==1.2.0
python-pptx==1.0.2
reportlab==4.4.4
pytest==8.4.2
源码仓库:
https://gitee.com/wu-xubin/change_image_to_pdf.git
也可以直接克隆运行:
git clone https://gitee.com/wu-xubin/change_image_to_pdf.git
cd change_image_to_pdf
启动方式:
.venv\\Scripts\\python –m pip install –r requirements.txt
.venv\\Scripts\\python app.py
访问:
http://127.0.0.1:5000
四、整体架构设计
项目采用比较直接的 Flask 单体结构:
change_image_to_pdf
├─ app.py
├─ requirements.txt
├─ static
│ ├─ app.js
│ └─ styles.css
├─ templates
│ └─ index.html
├─ tests
│ └─ test_app.py
└─ storage
├─ uploads
└─ generated
后端主要负责:
- 接收文件上传;
- 校验文件类型;
- 创建独立的转换目录;
- 调用对应处理函数;
- 返回下载地址。
前端主要负责:
- 首页功能卡片导航;
- 文件选择和参数填写;
- PDF 页面预览;
- 页面删除、旋转、拖拽排序;
- 调用后端接口;
- 展示下载按钮。
核心接口可以分成三类:
/api/upload 上传图片或 PDF
/api/generate 合并、删页、旋转、重排并生成 PDF
/api/pdf-to-word PDF 转 Word
/api/word-to-pdf Word 转 PDF
/api/excel-to-pdf Excel 转 PDF
/api/ppt-to-pdf PPT 转 PDF
/api/split-pdf PDF 拆分
/api/extract-pdf 提取页面
/api/compress-pdf PDF 压缩
/api/watermark-pdf 添加水印
/api/add-page-numbers 添加页码
/api/pdf-to-images PDF 转图片
/api/convert-images 图片格式转换
/api/ocr-pdf PDF 文本识别
/api/create-word 创建 Word
/api/create-excel 创建 Excel
/api/create-pdf 创建 PDF
五、PDF 合并、删页、旋转和重排
PDF 主工作台的思路是:
伪代码流程如下:
@app.post("/api/generate")
def generate_pdf():
payload = request.get_json(silent=True) or {}
session_id = payload.get("session_id", "")
settings = normalize_settings(payload.get("settings", {}))
deleted_pages = normalize_deleted_pages(payload.get("deleted_pages", []))
page_order = normalize_page_order(payload.get("page_order", []))
rotations = normalize_page_rotations(payload.get("rotations", {}))
page_result = build_document_pdf(
source_paths=source_paths,
output_path=output_path,
settings=settings,
deleted_pages=deleted_pages,
page_order=page_order,
rotations=rotations,
)
return jsonify({
"preview_url": preview_url,
"download_url": download_url,
"page_count": page_result["page_count"],
"kept_page_count": page_result["kept_page_count"],
"pages": page_result["pages"],
})
这里有一个比较重要的设计点:不要在前端直接修改 PDF 文件,而是让前端只维护“用户想怎么处理页面”的状态,真正的 PDF 写入由后端完成。这样前端逻辑更轻,后端也更容易测试。
六、PDF 拆分功能
PDF 拆分功能支持用户输入类似这样的页码范围:
1-3; 4-6; 7,9-10
分号表示要拆成多个文件,逗号和横线表示页码组合。
例如:
- 1-3 生成第一个 PDF;
- 4-6 生成第二个 PDF;
- 7,9-10 生成第三个 PDF。
后端处理完成后,会把多个 PDF 打包成 ZIP 返回。
这个设计比“只能每 N 页拆一个文件”更灵活,适合处理合同、扫描件、资料包这类文档。
七、Word、Excel、PPT 转 PDF
Office 转 PDF 是这个项目比较像办公套件的一部分。
1. Word 转 PDF
Word 转 PDF 使用 python-docx 读取 .docx,再用 ReportLab 写 PDF。
主要处理:
- 段落;
- 标题;
- 表格;
- 空文档兜底;
- 中文字体注册。
轻量版实现并不会 100% 复刻 Word 的所有排版,但对于普通文本和表格已经足够。
2. Excel 转 PDF
Excel 转 PDF 使用 openpyxl 读取 .xlsx,然后把工作表内容转成 ReportLab 的表格。
这里做了几个限制:
- 最多读取前 200 行;
- 最多读取前 30 列;
- 空单元格会保留为空字符串;
- 每个工作表单独分页;
- PDF 使用横向 A4,避免表格太窄。
核心思路:
def convert_xlsx_to_pdf(xlsx_path, output_path):
workbook = load_workbook(xlsx_path, data_only=True)
story = []
for sheet_index, sheet in enumerate(workbook.worksheets):
if sheet_index > 0:
story.append(PageBreak())
story.append(Paragraph(escape(sheet.title), styles["heading"]))
rows = worksheet_to_rows(sheet)
table_data = [
[Paragraph(format_paragraph_text(str(cell)), styles["body"]) for cell in row]
for row in rows
]
story.append(Table(table_data, repeatRows=1))
pdf = SimpleDocTemplate(str(output_path), pagesize=landscape(A4))
pdf.build(story)
3. PPT 转 PDF
PPT 转 PDF 是最近新增的功能。
实现思路是:
核心代码思路:
def convert_pptx_to_pdf(pptx_path, output_path):
presentation = Presentation(pptx_path)
page_width = emu_to_points(presentation.slide_width)
page_height = emu_to_points(presentation.slide_height)
pdf = canvas.Canvas(str(output_path), pagesize=(page_width, page_height))
for slide in presentation.slides:
pdf.setFillColor(colors.white)
pdf.rect(0, 0, page_width, page_height, fill=1, stroke=0)
for shape in slide.shapes:
draw_pptx_image(pdf, shape, page_height)
draw_pptx_text(pdf, shape, page_height, font_name)
pdf.showPage()
pdf.save()
需要注意:这个方案是轻量级方案,不依赖本机 Office 或 LibreOffice,因此部署简单;但复杂动画、母版、艺术字、图表和高级形状不一定能完美还原。如果目标是高保真转换,可以后续接入 LibreOffice 命令行。
八、在线创建 Word、Excel 和 PDF
除了文件处理工具,我还做了三个创建页面:
- Word 创建页;
- Excel 创建页;
- PDF 创建页。
前端负责收集用户编辑的内容,然后把结构化 JSON 发给后端。后端根据 JSON 生成对应文件。
例如 Word 创建的请求数据可以设计成:
{
"title": "新建文档",
"blocks": [
{
"type": "heading",
"text": "项目总结"
},
{
"type": "paragraph",
"text": "这里是正文内容。"
},
{
"type": "table",
"rows": [
["名称", "数量"],
["PDF 工具", "12"]
]
}
]
}
这种结构的好处是:前端可以做得比较自由,后端只关心数据结构,不需要解析复杂 HTML。
九、前端设计:把功能做成工具卡片
一开始 PDF 工具很多,如果全部堆在一个页面上,会非常乱。后来我把每个独立功能都做成卡片:
- 卡片有标题;
- 卡片有简短说明;
- 卡片有文件选择;
- 有参数就放输入框或下拉框;
- 提交成功后显示下载按钮。
前端只需要一套通用逻辑,就可以处理多个接口:
function bindToolCards() {
document.querySelectorAll(".tool-card[data-endpoint]").forEach((card) => {
const input = card.querySelector(".tool-file-input");
const button = card.querySelector(".tool-submit");
const downloadLink = card.querySelector(".tool-download");
button.addEventListener("click", async () => {
await submitToolCard(card, input, button, downloadLink);
});
});
}
每张卡片通过 data-endpoint 指定接口:
<article class="tool-card" data-endpoint="/api/ppt-to-pdf" data-file-field="file">
<h3>PPT 转 PDF</h3>
<p>将 PPTX 幻灯片转换为 PDF 页面。</p>
<label class="tool-file">
<span>选择 PPTX</span>
<input class="tool-file-input" type="file" accept=".pptx">
</label>
<button class="primary-btn tool-submit" type="button">开始转换</button>
<a class="secondary-btn tool-download disabled" href="#" download>下载结果</a>
</article>
这样后续新增工具时,很多情况下只需要:
这个扩展方式比较顺手。
十、中文字体和 PDF 生成问题
做 PDF 生成时,中文字体是绕不开的问题。
ReportLab 默认字体对中文支持不好,所以需要注册系统字体:
def register_pdf_font():
font_name = "AppUnicode"
font_candidates = [
Path("C:/Windows/Fonts/msyh.ttc"),
Path("C:/Windows/Fonts/simsun.ttc"),
Path("C:/Windows/Fonts/simhei.ttf"),
Path("/System/Library/Fonts/PingFang.ttc"),
Path("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"),
]
for font_path in font_candidates:
if font_path.exists():
pdfmetrics.registerFont(TTFont(font_name, str(font_path)))
return font_name
return "Helvetica"
Windows 下通常可以使用微软雅黑或宋体。跨平台时可以继续补充 macOS、Linux 的字体路径。
十一、扫描件 PDF 和 OCR 的边界
PDF 转 Word 这类功能有一个关键前提:PDF 里要有文本层。
如果 PDF 是纯扫描件,本质上每一页都是图片,这时用普通 PDF 文本提取方法是拿不到文字的。
所以当前项目里的“文本识别”逻辑是:
- 优先提取 PDF 文本层;
- 如果没有检测到文本,会在导出的 Word 中提示未检测到文本;
- 真正 OCR 可以后续接入 Tesseract、PaddleOCR 或其他本地 OCR 引擎。
这一点在文章里必须讲清楚,否则用户很容易误以为“PDF 转 Word”就一定能识别扫描件。
十二、测试验证
这个项目补了 pytest 测试,覆盖核心功能。
当前测试包括:
- 图片转 PDF;
- PDF 合并;
- PDF 删页;
- PDF 重排;
- PDF 旋转;
- PDF 拆分;
- PDF 页面提取;
- PDF 转 Word;
- Word 转 PDF;
- Excel 转 PDF;
- PPT 转 PDF;
- PDF 压缩;
- PDF 加水印;
- PDF 加页码;
- PDF 转图片;
- 图片格式转换;
- PDF 文本识别;
- Word、Excel、PDF 创建导出。
测试命令:
.venv\\Scripts\\python –m pytest –q
node —check static\\app.js
本地验证结果:
21 passed
有了测试之后,新增工具会安心很多。比如后面新增 PPT 转 PDF,只要补上接口、卡片和测试,就能快速确认没有把其他 PDF 功能弄坏。
十三、一些开发踩坑
1. Office 转 PDF 很难做到完美还原
Word、Excel、PPT 的排版模型都比 PDF 复杂。
如果只是轻量转换,可以用 python-docx、openpyxl、python-pptx 读取内容,再用 ReportLab 重新绘制 PDF。
如果想高保真转换,建议接入 LibreOffice:
soffice —headless —convert-to pdf input.docx —outdir output
但这样会增加部署成本,也需要考虑不同系统环境下 LibreOffice 的安装路径。
2. 文件处理要做好目录隔离
每次转换都应该创建一个独立目录:
storage/generated/<uuid>/
这样可以避免不同用户或不同任务之间文件名冲突。
即使是本地工具,也最好保留这种习惯。
3. 大文件上传要限制大小
Flask 可以通过 MAX_CONTENT_LENGTH 限制上传大小:
app.config.update(
MAX_CONTENT_LENGTH=128 * 1024 * 1024
)
否则用户误传几百 MB 的 PDF,很容易把本地服务拖慢。
4. 前端状态不要和文件内容强绑定
PDF 页面删除、旋转、重排这些操作,前端只维护状态:
{
"deleted_pages": [2, 4],
"page_order": [3, 1, 2],
"rotations": {
"1": 90
}
}
真正写 PDF 时再交给后端处理。这个思路能让前端更简单,也方便测试。
十四、后续可以继续优化什么
这个项目还能继续往“本地版 WPS 工具箱”方向扩展:
- 接入真正 OCR,支持扫描件文字识别;
- 接入 LibreOffice,提高 Office 转 PDF 的还原度;
- 增加批量任务队列;
- 增加处理历史记录;
- 增加文件自动清理机制;
- 增加更多 PDF 编辑能力,比如插入空白页、裁剪页面、重设页面尺寸;
- 增加 Docker 部署;
- 前端继续优化成更接近 WPS 的工具栏和文档标签页。
十五、总结
这个项目从最开始的图片转 PDF,逐步扩展成了一个本地办公文档工作台。
它的核心不是某一个复杂算法,而是把一组常用办公能力组合起来:
- Flask 负责接口和文件流转;
- pypdf 负责 PDF 结构处理;
- pypdfium2 负责 PDF 渲染预览;
- ReportLab 负责生成 PDF;
- python-docx、openpyxl、python-pptx 负责读取和生成 Office 文档;
- 前端用工具卡片把功能组织起来。
如果你也想做一个本地办公工具,可以从“图片转 PDF”这种小功能开始,然后按卡片式工具逐步扩展。每新增一个功能,都补齐接口、前端入口、测试和说明文档,项目就会越来越像一个正式产品。
网硕互联帮助中心





评论前必须登录!
注册