Suddividere un PDF ed estrarre intervalli di pagine
In sintesi
Sezione intitolata “In sintesi”Si dispone di un solo PDF e ne occorrono diversi. Questa ricetta scolpisce un
singolo documento in più file tramite l’interfaccia di suddivisione di Core,
NextPDF\Document\PdfSplitter. Si passa l’origine come stringa di byte PDF non
elaborati e si descrive quali pagine si desiderano. Lo splitter analizza
l’origine attraverso il grafo degli oggetti, copia gli oggetti raggiungibili di
ciascun intervallo richiesto in un nuovo documento rinumerato con il proprio
albero delle pagine e la propria tabella di riferimenti incrociati, e restituisce
PDF strutturalmente completi che si caricano in un lettore conforme.
Questa è l’operazione inversa della ricetta di unione: l’unione compone molti documenti in uno solo, la suddivisione scompone un documento in molti. La stessa interfaccia copre le tre attività di cui si ha più spesso bisogno:
- Suddividere per intervalli — produrre un documento di output per ogni intervallo di pagine che si specifica.
- Suddividere ogni N pagine — spezzare un file lungo in segmenti di dimensione fissa.
- Estrarre un intervallo — estrarre un singolo intervallo contiguo di pagine in un unico documento.
La suddivisione viene eseguita in-process, senza browser headless e senza
chiamate di rete. Occorre avere Core installato (composer require nextpdf/core:^3)
e un PDF leggibile.
Installazione
Sezione intitolata “Installazione”composer require nextpdf/core:^3Panoramica concettuale
Sezione intitolata “Panoramica concettuale”Un PDF individua le proprie pagine tramite un albero delle pagine la cui radice è
un nodo /Pages, e raggiunge ogni oggetto indiretto attraverso i propri dati di
riferimento incrociato (una tabella o uno stream). Non è possibile estrarre le
pagine ritagliando i byte: una singola pagina fa riferimento a font, immagini e
dizionari di risorse condivisi che risiedono altrove nel file, e gli offset di
riferimento incrociato non sarebbero più validi.
PdfSplitter svolge il lavoro reale. Per ogni intervallo percorre il grafo degli
oggetti a partire dagli oggetti pagina richiesti, raccoglie la chiusura degli
oggetti raggiungibili, rinumera tali oggetti in un nuovo spazio di indirizzamento,
ricostruisce un documento con un singolo albero delle pagine ed emette una vera
tabella di riferimenti incrociati conforme alla struttura del PDF 2.0
(ISO 32000-2:2020, tabella di riferimenti incrociati §7.5.4, albero delle pagine
§7.7.3). Ciascun output è un documento autonomo, non un frammento.
I numeri di pagina partono da 1 e sono inclusivi. Un intervallo è un value object
NextPDF\Document\PageRange: new PageRange(2, 5) indica le pagine da 2 a 5. Il
costruttore convalida i propri invarianti — rifiuta un inizio inferiore a 1 o una
fine precedente all’inizio sollevando NextPDF\Exception\PageLayoutException —
così che un intervallo impossibile fallisca al momento della costruzione, non in
profondità all’interno dello splitter. PageRange::parse() e PageRange::all()
sollevano la stessa PageLayoutException in caso di specifica malformata o di un
totale di pagine non positivo.
Interfaccia API
Sezione intitolata “Interfaccia API”new NextPDF\Document\PdfSplitter() espone tre metodi. Tutti accettano l’origine
come stringa di byte PDF non elaborati, mai un percorso.
split(string $pdfData, array $ranges, int $maxBytes = 100_000_000, int $maxRanges = 1000): SplitResultproduce un documento di output per ciascunPageRangein$ranges, in ordine. I due parametri di limite delimitano la dimensione dell’input e il numero di intervalli.splitEvery(string $pdfData, int $pagesPerSegment): SplitResultspezza il documento in segmenti di dimensione fissa di$pagesPerSegmentpagine ciascuno; l’ultimo segmento contiene il resto.extractPages(string $pdfData, PageRange $range): SplitDocumentestrae un singolo intervallo e restituisce direttamente quel documento.
split() e splitEvery() restituiscono un NextPDF\Document\SplitResult, un
oggetto readonly che porta con sé $documents (un elenco di segmenti),
$totalPages (le pagine nell’origine) e $sourceSize. Offre count(),
document(int $index) per recuperare un segmento tramite indice a partire da
zero, e totalOutputSize().
Ciascun segmento, e il valore restituito da extractPages(), è un
NextPDF\Document\SplitDocument: un oggetto readonly che espone $pdfData (i
byte del segmento), $range, $pageCount, $sizeBytes e l’helper isValid().
isValid() è un controllo di integrità ristretto sull’intestazione %PDF —
restituisce true quando i byte del segmento iniziano con %PDF — non una
convalida della struttura del documento né della conformità; conferma che lo
splitter ha prodotto un PDF, non che il file sia pienamente conforme.
Si costruisce un PageRange direttamente con new PageRange($start, $end),
oppure si analizza una specifica leggibile dall’uomo con
PageRange::parse('1-3,5,7-10'), che restituisce una list<PageRange> pronta da
passare a split(). PageRange::all($totalPages) restituisce un singolo
intervallo che copre l’intero documento.
Esempio di codice — Avvio rapido
Sezione intitolata “Esempio di codice — Avvio rapido”Questo esempio legge un file e lo suddivide in due documenti: le pagine da 1 a 3 e le pagine da 4 a 6. Tralascia la gestione degli errori per mostrare la forma della chiamata; l’esempio di produzione qui sotto aggiunge tutte le protezioni.
<?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());Esempio di codice — Produzione
Sezione intitolata “Esempio di codice — Produzione”Questo programma autonomo costruisce in memoria un piccolo documento
multipagina, così da poter essere eseguito senza un file esterno. Dimostra tutte
e tre le operazioni — suddivisione per intervalli, suddivisione ogni N pagine ed
estrazione di un singolo intervallo. Convalida e scrive i segmenti per intervallo
e la coda estratta, e riporta il risultato per dimensione come conteggio, così da
mostrare la forma di ciascuna chiamata senza tre cicli di scrittura quasi
identici. Cattura le eccezioni sollevate dall’interfaccia di suddivisione e
rilancia ciascuna con contesto anziché ignorarla. Sostituire l’origine in memoria
con la propria lettura file_get_contents() o con il recupero da object storage.
<?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);}Output standard atteso (le dimensioni in byte dipendono dalla 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.Casi limite e insidie
Sezione intitolata “Casi limite e insidie”- L’origine è costituita da byte, non da un percorso. Ogni metodo accetta una
stringa PDF non elaborata. Leggere prima il file con
file_get_contents()oppure recuperare i byte da object storage. Passare un percorso fa fallire l’analisi dell’origine. - I numeri di pagina partono da 1 e sono inclusivi.
new PageRange(1, 3)copre le pagine 1, 2 e 3 — tre pagine. Un inizio inferiore a 1 o una fine precedente all’inizio sollevaPageLayoutExceptiondallo stesso costruttore diPageRange. - Un intervallo oltre la fine è un errore, non un troncamento. Se la fine di un
intervallo supera il numero di pagine dell’origine,
split()sollevaPageLayoutException; non riduce mai silenziosamente l’intervallo all’ultima pagina. Ispezionare prima il numero di pagine se gli intervalli sono forniti dal chiamante. splitEvery()conserva il resto. L’ultimo segmento contiene tutte le pagine rimanenti, perciò un documento di 7 pagine suddiviso ogni 2 pagine produce quattro segmenti: tre da 2 pagine e uno da 1.$pagesPerSegmentdeve essere almeno 1, altrimenti si ottiene unInvalidArgumentException.- Un elenco di intervalli vuoto viene rifiutato.
split()con$ranges === []sollevaInvalidArgumentException. Costruire almeno un intervallo prima di chiamarlo. - I limiti sollevano un’eccezione anziché troncare. Superare
maxBytesomaxRangessollevaInvalidArgumentException. Lo splitter non elabora mai parzialmente un input sovradimensionato, perciò regolare entrambi i limiti in base al proprio carico di lavoro. - Le origini cifrate, firmate e con moduli falliscono in modo chiuso.
Un’origine cifrata (non può essere copiata senza la chiave), un’origine firmata
digitalmente (la ripaginazione invaliderebbe l’intervallo di byte della firma) o
un’origine che porta un modulo interattivo (i widget di un campo possono trovarsi
su pagine scartate e restare orfani) sollevano
UnsupportedSourceDocumentException. Lo splitter rifiuta anziché emettere un documento danneggiato o compromesso. La suddivisione di un documento con moduli è una limitazione nota di questa release. UnsupportedSourceDocumentExceptionrisiede sotto il namespaceMerge. Il suo nome completo èNextPDF\Document\Merge\UnsupportedSourceDocumentException. Quel percorsoMergein una pagina sulla suddivisione non è un errore di copia/incolla: è l’unica, condivisa eccezione di rifiuto del documento di origine che sia l’interfaccia di unione sia quella di suddivisione sollevano quando un’origine non può essere copiata in modo sicuro. Importarla da quel namespace.- L’output è strutturalmente nuovo, non stabile a livello di byte. Ciascun
segmento è un nuovo documento con il proprio catalogo, albero delle pagine e
trailer. Due esecuzioni sullo stesso input sono strutturalmente equivalenti, ma
non sono garantite identiche a livello di byte — da cui il profilo di
riproducibilità
structural.
Prestazioni
Sezione intitolata “Prestazioni”La suddivisione è lineare nel numero di pagine copiate attraverso tutti gli
intervalli. A dominare il lavoro sono l’analisi dell’origine e la copia della
chiusura degli oggetti di ciascun intervallo, non la contabilità interna dello
splitter. L’origine viene mantenuta in memoria come stringa e i byte di ciascun
segmento vengono mantenuti finché non li si scrive, perciò il picco di memoria
segue la dimensione dell’origine più l’intervallo più grande che si produce. La
protezione maxBytes mantiene delimitato il lato origine di quel picco. Per le
pipeline ad alto volume, impostare maxBytes e maxRanges ai valori più piccoli
necessari per il proprio carico di lavoro, così che un input malformato o
sovradimensionato fallisca rapidamente anziché esaurire la memoria.
Note sulla sicurezza
Sezione intitolata “Note sulla sicurezza”La suddivisione viene eseguita in-process; nessun byte del documento lascia l’host e non viene effettuata alcuna chiamata di rete. Trattare ogni PDF di origine come input non attendibile:
- Mantenere i limiti stretti.
maxBytesemaxRangessono la prima linea di difesa contro input di denial-of-service. Per qualsiasi interfaccia che accetta caricamenti, impostarli al proprio tetto reale, non ai valori predefiniti generosi. - Filtrare prima di suddividere. Un’origine cifrata o firmata fallisce in modo chiuso, ma è possibile rilevare tali condizioni in anticipo. Passare gli input non attendibili prima attraverso l’inspector di Core. Si veda Analizzare e ispezionare un PDF per una scansione delimitata che segnala cifratura, firme e indicatori di rischio prima di un’elaborazione più pesante.
- Non interpolare mai input dell’utente in un percorso. Questa ricetta scrive in una directory fissa o nel canale laterale del cookbook. Derivare i percorsi di output e i nomi dei segmenti da valori controllati dal server, mai da un campo di richiesta, per evitare il path traversal.
- Nessun segreto nell’output. Non scrivere i file dei segmenti in una posizione, o con un nome, che esponga identificatori interni a un client che non dovrebbe vederli.
Conformità
Sezione intitolata “Conformità”Questa ricetta non avanza alcuna affermazione normativa di conformità a standard.
Scompone un documento attraverso l’interfaccia di suddivisione di Core e verifica
l’integrità di ciascun segmento con il controllo %PDF di
SplitDocument::isValid() — un controllo di presenza del fatto che lo splitter
abbia emesso un PDF, non una convalida di conformità o di struttura del documento.
Le strutture dell’albero delle pagine e del riferimento incrociato che
PdfSplitter ricostruisce per ciascun segmento sono le strutture del PDF 2.0
descritte nel riferimento /modules/core/document/ (ISO 32000-2:2020, tabella di
riferimenti incrociati §7.5.4, albero delle pagine §7.7.3). Per una lettura
strutturale di qualsiasi documento di input o output, inclusi versione, numero di
pagine, cifratura e flag delle firme, usare l’inspector di Core documentato in
Analizzare e ispezionare un PDF.
Vedere anche
Sezione intitolata “Vedere anche”- Riferimento del modulo Document — l’interfaccia completa di suddivisione, unione e parti di documento.
- Unire PDF esterni — la ricetta inversa: comporre molti documenti in uno solo.
- Analizzare e ispezionare un PDF — filtrare gli input non attendibili prima di suddividerli.
- Gestione degli errori consapevole delle eccezioni
— la gerarchia delle eccezioni di NextPDF dietro
PageLayoutExceptioneUnsupportedSourceDocumentException.