How-to2026-09-2910 min czytania

pdfx w terminalu i w CI: od pierwszego polecenia do niezawodnego skryptu

Używaj pdfx w wierszu poleceń, żeby scalać, kompresować i dodawać znaki wodne, przetwarzać wiele plików naraz i łączyć narzędzia w jedno żądanie przez pipeline. W skryptach i CI dowiesz się, na czym można polegać: kodach wyjścia, raporcie --json, standardowym wejściu i wyjściu oraz tym, dlaczego warunkowe narzędzie filtrujące bez dopasowania też kończy się kodem 0.

PDF123 · Updated 2026-09-29

Zanim wstawisz pdfx do skryptu albo do ciągłej integracji (CI), zapamiętaj dwie rzeczy. Kod wyjścia 0 nie zawsze oznacza, że plik został zapisany: warunkowe narzędzie filtrujące bez dopasowania też kończy się kodem 0. A --json ma inny kształt dla jednego pliku wejściowego niż dla kilku, więc skrypt, który parsuje tylko tablicę files, nic nie znajdzie, gdy pasował tylko jeden plik. pdfx jest klientem HTTP: pliki są przesyłane na serwer do przetworzenia, lokalnego silnika nie ma i nie ma trybu offline. Pełna dokumentacja poleceń jest w przewodniku po CLI na stronie dla programistów.

Diagram: jedno wywołanie pdfx daje skryptowi trzy rzeczy: raport JSON na standardowym wyjściu, komunikaty o błędach na standardowym wyjściu błędów i kod wyjścia; skrypt czyta najpierw kod wyjścia

Zainstaluj, potem scal dwa pliki

Potrzebujesz Node 20.3 lub nowszego albo Bun:

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

Jeśli wolisz nie instalować globalnie, postaw npx @pdf123/cli przed poleceniem. Mając dwa pliki PDF w bieżącym katalogu:

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

Po sukcesie standardowe wyjście wypisuje merged.pdf. To było Połącz PDF. Zanim zmienisz narzędzie, zapytaj o pola, zamiast zgadywać nazwy parametrów:

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

(Fragment; pola obejmują też --rotation, --customColor i inne.) Pole to po prostu opcja wiersza poleceń:

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

pdfx list wypisuje wszystkie 95 narzędzi, --category security tylko jedną kategorię, a --query watermark szuka po słowie.

Wiele plików trafia do katalogu, a istniejące pliki nie są nadpisywane

Podaj narzędziu jednoplikowemu kilka plików wejściowych, a każdy plik trafi do katalogu wskazanego przez -o zaraz po zakończeniu, a nie dopiero na końcu. Jeśli jeden plik zawiedzie, pozostałe są przetwarzane dalej, a kod wyjścia na końcu partii wynosi 1.

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

Standardowe wyjście z jednego przebiegu wyglądało tak, a kolejność może być za każdym razem inna:

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

Istniejący katalog działa bez zmian; jeśli katalog jeszcze nie istnieje, -o wymaga ukośnika na końcu, inaczej small zostanie potraktowane jako nazwa pliku. Bez -o wyniki trafiają do bieżącego katalogu pod nazwą nadaną przez serwer; istniejący plik nie jest nadpisywany, a nowy dostaje nazwę a-1.pdf, a-2.pdf. Konkretna nazwa pliku (-o same.pdf) zastępuje istniejącą zawartość, tak jak kazałeś. Domyślnie przetwarzane są dwa pliki naraz; zmienisz to przez --concurrency. Jeśli w środku partii naciśniesz Ctrl-C, już zapisane pliki zostają, a kod wyjścia wynosi 130.

Zaszyfrowany plik otworzysz przez --input-password PASSWORD. Gdy partia miesza pliki zablokowane i niezablokowane, użyj --password-for FILE=PASSWORD, które można powtarzać.

Wywołuj narzędzia osobno, gdy chcesz pliki pośrednie, w przeciwnym razie użyj pipeline

Żeby scalić, potem dodać znak wodny, potem skompresować, gdy nie potrzebujesz wyników pośrednich na dysku, złóż to w jedno żądanie przez pipeline; pliki pośrednie nie są ponownie pobierane i przesyłane. Jeśli chcesz mieć plik z każdego kroku, wywołuj narzędzia osobno. Pipeline daje jeden plik.

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

# Gdy krok wymaga parametrów, opisz kroki w JSON-ie
pdfx pipeline a.pdf b.pdf \
  --steps '[{"tool":"merge"},{"tool":"watermark","params":{"watermarkText":"DRAFT"}},{"tool":"compress"}]' \
  -o out.pdf

