摘要:本文记录了从零开发一个基于 PyQt6 和 wkhtmltopdf 的 Markdown 转 PDF 工具的全过程。我们将深入探讨在 macOS 环境下遇到的依赖地狱、Emoji 渲染乱码、本地资源加载失败等真实技术难题,以及如何通过代码重构和自动化测试打造一个健壮的桌面应用。
1. 项目背景
在日常工作中,Markdown 是程序员最爱的文档格式,但交付给非技术人员时,PDF 往往是标准。市面上的转换工具要么是收费的 SaaS 服务,要么是不支持批量处理的命令行脚本。
目标:构建一个免费、开源、支持批量转换且拥有现代化 GUI 的本地工具。
核心栈:Python 3.9 + PyQt6 (UI) + pdfkit (转换核心) + markdown2 (解析)。
工具下载地址:gitee.com/qijinliangc…
2. 踩坑与填坑实录
开发过程并非一帆风顺,以下是我们在 "Pro" 版本迭代中解决的关键技术挑战。
2.1 依赖地狱:wkhtmltopdf 的消失
问题:pdfkit 依赖底层的 wkhtmltopdf 工具。在 macOS 上,我们习惯使用 brew install wkhtmltopdf,但发现该配方已从 Homebrew 核心库中移除(因上游项目停止维护)。
解决方案:
-
放弃 Homebrew,直接下载官方封装的
.pkg安装包。 -
在代码中实现智能路径检测:优先检测环境变量 -> 系统 PATH (
shutil.which) -> 常见安装目录 (/usr/local/bin,/opt/homebrew/bin等)。 -
用户体验优化:直接将
.pkg文件打包进项目仓库,并在 GUI 报错时给出明确的安装指引,而不是冷冰冰的 "Command not found"。
2.2 视觉灾难:Emoji 变方块 (Tofu)
问题:wkhtmltopdf 基于旧版 WebKit 引擎,对彩色 Emoji 字体(如 Apple Color Emoji)支持极差,转换出的 PDF 中 Emoji 全部显示为黑白方块或乱码。
尝试与失败:
-
❌ 尝试 CSS
font-family指定 Emoji 字体 -> 无效,WebKit 渲染层不支持。 -
❌ 尝试安装 Linux 字体包 -> macOS 上不适用。
最终方案:图片替换法。
引入 emoji 库,在 Markdown 转 HTML 的预处理阶段,将所有 Unicode Emoji 字符替换为 Twitter 开源的 Twemoji 图片 (<img> 标签)。
进阶挑战:彩虹旗 (🏳️🌈) 显示失败。
-
原因:Twemoji 的 CDN 文件名规则复杂。简单图标(如 🛠️)需要移除变体符
fe0f,而复杂 ZWJ 序列(如 🏳️🌈)必须保留fe0f。 -
修复:实现了混合编码策略,对包含零宽连接符 (
\u200d) 的序列保留完整 Hex,否则剔除fe0f。
2.3 资源加载:本地图片离奇失踪
问题:Markdown 中引用本地图片 ,转换后的 PDF 是一片空白。
原因:HTML 渲染引擎不知道相对路径是相对于谁的(内存中的 HTML 字符串没有“当前目录”的概念)。
解决方案:
-
Base Tag (已弃用):尝试在
<head>中添加<base href="...">,但在某些环境下表现不稳定。 -
绝对路径替换 (当前方案):使用正则表达式
re.sub,在转换前动态将 HTML 源码中的所有相对路径src="image.png"替换为绝对路径src="file:///Users/.../image.png"。这确保了 100% 的加载成功率。
3. 架构演进:从脚本到应用
初期代码是一个 300 行的 md_to_pdf_converter.py,逻辑耦合严重。为了提升可维护性,我们进行了重构:
-
main.py:极其精简的入口,只负责启动 App。 -
ui_main.py:专注于 PyQt6 界面布局。解决了QListWidget属性报错等兼容性问题。 -
converter_core.py:核心业务逻辑。封装了ConverterThread线程,确保转换耗时操作不会卡死 UI 界面。 -
app_styles.py:将 QSS 样式表抽离,实现了类似 Bootstrap 的现代化蓝白配色。
4. 质量保证:极限测试
为了验证工具的健壮性,我们设计了 9 个维度的测试用例(test_files/):
-
基础排版:验证 Markdown 语法解析。
-
代码与表格:验证 Pygments 代码高亮和表格边框。
-
多语言:中日韩俄混合,验证 CSS 字体回退机制 (
Microsoft YaHei,PingFang SC)。 -
本地资源:验证图片路径自动修复逻辑。
-
边界情况:
-
HTML 注入:确保
<script>不会被执行。 -
网络错误:模拟远程图片 404,确保程序捕获异常并跳过,而不是崩溃。
-
性能测试:生成 5000+ 行的长文档,验证内存占用和分页逻辑。
5. 总结
开发这个工具不仅是写代码,更是解决实际问题的过程。从解决底层的渲染引擎缺陷,到优化用户交互体验,每一个报错背后都是一次对技术细节的深入理解。
项目成果:一个稳定、美观、功能强大的 Mac 本地生产力工具。