How-to2026-09-2911 min de leitura

Usando o pdfx no terminal e no CI: do primeiro comando a um script confiável

Use o pdfx na linha de comando para juntar, comprimir e aplicar marca d'água, processar vários arquivos de uma vez e encadear ferramentas numa única requisição com um pipeline. Para scripts e CI, saiba em que confiar: códigos de saída, o relatório --json, entrada e saída padrão e por que uma ferramenta de filtro condicional sem correspondência ainda sai com 0.

PDF123 · Updated 2026-09-29

Antes de colocar o pdfx num script ou na integração contínua (CI), lembre de duas coisas. O código de saída 0 nem sempre significa que um arquivo foi gravado: uma ferramenta de filtro condicional sem correspondência também sai com 0. E o --json tem formato diferente para uma entrada e para várias, então um script que só interpreta o array files não encontra nada quando apenas um arquivo correspondeu. O pdfx é um cliente HTTP: os arquivos são enviados ao servidor para processamento, não há motor local e não há modo offline. A referência completa dos comandos está no guia da CLI na página para desenvolvedores.

Diagrama: uma chamada do pdfx entrega ao script três coisas, um relatório JSON na saída padrão, mensagens de falha no erro padrão e um código de saída; o script lê primeiro o código de saída

Instale e depois junte dois arquivos

Você precisa do Node 20.3 ou mais novo, ou do Bun:

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

Se preferir não instalar globalmente, coloque npx @pdf123/cli na frente do comando. Com dois PDFs no diretório atual:

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

Em caso de sucesso, a saída padrão imprime merged.pdf. Isso foi Juntar PDF. Antes de trocar de ferramenta, peça os campos em vez de adivinhar nomes de parâmetros:

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

(Um trecho; os campos incluem também --rotation, --customColor e outros.) Um campo é apenas uma opção de linha de comando:

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

pdfx list lista as 95 ferramentas, --category security lista só uma categoria e --query watermark busca por palavra.

Vários arquivos vão para um diretório, e arquivos existentes não são sobrescritos

Dê a uma ferramenta de arquivo único várias entradas e cada arquivo é gravado no diretório indicado por -o assim que termina, sem esperar o fim. Se um arquivo falha, os outros seguem, e o código de saída no fim do lote é 1.

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

A saída padrão de uma execução ficou assim, e a ordem pode mudar a cada vez:

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

Um diretório existente funciona como está; se o diretório ainda não existe, -o precisa de uma / no final, senão small é tratado como nome de arquivo. Sem -o, os resultados vão para o diretório atual com o nome que o servidor dá; um arquivo existente não é sobrescrito e o novo vira a-1.pdf, a-2.pdf. Um nome de arquivo específico (-o same.pdf) substitui o conteúdo existente, como você mandou. Por padrão, dois arquivos são processados por vez; mude isso com --concurrency. Se você apertar Ctrl-C no meio de um lote, os arquivos já gravados permanecem e o código de saída é 130.

Abra um arquivo criptografado com --input-password SENHA. Quando um lote mistura arquivos protegidos e não protegidos, use --password-for ARQUIVO=SENHA, que pode ser repetido.

Chame as ferramentas separadamente para ter arquivos intermediários; senão, use um pipeline

Para juntar, depois aplicar marca d'água e depois comprimir, quando você não precisa dos resultados intermediários em disco, reúna tudo numa requisição com pipeline; os arquivos intermediários não são baixados e enviados de novo. Se você quer o arquivo de cada etapa, chame as ferramentas separadamente. Um pipeline produz um único arquivo.

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

# Quando um passo precisa de parâmetros, descreva os passos em JSON
pdfx pipeline a.pdf b.pdf \
  --steps '[{"tool":"merge"},{"tool":"watermark","params":{"watermarkText":"DRAFT"}},{"tool":"compress"}]' \
  -o out.pdf

Um pipeline tem no máximo 8 passos; um 9º passo recebe HTTP 400: at most 8 pipeline steps allowed. Uma ferramenta que precisa de um segundo arquivo (por exemplo overlay-pdfs, que sobrepõe outro PDF) não pode ser um passo de pipeline. O pdfx a recusa antes de enviar, com código de saída 2 e a mensagem Tool "overlay-pdfs" needs a second file and cannot run as a pipeline step.

