使用技巧2026-09-29约 4 分钟阅读

用 pdfx 在终端和 CI 里处理 PDF:从第一条命令到可靠的脚本

用 pdfx 在命令行合并、压缩、加水印,一次处理多个文件,用管道把多个工具串成一个请求;写进脚本和 CI 时依赖的退出码、--json 报告、标准输入输出,以及条件过滤工具没有匹配时为什么以 0 退出。

PDF123 · 更新于 2026-09-29

把 pdfx 写进脚本或持续集成(CI,Continuous Integration)之前,先记住两件事。退出码 0 不一定表示写出了文件:条件过滤工具没有匹配时也是 0。--json 在只有一个输入和有多个输入时形状不同。只按 files 数组解析的脚本,在只匹配到一个文件时读不到结果。它是 HTTP 客户端,文件上传到服务器处理,没有本地引擎,也没有离线模式。完整的命令参考在开发者页面里的 CLI 指南。

示意图:一次 pdfx 调用向脚本交出三样东西,标准输出里的 JSON 报告、标准错误里的失败消息,以及退出码;脚本先读退出码

先装上,合并两个文件

需要 Node 20.3 或更新版本,或者 Bun:

npm install -g @pdf123/cli
pdfx --version

不想全局安装,就在命令前加 npx @pdf123/cli。当前目录里放好两个 PDF:

pdfx merge a.pdf b.pdf -o merged.pdf

成功后标准输出打印 merged.pdf。这是合并。换工具之前先问字段,不要猜参数名:

pdfx describe watermark
Add Watermark (watermark) - Add text or image watermarks to PDF files
Input:    1 file (.pdf)
Result:   file
Fields:
  --watermarkText <value>  Watermark text [default: PDF123]
  --fontSize <number>      Font size [default: 30]
                           min 6, max 200

(节选,字段还有 --rotation、--customColor 等。)字段就是命令行参数:

pdfx watermark in.pdf --watermarkText DRAFT -o marked.pdf

pdfx list 列出全部 95 个工具,--category security 只列一个类别,--query watermark 按词搜索。

多个文件写进目录,已有文件不会被盖掉

给单文件工具多个输入,每个文件完成时就写进 -o 指定的目录,不会攒到最后。一个文件失败,其余的继续,整批结束时退出码是 1。

pdfx compress a.pdf b.pdf -o small/

一次运行的标准输出是这样,顺序每次可能不同:

a.pdf -> small/a.pdf
b.pdf -> small/b.pdf

已有的目录可以直接用;目录还不存在时,-o 后面要带结尾的 /,否则 small 会被当成文件名。不写 -o,结果落在当前目录,用服务器给的文件名;已有同名文件时不会覆盖,而是变成 a-1.pdf、a-2.pdf。指定一个具体的文件名(-o same.pdf)则按你的指示替换已有内容。同时处理的文件数默认是 2,用 --concurrency 调整。批量运行中途按 Ctrl-C,已经写好的文件保留,退出码是 130。

加密的文件用 --input-password 密码 打开。一批里有的加锁、有的没有时,用 --password-for 文件名=密码,可以重复写。

要中间文件就分开调用,否则用 pipeline

先合并、再加水印、再压缩,中间结果不用落盘时,用 pipeline 收成一个请求,中间文件不会下载再上传。要每一步的文件,就分开调用。pipeline 只产出一个文件。

pdfx pipeline a.pdf b.pdf --step merge --step compress -o merged-small.pdf

# 需要给某一步带参数时,用 JSON 描述
pdfx pipeline a.pdf b.pdf \
  --steps '[{"tool":"merge"},{"tool":"watermark","params":{"watermarkText":"DRAFT"}},{"tool":"compress"}]' \
  -o out.pdf

一次最多 8 步,第 9 步得到 HTTP 400: at most 8 pipeline steps allowed。需要第二个文件的工具(比如叠加另一份 PDF 的 overlay-pdfs)不能放在 pipeline 里。pdfx 在上传之前就以退出码 2 拒绝,消息是 Tool "overlay-pdfs" needs a second file and cannot run as a pipeline step。