Pipeline ma co najwyżej 8 kroków; dziewiąty krok kończy się odpowiedzią HTTP 400: at most 8 pipeline steps allowed. Narzędzie, które potrzebuje drugiego pliku (na przykład overlay-pdfs, które nakłada inny PDF), nie może być krokiem pipeline. pdfx odrzuca to przed przesłaniem, z kodem wyjścia 2 i komunikatem Tool "overlay-pdfs" needs a second file and cannot run as a pipeline step.

Standardowego wejścia używaj tylko wtedy, gdy chcesz podłączyć polecenie do potoku. Argument pliku - czyta ze standardowego wejścia, a -o - zapisuje wynik na standardowe wyjście:

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

W tym trybie standardowe wyjście niesie wyłącznie bajty pliku; komunikaty i błędy idą na standardowe wyjście błędów, więc przekierowanie jest bezpieczne. Sprawdziliśmy to na pliku PDF o rozmiarze około 1 KB: standardowe wyjście zawierało plik PDF o rozmiarze 1040 bajtów, takim samym jak plik zapisany przez -o, a standardowe wyjście błędów było puste. Gdy czytasz ze standardowego wejścia i nie podajesz -o, wynik dostaje nazwę stdin.pdf, jest zapisywany w bieżącym katalogu, a na standardowym wyjściu błędów pojawia się notatka.

Skrypty powinny dopasowywać powód, nie komunikat

Kod wyjścia Znaczenie Przykłady
0 Sukces albo warunkowe narzędzie filtrujące bez dopasowania Scalanie się udało; warunek filter-page-count był fałszywy
1 Żądanie zostało wysłane, ale się nie powiodło; w partii zawiódł co najmniej jeden plik Złe hasło, to nie PDF, przekroczenie czasu, nie ma czego zwrócić
2 Błąd użycia, nic nie zostało przesłane Błędnie wpisana nazwa narzędzia, krok pipeline wymagający drugiego pliku, typ wyniku sprzeczny z rozszerzeniem pliku wyjściowego
130 Przerwałeś działanie Ctrl-C w trakcie partii

Przy niepowodzeniu standardowe wyjście błędów zawiera obok jednej linii komunikatu jeszcze trzy: reason:, code: i hint:. Zaszyfrowany plik ze złym hasłem dał lokalnie coś takiego:

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

Sposób podania hasła zmienia pierwszą linię komunikatu: unlock --password daje linię powyżej. Przy --input-password w innym narzędziu serwer traktuje odblokowanie jako krok wewnętrzny, a pierwsza linia brzmi HTTP 400: Pipeline step 0 (security/remove-password) failed: The password is incorrect., natomiast linia reason: w obu przypadkach to wrong_password. Bez hasła w ogóle komunikat to This PDF is password-protected. Enter its password., a reason to password_required. Skrypt powinien dopasowywać linię reason:. code jest grubszy, a częstą wartością jest bad_request.

Błędnie wpisana nazwa narzędzia kończy się kodem 2, a komunikat podpowiada podobne nazwy:

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

Typ wyniku sprzeczny z nazwą pliku też kończy się kodem 2, i to zanim cokolwiek zostanie zapisane. Zapis archiwum ZIP, które tworzy Podziel PDF, do 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

Przekroczenie czasu też kończy się kodem 1, a code ma wtedy wartość timeout. Jednostką --timeout są milisekundy, a wartość domyślna to 300000, czyli 5 minut, więc --timeout 60 oznacza 60 milisekund, a nie 60 sekund.

Gdy warunek jest fałszywy, kod wyjścia nadal wynosi 0

Jedna klasa narzędzi odpowiada na pytanie tak lub nie: czy liczba stron jest większa niż N, czy plik zawiera jakiś tekst, czy plik jest większy niż pewien rozmiar. Gdy odpowiedź brzmi tak, zwracają plik wejściowy bez zmian; gdy nie, nie zwracają nic, a pdfx wypisuje linię no match i kończy się kodem 0. Te narzędzia filtrujące istnieją tylko w SDK, wierszu poleceń i MCP; strona internetowa nie ma dla nich podstrony.

Różni się to od innego rodzaju pustego wyniku. Gdy PDF na CSV nie znajdzie w PDF żadnej tabeli, serwer zwraca 204, a pdfx kończy się kodem 1 i zgłasza no_content: narzędzie miało wytworzyć zawartość i tego nie zrobiło, a powód opisuje Pusty eksport tabeli (204): twój PDF prawdopodobnie nie ma kolumn. Dla narzędzia filtrującego "brak dopasowania" jest odpowiedzią, którą miało dać.

Żeby odczytać to w skrypcie, użyj --json. Dopasowanie wypisuje zapisany plik; brak dopasowania wypisuje { "matched": false }:

# Dopasowanie: plik jest zapisany, a jego informacje wypisane
$ pdfx filter-page-count three.pdf --pageCount 2 --comparator Greater --json -o big/
{ "path": "big/three.pdf", "contentType": "application/pdf", "bytes": 2594 }

