Combinar archivos PDF externos o anexar páginas de documentos existentes
En resumen
Sección titulada «En resumen»Hay varios archivos PDF en disco y se necesita un solo PDF. Esta receta combina documentos existentes de principio a fin con la superficie de fusión del Core, NextPDF\Document\PdfMerger. Se pasan cadenas de bytes de PDF en bruto. El merger renumera cada objeto para evitar colisiones, construye un único árbol de páginas y una única tabla de referencias cruzadas, y devuelve un NextPDF\Document\MergeResult que se puede escribir en disco o transmitir a un cliente.
La misma superficie cubre las tres tareas a las que los desarrolladores recurren con más frecuencia:
- Combinar una lista ordenada de PDF en un solo documento.
- Anexar un segundo PDF después de un PDF base.
- Anteponer páginas colocando el documento nuevo primero en el orden de entrada.
La fusión se ejecuta en el mismo proceso, sin navegador headless y sin llamadas de red. Se necesita una instalación del Core (composer require nextpdf/core:^3) y dos o más archivos PDF legibles.
Instalación
Sección titulada «Instalación»composer require nextpdf/core:^3Panorama conceptual
Sección titulada «Panorama conceptual»Un PDF organiza sus páginas en un árbol de páginas cuya raíz es un nodo /Pages, y ubica cada objeto indirecto mediante una tabla de referencias cruzadas. Al combinar dos documentos de origen, sus números de objeto se solapan. Normalmente, ambos archivos contienen un objeto 1 0 obj, un /Catalog y un nodo /Pages. Concatenar sus bytes produciría un archivo corrupto, porque las referencias dejan de apuntar a donde indican los números.
PdfMerger resuelve este problema. Extrae los objetos de página de cada entrada, renumera cada objeto en un único espacio de direccionamiento, reescribe la referencia /Parent de cada página para que apunte a un único nodo /Pages fusionado y emite un solo catálogo, un solo árbol de páginas y un solo tráiler. La salida es un documento estructuralmente nuevo, no una concatenación pegada de los originales.
La regla de orden es sencilla: las páginas aparecen según el orden en que sus archivos de origen aparecen en la lista de entrada. Para anexar, coloca el documento base primero. Para anteponer, coloca el documento nuevo primero. No hay un método separado de anteposición, porque el orden de entrada es el único control necesario.
Superficie de la API
Sección titulada «Superficie de la API»new NextPDF\Document\PdfMerger() expone dos métodos.
merge(list<string> $pdfFiles, int $maxFiles = 100, int $maxTotalBytes = 200_000_000): MergeResultcombina una lista ordenada de cadenas de bytes de PDF en bruto. Los dos parámetros de límite acotan la cantidad de archivos y el tamaño total de entrada. Ambos tienen valores predeterminados seguros para producción y se ajustan según cada carga de trabajo.append(string $basePdf, string $appendPdf): MergeResultes un método de conveniencia que fusiona exactamente dos documentos en orden. Es equivalente amerge([$basePdf, $appendPdf]).
Ambos devuelven un NextPDF\Document\MergeResult, un objeto readonly que contiene $pdfData (los bytes fusionados), $totalPages, $sourceCount, $mergedSize y el método auxiliar isValid() que confirma que la salida comienza con el encabezado %PDF.
Las entradas son cadenas de bytes en bruto, no rutas de archivo. El archivo se lee antes con file_get_contents() (o se extraen los bytes del almacenamiento de objetos). Esto mantiene al merger libre de suposiciones sobre el sistema de archivos y permite fusionar documentos que nunca tocan el disco.
Si se necesita importar una sola página de un PDF externo como un Form XObject reutilizable —por ejemplo, para estampar una página de membrete detrás del contenido generado— se usa el contrato de importación entre paquetes NextPDF\Contracts\ImportedFormObjectInterface, implementado por importadores como nextpdf/artisan. La composición de documentos completos y páginas completas es la superficie de PdfMerger que se documenta aquí.
Ejemplo de código — Inicio rápido
Sección titulada «Ejemplo de código — Inicio rápido»Este ejemplo lee dos archivos y escribe su fusión. Omite el manejo de errores para mostrar la forma de la llamada; el ejemplo de producción que aparece más abajo añade todas las protecciones.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Document\PdfMerger;
$merger = new PdfMerger();
$result = $merger->merge([ file_get_contents(__DIR__ . '/cover.pdf'), file_get_contents(__DIR__ . '/body.pdf'), file_get_contents(__DIR__ . '/appendix.pdf'),]);
file_put_contents(__DIR__ . '/combined.pdf', $result->pdfData);
printf("Merged %d source(s) into %d page(s).\n", $result->sourceCount, $result->totalPages);Ejemplo de código — Producción
Sección titulada «Ejemplo de código — Producción»Este es un programa autónomo. Construye dos documentos pequeños en memoria para poder ejecutarse sin ningún archivo externo, los fusiona, valida el resultado y escribe la salida. Captura las dos excepciones que lanza la superficie de fusión y las vuelve a lanzar con contexto, en lugar de silenciarlas. En un caso real, se reemplazan las entradas en memoria por lecturas de file_get_contents() (o extracciones del almacenamiento de objetos) y se conecta la salida a la respuesta o a la capa de almacenamiento.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Document\MergeResult;use NextPDF\Document\PdfMerger;use NextPDF\Exception\PageLayoutException;use NextPDF\Exception\WriterException;
/** * Build a tiny labelled PDF so the program is self-contained. * * In your own code, replace calls to this helper with reads of the external * PDFs you want to combine, for example file_get_contents($path). */function buildSample(string $label, int $pages): string{ $doc = Document::createStandalone(); $doc->setTitle($label);
for ($page = 1; $page <= $pages; $page++) { $doc->addPage(); $doc->setFont('helvetica', '', 12); $doc->cell(0, 10, sprintf('%s - page %d', $label, $page), newLine: true); }
return $doc->getPdfData();}
// Validate the input set before touching the merger. An empty set is a// configuration error, not an empty success./** @var list<string> $sources Raw PDF byte strings, in output order. */$sources = [ buildSample('Cover', 1), // first in the list -> first in the output (prepend position) buildSample('Body', 2), buildSample('Appendix', 1), // last in the list -> appended after the body];
if ($sources === []) { throw new RuntimeException('No source PDFs supplied to merge.');}
$merger = new PdfMerger();
try { // Bound the merge deliberately: at most 50 files, 100 MB total input. $result = $merger->merge($sources, maxFiles: 50, maxTotalBytes: 100_000_000);} catch (PageLayoutException $e) { // Raised when the list is empty or an input does not begin with %PDF. throw new RuntimeException( sprintf('Merge rejected an input: %s', $e->getConstraint()), previous: $e, );} catch (WriterException $e) { // Raised when the total input size exceeds the configured byte cap. throw new RuntimeException( sprintf('Merge exceeded its size budget at stage "%s".', $e->getWriterState()), previous: $e, );}
if (!$result->isValid()) { throw new RuntimeException('Merged output failed its structural header check.');}
emitResult($result);
/** * Write the merged document to the cookbook side-channel, or to a default file. */function emitResult(MergeResult $result): void{ printf( "Merged %d source(s) into %d page(s), %d bytes.\n", $result->sourceCount, $result->totalPages, $result->mergedSize, );
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT'); $path = $out !== false && $out !== '' ? $out : __DIR__ . '/combined.pdf';
if (file_put_contents($path, $result->pdfData) === false) { throw new RuntimeException(sprintf('Could not write merged PDF to "%s".', $path)); }}STDOUT esperado (el total de páginas es la suma de las páginas de los orígenes, y el tamaño en bytes depende de la generación):
Merged 3 source(s) into 4 page(s), <n> bytes.Casos límite y trampas
Sección titulada «Casos límite y trampas»- Las entradas son bytes, no rutas.
merge()toma cadenas de PDF en bruto. Lee primero el archivo confile_get_contents(). Pasar una ruta como cadena hace que la entrada no supere la verificación del encabezado%PDFy lanzaPageLayoutException. - El orden de entrada es el orden de salida. Las páginas quedan en el orden en que sus archivos de origen aparecen en la lista. No hay método de anteposición: coloca el documento nuevo primero para anteponerlo y último para anexarlo.
- La lista vacía es un error. Un
$pdfFilesvacío lanzaPageLayoutException, no un resultado vacío. Valida el conjunto antes de llamar. - Cada entrada se valida de antemano. Cada elemento debe ser no vacío y comenzar con
%PDF. La primera entrada que falla lanzaPageLayoutExceptioncon la restricción violada, y no se fusiona nada. - Los límites lanzan excepciones en lugar de truncar. Superar
maxFileslanza una excepción a través del guardián de recursos interno, y superarmaxTotalByteslanzaWriterException. El merger nunca descarta archivos ni recorta bytes de forma silenciosa, así que ambos límites deben ajustarse a la carga de trabajo. - La salida es estructuralmente nueva, no estable a nivel de bytes. El documento fusionado lleva un nuevo catálogo, árbol de páginas y tráiler. Dos ejecuciones sobre las mismas entradas son estructuralmente iguales, pero no se garantiza que sean idénticas byte a byte, y por eso esta receta declara un perfil de reproducibilidad
structural. - Anotaciones a nivel de página y recursos compartidos. La fusión compone los objetos de página en un solo árbol. Las estructuras a nivel de documento que residen fuera de los objetos de página en un archivo de origen no se trasladan. Cuando se necesita una sola página importada como gráfico reutilizable con sus recursos, se usa la vía
ImportedFormObjectInterfacea través de un importador comonextpdf/artisan.
Rendimiento
Sección titulada «Rendimiento»La fusión es lineal respecto del total de páginas y está dominada por el análisis y la renumeración de objetos, no por la propia gestión interna del merger. La memoria máxima crece con el total de bytes de entrada, porque cada origen se mantiene en memoria como una cadena mientras se ensambla la salida. El guardián maxTotalBytes mantiene ese pico acotado. Para canalizaciones de alto volumen, conviene fijar maxFiles y maxTotalBytes en los valores más pequeños que requiera la carga de trabajo, para que un lote malformado o sobredimensionado falle rápido en lugar de agotar la memoria. Una fusión pequeña típica se mantiene dentro de un presupuesto de 1500 ms de tiempo de reloj y un pico de 64 MB.
Notas de seguridad
Sección titulada «Notas de seguridad»La fusión se ejecuta en el mismo proceso; ningún byte del documento sale del host y no se realiza ninguna llamada de red. Cada PDF externo debe tratarse como entrada no confiable:
- Mantener los límites ajustados.
maxFilesymaxTotalBytesson la primera línea de defensa contra entradas de denegación de servicio. Deben fijarse en el techo real, no en los valores predeterminados generosos, para cualquier superficie que acepte cargas. - Validar antes de confiar. Una fusión exitosa significa que los bytes se combinaron, no que las entradas sean seguras. Las entradas no confiables deben pasar primero por el inspector del Core. Consultar Analizar e inspeccionar un PDF para un escaneo de triaje acotado que señala cifrado, firmas y marcadores de riesgo antes de un procesamiento más pesado.
- No interpolar nunca entrada de usuario en una ruta. Esta receta escribe en una ruta fija o en el canal lateral del cookbook. Las rutas de salida deben derivarse de valores controlados por el servidor, nunca de un campo de la solicitud, para evitar path traversal.
- No incluir secretos en el documento. No se deben incrustar credenciales, tokens ni identificadores internos en un documento fusionado que se devuelve a un cliente.
Conformidad
Sección titulada «Conformidad»Esta receta no hace ninguna afirmación normativa de estándares por sí misma. Compone documentos existentes a través de la superficie de fusión del Core y valida el resultado con la verificación de encabezado MergeResult::isValid(). El modelo de árbol de páginas que PdfMerger reconstruye es la estructura de árbol de páginas de PDF 2.0 descrita en la referencia /modules/core/document/. Para una lectura estructural de cualquier documento de entrada o salida —versión, cantidad de páginas, banderas de cifrado y firma— usa el inspector del Core documentado en Analizar e inspeccionar un PDF.
Consulta también
Sección titulada «Consulta también»- Referencia del módulo Document — la superficie completa de división, fusión y partes de documento.
- Analizar e inspeccionar un PDF — clasifica las entradas no confiables antes de fusionarlas.
- Manejo de errores basado en excepciones — la jerarquía de excepciones de NextPDF detrás de
PageLayoutExceptionyWriterException. - Construir un documento de varias páginas — crea las páginas que luego combinas.