How-to2026-09-2911 min di lettura

Usare pdfx nel terminale e in CI: dal primo comando a uno script affidabile

Usa pdfx da riga di comando per unire, comprimere e aggiungere filigrane, elaborare molti file insieme e concatenare strumenti in una sola richiesta con una pipeline. Per script e CI, scopri su cosa fare affidamento: codici di uscita, il report --json, standard input e output, e perché uno strumento di filtro condizionale senza corrispondenza termina comunque con 0.

PDF123 · Updated 2026-09-29

Prima di mettere pdfx in uno script o nell'integrazione continua (CI), ricorda due cose. Il codice di uscita 0 non significa sempre che sia stato scritto un file: anche uno strumento di filtro condizionale senza corrispondenza termina con 0. E --json ha una forma diversa per un solo input rispetto a più input, quindi uno script che analizza solo l'array files non trova nulla quando ha corrisposto un solo file. pdfx è un client HTTP: i file vengono caricati sul server per l'elaborazione, non esiste un motore locale e non c'è modalità offline. Il riferimento completo dei comandi è nella guida alla CLI nella pagina per sviluppatori.

Diagramma: una chiamata a pdfx consegna allo script tre cose, un report JSON sullo standard output, i messaggi di errore sullo standard error e un codice di uscita; lo script legge per primo il codice di uscita

Installalo, poi unisci due file

Servono Node 20.3 o successivo, oppure Bun:

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

Se preferisci non installarlo globalmente, anteponi npx @pdf123/cli al comando. Con due PDF nella cartella corrente:

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

In caso di successo, lo standard output stampa merged.pdf. Quello era Unisci PDF. Prima di passare a un altro strumento, chiedi i campi invece di indovinare i nomi dei parametri:

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

(Un estratto; tra i campi ci sono anche --rotation, --customColor e altri.) Un campo è semplicemente un'opzione da riga di comando:

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

pdfx list elenca tutti i 95 strumenti, --category security ne elenca una sola categoria e --query watermark cerca per parola.

Più file finiscono in una directory e i file esistenti non vengono sovrascritti

Dai a uno strumento a file singolo più input e ogni file viene scritto nella directory indicata da -o non appena termina, senza aspettare la fine. Se un file fallisce gli altri proseguono, e il codice di uscita alla fine del batch è 1.

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

Lo standard output di un'esecuzione era questo, e l'ordine può cambiare a ogni volta:

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

Una directory esistente funziona così com'è; se non esiste ancora, -o richiede una / finale, altrimenti small viene trattato come nome di file. Senza -o, i risultati finiscono nella directory corrente con il nome che dà loro il server; un file esistente non viene sovrascritto e il nuovo diventa a-1.pdf, a-2.pdf. Un nome di file preciso (-o same.pdf) sostituisce il contenuto esistente, come hai chiesto. Per impostazione predefinita vengono elaborati due file alla volta; cambialo con --concurrency. Se premi Ctrl-C a metà di un batch, i file già scritti restano e il codice di uscita è 130.

Apri un file cifrato con --input-password PASSWORD. Quando un batch mischia file bloccati e non bloccati, usa --password-for FILE=PASSWORD, che puoi ripetere.

Chiama gli strumenti separatamente per i file intermedi, altrimenti usa una pipeline

Per unire, poi aggiungere la filigrana, poi comprimere, quando non ti servono i risultati intermedi su disco, raccogli tutto in una sola richiesta con pipeline; i file intermedi non vengono scaricati e ricaricati. Se vuoi il file di ogni passaggio, chiama gli strumenti separatamente. Una pipeline produce un unico file.

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

# Quando un passaggio richiede parametri, descrivi i passaggi in JSON
pdfx pipeline a.pdf b.pdf \
  --steps '[{"tool":"merge"},{"tool":"watermark","params":{"watermarkText":"DRAFT"}},{"tool":"compress"}]' \
  -o out.pdf