# Brak dopasowania: żaden plik nie jest zapisany
$ pdfx filter-page-count three.pdf --pageCount 5 --comparator Greater --json
{ "matched": false }

Żeby zachować tylko pliki z więcej niż 2 stronami, możesz napisać to tak. Uruchomiliśmy to lokalnie na a.pdf (1 strona) i three.pdf (3 strony) i zachowany został tylko ten drugi:

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: pominięto"
  fi
done

jq -e '.matched == false' kończy się kodem 0, gdy dopasowania nie ma, i 1, gdy jest. Przy dopasowaniu pdfx zapisał już plik w big/; if rozstrzyga tylko, czy wypisać "pominięto", a nie czy zapisywać.

Przy pojedynczym pliku wejściowym --json nie ma tablicy files

Przy kilku plikach wejściowych standardowe wyjście pod --json jest pełnym raportem. input to ścieżka bezwzględna. processed liczy pliki, których żądanie się powiodło, razem z tymi bez dopasowania. unmatched mówi, ile z nich nie miało dopasowania, a ich wpisy to { "ok": true, "matched": false } bez path. Gdy żaden plik w partii nie ma dopasowania, kod wyjścia nadal wynosi 0. Gdy przekażesz partii --idempotency-key, klucz wysyłany dla każdego pliku to <key>:<index>; mechanizm opisuje Idempotency-Key: bezpieczne ponawianie zadań 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 }
  ]
}

Przy jednym pliku wejściowym używana jest ścieżka jednoplikowa: --json wypisuje { "path": ..., "contentType": ..., "bytes": ... } i nie ma tablicy files. Przy niepowodzeniu standardowe wyjście jest puste, a wszystko trafia na standardowe wyjście błędów. Gdy rozwijasz pliki globem, skrypt musi obsłużyć oba formaty, niezależnie od tego, czy pasował jeden plik, czy kilka.

Zbierz powyższe reguły w jednym kroku CI

Ten skrypt tylko kompresuje i nie używa narzędzia filtrującego. Niepowodzenie kompresji kończy się kodem 1, a krok kończy się z nim niepowodzeniem. Narzędzie filtrujące bez dopasowania kończy się kodem 0, więc CI nie pada tylko dlatego, że nie było dopasowania; to, czy brak dopasowania jest problemem, rozstrzygasz sam, czytając --json, tak jak w sekcji o filtrowaniu wyżej.

Jeśli jakikolwiek plik zostanie odrzucony, ten krok kończy się niepowodzeniem i wypisuje w logu nazwy plików oraz powody. Logika jest ta sama co wcześniej: najpierw odczytaj kod wyjścia, przy niepowodzeniu wypisz standardowe wyjście błędów, a potem wyciągnij przez jq pole reason z raportu partii. Bez argumentu katalogu kończy się od razu, żeby "$1"/*.pdf nigdy nie rozwinęło się do /*.pdf.

#!/usr/bin/env bash
# Użycie: ./ci-step.sh docs
docs="${1:?użycie: ./ci-step.sh <katalog>}"
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"

Uruchomiliśmy to lokalnie na czterech rodzajach danych wejściowych:

Pliki w katalogu Kod wyjścia Log
Dwa dobre pliki PDF 0 brak
Dwa dobre pliki PDF plus jeden uszkodzony 1 pdfx: broken.pdf: HTTP 400: ..., potem linia broken.pdf invalid_pdf
Jeden dobry plik PDF 0 brak; report.json ma format jednoplikowy
Jeden uszkodzony plik PDF 1 reason: invalid_pdf, code: invalid_document i linia hint; report.json jest pusty

Znak zapytania na końcu .files[]? w jq chroni przed błędem raport jednoplikowy (który nie ma files). Pojedynczy uszkodzony plik nie ma raportu partii, a powód pojawia się tylko w errors.log, więc zachowaj oba strumienie wyjścia.

Trzy rzeczy do sprawdzenia, zanim wdrożysz to w CI. pdfx potrzebuje sieci: pliki są przesyłane na PDFX_API_BASE, które domyślnie wskazuje https://pdf123.xyz. Dla dokumentów, które muszą zostać w twojej sieci, uruchom własną usługę i wskaż ją w tej zmiennej; zobacz Co naprawdę daje self-hosting (i ile kosztuje). Gdy potrzebujesz tożsamości, umieść klucz w PDFX_API_KEY, a nie w argumentach wiersza poleceń, gdzie zostałby na liście procesów i w logach. Aktualnie opublikowana wersja to 0.1.0; w CI przypnij ją przez npx @pdf123/[email protected] ..., żeby format wyjścia i kody wyjścia nie zmieniały się wraz z nowymi wydaniami, a aktualizacja była zmianą, którą wprowadzasz świadomie.

Strona pakietu w npm to @pdf123/cli.

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