只有要把命令接到管道上时,才用标准输入。文件参数写 - 就从标准输入读,-o - 就把结果写到标准输出:

cat report.pdf | pdfx compress - -o - > report-small.pdf

这个模式下,标准输出里只有文件的字节,消息和错误都走标准错误,重定向是安全的。对一份 1 KB 左右的 PDF 验证过:标准输出里是一份 1040 字节的 PDF,与 -o 写出的文件大小相同,标准错误为空。从标准输入读又没有给 -o 时,结果命名为 stdin.pdf,写进当前目录并在标准错误里提示。

脚本匹配 reason,不要匹配消息

退出码 含义 例子
0 成功,或者条件过滤工具没有匹配 合并成功;filter-page-count 条件不成立
1 请求发出去了,但失败了;批量里至少有一个文件失败 密码错误、文件不是 PDF、超时、没有可返回的内容
2 用法错误,什么都没有上传 工具名拼错、第二步需要第二个文件的 pipeline、结果类型与输出文件名的扩展名矛盾
130 你中断了它 批量运行中按下 Ctrl-C

失败时,除了一行消息,标准错误里还有 reason:、code:、hint: 三行。拿一个加密文件配错误密码,在本地得到的是:

pdfx: HTTP 400: The password is incorrect.
reason: wrong_password
code: bad_request
hint: Check the password and try again.

密码参数的写法不同,第一行消息也不同:unlock --password 是上面这一行;给其他工具加 --input-password 时,服务器把解锁当作内部的一步,第一行是 HTTP 400: Pipeline step 0 (security/remove-password) failed: The password is incorrect.,reason: 一行都是 wrong_password。没给密码时是 This PDF is password-protected. Enter its password.,reason 为 password_required。脚本匹配 reason: 那一行。code 更粗,常见的值是 bad_request。

工具名拼错时退出码是 2,消息给出相近的名字:

pdfx: Unknown tool "compres". Did you mean: compress, decompress-pdf? Run `pdfx list` to see all tools.

结果类型和文件名矛盾同样是退出码 2,而且发生在写出之前。把会产出 ZIP 的拆分写到 x.pdf:

pdfx: The result is application/zip but "x.pdf" has a .pdf extension; name it *.zip or write into a directory
code: output_mismatch

超时也是退出码 1,code 为 timeout。--timeout 的单位是毫秒,默认 300000,也就是 5 分钟,所以 --timeout 60 是 60 毫秒,不是 60 秒。

条件不成立时退出码仍是 0

有一类工具回答的是是非问题:页数是否大于 N,是否含有某段文字,文件是否超过某个大小。命中时,它们原样返回输入文件;条件不成立时什么也不返回,pdfx 打印一行 no match 并以 0 退出。这几个过滤工具只在 SDK、命令行和 MCP 里有,网页上没有对应的页面。

这和另一种空结果不同。PDF 转 CSV 在 PDF 里找不到表格时,服务器返回 204,pdfx 以退出码 1 报 no_content:工具想产出内容却没有产出,原因见空表格导出(204)。过滤工具的没有匹配,是它本来要给出的答案。

脚本里读它,用 --json。命中时输出写出的文件,没有匹配时输出 { "matched": false }:

# 命中:写出文件,输出文件信息
$ pdfx filter-page-count three.pdf --pageCount 2 --comparator Greater --json -o big/
{ "path": "big/three.pdf", "contentType": "application/pdf", "bytes": 2594 }

# 没有匹配:不写文件
$ pdfx filter-page-count three.pdf --pageCount 5 --comparator Greater --json
{ "matched": false }

只留下页数多于 2 的文件,可以这样写。在本地对 a.pdf(1 页)和 three.pdf(3 页)跑过,只有后者被留下:

mkdir -p big
for f in *.pdf; do
  if pdfx filter-page-count "$f" --pageCount 2 --comparator Greater --json -o big/ \
      | jq -e '.matched == false' >/dev/null; then
    echo "$f: 跳过"
  fi