Use a entrada padrão só quando quiser ligar o comando a um pipe. Um argumento de arquivo - lê da entrada padrão, e -o - escreve o resultado na saída padrão:

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

Nesse modo a saída padrão leva apenas os bytes do arquivo; mensagens e erros vão para o erro padrão, então redirecionar é seguro. Conferimos com um PDF de cerca de 1 KB: a saída padrão trouxe um PDF de 1040 bytes, do mesmo tamanho do arquivo gravado com -o, e o erro padrão ficou vazio. Quando você lê da entrada padrão e não informa -o, o resultado recebe o nome stdin.pdf, gravado no diretório atual com um aviso no erro padrão.

Os scripts devem casar com o motivo, não com a mensagem

Código de saída Significado Exemplos
0 Sucesso, ou uma ferramenta de filtro condicional sem correspondência Um merge deu certo; a condição de filter-page-count era falsa
1 A requisição foi enviada, mas falhou; num lote, pelo menos um arquivo falhou Senha errada, não é um PDF, timeout, nada a devolver
2 Erro de uso, nada foi enviado Nome de ferramenta errado, um passo de pipeline que precisa de um segundo arquivo, um tipo de resultado que contradiz a extensão do arquivo de saída
130 Você interrompeu Ctrl-C durante um lote

Em caso de falha, além de uma linha de mensagem, o erro padrão traz três linhas: reason:, code: e hint:. Um arquivo criptografado com a senha errada produziu isto localmente:

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

A forma de passar a senha muda a primeira linha da mensagem: unlock --password dá a linha acima. Com --input-password em outra ferramenta, o servidor trata o desbloqueio como um passo interno e a primeira linha é HTTP 400: Pipeline step 0 (security/remove-password) failed: The password is incorrect., enquanto a linha reason: é wrong_password nos dois casos. Sem nenhuma senha, a mensagem é This PDF is password-protected. Enter its password. e reason é password_required. Um script deve casar com a linha reason:. code é mais grosseiro, e um valor comum é bad_request.

Um nome de ferramenta errado sai com 2, e a mensagem sugere nomes parecidos:

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

Um tipo de resultado que contradiz o nome do arquivo também sai com 2, e acontece antes de qualquer coisa ser gravada. Gravando em x.pdf o ZIP que Dividir PDF produz:

pdfx: The result is application/zip but "x.pdf" has a .pdf extension; name it *.zip or write into a directory
code: output_mismatch

Um timeout também sai com 1, com code igual a timeout. A unidade de --timeout é milissegundos e o padrão é 300000, ou seja, 5 minutos; portanto --timeout 60 significa 60 milissegundos, não 60 segundos.

Quando a condição é falsa, o código de saída ainda é 0

Uma classe de ferramentas responde a uma pergunta de sim ou não: a contagem de páginas é maior que N, o arquivo contém certo texto, o arquivo é maior que certo tamanho. Quando a resposta é sim, devolvem o arquivo de entrada sem alteração; quando é não, não devolvem nada, e o pdfx imprime uma linha no match e sai com 0. Essas ferramentas de filtro existem apenas no SDK, na linha de comando e no MCP; o site não tem página para elas.

Isso difere de outro tipo de resultado vazio. Quando PDF para CSV não encontra tabela no PDF, o servidor devolve 204 e o pdfx sai com 1 e reporta no_content: a ferramenta devia produzir conteúdo e não produziu, e o motivo está em Exportação de tabela vazia (204): seu PDF provavelmente não tem colunas. Para uma ferramenta de filtro, "sem correspondência" é a resposta que ela devia dar.

Para ler isso num script, use --json. Uma correspondência imprime o arquivo gravado; sem correspondência imprime { "matched": false }:

# Correspondência: o arquivo é gravado e suas informações são impressas
$ pdfx filter-page-count three.pdf --pageCount 2 --comparator Greater --json -o big/
{ "path": "big/three.pdf", "contentType": "application/pdf", "bytes": 2594 }

