How-to2026-09-2912 min de lecture

Utiliser pdfx dans le terminal et en CI : de la première commande à un script fiable

Utilisez pdfx en ligne de commande pour fusionner, compresser et filigraner, traiter de nombreux fichiers à la fois et enchaîner des outils dans une seule requête avec un pipeline. Pour les scripts et la CI, voyez ce sur quoi vous pouvez compter : les codes de sortie, le rapport --json, l’entrée et la sortie standard, et la raison pour laquelle un outil de filtre conditionnel sans correspondance se termine quand même par 0.

PDF123 · Updated 2026-09-29

Avant de mettre pdfx dans un script ou dans l’intégration continue (CI), retenez deux choses. Le code de sortie 0 ne signifie pas toujours qu’un fichier a été écrit : un outil de filtre conditionnel sans correspondance se termine lui aussi par 0. Et --json n’a pas la même forme pour une entrée que pour plusieurs, de sorte qu’un script qui n’analyse que le tableau files ne trouve rien quand un seul fichier correspond. pdfx est un client HTTP : les fichiers sont envoyés au serveur pour traitement, il n’y a pas de moteur local et pas de mode hors ligne. La référence complète des commandes se trouve dans le guide de la CLI sur la page développeurs.

Schéma : un appel à pdfx remet au script trois choses, un rapport JSON sur la sortie standard, les messages d’échec sur l’erreur standard et un code de sortie ; le script lit d’abord le code de sortie

L’installer, puis fusionner deux fichiers

Il vous faut Node 20.3 ou plus récent, ou Bun :

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

Si vous préférez ne pas installer globalement, faites précéder la commande de npx @pdf123/cli. Avec deux PDF dans le répertoire courant :

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

En cas de succès, la sortie standard affiche merged.pdf. C’était Fusionner des PDF. Avant de changer d’outil, demandez les champs plutôt que de deviner les noms des paramètres :

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 extrait ; les champs comprennent aussi --rotation, --customColor et d’autres.) Un champ n’est rien d’autre qu’une option de ligne de commande :

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

pdfx list liste les 95 outils, --category security ne liste qu’une catégorie, et --query watermark cherche par mot.

Plusieurs fichiers vont dans un répertoire, et les fichiers existants ne sont pas écrasés

Donnez plusieurs entrées à un outil à fichier unique et chaque fichier est écrit dans le répertoire désigné par -o dès qu’il est terminé, sans attendre la fin. Si un fichier échoue, les autres continuent, et le code de sortie à la fin du lot est 1.

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

La sortie standard d’une exécution ressemblait à ceci, et l’ordre peut changer à chaque fois :

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

Un répertoire existant fonctionne tel quel ; s’il n’existe pas encore, -o a besoin d’un / final, sinon small est pris pour un nom de fichier. Sans -o, les résultats arrivent dans le répertoire courant sous le nom que leur donne le serveur ; un fichier existant n’est pas écrasé et le nouveau devient a-1.pdf, a-2.pdf. Un nom de fichier précis (-o same.pdf) remplace le contenu existant, comme vous l’avez demandé. Par défaut, deux fichiers sont traités à la fois ; changez cela avec --concurrency. Si vous appuyez sur Ctrl-C au milieu d’un lot, les fichiers déjà écrits restent et le code de sortie est 130.

Ouvrez un fichier chiffré avec --input-password PASSWORD. Quand un lot mélange fichiers verrouillés et non verrouillés, utilisez --password-for FILE=PASSWORD, que vous pouvez répéter.

Appelez les outils séparément pour garder les fichiers intermédiaires, sinon utilisez un pipeline

Pour fusionner, puis filigraner, puis compresser, quand vous n’avez pas besoin des résultats intermédiaires sur le disque, repliez-les en une seule requête avec pipeline ; les fichiers intermédiaires ne sont ni téléchargés ni renvoyés. Si vous voulez le fichier de chaque étape, appelez les outils séparément. Un pipeline produit un seul fichier.

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

# Quand une étape a besoin de paramètres, décrivez les étapes en JSON
pdfx pipeline a.pdf b.pdf \
  --steps '[{"tool":"merge"},{"tool":"watermark","params":{"watermarkText":"DRAFT"}},{"tool":"compress"}]' \
  -o out.pdf

