摘要
一份 PDF 双击就能打开,复制和打印却不听使唤,工具还说它已加密。这种文件多半只设了权限密码,我用 7 份自己生成的占位合同把这件事从头演了一遍。前半篇是实测,讲两种密码在阅读器里怎么认、Chromium 149 和 Firefox 151 各拦了什么、图映 PDF 压缩碰到 4 份加密文件全报失败时说了什么。后半篇是一个只读文件的分堆脚本和一套向对方要可编辑版的说法。我试完的结论是这类文件该找发件人而不是找工具。
版本声明
实测在 2026 年 9 月 30 日做完,分堆脚本在 10 月 1 日又补跑过一遍。机器是一台 Apple M4 的 Mac,系统是 macOS 26.5.2。看文件用的是两个浏览器自带的 PDF 阅读器,一个是 Chromium 149 开源构建,一个是 Firefox 151。Chromium 的阅读器界面是中文。Firefox 在这台机器上只有英文界面。脚本跑在 Python 3.9 上,用的库是 PyMuPDF 1.26.5。图映 PDF 压缩用的是当天的线上版本。Acrobat Reader、macOS 预览、Safari 正式版、WPS 和手机端都没测。
适用边界
样本全是我用脚本生成的 2 页占位文件,每份的正文就是「示例合同(占位文字,仅供测试)」这一行。里面没有一份真合同,也没有一份客户的文件。我只测了最常见的标准安全处理器,证书加密和带数字签名的文件都不在样本里。文中的处理办法只针对两类文件。一类是对方发给你、你也有权使用的,另一类是你自己做的。别人不愿意放开的限制怎么去掉这篇不讲。
文章目录
一 PDF已加密不能编辑该先做什么
- 我用七份占位合同演了一遍
- 找工具硬改不划算 二 打开不要密码为什么还显示已加密
- 弹不弹密码框是第一道分界
- 五份只设了权限密码
- 限制管不管用要看阅读器
- 换加密算法结果没有变化 三 压缩工具为什么也处理不了
- 图映对四份加密文件都报失败
- 只禁复制的那份也被拒了
- 换成我做工具也会先拒绝 四 收到一批PDF怎么先分三堆
- 只看权限整数分不清两份文件
- 先问要不要密码再看加密字典
- 权限位要翻成对方听得懂的话 五 向对方要可编辑版该怎么说
- 有限制的请对方重新导出
- 要密码的单独要密码
- 消息里写清要做哪件事
- 对方不放开时请对方来改 六 适用边界与风险 七 测量方法自身的坑
- 两个库的加密判断意思相反
- 打印拦没拦只是间接判断
- 进度卡写完成不等于压完了 八 还没解决的问题
一 PDF已加密不能编辑该先做什么
我用七份占位合同演了一遍
我接外包一直是按天报价的。报价之前我最怕的不是活难,是干到一半才发现对方给的文件用不了。合同、报价单和验收单这类 PDF 最容易出这种事。不妨设想甲方发来一份服务合同让你照着它把付款节点改成两期再出一份新报价。文件双击就开了而且从头到尾也没人问你要过密码。可是选中一段条款复制出来是空的,点打印也没有反应。你想先把它压小存个档时压缩工具又直接报错说这份文件有加密。碰到这种文件我以前的第一反应是去搜一个能把限制处理掉的工具。
先交代清楚上面这个场景是假设的,我还没在真合同上碰到过它。这回我在给自己的接单流程加一道收件检查时干脆拿自己生成的文件把它从头演了一遍。我用脚本做了 7 份每份都在 66 到 69 KB 之间的 2 页测试 PDF,页面上除了「示例合同(占位文字,仅供测试)」这行字就没有别的内容。7 份里 1 份不加密当对照,5 份只设了权限密码,剩下 1 份设的是打开密码。我先把它们挨个放进 Chromium 149 开源构建自带的阅读器里看再放进 Firefox 151 里又看了一遍。然后拿它们去试了一个压缩工具又写了个只读文件的小脚本把收到的 PDF 分成三堆并给每堆配一句向对方开口的话。试完以后我觉得这类文件该找发件人而不是找工具。
找工具硬改不划算
先说我为什么不想再走找工具这条路。头一个原因是算账。搜工具、装工具、一个个试过去再确认改出来的文件没坏,这一套下来半天就没了。我是按天报价的,这半天没人会给我结。更要命的是合同这种东西本身就决定了你在自己电脑上改出来的版本对方根本不认。真要改条款最后还得对方拿他们的原稿改好再出一份。我自己费劲改出来的那份说到底只是一份没人签的草稿。还有一层是边界,文件是对方的而且限制也是对方加的。人家没说可以放开的时候我去处理它就越界了。这一层我想了好一会儿才想明白,以前我一直只把它当成一个技术问题看。
时间上的账还有一笔容易漏掉。找工具的那半天往往不是在报价前花的,是在开工以后才花的。那时候工期已经报给对方了,多出来的半天只能从别的活里往外挤。要是在报价前就知道这份文件用不了,我可以先找对方要好文件再报工期。这半天也就根本不用花。我后来给自己定的规矩就是收到文件先过一遍检查再报价。这么一算整件事里我真正需要的只有快速判断这份文件属于哪种情况和知道该向对方要什么以及话该怎么说这两样。后面几章都围着这两样走。
二 打开不要密码为什么还显示已加密
弹不弹密码框是第一道分界
PDF 里能设的两种密码管的是完全不同的事。打开密码也叫用户密码,没有它文件根本打不开。权限密码也叫所有者密码。它不拦你打开文件只在文件里写明不许打印、不许复制、不许修改。最直观的区分办法就是看双击那一下有没有弹出密码框。我那份带打开密码的样本放进 Chromium 149 自带的阅读器以后一打开就停在了密码框前面。弹框标题是「需要密码」,下面一行写着「本文档设置了密码保护,请输入密码。」。能点的只有右下角那个「提交」。放进 Firefox 151 也同样被拦住了,英文界面上写的是「Enter the password to open this PDF file.」。
这张图要看的是密码框背后什么都没有。整页正文被挡得严严实实,只剩一个输入框和一个「提交」按钮。碰到这一种的时候手里其实还没有一份能读的合同。后面讲的分堆和开口都是从这里分岔的。另外 5 份只设了权限密码的样本完全是另一个样子。它们在两个阅读器里都是双击就直接显示正文而且界面上连一行限制提示都没有。你不去复制也不去打印的话根本察觉不到它们有什么不一样。
五份只设了权限密码
头一回看到这 5 份直接打开的时候我以为是生成脚本没把限制写进去,换了两个库去读才确认没写错。这 5 份文件都带着完整的加密字典而且内容也确实用 AES 或 RC4 加密过。只是打开用的那把用户密码是空的,阅读器自己拿空密码一试就解开了。人完全感觉不到这一步。工具说它加密了并没有说错。它确实是一份加密文件,只不过在这里加密的主要用处是把那几条限制写进文件。下面是 7 份样本的底细。
| 样本 | 打开时要不要密码 | 加密方式 | 文件里声明的限制 |
|---|---|---|---|
| S0 | 不要 | 无 | 无 |
| S1 | 不要 | AES-256 | 禁打印、高质量打印、修改、复制、注释、填表、组合 |
| S2 | 不要 | RC4 128 位 | 同 S1 |
| S3 | 不要 | AES-128 | 只禁复制 |
| S4 | 不要 | AES-256 | 只禁打印和高质量打印 |
| S5 | 要 | AES-256 | 同 S1 |
| S6 | 不要 | RC4 40 位 | 全禁,连无障碍提取也禁 |
表里 S1 到 S4 再加上 S6 就是打开不要密码而工具却说已加密的那一类。平时常听人说 PDF 打得开却改不了也复制不了,我猜说的多半就是这一类。这只是我的猜测,我手上没有真实文件的统计也不敢说比例。S5 那份最特别的地方在于它既要打开密码又在打开以后带着和 S1 一样的全禁限制。这一点到后面向对方要东西的时候会用上。我以前对加密这个词的理解也太粗了,一直觉得加密就等于打不开。拆开看才知道一份文件可以既加了密又谁都能打开。这有点像快递单上印的「易碎轻放」。字是实打实印上去的,快递员照不照做还得看快递员自己。
限制管不管用要看阅读器
权限密码写进文件的只是几条要靠阅读软件去执行的声明。同一份全禁的 S1 在 Chromium 149 自带的阅读器里拖选正文时蓝色高亮照常出来。我按 Cmd+C 再去另一个页面的文本框里粘贴却什么都没有粘出来。点工具栏上的打印按钮也没有反应,这两样它确实拦住了。可它允许我点「绘制」在页面上画笔迹,7 份样本都能画上去。Firefox 151 默认设置下打印、复制和编辑三样都没拦。把 Firefox 的权限执行开关打开再试一遍以后禁打印的那几份打印按钮被藏了起来,禁编辑的那几份编辑类按钮也变灰了。复制却照样能复制。pdf.js 和 PyMuPDF 这类库又是另一种做法,它们只把权限位报给调用它的程序再由写程序的人自己决定拦不拦。
写这些不是为了比哪个阅读器好用,更不是要你换个阅读器去绕开限制。它说明的是对方给文件加的限制在你这边生不生效取决于你恰好用什么软件打开。反过来也一样,我自己给报价单设了禁止复制的话对方那边拦不拦得住我说了不算。发文件的和收文件的都别把它当成可靠的保障。也别走到另一头说权限密码没用,Chromium 里复制和打印是实打实拦住了的。还有个我原本没想到的细节是 S5 那份在 Chromium 里输对打开密码以后复制和打印照样被拦。用户密码只管能不能打开,打开以后的限制照旧按文件里写的执行。拿到密码并不等于拿到一份能改的文件,这一点对后面开口要什么很关键。
换加密算法结果没有变化
动手之前我以为加密算法会影响阅读器怎么执行限制,老的 RC4 说不定会被哪个阅读器区别对待。我专门留了三份限制几乎相同而算法不同的文件来对照。S1 用的是 AES-256,S2 用的是 RC4 128 位,S6 用的是 RC4 40 位。S6 比另外两份多禁了一项无障碍提取,打印、复制和编辑这几项都一样。结果在 Chromium、Firefox 默认和 Firefox 开权限执行这三种情况下完全一致。只禁复制的 S3 和只禁打印的 S4 也都按各自写的限制在表现。影响行为的只有权限位。算法不管事。
我的猜测就这么被推翻了,可这条对我要做的事反倒是好消息。向对方要文件的时候我完全不用关心他用的是什么加密方式。我只需要知道打开要不要密码和打开以后限了哪几项这两件事。
三 压缩工具为什么也处理不了
图映对四份加密文件都报失败
我想知道开头场景里那个报错的压缩工具是不是换一个就能过去,就拿自己的样本试了一个。我用的是图映(imging.cn/)的 PDF 压缩,试的是自己生成的只设权限密码和带打开密码两种测试 PDF 外加那份不加密的对照,档位都选电子书。导入的时候队列里只写着「待处理 67 KB」,看不出任何异样。点开始压缩以后不加密的 S0 从 66 KB 压到了 49 KB,结果卡上写着「压缩完成 · 省下 25.7%」。导进去的 4 份加密文件 S1、S2、S3、S5 全部标成了「失败」。每一份后面跟的都是同一句「这份 PDF 有加密或权限保护,需要先解除保护」。
这张图要看左边红框里全禁的 S1 那一行。文件名后面跟着红色的「失败」两个字,下一行就是那句提示。红框下面的左栏常驻说明里,最后一句就是「加密或有权限保护的 PDF 需要先解除保护」,这条边界它在页面上一直写着,只是导入前我没留意。右边的进度卡同时写着「全部完成」和「0 个文件已压缩完成,可以逐个保存。」这两句。两句放在一起有点别扭,我放到第七章再说。这次处理全程在本机跑。我数了一下整个过程页面一共发了 46 个请求。其中非 GET 的是 0 个,接触的也只有 imging.cn 一个域名。合同这类文件我本来就不想往别人的服务器上传,这一点我挺在意。只是这回它帮不上忙,文件原样退回给了我。
只禁复制的那份也被拒了
最让我意外的是只禁了复制、修改和组合页面都允许的 S3。我原以为压缩碰不到复制这一项总该能过。结果照样被拒。带打开密码的 S5 也没有弹出让我输密码的框而是导进去就直接失败了。这么看下来图映对带加密字典的文件是一律拒绝的。它不分是哪种密码,也不看具体限了哪几项。界面上没有说为什么这么做。我猜是重写一份 PDF 就得把加密重新处理一遍还会碰到所有者设下的那些限制,干脆不处理更稳妥。这只是我的推测。图映的 PDF 引擎具体怎么写的我不知道也没法从外面看出来。我能确定的只有界面上那句原话和 4 份失败的结果。
我没有再去找别的压缩工具试也不打算为这事去找一个肯处理的。压缩在我的流程里只是可有可无的存档那一步。真正卡住我的是改合同,这一步换什么工具都绕不开对方。
换成我做工具也会先拒绝
我自己维护着两个小工具站,看到这个拒绝时我第一反应是站到做工具的那边去想。假如我的站也接 PDF 的话碰到带加密字典的文件我大概也会先一刀切拒掉。原因很现实。要区分打开密码和权限密码再按权限位决定哪些操作能做就得写一堆分支,每个分支都要测。我一个人维护根本测不过来。更麻烦的是出了事说不清。用户拿着处理过的合同去找对方的时候对方要是问这份文件谁动过,我这个做工具的就被卷进去了。拒绝是最省事也最不容易惹麻烦的做法。
不过真要做的话我会在拒绝之外多给一句话,告诉用户这份文件属于哪一种以及该去找谁要什么。图映现在的提示说的是「需要先解除保护」,没说由谁来解除。对懂行的人这句话够用了。对第一次碰到的人来说它很容易被读成去找个工具解掉,正好把人往我开头那条弯路上推。这是我站在用户这边的一点感受,不代表它就该这么改。换成我自己的站我会把提示拆成两句,打开要密码的那种写请向发件人索取打开密码,打得开却带限制的那种写请发件人重新导出一份不带限制的版本。两句话都不难写,难的是前面那一步判断,这正好就是下一章那个脚本在做的事。
我现在对这个拒绝的看法也跟以前不一样了。以前我总觉得工具拒绝就是工具不够强。现在我更愿意把它看成一个提醒,它在我准备走那段弯路之前就把我拦了下来。它的本意是不是这样我不知道,这只是我作为用户的感受。碰到这种文件以后知道该去找谁,对我来说比找到一个肯处理的工具有用得多。
四 收到一批PDF怎么先分三堆
前面那些都是一份一份手动看的。要是对方一次甩过来一个文件夹,合同、报价单和发票扫描件混在一起的话每份都双击一遍就太费时间了。我就写了个只打开文件读三个值的小脚本放在收件文件夹边上跑。它不输任何密码也不改、不存任何文件。我对它的要求只有跑完告诉我每份文件在哪一堆以及要向对方要什么。一开始我只想分能用和不能用两堆,写到一半我发现不能用的那堆里要对方做的事完全不一样才改成了三堆。脚本写完那会儿茶已经泡到第三泡早就没什么味了。
只看权限整数分不清两份文件
第一版我图省事只看 PyMuPDF 给的 permissions 这一个整数。思路是不加密的文件权限全开,跟它不一样的就算有限制。我先拿不加密的 S0 跑出来是 -4,就把 -4 当成了干净的标准。
# 第一版:只看权限整数,按「全允许就是干净」分拣(有问题的写法,留作对照)
import pathlib, sys
import pymupdf
ALL_OPEN = -4
for p in sorted(pathlib.Path(sys.argv[1]).glob("*.pdf")):
word = pymupdf.open(p).permissions
verdict = "干净" if word == ALL_OPEN else "有限制"
print(p.name[:2], word, verdict)
# 在 7 份样本上的输出:
# S0 -4 干净
# S1 -3392 有限制
# S2 -3392 有限制
# S3 -20 有限制
# S4 -2056 有限制
# S5 0 有限制
# S6 0 有限制
代码说明。这段乍一看没毛病,6 份加密文件都被判成了有限制。问题出在最后两行。S5 和 S6 读出来都是 0,可它们根本不是一回事。S6 是只设了权限密码又什么都禁的文件,双击就能打开。S5 是带打开密码、我还没输密码的文件,连内容都看不到。前一种要找对方重新导出,后一种要找对方要密码。开口的话完全不同。只看一个整数的话这两份就被并成了一堆。我这才意识到分堆的第一步不该是看权限。还有一点要留意的是 -4 只是我这版 PyMuPDF 对 S0 的读数,换个库或者换个版本未必还是它。拿它当标准本身就不稳。
先问要不要密码再看加密字典
第二版把顺序倒了过来。先看 needs_pass 也就是打开要不要密码。要的话直接归进要密码那一堆就不用再看后面的了。不要密码的再去看元数据里有没有加密方式这一项。有的话才去读权限整数,没有就是干净的。
import pathlib, sys
import pymupdf
def pile_of(pdf_path):
with pymupdf.open(pdf_path) as doc:
if doc.needs_pass:
return "要密码", None
scheme = (doc.metadata or {}).get("encryption")
if scheme:
return "有限制", doc.permissions
return "干净", None
# pile_of 在 7 份样本上的返回值:
# S0 干净 None
# S1 有限制 -3392
# S2 有限制 -3392
# S3 有限制 -20
# S4 有限制 -2056
# S5 要密码 None
# S6 有限制 0
代码说明。needs_pass 只对 S5 是 1,要密码的那一堆就这样单独拎了出来。元数据里的加密方式对 S1 读出来是「Standard V5 R6 256-bit AES」,对 S0 是 None。这里藏着一个顺序上的坑。还没解开的 S5 读这一项也是 None,跟不加密的 S0 一模一样。要是先看它的话 S5 就会被误判成干净的。needs_pass 只能排在最前面。分完的结果是干净 1 份、有限制 5 份、要密码 1 份,和我手动一份份看的结论对得上。用 with 打开是我的习惯,批量跑的时候文件句柄不会攒着不放。
权限位要翻成对方听得懂的话
分完堆还不够。有限制那一堆里 S3 只禁复制,S4 只禁打印,S1 又是全禁。向对方开口时要说的都不一样。权限整数直接贴给对方是没用的,-3392 这种数谁也看不懂。我只挑了接活真会用到的四项去翻译,分别是打印、复制文字、改内容和加批注。填表、组合页面和高质量打印我没放进去。它们接活时几乎用不上,放进去反倒让消息变长也让对方读着更糊涂。
ASK_ABOUT = (
("打印", pymupdf.PDF_PERM_PRINT),
("复制文字", pymupdf.PDF_PERM_COPY),
("改内容", pymupdf.PDF_PERM_MODIFY),
("加批注", pymupdf.PDF_PERM_ANNOTATE),
)
def locked_actions(perm_word):
return [label for label, bit in ASK_ABOUT if not perm_word & bit]
def message_for(file_name, pile, perm_word):
if pile == "要密码":
return f"《{file_name}》打开要密码,麻烦另外发我一下打开密码,别和文件放在同一封邮件里。"
if pile == "有限制":
what = "、".join(locked_actions(perm_word)) or "四项都没禁"
return (f"《{file_name}》能打开,但不能{what}。"
"方便的话用生成它的软件重新导出一份不带限制的,或者直接发原稿。")
return ""
files = sorted(pathlib.Path(sys.argv[1]).glob("*.pdf"))
piles = {"干净": [], "有限制": [], "要密码": []}
for p in files:
pile, word = pile_of(p)
piles[pile].append(p.name[:2])
note = message_for(p.name[:2], pile, word)
if note:
print(note)
print({k: v for k, v in piles.items()})
# 输出(S2、S6 两行与 S1 相同,略去):
# 《S1》能打开,但不能打印、复制文字、改内容、加批注。方便的话用生成它的软件重新导出一份不带限制的,或者直接发原稿。
# 《S3》能打开,但不能复制文字。方便的话用生成它的软件重新导出一份不带限制的,或者直接发原稿。
# 《S4》能打开,但不能打印。方便的话用生成它的软件重新导出一份不带限制的,或者直接发原稿。
# 《S5》打开要密码,麻烦另外发我一下打开密码,别和文件放在同一封邮件里。
# {'干净': ['S0'], '有限制': ['S1', 'S2', 'S3', 'S4', 'S6'], '要密码': ['S5']}
代码说明。PyMuPDF 里这四项的常量值分别是打印 4、改内容 8、复制 16、加批注 32。权限整数里哪一位是 0 就说明那一项被禁了,locked_actions 只做这一件事。S1、S2、S6 翻出来四项全禁,S3 只有复制文字,S4 只有打印,和样本表里写的限制一致。message_for 按堆生成一句话。要密码的请对方另外发密码,有限制的列出做不了的几项再请对方重新导出或者直接发原稿。干净的那堆不生成消息。整个脚本没有一行在改文件也没有一行在试密码,这是我给它定死的边界。它只负责告诉我该找谁和该说什么,文件本身一个字节都不动。这个脚本我放在收件文件夹边上,接活前跑一遍一眨眼就完。比起干到一半才发现文件用不了,这点时间我是愿意花的。它也不需要服务器,我那点可怜的带宽也不用为它操心。
我用它的方式很土。每次对方发来一批文件我先把它们丢进收件文件夹再在终端里跑一遍脚本,然后把输出的那几句话复制下来。干净的那堆我直接开工。有限制和要密码的那两堆我把消息改成对方能接受的口气以后一次性发过去,免得一份一份地来回问。以前我是干到哪一份卡住了才去问哪一份,对方要被我打扰好几次。现在一次问完以后对方也省事,我报工期的时候心里也有底。
五 向对方要可编辑版该怎么说
有限制的请对方重新导出
有限制那一堆最好办。文件已经能看了,缺的只是对方给的那几项权限。我会请对方用当初生成它的软件重新导出一份不带限制的,或者干脆把 Word 原稿发过来。对合同来说原稿往往比 PDF 更好用。付款节点直接在原稿里改好以后对方再出一份正式版,双方手里的东西就对得上了。还有一种情况是文件本来就是你自己做的,只是你忘了当初加过限制。这时候用当初生成它的软件输入你自己设的所有者密码再重新导出一份就行。这条只适用于你自己的文件,别人的文件不在这个范围里。
说法上还有个小坑。别跟对方说「你这个文件加密了」,很多人会以为你在说他发错了文件甚至觉得你在怪他。直接说你要做哪件事和卡在哪一步的话对方更容易明白也更愿意配合。
要密码的单独要密码
要密码那一堆只有向对方要打开密码这一件事。我的习惯是请对方换个渠道发,比如文件走邮件的话密码就走电话或者聊天软件,别把密码和文件放在同一封邮件里。脚本里那句话就是这么来的。拿到密码打开以后还得再看一眼有没有别的限制。前面说过 S5 输对密码以后复制和打印照样被拦,我那份 S5 就是两样都带的。碰到这种文件的话要完密码还得接着按有限制那一堆再开一次口。为了少一个来回,我会在要密码那条消息里顺手问一句打开以后能不能改和能不能打印。要密码的消息我会写得更短一点,因为我总觉得越是要密码这种事话越长对方越容易起疑心,以为我要拿文件干别的。
消息里写清要做哪件事
开口的时候我会把哪一份文件、我要拿它做什么和现在卡在哪一步这三样东西说清楚。脚本生成的那句话就是按这个思路写的。比如 S3 那份的消息是「能打开,但不能复制文字」,后面再跟一句请对方重新导出。真发给甲方的时候我会再补一句用途,比如要把付款节点改成两期或者要打印出来盖章。对方知道了用途就能自己判断是该给一份不带限制的 PDF 还是直接把原稿发过来。下面是我会发的一段话,人名是随手编的。
张经理您好,这份《服务合同》我这边能打开,但复制文字和打印都用不了。我要按您说的把付款节点改成两期再出新报价。方便的话麻烦用当初生成它的软件重新导出一份不带限制的,或者直接发一下 Word 原稿。改完我把修订稿发您确认。
这段话里我刻意没提加密和权限位这些词。对方多半不是做技术的,他只需要知道我要干什么和卡在哪里。最后那句改完发您确认也有用处,它让对方知道最终版还是他说了算,放开限制这件事在他看来就没什么风险了。
开口的时机我也想过,最好是在报价之前问而不是在开工以后问。报价前问的时候对方还在等你的报价,他有动力尽快把文件给你。开工以后再问的话对方会觉得你怎么现在才说,而且你自己的工期也已经开始走了。我现在的做法是收到文件当天就跑一遍脚本,有需要找对方要的东西就跟报价放在同一条消息里发出去。
对方不放开时请对方来改
也会有对方不方便给的时候。比如合同模板是法务定的,业务员手里只有那份 PDF。这时候我不会去处理那份文件,而是把要改的地方写成一份修改意见,标清第几页哪一条改成什么再发回去请对方改完出一份。这样会多一个来回,可最后那份文件是对方自己出的,后面扯不清的事就少了。时间上的账我也估过,一个来回大概率比我自己折腾半天要快,就算慢一点那段时间我也能去干别的活。要是连修改意见都提不了,比如对方只是发来让你看看,那它本来就是一份只读的文件。按只读的用法去用它就行,不必觉得它哪里出了问题。
六 适用边界与风险
这篇的数字都来自 7 份自建的 2 页占位文件。真实合同和银行回单这些文件里权限位怎么分布我手上没有数据,也不敢说大多数是哪一种。阅读器我只测了 Chromium 149 开源构建和 Firefox 151。Acrobat Reader、macOS 预览、Safari 正式版、WPS 和手机上的表现都没测。很多甲方用的恰恰是 Acrobat 和预览,这是我这次最大的缺口。证书加密、只加密附件和带数字签名的文件也都不在样本里。图映这边我只测了 PDF 压缩一个工具。站上的 PDF 转图片和内容提取碰到加密文件会怎样我没测也不敢替它说。
风险上有几件事我想单独交代。头一件是阅读器之间的差异不是办法。Firefox 默认不拦并不等于你可以拿它去处理对方限制过的文件。限制代表的是对方的意思,阅读器拦不拦只是软件的实现。再一件是 Chromium 能在禁批注的文件上画笔迹,可我只看到能画而没测保存。别把这个当成能改文件。脚本这边它只读三个值,不碰内容也不碰密码。要是你想在它上面加功能的话我的建议是往判断和提醒的方向加,别往处理文件的方向加。反过来说你要是发文件的那一方,想防止别人改你的报价单的话权限密码也不是可靠的办法。它在有的阅读器里拦得住而在有的阅读器里拦不住。真要防改的话我更愿意在报价单里写清以哪一版为准再自己留好原稿。
七 测量方法自身的坑
两个库的加密判断意思相反
同一个 is_encrypted 在两个库里的意思是反的。PyMuPDF 的 is_encrypted 指的是现在还锁着没解开,对只设权限密码的 S1 返回 False。pypdf 的 is_encrypted 指的是文件带不带加密字典,对 S1 返回 True。要是只看 PyMuPDF 这一个值就很容易得出 S1 没加密的结论。这跟工具报的加密对不上就让人糊涂了。分堆脚本里我干脆不用这个布尔值,改看 needs_pass 和元数据里的加密方式。换库的时候这一步一定要重新核对,同名的字段不一定是同一个意思。
打印拦没拦只是间接判断
Chromium 的打印我没看到真正的打印对话框。允许打印的 S0 和 S3 点了按钮会进入打印流程。禁打印的那几份点了以后页面没有任何变化,我是按这个区别来判的。键盘上按 Cmd+P 会怎样以及打印预览里会怎样我都没测。复制的判断也有讲究,每次测之前我都先把剪贴板清空或者写进一个哨兵字符串。不这么做的话粘贴出来的可能是上一次复制的正文,拦住了也会被误判成没拦住。我在第二章里说拦住了复制靠的就是这一步。
进度卡写完成不等于压完了
图映那次的结果不能只看右边的进度卡。卡上写着「全部完成」而且进度是 100%,下面一行却是「0 个文件已压缩完成,可以逐个保存。」。四个步骤里第一步停在「解析」,第四步「回读校验页面」反倒打了勾。要是只扫一眼标题就很容易以为压好了。我后来都以左栏队列里每一行的状态为准。那里写的是「失败」还是「−25.7%」一看就清楚。批量导入的时候更得这样一行行对,不然失败的那几份混在一堆文件里很容易被漏掉。
八 还没解决的问题
有两件事我还没想清楚。一件是真实文件。我的样本是自己造的,限制也是我按几种典型组合设的。现实里甲方发来的合同多半是哪一种以及限了哪几项我没有数据。等真碰上了我会先跑一遍脚本再把结果记下来。另一件是 Acrobat 和 macOS 预览,很多甲方用的就是它们。它们怎么执行这些限制我这次没法测。要是对方回我一句我这边能复制啊,我现在也解释不清是哪边的原因而只能请对方换个方式把原稿给我。还有一个小疑问我也没答案。S5 这种既要打开密码又带全禁限制的文件,对方当初为什么两样都设我说不准。我猜是导出软件里的勾选项默认就是这样,可我没有拿任何导出软件去核对过。这块我先这么留着。
要是你今天就收到了这样一份 PDF,可以先做两步。先看双击打开时有没有弹密码框,弹了就换个渠道去要密码。没弹的话再试试能不能复制和打印,把做不了的那几项记下来。然后按第五章那段话的意思请对方重新导出一份不带限制的或者直接发原稿。文件多的话就用第四章那个只读的脚本先分好堆再开口。
参考资料
- ISO 32000-1:2008《Document management — Portable document format — Part 1: PDF 1.7》第 7.6 节「Encryption」,加密字典与用户访问权限 /P 的定义。Adobe 公开的副本 PDF32000_2008.pdf。
- PyMuPDF 文档 Document,needs_pass、is_encrypted、permissions、metadata 几个属性的说明。
- pypdf 文档 PdfReader,is_encrypted 属性的说明。
- pdf.js 项目 mozilla/pdf.js,Firefox 自带阅读器的来源,权限执行开关对应 pdfjs.enablePermissions 这一项设置。
- 文中所有数字来自我自建的 7 份 2 页占位 PDF 在 2026 年 9 月 30 日与 10 月 1 日的实测记录。样本与分堆脚本都是自己生成和编写的。