# Sem correspondência: nenhum arquivo é gravado
$ pdfx filter-page-count three.pdf --pageCount 5 --comparator Greater --json
{ "matched": false }

Para manter apenas os arquivos com mais de 2 páginas, você pode escrever assim. Rodamos localmente em a.pdf (1 página) e three.pdf (3 páginas), e só o segundo foi mantido:

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: ignorado"
  fi
done

jq -e '.matched == false' sai com 0 quando não há correspondência e com 1 quando há. Numa correspondência, o arquivo já foi gravado em big/ pelo pdfx; o if só decide se imprime "ignorado", não se grava.

Com uma única entrada, --json não tem array files

Com várias entradas, a saída padrão sob --json é um relatório completo. input é um caminho absoluto. processed conta os arquivos cuja requisição teve sucesso, inclusive os sem correspondência. unmatched é quantos deles não tiveram correspondência, e suas entradas são { "ok": true, "matched": false } sem path. Quando todos os arquivos de um lote não têm correspondência, o código de saída ainda é 0. Quando você passa --idempotency-key a um lote, a chave enviada para cada arquivo é <key>:<index>; o mecanismo está em Idempotency-Key: novas tentativas seguras em tarefas de 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 }
  ]
}

Com uma única entrada, vale o caminho de arquivo único: --json imprime { "path": ..., "contentType": ..., "bytes": ... } e não há array files. Em caso de falha, a saída padrão fica vazia e tudo vai para o erro padrão. Quando você expande arquivos com um glob, o script precisa tratar os dois formatos, tenha correspondido um arquivo ou vários.

Reúna as regras acima num único passo de CI

Este script só comprime e não usa nenhuma ferramenta de filtro. Uma falha de compressão sai com 1, e o passo falha junto com ela. Uma ferramenta de filtro sem correspondência sai com 0, para que o CI não falhe só porque não houve correspondência; se a falta de correspondência é um problema, quem decide é você, lendo o --json, como na seção sobre filtros acima.

Se algum arquivo for recusado, este passo falha e lista no log os nomes e os motivos. A lógica é a mesma de antes: ler primeiro o código de saída, imprimir o erro padrão em caso de falha e depois usar jq para extrair o reason do relatório do lote. Sem o argumento de diretório, ele sai de imediato, para que "$1"/*.pdf nunca seja expandido para /*.pdf.

#!/usr/bin/env bash
# Uso: ./ci-step.sh docs
docs="${1:?uso: ./ci-step.sh <diretório>}"
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"

Rodamos localmente com quatro tipos de entrada:

Arquivos no diretório Código de saída Log
Dois PDFs bons 0 nenhum
Dois PDFs bons mais um danificado 1 pdfx: broken.pdf: HTTP 400: ..., depois uma linha broken.pdf invalid_pdf
Um único PDF bom 0 nenhum; report.json está no formato de arquivo único
Um único PDF danificado 1 reason: invalid_pdf, code: invalid_document e uma linha hint; report.json está vazio

O ponto de interrogação no fim de .files[]? no jq evita que o relatório de arquivo único (que não tem files) cause erro. Um único arquivo danificado não tem relatório de lote e o motivo aparece só em errors.log, então guarde as duas saídas.

Mais três coisas a conferir antes de colocar isso no CI. O pdfx precisa de rede: os arquivos são enviados para PDFX_API_BASE, que por padrão é https://pdf123.xyz. Para documentos que precisam ficar dentro da sua rede, rode um serviço por conta própria e aponte essa variável para ele; veja O que o self-host realmente traz (e quanto custa). Quando você precisa de uma identidade, ponha a chave em PDFX_API_KEY e não nos argumentos da linha de comando, onde ela ficaria na lista de processos e nos logs. A versão publicada no momento é a 0.1.0; no CI use npx @pdf123/[email protected] ... para fixá-la, de modo que o formato de saída e os códigos de saída não mudem com novos lançamentos e uma atualização seja uma mudança que você faz de propósito.

A página do pacote no npm é @pdf123/cli.

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