Pular para o conteúdo
getnextpdf.com

Divida um PDF e extraia intervalos de páginas

Você tem um PDF e precisa de vários. Esta receita recorta um único documento em vários arquivos com a superfície de divisão da Core, NextPDF\Document\PdfSplitter. Você passa a origem como uma string de bytes brutos de PDF e descreve quais páginas quer. O splitter analisa a origem por meio do grafo de objetos, copia os objetos alcançáveis de cada intervalo solicitado para um documento novo e renumerado, com sua própria árvore de páginas e tabela de referências cruzadas, e devolve PDFs estruturalmente completos que carregam em um leitor em conformidade.

Esta é a inversa da receita de mesclagem: a mesclagem compõe muitos documentos em um, a divisão decompõe um documento em muitos. A mesma superfície cobre as três tarefas de que você mais precisa:

  • Dividir por intervalos — produz um documento de saída por intervalo de páginas que você nomeia.
  • Dividir a cada N páginas — corta um arquivo longo em segmentos de tamanho fixo.
  • Extrair um intervalo — puxa um único intervalo contíguo de páginas para um documento.

A divisão roda no processo, sem um navegador headless ou uma chamada de rede. Você precisa da Core instalada (composer require nextpdf/core:^3) e de um PDF legível.

Terminal window
composer require nextpdf/core:^3

Um PDF localiza suas páginas por meio de uma árvore de páginas enraizada em um nó /Pages, e alcança cada objeto indireto por meio de seus dados de referência cruzada (uma tabela ou um stream). Você não pode extrair páginas fatiando bytes: uma única página referencia fontes, imagens e dicionários de recursos compartilhados que vivem em outro lugar do arquivo, e os offsets de referência cruzada deixariam de ser válidos.

O PdfSplitter faz o trabalho de verdade. Para cada intervalo, ele percorre o grafo de objetos a partir dos objetos de página solicitados, coleta o fecho de objetos alcançáveis, renumera esses objetos para um novo espaço de endereços, reconstrói um documento com uma única árvore de páginas e emite uma tabela de referências cruzadas real conforme a estrutura do PDF 2.0 (ISO 32000-2:2020, tabela de referências cruzadas §7.5.4, árvore de páginas §7.7.3). Cada saída é um documento autocontido, não um fragmento.

Os números de página são baseados em 1 e inclusivos. Um intervalo é um value object NextPDF\Document\PageRange: new PageRange(2, 5) significa as páginas 2 a 5. O construtor valida suas próprias invariantes — ele rejeita um início abaixo de 1 ou um fim antes do início lançando NextPDF\Exception\PageLayoutException — então um intervalo impossível falha na construção, não lá no fundo do splitter. PageRange::parse() e PageRange::all() lançam a mesma PageLayoutException em uma especificação malformada ou um total de páginas não positivo.

new NextPDF\Document\PdfSplitter() expõe três métodos. Todos recebem a origem como uma string de bytes brutos de PDF, nunca um caminho.

  • split(string $pdfData, array $ranges, int $maxBytes = 100_000_000, int $maxRanges = 1000): SplitResult produz um documento de saída por PageRange em $ranges, em ordem. Os dois parâmetros de limite restringem o tamanho da entrada e a contagem de intervalos.
  • splitEvery(string $pdfData, int $pagesPerSegment): SplitResult corta o documento em segmentos de tamanho fixo de $pagesPerSegment páginas cada; o último segmento guarda o restante.
  • extractPages(string $pdfData, PageRange $range): SplitDocument extrai um único intervalo e retorna esse documento diretamente.

split() e splitEvery() retornam um NextPDF\Document\SplitResult, um objeto readonly que carrega $documents (uma lista de segmentos), $totalPages (páginas na origem) e $sourceSize. Ele oferece count(), document(int $index) para buscar um segmento por índice baseado em zero, e totalOutputSize().

Cada segmento, e o valor de retorno de extractPages(), é um NextPDF\Document\SplitDocument: um objeto readonly que expõe $pdfData (os bytes do segmento), $range, $pageCount, $sizeBytes e o auxiliar isValid(). O isValid() é uma verificação estreita de sanidade do cabeçalho %PDF — ele retorna true quando os bytes do segmento começam com %PDF — não uma validação de estrutura de documento ou de conformidade; ele confirma que o splitter produziu um PDF, não que o arquivo está totalmente em conformidade.

Você constrói um PageRange diretamente com new PageRange($start, $end), ou analisa uma especificação legível por humanos com PageRange::parse('1-3,5,7-10'), que retorna uma list<PageRange> pronta para passar a split(). PageRange::all($totalPages) retorna um único intervalo cobrindo o documento inteiro.