done

jq -e '.matched == false' 在没有匹配时退出 0,命中时退出 1。文件在 pdfx 命中时已经写进 big/,这个 if 只决定要不要打印“跳过”,不决定写不写。

只有一个输入时,--json 没有 files 数组

多个输入时,--json 的标准输出是一份完整报告。input 是绝对路径。processed 数的是请求成功的文件,其中没有匹配的也算在内。unmatched 是这些文件里没有匹配的个数,它们的条目是 { "ok": true, "matched": false },没有 path。批量里全部没有匹配时,退出码仍是 0。给批量加了 --idempotency-key 时,每个文件发出的键是 <键>:<序号>,机制见幂等键。

{
  "processed": 2,
  "unmatched": 0,
  "failed": 1,
  "files": [
    { "input": "/work/a.pdf", "ok": true, "path": "out/a.pdf", "contentType": "application/pdf", "bytes": 1040 },
    { "input": "/work/broken.pdf", "ok": false, "error": "HTTP 400: The file is not a valid PDF or it is damaged.", "reason": "invalid_pdf" },
    { "input": "/work/b.pdf", "ok": true, "path": "out/b.pdf", "contentType": "application/pdf", "bytes": 1027 }
  ]
}

只有一个输入时走单文件路径:--json 打印 { "path": ..., "contentType": ..., "bytes": ... },没有 files 数组。失败时标准输出是空的,信息都在标准错误里。用 glob 展开文件时,匹配到一个还是多个,两种格式都要能处理。

把上面的规则收成一个 CI 步骤

这个脚本只负责压缩,不涉及过滤工具。压缩失败时退出码是 1,这一步随之失败。过滤工具没有匹配时退出码是 0,所以 CI 不会因为“没有匹配”而失败;要不要把没有匹配当作问题,由你像上面过滤工具那一节一样读 --json 来判断。

任何文件被拒绝,这一步就失败,并在日志里列出文件名和原因。判断和前面一样:先看退出码,失败时打印标准错误,再用 jq 从批量报告里取出 reason。不带目录参数时会直接退出,避免 "$1"/*.pdf 被展开成 /*.pdf。

#!/usr/bin/env bash
# 用法:./ci-step.sh docs
docs="${1:?用法: ./ci-step.sh <目录>}"
mkdir -p out
pdfx compress "$docs"/*.pdf -o out/ --json > report.json 2> errors.log
status=$?
if [ "$status" -ne 0 ]; then
  cat errors.log >&2
  jq -r '.files[]? | select(.ok | not) | "\(.input | split("/") | last)\t\(.reason)"' report.json >&2
fi
exit "$status"

我们用四种输入在本地跑过:

目录里的文件 退出码 日志
两个正常 PDF 0 无
两个正常 PDF 加一个损坏的 1 pdfx: broken.pdf: HTTP 400: ...,随后一行 broken.pdf invalid_pdf
只有一个正常 PDF 0 无;report.json 是单文件格式
只有一个损坏的 PDF 1 reason: invalid_pdf、code: invalid_document 和一行 hint;report.json 为空

jq 里的 .files[]? 末尾的问号,让单文件的报告(没有 files)不报错。单个损坏文件没有批量报告,原因只在 errors.log 里,所以这两行输出都要保留。

放进 CI 之前,还要核对三件事。pdfx 需要网络:文件上传到 PDFX_API_BASE,默认是 https://pdf123.xyz。必须留在内网的文档,就自托管一个服务并把这个变量指过去,参见自托管能换来什么。需要身份时,把密钥放在 PDFX_API_KEY 里,不要写进命令行参数,那会留在进程列表和日志里。当前发布的是 0.1.0,CI 里用 npx @pdf123/[email protected] ... 固定版本,输出格式和退出码就不会随着新版本变化,升级成为你主动做的一次变更。

这个包在 npm 上的页面是 @pdf123/cli。

打开工具
在浏览器里完成处理,不加水印,文件处理完就会删除。
打开工具