Mesclar documentos PDF em PHP com FPDI e FPDF
Para mesclar PDFs em PHP, importe cada página de origem com o FPDI e crie uma página de saída correspondente com o FPDF. Este exemplo de linha de comando combina PDFs locais na ordem em que você os informa, preserva tamanhos de página variados e falha se alguma entrada solicitada não puder ser importada. Você vai gerar dois PDFs de exemplo, mesclá-los e verificar as três páginas resultantes.
Pré-requisitos
Use PHP 8.3–8.5, Composer 2 e um diretório local com permissão de escrita. Este passo a passo foi testado em um shell Bash no Linux com PHP 8.5.10, Composer 2.10.3, FPDI 2.6.8 e FPDF 1.9.0.
Ative as extensões GD e Zlib na CLI do PHP antes de instalar as dependências. Ambas estão
declaradas no manifesto do Composer do FPDF;
o FPDI também exige a Zlib. Verifique a configuração da CLI com
php --ini e as extensões carregadas por ela com php -m. Um servidor web pode usar
uma configuração de PHP diferente.
Instale os comandos pdfinfo e pdftotext do Poppler se quiser executar as verificações
independentes abaixo. Eles inspecionam o resultado; não são dependências do script de mesclagem em PHP.
Instalar o FPDI e o FPDF
Comece em um diretório pai onde você quer criar um novo projeto 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'
A cadeia && é interrompida se a criação do diretório ou a navegação até ele falhar. Se o
projeto já existir, escolha outro nome; não o exclua para executar a configuração de novo. Continue
somente depois que o Composer concluir com sucesso, mantendo o composer.lock gerado para instalações
reproduzíveis. Salve os scripts a seguir dentro de pdf-merge-demo e execute todos os comandos restantes a
partir desse diretório.
Gerar PDFs de exemplo
Salve isto como samples.php. O script cria a.pdf com uma página A4 em retrato e uma página A3
em paisagem, além de b.pdf com uma página US Letter em retrato. Os rótulos facilitam a verificação
da ordem das páginas. Executar este gerador de exemplos novamente substitui a.pdf e
b.pdf no diretório do 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
Importar PDFs existentes com o FPDI
O FPDI cria um novo documento a partir do conteúdo de páginas importadas. Seu
método setSourceFile()
retorna a contagem de páginas da origem. Para cada página, o loop de mesclagem abaixo chama
importPage(), obtém as dimensões dela com getTemplateSize() e passa essas dimensões e a orientação para
AddPage(). Chamar AddPage() sem esses argumentos usaria, em vez disso, a página padrão A4 em
retrato.
Por padrão, importPage() usa a CropBox, o limite visível da página, e recorre à MediaBox quando
necessário. A saída corresponde a essa área importada, incluindo a rotação da página de origem; ela
não preserva caixas separadas de produção gráfica. Consulte a
implementação dos limites de página do FPDI.
Mesclar qualquer quantidade de PDFs
Salve este script completo como merge.php. mergeMany() retorna o número de páginas de saída em
caso de sucesso e lança uma exceção em caso de falha. Nenhuma entrada é ignorada. A CLI captura as
falhas, escreve um erro na saída de erro padrão e encerra com o status 1.
A política de saída é diferente da do gerador de exemplos descartável: a mesclagem se recusa a
sobrescrever um destino existente, incluindo um arquivo de entrada. Ela conclui a importação e a
serialização de todas as páginas antes de abrir a saída. O
modo de arquivo xb do PHP cria um novo arquivo binário de forma
exclusiva, portanto uma nova execução não consegue truncar um 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') retorna os bytes do PDF como string. Falhas de importação ou
serialização deixam o destino intacto. Uma falha de gravação detectada remove a saída nova e
incompleta. Este script local não é um mecanismo de publicação seguro contra travamentos: uma
interrupção durante a gravação pode deixar um arquivo parcial, por isso os consumidores devem
aguardar um encerramento bem-sucedido.
Mesclar dois PDFs
Execute o script com o destino primeiro, seguido das entradas na ordem desejada:
php merge.php merged.pdf a.pdf b.pdf
Ele deve imprimir Merged 3 pages into merged.pdf e encerrar com o status 0. O mesmo comando aceita mais
caminhos de entrada; coloque entre aspas os caminhos que contêm espaços. Para reordenar estes
exemplos, gere um resultado separado:
php merge.php reversed.pdf b.pdf a.pdf
Os rótulos em reversed.pdf devem ser B1, A1 e A2. Os arquivos de origem
permanecem inalterados.
Verificar o resultado e o comportamento em caso de falha
pdfinfo -f 1 -l 3 merged.pdf &&
pdftotext -layout merged.pdf -
Espere Pages: 3, estas dimensões de página e os rótulos A1, A2 e B1, nesta
ordem. Os pontos de PDF equivalem a 1/72 de polegada; pequenas diferenças de arredondamento são
normais.
| Página | Rótulo | Formato | Largura × altura em pontos |
|---|---|---|---|
| 1 | A1 | A4 retrato | 595,28 × 841,89 |
| 2 | A2 | A3 paisagem | 1190,55 × 841,89 |
| 3 | B1 | US Letter retrato | 612 × 792 |
Uma entrada ausente deve fazer toda a solicitação falhar, mesmo quando há entradas válidas ao redor
dela. Mantenha missing.pdf ausente e use um novo destino:
php merge.php incomplete.pdf a.pdf missing.pdf b.pdf
Isso encerra com o status 1, informa Cannot read input PDF: missing.pdf e não cria
incomplete.pdf. Um arquivo ilegível, um diretório usado como entrada ou um PDF malformado também causam
falha. Informar apenas um caminho de saída falha porque a lista de entradas fica vazia. Repetir o
comando de mesclagem bem-sucedido falha porque merged.pdf existe; seus bytes permanecem inalterados.
Escolha um novo nome de saída para manter as duas versões.
Entender o que o parser gratuito pode preservar
Estas são importações estáticas de páginas. Este exemplo não copia campos de formulário interativos,
anotações, links clicáveis, marcadores, camadas nem ações do documento. O FPDI pode, opcionalmente,
importar anotações de links URI externos com importPage() e seu parâmetro importExternalLinks, mas aqui o valor
padrão é false. Essa opção não restaura formulários, navegação interna nem outras anotações.
O parser gratuito também rejeita PDFs criptografados/protegidos por senha e PDFs que usam streams de referência cruzada compactados ou streams de objetos. Conteúdo de página compactado comum é suportado, como nos exemplos do FPDF. O número de versão do PDF, por si só, não indica se as estruturas não suportadas estão presentes. Essas restrições estão documentadas nas limitações do FPDI.
Se você vir o erro de compactação não suportada, obtenha uma exportação compatível ou avalie o complemento opcional FPDI PDF-Parser da Setasign. Estender o parser não transforma uma importação de conteúdo de página em uma mesclagem que mantém os recursos interativos de todo o documento. Escolha uma ferramenta de mesclagem no nível do documento quando esses recursos forem necessários.
Gerenciar memória para arquivos grandes
O FPDF monta a saída na memória, e este script também mantém a string retornada por Output('S')
enquanto a salva. Por isso, processar uma página por vez não transforma a tarefa em uma mesclagem via
streaming. Meça o pico de memória com entradas representativas antes de escolher um limite de memória
do PHP ou o tamanho da tarefa; não existe um limite confiável baseado apenas na quantidade de PDFs.
Descartar o objeto FPDI após uma tarefa pode liberar os recursos dele, mas a coleta de lixo após a mesclagem não consegue reduzir o pico de memória dessa tarefa. Em um worker de longa duração, libere os objetos de cada tarefa concluída antes de iniciar a próxima.
