PDF 분할 및 페이지 범위 추출하기
한눈에 보기
섹션 제목: “한눈에 보기”PDF 하나가 있는데 여러 개가 필요합니다. 이 레시피는 Core 분할 표면인
NextPDF\Document\PdfSplitter로 단일 문서를 여러 파일로 잘라 냅니다. 소스를
원시 PDF 바이트 문자열로 전달하고 어떤 페이지를 원하는지 기술합니다. 스플리터는
객체 그래프를 통해 소스를 파싱하고, 요청한 각 범위의 도달 가능한 객체를 자체
페이지 트리와 상호 참조 테이블을 가진 새롭게 번호가 매겨진 문서로 복사한 뒤,
적합한 리더에서 로드되는 구조적으로 완전한 PDF를 돌려줍니다.
이는 병합 레시피의 역입니다. 병합은 여러 문서를 하나로 구성하고, 분할은 하나의 문서를 여럿으로 분해합니다. 동일한 표면이 가장 자주 필요한 세 가지 작업을 다룹니다.
- 범위별 분할 — 지정한 페이지 범위당 하나의 출력 문서를 생성합니다.
- N페이지마다 분할 — 긴 파일을 고정 크기 세그먼트로 자릅니다.
- 범위 추출 — 단일 연속 페이지 범위를 하나의 문서로 끌어냅니다.
분할은 헤드리스 브라우저나 네트워크 호출 없이 인프로세스로 실행됩니다. Core가
설치되어 있어야 하고(composer require nextpdf/core:^3) 읽을 수 있는 PDF 하나가
필요합니다.
composer require nextpdf/core:^3개념 개요
섹션 제목: “개념 개요”PDF는 /Pages 노드에 뿌리를 둔 페이지 트리를 통해 페이지를 찾고, 상호 참조
데이터(테이블 또는 스트림)를 통해 모든 간접 객체에 도달합니다. 바이트를 잘라
페이지를 추출할 수는 없습니다. 단일 페이지는 파일 다른 곳에 존재하는 공유 글꼴,
이미지, 리소스 딕셔너리를 참조하며, 상호 참조 오프셋도 더 이상 유효하지 않게
됩니다.
PdfSplitter가 실제 작업을 수행합니다. 각 범위에 대해 요청한 페이지 객체에서
객체 그래프를 따라가 도달 가능한 객체 클로저를 수집하고, 그 객체들을 새 주소
공간으로 다시 번호 매기며, 단일 페이지 트리 문서를 재구축하고, PDF 2.0
구조에 따라 실제 상호 참조 테이블을 내보냅니다(ISO 32000-2:2020, 상호 참조
테이블 §7.5.4, 페이지 트리 §7.7.3). 각 출력은 조각이 아니라 자체 완결적인
문서입니다.
페이지 번호는 1 기반이며 양 끝을 포함합니다. 범위는
NextPDF\Document\PageRange 값 객체입니다. new PageRange(2, 5)는 2페이지부터
5페이지까지를 의미합니다. 생성자는 자체 불변식을 검증합니다 — 시작이 1 미만이거나
끝이 시작보다 앞서면 NextPDF\Exception\PageLayoutException을 발생시켜 거부하므로,
불가능한 범위는 스플리터 깊숙한 곳이 아니라 생성 시점에 실패합니다.
PageRange::parse()와 PageRange::all()은 잘못된 형식의 명세나 양수가 아닌
페이지 총수에 대해 동일한 PageLayoutException을 발생시킵니다.
API 표면
섹션 제목: “API 표면”new NextPDF\Document\PdfSplitter()는 세 가지 메서드를 노출합니다. 모두 소스를
경로가 아니라 원시 PDF 바이트 문자열로 받습니다.
split(string $pdfData, array $ranges, int $maxBytes = 100_000_000, int $maxRanges = 1000): SplitResult는$ranges의 각PageRange당 하나의 출력 문서를 순서대로 생성합니다. 두 제한 매개변수는 입력 크기와 범위 수에 상한을 둡니다.splitEvery(string $pdfData, int $pagesPerSegment): SplitResult는 문서를 각$pagesPerSegment페이지의 고정 크기 세그먼트로 자릅니다. 마지막 세그먼트는 나머지를 담습니다.extractPages(string $pdfData, PageRange $range): SplitDocument는 단일 범위를 추출하여 그 하나의 문서를 직접 반환합니다.
split()과 splitEvery()는 NextPDF\Document\SplitResult를 반환합니다. 이는
$documents(세그먼트 목록), $totalPages(소스의 페이지 수), $sourceSize를
담은 readonly 객체입니다. count(), 0 기반 인덱스로 세그먼트를 가져오는
document(int $index), 그리고 totalOutputSize()를 제공합니다.
각 세그먼트, 그리고 extractPages()의 반환값은
NextPDF\Document\SplitDocument입니다. 이는 $pdfData(세그먼트 바이트),
$range, $pageCount, $sizeBytes, 그리고 isValid() 헬퍼를 노출하는
readonly 객체입니다. isValid()는 좁은 %PDF 헤더 정상성 검사입니다 —
세그먼트 바이트가 %PDF로 시작할 때 true를 반환합니다 — 문서 구조나 적합성
검증이 아닙니다. 스플리터가 PDF를 생성했는지를 확인할 뿐, 파일이 완전히
적합한지를 확인하지는 않습니다.
PageRange는 new PageRange($start, $end)로 직접 만들거나, 사람이 읽을 수 있는
명세를 PageRange::parse('1-3,5,7-10')로 파싱하면 split()에 바로 전달할 수
있는 list<PageRange>를 반환합니다. PageRange::all($totalPages)는 문서 전체를
포괄하는 단일 범위를 반환합니다.
코드 샘플 — 빠른 시작
섹션 제목: “코드 샘플 — 빠른 시작”이 샘플은 파일 하나를 읽어 13페이지와 46페이지, 두 문서로 분할합니다. 호출
형태를 보여 주기 위해 오류 처리는 생략했습니다. 아래의 프로덕션 샘플이 전체
가드를 추가합니다.
<?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());코드 샘플 — 프로덕션
섹션 제목: “코드 샘플 — 프로덕션”이 자체 완결적 프로그램은 메모리 내에 작은 다중 페이지 문서 하나를 구축하므로
외부 파일 없이 실행됩니다. 세 가지 작업 — 범위별 분할, N페이지마다 분할, 단일
범위 추출 — 을 모두 시연합니다. 범위별 세그먼트와 추출한 꼬리를 검증하고
기록하며, 크기별 결과는 개수로 보고하여 거의 동일한 쓰기 루프를 셋이나 두지
않고도 각 호출 형태를 볼 수 있습니다. 분할 표면이 발생시키는 예외를 포착하고
삼키는 대신 각각을 컨텍스트와 함께 다시 던집니다. 메모리 내 소스를 직접의
file_get_contents() 읽기나 객체 스토리지 가져오기로 교체하십시오.
<?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);}예상 표준 출력(바이트 크기는 빌드에 따라 달라집니다):
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.엣지 케이스 및 주의 사항
섹션 제목: “엣지 케이스 및 주의 사항”- 소스는 경로가 아니라 바이트입니다. 모든 메서드는 원시 PDF 문자열을
받습니다. 먼저
file_get_contents()로 파일을 읽거나 객체 스토리지에서 바이트를 가져오십시오. 경로를 전달하면 소스가 파싱에 실패합니다. - 페이지 번호는 1 기반이며 양 끝을 포함합니다.
new PageRange(1, 3)은 1, 2, 3페이지 — 세 페이지를 포괄합니다. 시작이 1 미만이거나 끝이 시작보다 앞서면PageRange생성자 자체가PageLayoutException을 발생시킵니다. - 끝을 넘는 범위는 클램프가 아니라 오류입니다. 범위의 끝이 소스 페이지
수를 초과하면
split()이PageLayoutException을 발생시킵니다. 범위를 마지막 페이지로 조용히 다듬지 않습니다. 범위가 호출자 제공이라면 먼저 페이지 수를 검사하십시오. splitEvery()는 나머지를 유지합니다. 마지막 세그먼트는 남은 페이지를 무엇이든 담으므로, 7페이지 문서를 2페이지마다 분할하면 네 세그먼트가 나옵니다 — 2페이지짜리 셋과 1페이지짜리 하나입니다.$pagesPerSegment는 최소 1이어야 하며, 그렇지 않으면InvalidArgumentException이 발생합니다.- 빈 범위 목록은 거부됩니다.
$ranges === []로split()을 호출하면InvalidArgumentException이 발생합니다. 호출하기 전에 최소 하나의 범위를 구축하십시오. - 경계는 잘라 내는 대신 발생시킵니다.
maxBytes나maxRanges를 초과하면InvalidArgumentException이 발생합니다. 스플리터는 과도하게 큰 입력을 부분적으로 처리하지 않으므로, 워크로드에 맞게 두 경계를 모두 조정하십시오. - 암호화, 서명, 폼 보유 소스는 닫힌 상태로 실패합니다. 암호화된 소스(키
없이는 복사할 수 없음), 디지털 서명된 소스(다시 페이지 매김하면 서명 바이트
범위가 무효화됨), 또는 대화형 폼을 보유한 소스(필드의 위젯이 삭제된 페이지에
있어 고아가 될 수 있음)는
UnsupportedSourceDocumentException을 발생시킵니다. 스플리터는 손상되거나 손상된 문서를 내보내는 대신 거부합니다. 폼 문서 분할은 이 릴리스의 알려진 제한입니다. UnsupportedSourceDocumentException은Merge네임스페이스 아래에 있습니다. 그 정규화된 이름은NextPDF\Document\Merge\UnsupportedSourceDocumentException입니다. 분할 페이지의 그Merge경로는 복사/붙여넣기 오류가 아닙니다 — 이는 소스를 안전하게 복사할 수 없을 때 병합과 분할 표면 모두가 발생시키는, 단일하게 공유되는 소스 문서 거부 예외입니다. 그 네임스페이스에서 임포트하십시오.- 출력은 구조적으로 새롭지만 바이트 안정적이지 않습니다. 각 세그먼트는 자체
카탈로그, 페이지 트리, 트레일러를 가진 새 문서입니다. 동일한 입력에 대한 두
실행은 구조적으로 동등하지만 바이트 단위로 동일하다고 보장되지 않습니다 —
그래서
structural재현성 프로필입니다.
분할은 모든 범위에 걸쳐 복사되는 페이지 수에 선형입니다. 스플리터 자체의
부기가 아니라 소스를 파싱하고 각 범위의 객체 클로저를 복사하는 작업이 작업을
지배합니다. 소스는 메모리에 문자열로 보관되고, 각 세그먼트의 바이트는 기록할
때까지 보관되므로, 최대 메모리는 소스 크기에 생성하는 가장 큰 범위를 더한
값을 추적합니다. maxBytes 가드는 그 최대치의 소스 측을 제한된 상태로
유지합니다. 대량 파이프라인의 경우, 잘못된 형식이거나 과도하게 큰 입력이
메모리를 고갈시키는 대신 빠르게 실패하도록 maxBytes와 maxRanges를
워크로드가 필요로 하는 가장 작은 값으로 설정하십시오.
보안 참고 사항
섹션 제목: “보안 참고 사항”분할은 인프로세스로 실행됩니다. 어떤 문서 바이트도 호스트를 떠나지 않으며, 네트워크 호출도 하지 않습니다. 모든 소스 PDF를 신뢰할 수 없는 입력으로 취급하십시오.
- 경계를 타이트하게 유지하십시오.
maxBytes와maxRanges는 서비스 거부 입력에 대한 첫 번째 방어선입니다. 업로드를 받는 모든 표면에 대해, 넉넉한 기본값이 아니라 실제 상한으로 설정하십시오. - 분할 전에 분류하십시오. 암호화되거나 서명된 소스는 닫힌 상태로 실패하지만, 그런 조건을 더 일찍 감지할 수 있습니다. 신뢰할 수 없는 입력을 먼저 Core 검사기를 통해 실행하십시오. 더 무거운 처리 전에 암호화, 서명, 위험 마커를 표시하는 제한된 스캔은 PDF 파싱 및 검사를 참고하십시오.
- 사용자 입력을 경로에 절대 보간하지 마십시오. 이 레시피는 고정 디렉터리나 쿡북 사이드 채널에 기록합니다. 출력 경로와 세그먼트 이름을 요청 필드가 아니라 서버가 제어하는 값에서 도출하여 경로 순회를 피하십시오.
- 출력에 비밀 정보를 두지 마십시오. 보지 말아야 할 클라이언트에게 내부 식별자를 노출하는 위치나 이름으로 세그먼트 파일을 기록하지 마십시오.
적합성
섹션 제목: “적합성”이 레시피는 자체적인 규범적 표준 주장을 하지 않습니다. Core 분할 표면을 통해
하나의 문서를 분해하고 SplitDocument::isValid()의 %PDF 헤더 검사로 각
세그먼트를 정상성 검사합니다 — 이는 스플리터가 PDF를 내보냈다는 존재 검사이지
적합성이나 문서 구조 검증이 아닙니다. PdfSplitter가 각 세그먼트에 대해
재구축하는 페이지 트리와 상호 참조 구조는 /modules/core/document/ 레퍼런스에
설명된 PDF 2.0 구조입니다(ISO 32000-2:2020, 상호 참조 테이블 §7.5.4, 페이지
트리 §7.7.3). 버전, 페이지 수, 암호화, 서명 플래그를 포함하여 입력이나 출력
문서의 구조를 읽으려면,
PDF 파싱 및 검사에 문서화된 Core
검사기를 사용하십시오.
참고 자료
섹션 제목: “참고 자료”- Document 모듈 레퍼런스 — 전체 분할, 병합, 문서 파트 표면.
- 외부 PDF 병합 — 역 레시피: 여러 문서를 하나로 구성합니다.
- PDF 파싱 및 검사 — 분할 전에 신뢰할 수 없는 입력을 분류합니다.
- 예외 인식 오류 처리
—
PageLayoutException과UnsupportedSourceDocumentException뒤에 있는 NextPDF 예외 계층.