How-to2026-09-2912 phút đọc

Dùng pdfx trong terminal và CI: từ lệnh đầu tiên đến một script đáng tin cậy

Dùng pdfx trên dòng lệnh để gộp, nén và thêm hình mờ, xử lý nhiều tệp cùng lúc và nối các công cụ thành một yêu cầu bằng pipeline. Với script và CI, hãy biết nên dựa vào đâu: mã thoát, báo cáo --json, đầu vào và đầu ra chuẩn, và vì sao công cụ lọc theo điều kiện không khớp vẫn thoát với mã 0.

PDF123 · Updated 2026-09-29

Trước khi đưa pdfx vào script hoặc tích hợp liên tục (CI), hãy nhớ hai điều. Mã thoát 0 không phải lúc nào cũng có nghĩa là một tệp đã được ghi: công cụ lọc theo điều kiện không khớp cũng thoát với mã 0. Và --json có hình dạng khác nhau cho một đầu vào và cho nhiều đầu vào, nên một script chỉ phân tích mảng files sẽ không tìm thấy gì khi chỉ có một tệp khớp. pdfx là một HTTP client: tệp được tải lên máy chủ để xử lý, không có engine cục bộ, và cũng không có chế độ ngoại tuyến. Tài liệu tham khảo đầy đủ về lệnh nằm trong hướng dẫn CLI ở trang dành cho nhà phát triển.

Sơ đồ: một lần gọi pdfx đưa cho script ba thứ, một báo cáo JSON trên đầu ra chuẩn, thông báo lỗi trên đầu ra lỗi chuẩn và một mã thoát; script đọc mã thoát trước

Cài đặt rồi gộp hai tệp

Bạn cần Node 20.3 trở lên, hoặc Bun:

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

Nếu không muốn cài toàn cục, hãy đặt npx @pdf123/cli trước lệnh. Với hai tệp PDF trong thư mục hiện tại:

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

Khi thành công, đầu ra chuẩn in merged.pdf. Đó là công cụ Gộp PDF. Trước khi chuyển sang công cụ khác, hãy hỏi danh sách trường thay vì đoán tên tham số:

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

(Đây là phần trích; các trường còn gồm --rotation, --customColor và những trường khác.) Một trường chỉ là một tùy chọn dòng lệnh:

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

pdfx list liệt kê đủ 95 công cụ, --category security chỉ liệt kê một nhóm, còn --query watermark tìm theo từ.

Nhiều tệp vào một thư mục, và tệp có sẵn không bị ghi đè

Đưa cho công cụ xử lý một tệp nhiều đầu vào, mỗi tệp được ghi vào thư mục chỉ định bởi -o ngay khi nó xong, không bị giữ đến cuối. Nếu một tệp lỗi, các tệp khác vẫn tiếp tục, và mã thoát ở cuối lô là 1.

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

Đầu ra chuẩn của một lần chạy trông như sau, và thứ tự có thể khác nhau mỗi lần:

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

Thư mục đã có thì dùng được ngay; nếu thư mục chưa tồn tại, -o cần dấu / ở cuối, nếu không small sẽ bị coi là tên tệp. Không có -o, kết quả nằm trong thư mục hiện tại với tên do máy chủ đặt; tệp có sẵn không bị ghi đè và tệp mới trở thành a-1.pdf, a-2.pdf. Một tên tệp cụ thể (-o same.pdf) sẽ thay thế nội dung cũ, đúng như bạn đã yêu cầu. Mặc định hai tệp được xử lý cùng lúc; đổi bằng --concurrency. Nếu bạn nhấn Ctrl-C giữa chừng một lô, các tệp đã ghi vẫn còn và mã thoát là 130.

Mở tệp đã mã hóa bằng --input-password PASSWORD. Khi một lô trộn lẫn tệp khóa và không khóa, dùng --password-for FILE=PASSWORD, có thể lặp lại nhiều lần.

Gọi riêng từng công cụ nếu cần tệp trung gian, còn lại dùng pipeline

Để gộp, rồi thêm hình mờ, rồi nén, khi bạn không cần kết quả trung gian trên đĩa, hãy gộp thành một yêu cầu bằng pipeline; các tệp trung gian không bị tải xuống rồi tải lên lại. Nếu bạn muốn có tệp của từng bước, hãy gọi riêng từng công cụ. Một pipeline chỉ tạo ra một tệp.

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

# Khi một bước cần tham số, hãy mô tả các bước bằng JSON
pdfx pipeline a.pdf b.pdf \
  --steps '[{"tool":"merge"},{"tool":"watermark","params":{"watermarkText":"DRAFT"}},{"tool":"compress"}]' \
  -o out.pdf

