Fusionner des documents PDF en PHP avec FPDI et FPDF
Pour fusionner des PDF en PHP, importez chaque page source avec FPDI et créez une page de sortie correspondante avec FPDF. Cet exemple en ligne de commande combine des PDF locaux dans l’ordre où vous les fournissez, conserve des formats de page mixtes et échoue si l’une des entrées demandées ne peut pas être importée. Vous allez générer deux PDF d’exemple, les fusionner et vérifier les trois pages obtenues.
Prérequis
Utilisez PHP 8.3–8.5, Composer 2 et un répertoire local accessible en écriture. Ce tutoriel a été testé dans un shell Bash sous Linux avec PHP 8.5.10, Composer 2.10.3, FPDI 2.6.8 et FPDF 1.9.0.
Activez les extensions GD et Zlib dans la CLI de PHP avant d’installer les dépendances. Toutes
deux sont déclarées dans le manifeste Composer de FPDF ;
FPDI requiert également Zlib. Vérifiez la configuration de la CLI
avec php --ini et ses extensions chargées avec php -m. Un serveur web peut utiliser
une configuration PHP différente.
Installez les commandes pdfinfo et pdftotext de Poppler si vous souhaitez exécuter les vérifications
indépendantes ci-dessous. Elles inspectent le résultat ; ce ne sont pas des dépendances du script de
fusion PHP.
Installer FPDI et FPDF
Placez-vous dans un répertoire parent où vous souhaitez créer un nouveau projet pdf-merge-demo :
mkdir pdf-merge-demo &&
cd pdf-merge-demo &&
composer require --no-interaction 'setasign/fpdf:1.9.0' 'setasign/fpdi:2.6.8'
La chaîne && s’arrête si la création du répertoire ou le déplacement dans celui-ci échoue. Si le
projet existe déjà, choisissez un autre nom ; ne le supprimez pas pour relancer l’installation. Ne
continuez qu’une fois Composer exécuté avec succès, en conservant le fichier composer.lock généré pour
des installations reproductibles. Enregistrez les scripts suivants dans pdf-merge-demo et exécutez
toutes les commandes restantes depuis ce répertoire.
Générer des PDF d’exemple
Enregistrez ce code sous samples.php. Il crée a.pdf avec une page A4 portrait et une page A3
paysage, ainsi que b.pdf avec une page US Letter portrait. Les libellés facilitent la
vérification de l’ordre des pages. Exécuter à nouveau ce générateur d’exemples
remplace a.pdf et b.pdf dans le répertoire du tutoriel.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
$samples = [
'a.pdf' => [['P', 'A4', 'A1'], ['L', 'A3', 'A2']],
'b.pdf' => [['P', 'Letter', 'B1']],
];
foreach ($samples as $name => $pages) {
$pdf = new FPDF();
foreach ($pages as [$orientation, $format, $label]) {
$pdf->AddPage($orientation, $format);
$pdf->SetFont('Helvetica', '', 20);
$pdf->Cell(0, 10, $label);
}
$pdf->Output('F', __DIR__ . '/' . $name);
}
php samples.php
Importer des PDF existants avec FPDI
FPDI crée un nouveau document à partir du contenu de pages importées. Sa
méthode setSourceFile()
renvoie le nombre de pages de la source. Pour chaque page, la boucle de fusion ci-dessous appelle
importPage(), obtient ses dimensions avec getTemplateSize() et transmet ces dimensions et l’orientation à
AddPage(). Appeler AddPage() sans ces arguments utiliserait à la place la page par défaut A4
portrait.
Par défaut, importPage() utilise la CropBox, c’est-à-dire la zone visible de la page, et se
rabat sur la MediaBox si nécessaire. La sortie correspond à cette zone importée, rotation de la page
source comprise ; elle ne conserve pas les zones distinctes destinées à la production d’impression.
Consultez l’implémentation des zones de page dans FPDI.
Fusionner un nombre quelconque de PDF
Enregistrez ce script complet sous merge.php. mergeMany() renvoie le nombre de pages de sortie
en cas de succès et lève une exception en cas d’échec. Aucune entrée n’est jamais ignorée. La CLI
intercepte les échecs, écrit une erreur sur la sortie d’erreur standard et se termine avec le code
de sortie 1.
La politique de sortie diffère de celle du générateur d’exemples jetable : la fusion refuse
d’écraser une destination existante, y compris un fichier d’entrée. Elle termine l’importation et
la sérialisation de toutes les pages avant d’ouvrir la sortie. Le
mode de fichier xb de PHP crée exclusivement un nouveau fichier
binaire, si bien qu’une nouvelle exécution ne peut pas tronquer un résultat précédent.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use setasign\Fpdi\Fpdi;
function mergeMany(array $files, string $out): int
{
if ($files === []) {
throw new InvalidArgumentException('Provide at least one input PDF.');
}
$pdf = new Fpdi();
foreach ($files as $file) {
if (!is_file($file) || !is_readable($file)) {
throw new RuntimeException("Cannot read input PDF: $file");
}
$pageCount = $pdf->setSourceFile($file);
if ($pageCount < 1) {
throw new RuntimeException("Input PDF has no pages: $file");
}
for ($page = 1; $page <= $pageCount; $page++) {
$template = $pdf->importPage($page);
$size = $pdf->getTemplateSize($template);
$pdf->AddPage($size['orientation'], [$size['width'], $size['height']]);
$pdf->useTemplate($template);
}
}
// Serialization can fail too; do it before creating the destination.
$bytes = $pdf->Output('S');
$handle = @fopen($out, 'xb');
if ($handle === false) {
throw new RuntimeException("Cannot create output: $out (exists or is not writable).");
}
try {
if (fwrite($handle, $bytes) !== strlen($bytes) || !fflush($handle)) {
throw new RuntimeException("Could not write the complete PDF: $out");
}
} catch (Throwable $error) {
fclose($handle);
unlink($out);
throw $error;
}
fclose($handle);
return $pdf->PageNo();
}
try {
if ($argc < 2) {
throw new InvalidArgumentException('Usage: php merge.php OUTPUT.pdf INPUT.pdf ...');
}
$pages = mergeMany(array_slice($argv, 2), $argv[1]);
fwrite(STDOUT, "Merged $pages pages into {$argv[1]}\n");
} catch (Throwable $error) {
fwrite(STDERR, 'Merge failed: ' . $error->getMessage() . "\n");
exit(1);
}
Output('S') renvoie les octets du PDF sous forme de chaîne. Les échecs
d’importation ou de sérialisation laissent la destination intacte. Un échec d’écriture détecté
supprime la nouvelle sortie incomplète. Ce script local n’est pas un mécanisme de publication
résistant aux plantages : une interruption pendant l’écriture peut laisser un fichier partiel, les
consommateurs doivent donc attendre une fin d’exécution réussie.
Fusionner deux PDF
Exécutez le script en indiquant d’abord la destination, puis les entrées dans l’ordre souhaité :
php merge.php merged.pdf a.pdf b.pdf
Il devrait afficher Merged 3 pages into merged.pdf et se terminer avec le code de sortie 0. Pour davantage de
chemins d’entrée, utilisez la même commande ; mettez entre guillemets les chemins contenant des
espaces. Pour réordonner ces exemples, écrivez un résultat distinct :
php merge.php reversed.pdf b.pdf a.pdf
Les libellés de reversed.pdf devraient être B1, A1 et A2. Les fichiers sources restent inchangés.
Vérifier le résultat et le comportement en cas d’échec
pdfinfo -f 1 -l 3 merged.pdf &&
pdftotext -layout merged.pdf -
Attendez-vous à obtenir Pages: 3, ces dimensions de page et les libellés A1, A2 et B1, dans cet ordre.
Un point PDF vaut 1/72 de pouce ; de légères différences d’arrondi sont normales.
| Page | Libellé | Format | Largeur × hauteur (points) |
|---|---|---|---|
| 1 | A1 | A4 portrait | 595,28 × 841,89 |
| 2 | A2 | A3 paysage | 1190,55 × 841,89 |
| 3 | B1 | US Letter portrait | 612 × 792 |
Une entrée manquante doit faire échouer toute la requête, même lorsque des entrées valides
l’entourent. Laissez missing.pdf absent et utilisez une nouvelle destination :
php merge.php incomplete.pdf a.pdf missing.pdf b.pdf
Cette commande se termine avec le code de sortie 1, signale Cannot read input PDF: missing.pdf et ne crée aucun
fichier incomplete.pdf. Un fichier illisible, un répertoire utilisé comme entrée ou un PDF malformé
provoque également un échec. Fournir uniquement un chemin de sortie échoue, car la liste des entrées
est vide. Répéter la commande de fusion réussie échoue, car merged.pdf existe ; ses octets restent
inchangés. Choisissez un nouveau nom de sortie pour conserver les deux versions.
Savoir ce que le parseur gratuit peut conserver
Il s’agit d’importations statiques de pages. Cet exemple ne copie pas les champs de formulaire
interactifs, les annotations, les liens cliquables, les signets, les calques ni les actions du
document. FPDI peut, en option, importer les annotations de liens URI externes via importPage() et
son paramètre importExternalLinks, mais celui-ci vaut ici false par défaut. Cette option ne restaure ni
les formulaires, ni la navigation interne, ni les autres annotations.
Le parseur gratuit rejette également les PDF chiffrés ou protégés par mot de passe, ainsi que les PDF qui utilisent des flux de références croisées compressés ou des flux d’objets. Le contenu de page compressé ordinaire est pris en charge, comme dans les exemples FPDF. Le numéro de version d’un PDF ne suffit pas à indiquer si les structures non prises en charge sont présentes. Ces restrictions sont documentées dans les limitations de FPDI.
Si vous rencontrez l’erreur de compression non prise en charge, obtenez un export compatible ou évaluez le module complémentaire FPDI PDF-Parser optionnel de Setasign. Étendre le parseur ne transforme pas une importation du contenu des pages en une fusion qui conserve toutes les fonctionnalités interactives du document. Choisissez un outil de fusion au niveau du document lorsque ces fonctionnalités sont requises.
Gérer la mémoire pour les fichiers volumineux
FPDF construit la sortie en mémoire, et ce script conserve également la chaîne renvoyée par Output('S')
pendant son enregistrement. Traiter une page à la fois ne fait donc pas de cette tâche une fusion en
streaming. Mesurez le pic de mémoire avec des entrées représentatives avant de choisir une limite de
mémoire PHP ou une taille de tâche ; il n’existe pas de limite fiable fondée uniquement sur le nombre
de PDF.
Relâcher l’objet FPDI après une tâche peut libérer ses ressources, mais le ramasse-miettes après la fusion ne peut pas réduire le pic de mémoire de cette tâche. Pour un processus worker de longue durée, relâchez les objets de chaque tâche terminée avant de lancer la suivante.
