Document: DPart, split / merge 및 벤더 확장
한눈에 보기
섹션 제목: “한눈에 보기”Document 모듈은 페이지 콘텐츠가 아닌 PDF 문서 전체를 대상으로 동작합니다. 규제 워크플로에서 메타데이터를 붙일 수 있는 Document Part 계층 구조를 구성하고, PDF를 페이지 범위 세그먼트로 분할하며, 여러 PDF를 하나로 병합합니다. 또한 개발자 확장을 문서 카탈로그에 등록합니다.
composer require nextpdf/core:^3개념 개요
섹션 제목: “개념 개요”이 모듈은 페이지 콘텐츠보다 상위 계층에서 동작합니다. Graphics와 Content가 연산자를 내보내는 반면, Document는 구조 수준 — 페이지 트리, 문서 카탈로그, Document Part 트리 — 에서 동작합니다.
Document Part(DPart)는 PDF의 논리적 분할 단위입니다. ISO 32000-2는 노드마다 Document Part Metadata(DPM)를 담는 DPart 계층 구조를 정의합니다. 예를 들어 제약, 법률, 아카이빙과 같은 규제 워크플로는 파일 전체가 아니라 페이지 하위 범위에 메타데이터를 연결할 수 있습니다 — §14.12. DPart는 불변 readonly 노드입니다. 리프 노드는 연속된 페이지 인덱스 구간을 참조하고, 중간 노드는 하위 DPart 노드를 트리로 묶습니다. DPartRoot는 Writer가 직렬화하는 트리 루트입니다. 리프 노드의 /Start 및 /End 엔트리는 페이지 인덱스 정수가 아닌 페이지 객체에 대한 간접 참조입니다 — §14.12. DPart::resolveWithPageObjects()는 Writer가 제공하는 페이지 인덱스→객체 번호 맵을 해석하고 /Start(및 선택적 /End) 참조 형태를 반환합니다. 맵을 사용할 수 없는 테스트 경로에서만 정수 형태로 폴백합니다.
PdfMerger와 PdfSplitter는 문서 구성용 API 표면입니다. PdfMerger는 여러 입력 PDF의 페이지 객체를 결합하고, 충돌을 피하도록 객체 번호를 다시 매기며, 단일 페이지 트리와 상호 참조 테이블을 재구성합니다. 이때 생성되는 페이지 트리는 Kids와 Count를 갖는 균형 잡힌 Pages 노드이며, PDF가 페이지 트리 노드에 대해 정의하는 상속 가능 속성 모델을 따릅니다 — §7.7.3. PdfSplitter는 그 반대 작업으로, 페이지 범위를 독립적인 SplitDocument 객체로 추출합니다. PageRange는 둘 모두에서 사용하는 값 객체입니다. 1-based이며, 경계를 검증하고, contains(), count(), toArray()에 응답합니다.
VendorExtensionRegistry, ExtensionsDictionary, DeveloperExtensionEntry는 문서 카탈로그의 개발자 확장 사전을 모델링합니다 — 엔진이 기본 사양을 넘어서는 벤더 확장 레벨을 선언하는 메커니즘입니다. 레지스트리는 동일한 벤더 접두사를 충돌하는 형태로 다시 등록하려는 시도를 VendorExtensionRegistryConflictException으로 거부합니다. CollectionDictionary와 CollectionSort는 PDF collection(포터블 컬렉션 / 포트폴리오) 카탈로그 엔트리를 모델링합니다.
API 표면
섹션 제목: “API 표면”| 클래스 | 주요 메서드 | 역할 |
|---|---|---|
DPart | isLeaf(), hasMetadata(), resolveWithPageObjects(), write() | 불변 Document Part 노드(@since 1.12.0) |
DPartRoot | isEmpty(), write() | Writer가 직렬화하는 DPart 트리 루트(@since 1.12.0) |
PdfMerger | merge(array $pdfFiles, int $maxFiles = 100, int $maxTotalBytes = 200_000_000), append() | 객체 번호 재지정을 포함한 다중 PDF 병합(@since 1.9.0) |
PdfSplitter | split(), splitEvery(), extractPages() | 페이지 범위를 SplitDocument로 분할(@since 1.9.0) |
PageRange | contains(int $page), count(), toArray() | 1-based 페이지 범위 값 객체 |
MergeResult / SplitResult | isValid(), count(), document(), totalOutputSize() | 구성 결과 객체 |
VendorExtensionRegistry | 확장 등록 | 개발자 확장 레지스트리(@since 2.2.0) |
ExtensionsDictionary | withEntry(), entries(), isEmpty(), toPdfDictionary() | 불변 확장 사전 빌더(@since 2.0.0) |
CollectionDictionary | toPdfDictionary() | 포터블 컬렉션 카탈로그 엔트리(@since 2.0.0) |
전체 PHPDoc 표를 보려면 composer docs:generate-api-php -- --module=Document를 실행하십시오.
코드 샘플 — 빠른 시작
섹션 제목: “코드 샘플 — 빠른 시작”PDF를 여러 개의 한 페이지짜리 문서로 분할하고 결과를 확인합니다.
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Document\PageRange;use NextPDF\Document\PdfSplitter;
$splitter = new PdfSplitter();$result = $splitter->splitEvery(file_get_contents('/srv/in/report.pdf'), 1);
foreach (range(0, $result->count() - 1) as $index) { $segment = $result->document($index); file_put_contents("/srv/out/page-{$index}.pdf", $segment->pdfData);}
$singlePage = $splitter->extractPages( file_get_contents('/srv/in/report.pdf'), new PageRange(2, 4),);코드 샘플 — 프로덕션
섹션 제목: “코드 샘플 — 프로덕션”명시적인 입력 예산을 적용해 여러 PDF를 병합한 다음, 병합된 출력을 쓰기 전에 유효성을 확인합니다.
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Document\PdfMerger;use NextPDF\Exception\PageLayoutException;
/** @var list<string> $sources Raw PDF byte strings to combine. */$sources = array_map( static fn (string $path): string => file_get_contents($path), glob('/srv/batch/*.pdf') ?: [],);
$merger = new PdfMerger();
try { // Bound the merge: at most 50 files, 100 MB total. $merged = $merger->merge($sources, maxFiles: 50, maxTotalBytes: 100_000_000);} catch (PageLayoutException $e) { throw new \RuntimeException('Merge rejected: empty or invalid input set.', previous: $e);}
if (!$merged->isValid()) { throw new \RuntimeException('Merged document failed structural validation.');}
file_put_contents('/srv/out/combined.pdf', $merged->pdfData);엣지 케이스 및 주의 사항
섹션 제목: “엣지 케이스 및 주의 사항”PdfMerger::merge()와PdfSplitter::split()는ResourceGuard를 통해 입력 경계를 강제합니다. 개수나 크기 제한을 초과한 입력은 조용히 잘라내지 않고 예외를 발생시킵니다. 워크로드에 맞춰maxFiles/maxTotalBytes를 의도적으로 조정하십시오.- 빈 파일 목록 또는 빈 범위 목록은
PageLayoutException을 발생시킵니다 — 이는 빈 결과가 아니라 구성 오류입니다. PageRange는 1-based이며 양 끝을 포함합니다. 리프DPart의pages목록은 0-based 페이지 인덱스입니다. 두 추상화는 인덱스 기준을 공유하지 않습니다. 둘을 넘나들 때는 명시적으로 변환하십시오.DPart는readonly입니다. 다른 트리를 만들려면 기존 노드를 변경하는 대신 새 노드를 구성해야 합니다.resolveWithPageObjects()는 페이지 객체 맵이 비어 있을 때만 정수 인덱스 폴백 형태를 반환합니다. 프로덕션 출력에서는 그 경로에 의존하지 마십시오.VendorExtensionRegistry는 벤더 접두사가 중복되면VendorExtensionRegistryConflictException을 발생시킵니다. 각 접두사는 한 번만 등록하십시오.
분할과 병합은 페이지 수에 선형적으로 비례하며, 성능은 모듈 자체의 부기보다 파싱과 객체 번호 재지정에 주로 좌우됩니다. 기본 참조 워크로드는 1500 ms 벽시계 시간 / 64 MB 피크 예산 안에 들어갑니다. 대규모 병합에서는 총 입력 바이트 수가 주된 제약입니다. maxTotalBytes 가드는 피크 메모리를 제한된 상태로 유지하기 위해 존재합니다. 재현성 프로파일은 structural입니다. 병합되거나 분할된 PDF는 새로운 트레일러와 /ID를 가지므로, 두 번의 실행은 구조적으로는 동일하지만 바이트 단위로 동일하지는 않습니다.
보안 참고 사항
섹션 제목: “보안 참고 사항”PdfMerger::merge()와 PdfSplitter::split()는 신뢰할 수 없는 PDF 바이트를 입력으로 받습니다. 둘 다 파싱 전에 입력을 ResourceGuard::assertSize() / assertCount()를 통과시키며, 이는 압축 또는 객체 수 증폭 기반의 서비스 거부를 제한합니다. 기본값에 의존하지 말고 배포 환경에 맞춰 maxFiles, maxTotalBytes, maxBytes 인자를 엄격하게 유지하십시오. 모든 입력 PDF를 적대적인 것으로 취급하십시오. 소스가 사용자 제공일 때는 제한된 워커에서 배치 구성을 실행하십시오. 신뢰 경계에 대해서는 /modules/core/security/의 엔진 위협 모델을 참조하십시오.
적합성
섹션 제목: “적합성”이 모듈이 구성하는 DPart 트리는 ISO 32000-2 §14.12의 Document Part 모델을 따르며, 리프 /Start 및 /End 엔트리는 동일한 절에 따라 페이지 객체에 대한 간접 참조로 방출됩니다. 병합된 출력은 §7.7.3에 정의된 페이지 트리 노드 구조를 사용합니다. 이는 src/Document/가 생성하고 tests/Unit/Document/(DPartTest, DPartRootTest, DPartPageRefTest, DocumentPdfMergerDeepTest, DocumentPageRangeParseDeepTest)가 검증하는 구현 사실입니다. 이는 종단 간 PDF 2.0 적합성에 대한 진술이 아닙니다. 전체 문서 적합성은 /modules/core/conformance/에 설명된 오라클과 골든 스위트에 의해 검증됩니다.
관련 항목
섹션 제목: “관련 항목”- 코어 모듈
- Writer 모듈 — DPart 트리와 페이지 트리를 직렬화합니다.
- Metadata 모듈 — DPM과 짝을 이루는 XMP입니다.
- Navigation 모듈
- 적합성 개요
- 엔진 보안 모델