Przejdź do głównej zawartości
getnextpdf.com

Dzielenie pliku PDF i wyodrębnianie zakresów stron

Masz jeden plik PDF, a potrzebujesz kilku. Ten przepis dzieli pojedynczy dokument na wiele plików za pomocą interfejsu dzielenia Core, NextPDF\Document\PdfSplitter. Przekazujesz źródło jako surowy łańcuch bajtów PDF i opisujesz, które strony chcesz uzyskać. Mechanizm dzielenia parsuje źródło przez graf obiektów, kopiuje obiekty osiągalne dla każdego żądanego zakresu do świeżego, przenumerowanego dokumentu z własnym drzewem stron i tablicą odsyłaczy oraz zwraca strukturalnie kompletne pliki PDF, które wczytują się w zgodnym czytniku.

Jest to odwrotność przepisu na scalanie: scalanie składa wiele dokumentów w jeden, a dzielenie rozkłada jeden dokument na wiele. Ten sam interfejs obsługuje trzy zadania, których potrzebujesz najczęściej:

  • Podział według zakresów — wytwórz po jednym dokumencie wyjściowym dla każdego nazwanego zakresu stron.
  • Podział co N stron — potnij długi plik na segmenty o stałym rozmiarze.
  • Wyodrębnianie zakresu — wyciągnij pojedynczy ciągły zakres stron do jednego dokumentu.

Dzielenie odbywa się w ramach procesu, bez przeglądarki headless ani wywołania sieciowego. Potrzebujesz zainstalowanego Core (composer require nextpdf/core:^3) oraz jednego możliwego do odczytania pliku PDF.

Okno terminala
composer require nextpdf/core:^3

Plik PDF odnajduje swoje strony za pomocą drzewa stron, którego korzeniem jest węzeł /Pages, a każdy obiekt pośredni osiąga za pomocą swoich danych odsyłaczy (tablicy lub strumienia). Nie możesz wyodrębniać stron, krojąc bajty: pojedyncza strona odwołuje się do współdzielonych czcionek, obrazów i słowników zasobów, które żyją w innym miejscu pliku, a przesunięcia odsyłaczy przestałyby być prawidłowe.

PdfSplitter wykonuje właściwą pracę. Dla każdego zakresu przechodzi graf obiektów od żądanych obiektów stron, gromadzi domknięcie osiągalnych obiektów, przenumerowuje te obiekty do świeżej przestrzeni adresowej, odbudowuje dokument z pojedynczym drzewem stron oraz emituje prawdziwą tablicę odsyłaczy zgodną ze strukturą PDF 2.0 (ISO 32000-2:2020, tablica odsyłaczy §7.5.4, drzewo stron §7.7.3). Każde wyjście jest samodzielnym dokumentem, a nie fragmentem.

Numery stron są liczone od 1 i włączające. Zakres to obiekt wartości NextPDF\Document\PageRange: new PageRange(2, 5) oznacza strony od 2 do 5. Konstruktor waliduje własne niezmienniki — odrzuca początek poniżej 1 lub koniec przed początkiem, zgłaszając NextPDF\Exception\PageLayoutException — więc niemożliwy zakres zawodzi przy konstrukcji, a nie głęboko wewnątrz mechanizmu dzielenia. PageRange::parse() oraz PageRange::all() zgłaszają ten sam PageLayoutException przy zniekształconej specyfikacji lub niedodatniej łącznej liczbie stron.

new NextPDF\Document\PdfSplitter() udostępnia trzy metody. Wszystkie przyjmują źródło jako surowy łańcuch bajtów PDF, nigdy jako ścieżkę.

  • split(string $pdfData, array $ranges, int $maxBytes = 100_000_000, int $maxRanges = 1000): SplitResult wytwarza po jednym dokumencie wyjściowym dla każdego PageRange w $ranges, w kolejności. Dwa parametry graniczne ograniczają rozmiar wejścia oraz liczbę zakresów.
  • splitEvery(string $pdfData, int $pagesPerSegment): SplitResult tnie dokument na segmenty o stałym rozmiarze po $pagesPerSegment stron każdy; ostatni segment zawiera resztę.
  • extractPages(string $pdfData, PageRange $range): SplitDocument wyodrębnia pojedynczy zakres i zwraca ten jeden dokument bezpośrednio.

