ข้ามไปยังเนื้อหา

ฝังไฟล์และสร้างพอร์ตโฟลิโอ PDF

สูตรนี้แนบไฟล์หนึ่งไฟล์หรือหลายไฟล์เข้ากับ PDF และเมื่อมีไฟล์แนบหลายรายการ จะจัดเรียงไฟล์เหล่านั้นเป็นพอร์ตโฟลิโอ PDF ใช้สูตรนี้เมื่อเอกสารต้องพกพาหลักฐานประกอบไว้ในไฟล์เดียวกัน เช่น ใบแจ้งหนี้พร้อมใบบันทึกเวลาทำงานที่เป็นแหล่งข้อมูล เอกสารข้อมูลผลิตภัณฑ์พร้อมไฟล์ส่งออกแบบ Computer-Aided Design (CAD) หรือบันทึกเพื่อการเก็บถาวรที่เก็บสเปรดชีตต้นทางไว้คู่กับรายงานที่เรนเดอร์แล้ว

NextPDF มีจุดเรียกใช้งานสองจุดบนออบเจกต์เอกสาร ได้แก่ embedFile() สำหรับอ่านไฟล์จากดิสก์ และ embedFileFromString() สำหรับฝังไบต์ในหน่วยความจำที่คุณสร้างขึ้น ณ รันไทม์ ทั้งสองเมธอดลงทะเบียนไฟล์แนบ ณ save() เอนจินจะเขียนไฟล์แนบแต่ละรายการเป็นสตรีมไฟล์ที่ฝัง ห่อด้วยพจนานุกรมข้อกำหนดไฟล์ และเชื่อมโยงข้อกำหนดไฟล์ทุกรายการกับ name tree ระดับเอกสาร EmbeddedFiles ISO 32000-2 กำหนดให้ name tree นี้เป็นตำแหน่งที่สตรีมไฟล์ที่ฝังแนบกับเอกสารทั้งฉบับผ่านพจนานุกรมชื่อ

ฟีเจอร์นี้เป็นความสามารถของ Core และไม่มีข้อจำกัดเชิงพาณิชย์ Application Programming Interface (API) สำหรับไฟล์แนบมีเสถียรภาพมาตั้งแต่ 1.0.0 และทำงานได้ครอบคลุมเมทริกซ์ backport 8.1-8.4

Terminal window
composer require nextpdf/core:^3

ไม่จำเป็นต้องใช้ส่วนขยายเสริมใด ๆ

การทำงานของไฟล์แนบเกี่ยวข้องกับโครงสร้าง PDF สามส่วน การเข้าใจโครงสร้างเหล่านี้ช่วยให้คุณตรวจสอบผลลัพธ์และดีบักไฟล์ที่ไม่เป็นไปตามข้อกำหนดได้

  1. สตรีมไฟล์ที่ฝัง ไบต์ดิบของไฟล์แนบที่ถูกบีบอัดด้วย Flate และเขียนเป็นออบเจกต์สตรีมที่มี /Type เป็น /EmbeddedFile NextPDF บันทึกขนาดเดิม checksum แบบ MD5 และวันที่แก้ไขไว้ในพจนานุกรมพารามิเตอร์ของสตรีม พร้อมเข้ารหัสประเภท Multipurpose Internet Mail Extensions (MIME) ที่ตรวจพบเป็น /Subtype ของสตรีม
  2. พจนานุกรมข้อกำหนดไฟล์ ตัวห่อเมทาดาทา พจนานุกรมนี้เก็บชื่อไฟล์ที่แสดง (/F และ /UF แบบ Unicode) คำอธิบายที่มนุษย์อ่านได้ (/Desc) การอ้างอิงไปยังสตรีมที่ฝัง (/EF) และความสัมพันธ์ของไฟล์กับเอกสารโฮสต์ (/AFRelationship)
  3. name tree EmbeddedFiles ดัชนีระดับเอกสารเพียงชุดเดียวที่จับคู่ชื่อของไฟล์แนบแต่ละรายการกับข้อกำหนดไฟล์ของไฟล์นั้น ISO 32000-2 กำหนดให้ข้อกำหนดไฟล์ทุกรายการที่เข้าถึงผ่านโครงสร้างนี้ต้องมีรายการ EF ที่อ้างอิงไปยังสตรีมไฟล์ที่ฝัง NextPDF สร้างและจัดสมดุล tree นี้ให้คุณ ณ save()

