Aller au contenu

Document : DParts, split / merge et extensions tierces

Le module Document travaille sur des documents PDF entiers plutôt que sur le contenu des pages. Il construit la hiérarchie des parties de document à laquelle les flux réglementés rattachent leurs métadonnées. Il découpe un PDF en segments selon des plages de pages. Il fusionne plusieurs PDF en un seul. Il enregistre les extensions développeur dans le catalogue du document.

Fenêtre de terminal
composer require nextpdf/core:^3

Ce module opère au-dessus du contenu des pages. Là où Graphics et Content émettent des opérateurs, Document travaille au niveau structurel — arborescences de pages, catalogue du document et arbre des parties de document.

Une partie de document (DPart) est une partition logique d’un PDF. ISO 32000-2 définit une hiérarchie DPart dont les nœuds portent des métadonnées de partie de document (DPM). Un flux réglementé, par exemple dans les domaines pharmaceutique, juridique ou archivistique, peut associer des métadonnées à une sous-plage de pages plutôt qu’au fichier entier — §14.12. DPart est un nœud immuable readonly : une feuille référence une suite contiguë d’indices de pages ; un nœud intermédiaire regroupe des nœuds DPart enfants en un arbre. DPartRoot est la racine de l’arbre que le Writer sérialise. Les entrées /Start et /End d’un nœud feuille sont des références indirectes vers des objets de page, et non des entiers représentant des indices de page — §14.12. DPart::resolveWithPageObjects() effectue cette résolution à partir d’une table indice-de-page→numéro-d’objet fournie par le writer et renvoie la forme de référence /Start (et, optionnellement, /End). Elle ne se rabat sur la forme entière que dans les chemins de test où la table est indisponible.

PdfMerger et PdfSplitter constituent la surface de composition de documents. PdfMerger combine les objets de page de plusieurs PDF d’entrée, renumérote les objets pour éviter les collisions et reconstruit une arborescence de pages ainsi qu’une table unique de références croisées. L’arborescence de pages produite est un nœud Pages équilibré, avec Kids et Count, et respecte le modèle d’attributs héritables que le PDF définit pour les nœuds d’arborescence de pages — §7.7.3. PdfSplitter fait l’opération inverse : il extrait des plages de pages vers des objets SplitDocument autonomes. PageRange est l’objet valeur que consomment les deux. Il est indexé à partir de 1, valide ses bornes et répond à contains(), count() et toArray().

VendorExtensionRegistry, ExtensionsDictionary et DeveloperExtensionEntry modélisent le dictionnaire des extensions développeur dans le catalogue du document — le mécanisme par lequel un moteur déclare un niveau d’extension tierce au-delà de la spécification de base. Le registre rejette toute réinscription conflictuelle du même préfixe fournisseur avec VendorExtensionRegistryConflictException. CollectionDictionary et CollectionSort modélisent une entrée de catalogue de collection PDF (collection portable / portfolio).

ClasseMéthodes clésRôle
DPartisLeaf(), hasMetadata(), resolveWithPageObjects(), write()Nœud de partie de document immuable (@since 1.12.0)
DPartRootisEmpty(), write()Racine de l’arbre DPart que le Writer sérialise (@since 1.12.0)
PdfMergermerge(array $pdfFiles, int $maxFiles = 100, int $maxTotalBytes = 200_000_000), append()Fusion multi-PDF avec renumérotation des objets (@since 1.9.0)
PdfSplittersplit(), splitEvery(), extractPages()Découpage par plages de pages en SplitDocument (@since 1.9.0)
PageRangecontains(int $page), count(), toArray()Objet valeur de plage de pages indexé à partir de 1
MergeResult / SplitResultisValid(), count(), document(), totalOutputSize()Objets résultat de composition
VendorExtensionRegistryenregistrement d’extensionsRegistre des extensions développeur (@since 2.2.0)
ExtensionsDictionarywithEntry(), entries(), isEmpty(), toPdfDictionary()Constructeur immuable de dictionnaire d’extensions (@since 2.0.0)
CollectionDictionarytoPdfDictionary()Entrée de catalogue de collection portable (@since 2.0.0)

Exécute composer docs:generate-api-php -- --module=Document pour obtenir la table PHPDoc complète.

