외부 PDF를 병합하거나 기존 문서에 페이지 추가하기
한눈에 보기
섹션 제목: “한눈에 보기”디스크에 여러 PDF 파일이 있고 하나의 PDF가 필요합니다. 이 레시피는 Core 병합 표면인 NextPDF\Document\PdfMerger로 기존 문서를 처음부터 끝까지 합치는 방법을 설명합니다. 원시 PDF 바이트 문자열을 전달하면, 병합기는 충돌을 피하기 위해 모든 객체 번호를 다시 매기고 하나의 페이지 트리와 하나의 상호 참조 테이블을 만든 다음, 디스크에 기록하거나 클라이언트에 스트리밍할 NextPDF\Document\MergeResult를 반환합니다.
동일한 표면은 개발자가 가장 자주 찾는 세 가지 작업을 지원합니다.
- 병합: 순서가 지정된 PDF 목록을 하나의 문서로 합칩니다.
- 추가: 기본 PDF 뒤에 두 번째 PDF를 더합니다.
- 앞에 붙이기: 새 문서를 입력 순서의 맨 앞에 두어 페이지를 추가합니다.
병합은 헤드리스 브라우저나 네트워크 호출 없이 프로세스 내에서 실행됩니다. Core 설치(composer require nextpdf/core:^3)와 읽을 수 있는 두 개 이상의 PDF 파일이 필요합니다.
composer require nextpdf/core:^3개념 개요
섹션 제목: “개념 개요”PDF는 루트가 /Pages 노드인 페이지 트리로 페이지를 구성하며, 상호 참조 테이블을 통해 모든 간접 객체를 찾습니다. 두 원본 문서를 합치면 객체 번호가 겹칩니다. 두 파일 모두 거의 항상 1 0 obj 객체, /Catalog, 그리고 /Pages 노드를 포함합니다. 바이트를 단순히 이어 붙이면 참조가 더 이상 해당 번호가 가리키는 위치를 가리키지 않으므로 손상된 파일이 만들어집니다.
PdfMerger가 이 문제를 해결합니다. 각 입력에서 페이지 객체를 추출하고, 모든 객체 번호를 하나의 주소 공간 안에서 다시 매기며, 각 페이지의 /Parent 참조가 병합된 단일 /Pages 노드를 가리키도록 다시 쓴 다음, 하나의 카탈로그, 하나의 페이지 트리, 하나의 트레일러를 내보냅니다. 출력은 단순히 묶어 이어 붙인 결과가 아니라 구조적으로 새로운 문서입니다.
순서 규칙은 직관적입니다. 페이지는 해당 원본 파일이 입력 목록에 나타나는 순서대로 표시됩니다. 뒤에 추가하려면 기본 문서를 맨 앞에 둡니다. 앞에 붙이려면 새 문서를 맨 앞에 둡니다. 입력 순서가 필요한 유일한 제어 수단이므로, 별도의 앞에 붙이기 메서드는 없습니다.
API 표면
섹션 제목: “API 표면”new NextPDF\Document\PdfMerger()는 두 개의 메서드를 노출합니다.
merge(list<string> $pdfFiles, int $maxFiles = 100, int $maxTotalBytes = 200_000_000): MergeResult는 순서가 지정된 원시 PDF 바이트 문자열 목록을 합칩니다. 두 제한 매개변수는 파일 개수와 총 입력 크기를 제한합니다. 둘 다 안전한 프로덕션 값을 기본값으로 가지며, 워크로드에 맞춰 더 엄격하게 조정합니다.append(string $basePdf, string $appendPdf): MergeResult는 정확히 두 개의 문서를 순서대로 병합하는 편의 래퍼입니다. 이는merge([$basePdf, $appendPdf])와 동일합니다.
둘 다 NextPDF\Document\MergeResult를 반환합니다. 이 객체는 readonly이며 $pdfData(병합된 바이트), $totalPages, $sourceCount, $mergedSize, 그리고 출력이 %PDF 헤더로 시작하는지 확인하는 isValid() 헬퍼를 담고 있습니다.
입력은 파일 경로가 아니라 원시 바이트 문자열입니다. file_get_contents()로 파일을 직접 읽습니다(또는 객체 스토리지에서 바이트를 가져옵니다). 이렇게 하면 병합기가 파일 시스템에 대한 가정에서 벗어날 수 있고, 디스크를 전혀 거치지 않는 문서도 병합할 수 있습니다.
외부 PDF에서 단일 페이지를 재사용 가능한 Form XObject로 가져와야 한다면 — 예를 들어, 생성된 콘텐츠 뒤에 레터헤드 페이지를 찍기 위해 — 패키지 간 임포터 계약인 NextPDF\Contracts\ImportedFormObjectInterface를 사용하십시오. 이는 nextpdf/artisan과 같은 임포터가 구현합니다. 문서 전체 및 페이지 전체 구성에는 여기에 문서화된 PdfMerger 표면을 사용합니다.
코드 예제 — 빠른 시작
섹션 제목: “코드 예제 — 빠른 시작”이 예제는 두 개의 파일을 읽고 병합 결과를 기록합니다. 호출 형태를 보여 주기 위해 오류 처리를 생략했으며, 아래 프로덕션 예제에서는 전체 가드를 추가합니다.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Document\PdfMerger;
$merger = new PdfMerger();
$result = $merger->merge([ file_get_contents(__DIR__ . '/cover.pdf'), file_get_contents(__DIR__ . '/body.pdf'), file_get_contents(__DIR__ . '/appendix.pdf'),]);
file_put_contents(__DIR__ . '/combined.pdf', $result->pdfData);
printf("Merged %d source(s) into %d page(s).\n", $result->sourceCount, $result->totalPages);코드 예제 — 프로덕션
섹션 제목: “코드 예제 — 프로덕션”이 예제는 자체 완결형 프로그램입니다. 외부 파일 없이 실행되도록 메모리에 두 개의 작은 문서를 만들고, 이를 병합하고, 결과를 검증한 다음, 출력을 기록합니다. 병합 표면이 발생시키는 두 가지 예외를 잡고, 각각을 무시하지 않고 컨텍스트와 함께 다시 던집니다. 메모리 내 입력을 직접 수행하는 file_get_contents() 읽기(또는 객체 스토리지 가져오기)로 교체하고, 출력을 응답 또는 스토리지 계층에 연결하십시오.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Document\MergeResult;use NextPDF\Document\PdfMerger;use NextPDF\Exception\PageLayoutException;use NextPDF\Exception\WriterException;
/** * Build a tiny labelled PDF so the program is self-contained. * * In your own code, replace calls to this helper with reads of the external * PDFs you want to combine, for example file_get_contents($path). */function buildSample(string $label, int $pages): string{ $doc = Document::createStandalone(); $doc->setTitle($label);
for ($page = 1; $page <= $pages; $page++) { $doc->addPage(); $doc->setFont('helvetica', '', 12); $doc->cell(0, 10, sprintf('%s - page %d', $label, $page), newLine: true); }
return $doc->getPdfData();}
// Validate the input set before touching the merger. An empty set is a// configuration error, not an empty success./** @var list<string> $sources Raw PDF byte strings, in output order. */$sources = [ buildSample('Cover', 1), // first in the list -> first in the output (prepend position) buildSample('Body', 2), buildSample('Appendix', 1), // last in the list -> appended after the body];
if ($sources === []) { throw new RuntimeException('No source PDFs supplied to merge.');}
$merger = new PdfMerger();
try { // Bound the merge deliberately: at most 50 files, 100 MB total input. $result = $merger->merge($sources, maxFiles: 50, maxTotalBytes: 100_000_000);} catch (PageLayoutException $e) { // Raised when the list is empty or an input does not begin with %PDF. throw new RuntimeException( sprintf('Merge rejected an input: %s', $e->getConstraint()), previous: $e, );} catch (WriterException $e) { // Raised when the total input size exceeds the configured byte cap. throw new RuntimeException( sprintf('Merge exceeded its size budget at stage "%s".', $e->getWriterState()), previous: $e, );}
if (!$result->isValid()) { throw new RuntimeException('Merged output failed its structural header check.');}
emitResult($result);
/** * Write the merged document to the cookbook side-channel, or to a default file. */function emitResult(MergeResult $result): void{ printf( "Merged %d source(s) into %d page(s), %d bytes.\n", $result->sourceCount, $result->totalPages, $result->mergedSize, );
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT'); $path = $out !== false && $out !== '' ? $out : __DIR__ . '/combined.pdf';
if (file_put_contents($path, $result->pdfData) === false) { throw new RuntimeException(sprintf('Could not write merged PDF to "%s".', $path)); }}예상 STDOUT(페이지 총합은 원본 페이지 수의 합이며, 바이트 크기는 빌드에 따라 달라집니다):
Merged 3 source(s) into 4 page(s), <n> bytes.엣지 케이스 및 주의 사항
섹션 제목: “엣지 케이스 및 주의 사항”- 입력은 경로가 아니라 바이트입니다.
merge()는 원시 PDF 문자열을 받습니다. 먼저file_get_contents()로 파일을 읽으십시오. 경로 문자열을 전달하면 입력이%PDF헤더 검사에 실패하고PageLayoutException을 발생시킵니다. - 순서가 곧 출력 순서입니다. 페이지는 해당 원본 파일이 목록에 나타나는 순서대로 배치됩니다. 앞에 붙이기 메서드는 없습니다. 앞에 붙이려면 새 문서를 맨 앞에, 뒤에 추가하려면 맨 뒤에 두십시오.
- 빈 목록은 오류입니다. 비어 있는
$pdfFiles는 빈 결과가 아니라PageLayoutException을 발생시킵니다. 호출하기 전에 집합을 검증하십시오. - 모든 입력은 사전에 검증됩니다. 각 항목은 비어 있지 않아야 하며
%PDF로 시작해야 합니다. 실패한 첫 번째 입력은 위반된 제약 조건을 담은PageLayoutException을 발생시키며, 아무것도 병합되지 않습니다. - 한계는 잘라내지 않고 예외를 발생시킵니다.
maxFiles를 초과하면 내부 리소스 가드를 통해 예외가 발생하고,maxTotalBytes를 초과하면WriterException이 발생합니다. 병합기는 파일을 조용히 누락하거나 바이트를 잘라내지 않으므로, 워크로드에 맞춰 두 한계를 모두 조정하십시오. - 출력은 바이트가 안정적인 것이 아니라 구조적으로 새롭습니다. 병합된 문서는 새로운 카탈로그, 페이지 트리, 트레일러를 담습니다. 동일한 입력에 대한 두 번의 실행은 구조적으로 동일하지만 바이트가 동일하다고 보장되지는 않으며, 이 때문에 이 레시피는
structural재현성 프로필을 선언합니다. - 페이지 수준 주석 및 공유 리소스. 병합은 페이지 객체를 하나의 트리로 구성합니다. 원본 파일에서 페이지 객체 외부에 존재하는 문서 수준 구조는 이전되지 않습니다. 단일 페이지를 리소스와 함께 재사용 가능한 그래픽으로 가져와야 한다면,
ImportedFormObjectInterface경로를nextpdf/artisan과 같은 임포터를 통해 사용하십시오.
병합은 총 페이지 수에 대해 선형적이며, 병합기 자체의 관리 작업보다 파싱과 객체 번호 재지정의 영향이 더 큽니다. 출력이 조립되는 동안 모든 원본이 문자열로 메모리에 유지되므로, 최대 메모리는 총 입력 바이트에 비례합니다. maxTotalBytes 가드는 그 최대치를 제한된 범위 안에 유지합니다. 대용량 파이프라인에서는 maxFiles와 maxTotalBytes를 워크로드에 필요한 가장 작은 값으로 설정하여, 형식이 잘못되었거나 크기가 과도한 배치가 메모리를 고갈시키는 대신 빠르게 실패하도록 하십시오. 일반적인 소규모 병합은 1500 ms 실행 시간과 64 MB 최대 예산 안에 들어갑니다.
보안 참고 사항
섹션 제목: “보안 참고 사항”병합은 프로세스 내에서 실행됩니다. 문서 바이트는 호스트를 벗어나지 않으며 네트워크 호출도 이루어지지 않습니다. 모든 외부 PDF를 신뢰할 수 없는 입력으로 취급하십시오.
- 한계를 엄격하게 유지하십시오.
maxFiles와maxTotalBytes는 서비스 거부 입력에 대한 첫 번째 방어선입니다. 업로드를 받는 모든 표면에서는 관대한 기본값이 아니라 실제 상한선으로 설정하십시오. - 신뢰하기 전에 검증하십시오. 병합이 성공했다는 것은 바이트가 합쳐졌다는 의미이지, 입력이 안전하다는 의미는 아닙니다. 신뢰할 수 없는 입력은 먼저 Core 검사기를 통해 실행하십시오. 더 무거운 처리에 앞서 암호화, 서명, 위험 표시를 식별하는 제한된 분류 스캔에 대해서는 PDF 파싱 및 검사를 참조하십시오.
- 사용자 입력을 경로에 절대 끼워 넣지 마십시오. 이 레시피는 고정된 경로 또는 쿡북 측면 채널에 기록합니다. 경로 탐색을 방지하려면 출력 경로를 요청 필드가 아니라 서버에서 제어하는 값에서 도출하십시오.
- 문서에 비밀 정보를 넣지 마십시오. 클라이언트에 반환하는 병합된 문서에 자격 증명, 토큰, 내부 식별자를 포함하지 마십시오.
적합성
섹션 제목: “적합성”이 레시피는 자체적으로 규범적 표준을 주장하지 않습니다. Core 병합 표면을 통해 기존 문서를 구성하고 MergeResult::isValid() 헤더 검사로 결과를 검증합니다. PdfMerger가 다시 구축하는 페이지 트리 모델은 /modules/core/document/ 참조에 설명된 PDF 2.0 페이지 트리 구조입니다. 입력 또는 출력 문서의 구조적 판독 — 버전, 페이지 수, 암호화 및 서명 플래그 — 에는 PDF 파싱 및 검사에 문서화된 Core 검사기를 사용하십시오.
함께 보기
섹션 제목: “함께 보기”- Document 모듈 참조 — 전체 분할, 병합, 문서 부분 표면입니다.
- PDF 파싱 및 검사 — 신뢰할 수 없는 입력을 병합하기 전에 분류합니다.
- 예외 인식 오류 처리 —
PageLayoutException과WriterException뒤에 있는 NextPDF 예외 계층 구조입니다. - 다중 페이지 문서 만들기 — 나중에 합칠 페이지를 작성합니다.