Este exemplo lê um arquivo e o divide em dois documentos: páginas 1 a 3 e páginas 4 a 6. Ele deixa de fora o tratamento de erros para mostrar o formato da chamada; o exemplo de produção abaixo acrescenta as proteções completas.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Document\PageRange;
use NextPDF\Document\PdfSplitter;
$splitter = new PdfSplitter();
$result = $splitter->split(
file_get_contents(__DIR__ . '/report.pdf'),
[
new PageRange(1, 3),
new PageRange(4, 6),
],
);
foreach ($result->documents as $i => $segment) {
file_put_contents(__DIR__ . sprintf('/part-%d.pdf', $i + 1), $segment->pdfData);
}
printf("Split %d-page source into %d document(s).\n", $result->totalPages, $result->count());

Este programa autocontido constrói um pequeno documento de várias páginas em memória, então ele roda sem um arquivo externo. Ele demonstra as três operações — dividir por intervalos, dividir a cada N páginas e extrair um único intervalo. Ele valida e escreve os segmentos por intervalo e a cauda extraída, e reporta o resultado por tamanho como uma contagem, para que você veja o formato de cada chamada sem três loops de escrita quase idênticos. Ele captura as exceções que a superfície de divisão lança e relança cada uma com contexto em vez de engoli-la. Substitua a origem em memória pela sua própria leitura com file_get_contents() ou busca em armazenamento de objetos.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use InvalidArgumentException;
use NextPDF\Core\Document;
use NextPDF\Document\Merge\UnsupportedSourceDocumentException;
use NextPDF\Document\PageRange;
use NextPDF\Document\PdfSplitter;
use NextPDF\Document\SplitDocument;
use NextPDF\Exception\PageLayoutException;
/**
* Build a tiny labelled multi-page PDF so the program is self-contained.
*
* In your own code, replace this with a read of the PDF you want to split,
* for example file_get_contents($path).
*/
function buildSample(int $pages): string
{
$doc = Document::createStandalone();
$doc->setTitle('Split sample');
for ($page = 1; $page <= $pages; $page++) {
$doc->addPage();
$doc->setFont('helvetica', '', 12);
$doc->cell(0, 10, sprintf('Source page %d', $page), newLine: true);
}
return $doc->getPdfData();
}
$source = buildSample(7);
$splitter = new PdfSplitter();
try {
// 1. Split into named ranges: one output per PageRange, in order.
$byRange = $splitter->split(
$source,
PageRange::parse('1-3,4-6'),
maxBytes: 50_000_000,
maxRanges: 100,
);
// 2. Split every 2 pages: segments of [1-2], [3-4], [5-6], [7] (remainder).
$bySize = $splitter->splitEvery($source, 2);
// 3. Extract a single range as one document.
$tail = $splitter->extractPages($source, new PageRange(7, 7));
} catch (InvalidArgumentException $e) {
// Raised on an oversized input, an empty range list, or too many ranges.
throw new RuntimeException('Split rejected its input: ' . $e->getMessage(), previous: $e);
} catch (PageLayoutException $e) {
// Raised when a range exceeds the source page count, and also by the
// PageRange constructor / PageRange::parse() on an invalid or malformed range.
throw new RuntimeException(
sprintf('Range out of bounds (page %d): %s', $e->getPageNumber(), $e->getConstraint()),
previous: $e,
);
} catch (UnsupportedSourceDocumentException $e) {
// Raised fail-closed on an encrypted, signed, or form-bearing source.
throw new RuntimeException('Source cannot be split: ' . $e->getMessage(), previous: $e);
}
printf(
"Source has %d page(s). By-range produced %d doc(s); by-size produced %d doc(s).\n",
$byRange->totalPages,
$byRange->count(),
$bySize->count(),
);
foreach ($byRange->documents as $i => $segment) {
emitSegment(sprintf('range-%d', $i + 1), $segment);
}
emitSegment('tail', $tail);
/**
* Validate a segment and write it to the cookbook side-channel directory,
* or to the script directory by default.
*/
function emitSegment(string $name, SplitDocument $segment): void
{
if (!$segment->isValid()) {
throw new RuntimeException(sprintf('Segment "%s" failed its %%PDF header check.', $name));
}
$dir = getenv('NEXTPDF_COOKBOOK_OUTPUT');
$dir = $dir !== false && $dir !== '' ? $dir : __DIR__;
$path = sprintf('%s/%s.pdf', rtrim($dir, '/'), $name);
if (file_put_contents($path, $segment->pdfData) === false) {
throw new RuntimeException(sprintf('Could not write segment to "%s".', $path));
}
printf("Wrote %s: pages %d-%d, %d bytes.\n", $name, $segment->range->start, $segment->range->end, $segment->sizeBytes);
}

Saída padrão esperada (os tamanhos em bytes dependem do build):

