一个纯静态网页:把 PDF 拆成 奇数页 和 偶数页 两遍打印,配合「翻面 → 放回纸盒」实现手动双面打印。 适合没有自动双面功能(或驱动不支持)的打印机。
- 文件不上传、不保存、不修改,全部在浏览器内存里完成
- 运行时库全部从
js/lib/本地加载,不依赖 CDN - 应用本身无构建步骤、无依赖安装;用任意静态服务器打开即可(页面必须通过 http 访问)
- 自动跟随系统的浅色 / 深色外观(包括中间打印加载页),不需要额外设置
- 直接复制原 PDF 页面,支持中文等带嵌入字体的 PDF、横向页面和页面范围选择
- 矢量输出:把这一遍要打印的页面抽成一份新 PDF 直接交给浏览器,不逐页转图片;清晰度由 PDF 本身决定,页数多也不用逐页栅格化
- 附带 4 页「对齐测试页」,用于快速确认你的打印机该用哪种设置
把整个目录放到任意静态服务器(无后端依赖、无构建步骤)。本地预览示例:在项目目录运行 python -m http.server 8765,再访问 http://127.0.0.1:8765/。
不能直接双击
index.html:浏览器会以file://打开,ES 模块会被 CORS 拦截。直接打开时页面会显示一条提示,告诉你改用本地服务器。
建议先用页面上的 「双面打印测试」 走一遍完整流程,确认顺序和方向都正确,再用同样的设置打印正式文件。
- 打印奇数页 — 纸出来后整叠拿起来,不要整理、不要打乱顺序。
- 翻面放回纸盒 — 翻面方式由「翻转方式」决定:
- 长边翻转(书本式):像翻书一样左右翻
- 短边翻转(便签式):像翻记事本一样上下翻
- 打印偶数页 — 奇数页打印后本页会弹出「接下来打印偶数页」提示;确认纸已翻面后可直接点 「打印偶数页」,也可以点 「稍后」 再手动点页面上的按钮。机器会按背面顺序把偶数页印到空白面上。
点完按钮后会先弹出一个中间页(上面显示「正在生成…PDF」),生成完这份打印用 PDF 后会自动弹出打印对话框。 如果打印对话框没弹出来(iPhone / iPad 上比较常见):回到本页,在 「打印文件已生成」 卡片里点 「打开 PDF 再打印」,在打开的 PDF 页里用 分享 → 打印。
- 边距 → 无
- 缩放 → 默认 / 100%
- 取消勾选「页眉和页脚」
- 纸张尺寸与 PDF 页面一致(页面会显示识别到的尺寸,如 A4 210×297 mm)
| 设置 | 说明 |
|---|---|
| 页面范围 | 留空 = 全部。支持 1-10、2,5,7-9、2至4。选中的页面会按页码升序重新编号,所以双面对齐仍然正确 |
| 翻转方式 | 长边 / 短边,会自动带出下面两个勾选项的推荐值 |
| 偶数页倒序打印 | 打印背面时纸叠已被翻过,进纸顺序与正面相反,所以背面页通常要倒序排列 |
| 背面内容旋转 180° | 短边翻转后背面内容相对正面会倒过来,需要勾选 |
纸叠怎么翻、打印机出纸是正面朝上还是朝下,跟机型有关,没有一种设置能通吃。所以:
- 打印内置的 4 页测试页(走完两遍);
- 检查:每张纸两面的页码是否连续(1/2、3/4),文字方向是否正确;
- 不对就切换「偶数页倒序 / 背面旋转 180°」再试一次,直到正确;
- 用这套设置打印正式文件。
顺序不对 → 切换「偶数页倒序」;文字倒着 → 切换「背面旋转 180°」。
设选中的页面共 K 页(按页码升序重新编号为 1..K),需要 ceil(K/2) 张纸,第 s 张纸的正面是第 2s-1 页、背面是第 2s 页。
- 按顺序打印
1, 3, 5, … - 按纸张顺序列出所有背面
2, 4, 6, …(末张纸若没有背面则为空白页,用来占位对齐),再根据翻转方式决定是正序还是倒序输出
点「打印奇数页 / 偶数页」后,页面用 pdf-lib(内置在 js/lib/,MIT)从已经解析到内存里的原文档中只复制这一遍要打印的页面,按上面的顺序拼成一份新 PDF:正面是 1,3,5…,背面是 2,4,6…(需要倒序就倒序,需要旋转就把页面旋转 180°,末张纸没有背面就补一张同样大小的空白页)。这份 PDF 在中间页里由浏览器的 PDF 查看器打开,并自动调用 window.print()。打印后不会自动关闭那个标签页:PDF 由原生查看器渲染,无可靠方式判断打印何时结束;打印完后请手动关闭标签页,或回主页用 「打开 PDF 再打印」 重新打开。需要重印时,同一个子集 PDF 的 blob URL 依然有效,可反复打开打印。
第 1 遍发起后,主页会弹出「接下来打印偶数页」提示,并按当前翻转方式说明怎么翻面;确认纸已放回纸盒后,可以直接从提示里进入第 2 遍。浏览器无法可靠判断第 1 遍是否真正打印完成,所以这个提示以「已发起第 1 遍打印」为触发点;如果取消了第 1 遍打印,点「稍后」关掉即可。
原 PDF 只在打开时用 pdf-lib 解析一次,打印时直接复用这份内存文档复制页面,不再重新读取文件。页面尺寸会读取 /Rotate 后的视觉宽高用于识别 A4 / Letter,子集 PDF 则保留原页旋转,保证横向页面按横向输出。
好处:
- 清晰度不受渲染分辨率影响:交给打印机的是原始矢量页面(文字还是文字),不像截图那样有 DPI 上限
- 快:只复制页面对象,不逐页栅格化,几 MB 的文档通常几百毫秒
- 内存低:不再为每张纸在内存里保留一份位图,页数多也不会把标签页撑爆
- 分页由 PDF 自己决定:不经过 CSS 分页,不会出现「每页后面多一张空白」
- 打开阶段无法解析的文件(例如加密 PDF、损坏文件):主页提示 「打开失败:…」,不显示打印按钮;加密 PDF 请先解密。
- 生成子集 PDF 时失败:关闭中间页,并在主页提示 「打印准备失败:…」;不会退回逐页渲染或画布打印。
本版本不提供预览缩略图和翻面示意图,用内置测试页校准即可。
主路径(子集 PDF):本机 Edge / Chromium,真实加载页面,12 页文档打印第 1 遍(6 张纸),从点按钮到「已生成 N 页」:
| 文件 | 整遍耗时 |
|---|---|
| 英文文本 124 KB | 199 ms |
| 中文文本 250 KB | 455 ms |
| 图文混排 5.6 MB | 698 ms |
抽页正确性在外部 E2E 验证脚本里用 PDF.js 把子集页与原页按同一比例渲染并比较:三份文件逐页内容一致,页面尺寸一致,空白占位页为纯白;copyPages / save 通常分别只占几毫秒到一百多毫秒。
页面代码全部使用原生 ES 模块:index.html 只加载 js/main.js,其余文件通过 import 显式依赖;js/lib/pdf-lib.esm.min.js 也在 js/core.js 顶部静态导入,页面启动即加载。没有运行时的 <script> 注入,应用代码也不依赖全局变量。file:// 检测是 index.html 里唯一的内联普通脚本——模块在 file:// 下会被 CORS 拦截,所以这段检查必须在模块之外执行。
package.json里的"type": "module"只是让 Node(单元测试、工具脚本)按 ESM 解析.js文件,不需要npm install,也没有构建步骤。
- 需要支持 ES 模块的现代浏览器(近几年的 Chrome / Edge / Firefox / Safari,iOS 15.4+)
- 每一遍都是「一个打印作业」,所以两遍之间要手动翻面
- 浏览器无法可靠判断第 1 遍打印何时真正结束;翻面提示在第 1 遍发起后出现,取消打印时点「稍后」关闭即可
- iOS / Safari 上自动弹出的打印对话框不一定出现(系统限制)。这时回到本页,在 「打印文件已生成」 卡片点 打开 PDF 再打印,再在 PDF 页里 分享 → 打印
- 加密 PDF 无法被 pdf-lib 解析;本版本不再提示输入密码,请先解密原文件
- 损坏 / 结构特殊的 PDF 可能无法被 pdf-lib 抽取页面,打开或打印时会提示失败
- 打印机有不可打印边距,内容紧贴页边的 PDF 可能被裁掉一点点;可在打印对话框把边距改成「最小」
- 不处理「一张纸印两页」(2-up)等拼版需求