Ir al contenido

Document: DParts, split / merge y extensiones de proveedor

El módulo Document trabaja sobre documentos PDF completos, en vez de sobre el contenido de las páginas. Permite construir la jerarquía de partes del documento a la que los flujos de trabajo regulados adjuntan metadatos, dividir un PDF en segmentos por rangos de páginas, combinar varios PDF en uno solo y registrar extensiones de desarrollador en el catálogo del documento.

Ventana de terminal
composer require nextpdf/core:^3

Este módulo se sitúa por encima del contenido de las páginas. Mientras Graphics y Content emiten operadores, Document trabaja a nivel estructural: árboles de páginas, catálogo del documento y árbol de partes del documento.

Una parte del documento (DPart) es una partición lógica de un PDF. ISO 32000-2 define una jerarquía DPart cuyos nodos contienen metadatos de partes del documento (DPM). Un flujo de trabajo regulado (por ejemplo, farmacéutico, jurídico o de archivo) puede asociar metadatos a un subrango de páginas en lugar de a todo el archivo — §14.12. DPart es un nodo inmutable readonly: una hoja referencia una secuencia contigua de índices de página; un nodo intermedio agrupa nodos DPart hijos en un árbol. DPartRoot es la raíz del árbol que serializa el Writer. Las entradas /Start y /End de un nodo hoja son referencias indirectas a objetos de página, no enteros de índice de página — §14.12. DPart::resolveWithPageObjects() realiza esa resolución frente a un mapa índice-de-página→número-de-objeto suministrado por el escritor y devuelve la forma de referencia de /Start (y, opcionalmente, de /End). Solo recurre a la forma entera en rutas de prueba donde el mapa no está disponible.

PdfMerger y PdfSplitter constituyen la superficie para componer documentos. PdfMerger combina objetos de página de varios PDF de entrada, renumera los objetos para evitar colisiones y reconstruye un único árbol de páginas y una tabla de referencias cruzadas. El árbol de páginas resultante es un nodo Pages equilibrado, con Kids y Count, y el modelo de atributos heredables que PDF define para los nodos del árbol de páginas — §7.7.3. PdfSplitter realiza la operación inversa: extrae rangos de páginas en objetos SplitDocument independientes. PageRange es el objeto de valor que consumen ambos. Usa base 1, valida sus límites y responde a contains(), count() y toArray().

VendorExtensionRegistry, ExtensionsDictionary y DeveloperExtensionEntry modelan el diccionario de extensiones de desarrollador en el catálogo del documento: el mecanismo por el cual un motor declara un nivel de extensión de proveedor más allá de la especificación base. El registro rechaza un nuevo registro conflictivo del mismo prefijo de proveedor con VendorExtensionRegistryConflictException. CollectionDictionary y CollectionSort modelan una entrada de colección PDF en el catálogo (colección portátil / portafolio).

ClaseMétodos claveFunción
DPartisLeaf(), hasMetadata(), resolveWithPageObjects(), write()Nodo de parte del documento inmutable (@since 1.12.0)
DPartRootisEmpty(), write()Raíz del árbol DPart que el Writer serializa (@since 1.12.0)
PdfMergermerge(array $pdfFiles, int $maxFiles = 100, int $maxTotalBytes = 200_000_000), append()Combinación de varios PDF con renumeración de objetos (@since 1.9.0)
PdfSplittersplit(), splitEvery(), extractPages()División por rangos de páginas en SplitDocument (@since 1.9.0)
PageRangecontains(int $page), count(), toArray()Objeto de valor de rango de páginas basado en 1
MergeResult / SplitResultisValid(), count(), document(), totalOutputSize()Objetos de resultado de composición
VendorExtensionRegistryregistro de extensionesRegistro de extensiones de desarrollador (@since 2.2.0)
ExtensionsDictionarywithEntry(), entries(), isEmpty(), toPdfDictionary()Constructor inmutable del diccionario de extensiones (@since 2.0.0)
CollectionDictionarytoPdfDictionary()Entrada de catálogo de colección portátil (@since 2.0.0)

Ejecutar composer docs:generate-api-php -- --module=Document para obtener la tabla completa de PHPDoc.

Dividir un PDF en documentos de una sola página e inspeccionar el resultado.

<?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),
);

Combinar varios PDF con un presupuesto de entrada explícito y, a continuación, comprobar la validez antes de escribir la salida combinada.

<?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() y PdfSplitter::split() aplican límites a la entrada mediante ResourceGuard. Una entrada que excede el recuento o el tamaño provoca un error en lugar de truncarse silenciosamente. Ajustar maxFiles / maxTotalBytes deliberadamente según la carga de trabajo.
  • Una lista de archivos vacía o una lista de rangos vacía provoca PageLayoutException: son errores de configuración, no resultados vacíos.
  • PageRange usa base 1 y es inclusivo. La lista pages de un DPart hoja contiene índices de página basados en 0. Las dos abstracciones no comparten la misma base de índice. Convertir los índices explícitamente al pasar de una a otra.
  • DPart es readonly. Construir un árbol diferente significa crear nodos nuevos, no mutar uno existente. resolveWithPageObjects() devuelve la forma alternativa de índice entero solo cuando el mapa de objetos de página está vacío. No depender de esa ruta en la salida de producción.
  • VendorExtensionRegistry provoca VendorExtensionRegistryConflictException ante un prefijo de proveedor duplicado. Registrar cada prefijo una sola vez.

La división y la combinación son lineales con respecto al número de páginas y están dominadas por el análisis y la renumeración de objetos, no por la gestión interna del módulo. La carga de trabajo de referencia predeterminada se mantiene dentro de un presupuesto de 1500 ms de tiempo real / 64 MB de pico. Las combinaciones grandes están limitadas principalmente por el total de bytes de entrada. La protección maxTotalBytes existe para mantener acotado el pico de memoria. El perfil de reproducibilidad es structural: un PDF combinado o dividido lleva un tráiler y un /ID nuevos, por lo que dos ejecuciones son estructuralmente iguales, pero no idénticas byte a byte.

PdfMerger::merge() y PdfSplitter::split() procesan bytes de PDF no confiables. Ambos pasan la entrada por ResourceGuard::assertSize() / assertCount() antes de analizarla, lo que acota el riesgo de denegación de servicio por amplificación de descompresión o por recuento de objetos. Mantener ajustados los argumentos maxFiles, maxTotalBytes y maxBytes según el despliegue, en lugar de depender de los valores predeterminados. Tratar cada PDF de entrada como hostil. Ejecutar la composición por lotes en un trabajador restringido cuando las fuentes provengan del usuario. Consultar el modelo de amenazas del motor en /modules/core/security/ para conocer el límite de confianza.

El árbol DPart que construye este módulo sigue el modelo de partes del documento de ISO 32000-2 §14.12, con las entradas hoja /Start y /End emitidas como referencias indirectas a objetos de página según la misma cláusula. La salida combinada utiliza la estructura de nodo del árbol de páginas definida en §7.7.3. Estos son hechos de implementación producidos por src/Document/ y ejercitados por tests/Unit/Document/ (DPartTest, DPartRootTest, DPartPageRefTest, DocumentPdfMergerDeepTest, DocumentPageRangeParseDeepTest). No constituyen una declaración de conformidad integral con PDF 2.0. Las suites de oráculo y de referencia descritas en /modules/core/conformance/ validan la conformidad del documento completo.