Một pipeline có tối đa 8 bước; bước thứ 9 nhận HTTP 400: at most 8 pipeline steps allowed. Công cụ cần tệp thứ hai (ví dụ overlay-pdfs, chồng một PDF khác lên) không thể là một bước của pipeline. pdfx từ chối nó trước khi tải lên, với mã thoát 2 và thông báo Tool "overlay-pdfs" needs a second file and cannot run as a pipeline step.

Chỉ dùng đầu vào chuẩn khi bạn muốn gắn lệnh vào một pipe. Đối số tệp là - sẽ đọc từ đầu vào chuẩn, và -o - ghi kết quả ra đầu ra chuẩn:

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

Ở chế độ này, đầu ra chuẩn chỉ mang byte của tệp; thông báo và lỗi đi ra đầu ra lỗi chuẩn, nên việc chuyển hướng là an toàn. Chúng tôi đã kiểm tra bằng một tệp PDF khoảng 1 KB: đầu ra chuẩn chứa một tệp PDF 1040 byte, cùng kích thước với tệp ghi bằng -o, và đầu ra lỗi chuẩn trống. Khi bạn đọc từ đầu vào chuẩn và không cho -o, kết quả được đặt tên là stdin.pdf, ghi vào thư mục hiện tại kèm một ghi chú trên đầu ra lỗi chuẩn.

Script nên khớp theo lý do, không theo thông báo

Mã thoát Ý nghĩa Ví dụ
0 Thành công, hoặc công cụ lọc theo điều kiện không khớp Gộp thành công; điều kiện filter-page-count sai
1 Yêu cầu đã được gửi nhưng thất bại; trong một lô, ít nhất một tệp lỗi Sai mật khẩu, không phải PDF, hết thời gian chờ, không có gì để trả về
2 Lỗi cách dùng, chưa tải lên gì Gõ sai tên công cụ, bước pipeline cần tệp thứ hai, kiểu kết quả mâu thuẫn với phần mở rộng của tệp đầu ra
130 Bạn đã ngắt Ctrl-C giữa một lô

Khi thất bại, ngoài một dòng thông báo, đầu ra lỗi chuẩn còn có ba dòng: reason:, code: và hint:. Một tệp mã hóa với mật khẩu sai đã cho ra kết quả sau khi chạy cục bộ:

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

Cách truyền mật khẩu làm đổi dòng đầu của thông báo: unlock --password cho dòng ở trên. Với --input-password ở công cụ khác, máy chủ coi việc mở khóa là một bước nội bộ và dòng đầu là HTTP 400: Pipeline step 0 (security/remove-password) failed: The password is incorrect., trong khi dòng reason: đều là wrong_password ở cả hai trường hợp. Khi không có mật khẩu nào, thông báo là This PDF is password-protected. Enter its password. và reason là password_required. Script nên khớp theo dòng reason:. code thô hơn, và một giá trị phổ biến là bad_request.

Gõ sai tên công cụ thoát với mã 2, và thông báo gợi ý các tên tương tự:

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

Kiểu kết quả mâu thuẫn với tên tệp cũng thoát với mã 2, và điều đó xảy ra trước khi có gì được ghi. Ghi tệp ZIP mà Tách PDF tạo ra vào 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

Hết thời gian chờ cũng thoát với mã 1, với code là timeout. Đơn vị của --timeout là mili giây và mặc định là 300000, tức 5 phút, nên --timeout 60 nghĩa là 60 mili giây, không phải 60 giây.

Khi điều kiện sai, mã thoát vẫn là 0

Một nhóm công cụ trả lời câu hỏi có hoặc không: số trang có lớn hơn N không, tệp có chứa một đoạn văn bản nào đó không, tệp có lớn hơn một dung lượng nào đó không. Khi câu trả lời là có, chúng trả lại tệp đầu vào không đổi; khi là không, chúng không trả gì, và pdfx in một dòng no match rồi thoát với mã 0. Các công cụ lọc này chỉ có trong SDK, dòng lệnh và MCP; trang web không có trang cho chúng.

Điều này khác với một loại kết quả rỗng khác. Khi PDF sang CSV không tìm thấy bảng nào trong PDF, máy chủ trả 204 và pdfx thoát với mã 1 và báo no_content: công cụ vốn phải tạo ra nội dung mà không tạo được, và lý do nằm trong Xuất bảng trống (204): PDF của bạn có lẽ không có cột. Với công cụ lọc, "không khớp" chính là câu trả lời nó phải đưa ra.

Để đọc điều này trong script, dùng --json. Khi khớp, nó in tệp đã được ghi; khi không khớp, nó in { "matched": false }:

# Khớp: tệp được ghi, và thông tin của nó được in ra
$ pdfx filter-page-count three.pdf --pageCount 2 --comparator Greater --json -o big/
{ "path": "big/three.pdf", "contentType": "application/pdf", "bytes": 2594 }