Un pipeline compte au plus 8 étapes ; une 9e étape reçoit HTTP 400: at most 8 pipeline steps allowed. Un outil qui a besoin d’un second fichier (par exemple overlay-pdfs, qui superpose un autre PDF) ne peut pas être une étape de pipeline. pdfx le refuse avant l’envoi, avec le code de sortie 2 et le message Tool "overlay-pdfs" needs a second file and cannot run as a pipeline step.

N’utilisez l’entrée standard que lorsque vous voulez brancher la commande sur un tube. Un argument de fichier - lit l’entrée standard, et -o - écrit le résultat sur la sortie standard :

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

Dans ce mode, la sortie standard ne porte que les octets du fichier ; les messages et les erreurs vont sur l’erreur standard, la redirection est donc sans danger. Nous l’avons vérifié avec un PDF d’environ 1 KB : la sortie standard contenait un PDF de 1040 octets, de la même taille que le fichier écrit avec -o, et l’erreur standard était vide. Quand vous lisez l’entrée standard sans donner -o, le résultat s’appelle stdin.pdf, écrit dans le répertoire courant avec une note sur l’erreur standard.

Les scripts doivent se fier à la raison, pas au message

Code de sortie Signification Exemples
0 Succès, ou outil de filtre conditionnel sans correspondance Une fusion a réussi ; la condition de filter-page-count était fausse
1 La requête a été envoyée mais a échoué ; dans un lot, au moins un fichier a échoué Mauvais mot de passe, pas un PDF, délai dépassé, rien à renvoyer
2 Erreur d’usage, rien n’a été envoyé Nom d’outil mal orthographié, étape de pipeline qui exige un second fichier, type de résultat en contradiction avec l’extension du fichier de sortie
130 Vous l’avez interrompu Ctrl-C pendant un lot

En cas d’échec, outre une ligne de message, l’erreur standard porte trois lignes : reason:, code: et hint:. Un fichier chiffré avec un mauvais mot de passe a produit ceci en local :

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

La façon de passer le mot de passe change la première ligne du message : unlock --password donne la ligne ci-dessus. Avec --input-password sur un autre outil, le serveur traite le déverrouillage comme une étape interne et la première ligne est HTTP 400: Pipeline step 0 (security/remove-password) failed: The password is incorrect., tandis que la ligne reason: vaut wrong_password dans les deux cas. Sans aucun mot de passe, le message est This PDF is password-protected. Enter its password. et reason vaut password_required. Un script doit comparer la ligne reason:. code est plus grossier, et une valeur courante est bad_request.

Un nom d’outil mal orthographié se termine par 2, et le message suggère des noms proches :

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

Un type de résultat qui contredit le nom de fichier se termine aussi par 2, et cela se produit avant toute écriture. Écrire dans x.pdf le ZIP que produit Diviser le 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

Un délai dépassé se termine aussi par 1, avec code à timeout. L’unité de --timeout est la milliseconde et la valeur par défaut est 300000, soit 5 minutes ; --timeout 60 signifie donc 60 millisecondes, pas 60 secondes.

Quand la condition est fausse, le code de sortie reste 0

Une catégorie d’outils répond à une question par oui ou non : le nombre de pages est-il supérieur à N, le fichier contient-il un certain texte, le fichier dépasse-t-il une certaine taille. Quand la réponse est oui, ils renvoient le fichier d’entrée inchangé ; quand elle est non, ils ne renvoient rien, et pdfx affiche une ligne no match et se termine par 0. Ces outils de filtre n’existent que dans le SDK, en ligne de commande et dans MCP ; le site n’a pas de page pour eux.

C’est différent d’un autre type de résultat vide. Quand PDF vers CSV ne trouve aucun tableau dans le PDF, le serveur renvoie 204 et pdfx se termine par 1 en signalant no_content : l’outil devait produire du contenu et n’en a pas produit, et la raison est dans Export de tableau vide (204) : votre PDF n’a probablement pas de colonnes. Pour un outil de filtre, « pas de correspondance » est la réponse qu’il devait donner.

Pour le lire dans un script, utilisez --json. Une correspondance affiche le fichier écrit ; l’absence de correspondance affiche { "matched": false } :

# Correspondance : le fichier est écrit, et ses informations sont affichées
$ pdfx filter-page-count three.pdf --pageCount 2 --comparator Greater --json -o big/
{ "path": "big/three.pdf", "contentType": "application/pdf", "bytes": 2594 }

