Dzielenie pliku PDF i wyodrębnianie zakresów stron
W skrócie
Dział zatytułowany „W skrócie”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.
Instalacja
Dział zatytułowany „Instalacja”composer require nextpdf/core:^3Przegląd koncepcyjny
Dział zatytułowany „Przegląd koncepcyjny”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.
Powierzchnia API
Dział zatytułowany „Powierzchnia API”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): SplitResultwytwarza po jednym dokumencie wyjściowym dla każdegoPageRangew$ranges, w kolejności. Dwa parametry graniczne ograniczają rozmiar wejścia oraz liczbę zakresów.splitEvery(string $pdfData, int $pagesPerSegment): SplitResulttnie dokument na segmenty o stałym rozmiarze po$pagesPerSegmentstron każdy; ostatni segment zawiera resztę.extractPages(string $pdfData, PageRange $range): SplitDocumentwyodrę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.
Przykład kodu — szybki start
Dział zatytułowany „Przykład kodu — szybki start”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());Przykład kodu — wersja produkcyjna
Dział zatytułowany „Przykład kodu — wersja produkcyjna”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.Przypadki brzegowe i pułapki
Dział zatytułowany „Przypadki brzegowe i pułapki”- Ź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łaszaPageLayoutExceptionz samego konstruktoraPageRange. - Zakres poza końcem to błąd, a nie przycięcie. Jeśli koniec zakresu przekracza
liczbę stron źródła,
split()zgłaszaPageLayoutException; 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.$pagesPerSegmentmusi wynosić co najmniej 1, inaczej otrzymaszInvalidArgumentException.- Pusta lista zakresów jest odrzucana.
split()z$ranges === []zgłaszaInvalidArgumentException. Zbuduj co najmniej jeden zakres, zanim go wywołasz. - Granice zgłaszają wyjątek, a nie obcinają. Przekroczenie
maxByteslubmaxRangeszgłaszaInvalidArgumentException. 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 nazwMerge. Jego w pełni kwalifikowana nazwa toNextPDF\Document\Merge\UnsupportedSourceDocumentException. Ta ścieżkaMergena 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.
Wydajność
Dział zatytułowany „Wydajność”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ęć.
Uwagi dotyczące bezpieczeństwa
Dział zatytułowany „Uwagi dotyczące bezpieczeństwa”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.
maxBytesorazmaxRangesto 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ć.
Zgodność
Dział zatytułowany „Zgodność”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.
Zobacz też
Dział zatytułowany „Zobacz też”- Dokumentacja modułu Document — pełny interfejs dzielenia, scalania i części dokumentu.
- Scalanie zewnętrznych plików PDF — przepis odwrotny: składanie wielu dokumentów w jeden.
- Parsowanie i inspekcja pliku PDF — segregowanie niezaufanych wejść przed ich podzieleniem.
- Obsługa błędów świadoma wyjątków
— hierarchia wyjątków NextPDF stojąca za
PageLayoutExceptionorazUnsupportedSourceDocumentException.