Fusionner et extraire des pages PDF avec PDFtk et Python
L’opération cat de PDFtk peut assembler des PDF, extraire une plage de pages ou réordonner
des pages sans les rastériser. Cette procédure utilise PDFtk Java pour fusionner deux petits
documents d’exemple, sélectionner des pages dans le résultat et automatiser la fusion en Python,
avec un code de sortie non nul en cas d’échec et sans remplacement des fichiers de sortie existants.
Configuration requise
Les commandes ci-dessous ont été testées sous Linux avec Bash 5.3.15, OpenJDK 21.0.12.1,
Python 3.14.7, qpdf 12.4.1 et Poppler 26.08.0. Assurez-vous que java, python3, qpdf, pdftotext,
curl et sha256sum se trouvent dans votre PATH avant de commencer. Ce sont les versions testées,
ce qui ne signifie pas que toutes les versions antérieures fonctionnent. Poppler fournit pdftotext ;
qpdf vérifie la structure PDF indépendamment de PDFtk.
Utilisez des PDF fiables et non chiffrés pour cet exemple local. Le fait qu’un analyseur accepte un PDF ne prouve pas que celui-ci est inoffensif, visuellement intact ou exempt de contenu actif. Traitez les documents non fiables dans un environnement isolé de manière appropriée, et non avec ce script en guise de filtre de sécurité.
Installer PDFtk
PDFtk Java est un portage de PDFtk, et non l’exécutable natif d’origine PDFtk Server. Son
guide d’installation versionné
répertorie des paquets de distribution et un JAR autonome contenant ses dépendances. Nous utilisons
ce JAR afin que chaque invocation sélectionne la version 3.3.3 plutôt que le pdftk fourni par un
gestionnaire de paquets, quel qu’il soit. Cette procédure utilise des commandes shell Linux ; elle
ne couvre pas les programmes d’installation pour macOS ou Windows.
Collez ce bloc dans Bash depuis un répertoire accessible en écriture. Il crée un nouveau répertoire
pdf-workflow et laisse votre shell dans son répertoire d’origine. Un répertoire existant, un téléchargement
échoué ou une somme de contrôle non concordante arrête le bloc. Ne supprimez pas un projet existant
pour le faire aboutir.
(
set -eu
mkdir pdf-workflow
cd pdf-workflow
curl -fsSLo pdftk-java-3.3.3-all.jar \
https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/v3.3.3/pdftk-all.jar
printf '%s %s\n' \
a694d49bd03e1edd4c23b3ba808bc221eb8a8ccfe7bfd2a0a884b2b2fb425188 \
pdftk-java-3.3.3-all.jar | sha256sum -c -
java -Xms16m -Xmx256m -XX:ActiveProcessorCount=2 -XX:-UsePerfData \
-Djava.io.tmpdir="$PWD" -Duser.home="$PWD" \
-jar pdftk-java-3.3.3-all.jar --version
)
Le hachage fixe les octets téléchargés ; ce n’est pas une signature de l’éditeur. La sortie de version doit identifier PDFtk Java 3.3.3. Une installation échouée peut laisser derrière elle le nouveau répertoire et le fichier téléchargé ; examinez-les avant de choisir un nouvel emplacement.
Opérations PDF de base
Pour obtenir une entrée reproductible, enregistrez le code suivant sous pdf-workflow/make_samples.py. Il utilise
uniquement la bibliothèque standard de Python pour créer un PDF de deux pages portant les libellés
ALPHA ONE et ALPHA TWO, ainsi qu’un PDF d’une page portant le libellé BETA ONE. Il refuse les noms de fichiers
d’exemple déjà existants plutôt que de remplacer ces fichiers.
from pathlib import Path
def write_pdf(path, labels):
page_ids = [4 + 2 * index for index in range(len(labels))]
kids = ' '.join(f'{page_id} 0 R' for page_id in page_ids)
objects = [
b'<< /Type /Catalog /Pages 2 0 R >>',
f'<< /Type /Pages /Count {len(labels)} /Kids [{kids}] >>'.encode(),
b'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
]
for page_id, label in zip(page_ids, labels):
stream = f'BT /F1 24 Tf 40 180 Td ({label}) Tj ET\n'.encode('ascii')
objects.append(
f'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 360 240] '
f'/Resources << /Font << /F1 3 0 R >> >> '
f'/Contents {page_id + 1} 0 R >>'.encode()
)
objects.append(f'<< /Length {len(stream)} >>\nstream\n'.encode()
+ stream + b'endstream')
data = bytearray(b'%PDF-1.4\n')
offsets = [0]
for object_id, payload in enumerate(objects, start=1):
offsets.append(len(data))
data.extend(f'{object_id} 0 obj\n'.encode() + payload + b'\nendobj\n')
xref = len(data)
data.extend(f'xref\n0 {len(offsets)}\n0000000000 65535 f \n'.encode())
for offset in offsets[1:]:
data.extend(f'{offset:010d} 00000 n \n'.encode())
data.extend(f'trailer\n<< /Size {len(offsets)} /Root 1 0 R >>\n'
f'startxref\n{xref}\n%%EOF\n'.encode())
with path.open('xb') as output:
output.write(data)
write_pdf(Path('input-a.pdf'), ['ALPHA ONE', 'ALPHA TWO'])
write_pdf(Path('input-b.pdf'), ['BETA ONE'])
Fusionner des PDF
Exécutez ceci depuis le même répertoire parent que lors de l’installation. A et B sont des
descripteurs d’entrée ; A1-end B1-end inclut toutes les pages de A suivies de toutes les pages de B.
Le manuel de PDFtk documente les plages de pages et
les descripteurs. Avec vos propres documents, remplacez les noms d’entrée et vérifiez d’abord leur
nombre de pages.
(
set -eu
cd pdf-workflow
python3 make_samples.py
java -Xms16m -Xmx256m -XX:ActiveProcessorCount=2 -XX:-UsePerfData \
-Djava.io.tmpdir="$PWD" -Duser.home="$PWD" \
-jar pdftk-java-3.3.3-all.jar A=input-a.pdf B=input-b.pdf \
cat A1-end B1-end output combined.pdf dont_ask
)
Les commandes de bas niveau utilisent délibérément dont_ask : elles s’exécutent sans invite et
écrasent leurs sorties désignées. Ce sont des commandes d’exemple séquentielles, et non des
enveloppes de publication sûres, et un échec peut laisser une sortie partielle. Ne les dirigez pas
vers des fichiers que vous devez conserver. Le script de fusion Python ci-dessous applique une
politique différente, sans remplacement. Si vous réexécutez tout le bloc de fusion, il s’arrête sur
les fichiers d’exemple existants ; il ne continue pas avec des entrées obsolètes après cet échec.
Scinder des PDF
Extrayez et réordonnez des pages à partir du PDF fusionné produit ci-dessus. Les pages sont
numérotées à partir de un. cat 3 1 sélectionne sa troisième page suivie de sa première ; cat 1-2
sélectionnerait une plage contiguë.
(
set -eu
cd pdf-workflow
java -Xms16m -Xmx256m -XX:ActiveProcessorCount=2 -XX:-UsePerfData \
-Djava.io.tmpdir="$PWD" -Duser.home="$PWD" \
-jar pdftk-java-3.3.3-all.jar combined.pdf cat 3 1 output selected.pdf dont_ask
)
Vérifiez les deux sorties réelles, et pas seulement le code de sortie de la commande qui les produit :
(
set -eu
cd pdf-workflow
qpdf --check combined.pdf >/dev/null
test "$(qpdf --show-npages combined.pdf)" = 3
printf 'Merged pages:\n'
pdftotext -layout combined.pdf -
qpdf --check selected.pdf >/dev/null
test "$(qpdf --show-npages selected.pdf)" = 2
printf 'Selected pages:\n'
pdftotext -layout selected.pdf -
)
Le texte fusionné doit être ALPHA ONE, ALPHA TWO, puis BETA ONE ; le texte sélectionné doit être
BETA ONE, puis ALPHA ONE. Ouvrez les deux PDF dans une visionneuse pour vérifier leur apparence.
Ces vérifications du texte et de la structure ne prouvent pas que des documents quelconques
conservent les formulaires, les annotations, les signatures ou chaque détail visuel. Les
numérisations composées uniquement d’images peuvent ne contenir aucun texte extractible.
Optimiser la taille des fichiers PDF
L’option compress de PDFtk restaure la
compression des flux de page ; ce n’est ni un outil de sous-échantillonnage d’images ni une
garantie d’obtenir un PDF plus petit. La fusion ne « normalise » pas non plus un document. La
compression d’images nécessite un flux de travail de réécriture distinct, avec ses propres contrôles
de qualité ; c’est pourquoi cet exemple ne comporte aucune étape de conversion Ghostscript.
Exemple d’intégration
Enregistrez ceci sous pdf-workflow/merge_pdfs.py. Le script accepte un JAR, un nouveau nom de fichier de sortie et
une ou plusieurs entrées. Placez -- avant l’argument du JAR, comme dans l’invocation ci-dessous,
afin que l’analyseur d’arguments de Python traite littéralement les tirets en début de nom de
fichier. Le script résout les chemins d’entrée avant de les transmettre comme arguments distincts au
sous-processus ; il ne construit pas de commande shell à partir de vos noms de fichiers.
Ce script Linux local rejette une taille d’entrée cumulée supérieure à 100 MiB, vérifie chaque
entrée avec qpdf et prépare le PDF fusionné dans le répertoire de destination. Après avoir vérifié
le PDF préparé, il utilise os.link pour le publier sans remplacer
une destination existante. Le système de fichiers doit prendre en charge les liens physiques. Le
script ne promet ni la durabilité en cas de plantage ni un fonctionnement sûr si quelqu’un modifie
malicieusement le répertoire ou les entrées pendant le traitement.
import argparse
import os
import subprocess
import sys
import tempfile
from pathlib import Path
def main():
parser = argparse.ArgumentParser(description='Merge PDFs without replacing an output.')
parser.add_argument('jar', type=Path)
parser.add_argument('output', type=Path)
parser.add_argument('inputs', type=Path, nargs='+')
args = parser.parse_args()
destination = args.output.absolute()
if os.path.lexists(destination):
raise FileExistsError(f'Output already exists: {destination}')
jar = args.jar.resolve(strict=True)
inputs = [path.resolve(strict=True) for path in args.inputs]
if not jar.is_file() or any(not path.is_file() for path in inputs):
raise ValueError('The JAR and inputs must be regular files.')
if sum(path.stat().st_size for path in inputs) > 100 * 1024 * 1024:
raise ValueError('Combined inputs exceed 100 MiB.')
for path in inputs:
subprocess.run(['qpdf', '--check', str(path)], check=True,
capture_output=True, timeout=60)
with tempfile.TemporaryDirectory(prefix='.pdf-merge-', dir=destination.parent) as staging:
staged = Path(staging) / 'merged.pdf'
command = [
'java', '-Xms16m', '-Xmx256m', '-XX:ActiveProcessorCount=2', '-XX:-UsePerfData',
f'-Djava.io.tmpdir={staging}', f'-Duser.home={staging}', '-jar', str(jar),
*map(str, inputs), 'cat', 'output', str(staged), 'dont_ask',
]
subprocess.run(command, check=True, capture_output=True, timeout=60)
subprocess.run(['qpdf', '--check', str(staged)], check=True,
capture_output=True, timeout=60)
os.link(staged, destination)
print(f'Created {destination.name}')
if __name__ == '__main__':
try:
main()
except (OSError, ValueError, subprocess.SubprocessError) as error:
print(f'Cannot merge PDFs: {error}', file=sys.stderr)
sys.exit(1)
Exécutez-le sur les mêmes entrées d’exemple. La sortie comporte trois pages, dans le même ordre que
combined.pdf :
(
set -eu
cd pdf-workflow
python3 merge_pdfs.py -- pdftk-java-3.3.3-all.jar merged-from-python.pdf input-a.pdf input-b.pdf
qpdf --check merged-from-python.pdf >/dev/null
test "$(qpdf --show-npages merged-from-python.pdf)" = 3
pdftotext -layout merged-from-python.pdf -
)
Des fichiers manquants, des PDF invalides, des entrées trop volumineuses, un exécutable manquant, un
dépassement de délai ou un échec de publication renvoient un code de sortie non nul sans signaler de
fusion réussie. Une deuxième invocation refuse merged-from-python.pdf et laisse ses octets inchangés. Les échecs
des vérifications préalables ne créent aucune sortie préparée ; lors des échecs ordinaires pendant
le traitement, le répertoire de préparation est nettoyé. Un arrêt forcé peut laisser ce répertoire
privé derrière lui. Chaque sous-processus natif dispose d’un délai de 60 secondes, et non d’un
délai de 60 secondes pour l’ensemble de la fusion. Ici, les avertissements de qpdf sont traités
comme des échecs ; examinez votre source plutôt que d’accepter silencieusement un document réparé.
Résoudre les problèmes courants
Erreurs liées à la mémoire
La vérification de 100 MiB est une politique d’admission des entrées, et non une estimation de la
RAM. La complexité du PDF, la décompression et les allocations hors tas de Java comptent aussi.
-Xmx256m plafonne le tas Java, et non la mémoire totale du processus, et ne garantit pas qu’une
entrée acceptée ira jusqu’au bout. Il n’y a pas de traitement par lots automatique : un document
trop volumineux est rejeté, et non placé dans un lot surdimensionné ou précédé d’un lot vide.
Erreurs d’accès aux fichiers
Vérifiez que les entrées et le JAR sont lisibles, et que le répertoire parent de la sortie existe et est accessible en écriture. Choisissez une nouvelle destination lorsque celle-ci existe déjà ; le script Python ne la supprime pas, même si une autre étape échoue. N’assouplissez pas largement les permissions de fichiers simplement pour faire fonctionner une fusion. En cas d’échec de qpdf ou de PDFtk, exécutez sa commande localement sur l’entrée en cause pour examiner le diagnostic.
Pour commencer, traitez une tâche à la fois. Des fichiers d’entrée plus petits ne garantissent pas à eux seuls une mémoire bornée ni un meilleur débit ; n’ajoutez de la concurrence qu’après avoir mesuré votre propre charge de travail et les ressources disponibles.
Considérations de sécurité
Gestion sécurisée des PDF
Le mode encrypt_128bit de PDFtk
utilise l’algorithme hérité RC4, et non l’AES moderne. Ne l’utilisez pas pour protéger des
documents confidentiels. Un mot de passe propriétaire seul n’exige pas de mot de passe pour ouvrir
un PDF ; le mot de passe utilisateur est le mécanisme distinct de mot de passe à l’ouverture.
Consultez les options de mot de passe si vous
maintenez un flux de travail hérité, plutôt que de copier des mots de passe dans des arguments de
commande ou des scripts. PROMPT permet de saisir un mot de passe de manière interactive ; ce
script de fusion non interactif ne gère pas les entrées protégées par mot de passe.
Gérer les permissions de fichiers
Les restrictions d’impression et de modification d’un PDF dépendent du respect de celles-ci par le lecteur. Elles ne constituent pas une frontière de contrôle d’accès. Restreignez l’accès aux fichiers eux-mêmes ; le script de fusion ne supprime ni le texte ni les métadonnées sensibles, et ne définit pas la politique de stockage et de partage d’une organisation.
Conclusion
Utilisez des plages de pages explicites lorsque l’ordre compte, examinez le document obtenu et décidez s’il est acceptable de remplacer une sortie avant d’automatiser la commande. Le projet PDFtk Java est l’endroit où explorer d’autres opérations une fois que ce petit flux de travail répond à vos besoins.