# Không khớp: không có tệp nào được ghi
$ pdfx filter-page-count three.pdf --pageCount 5 --comparator Greater --json
{ "matched": false }

Để chỉ giữ các tệp có hơn 2 trang, bạn có thể viết như sau. Chúng tôi đã chạy cục bộ với a.pdf (1 trang) và three.pdf (3 trang), và chỉ tệp sau được giữ lại:

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: bỏ qua"
  fi
done

jq -e '.matched == false' thoát với mã 0 khi không khớp và 1 khi khớp. Khi khớp, tệp đã được pdfx ghi vào big/; if chỉ quyết định có in "bỏ qua" hay không, chứ không quyết định có ghi hay không.

Khi chỉ có một đầu vào, --json không có mảng files

Với nhiều đầu vào, đầu ra chuẩn dưới --json là một báo cáo đầy đủ. input là đường dẫn tuyệt đối. processed đếm các tệp có yêu cầu thành công, kể cả những tệp không khớp. unmatched là số tệp trong đó không khớp, và mục của chúng là { "ok": true, "matched": false } không có path. Khi mọi tệp trong một lô đều không khớp, mã thoát vẫn là 0. Khi bạn truyền --idempotency-key cho một lô, khóa gửi cho từng tệp là <key>:<index>; cơ chế nằm trong Idempotency-Key: thử lại an toàn cho tác vụ PDF.

{
  "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 }
  ]
}

Với một đầu vào duy nhất, đường đi cho một tệp được dùng: --json in { "path": ..., "contentType": ..., "bytes": ... } và không có mảng files. Khi thất bại, đầu ra chuẩn trống và mọi thứ nằm trên đầu ra lỗi chuẩn. Khi bạn mở rộng tệp bằng glob, script phải xử lý cả hai định dạng, dù khớp một tệp hay nhiều tệp.

Gom các quy tắc trên vào một bước CI

Script này chỉ nén và không dùng công cụ lọc nào. Nén thất bại thì thoát với mã 1, và bước đó thất bại theo. Công cụ lọc không có tệp khớp thoát với mã 0, nên CI không thất bại chỉ vì không có tệp khớp; việc không khớp có phải là vấn đề hay không là do bạn quyết định bằng cách đọc --json, như trong phần về công cụ lọc ở trên.

Nếu có bất kỳ tệp nào bị từ chối, bước này thất bại và liệt kê tên tệp cùng lý do trong log. Logic vẫn như trước: đọc mã thoát trước, in đầu ra lỗi chuẩn khi thất bại, rồi dùng jq để lấy reason ra từ báo cáo lô. Nếu không có đối số thư mục, script thoát ngay, để "$1"/*.pdf không bao giờ bị mở rộng thành /*.pdf.

#!/usr/bin/env bash
# Cách dùng: ./ci-step.sh docs
docs="${1:?cách dùng: ./ci-step.sh <thư mục>}"
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"

Chúng tôi đã chạy nó cục bộ với bốn loại đầu vào:

Tệp trong thư mục Mã thoát Log
Hai tệp PDF tốt 0 không có
Hai tệp PDF tốt cộng một tệp hỏng 1 pdfx: broken.pdf: HTTP 400: ..., rồi một dòng broken.pdf invalid_pdf
Một tệp PDF tốt 0 không có; report.json ở định dạng một tệp
Một tệp PDF hỏng 1 reason: invalid_pdf, code: invalid_document và một dòng hint; report.json trống

Dấu hỏi ở cuối .files[]? trong jq giúp báo cáo một tệp (không có files) không gây lỗi. Một tệp hỏng duy nhất không có báo cáo lô và lý do chỉ xuất hiện trong errors.log, nên hãy giữ cả hai luồng đầu ra.

Còn ba điều nữa cần kiểm tra trước khi đưa vào CI. pdfx cần mạng: tệp được tải lên PDFX_API_BASE, mặc định là https://pdf123.xyz. Với tài liệu phải ở lại trong mạng của bạn, hãy tự chạy một dịch vụ và trỏ biến này tới nó; xem Self-host thực sự mua được gì (và tốn gì). Khi cần danh tính, hãy đặt khóa trong PDFX_API_KEY chứ không đặt trong đối số dòng lệnh, nơi nó sẽ nằm lại trong danh sách tiến trình và log. Phiên bản đang phát hành hiện là 0.1.0; trong CI hãy dùng npx @pdf123/[email protected] ... để ghim phiên bản, để định dạng đầu ra và mã thoát không đổi theo các bản phát hành mới, và việc nâng cấp trở thành một thay đổi do bạn chủ động thực hiện.

Trang của gói trên npm là @pdf123/cli.

Open tool
Process in the browser — no watermark, files removed after the job.
Open tool