ค่าความสัมพันธ์มีความสำคัญต่อการเป็นไปตามข้อกำหนด PDF Association Application Note 0002 ระบุว่าไฟล์ที่เชื่อมโยงต้องมีรายการ AFRelationship ที่เลือกจากชุดค่าคงที่ของ PDF 2.0 ได้แก่ Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema หรือ Unspecified NextPDF สร้างแบบจำลองชุดนั้นเป็น enum AFRelationship และปฏิเสธค่าอื่นทั้งหมด เลือกค่าที่อธิบายเหตุผลที่ไฟล์นั้นปรากฏอยู่ ใบบันทึกเวลาทำงานที่ใช้สร้างใบแจ้งหนี้คือ Source ส่วนชุดข้อมูลที่เครื่องอ่านได้ซึ่งใช้สร้างแผนภูมิคือ Data

ส่วน พอร์ตโฟลิโอ PDF (เรียกว่า collection ใน ISO 32000-2) คือชั้นโครงสร้างถัดขึ้นไป เมื่อเอกสารพกพาไฟล์แนบหลายรายการ พจนานุกรม Collection ในแคทาล็อกจะบอกโปรแกรมอ่านว่าจะนำเสนอไฟล์เหล่านั้นอย่างไร เช่น ตารางรายละเอียดที่จัดเรียงได้ เค้าโครงแบบไทล์ หรือซองแบบซ่อน ISO 32000-2 อธิบายพจนานุกรม Collection ว่าเป็นตัวควบคุมที่โปรเซสเซอร์ PDF ใช้นำเสนอไฟล์แนบเป็นพอร์ตโฟลิโอที่จัดระเบียบแล้ว NextPDF สร้างแบบจำลองสิ่งนี้เป็น value object CollectionDictionary โดยมี CollectionSort สำหรับลำดับคอลัมน์ในมุมมองรายละเอียด

เมธอดระดับเอกสารมาจาก concern HasFileAttachments บน \NextPDF\Core\Document:

  • embedFile(string $path, string $description = ''): static — อ่านไฟล์จาก $path และแนบไฟล์นั้น NextPDF ตรวจหาประเภท MIME จากนามสกุลไฟล์ ความสัมพันธ์มีค่าเริ่มต้นเป็น Unspecified รองรับการอ่านได้สูงสุด 100 MB ใช้ embedFileFromString() สำหรับเพย์โหลดที่ใหญ่กว่านั้น คืนค่าเอกสารเพื่อให้เรียกต่อแบบ chaining ได้
  • embedFileFromString(string $data, string $filename, string $description = '', string $afRelationship = '/Unspecified'): static — แนบไบต์ในหน่วยความจำภายใต้ชื่อที่แสดง $filename ส่งค่าลิเทอรัล AFRelationship (จะมีเครื่องหมายทับนำหน้าหรือไม่ก็ได้) เพื่อกำหนดความสัมพันธ์ คืนค่าเอกสารเพื่อให้เรียกต่อแบบ chaining ได้

ประเภทที่รองรับอยู่ในเนมสเปซ \NextPDF\Navigation และ \NextPDF\Document:

  • \NextPDF\Navigation\AFRelationship — enum สำหรับค่าความสัมพันธ์ที่ถูกต้องแปดค่า AFRelationship::coerce() ปรับสตริงหรือ enum case ให้เป็นรูปแบบมาตรฐาน และโยนข้อยกเว้นเมื่อพบค่าที่ไม่รู้จัก toPdfName() ส่งออกค่าลิเทอรัล /Name
  • \NextPDF\Document\CollectionDictionary — สร้างพจนานุกรม Collection ในแคทาล็อก ค่าคงที่ VIEW_DETAILS, VIEW_TILE, VIEW_HIDDEN, VIEW_CUSTOM และ VIEW_NONE เลือกโหมดการนำเสนอ ส่วนคอนสตรักเตอร์ยังรับชื่อเอกสารเริ่มต้นและการจัดเรียงทางเลือกได้ด้วย
  • \NextPDF\Document\CollectionSort — value object สำหรับการจัดลำดับคอลัมน์ของพอร์ตโฟลิโอในมุมมองรายละเอียด

ตัวอย่างขั้นต่ำนี้แนบชุดข้อมูล comma-separated values (CSV) ที่สร้างขึ้นเข้ากับหน้าใบแจ้งหนี้ และประกาศว่าเป็น 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');

โปรแกรมอ่าน PDF จะแสดง line-items.csv ในแผงไฟล์แนบ และความสัมพันธ์จะระบุว่าไฟล์นี้เป็นต้นทางของใบแจ้งหนี้

ตัวอย่างแบบสมบูรณ์นี้แนบไฟล์จากดิสก์และชุดข้อมูลในหน่วยความจำ ตรวจสอบเส้นทางบนดิสก์กับไดเรกทอรีฐานในรายการที่อนุญาตก่อนอ่านไฟล์ และสร้างพอร์ตโฟลิโอไฟล์แนบที่จัดเรียงได้ ตัวอย่างนี้ดักจับข้อยกเว้นเฉพาะของ NextPDF ที่เส้นทางไฟล์แนบอาจทำให้เกิดขึ้น แล้วคืนค่า exit code ที่กำหนดไว้ แทนที่จะกลืนความล้มเหลวไว้เงียบ ๆ