Una pipeline ha al massimo 8 passaggi; un 9º passaggio riceve HTTP 400: at most 8 pipeline steps allowed. Uno strumento che richiede un secondo file (per esempio overlay-pdfs, che sovrappone un altro PDF) non può essere un passaggio di pipeline. pdfx lo rifiuta prima del caricamento, con codice di uscita 2 e il messaggio Tool "overlay-pdfs" needs a second file and cannot run as a pipeline step.

Usa lo standard input solo quando vuoi collegare il comando a una pipe. Un argomento file - legge dallo standard input, e -o - scrive il risultato sullo standard output:

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

In questa modalità lo standard output porta soltanto i byte del file; messaggi ed errori vanno sullo standard error, quindi il reindirizzamento è sicuro. L'abbiamo verificato con un PDF di circa 1 KB: lo standard output conteneva un PDF di 1040 byte, della stessa dimensione del file scritto con -o, e lo standard error era vuoto. Quando leggi dallo standard input e non dai -o, il risultato si chiama stdin.pdf, scritto nella directory corrente con una nota sullo standard error.

Gli script devono confrontare il reason, non il messaggio

Codice di uscita Significato Esempi
0 Successo, oppure uno strumento di filtro condizionale senza corrispondenza Un'unione riuscita; la condizione di filter-page-count era falsa
1 La richiesta è stata inviata ma è fallita; in un batch, almeno un file è fallito Password errata, non è un PDF, timeout, niente da restituire
2 Errore d'uso, non è stato caricato nulla Nome di strumento sbagliato, un passaggio di pipeline che richiede un secondo file, un tipo di risultato che contraddice l'estensione del file di output
130 L'hai interrotto tu Ctrl-C durante un batch

In caso di errore, oltre a una riga di messaggio, lo standard error porta tre righe: reason:, code: e hint:. Un file cifrato con password errata ha prodotto questo in locale:

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

Il modo in cui passi la password cambia la prima riga del messaggio: unlock --password dà la riga qui sopra. Con --input-password su un altro strumento, il server tratta lo sblocco come un passaggio interno e la prima riga è HTTP 400: Pipeline step 0 (security/remove-password) failed: The password is incorrect., mentre la riga reason: è wrong_password in entrambi i casi. Senza alcuna password il messaggio è This PDF is password-protected. Enter its password. e reason è password_required. Uno script dovrebbe confrontare la riga reason:. code è più grossolano e un valore comune è bad_request.

Un nome di strumento sbagliato termina con 2, e il messaggio suggerisce nomi simili:

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

Anche un tipo di risultato che contraddice il nome del file termina con 2, e succede prima che venga scritto qualsiasi cosa. Scrivere in x.pdf lo ZIP prodotto da Dividi 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

Anche un timeout termina con 1, con code impostato a timeout. L'unità di --timeout è il millisecondo e il valore predefinito è 300000, cioè 5 minuti, quindi --timeout 60 significa 60 millisecondi, non 60 secondi.

Quando la condizione è falsa, il codice di uscita è comunque 0

Una classe di strumenti risponde a una domanda sì o no: il numero di pagine è maggiore di N, il file contiene un certo testo, il file supera una certa dimensione. Quando la risposta è sì, restituiscono il file di input invariato; quando è no, non restituiscono nulla, e pdfx stampa una riga no match e termina con 0. Questi strumenti di filtro esistono solo nell'SDK, nella riga di comando e in MCP; il sito non ha una pagina per loro.

È diverso da un altro tipo di risultato vuoto. Quando PDF in CSV non trova alcuna tabella nel PDF, il server risponde 204 e pdfx termina con 1 e segnala no_content: lo strumento doveva produrre contenuto e non l'ha fatto, e il motivo è spiegato in Esportazione tabella vuota (204): probabilmente il PDF non ha colonne. Per uno strumento di filtro, «nessuna corrispondenza» è la risposta che doveva dare.

Per leggerlo in uno script usa --json. Una corrispondenza stampa il file scritto; nessuna corrispondenza stampa { "matched": false }:

# Corrispondenza: il file viene scritto e ne vengono stampate le informazioni
$ pdfx filter-page-count three.pdf --pageCount 2 --comparator Greater --json -o big/
{ "path": "big/three.pdf", "contentType": "application/pdf", "bytes": 2594 }

