Lewati ke konten

Menyematkan berkas dan membuat portofolio PDF

Resep ini melampirkan satu atau beberapa berkas ke PDF dan, ketika Anda memiliki beberapa lampiran, menyusunnya sebagai portofolio PDF. Gunakan resep ini ketika dokumen perlu membawa bukti pendukung dalam berkas yang sama: faktur beserta lembar waktu yang mendasarinya, lembar data produk beserta ekspor Computer-Aided Design (CAD), atau catatan arsip yang menyimpan spreadsheet sumber di samping laporan yang dirender.

NextPDF menyediakan dua titik masuk pada objek dokumen. embedFile() membaca berkas dari disk; embedFileFromString() menyematkan bita dalam memori yang Anda hasilkan saat runtime. Keduanya mendaftarkan lampiran tersebut. Saat save(), mesin menulis tiap lampiran sebagai aliran berkas tersemat, membungkusnya dalam kamus spesifikasi berkas, dan menautkan setiap spesifikasi ke pohon nama EmbeddedFiles pada tingkat dokumen. ISO 32000-2 mendefinisikan pohon nama tersebut sebagai tempat aliran berkas tersemat dilekatkan ke dokumen secara keseluruhan melalui kamus nama.

Ini adalah kemampuan Core tanpa batasan komersial. Application Programming Interface (API) lampiran telah stabil sejak 1.0.0 dan berjalan di seluruh matriks backport 8.1-8.4.

Terminal window
composer require nextpdf/core:^3

Tidak diperlukan ekstensi opsional.

Setiap lampiran melewati tiga struktur PDF. Memahaminya membantu Anda memeriksa keluaran dan men-debug berkas yang tidak sesuai.

  1. Aliran berkas tersemat. Bita mentah dari berkas yang dilampirkan dikompresi dengan Flate, lalu ditulis sebagai objek aliran dengan /Type bernilai /EmbeddedFile. NextPDF mencatat ukuran asli, checksum MD5, dan tanggal modifikasi di kamus parameter aliran tersebut. Mesin mengodekan tipe Multipurpose Internet Mail Extensions (MIME) yang terdeteksi sebagai /Subtype aliran tersebut.
  2. Kamus spesifikasi berkas. Pembungkus metadata yang membawa nama berkas yang ditampilkan (/F dan versi Unicode /UF), deskripsi yang dapat dibaca manusia (/Desc), referensi ke aliran tersemat (/EF), serta hubungan berkas dengan dokumen induk (/AFRelationship).
  3. Pohon nama EmbeddedFiles. Indeks tunggal pada tingkat dokumen yang memetakan nama setiap lampiran ke spesifikasi berkasnya. ISO 32000-2 mewajibkan setiap spesifikasi berkas yang dijangkau melalui pohon ini membawa entri EF yang nilainya mereferensikan aliran berkas tersemat. NextPDF membangun dan menyeimbangkan pohon ini untuk Anda saat save().

Nilai hubungan penting untuk kesesuaian. PDF Association Application Note 0002 menyatakan bahwa berkas terkait memerlukan entri AFRelationship yang dipilih dari himpunan tetap PDF 2.0: Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema, atau Unspecified. NextPDF memodelkan himpunan tersebut sebagai enum AFRelationship dan menolak nilai lainnya. Pilih istilah yang menjelaskan alasan keberadaan berkas tersebut: lembar waktu di balik faktur adalah Source; kumpulan data yang dapat dibaca mesin di balik grafik adalah Data.

Sebuah portofolio PDF (disebut collection dalam ISO 32000-2) adalah lapisan berikutnya di atas struktur tersebut. Ketika dokumen membawa beberapa lampiran, kamus Collection katalog memberi tahu pembaca cara menyajikannya: tabel detail yang dapat diurutkan, tata letak ubin, atau amplop tersembunyi. ISO 32000-2 menjelaskan kamus Collection sebagai kontrol yang digunakan pemroses PDF untuk menyajikan lampiran berkas sebagai portofolio yang terorganisasi. NextPDF memodelkannya sebagai objek nilai CollectionDictionary, dengan CollectionSort untuk urutan kolom dalam tampilan detail.

