Bỏ qua để đến nội dung

Nhúng tệp và tạo PDF portfolio

Công thức này đính kèm một hoặc nhiều tệp vào PDF và, khi có nhiều tệp đính kèm, sắp xếp chúng thành một PDF portfolio. Hãy dùng nó khi một tài liệu cần lưu kèm tài liệu hỗ trợ trong cùng một tệp: một hóa đơn kèm bảng chấm công gốc, một bảng thông số sản phẩm kèm bản xuất Computer-Aided Design (CAD), hoặc một hồ sơ lưu trữ đặt bảng tính nguồn cạnh báo cáo đã kết xuất.

NextPDF cung cấp cho bạn hai điểm vào trên đối tượng tài liệu. embedFile() đọc một tệp từ ổ đĩa; embedFileFromString() nhúng các byte trong bộ nhớ được tạo lúc chạy. Cả hai đều đăng ký tệp đính kèm. Khi gọi save(), engine ghi từng tệp đính kèm thành một luồng tệp nhúng, bọc luồng đó trong một file specification dictionary, rồi liên kết mọi specification vào name tree EmbeddedFiles ở cấp tài liệu. ISO 32000-2 định nghĩa name tree đó là nơi các luồng tệp nhúng được gắn với toàn bộ tài liệu thông qua name dictionary.

Đây là một tính năng Core không bị giới hạn bởi gói thương mại. Application Programming Interface (API) cho tệp đính kèm đã ổn định kể từ 1.0.0 và chạy trên toàn bộ ma trận backport 8.1-8.4.

Terminal window
composer require nextpdf/core:^3

Không cần extension tùy chọn.

Một tệp đính kèm được biểu diễn qua ba cấu trúc PDF. Hiểu các cấu trúc này giúp bạn kiểm tra đầu ra và gỡ lỗi khi tệp không tuân thủ.

  1. Luồng tệp nhúng. Các byte thô của tệp đính kèm được nén Flate rồi ghi thành một đối tượng stream có /Type/EmbeddedFile. NextPDF ghi kích thước gốc, một checksum MD5, và ngày sửa đổi vào parameter dictionary của stream. Nó mã hóa kiểu Multipurpose Internet Mail Extensions (MIME) được phát hiện thành /Subtype của stream.
  2. File specification dictionary. Lớp bọc metadata. Nó mang tên tệp hiển thị (/F/UF dạng Unicode), một mô tả mà reader có thể hiển thị (/Desc), một tham chiếu tới luồng tệp nhúng (/EF), và mối quan hệ của tệp với tài liệu chủ (/AFRelationship).
  3. Name tree EmbeddedFiles. Một chỉ mục duy nhất ở cấp tài liệu, ánh xạ tên của từng tệp đính kèm tới file specification của tệp đó. ISO 32000-2 yêu cầu mọi file specification truy cập được qua tree này phải có một entry EF với giá trị tham chiếu tới một luồng tệp nhúng. NextPDF dựng và cân bằng tree này cho bạn tại save().

Giá trị quan hệ có ý nghĩa quan trọng với tuân thủ. PDF Association Application Note 0002 nêu rằng một tệp liên kết cần có một entry AFRelationship được chọn từ tập cố định của PDF 2.0: Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema, hoặc Unspecified. NextPDF mô hình hóa tập đó thành enum AFRelationship và từ chối mọi giá trị khác. Hãy chọn thuật ngữ giải thích vì sao tệp đó có mặt: một bảng chấm công làm nền cho hóa đơn là Source; một tập dữ liệu máy đọc được làm nền cho biểu đồ là Data.

Một PDF portfolio (được gọi là collection trong ISO 32000-2) là lớp khái niệm nằm phía trên. Khi một tài liệu mang theo nhiều tệp đính kèm, Collection dictionary trong catalog cho reader biết cách trình bày chúng: một bảng chi tiết có thể sắp xếp, một bố cục ô lát, hoặc một bao thư ẩn. ISO 32000-2 mô tả Collection dictionary là cơ chế điều khiển mà một PDF processor dùng để trình bày các tệp đính kèm thành một portfolio có tổ chức. NextPDF mô hình hóa điều này thành value object CollectionDictionary, với CollectionSort cho thứ tự cột trong chế độ xem chi tiết.

