Combinar documentos PDF en PHP con FPDI y FPDF
Para combinar PDF en PHP, importa cada página de origen con FPDI y crea una página de salida correspondiente con FPDF. Este ejemplo de línea de comandos combina PDF locales en el orden que indiques, conserva los distintos tamaños de página y falla si no se puede importar alguna entrada solicitada. Generarás dos PDF de ejemplo, los combinarás y comprobarás las tres páginas resultantes.
Requisitos previos
Usa PHP 8.3–8.5, Composer 2 y un directorio local con permisos de escritura. Este tutorial se probó en un shell Bash en Linux con PHP 8.5.10, Composer 2.10.3, FPDI 2.6.8 y FPDF 1.9.0.
Habilita las extensiones GD y Zlib en la CLI de PHP antes de instalar las dependencias. Ambas
están declaradas en el manifiesto de Composer de FPDF;
FPDI también requiere Zlib. Comprueba la configuración de la CLI
con php --ini y sus extensiones cargadas con php -m.
Un servidor web puede usar una configuración de PHP diferente.
Instala los comandos pdfinfo y pdftotext de Poppler
si quieres ejecutar las comprobaciones independientes que se muestran más adelante.
Estos inspeccionan el resultado; no son dependencias del script PHP de combinación.
Instala FPDI y FPDF
Empieza en un directorio padre donde quieras crear un nuevo proyecto 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 cadena && se detiene si falla la creación del directorio o el cambio
a este. Si el proyecto ya existe, elige otro nombre; no lo elimines para repetir la configuración.
Continúa solo cuando Composer termine correctamente y conserva el archivo
composer.lock generado para poder reproducir las instalaciones. Guarda los
siguientes scripts dentro de pdf-merge-demo y ejecuta todos los comandos restantes
desde ese directorio.
Genera PDF de ejemplo
Guarda este código como samples.php. Crea a.pdf con una
página A4 vertical y una A3 horizontal, además de b.pdf con una página
US Letter vertical. Las etiquetas facilitan la comprobación del orden de las páginas. Al ejecutar
de nuevo este generador de ejemplo, se reemplazan a.pdf y
b.pdf en el directorio del tutorial.
<?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
Importa PDF existentes con FPDI
FPDI crea un documento nuevo a partir del contenido de las páginas importadas. Su
método setSourceFile()
devuelve el número de páginas de origen. Para cada página, el bucle de combinación siguiente llama a
importPage(), obtiene sus dimensiones con getTemplateSize() y pasa
esas dimensiones y la orientación a AddPage(). Si se llama a
AddPage() sin esos argumentos, se usa en su lugar la página A4 vertical
predeterminada.
De forma predeterminada, importPage() usa CropBox, el límite visible de la
página, y recurre a MediaBox cuando es necesario. La salida coincide con esa área importada,
incluida la rotación de la página de origen; no conserva los recuadros independientes para la
producción de impresión. Consulta la
implementación de los límites de página de FPDI.
Combina cualquier cantidad de PDF
Guarda este script completo como merge.php. mergeMany()
devuelve el número de páginas de salida si la operación tiene éxito y lanza una excepción si falla.
Nunca omite una entrada. La CLI captura los fallos, escribe un error en la salida de error estándar
y termina con el estado 1.
La política de salida difiere de la del generador de muestras desechables: la combinación se niega
a sobrescribir un destino existente, incluido un archivo de entrada. Termina de importar y
serializar todas las páginas antes de abrir la salida. El
modo de archivo xb de PHP
crea un archivo binario nuevo de forma exclusiva, por lo que volver a ejecutarlo no puede truncar
un resultado anterior.
<?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') devuelve los bytes del PDF como una
cadena. Los fallos de importación o serialización dejan el destino intacto. Si se detecta un fallo
de escritura, se elimina la salida nueva e incompleta. Este script local no es un mecanismo de
publicación a prueba de interrupciones: una interrupción durante la escritura puede dejar un
archivo parcial, por lo que los consumidores deben esperar a que termine correctamente.
Combina dos PDF
Ejecuta el script con el destino primero, seguido de las entradas en el orden deseado:
php merge.php merged.pdf a.pdf b.pdf
Debería imprimir Merged 3 pages into merged.pdf y terminar con el estado
0. Usa el mismo comando para añadir más rutas de entrada; encierra
entre comillas las rutas que contengan espacios. Para cambiar el orden de estas muestras, escribe
un resultado aparte:
php merge.php reversed.pdf b.pdf a.pdf
Las etiquetas de reversed.pdf deberían ser B1,
A1 y A2. Los archivos de origen no cambian.
Comprueba el resultado y el comportamiento ante fallos
pdfinfo -f 1 -l 3 merged.pdf &&
pdftotext -layout merged.pdf -
El resultado esperado es Pages: 3, estas dimensiones de página y las etiquetas
A1, A2 y B1
en ese orden. Los puntos PDF equivalen a 1/72 de pulgada; las pequeñas diferencias de redondeo son
normales.
| Página | Etiqueta | Formato | Ancho × alto en puntos |
|---|---|---|---|
| 1 | A1 | A4 vertical | 595,28 × 841,89 |
| 2 | A2 | A3 horizontal | 1190,55 × 841,89 |
| 3 | B1 | US Letter vertical | 612 × 792 |
Una entrada ausente debe hacer fallar toda la solicitud, incluso si hay entradas válidas antes y
después de ella. Asegúrate de que missing.pdf siga sin existir y usa un destino
nuevo:
php merge.php incomplete.pdf a.pdf missing.pdf b.pdf
Esto termina con el estado 1, informa de
Cannot read input PDF: missing.pdf y no crea incomplete.pdf. Un archivo ilegible,
un directorio usado como entrada o un PDF mal formado también provocan un fallo. Si solo se indica
una ruta de salida, la operación falla porque la lista de entradas está vacía. Repetir el comando
de combinación que terminó correctamente falla porque merged.pdf ya existe;
sus bytes no cambian. Elige un nombre de salida nuevo para conservar ambas versiones.
Conoce lo que puede conservar el analizador gratuito
Estas son importaciones de páginas estáticas. Este ejemplo no copia campos de formulario
interactivos, anotaciones, enlaces en los que se pueda hacer clic, marcadores, capas ni acciones del
documento. FPDI puede importar opcionalmente anotaciones de enlaces URI externos con
importPage(), mediante su parámetro importExternalLinks, pero aquí
su valor predeterminado es false. Esa opción no restaura formularios,
navegación interna ni otras anotaciones.
El analizador gratuito también rechaza los PDF cifrados o protegidos con contraseña y los PDF que usan flujos comprimidos de referencias cruzadas o flujos de objetos. Sí admite el contenido comprimido normal de las páginas, como el de los ejemplos de FPDF. El número de versión de un PDF por sí solo no permite saber si contiene estructuras no compatibles. Estas restricciones están documentadas en las limitaciones de FPDI.
Si aparece el error de compresión no compatible, obtén una exportación compatible o evalúa el complemento opcional FPDI PDF-Parser de Setasign. Ampliar el analizador no convierte una importación de contenido de páginas en una combinación que conserve todas las funciones interactivas del documento. Elige una herramienta de combinación a nivel de documento cuando necesites esas funciones.
Gestiona la memoria para archivos grandes
FPDF construye la salida en memoria, y este script también retiene la cadena devuelta por
Output('S') mientras la guarda. Por lo tanto, procesar una página a la vez no
convierte la tarea en una combinación en streaming. Mide el consumo máximo de memoria con entradas
representativas antes de elegir un límite de memoria de PHP o un tamaño de tarea; no existe un
límite fiable basado únicamente en el número de PDF.
Liberar el objeto FPDI después de una tarea puede liberar sus recursos, pero la recolección de basura después de la combinación no puede reducir el consumo máximo de memoria de esa tarea. En un proceso de trabajo de larga duración, libera los objetos de cada tarea completada antes de iniciar la siguiente.