Source has 7 page(s). By-range produced 2 doc(s); by-size produced 4 doc(s).
Wrote range-1: pages 1-3, <n> bytes.
Wrote range-2: pages 4-6, <n> bytes.
Wrote tail: pages 7-7, <n> bytes.
  • A origem são bytes, não um caminho. Cada método recebe uma string bruta de PDF. Leia o arquivo com file_get_contents() primeiro, ou puxe os bytes do armazenamento de objetos. Passar um caminho faz a origem falhar na análise.
  • Os números de página são baseados em 1 e inclusivos. new PageRange(1, 3) cobre as páginas 1, 2 e 3 — três páginas. Um início abaixo de 1 ou um fim antes do início lança PageLayoutException a partir do próprio construtor do PageRange.
  • Um intervalo além do fim é um erro, não um clamp. Se o fim de um intervalo exceder a contagem de páginas da origem, split() lança PageLayoutException; ele nunca corta silenciosamente o intervalo até a última página. Inspecione a contagem de páginas primeiro se os seus intervalos vierem de quem chama.
  • splitEvery() mantém o restante. O último segmento guarda quaisquer páginas que sobrarem, então um documento de 7 páginas dividido a cada 2 páginas gera quatro segmentos: três de 2 páginas e um de 1. $pagesPerSegment deve ser pelo menos 1, ou você recebe uma InvalidArgumentException.
  • Uma lista de intervalos vazia é rejeitada. split() com $ranges === [] lança InvalidArgumentException. Construa pelo menos um intervalo antes de chamá-lo.
  • Os limites lançam em vez de truncar. Exceder maxBytes ou maxRanges lança InvalidArgumentException. O splitter nunca processa parcialmente uma entrada grande demais, então ajuste ambos os limites para a sua carga de trabalho.
  • Origens criptografadas, assinadas e com formulário falham fechadas. Uma origem criptografada (não pode ser copiada sem a chave), uma origem assinada digitalmente (repaginar invalidaria o byte range da assinatura) ou uma origem que carrega um formulário interativo (os widgets de um campo podem ficar em páginas removidas e ficar órfãos) lançam UnsupportedSourceDocumentException. O splitter se recusa em vez de emitir um documento corrompido ou comprometido. Dividir um documento com formulário é uma limitação conhecida desta versão.
  • UnsupportedSourceDocumentException vive sob o namespace Merge. Seu nome totalmente qualificado é NextPDF\Document\Merge\UnsupportedSourceDocumentException. Esse caminho Merge em uma página de divisão não é um erro de copiar/colar: é a exceção única e compartilhada de rejeição de documento de origem que tanto a superfície de mesclagem quanto a de divisão lançam quando uma origem não pode ser copiada com segurança. Importe-a desse namespace.
  • A saída é estruturalmente nova, não estável em bytes. Cada segmento é um documento novo com seu próprio catálogo, árvore de páginas e trailer. Duas execuções sobre a mesma entrada são estruturalmente iguais, mas não há garantia de que sejam idênticas byte a byte — daí o perfil de reprodutibilidade structural.

A divisão é linear no número de páginas copiadas em todos os intervalos. Analisar a origem e copiar o fecho de objetos de cada intervalo, não a contabilidade própria do splitter, dominam o trabalho. A origem é mantida em memória como uma string, e os bytes de cada segmento são mantidos até você escrevê-los, então o pico de memória acompanha o tamanho da origem mais o maior intervalo que você produz. A proteção maxBytes mantém o lado da origem desse pico limitado. Para pipelines de alto volume, defina maxBytes e maxRanges com os menores valores que a sua carga de trabalho precisa, para que uma entrada malformada ou grande demais falhe rápido em vez de esgotar a memória.

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

  • Mantenha os limites apertados. maxBytes e maxRanges são sua primeira linha de defesa contra entrada de negação de serviço. Para qualquer superfície que aceita uploads, defina-os no seu teto real, não nos padrões generosos.
  • Faça a triagem antes de dividir. Uma origem que está criptografada ou assinada falha fechada, mas você pode detectar essas condições mais cedo. Passe entradas não confiáveis pelo inspetor da Core primeiro. Consulte Analise e inspecione um PDF para uma varredura 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 escreve em um diretório fixo ou no side-channel do cookbook. Derive os caminhos de saída e os nomes de segmento de valores controlados pelo servidor, nunca de um campo de requisição, para evitar path traversal.
  • Sem segredos na saída. Não escreva arquivos de segmento em um local, ou com um nome, que exponha identificadores internos a um cliente que não deveria vê-los.

Esta receita não faz nenhuma afirmação normativa de padrões por conta própria. Ela decompõe um documento por meio da superfície de divisão da Core e faz uma verificação de sanidade de cada segmento com a verificação de cabeçalho %PDF do SplitDocument::isValid() — uma verificação de presença de que o splitter emitiu um PDF, não uma validação de conformidade ou de estrutura de documento. As estruturas de árvore de páginas e de referências cruzadas que o PdfSplitter reconstrói para cada segmento são as estruturas do PDF 2.0 descritas na referência /modules/core/document/ (ISO 32000-2:2020, tabela de referências cruzadas §7.5.4, árvore de páginas §7.7.3). Para uma leitura estrutural de qualquer documento de entrada ou de saída, incluindo versão, contagem de páginas, criptografia e flags de assinatura, use o inspetor da Core documentado em Analise e inspecione um PDF.