Các phương thức cấp tài liệu được cung cấp bởi concern HasFileAttachments trên \NextPDF\Core\Document:

  • embedFile(string $path, string $description = ''): static — đọc một tệp từ $path và đính kèm tệp đó. NextPDF phát hiện kiểu MIME từ phần mở rộng; mối quan hệ mặc định là Unspecified. Đọc tối đa 100 MB; dùng embedFileFromString() cho các payload lớn hơn. Trả về tài liệu để gọi nối chuỗi.
  • embedFileFromString(string $data, string $filename, string $description = '', string $afRelationship = '/Unspecified'): static — đính kèm các byte trong bộ nhớ dưới tên hiển thị $filename. Truyền một literal AFRelationship (có hoặc không có dấu gạch chéo đứng đầu) để thiết lập mối quan hệ. Trả về tài liệu để gọi nối chuỗi.

Các kiểu hỗ trợ nằm trong các namespace \NextPDF\Navigation\NextPDF\Document:

  • \NextPDF\Navigation\AFRelationship — enum cho tám giá trị quan hệ hợp lệ. AFRelationship::coerce() chuẩn hóa một chuỗi hoặc một trường hợp enum và ném ngoại lệ khi gặp giá trị không xác định. toPdfName() xuất literal /Name.
  • \NextPDF\Document\CollectionDictionary — dựng Collection dictionary trong catalog. Các hằng VIEW_DETAILS, VIEW_TILE, VIEW_HIDDEN, VIEW_CUSTOM, và VIEW_NONE chọn chế độ trình bày; constructor cũng nhận tên tài liệu ban đầu và một sort tùy chọn.
  • \NextPDF\Document\CollectionSort — value object sắp xếp cột cho portfolio dạng xem chi tiết.

Ví dụ tối giản này đính kèm một tập dữ liệu comma-separated values (CSV) được tạo lúc chạy vào một trang hóa đơn và khai báo nó là dữ liệu Source mà hóa đơn được dựng từ đó.

<?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');

Reader hiển thị line-items.csv trong bảng tệp đính kèm, và mối quan hệ này đánh dấu nó là nguồn của hóa đơn.

Ví dụ hoàn chỉnh này đính kèm một tệp từ ổ đĩa và một tập dữ liệu trong bộ nhớ, xác thực đường dẫn trên ổ đĩa bằng một thư mục gốc trong danh sách cho phép trước khi đọc, rồi dựng một portfolio có thể sắp xếp cho các tệp đính kèm. Nó bắt các exception NextPDF cụ thể nhất có thể phát sinh trên đường xử lý tệp đính kèm, rồi trả về một mã thoát đã xác định thay vì nuốt lỗi.

<?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);
}

CollectionDictionaryCollectionSort là các value object. Chúng xác thực đầu vào ngay khi khởi tạo và tuần tự hóa thành literal /Collection trong catalog, nơi điều khiển chế độ xem portfolio trong reader.

Trường hợp đặc biệt & điểm cần lưu ý

Phần tiêu đề “Trường hợp đặc biệt & điểm cần lưu ý”
  • Đầu vào đường dẫn là trách nhiệm của bạn. embedFile() phòng vệ trước các byte null và stream wrapper rồi phân giải đường dẫn thật, nhưng nó không áp đặt danh sách cho phép theo thư mục gốc. Khi đường dẫn đến từ một request, hãy xác thực nó trước, như mẫu production làm với resolveWithinBase().
  • Mức trần 100 MB chỉ áp dụng cho embedFile(). Một tệp vượt quá 104,857,600 byte sẽ ném PageLayoutException. Đối với các payload lớn hơn, hãy tự stream byte và truyền chúng cho embedFileFromString().
  • Các tên kiểu MIME dài sẽ bị từ chối. Kiểu MIME được phát hiện trở thành /Subtype của luồng tệp nhúng, một PDF name token bị giới hạn ở 127 byte theo ISO 32000-2. Một kiểu dài bất thường (một số định dạng Office tiệm cận 90 byte) vẫn nằm dưới giới hạn khá xa, nhưng một kiểu được cung cấp thủ công vượt quá giới hạn đó sẽ ném PageLayoutException. Hãy để engine phát hiện kiểu từ phần mở rộng, trừ khi bạn có lý do cụ thể để ghi đè.
  • Một mối quan hệ không xác định sẽ ném ngoại lệ. AFRelationship::coerce() từ chối mọi giá trị nằm ngoài tập cố định thay vì hạ cấp xuống Unspecified. Hãy truyền một trường hợp enum (AFRelationship::Source->value) để lỗi gõ nhầm không lọt tới lúc chạy.
  • Các tên tệp phải khác biệt trong name tree. Hai tệp đính kèm có cùng tên hiển thị sẽ xung đột trong chỉ mục EmbeddedFiles. Hãy đặt tên tệp duy nhất cho mỗi tệp đính kèm.
  • _ModDate được ghi theo Coordinated Universal Time (UTC). embedFile() đọc thời gian sửa đổi tệp và ghi bằng gmdate() để cùng một fixture tạo ra ngày giống hệt nhau theo từng byte trên các máy khác nhau, bất kể thiết lập múi giờ.

