pdfx у терміналі та в CI: від першої команди до надійного скрипта
Використовуйте pdfx у командному рядку, щоб об’єднувати, стискати й додавати водяні знаки, обробляти багато файлів одразу та зчіплювати інструменти в один запит через конвеєр. Для скриптів і CI з’ясуйте, на що покладатися: коди виходу, звіт --json, стандартний ввід і вивід, а також чому умовний інструмент-фільтр без збігу все одно завершується з кодом 0.

Перш ніж ставити pdfx у скрипт або в безперервну інтеграцію (CI), запам’ятайте дві речі. Код виходу 0 не завжди означає, що файл записано: умовний інструмент-фільтр без збігу теж завершується з кодом 0. А --json має різну форму для одного вхідного файлу і для кількох, тож скрипт, який розбирає лише масив files, нічого не знайде, коли збігся лише один файл. pdfx — це HTTP-клієнт: файли завантажуються на сервер для обробки, локального рушія немає, режиму офлайн теж. Повний довідник команд — у посібнику з CLI на сторінці для розробників.
Встановіть і об’єднайте два файли
Потрібен 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. Це був інструмент Об’єднати 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) замінює наявний вміст, як ви й сказали. За замовчуванням одночасно обробляються два файли; змінити це можна через --concurrency. Якщо натиснути Ctrl-C посеред пакета, уже записані файли лишаються, а код виходу дорівнює 130.
Зашифрований файл відкривається через --input-password PASSWORD. Коли в пакеті змішано заблоковані й незаблоковані файли, скористайтеся --password-for FILE=PASSWORD, який можна повторювати.
Для проміжних файлів викликайте інструменти окремо, в інших випадках беріть конвеєр
Щоб об’єднати, потім додати водяний знак, потім стиснути, коли проміжні результати на диску не потрібні, згорніть усе в один запит через 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 кроків; дев’ятий крок отримує HTTP 400: at most 8 pipeline steps allowed. Інструмент, якому потрібен другий файл (наприклад, overlay-pdfs, що накладає інший PDF), не може бути кроком конвеєра. 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
У цьому режимі стандартний вивід несе лише байти файлу; повідомлення й помилки йдуть у стандартний потік помилок, тож перенаправлення безпечне. Ми перевірили це на PDF близько 1 КБ: стандартний вивід містив PDF на 1040 байтів, такого самого розміру, як файл, записаний через -o, а стандартний потік помилок був порожній. Коли ви читаєте зі стандартного вводу й не вказуєте -o, результат отримує ім’я stdin.pdf, записується в поточну теку з приміткою в стандартному потоці помилок.
Скрипти мають зіставляти причину, а не повідомлення
| Код виходу | Значення | Приклади |
|---|---|---|
| 0 | Успіх, або умовний інструмент-фільтр без збігу | Об’єднання вдалося; умова filter-page-count була хибною |
| 1 | Запит надіслано, але він завершився помилкою; у пакеті принаймні один файл завершився помилкою | Неправильний пароль, не PDF, тайм-аут, нічого повертати |
| 2 | Помилка використання, нічого не завантажено | Помилка в назві інструмента, крок конвеєра, якому потрібен другий файл, тип результату суперечить розширенню вихідного файлу |
| 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, який створює Розрізати PDF, у 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): у вашому PDF, найімовірніше, немає колонок. Для інструмента-фільтра «немає збігу» — це відповідь, яку він і мав дати.
Щоб прочитати це в скрипті, використайте --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 для пакета, ключ, що надсилається для кожного файлу, має вигляд <key>:<index>; механізм описано в матеріалі Idempotency-Key: безпечні повторні спроби для завдань 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 }
]
}
З єдиним вхідним файлом використовується шлях для одного файлу: --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 порожній |
Знак питання наприкінці .files[]? у jq не дає звіту для одного файлу (де 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.