Pular para o conteúdo

Mescle PDFs externos ou anexe páginas de documentos já existentes

Você tem vários arquivos PDF no disco e precisa gerar um único PDF. Esta receita combina documentos existentes de ponta a ponta com a interface de mesclagem do Core, NextPDF\Document\PdfMerger. Você passa strings de bytes brutos de PDF. O mesclador renumera todos os objetos para evitar colisões, constrói uma única árvore de páginas e uma única tabela de referências cruzadas, e retorna um NextPDF\Document\MergeResult que você pode gravar no disco ou transmitir para um cliente.

A mesma interface cobre as três tarefas de que você provavelmente mais precisa:

  • Mescle uma lista ordenada de PDFs em um único documento.
  • Anexe um segundo PDF após um PDF base.
  • Anteponha páginas colocando o novo documento em primeiro lugar na ordem de entrada.

A mesclagem é executada no próprio processo, sem um navegador headless ou uma chamada de rede. Você precisa do Core instalado (composer require nextpdf/core:^3) e de dois ou mais arquivos PDF legíveis.

Terminal window
composer require nextpdf/core:^3

Um PDF organiza as páginas em uma árvore de páginas cuja raiz é um nó /Pages e localiza cada objeto indireto por meio de uma tabela de referências cruzadas. Quando você combina dois documentos de origem, os números dos objetos se sobrepõem. Quase sempre, os dois arquivos contêm um objeto 1 0 obj, um /Catalog e um nó /Pages. Se você apenas concatenar os bytes, produz um arquivo corrompido, porque as referências deixam de apontar para os objetos que identificam.

PdfMerger resolve essa sobreposição. Ele extrai os objetos de página de cada entrada, renumera todos os objetos em um único espaço de endereçamento, reescreve a referência /Parent de cada página para apontar para um único nó /Pages mesclado e emite um catálogo, uma árvore de páginas e um trailer. A saída é um documento estruturalmente novo, não uma concatenação improvisada.

A regra de ordenação é simples: as páginas aparecem na mesma ordem dos arquivos de origem na lista de entrada. Para anexar, coloque o documento base em primeiro lugar. Para antepor, coloque o novo documento em primeiro lugar. Não há um método de anteposição separado, porque a ordem de entrada é o único controle de que você precisa.

new NextPDF\Document\PdfMerger() expõe dois métodos.

  • merge(list<string> $pdfFiles, int $maxFiles = 100, int $maxTotalBytes = 200_000_000): MergeResult combina uma lista ordenada de strings de bytes brutos de PDF. Os dois parâmetros de limite restringem a contagem de arquivos e o tamanho total da entrada. Ambos usam, por padrão, valores seguros para produção; ajuste-os para cada carga de trabalho.
  • append(string $basePdf, string $appendPdf): MergeResult é um wrapper de conveniência que mescla exatamente dois documentos em ordem. É equivalente a merge([$basePdf, $appendPdf]).

Ambos os métodos retornam um NextPDF\Document\MergeResult, um objeto readonly que carrega $pdfData (os bytes mesclados), $totalPages, $sourceCount, $mergedSize e o auxiliar isValid() que confirma que a saída começa com o cabeçalho %PDF.

As entradas são strings de bytes brutos, não caminhos de arquivo. Leia o arquivo você mesmo com file_get_contents() ou obtenha os bytes de um armazenamento de objetos. Isso mantém o mesclador livre de premissas sobre o sistema de arquivos e permite que você mescle documentos que nunca tocam o disco.

Se você precisar importar uma única página de um PDF externo como um Form XObject reutilizável, por exemplo, para aplicar uma página de papel timbrado atrás do conteúdo gerado, use o contrato de importador entre pacotes NextPDF\Contracts\ImportedFormObjectInterface, implementado por importadores como nextpdf/artisan. Para compor documentos inteiros e páginas inteiras, use a interface PdfMerger documentada aqui.

Este exemplo lê dois arquivos e grava a saída mesclada. Ele omite o tratamento de erros para mostrar o formato da chamada; o exemplo de produção abaixo adiciona todas as proteções necessárias.

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

Este programa autossuficiente constrói dois pequenos documentos na memória, por isso é executado sem nenhum arquivo externo. Ele os mescla, valida o resultado e grava a saída. Ele captura as duas exceções que a interface de mesclagem lança e relança cada uma com contexto, em vez de engoli-la. Substitua as entradas em memória pelas suas próprias leituras de file_get_contents() ou buscas em armazenamento de objetos, e conecte a saída à camada de resposta ou de armazenamento.

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

