ฝังไฟล์และสร้างพอร์ตโฟลิโอ 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
การติดตั้ง
หัวข้อที่มีชื่อว่า “การติดตั้ง”composer require nextpdf/core:^3ไม่จำเป็นต้องใช้ส่วนขยายเสริมใด ๆ
ภาพรวมเชิงแนวคิด
หัวข้อที่มีชื่อว่า “ภาพรวมเชิงแนวคิด”การทำงานของไฟล์แนบเกี่ยวข้องกับโครงสร้าง PDF สามส่วน การเข้าใจโครงสร้างเหล่านี้ช่วยให้คุณตรวจสอบผลลัพธ์และดีบักไฟล์ที่ไม่เป็นไปตามข้อกำหนดได้
- สตรีมไฟล์ที่ฝัง ไบต์ดิบของไฟล์แนบที่ถูกบีบอัดด้วย Flate และเขียนเป็นออบเจกต์สตรีมที่มี
/Typeเป็น/EmbeddedFileNextPDF บันทึกขนาดเดิม checksum แบบ MD5 และวันที่แก้ไขไว้ในพจนานุกรมพารามิเตอร์ของสตรีม พร้อมเข้ารหัสประเภท Multipurpose Internet Mail Extensions (MIME) ที่ตรวจพบเป็น/Subtypeของสตรีม - พจนานุกรมข้อกำหนดไฟล์ ตัวห่อเมทาดาทา พจนานุกรมนี้เก็บชื่อไฟล์ที่แสดง (
/Fและ/UFแบบ Unicode) คำอธิบายที่มนุษย์อ่านได้ (/Desc) การอ้างอิงไปยังสตรีมที่ฝัง (/EF) และความสัมพันธ์ของไฟล์กับเอกสารโฮสต์ (/AFRelationship) - 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 สำหรับลำดับคอลัมน์ในมุมมองรายละเอียด
ส่วนติดต่อ API
หัวข้อที่มีชื่อว่า “ส่วนติดต่อ API”เมธอดระดับเอกสารมาจาก 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-2 | 7.11.4 | |
name tree EmbeddedFiles จับคู่ชื่อกับข้อกำหนดไฟล์ที่มีรายการ EF อ้างอิงไปยังสตรีมไฟล์ที่ฝัง | ISO 32000-2 | 7.7.4 | |
ไฟล์ที่เชื่อมโยงต้องมีค่า AFRelationship จากชุดค่าคงที่ของ PDF 2.0 | PDF Association AN002 | 3 | |
พจนานุกรม Collection ในแคทาล็อกควบคุมการนำเสนอไฟล์แนบเป็นพอร์ตโฟลิโอ | ISO 32000-2 | 7.11.6 |
โปรไฟล์ความสามารถในการทำซ้ำ — เชิงโครงสร้าง /ID ใน trailer อะตอมวันที่ต่อการบันทึก และ /ModDate ของสตรีมที่ฝังจะแตกต่างกันในแต่ละครั้งที่รัน ดังนั้นการเปรียบเทียบเชิงโครงสร้างจึงตัดค่าเหล่านั้นออกก่อน diff กราฟออบเจกต์ สูตรนี้อธิบายว่า NextPDF สร้างโครงสร้างอย่างไร แต่ไม่ได้ยืนยันความสอดคล้อง PDF/A-4f แบบครอบคลุม ซึ่งขึ้นอยู่กับเอกสารทั้งฉบับ สำหรับโปรไฟล์การเก็บถาวรที่กำหนดให้ไฟล์แนบทุกรายการต้องประกาศความสัมพันธ์และคำอธิบาย ดูสูตร PDF/A-4