Découper un PDF et extraire des plages de pages
En un coup d’œil
Section intitulée « En un coup d’œil »Tu as un seul PDF, et tu en veux plusieurs. Cette recette découpe un document
unique en plusieurs fichiers avec la surface de découpe du Core,
NextPDF\Document\PdfSplitter. Tu fournis la source sous forme de chaîne d’octets
PDF bruts et tu décris les pages que tu veux. Le splitter analyse la source à
travers le graphe d’objets, copie les objets atteignables de chaque plage demandée
dans un document neuf et renuméroté doté de son propre arbre de pages et de sa
propre table de références croisées, puis renvoie des PDF structurellement complets
qui se chargent dans un lecteur conforme.
C’est l’inverse de la recette de fusion : la fusion compose plusieurs documents en un seul, la découpe décompose un document en plusieurs. La même surface couvre les trois tâches dont tu as le plus souvent besoin :
- Découper par plages — produire un document de sortie par plage de pages que tu nommes.
- Découper toutes les N pages — segmenter un long fichier en tranches de taille fixe.
- Extraire une plage — extraire une seule plage de pages contiguë dans un seul document.
La découpe s’exécute dans le processus, sans navigateur headless et sans appel
réseau. Tu as besoin du Core installé (composer require nextpdf/core:^3) et d’un
PDF lisible.
Installation
Section intitulée « Installation »composer require nextpdf/core:^3Vue d’ensemble conceptuelle
Section intitulée « Vue d’ensemble conceptuelle »Un PDF localise ses pages grâce à un arbre de pages dont la racine est un nœud
/Pages, et il atteint chaque objet indirect grâce à ses données de références
croisées (une table ou un flux). Tu ne peux pas extraire des pages en découpant des
octets : une seule page référence des polices, des images et des dictionnaires de
ressources partagés qui vivent ailleurs dans le fichier, et les décalages des
références croisées ne seraient plus valides.
PdfSplitter fait le vrai travail. Pour chaque plage, il parcourt le graphe
d’objets à partir des objets de page demandés, collecte la fermeture des objets
atteignables, renumérote ces objets dans un nouvel espace d’adressage, reconstruit
un document à arbre de pages unique, et émet une vraie table de références croisées
conforme à la structure PDF 2.0 (ISO 32000-2:2020, table de références croisées
§7.5.4, arbre de pages §7.7.3). Chaque sortie est un document autonome, pas un
fragment.
Les numéros de pages commencent à 1 et sont inclusifs. Une plage est un objet valeur
NextPDF\Document\PageRange : new PageRange(2, 5) désigne les pages 2 à 5. Le
constructeur valide ses propres invariants — il rejette un début inférieur à 1 ou
une fin antérieure au début en levant une NextPDF\Exception\PageLayoutException —
de sorte qu’une plage impossible échoue à la construction, et non au fond du
splitter. PageRange::parse() et PageRange::all() lèvent la même
PageLayoutException sur une spécification mal formée ou un total de pages non
positif.
Surface de l’API
Section intitulée « Surface de l’API »new NextPDF\Document\PdfSplitter() expose trois méthodes. Toutes prennent la
source sous forme de chaîne d’octets PDF bruts, jamais un chemin.
split(string $pdfData, array $ranges, int $maxBytes = 100_000_000, int $maxRanges = 1000): SplitResultproduit un document de sortie parPageRangedans$ranges, dans l’ordre. Les deux paramètres de bornage plafonnent la taille d’entrée et le nombre de plages.splitEvery(string $pdfData, int $pagesPerSegment): SplitResultsegmente le document en tranches de taille fixe de$pagesPerSegmentpages chacune ; la dernière tranche contient le reste.extractPages(string $pdfData, PageRange $range): SplitDocumentextrait une seule plage et renvoie directement ce document.
split() et splitEvery() renvoient un NextPDF\Document\SplitResult, un objet
readonly qui porte $documents (une liste de tranches), $totalPages (les pages
de la source) et $sourceSize. Il offre count(), document(int $index) pour
récupérer une tranche par index commençant à zéro, et totalOutputSize().
Chaque tranche, ainsi que la valeur de retour d’extractPages(), est un
NextPDF\Document\SplitDocument : un objet readonly exposant $pdfData (les
octets de la tranche), $range, $pageCount, $sizeBytes, et l’utilitaire
isValid(). isValid() est un contrôle de cohérence restreint sur l’en-tête
%PDF — il renvoie true quand les octets de la tranche commencent par %PDF —
et non une validation de structure de document ou de conformité ; il confirme que le
splitter a produit un PDF, pas que le fichier est entièrement conforme.
Tu construis une PageRange directement avec new PageRange($start, $end), ou tu
analyses une spécification lisible par un humain avec
PageRange::parse('1-3,5,7-10'), qui renvoie une list<PageRange> prête à passer à
split(). PageRange::all($totalPages) renvoie une seule plage couvrant tout le
document.
Exemple de code — Démarrage rapide
Section intitulée « Exemple de code — Démarrage rapide »Cet exemple lit un fichier et le découpe en deux documents : les pages 1 à 3 et les pages 4 à 6. Il omet la gestion des erreurs pour montrer la forme de l’appel ; l’exemple de production ci-dessous ajoute tous les garde-fous.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Document\PageRange;use NextPDF\Document\PdfSplitter;
$splitter = new PdfSplitter();
$result = $splitter->split( file_get_contents(__DIR__ . '/report.pdf'), [ new PageRange(1, 3), new PageRange(4, 6), ],);
foreach ($result->documents as $i => $segment) { file_put_contents(__DIR__ . sprintf('/part-%d.pdf', $i + 1), $segment->pdfData);}
printf("Split %d-page source into %d document(s).\n", $result->totalPages, $result->count());Exemple de code — Production
Section intitulée « Exemple de code — Production »Ce programme autonome construit un petit document multipage en mémoire, de sorte
qu’il s’exécute sans fichier externe. Il démontre les trois opérations — découper
par plages, découper toutes les N pages, et extraire une seule plage. Il valide et
écrit les tranches par plage et la fin extraite, et rapporte le résultat par taille
sous forme de comptage, pour que tu voies la forme de chaque appel sans trois
boucles d’écriture quasi identiques. Il intercepte les exceptions que la surface de
découpe lève et relance chacune avec son contexte au lieu de l’avaler. Remplace la
source en mémoire par ta propre lecture file_get_contents() ou ta récupération
depuis un stockage objet.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use InvalidArgumentException;use NextPDF\Core\Document;use NextPDF\Document\Merge\UnsupportedSourceDocumentException;use NextPDF\Document\PageRange;use NextPDF\Document\PdfSplitter;use NextPDF\Document\SplitDocument;use NextPDF\Exception\PageLayoutException;
/** * Build a tiny labelled multi-page PDF so the program is self-contained. * * In your own code, replace this with a read of the PDF you want to split, * for example file_get_contents($path). */function buildSample(int $pages): string{ $doc = Document::createStandalone(); $doc->setTitle('Split sample');
for ($page = 1; $page <= $pages; $page++) { $doc->addPage(); $doc->setFont('helvetica', '', 12); $doc->cell(0, 10, sprintf('Source page %d', $page), newLine: true); }
return $doc->getPdfData();}
$source = buildSample(7);
$splitter = new PdfSplitter();
try { // 1. Split into named ranges: one output per PageRange, in order. $byRange = $splitter->split( $source, PageRange::parse('1-3,4-6'), maxBytes: 50_000_000, maxRanges: 100, );
// 2. Split every 2 pages: segments of [1-2], [3-4], [5-6], [7] (remainder). $bySize = $splitter->splitEvery($source, 2);
// 3. Extract a single range as one document. $tail = $splitter->extractPages($source, new PageRange(7, 7));} catch (InvalidArgumentException $e) { // Raised on an oversized input, an empty range list, or too many ranges. throw new RuntimeException('Split rejected its input: ' . $e->getMessage(), previous: $e);} catch (PageLayoutException $e) { // Raised when a range exceeds the source page count, and also by the // PageRange constructor / PageRange::parse() on an invalid or malformed range. throw new RuntimeException( sprintf('Range out of bounds (page %d): %s', $e->getPageNumber(), $e->getConstraint()), previous: $e, );} catch (UnsupportedSourceDocumentException $e) { // Raised fail-closed on an encrypted, signed, or form-bearing source. throw new RuntimeException('Source cannot be split: ' . $e->getMessage(), previous: $e);}
printf( "Source has %d page(s). By-range produced %d doc(s); by-size produced %d doc(s).\n", $byRange->totalPages, $byRange->count(), $bySize->count(),);
foreach ($byRange->documents as $i => $segment) { emitSegment(sprintf('range-%d', $i + 1), $segment);}
emitSegment('tail', $tail);
/** * Validate a segment and write it to the cookbook side-channel directory, * or to the script directory by default. */function emitSegment(string $name, SplitDocument $segment): void{ if (!$segment->isValid()) { throw new RuntimeException(sprintf('Segment "%s" failed its %%PDF header check.', $name)); }
$dir = getenv('NEXTPDF_COOKBOOK_OUTPUT'); $dir = $dir !== false && $dir !== '' ? $dir : __DIR__; $path = sprintf('%s/%s.pdf', rtrim($dir, '/'), $name);
if (file_put_contents($path, $segment->pdfData) === false) { throw new RuntimeException(sprintf('Could not write segment to "%s".', $path)); }
printf("Wrote %s: pages %d-%d, %d bytes.\n", $name, $segment->range->start, $segment->range->end, $segment->sizeBytes);}Sortie standard attendue (la taille en octets dépend du build) :
Source has 7 page(s). By-range produced 2 doc(s); by-size produced 4 doc(s).Wrote range-1: pages 1-3, <n> bytes.Wrote range-2: pages 4-6, <n> bytes.Wrote tail: pages 7-7, <n> bytes.Cas limites et pièges
Section intitulée « Cas limites et pièges »- La source, ce sont des octets, pas un chemin. Chaque méthode prend une chaîne
PDF brute. Lis d’abord le fichier avec
file_get_contents(), ou récupère les octets depuis un stockage objet. Passer un chemin fait échouer l’analyse de la source. - Les numéros de pages commencent à 1 et sont inclusifs.
new PageRange(1, 3)couvre les pages 1, 2 et 3 — trois pages. Un début inférieur à 1 ou une fin antérieure au début lève unePageLayoutExceptiondepuis le constructeur dePageRangelui-même. - Une plage qui dépasse la fin est une erreur, pas un écrêtage. Si la fin d’une
plage dépasse le nombre de pages de la source,
split()lève unePageLayoutException; il ne tronque jamais silencieusement la plage à la dernière page. Inspecte d’abord le nombre de pages si tes plages sont fournies par l’appelant. splitEvery()conserve le reste. La dernière tranche contient les pages restantes, donc un document de 7 pages découpé toutes les 2 pages donne quatre tranches : trois de 2 pages et une de 1.$pagesPerSegmentdoit valoir au moins 1, sinon tu obtiens uneInvalidArgumentException.- Une liste de plages vide est rejetée.
split()avec$ranges === []lève uneInvalidArgumentException. Construis au moins une plage avant de l’appeler. - Les bornes lèvent au lieu de tronquer. Dépasser
maxBytesoumaxRangeslève uneInvalidArgumentException. Le splitter ne traite jamais partiellement une entrée surdimensionnée, donc ajuste les deux bornes pour ta charge de travail. - Les sources chiffrées, signées et porteuses de formulaire échouent à fermé.
Une source chiffrée (elle ne peut pas être copiée sans la clé), une source signée
numériquement (la repagination invaliderait la plage d’octets de la signature) ou
une source portant un formulaire interactif (les widgets d’un champ peuvent se
trouver sur des pages supprimées et devenir orphelins) lèvent une
UnsupportedSourceDocumentException. Le splitter refuse plutôt que d’émettre un document corrompu ou compromis. Découper un document à formulaire est une limitation connue de cette version. UnsupportedSourceDocumentExceptionvit sous l’espace de nomsMerge. Son nom pleinement qualifié estNextPDF\Document\Merge\UnsupportedSourceDocumentException. Ce cheminMergesur une page de découpe n’est pas une erreur de copier/coller : c’est l’unique exception partagée de rejet de document source que les surfaces de fusion et de découpe lèvent toutes deux quand une source ne peut pas être copiée en toute sûreté. Importe-la depuis cet espace de noms.- La sortie est structurellement neuve, pas stable au niveau des octets. Chaque
tranche est un nouveau document doté de son propre catalogue, de son propre arbre
de pages et de sa propre queue. Deux exécutions sur la même entrée sont
structurellement égales, mais sans garantie d’identité au niveau des octets —
d’où le profil de reproductibilité
structural.
Performances
Section intitulée « Performances »La découpe est linéaire par rapport au nombre de pages copiées à travers toutes les
plages. L’analyse de la source et la copie de la fermeture d’objets de chaque plage,
et non la comptabilité propre au splitter, dominent le travail. La source est
gardée en mémoire sous forme de chaîne, et les octets de chaque tranche sont
conservés jusqu’à ce que tu les écrives, donc le pic mémoire suit la taille de la
source plus la plus grande plage que tu produis. Le garde-fou maxBytes borne le
côté source de ce pic. Pour les pipelines à fort volume, fixe maxBytes et
maxRanges aux plus petites valeurs dont ta charge de travail a besoin, afin
qu’une entrée mal formée ou surdimensionnée échoue tôt plutôt que d’épuiser la
mémoire.
Notes de sécurité
Section intitulée « Notes de sécurité »La découpe s’exécute dans le processus ; aucun octet de document ne quitte l’hôte, et aucun appel réseau n’est fait. Traite chaque PDF source comme une entrée non fiable :
- Garde les bornes serrées.
maxBytesetmaxRangessont ta première ligne de défense contre une entrée de déni de service. Pour toute surface qui accepte des téléversements, fixe-les à ton vrai plafond, pas aux valeurs par défaut généreuses. - Trie avant de découper. Une source chiffrée ou signée échoue à fermé, mais tu peux détecter ces conditions plus tôt. Fais passer les entrées non fiables par l’inspecteur du Core d’abord. Voir Analyser et inspecter un PDF pour un balayage borné qui signale le chiffrement, les signatures et les marqueurs de risque avant un traitement plus lourd.
- Ne jamais interpoler une entrée utilisateur dans un chemin. Cette recette écrit dans un répertoire fixe ou le canal latéral du livre de recettes. Dérive les chemins de sortie et les noms de tranches de valeurs contrôlées par le serveur, jamais d’un champ de requête, pour éviter la traversée de chemin.
- Aucun secret dans la sortie. N’écris pas les fichiers de tranches à un emplacement, ou avec un nom, qui expose des identifiants internes à un client qui ne devrait pas les voir.
Conformité
Section intitulée « Conformité »Cette recette ne fait aucune revendication normative de standard qui lui soit
propre. Elle décompose un document via la surface de découpe du Core et effectue un
contrôle de cohérence sur chaque tranche avec le contrôle d’en-tête %PDF de
SplitDocument::isValid() — un contrôle de présence qui confirme que le splitter a
émis un PDF, et non une validation de conformité ou de structure de document. Les
structures d’arbre de pages et de références croisées que PdfSplitter reconstruit
pour chaque tranche sont les structures PDF 2.0 décrites dans la référence
/modules/core/document/ (ISO 32000-2:2020, table de références croisées §7.5.4,
arbre de pages §7.7.3). Pour une lecture structurelle de tout document d’entrée ou
de sortie, y compris la version, le nombre de pages, le chiffrement et les
indicateurs de signature, utilise l’inspecteur du Core documenté dans
Analyser et inspecter un PDF.
Voir aussi
Section intitulée « Voir aussi »- Référence du module Document — la surface complète de découpe, de fusion et de parties de document.
- Fusionner des PDF externes — la recette inverse : composer plusieurs documents en un seul.
- Analyser et inspecter un PDF — trier les entrées non fiables avant de les découper.
- Gestion des erreurs basée sur les exceptions
— la hiérarchie d’exceptions NextPDF derrière
PageLayoutExceptionetUnsupportedSourceDocumentException.