<?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 เป็น value object ออบเจกต์เหล่านี้ตรวจสอบอินพุตของตนเองเมื่อสร้างขึ้น และซีเรียไลซ์เป็นค่าลิเทอรัล /Collection ในแคทาล็อก ซึ่งใช้ควบคุมมุมมองพอร์ตโฟลิโอในโปรแกรมอ่าน

  • อินพุตเส้นทางเป็นความรับผิดชอบของคุณ embedFile() ป้องกัน null byte และ stream wrapper พร้อมแปลงค่าเป็นเส้นทางจริง แต่ไม่บังคับใช้รายการที่อนุญาตของไดเรกทอรีฐาน เมื่อเส้นทางมาจากคำขอ ให้ตรวจสอบก่อน ตามที่ตัวอย่างระดับโปรดักชันทำผ่าน resolveWithinBase()
  • ขีดจำกัด 100 MB ใช้กับ embedFile() เท่านั้น ไฟล์ที่มีขนาดเกิน 104,857,600 ไบต์จะทำให้เกิด PageLayoutException สำหรับเพย์โหลดที่ใหญ่กว่า ให้สตรีมไบต์เองและส่งไปยัง embedFileFromString() แทน
  • ชื่อประเภท MIME ที่ยาวจะถูกปฏิเสธ ประเภท MIME ที่ตรวจพบจะกลายเป็น /Subtype ของสตรีมที่ฝัง ซึ่งเป็น PDF name token ที่ ISO 32000-2 จำกัดไว้ที่ 127 ไบต์ ประเภทที่ยาวผิดปกติ (รูปแบบ Office บางรูปแบบเข้าใกล้ 90 ไบต์) ยังคงต่ำกว่าขีดจำกัดมาก แต่ประเภทที่ระบุเองซึ่งเกินขีดจำกัดจะทำให้เกิด PageLayoutException ปล่อยให้เอนจินตรวจหาประเภทจากนามสกุลไฟล์ เว้นแต่คุณมีเหตุผลเฉพาะที่จะแทนที่ค่านั้น
  • ความสัมพันธ์ที่ไม่รู้จักจะโยนข้อยกเว้น AFRelationship::coerce() ปฏิเสธค่าใด ๆ ที่อยู่นอกชุดค่าคงที่ แทนที่จะลดระดับลงเป็น Unspecified ส่ง enum case (AFRelationship::Source->value) เพื่อป้องกันไม่ให้การพิมพ์ผิดเล็ดลอดไปถึงรันไทม์
  • ชื่อไฟล์ต้องไม่ซ้ำกันใน name tree ไฟล์แนบสองรายการที่มีชื่อที่แสดงเดียวกันจะเกิดการชนกันในดัชนี EmbeddedFiles กำหนดชื่อไฟล์ที่ไม่ซ้ำกันให้กับไฟล์แนบแต่ละรายการ
  • _ModDate ถูกบันทึกเป็น Coordinated Universal Time (UTC) embedFile() อ่านเวลาแก้ไขไฟล์และเขียนด้วย gmdate() ดังนั้น fixture เดียวกันจึงสร้างวันที่ที่เหมือนกันทุกไบต์บนทุกเครื่อง โดยไม่ขึ้นกับการตั้งค่าเขตเวลา

ไฟล์แนบแต่ละรายการถูกบีบอัดหนึ่งครั้งด้วย gzcompress() ที่ระดับ 9 และเขียนเป็นสตรีมเดียว ณ save() การบีบอัดเป็นต้นทุนหลักและเพิ่มขึ้นตามขนาดเพย์โหลดที่แนบ ไม่ใช่ตามเนื้อหาของหน้า ไฟล์ประกอบขนาดเล็กไม่กี่ไฟล์ (ชุดข้อมูล สเปรดชีต ไฟล์ใบบันทึกเวลาทำงาน PDF) ยังคงอยู่ภายในงบประมาณ 2000 ms / 64 MB สำหรับไฟล์แนบขนาดใหญ่จำนวนมาก ไบต์ที่ฝังจะเป็นต้นทุนหน่วยความจำขั้นต่ำ ไฟล์แนบขนาด 50 MB ที่เก็บไว้เป็นสตริงจะใช้พื้นที่อย่างน้อยเท่ากับขนาดนั้นก่อนการบีบอัด ควรใช้ embedFileFromString() พร้อมการสร้างแบบแบ่งก้อน แทนการโหลดไฟล์ขนาดใหญ่หลายไฟล์พร้อมกัน

