How-to2026-09-29약 8분

터미널과 CI에서 pdfx 쓰기: 첫 명령부터 믿을 수 있는 스크립트까지

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로 엽니다. 잠긴 파일과 잠기지 않은 파일이 섞인 배치에서는 반복해서 쓸 수 있는 --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단계이고, 9번째 단계는 HTTP 400: at most 8 pipeline steps allowed를 받습니다. 두 번째 파일이 필요한 도구(예를 들어 다른 PDF를 겹쳐 놓는 overlay-pdfs)는 파이프라인 단계가 될 수 없습니다. 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 사용법 오류, 아무것도 업로드되지 않음 도구 이름 철자 오류, 두 번째 파일이 필요한 파이프라인 단계, 출력 파일 확장자와 모순되는 결과 유형
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로 끝나며, 아무것도 쓰이기 전에 일어납니다. PDF 분할이 만드는 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): 이 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는 그중 일치하지 않은 수이고, 그 항목은 path가 없는 { "ok": true, "matched": false }입니다. 배치의 모든 파일이 일치하지 않아도 종료 코드는 여전히 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 <directory>}"
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입니다.

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