Divida um PDF e extraia intervalos de páginas
Visão geral
Seção intitulada “Visão geral”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.
Instalação
Seção intitulada “Instalação”composer require nextpdf/core:^3Visão conceitual
Seção intitulada “Visão conceitual”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.
Superfície da API
Seção intitulada “Superfície da API”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): SplitResultproduz um documento de saída porPageRangeem$ranges, em ordem. Os dois parâmetros de limite restringem o tamanho da entrada e a contagem de intervalos.splitEvery(string $pdfData, int $pagesPerSegment): SplitResultcorta o documento em segmentos de tamanho fixo de$pagesPerSegmentpáginas cada; o último segmento guarda o restante.extractPages(string $pdfData, PageRange $range): SplitDocumentextrai 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.
Exemplo de código — Início rápido
Seção intitulada “Exemplo de código — Início rápido”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());Exemplo de código — Produção
Seção intitulada “Exemplo de código — Produção”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.Casos extremos e pegadinhas
Seção intitulada “Casos extremos e pegadinhas”- 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çaPageLayoutExceptiona partir do próprio construtor doPageRange. - 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çaPageLayoutException; 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.$pagesPerSegmentdeve ser pelo menos 1, ou você recebe umaInvalidArgumentException.- Uma lista de intervalos vazia é rejeitada.
split()com$ranges === []lançaInvalidArgumentException. Construa pelo menos um intervalo antes de chamá-lo. - Os limites lançam em vez de truncar. Exceder
maxBytesoumaxRangeslançaInvalidArgumentException. 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. UnsupportedSourceDocumentExceptionvive sob o namespaceMerge. Seu nome totalmente qualificado éNextPDF\Document\Merge\UnsupportedSourceDocumentException. Esse caminhoMergeem 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.
Desempenho
Seção intitulada “Desempenho”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.
Notas de segurança
Seção intitulada “Notas de segurança”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.
maxBytesemaxRangessã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.
Conformidade
Seção intitulada “Conformidade”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.
Veja também
Seção intitulada “Veja também”- Referência do módulo Document — a superfície completa de divisão, mesclagem e partes de documento.
- Mescle PDFs externos — a receita inversa: compor muitos documentos em um.
- Analise e inspecione um PDF — faça a triagem de entradas não confiáveis antes de dividi-las.
- Tratamento de erros com reconhecimento de exceções
— a hierarquia de exceções do NextPDF por trás de
PageLayoutExceptioneUnsupportedSourceDocumentException.