name tree ถูกสร้างขึ้นหนึ่งครั้ง ณ save() เมื่อมีรายการไม่เกิน 64 รายการ tree จะยังเป็นแบบรากเดี่ยวที่แบนราบ เกินกว่านั้น NextPDF จะแบ่ง tree ออกเป็นช่วง Kids และ Limits ที่สมดุล ดังนั้นต้นทุนดัชนีจึงยังคงเป็นลอการิทึมสำหรับชุดไฟล์แนบขนาดใหญ่

  • ตรวจสอบทุกเส้นทางที่ไม่น่าเชื่อถือเทียบกับรายการที่อนุญาต การฝังจะอ่านไฟล์ใด ๆ ที่กระบวนการ PHP เข้าถึงได้ หากไม่มีการตรวจสอบไดเรกทอรีฐาน ชื่อไฟล์ที่ถูกสร้างขึ้นเพื่อโจมตีจะเปลี่ยนไฟล์แนบให้กลายเป็น Local File Inclusion (LFI) ตัวอย่างระดับโปรดักชันแสดงการป้องกันด้วยรายการที่อนุญาต ให้ใช้ทุกครั้งที่ชื่อไฟล์ไม่ใช่ค่าคงที่ ณ เวลาคอมไพล์
  • ถือว่าไบต์ที่แนบเป็นข้อมูลที่ไม่น่าเชื่อถือในฝั่งผู้ใช้ NextPDF มองไฟล์ที่ฝังเป็นข้อมูลทึบแสง เอนจินไม่แยกวิเคราะห์หรือเรียกใช้ไฟล์นั้น ความเสี่ยงอยู่ที่จุดที่ไฟล์ถูกเปิดในภายหลัง กำหนดความสัมพันธ์และคำอธิบายเพื่อให้ผู้ใช้ปลายทางทราบว่าไฟล์แนบแต่ละรายการคืออะไรก่อนที่จะแยกออกมา
  • ห้ามใส่ความลับในไฟล์แนบหรือคำอธิบาย ชื่อไฟล์ คำอธิบาย และไบต์จะถูกจัดเก็บแบบเปิดเผย เว้นแต่จะเข้ารหัสลับทั้งเอกสาร หากต้องการปกป้องไฟล์แนบ ให้เข้ารหัสลับเอกสารด้วยนโยบายสิทธิ์ (ดูสูตรที่เกี่ยวข้อง) อย่าฝังข้อมูลรับรอง คีย์ หรือข้อมูลส่วนบุคคลที่คุณจะไม่ใส่ไว้ในหน้าที่เรนเดอร์แล้ว
  • ไม่มีการเข้าถึงเครือข่ายในสูตรนี้ ทุกไบต์ถูกอ่านจากเส้นทางในเครื่องที่ผ่านการตรวจสอบแล้ว หรือถูกป้อนในหน่วยความจำ
ข้อความระบุข้อกำหนดข้อรหัสอ้างอิง (reference_id)
สตรีมไฟล์ที่ฝังแนบกับเอกสารผ่านรายการ EmbeddedFiles ในพจนานุกรมชื่อISO 32000-27.11.4
name tree EmbeddedFiles จับคู่ชื่อกับข้อกำหนดไฟล์ที่มีรายการ EF อ้างอิงไปยังสตรีมไฟล์ที่ฝังISO 32000-27.7.4
ไฟล์ที่เชื่อมโยงต้องมีค่า AFRelationship จากชุดค่าคงที่ของ PDF 2.0PDF Association AN0023
พจนานุกรม Collection ในแคทาล็อกควบคุมการนำเสนอไฟล์แนบเป็นพอร์ตโฟลิโอISO 32000-27.11.6

โปรไฟล์ความสามารถในการทำซ้ำ — เชิงโครงสร้าง /ID ใน trailer อะตอมวันที่ต่อการบันทึก และ /ModDate ของสตรีมที่ฝังจะแตกต่างกันในแต่ละครั้งที่รัน ดังนั้นการเปรียบเทียบเชิงโครงสร้างจึงตัดค่าเหล่านั้นออกก่อน diff กราฟออบเจกต์ สูตรนี้อธิบายว่า NextPDF สร้างโครงสร้างอย่างไร แต่ไม่ได้ยืนยันความสอดคล้อง PDF/A-4f แบบครอบคลุม ซึ่งขึ้นอยู่กับเอกสารทั้งฉบับ สำหรับโปรไฟล์การเก็บถาวรที่กำหนดให้ไฟล์แนบทุกรายการต้องประกาศความสัมพันธ์และคำอธิบาย ดูสูตร PDF/A-4