Metode tingkat dokumen disediakan oleh concern HasFileAttachments pada \NextPDF\Core\Document:

  • embedFile(string $path, string $description = ''): static — membaca berkas dari $path dan melampirkannya. NextPDF mendeteksi tipe MIME dari ekstensi; hubungan default adalah Unspecified. Membaca berkas hingga 100 MB; gunakan embedFileFromString() untuk payload yang lebih besar. Mengembalikan dokumen agar dapat digunakan untuk chaining.
  • embedFileFromString(string $data, string $filename, string $description = '', string $afRelationship = '/Unspecified'): static — melampirkan bita dalam memori dengan nama tampilan $filename. Berikan literal AFRelationship (dengan atau tanpa garis miring di awal) untuk menetapkan hubungan tersebut. Mengembalikan dokumen agar dapat digunakan untuk chaining.

Tipe pendukung berada di namespace \NextPDF\Navigation dan \NextPDF\Document:

  • \NextPDF\Navigation\AFRelationship — enum untuk delapan nilai hubungan yang valid. AFRelationship::coerce() menormalkan string atau case enum dan melempar pengecualian untuk nilai yang tidak dikenal. toPdfName() menghasilkan literal /Name.
  • \NextPDF\Document\CollectionDictionary — membangun kamus Collection katalog. Konstanta VIEW_DETAILS, VIEW_TILE, VIEW_HIDDEN, VIEW_CUSTOM, dan VIEW_NONE memilih mode penyajian; konstruktor juga menerima nama dokumen awal dan pengurutan opsional.
  • \NextPDF\Document\CollectionSort — objek nilai pengurutan kolom untuk portofolio tampilan detail.

Contoh minimal ini melampirkan kumpulan data comma-separated values (CSV) yang dihasilkan ke halaman faktur dan mendeklarasikannya sebagai Source yang menjadi dasar pembuatan faktur tersebut.

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

Pembaca menampilkan line-items.csv pada panel lampiran, dan hubungan tersebut menandainya sebagai sumber faktur.

Contoh lengkap ini melampirkan berkas dari disk dan kumpulan data dalam memori, memvalidasi jalur disk terhadap direktori basis yang masuk daftar izin sebelum membacanya, serta membangun portofolio yang dapat diurutkan untuk lampiran tersebut. Contoh ini menangkap pengecualian NextPDF paling spesifik yang mungkin muncul dari jalur lampiran, lalu mengembalikan kode keluar yang terdefinisi, bukan menelan kegagalan begitu saja.

<?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 dan CollectionSort adalah objek nilai. Keduanya memvalidasi masukan saat konstruksi dan melakukan serialisasi menjadi literal katalog /Collection yang menggerakkan tampilan portofolio di pembaca.

  • Masukan jalur adalah tanggung jawab Anda. embedFile() melindungi dari bita null dan stream wrapper serta menyelesaikan jalur sebenarnya, tetapi tidak menerapkan daftar izin untuk direktori basis. Jika jalur berasal dari permintaan, validasi terlebih dahulu, seperti yang dilakukan contoh produksi dengan resolveWithinBase().
  • Batas atas 100 MB hanya berlaku untuk embedFile(). Berkas yang melebihi 104,857,600 bita menimbulkan PageLayoutException. Untuk payload yang lebih besar, alirkan sendiri bita tersebut dan teruskan ke embedFileFromString().
  • Nama tipe MIME yang panjang ditolak. Tipe MIME yang terdeteksi menjadi /Subtype aliran tersemat, yaitu token nama PDF yang dibatasi hingga 127 bita oleh ISO 32000-2. Tipe yang sangat panjang (beberapa format Office mendekati 90 bita) tetap jauh di bawah batas, tetapi tipe yang diberikan secara manual dan melebihinya menimbulkan PageLayoutException. Biarkan mesin mendeteksi tipe dari ekstensi kecuali Anda memiliki alasan khusus untuk menggantinya.
  • Hubungan yang tidak dikenal melempar pengecualian. AFRelationship::coerce() menolak nilai apa pun di luar himpunan tetap, bukan menurunkannya menjadi Unspecified. Berikan case enum (AFRelationship::Source->value) agar salah ketik tidak sampai ke runtime.
  • Nama berkas harus unik di dalam pohon nama. Dua lampiran dengan nama tampilan yang sama akan bertabrakan di indeks EmbeddedFiles. Beri setiap lampiran nama berkas yang unik.
  • _ModDate dicatat dalam Coordinated Universal Time (UTC). embedFile() membaca waktu modifikasi berkas dan menulisnya dengan gmdate() sehingga fixture yang sama menghasilkan tanggal yang identik pada tingkat bita di seluruh mesin, terlepas dari pengaturan zona waktu.