# Pas de correspondance : aucun fichier n’est écrit
$ pdfx filter-page-count three.pdf --pageCount 5 --comparator Greater --json
{ "matched": false }

Pour ne garder que les fichiers de plus de 2 pages, vous pouvez écrire ceci. Nous l’avons exécuté en local sur a.pdf (1 page) et three.pdf (3 pages), et seul le second a été conservé :

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

jq -e '.matched == false' se termine par 0 quand il n’y a pas de correspondance et par 1 quand il y en a une. En cas de correspondance, le fichier a déjà été écrit dans big/ par pdfx ; le if décide seulement d’afficher ou non « ignoré », pas d’écrire.

Avec une seule entrée, --json n’a pas de tableau files

Avec plusieurs entrées, la sortie standard sous --json est un rapport complet. input est un chemin absolu. processed compte les fichiers dont la requête a réussi, y compris ceux sans correspondance. unmatched est le nombre de ceux-ci qui n’avaient pas de correspondance, et leurs entrées sont { "ok": true, "matched": false } sans path. Quand chaque fichier d’un lot est sans correspondance, le code de sortie reste 0. Quand vous passez --idempotency-key à un lot, la clé envoyée pour chaque fichier est <key>:<index> ; le mécanisme est dans Idempotency-Key : des reprises sûres pour les tâches 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 }
  ]
}

Avec une seule entrée, c’est le chemin du fichier unique qui est emprunté : --json affiche { "path": ..., "contentType": ..., "bytes": ... } et il n’y a pas de tableau files. En cas d’échec, la sortie standard est vide et tout est sur l’erreur standard. Quand vous développez des fichiers avec un glob, le script doit gérer les deux formats, qu’un fichier corresponde ou plusieurs.

Réunir les règles ci-dessus en une seule étape de CI

Ce script ne fait que compresser et n’utilise aucun outil de filtre. Un échec de compression se termine par le code 1, et l’étape échoue avec lui. Un outil de filtre sans correspondance se termine par 0, si bien que la CI n’échoue pas simplement parce qu’il n’y avait pas de correspondance ; c’est à vous de décider si l’absence de correspondance est un problème, en lisant --json, comme dans la section sur les filtres plus haut.

Si un fichier est rejeté, cette étape échoue et liste dans le journal les noms de fichiers et les raisons. La logique est la même qu’avant : lire d’abord le code de sortie, afficher l’erreur standard en cas d’échec, puis utiliser jq pour extraire le reason du rapport de lot. Sans argument de répertoire, il se termine aussitôt, de sorte que "$1"/*.pdf ne soit jamais développé en /*.pdf.

#!/usr/bin/env bash
# Usage : ./ci-step.sh docs
docs="${1:?usage: ./ci-step.sh <répertoire>}"
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"

Nous l’avons exécuté en local avec quatre types d’entrée :

Fichiers dans le répertoire Code de sortie Journal
Deux PDF valides 0 aucun
Deux PDF valides plus un endommagé 1 pdfx: broken.pdf: HTTP 400: ..., puis une ligne broken.pdf invalid_pdf
Un seul PDF valide 0 aucun ; report.json est au format fichier unique
Un seul PDF endommagé 1 reason: invalid_pdf, code: invalid_document et une ligne hint ; report.json est vide

Le point d’interrogation à la fin de .files[]? dans jq évite qu’un rapport de fichier unique (qui n’a pas de files) ne provoque une erreur. Un seul fichier endommagé n’a pas de rapport de lot et la raison n’apparaît que dans errors.log ; gardez donc les deux sorties.

Trois autres points à vérifier avant de mettre cela en CI. pdfx a besoin du réseau : les fichiers sont envoyés à PDFX_API_BASE, qui vaut par défaut https://pdf123.xyz. Pour les documents qui doivent rester dans votre réseau, faites tourner un service vous-même et pointez cette variable dessus ; voir Ce que l’auto-hébergement vous apporte vraiment (et ce qu’il coûte). Quand vous avez besoin d’une identité, mettez la clé dans PDFX_API_KEY et non dans les arguments de la ligne de commande, où elle resterait dans la liste des processus et dans les journaux. La version actuellement publiée est 0.1.0 ; en CI, utilisez npx @pdf123/[email protected] ... pour la figer, afin que le format de sortie et les codes de sortie ne changent pas avec les nouvelles versions et que la mise à jour devienne un changement que vous faites exprès.

La page du paquet sur npm est @pdf123/cli.

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