# Nessuna corrispondenza: non viene scritto alcun file
$ pdfx filter-page-count three.pdf --pageCount 5 --comparator Greater --json
{ "matched": false }

Per tenere solo i file con più di 2 pagine puoi scrivere così. L'abbiamo eseguito in locale su a.pdf (1 pagina) e three.pdf (3 pagine), e solo il secondo è stato conservato:

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

jq -e '.matched == false' termina con 0 quando non c'è corrispondenza e con 1 quando c'è. In caso di corrispondenza il file è già stato scritto in big/ da pdfx; l'if decide soltanto se stampare «saltato», non se scrivere.

Con un solo input, --json non ha l'array files

Con più input, lo standard output sotto --json è un report completo. input è un percorso assoluto. processed conta i file la cui richiesta è riuscita, compresi quelli senza corrispondenza. unmatched è quanti di questi non hanno avuto corrispondenza, e le loro voci sono { "ok": true, "matched": false } senza path. Quando ogni file di un batch non ha corrispondenza, il codice di uscita è comunque 0. Quando passi --idempotency-key a un batch, la chiave inviata per ogni file è <key>:<index>; il meccanismo è in Idempotency-Key: nuovi tentativi sicuri per le attività 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 }
  ]
}

Con un solo input si usa il percorso del file singolo: --json stampa { "path": ..., "contentType": ..., "bytes": ... } e non c'è alcun array files. In caso di errore lo standard output è vuoto e tutto va sullo standard error. Quando espandi i file con un glob, lo script deve gestire entrambi i formati, sia che corrisponda un file sia che ne corrispondano più.

Riunisci le regole precedenti in un unico passaggio di CI

Questo script si limita a comprimere e non usa alcuno strumento di filtro. Un errore di compressione termina con 1, e il passaggio fallisce con esso. Uno strumento di filtro senza corrispondenza termina con 0, quindi la CI non fallisce solo perché non c'è stata corrispondenza; se l'assenza di corrispondenza sia un problema lo decidi tu leggendo --json, come nella sezione sui filtri qui sopra.

Se un file viene rifiutato, questo passaggio fallisce ed elenca nel log i nomi dei file e i motivi. La logica è la stessa di prima: leggi prima il codice di uscita, stampa lo standard error in caso di errore, poi usa jq per estrarre il reason dal report del batch. Senza un argomento directory esce subito, così "$1"/*.pdf non viene mai espanso in /*.pdf.

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

L'abbiamo eseguito in locale con quattro tipi di input:

File nella directory Codice di uscita Log
Due PDF validi 0 nessuno
Due PDF validi più uno danneggiato 1 pdfx: broken.pdf: HTTP 400: ..., poi una riga broken.pdf invalid_pdf
Un solo PDF valido 0 nessuno; report.json è nel formato del file singolo
Un solo PDF danneggiato 1 reason: invalid_pdf, code: invalid_document e una riga hint; report.json è vuoto

Il punto interrogativo alla fine di .files[]? in jq evita che il report del file singolo (che non ha files) provochi un errore. Un singolo file danneggiato non ha un report di batch e il motivo compare solo in errors.log, quindi conserva entrambe le righe di output.

Altre tre cose da controllare prima di mettere questo in CI. pdfx ha bisogno della rete: i file vengono caricati su PDFX_API_BASE, che per impostazione predefinita è https://pdf123.xyz. Per i documenti che devono restare dentro la tua rete, esegui un servizio tuo e punta questa variabile a esso; vedi Cosa ti dà davvero il self-hosting (e quanto costa). Quando ti serve un'identità, metti la chiave in PDFX_API_KEY e non negli argomenti della riga di comando, dove resterebbe nell'elenco dei processi e nei log. La versione attualmente pubblicata è la 0.1.0; in CI usa npx @pdf123/[email protected] ... per fissarla, così il formato di output e i codici di uscita non cambiano con le nuove versioni e un aggiornamento diventa una modifica che fai di proposito.

La pagina del pacchetto su npm è @pdf123/cli.

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