Mỗi tệp đính kèm được nén một lần bằng gzcompress() ở mức 9 và được ghi thành một luồng đơn tại save(). Việc nén chiếm phần lớn chi phí và tỷ lệ thuận với kích thước payload đính kèm, chứ không phải với nội dung trang. Một vài tệp hỗ trợ nhỏ (tập dữ liệu, bảng tính, một tệp PDF bảng chấm công) vẫn nằm trong ngân sách 2000 ms / 64 MB. Với nhiều tệp đính kèm lớn, các byte nhúng là mức bộ nhớ tối thiểu: một tệp đính kèm 50 MB được giữ dưới dạng chuỗi sẽ chiếm ít nhất chừng đó trước khi nén. Hãy ưu tiên embedFileFromString() cùng cách tạo dữ liệu theo từng phần hơn là nạp nhiều tệp lớn cùng một lúc.

Name tree được dựng một lần tại save(). Tối đa 64 entry nằm trong một tree phẳng một gốc. Vượt quá mức đó, NextPDF phân vùng tree thành các dải KidsLimits cân bằng, nên chi phí lập chỉ mục vẫn ở mức logarit với các tập tệp đính kèm lớn.

  • Hãy xác thực mọi đường dẫn không đáng tin với một danh sách cho phép. Việc nhúng đọc bất kỳ tệp nào mà tiến trình PHP có thể truy cập tới. Nếu không kiểm tra thư mục gốc, một tên tệp được chủ ý tạo ra có thể biến tệp đính kèm thành Local File Inclusion (LFI). Mẫu production cho thấy cơ chế phòng vệ bằng danh sách cho phép; hãy áp dụng nó bất cứ khi nào tên tệp không phải là một hằng số ở thời điểm biên dịch.
  • Hãy coi các byte đính kèm là không đáng tin ở phía tiêu thụ. Một tệp nhúng là một khối dữ liệu mà NextPDF không diễn giải. Engine không phân tích cú pháp hay thực thi nó. Rủi ro nằm ở nơi tệp được mở về sau. Hãy đặt mối quan hệ và mô tả để thành phần tiêu thụ về sau biết mỗi tệp đính kèm là gì trước khi trích xuất nó.
  • Không để thông tin bí mật trong tệp đính kèm hoặc mô tả. Tên tệp, mô tả, và các byte được lưu ở dạng rõ trừ khi toàn bộ tài liệu được mã hóa. Để bảo vệ một tệp đính kèm, hãy mã hóa tài liệu bằng một chính sách quyền (xem công thức liên quan). Đừng nhúng thông tin xác thực, khóa, hay dữ liệu cá nhân mà bạn sẽ không đặt vào trang đã kết xuất.
  • Không có truy cập mạng nào xảy ra trong công thức này. Mọi byte đều được đọc từ đường dẫn cục bộ đã xác thực hoặc được cung cấp trong bộ nhớ.
Tuyên bốĐặc tảĐiều khoảnreference_id
Các luồng tệp nhúng gắn vào tài liệu thông qua entry EmbeddedFiles trong name dictionary.ISO 32000-27.11.4
Name tree EmbeddedFiles ánh xạ các tên tới các file specification có entry EF tham chiếu tới một luồng tệp nhúng.ISO 32000-27.7.4
Một tệp liên kết cần một giá trị AFRelationship từ tập cố định của PDF 2.0.PDF Association AN0023
Từ điển Collection trong catalog điều khiển cách trình bày portfolio cho các tệp đính kèm.ISO 32000-27.11.6

Hồ sơ tái lập — cấu trúc. /ID trong trailer, các giá trị ngày được tạo ở mỗi lần lưu, và /ModDate của luồng tệp nhúng thay đổi giữa các lần chạy, nên phép so sánh theo cấu trúc sẽ loại bỏ những giá trị đó trước khi so sánh object graph. Công thức này mô tả cách NextPDF tạo ra cấu trúc. Nó không khẳng định việc tuân thủ PDF/A-4f một cách toàn diện, vốn phụ thuộc vào toàn bộ tài liệu. Đối với một hồ sơ lưu trữ yêu cầu mọi tệp đính kèm phải khai báo một mối quan hệ và một mô tả, hãy xem công thức PDF/A-4.