How-to2026-09-2910 мин чтения

pdfx в терминале и в CI: от первой команды до надёжного скрипта

Командой pdfx можно объединять, сжимать и ставить водяные знаки, обрабатывать сразу много файлов и собирать инструменты в один запрос через конвейер. Для скриптов и CI разберём, на что можно полагаться: коды выхода, отчёт --json, стандартные ввод и вывод и почему инструмент условной фильтрации без совпадения всё равно завершается с кодом 0.

PDF123 · Updated 2026-09-29

Прежде чем ставить pdfx в скрипт или в непрерывную интеграцию (CI), запомните две вещи. Код выхода 0 не всегда означает, что файл записан: инструмент условной фильтрации без совпадения тоже завершается с 0. А у --json разная форма для одного входного файла и для нескольких, поэтому скрипт, который разбирает только массив files, ничего не найдёт, если совпал всего один файл. pdfx — 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. Это был Объединить 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-for ФАЙЛ=ПАРОЛЬ; опцию можно повторять.

Нужны промежуточные файлы — вызывайте инструменты по отдельности, иначе берите конвейер

Чтобы объединить, затем поставить водяной знак и затем сжать, когда промежуточные результаты на диске не нужны, соберите всё в один запрос через 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. Инструмент, которому нужен второй файл (например, 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, причём до того, как что-либо записано. Запись в x.pdf ZIP-архива, который выдаёт Разделить 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 нет. При сбое стандартный вывод пуст, а всё находится в стандартном потоке ошибок. Когда вы раскрываете файлы по шаблону, скрипт должен обрабатывать оба формата, совпал один файл или несколько.

Соберите все эти правила в один шаг 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.

Open tool
Process in the browser — no watermark, files removed after the job.
Open tool
Read next
Номера страниц в PDF: положение, начальный номер и ловушка повёрнутых страниц
Как пронумеровать страницы готового PDF онлайн: выберите угол, задайте начальный номер, например 101, и избегайте номеров, лежащих боком на страницах, которые хранятся как повёрнутые.
Конвертация PDF в JPG или PNG: что меняет DPI и как экспортировать одну страницу
«PDF в изображения» отрисовывает каждую страницу в ZIP, по умолчанию в PNG, если не выбрать JPEG. В форме браузера есть только формат и DPI; поля API для выбора страниц и WebP игнорируются. Измеренные размеры файлов при 150 и 300 DPI и способ экспортировать одну страницу.
Фото с телефона в PDF: почему файл получается огромным, а страница повёрнута набок
«Изображения в PDF» помещает каждое фото на отдельную страницу в полном размере в пикселях: три JPEG по 3,2 МБ с телефона превратились в PDF на 41,5 МБ, а портретное фото попало на альбомную страницу. Уменьшите фото перед конвертацией, а повёрнутую страницу поверните потом.