Découpe un PDF en documents d’une seule page et inspecte le résultat.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Document\PageRange;
use NextPDF\Document\PdfSplitter;
$splitter = new PdfSplitter();
$result = $splitter->splitEvery(file_get_contents('/srv/in/report.pdf'), 1);
foreach (range(0, $result->count() - 1) as $index) {
$segment = $result->document($index);
file_put_contents("/srv/out/page-{$index}.pdf", $segment->pdfData);
}
$singlePage = $splitter->extractPages(
file_get_contents('/srv/in/report.pdf'),
new PageRange(2, 4),
);

Fusionne plusieurs PDF sous un budget d’entrée explicite, puis vérifie la validité avant d’écrire la sortie combinée.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Document\PdfMerger;
use NextPDF\Exception\PageLayoutException;
/** @var list<string> $sources Raw PDF byte strings to combine. */
$sources = array_map(
static fn (string $path): string => file_get_contents($path),
glob('/srv/batch/*.pdf') ?: [],
);
$merger = new PdfMerger();
try {
// Bound the merge: at most 50 files, 100 MB total.
$merged = $merger->merge($sources, maxFiles: 50, maxTotalBytes: 100_000_000);
} catch (PageLayoutException $e) {
throw new \RuntimeException('Merge rejected: empty or invalid input set.', previous: $e);
}
if (!$merged->isValid()) {
throw new \RuntimeException('Merged document failed structural validation.');
}
file_put_contents('/srv/out/combined.pdf', $merged->pdfData);
  • PdfMerger::merge() et PdfSplitter::split() imposent des bornes d’entrée via ResourceGuard. Une entrée qui dépasse le nombre ou la taille maximaux déclenche une exception au lieu d’être tronquée silencieusement. Ajuste maxFiles / maxTotalBytes délibérément pour ta charge de travail.
  • Une liste de fichiers vide ou une liste de plages vide lève PageLayoutException — ce sont des erreurs de configuration, pas des résultats vides.
  • PageRange est indexé à partir de 1 et inclusif. La liste pages d’un DPart feuille contient des indices de pages indexés à partir de 0. Les deux abstractions ne partagent pas la même base d’indexation. Convertis explicitement lorsque tu passes de l’une à l’autre.
  • DPart est readonly. Construire un arbre différent revient à créer de nouveaux nœuds, et non à en muter un. resolveWithPageObjects() ne renvoie la forme de repli en indices entiers que lorsque la table des objets de page est vide. Ne te fie pas à ce chemin pour la sortie de production.
  • VendorExtensionRegistry lève VendorExtensionRegistryConflictException en cas de préfixe fournisseur en double. Enregistre chaque préfixe une seule fois.

Le découpage et la fusion sont linéaires en nombre de pages et dominés par l’analyse et la renumérotation des objets, et non par le suivi interne du module. La charge de référence par défaut tient dans un budget de 1500 ms de temps réel / 64 Mo de pic. Les fusions volumineuses sont contraintes avant tout par le total des octets d’entrée. La garde maxTotalBytes existe pour borner le pic mémoire. Le profil de reproductibilité est structural : un PDF fusionné ou découpé porte une nouvelle queue et un nouvel /ID, si bien que deux exécutions sont structurellement équivalentes, mais pas identiques à l’octet près.

PdfMerger::merge() et PdfSplitter::split() consomment des octets PDF non fiables. Les deux font passer l’entrée par ResourceGuard::assertSize() / assertCount() avant l’analyse, ce qui limite le risque de déni de service par amplification de décompression ou par nombre d’objets. Garde les arguments maxFiles, maxTotalBytes et maxBytes stricts en déploiement plutôt que de t’appuyer sur les valeurs par défaut. Considère chaque PDF d’entrée comme hostile. Exécute la composition par lots dans un worker restreint lorsque les sources sont fournies par l’utilisateur. Consulte le modèle de menace du moteur dans /modules/core/security/ pour la frontière de confiance.

L’arbre DPart que ce module construit suit le modèle de partie de document défini dans ISO 32000-2 §14.12, avec des entrées feuille /Start et /End émises comme références indirectes vers des objets de page selon la même clause. La sortie fusionnée utilise la structure de nœud d’arborescence de pages définie au §7.7.3. Ce sont des faits d’implémentation produits par src/Document/ et exercés par tests/Unit/Document/ (DPartTest, DPartRootTest, DPartPageRefTest, DocumentPdfMergerDeepTest, DocumentPageRangeParseDeepTest). Ils ne constituent pas une affirmation de conformité PDF 2.0 de bout en bout. La conformité du document complet est validée par les suites oracle et golden décrites dans /modules/core/conformance/.