파일 첨부 및 PDF 포트폴리오 생성
한눈에 보기
섹션 제목: “한눈에 보기”이 레시피는 하나 이상의 파일을 PDF에 첨부하고, 첨부 파일이 여러 개이면 PDF 포트폴리오로 구성하는 방법을 다룹니다. 문서가 근거 자료를 같은 파일 안에 함께 담아야 할 때 사용합니다. 예를 들어 근거가 되는 근무 시간표를 함께 제공하는 청구서, CAD(Computer-Aided Design) 내보내기 파일을 묶은 제품 데이터시트, 또는 렌더링된 보고서와 원본 스프레드시트를 함께 보관하는 아카이브 기록 등이 있습니다.
NextPDF는 문서 객체에 두 개의 진입점을 제공합니다. embedFile()는 디스크에서 파일을 읽고, embedFileFromString()는 런타임에 생성한 메모리 내 바이트를 임베드합니다. 두 메서드 모두 첨부 파일을 등록합니다. save() 시점에 엔진은 각 항목을 임베드 파일 스트림으로 작성하고, 이를 파일 사양 딕셔너리로 감싼 뒤, 모든 사양을 문서 수준의 EmbeddedFiles 이름 트리에 연결합니다. ISO 32000-2는 이 이름 트리를 임베드 파일 스트림이 이름 딕셔너리를 통해 문서 전체에 연결되는 지점으로 정의합니다.
이 기능은 상업적 제한 없이 사용할 수 있는 Core 기능입니다. 첨부 파일 API(Application Programming Interface)는 1.0.0 이후 안정적이며 8.1-8.4 백포트 매트릭스 전체에서 동작합니다.
composer require nextpdf/core:^3선택 확장은 필요 없습니다.
개념 개요
섹션 제목: “개념 개요”첨부 파일은 세 가지 PDF 구조를 거칩니다. 이를 이해하면 출력을 해석하고 준수하지 않는 파일을 디버깅하는 데 도움이 됩니다.
- 임베드 파일 스트림. 첨부 파일의 원시 바이트입니다. Flate로 압축되며
/Type이/EmbeddedFile인 스트림 객체로 작성됩니다. NextPDF는 원본 크기, MD5 체크섬, 수정 날짜를 스트림의 매개변수 딕셔너리에 기록합니다. 감지한 MIME(Multipurpose Internet Mail Extensions) 유형은 스트림/Subtype으로 인코딩됩니다. - 파일 사양 딕셔너리. 메타데이터 래퍼입니다. 여기에는 표시용 파일명(
/F및 유니코드/UF), 사람이 읽을 수 있는 설명(/Desc), 임베드 스트림에 대한 참조(/EF), 그리고 파일이 호스트 문서에 대해 갖는 관계(/AFRelationship)가 담깁니다. EmbeddedFiles이름 트리. 각 첨부 파일의 이름을 해당 파일 사양에 매핑하는 문서 수준의 단일 인덱스입니다. ISO 32000-2는 이 트리를 통해 도달하는 모든 파일 사양이, 임베드 파일 스트림을 참조하는 값을 갖는EF항목을 포함하도록 요구합니다. NextPDF는save()시점에 이 트리를 자동으로 구축하고 균형을 맞춥니다.
관계 값은 준수 측면에서 중요합니다. PDF Association Application Note 0002에 따르면 연관 파일에는 고정된 PDF 2.0 집합에서 선택한 AFRelationship 항목이 필요합니다. 즉 Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema, 또는 Unspecified 중 하나여야 합니다. NextPDF는 이 집합을 AFRelationship 열거형으로 모델링하며 그 밖의 값은 거부합니다. 파일이 존재하는 이유를 설명하는 용어를 선택하십시오. 청구서를 뒷받침하는 근무 시간표는 Source이고, 차트를 뒷받침하는 기계 판독 가능 데이터셋은 Data입니다.
그다음 상위 계층은 PDF 포트폴리오(ISO 32000-2에서는 컬렉션이라고 함)입니다. 문서에 여러 첨부 파일이 있을 때, 카탈로그 Collection 딕셔너리는 뷰어에 표시 방식을 알려줍니다. 정렬 가능한 상세 정보 테이블, 타일 레이아웃, 또는 숨겨진 봉투 형태가 그 예입니다. ISO 32000-2는 Collection 딕셔너리를 PDF 프로세서가 파일 첨부를 체계적인 포트폴리오로 표시하는 데 사용하는 제어 수단으로 설명합니다. NextPDF는 이를 CollectionDictionary 값 객체로 모델링하며, 상세 정보 뷰의 열 순서를 위해 CollectionSort를 사용합니다.
API 표면
섹션 제목: “API 표면”문서 수준 메서드(HasFileAttachments concern에서 제공되며, 이 concern은 \NextPDF\Core\Document에 있음):
embedFile(string $path, string $description = ''): static—$path에서 파일을 읽어 첨부합니다. MIME 유형은 확장자를 기준으로 감지되며, 관계는 기본값으로Unspecified가 됩니다. 최대 100 MB까지 읽으며, 더 큰 페이로드에는embedFileFromString()을 사용하십시오. 체이닝할 수 있도록 문서를 반환합니다.embedFileFromString(string $data, string $filename, string $description = '', string $afRelationship = '/Unspecified'): static— 메모리 내 바이트를 표시 이름인$filename으로 첨부합니다. 관계를 설정하려면AFRelationship리터럴(선행 슬래시는 포함해도 되고 생략해도 됨)을 전달하십시오. 체이닝할 수 있도록 문서를 반환합니다.
지원 타입(네임스페이스 \NextPDF\Navigation 및 \NextPDF\Document):
\NextPDF\Navigation\AFRelationship— 여덟 개의 유효한 관계 값으로 구성된 열거형입니다.AFRelationship::coerce()는 문자열 또는 열거형 케이스를 정규화하며 알 수 없는 값에는 예외를 던집니다.toPdfName()은/Name리터럴을 생성합니다.\NextPDF\Document\CollectionDictionary— 카탈로그Collection딕셔너리를 구축합니다.VIEW_DETAILS,VIEW_TILE,VIEW_HIDDEN,VIEW_CUSTOM,VIEW_NONE상수로 표시 모드를 선택하고, 생성자는 초기 문서 이름과 선택적 정렬도 인수로 받습니다.\NextPDF\Document\CollectionSort— 상세 정보 뷰 포트폴리오에서 사용하는 열 정렬 값 객체입니다.
코드 샘플 — 빠른 시작
섹션 제목: “코드 샘플 — 빠른 시작”이 최소 예제는 생성된 CSV(comma-separated values) 데이터셋을 청구서 페이지에 첨부하고, 이 데이터셋을 청구서가 작성된 기반인 Source 데이터로 선언합니다.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Navigation\AFRelationship;
$doc = Document::createStandalone();$doc->addPage();$doc->setFont('helvetica', 'B', 18);$doc->cell(0, 12, 'Invoice INV-2026-0042', newLine: true);
// Attach the line-item dataset the invoice was rendered from.$csv = "sku,qty,unit_price\nA-100,3,49.00\nB-220,1,180.00\n";$doc->embedFileFromString( data: $csv, filename: 'line-items.csv', description: 'Source line items for INV-2026-0042', afRelationship: AFRelationship::Source->value,);
$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/invoice-with-attachment.pdf');뷰어는 첨부 파일 패널에 line-items.csv를 표시하며, 관계는 이 파일이 청구서가 파생된 원본임을 나타냅니다.
코드 샘플 — 프로덕션
섹션 제목: “코드 샘플 — 프로덕션”다음 전체 예제는 디스크의 파일과 메모리 내 데이터셋을 첨부하고, 디스크 경로를 읽기 전에 허용 목록으로 지정한 기준 디렉터리 안에 있는지 검증하며, 첨부 파일 위에 정렬 가능한 포트폴리오를 구축합니다. 첨부 파일 경로에서 발생할 수 있는 가장 구체적인 NextPDF 예외를 포착한 다음, 실패를 숨기지 않고 정의된 종료 코드를 반환합니다.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Document\CollectionDictionary;use NextPDF\Document\CollectionSort;use NextPDF\Exception\CompressionException;use NextPDF\Exception\InvalidConfigException;use NextPDF\Exception\PageLayoutException;use NextPDF\Navigation\AFRelationship;
/** * Resolve a caller-supplied filename against an allowed base directory. * * Rejects path traversal and stream wrappers so an embedded attachment can * never read outside the directory the application owns. Returns the * canonical absolute path, or null when the input escapes the base. * * @param non-empty-string $baseDir Absolute path to the allowed directory. * @param non-empty-string $userName Untrusted filename from the request. */function resolveWithinBase(string $baseDir, string $userName): ?string{ $base = \realpath($baseDir); if ($base === false) { return null; }
$candidate = \realpath($base . \DIRECTORY_SEPARATOR . \basename($userName)); if ($candidate === false || !\str_starts_with($candidate, $base . \DIRECTORY_SEPARATOR)) { return null; }
return $candidate;}
$attachmentsDir = __DIR__ . '/attachments';$requestedFile = 'timesheet-2026-05.pdf';
$safePath = resolveWithinBase($attachmentsDir, $requestedFile);if ($safePath === null) { \fwrite(\STDERR, "Rejected attachment path: outside the allowed directory\n"); exit(2);}
try { $doc = Document::createStandalone(); $doc->setTitle('Invoice INV-2026-0042 with supporting documents'); $doc->addPage(); $doc->setFont('helvetica', 'B', 18); $doc->cell(0, 12, 'Invoice INV-2026-0042', newLine: true);
// 1. A validated file from disk: the supporting timesheet. $doc->embedFile( $safePath, 'Timesheet supporting the billed hours', );
// 2. An in-memory dataset generated at runtime. $lineItems = "sku,qty,unit_price\nA-100,3,49.00\nB-220,1,180.00\n"; $doc->embedFileFromString( data: $lineItems, filename: 'line-items.csv', description: 'Machine-readable line items', afRelationship: AFRelationship::Data->value, );
// Present both attachments as a sortable details portfolio. The sort // keys reference columns declared in the portfolio /Schema; here the // built-in filename and modification-date fields order the view. $portfolio = new CollectionDictionary( view: CollectionDictionary::VIEW_DETAILS, initialDocument: 'line-items.csv', sort: new CollectionSort( keys: ['_Filename', '_ModDate'], ascending: [true, false], ), ); // $portfolio->toPdfDictionary() yields the catalog /Collection literal, // shared with the unencrypted-wrapper envelope path.
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/invoice-portfolio.pdf'; $doc->save($out);
echo "Wrote {$out} with 2 attachments and a details portfolio\n";} catch (PageLayoutException $e) { // Unreadable path, oversized file, null byte, or a MIME-type name that // exceeds the 127-byte PDF name limit. \fwrite(\STDERR, "Attachment rejected: {$e->getMessage()}\n"); exit(1);} catch (CompressionException | InvalidConfigException $e) { // The attachment data could not be compressed, or a config value was invalid. \fwrite(\STDERR, "Write failed: {$e->getMessage()}\n"); exit(1);}CollectionDictionary와 CollectionSort는 값 객체입니다. 이들은 생성 시점에 입력을 검증하고, 뷰어에서 포트폴리오 뷰를 구동하는 카탈로그 /Collection 리터럴로 직렬화됩니다.
엣지 케이스 및 주의 사항
섹션 제목: “엣지 케이스 및 주의 사항”- 경로 입력은 사용자의 책임입니다.
embedFile()은 널 바이트와 스트림 래퍼를 방어하고 실제 경로를 해석하지만, 기준 디렉터리 허용 목록까지 강제하지는 않습니다. 요청에서 받은 경로라면 프로덕션 샘플이resolveWithinBase()로 하는 것처럼 먼저 검증하십시오. - 100 MB 상한은
embedFile()에만 적용됩니다.104,857,600바이트를 초과하는 파일은PageLayoutException을 발생시킵니다. 더 큰 페이로드는 바이트를 직접 스트리밍하여embedFileFromString()에 전달하십시오. - 긴 MIME 유형 이름은 거부됩니다. 감지한 MIME 유형은 임베드 스트림의
/Subtype이 되며, 이는 ISO 32000-2에 의해 127 바이트로 제한되는 PDF 이름 토큰입니다. 비정상적으로 긴 유형(일부 Office 형식은 90 바이트에 근접)도 한도를 충분히 밑돌지만, 직접 지정한 유형이 이를 초과하면PageLayoutException을 발생시킵니다. 재정의할 특별한 이유가 없다면 엔진이 확장자에서 유형을 감지하도록 두십시오. - 알 수 없는 관계는 예외를 던집니다.
AFRelationship::coerce()는 고정 집합을 벗어난 값을Unspecified로 격하하는 대신 거부합니다. 오타가 런타임까지 넘어가지 않도록 열거형 케이스(AFRelationship::Source->value)를 전달하십시오. - 파일명은 이름 트리에서 서로 구별되어야 합니다. 표시 이름이 같은 두 첨부 파일은
EmbeddedFiles인덱스에서 충돌합니다. 각 첨부 파일에 고유한 파일명을 부여하십시오. _ModDate는 협정 세계시(UTC)로 기록됩니다.embedFile()은 파일의 수정 시각을 읽어gmdate()로 기록하므로, 동일한 픽스처는 시간대 설정과 관계없이 머신 전반에서 바이트 단위로 동일한 날짜를 생성합니다.
각 첨부 파일은 gzcompress()로 레벨 9에서 한 번 압축되며 save() 시점에 단일 스트림으로 작성됩니다. 압축이 비용의 대부분을 차지하며, 페이지 콘텐츠가 아니라 첨부된 페이로드 크기에 따라 늘어납니다. 소수의 작은 보조 파일(데이터셋, 스프레드시트, 근무 시간표 PDF)은 2000 ms / 64 MB 예산 안에 들어옵니다. 다수의 대용량 첨부 파일의 경우, 임베드 바이트가 최소 메모리 사용량이 됩니다. 문자열로 보유한 50 MB 첨부 파일은 압축 전에 최소한 그만큼의 메모리를 차지합니다. 여러 대용량 파일을 한 번에 로드하는 것보다 청크 단위 생성과 함께 embedFileFromString()을 사용하는 것이 좋습니다.
이름 트리는 save() 시점에 한 번 구축됩니다. 최대 64개 항목까지는 평면 단일 루트 트리로 유지됩니다. 이를 넘으면 NextPDF는 트리를 균형 잡힌 Kids 및 Limits 범위로 분할하므로, 대규모 첨부 파일 집합에서도 인덱스 비용이 로그 수준으로 유지됩니다.
보안 참고 사항
섹션 제목: “보안 참고 사항”- 신뢰할 수 없는 모든 경로를 허용 목록에 대해 검증하십시오. 임베딩 과정에서는 PHP 프로세스가 접근할 수 있는 모든 파일을 읽습니다. 기준 디렉터리 검사가 없으면 조작된 파일명이 첨부를 로컬 파일 포함(LFI, Local File Inclusion)으로 바꿔버립니다. 프로덕션 샘플은 허용 목록 가드를 보여줍니다. 파일명이 컴파일 타임 상수가 아닐 때마다 이를 적용하십시오.
- 첨부된 바이트를 소비 측에서 신뢰할 수 없는 것으로 취급하십시오. 임베드 파일은 NextPDF에는 불투명합니다. 엔진은 이를 파싱하거나 실행하지 않습니다. 위험은 파일이 나중에 열리는 지점에 있습니다. 다운스트림 소비자가 추출하기 전에 각 첨부 파일이 무엇인지 알 수 있도록 관계와 설명을 설정하십시오.
- 첨부 파일이나 설명에 비밀 정보를 넣지 마십시오. 문서 전체가 암호화되지 않는 한 파일명, 설명, 바이트는 평문으로 저장됩니다. 첨부 파일을 보호하려면 권한 정책으로 문서를 암호화하십시오(관련 레시피 참조). 렌더링된 페이지에 두지 않을 자격 증명, 키 또는 개인 데이터는 임베드하지 마십시오.
- 이 레시피에서는 네트워크 접근이 발생하지 않습니다. 모든 바이트는 검증된 로컬 경로에서 읽히거나 메모리에서 제공됩니다.
| 명제 | 사양 | 절 | reference_id (참조 ID) |
|---|---|---|---|
임베드 파일 스트림은 이름 딕셔너리의 EmbeddedFiles 항목을 통해 문서에 연결됩니다. | ISO 32000-2 | 7.11.4 | |
해당 EmbeddedFiles 이름 트리는 이름을 EF 항목으로 임베드 파일 스트림을 참조하는 파일 사양에 매핑합니다. | ISO 32000-2 | 7.7.4 | |
연관 파일에는 고정된 PDF 2.0 집합에서 가져온 AFRelationship 값이 필요합니다. | PDF Association AN002 권고 | 3 | |
카탈로그 Collection 딕셔너리는 첨부 파일의 포트폴리오 표시를 제어합니다. | ISO 32000-2 | 7.11.6 |
재현성 프로파일 — 구조적. 트레일러 /ID, 저장 때마다 달라지는 날짜 원자, 그리고 임베드 스트림 /ModDate는 실행마다 달라지므로, 구조적 비교에서는 객체 그래프를 비교하기 전에 이들을 제거합니다. 이 레시피는 NextPDF가 이 구조를 어떻게 생성하는지 설명합니다. 전체 문서에 좌우되는 포괄적인 PDF/A-4f 준수를 단언하지는 않습니다. 모든 첨부 파일이 관계와 설명을 선언하도록 요구하는 아카이브 프로파일에 대해서는 PDF/A-4 레시피를 참조하십시오.