Setiap lampiran dikompresi satu kali dengan gzcompress() pada level 9 dan ditulis sebagai satu aliran tunggal saat save(). Kompresi mendominasi biaya dan berskala mengikuti ukuran payload yang dilampirkan, bukan konten halaman. Beberapa berkas pendukung kecil (kumpulan data, spreadsheet, satu lembar waktu PDF) tetap berada dalam anggaran 2000 ms / 64 MB. Untuk banyak lampiran berukuran besar, bita tersemat menjadi batas bawah penggunaan memori: sebuah lampiran 50 MB yang disimpan sebagai string menempati setidaknya sebesar itu sebelum kompresi. Utamakan embedFileFromString() dengan pembuatan berbasis potongan daripada memuat beberapa berkas besar sekaligus.

Pohon nama dibangun satu kali saat save(). Hingga 64 entri tetap berada dalam pohon datar dengan satu akar. Di luar itu, NextPDF mempartisi pohon menjadi rentang Kids dan Limits yang seimbang, sehingga biaya indeks tetap logaritmik untuk himpunan lampiran besar.

  • Validasi setiap jalur yang tidak tepercaya terhadap daftar izin. Penyematan membaca berkas apa pun yang dapat dijangkau oleh proses PHP. Tanpa pemeriksaan direktori basis, nama berkas yang dirancang khusus dapat mengubah lampiran menjadi Local File Inclusion (LFI). Contoh produksi menunjukkan pemeriksaan daftar izin; terapkan setiap kali nama berkas bukan konstanta saat kompilasi.
  • Perlakukan bita yang dilampirkan sebagai tidak tepercaya di sisi yang mengonsumsinya. Bagi NextPDF, berkas tersemat bersifat opaque. Mesin tidak mengurai atau mengeksekusinya. Risiko berada di tempat berkas tersebut nantinya dibuka. Tetapkan hubungan dan deskripsi agar konsumen di hilir mengetahui isi setiap lampiran sebelum mengekstraknya.
  • Jangan simpan rahasia di dalam lampiran atau deskripsi. Nama berkas, deskripsi, dan bita disimpan secara terbuka kecuali seluruh dokumen dienkripsi. Untuk melindungi lampiran, enkripsi dokumen dengan kebijakan izin (lihat resep terkait). Jangan sematkan kredensial, kunci, atau data pribadi yang tidak akan Anda tempatkan di halaman yang dirender.
  • Tidak ada akses jaringan dalam resep ini. Setiap bita dibaca dari jalur lokal yang telah divalidasi atau dipasok di dalam memori.
PernyataanSpesifikasiKlausulreference_id
Aliran berkas tersemat menempel pada dokumen melalui entri EmbeddedFiles di kamus nama.ISO 32000-27.11.4
Pohon nama EmbeddedFiles memetakan nama ke spesifikasi berkas yang entri EF-nya mereferensikan aliran berkas tersemat.ISO 32000-27.7.4
Berkas terkait memerlukan nilai AFRelationship dari himpunan tetap PDF 2.0.PDF Association AN0023
Kamus Collection katalog mengontrol penyajian portofolio dari lampiran.ISO 32000-27.11.6

Profil reproduktibilitas — struktural. /ID pada trailer, atom tanggal per-penyimpanan, dan /ModDate aliran tersemat bervariasi antar-eksekusi, sehingga perbandingan struktural menghapus nilai-nilai tersebut sebelum melakukan diff pada grafik objek. Resep ini menjelaskan cara NextPDF menghasilkan struktur tersebut. Resep ini tidak menyatakan kesesuaian PDF/A-4f secara menyeluruh, yang bergantung pada dokumen lengkap. Untuk profil arsip yang mewajibkan setiap lampiran mendeklarasikan hubungan dan deskripsi, lihat resep PDF/A-4.