split() oraz splitEvery() zwracają NextPDF\Document\SplitResult, obiekt readonly, który zawiera $documents (listę segmentów), $totalPages (strony w źródle) oraz $sourceSize. Udostępnia count(), document(int $index), aby pobrać segment po indeksie liczonym od zera, oraz totalOutputSize().

Każdy segment, a także wartość zwracana przez extractPages(), to NextPDF\Document\SplitDocument: obiekt readonly, który udostępnia $pdfData (bajty segmentu), $range, $pageCount, $sizeBytes oraz funkcję pomocniczą isValid(). isValid() to wąska kontrola poprawności nagłówka %PDF — zwraca true, gdy bajty segmentu zaczynają się od %PDF — a nie walidacja struktury dokumentu ani zgodności; potwierdza, że mechanizm dzielenia wytworzył plik PDF, a nie że plik jest w pełni zgodny.

PageRange budujesz bezpośrednio za pomocą new PageRange($start, $end) albo parsujesz specyfikację czytelną dla człowieka za pomocą PageRange::parse('1-3,5,7-10'), która zwraca list<PageRange> gotowy do przekazania do split(). PageRange::all($totalPages) zwraca pojedynczy zakres obejmujący cały dokument.

Ten przykład odczytuje jeden plik i dzieli go na dwa dokumenty: strony od 1 do 3 oraz strony od 4 do 6. Pomija obsługę błędów, aby pokazać kształt wywołania; poniższy przykład produkcyjny dodaje pełne zabezpieczenia.

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

Ten samodzielny program buduje w pamięci jeden mały dokument wielostronicowy, dzięki czemu działa bez zewnętrznego pliku. Demonstruje wszystkie trzy operacje — podział według zakresów, podział co N stron oraz wyodrębnienie pojedynczego zakresu. Waliduje i zapisuje segmenty według zakresów oraz wyodrębniony koniec, a wynik podziału według rozmiaru raportuje jako liczbę, dzięki czemu widzisz kształt każdego wywołania bez trzech niemal identycznych pętli zapisu. Przechwytuje wyjątki, które zgłasza interfejs dzielenia, i ponownie zgłasza każdy z kontekstem, zamiast go połykać. Zastąp źródło w pamięci własnym odczytem file_get_contents() lub pobraniem z magazynu obiektów.

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

Oczekiwane standardowe wyjście (rozmiary w bajtach zależą od kompilacji):

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.
  • Źródłem są bajty, a nie ścieżka. Każda metoda przyjmuje surowy łańcuch PDF. Najpierw odczytaj plik za pomocą file_get_contents() albo pobierz bajty z magazynu obiektów. Przekazanie ścieżki sprawia, że źródło nie daje się sparsować.
  • Numery stron są liczone od 1 i włączające. new PageRange(1, 3) obejmuje strony 1, 2 i 3 — trzy strony. Początek poniżej 1 lub koniec przed początkiem zgłasza PageLayoutException z samego konstruktora PageRange.
  • Zakres poza końcem to błąd, a nie przycięcie. Jeśli koniec zakresu przekracza liczbę stron źródła, split() zgłasza PageLayoutException; nigdy po cichu nie przycina zakresu do ostatniej strony. Najpierw sprawdź liczbę stron, jeśli Twoje zakresy dostarcza wywołujący.
  • splitEvery() zachowuje resztę. Ostatni segment zawiera wszystkie pozostałe strony, więc dokument 7-stronicowy dzielony co 2 strony daje cztery segmenty: trzy po 2 strony i jeden po 1. $pagesPerSegment musi wynosić co najmniej 1, inaczej otrzymasz InvalidArgumentException.
  • Pusta lista zakresów jest odrzucana. split() z $ranges === [] zgłasza InvalidArgumentException. Zbuduj co najmniej jeden zakres, zanim go wywołasz.
  • Granice zgłaszają wyjątek, a nie obcinają. Przekroczenie maxBytes lub maxRanges zgłasza InvalidArgumentException. Mechanizm dzielenia nigdy częściowo nie przetwarza zbyt dużego wejścia, więc dostrój obie granice do swojego obciążenia.
  • Źródła zaszyfrowane, podpisane i zawierające formularze zawodzą fail-closed. Źródło zaszyfrowane (nie da się go skopiować bez klucza), źródło podpisane cyfrowo (ponowna paginacja unieważniłaby zakres bajtów podpisu) lub źródło zawierające formularz interaktywny (widżety pola mogą znajdować się na porzuconych stronach i osierocić się) zgłaszają UnsupportedSourceDocumentException. Mechanizm dzielenia odmawia, zamiast emitować uszkodzony lub skompromitowany dokument. Dzielenie dokumentu z formularzem to znane ograniczenie tego wydania.
  • UnsupportedSourceDocumentException żyje w przestrzeni nazw Merge. Jego w pełni kwalifikowana nazwa to NextPDF\Document\Merge\UnsupportedSourceDocumentException. Ta ścieżka Merge na stronie o dzieleniu nie jest błędem kopiowania/wklejania: to pojedynczy, współdzielony wyjątek odrzucenia dokumentu źródłowego, który zgłaszają zarówno interfejs scalania, jak i dzielenia, gdy źródła nie da się bezpiecznie skopiować. Zaimportuj go z tej przestrzeni nazw.
  • Wyjście jest strukturalnie świeże, a nie stabilne bajtowo. Każdy segment to nowy dokument z własnym katalogiem, drzewem stron i trailerem. Dwa przebiegi na tym samym wejściu są strukturalnie równe, ale nie gwarantowanie identyczne bajtowo — stąd profil odtwarzalności structural.

