pypdf 使用指南:用 add_js 为 PDF 注入 JavaScript,实现打开即自动打印
【免费下载链接】pypdfA pure-python PDF library capable of splitting, merging, cropping, and transforming the pages of PDF files项目地址: https://gitcode.com/GitHub_Trending/py/pypdf
导读
本文讲解 pypdf 为 PDF 文档注入 JavaScript 的能力,核心是PdfWriter.add_js()方法。你可以在生成、合并或加工 PDF 后,给文档附加一段在阅读器打开时自动执行的脚本——最常见的实战场景是打开 PDF 即弹出打印窗口(this.print())。读完本文,你将掌握add_js的调用方式、PDF 内部 JavaScript 动作的存储原理,以及它与加密(encrypt)等特性组合时的注意事项。
PDF 对 JavaScript 的支持现状
PDF 规范允许在文档中嵌入 JavaScript 动作(Action),但一个必须正视的现实是:不同的 PDF 阅读器对 JavaScript 的支持程度差异极大,有些阅读器根本不支持。也就是说,为 PDF 注入的脚本是"尽力而为"的增强手段,不能把它当作所有阅读器都能保证执行的功能。
在支持 JavaScript 的阅读器中,通常以 Adobe Acrobat 系列的 JavaScript 模型为事实标准(Adobe 官方维护着完整的 JavaScript for Acrobat API 参考文档,涵盖this.print、this.closeDoc、app.alert等对象与方法)。pypdf 本身只负责把 JavaScript 字符串正确写入 PDF 文件结构,不解析、不校验脚本内容——脚本能否运行、有哪些可用 API,取决于最终打开该 PDF 的阅读器。因此,编写脚本前应先确认目标阅读器支持的 JavaScript 方言。
核心 API:PdfWriter.add_js
add_js定义在 pypdf/_writer.py,签名如下:
def add_js(self, javascript: str) -> None:参数javascript是要注入的 JavaScript 源码字符串。从源码结构看,它直接在写入器(PdfWriter)的根对象上操作,属于文档级动作(document-level action),即脚本在文档打开时触发,而不是绑定到某个具体页面或控件。
实战:打开 PDF 时自动弹出打印窗口
文档给出的最典型用法,是在 PDF 打开时启动打印窗口。完整代码:
from pypdf import PdfWriter writer = PdfWriter(clone_from="example.pdf") # Add JavaScript to launch the print window on opening this PDF. writer.add_js("this.print({bUI:true,bSilent:false,bShrinkToFit:true});") writer.write("out-print-window.pdf")这段代码做了三件事:
PdfWriter(clone_from="example.pdf"):以现有 PDF 为模板创建写入器,克隆其页面内容,这样输出文件会保留原文档的页面;writer.add_js(...):向文档根目录注册一段 JavaScript 动作,脚本为this.print({...});writer.write("out-print-window.pdf"):将结果写出为新文件。
this.print 参数说明
this.print是 Acrobat JavaScript 中打开打印对话框的方法,这里用到的三个选项:
| 参数 | 取值 | 含义 |
|---|---|---|
bUI | true/false | 是否显示打印对话框(UI)。true时用户可见打印设置界面 |
bSilent | true/false | 是否静默打印。false表示不静默,与bUI:true配合弹出可见对话框 |
bShrinkToFit | true/false | 打印时是否按页面大小缩放以适应纸张 |
(这些参数遵循 Adobe JavaScript for Acrobat 的this.print签名,实际支持程度取决于阅读器。)
如果你希望静默打印(不弹对话框、直接打印),可以调整参数为this.print({bUI:false,bSilent:true,bShrinkToFit:true});——但请注意静默打印对阅读器权限要求更高,很多阅读器会忽略该请求。
底层原理:JavaScript 在 PDF 文件里如何存储
add_js的实现并不神秘,它遵循 PDF 规范的文档级 JavaScript 机制。拆解 pypdf/_writer.py 的源码可以看到完整的写入路径:
第一步:确保根对象存在/Names名称字典
if "/Names" not in self._root_object: self._root_object[NameObject(CatalogAttributes.NAMES)] = DictionaryObject()/Names是 PDF 文档目录(catalog)中的一个可选条目。CatalogAttributes.NAMES = "/Names"定义于 pypdf/constants.py,对应 PDF 规范 §7.7.2 中 catalog 的 Names 属性。
第二步:在名称字典下创建/JavaScript名称树
names[NameObject("/JavaScript")] = DictionaryObject( {NameObject("/Names"): ArrayObject()} )/JavaScript是一个名称树(name tree),其/Names是键值对数组,用于容纳多条JavaScript 动作。
第三步:为每条脚本分配唯一名称并追加动作
js_list.append(create_string_object(str(uuid.uuid4()))) js = DictionaryObject( { NameObject(PagesAttributes.TYPE): NameObject("/Action"), NameObject("/S"): NameObject("/JavaScript"), NameObject("/JS"): TextStringObject(f"{javascript}"), } ) js_list.append(self._add_object(js))每个动作字典包含三个关键键:
/S /JavaScript:声明这是一个 JavaScript 动作(Action subtype);/JS:存放 JavaScript 源码字符串;/Type /Action:动作对象类型标记。
值得注意的两个设计细节:
- 名称用
uuid.uuid4()生成:源码注释说明 "We need a name for parameterized JavaScript in the PDF file, but it can be anything"——名称树的键只是 PDF 内部标识,内容不限,pypdf 用 UUID 保证每次生成的名称全局唯一,避免与既有条目冲突; - 每次调用都追加新动作,而不是覆盖旧动作:
/Names数组会被不断 append,因此多次调用add_js会在文档中积累多条脚本。
多次调用 add_js 的行为
对应测试 tests/test_javascript.py 专门验证了这一点:连续两次调用add_js后,从名称树中取出的两个名称条目不同,断言first_js != second_js,确认"add_js should add to the previous script in the catalog"——即后一次调用会追加到既有脚本之后,而不是替换。
同时 tests/test_javascript.py 验证了调用add_js后写入器的根对象会新增/Names,且/Names下包含/JavaScript名称树,这与源码实现完全对应。
与加密(encrypt)组合时的注意事项
add_js常与writer.encrypt()配合使用,tests/test_workflows.py 就展示了这样一个真实工作流:
# add some Javascript to launch the print window on opening this PDF. # the password dialog may prevent the print dialog from being shown, # comment the encryption lines, if that's the case, to try this out writer.add_js("this.print({bUI:true,bSilent:false,bShrinkToFit:true});") # encrypt your new PDF and add a password password = "secret" writer.encrypt(password) writer.encrypt(password) # doing it twice should not change anything with open(write_path, "wb") as output_stream: writer.write(output_stream)测试注释中给出的实用提醒非常关键:如果 PDF 被加密,阅读器在打开文档时可能先弹出密码验证对话框,而密码对话框会阻止打印对话框正常显示。如果你的目标是"打开即打印",且测试时发现打印框没出现,先注释掉加密相关代码排查。另外,测试还顺带验证了encrypt重复调用不会造成额外影响。
如何验证 JavaScript 是否写入成功
除了用支持 JS 的阅读器打开产物实测,还可以从两个层面自检:
- 阅读器行为验证:用 Adobe Acrobat 或支持 Acrobat JavaScript 的阅读器打开输出文件,观察是否弹出打印窗口;
- 文件结构验证:用 pypdf 自己检查根对象结构——
add_js之后,writer._root_object中应存在/Names → /JavaScript → /Names的层级(内部 API,仅用于调试),也可参考 tests/test_javascript.py 中的断言方式。
适用范围与限制小结
add_js注入的是打开文档时触发的 JavaScript 动作;如需页面级或控件级交互(如按钮点击触发),属于另外的注释/表单机制,不在本文讨论范围;- pypdf 不提供 JavaScript 引擎,脚本内容完全透传写入,语法错误不会在写入时报错,而是在阅读器执行时报错或静默失败;
- 最终效果依赖阅读器:部分阅读器完全不执行 JavaScript,部分仅支持
this.print等少数方法,发布前应在目标阅读器上实测; this.print的具体参数语义以 Adobe JavaScript API 为准,不同阅读器的实现细节可能存在差异。
参考路径
- API 实现:pypdf/_writer.py 中的
PdfWriter.add_js - Catalog 常量定义:pypdf/constants.py 中的
CatalogAttributes(NAMES = "/Names") - 单元测试:tests/test_javascript.py(验证名称树结构与多次追加行为)
- 组合工作流示例:tests/test_workflows.py(
add_js与encrypt共用及密码对话框注意事项)
【免费下载链接】pypdfA pure-python PDF library capable of splitting, merging, cropping, and transforming the pages of PDF files项目地址: https://gitcode.com/GitHub_Trending/py/pypdf
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考