Saída padrão esperada (o total de páginas é a soma das contagens de páginas das origens, e o tamanho em bytes depende da construção):

Merged 3 source(s) into 4 page(s), <n> bytes.
  • As entradas são bytes, não caminhos. merge() recebe strings brutas de PDF. Leia o arquivo primeiro com file_get_contents(). Passar uma string de caminho faz a entrada falhar na verificação do cabeçalho %PDF e lança PageLayoutException.
  • A ordem é a ordem de saída. As páginas ficam na ordem em que seus arquivos de origem aparecem na lista. Não há método de anteposição: coloque o novo documento em primeiro lugar para antepor ou por último para anexar.
  • Uma lista vazia é um erro. Um $pdfFiles vazio lança PageLayoutException, não um resultado vazio. Valide o conjunto antes de chamar o mesclador.
  • Toda entrada é validada de antemão. Cada entrada deve ser não vazia e começar com %PDF. A primeira entrada que falha lança PageLayoutException com a restrição violada, e nada é mesclado.
  • Os limites lançam exceção em vez de truncar. Exceder maxFiles lança por meio da proteção interna de recursos, e exceder maxTotalBytes lança WriterException. O mesclador nunca descarta arquivos nem corta bytes silenciosamente, então ajuste ambos os limites à sua carga de trabalho.
  • A saída é estruturalmente nova, não estável em bytes. O documento mesclado carrega um novo catálogo, uma árvore de páginas e um trailer. Duas execuções com as mesmas entradas são estruturalmente iguais, mas não há garantia de que sejam idênticas byte a byte. É por isso que esta receita declara um perfil de reprodutibilidade structural.
  • Anotações no nível da página e recursos compartilhados. A mesclagem compõe os objetos de página em uma única árvore. Estruturas no nível do documento que ficam fora dos objetos de página em um arquivo de origem não são transferidas. Quando você precisar importar uma única página como um gráfico reutilizável com seus recursos, use o caminho ImportedFormObjectInterface por meio de um importador como nextpdf/artisan.

A mesclagem é linear em relação à contagem total de páginas. A análise e a renumeração de objetos, e não a contabilidade interna do mesclador, dominam o trabalho. O pico de memória acompanha o total de bytes de entrada, porque cada origem é mantida na memória como uma string enquanto a saída é montada. A proteção maxTotalBytes mantém esse pico limitado. Para pipelines de alto volume, defina maxFiles e maxTotalBytes com os menores valores de que sua carga de trabalho precisa, para que um lote malformado ou grande demais falhe rapidamente em vez de esgotar a memória. Uma mesclagem pequena típica cabe em um orçamento de 1500 ms de tempo de parede e 64 MB de pico.

A mesclagem é executada no processo; nenhum byte do documento sai do host, e nenhuma chamada de rede é feita. Trate todo PDF externo como entrada não confiável:

  • Mantenha os limites apertados. maxFiles e maxTotalBytes são a sua primeira linha de defesa contra entradas de negação de serviço. Para qualquer interface que aceite uploads, defina-os no seu teto real, não nos padrões generosos.
  • Valide antes de confiar. Uma mesclagem bem-sucedida significa que os bytes foram combinados, não que as entradas sejam seguras. Passe primeiro as entradas não confiáveis pelo inspetor do Core. Veja Analise e inspecione um PDF para uma varredura de triagem limitada que sinaliza criptografia, assinaturas e marcadores de risco antes de um processamento mais pesado.
  • Nunca interpole entrada do usuário em um caminho. Esta receita grava em um caminho fixo ou no canal lateral do cookbook. Derive os caminhos de saída de valores controlados pelo servidor, nunca de um campo da requisição, para evitar travessia de caminho.
  • Sem segredos no documento. Não incorpore credenciais, tokens ou identificadores internos em um documento mesclado que você retorna a um cliente.

Esta receita não faz nenhuma afirmação normativa de padrões por conta própria. Ela compõe documentos existentes por meio da interface de mesclagem do Core e valida o resultado com a verificação de cabeçalho MergeResult::isValid(). O modelo de árvore de páginas que o PdfMerger reconstrói é a estrutura de árvore de páginas do PDF 2.0 descrita na referência /modules/core/document/. Para uma leitura estrutural de qualquer documento de entrada ou saída, incluindo versão, contagem de páginas, criptografia e sinalizadores de assinatura, use o inspetor do Core documentado em Analise e inspecione um PDF.