Dzielenie jest liniowe względem liczby stron skopiowanych we wszystkich zakresach. Parsowanie źródła i kopiowanie domknięcia obiektów każdego zakresu, a nie własna ewidencja mechanizmu dzielenia, dominują w pracy. Źródło jest przechowywane w pamięci jako łańcuch, a bajty każdego segmentu są przechowywane do momentu ich zapisania, więc szczytowe zużycie pamięci śledzi rozmiar źródła powiększony o największy wytworzony zakres. Zabezpieczenie maxBytes utrzymuje ograniczoną stronę źródłową tego szczytu. W przypadku potoków o dużym wolumenie ustaw maxBytes oraz maxRanges na najmniejsze wartości, których potrzebuje Twoje obciążenie, aby zniekształcone lub zbyt duże wejście zawiodło szybko, zamiast wyczerpywać pamięć.

Dzielenie odbywa się w ramach procesu; żadne bajty dokumentu nie opuszczają hosta i nie jest wykonywane żadne wywołanie sieciowe. Traktuj każdy źródłowy plik PDF jako niezaufane wejście:

  • Utrzymuj ciasne granice. maxBytes oraz maxRanges to Twoja pierwsza linia obrony przed wejściem powodującym odmowę usługi. Dla dowolnego interfejsu, który przyjmuje przesyłane pliki, ustaw je na rzeczywisty pułap, a nie na hojne wartości domyślne.
  • Posegreguj, zanim podzielisz. Źródło zaszyfrowane lub podpisane zawodzi fail-closed, ale te warunki możesz wykryć wcześniej. Przepuść niezaufane wejścia najpierw przez inspektora Core. Zobacz Parsowanie i inspekcja pliku PDF, aby poznać ograniczony skan, który oznacza szyfrowanie, podpisy i markery ryzyka przed cięższym przetwarzaniem.
  • Nigdy nie wstawiaj danych wejściowych użytkownika do ścieżki. Ten przepis zapisuje do stałego katalogu lub do katalogu pomocniczego kanału cookbook. Wyprowadzaj ścieżki wyjściowe i nazwy segmentów z wartości kontrolowanych przez serwer, nigdy z pola żądania, aby uniknąć przejścia po ścieżce.
  • Brak sekretów w wyjściu. Nie zapisuj plików segmentów w lokalizacji ani pod nazwą, która ujawnia wewnętrzne identyfikatory klientowi, który nie powinien ich widzieć.

Ten przepis nie zgłasza własnego normatywnego roszczenia co do standardów. Rozkłada jeden dokument za pomocą interfejsu dzielenia Core i sprawdza poprawność każdego segmentu kontrolą nagłówka %PDF SplitDocument::isValid() — kontrolą obecności, że mechanizm dzielenia wyemitował plik PDF, a nie walidacją zgodności ani struktury dokumentu. Struktury drzewa stron i odsyłaczy, które PdfSplitter odbudowuje dla każdego segmentu, to struktury PDF 2.0 opisane w dokumentacji /modules/core/document/ (ISO 32000-2:2020, tablica odsyłaczy §7.5.4, drzewo stron §7.7.3). Aby uzyskać strukturalny odczyt dowolnego dokumentu wejściowego lub wyjściowego, w tym wersji, liczby stron, szyfrowania i flag podpisu, użyj inspektora Core udokumentowanego w Parsowanie i inspekcja pliku PDF.