PDF 是一個容器:內嵌檔案與關聯資料
Spec: ISO 32000-2, §7.11.4ISO 32000-2 §7.11.4Spec: ISO 32000-2, §14.13ISO 32000-2 §14.13Spec: ISO 19005-3, PDF/A-3ISO 19005-3 PDF/A-3
多數人把 PDF 想成一疊頁面。那是你看得見的部分。但 PDF 也是一個容器,它能在內部攜帶整份的其他檔案——一份試算表、一段 XML 酬載、原始的來源文件——全部捆綁進你交給別人的那同一個單一檔案裡。
本頁說明這是如何運作的:儲存位元組的內嵌檔案串流、列出它們的名稱樹,以及那把決定一個附件究竟只是擺在那裡、還是真正具有某種意義的單一鍵。
為何這很重要
標題為「為何這很重要」的區段一個無型別的附件與一個有型別的附件,在人眼看來一模一樣。兩者都是搭乘在 PDF 內部的一份檔案,而且——在這個引擎中——兩者都與文件相關聯。差別在於:其中一個告訴機器它的用途是什麼,另一個則把關係留空,讓機器去猜。
這個差別,對一份混合式電子發票而言就是全部勝負所在。稅務平台不會讀你的發票頁面;它讀的是你內嵌的 XML。如果那段 XML 是以一團未經區分的位元組附上、而非作為可見文件的發票資料,那麼符合標準的讀取器便沒有可靠的方式得知它就是要處理的酬載。頁面看起來完美無瑕。發票卻遭到退回。失敗在數天後才到來,而其後還拖著一筆被扣住的款項。
在產生檔案的那一層把關係弄對,遠比一張被退回的發票一張地發現問題要便宜得多。
簡短版本
標題為「簡短版本」的區段- PDF 能把任何檔案的位元組內嵌為一個內嵌檔案串流(Spec: ISO 32000-2, §7.11.4ISO 32000-2 §7.11.4)。該串流攜帶資料,外加一份小型的參數字典:原始大小、日期,以及一個總和檢查碼。
- 內嵌檔案被編入
EmbeddedFiles名稱樹,因此讀取器無須掃描整份文件就能依名稱逐一列舉它們。 - 關聯檔案更進一步:它宣告一個
AFRelationship(Spec: ISO 32000-2, §7.11.3ISO 32000-2 §7.11.3)——八個標準值之一(Source、Data、Alternative、Supplement、EncryptedPayload、FormData、Schema、Unspecified),或一個自訂值——說明該檔案如何與它所附加的內容相關。 - 這個有型別的關係,正是混合式電子發票(ZUGFeRD/Factur-X)與 PDF/A-3 附件背後的機制(Spec: ISO 19005-3, PDF/A-3ISO 19005-3 PDF/A-3)。
- NextPDF 在核心支援原始的容器基本元件:帶有明確關係的
embedFile()與embedFileFromString()。進階版本則在這些基本元件之上加入專屬的 EN 16931/ZUGFeRD/Factur-X 電子發票內嵌器。
NextPDF 如何處理
標題為「NextPDF 如何處理」的區段把它想成兩層彼此疊在一起的結構。
下層是儲存。一個內嵌檔案串流(Spec: ISO 32000-2, §7.11.4ISO 32000-2 §7.11.4)是原始檔案的位元組,包裹在一個 PDF 串流物件之中,並帶有一份記錄原始大小、修改日期以及未壓縮資料總和檢查碼的參數字典。這個串流是透過一份檔案規格字典來抵達的,其 /EF 字典指向該內嵌檔案串流——串流本身並不攜帶 /EF。讀取器可以把檔案逐位元組地原樣取回。為了讓這些檔案可被尋得,文件目錄保有一個 EmbeddedFiles 名稱樹——一張從名稱到各個檔案規格的排序對應表——好讓檢視器無須走訪每一頁就能列出「這份 PDF 裡有這 3 個檔案」。
上層是意義。內嵌檔案就其自身而言只是「存在」。關聯檔案機制(Spec: ISO 32000-2, §14.13ISO 32000-2 §14.13)把一個檔案附加到某樣東西上——整份文件、某一頁、某個圖形物件——並為它蓋上一個 AFRelationship 戳記。ISO 32000-2 定義了一套由八個標準值組成的小詞彙(Spec: ISO 32000-2, §7.11.3ISO 32000-2 §7.11.3),也允許自訂值;每個標準值都回答一個精確的問題:
AFRelationship | 它對該檔案宣告了什麼 |
|---|---|
Source | 這是可見內容據以產生的來源素材(例如,原始的文書處理文件)。 |
Data | 這是與可見內容相繫的結構化資料——標準範例就是已繪製發票頁面背後的發票 XML。 |
Alternative | 這是同一內容的另一種替代呈現(例如,音訊或視訊版本)。 |
Supplement | 這是擴充內容、但不屬於內容本身的補充素材。 |
EncryptedPayload | 該內嵌檔案是一段加密酬載,PDF 以一團不透明的位元組將其包裹。 |
FormData | 該檔案是表單資料(FDF、XFDF,或一段 XML 表單酬載)。 |
Schema | 該檔案是描述某個 Data 檔案結構的綱要(例如,用於 XML 資料的 XSD,或一份 JSON Schema)。 |
Unspecified | 關係刻意未予言明。誠實,但它對機器什麼也沒說。 |
在這八個之外,標準也允許應用程式專屬的自訂關係值,因此這套詞彙是可擴充而非固定的。
一個關聯檔案是由兩樣協同運作的東西所定義,而非單一一個鍵。/AF 關聯把檔案規格繫結到文件的某一部分;接著檔案規格中的 AFRelationship 鍵則陳述其語意關係。關聯點上(文件目錄、某一頁或某個物件)的 /AF 項目是一個陣列——該陣列含有一份或多份檔案規格字典,通常以間接參照表示;/AF 並不是單一一個參照。一個文件層級的關聯檔案,就是列在文件目錄 /AF 陣列中、並攜帶其 AFRelationship 的那份檔案規格。把那份試算表標為 Unspecified,你就只是把它與文件相關聯,卻沒告訴機器為什麼。把同一份試算表標為 Data,你就告訴了每一個符合標準的讀取器它是什麼、它的用途為何。位元組是一樣的。語意則不然。
這就是為什麼電子發票的情形並不是「附上一個 XML 檔案」。它是「把這段 XML 內嵌為這份文件的 Data 關聯檔案,置於一個符合標準的 PDF/A-3 載體之中」——而發票的有效性與法律上的受理,仍是載體所不執行的、各自獨立的檢查。整個流程有四個階段,而順序正是讓它保持正確的關鍵。
- Store the bytesThe file is wrapped in an embedded file stream with its size, dates, and a checksum (ISO 32000-2 §7.11.4).
- Register it by nameThe file specification is added to the EmbeddedFiles name tree so a reader can enumerate attachments without scanning the document.
- Declare the relationshipAn AFRelationship value (one of the eight standard values such as Source or Data) marks how the file relates to the content, associated at the document level (ISO 32000-2 §14.13.3).
- Make it archivalA PDF/A-3 carrier permits the embedded payload to ride inside one conforming archival PDF/A document; invoice validity and legal acceptance remain separate checks (ISO 19005-3).
第四個階段正是 PDF/A-3 之所以作為一個獨立設定檔存在的原因。較早的封存設定檔限制了可被內嵌的內容;PDF/A-3(Spec: ISO 19005-3, PDF/A-3ISO 19005-3 PDF/A-3)正是允許任何格式的檔案搭乘於一份符合標準的封存文件內部的那一部分。它允許內嵌的酬載——它並不驗證該酬載,也不賦予法律地位。沒有它,那份混合式發票——一個既是人所閱讀的頁面、又是稅務系統所解析資料的單一檔案——便根本無法成為一份符合標準的封存 PDF/A 文件;而發票是否有效、是否在法律上受理,仍是另一個獨立的問題。進階版本所加入的那個專屬電子發票內嵌器,正是針對這件事的便利接縫:它內嵌酬載、把關係設為 Data,並正確地將其登錄,讓你不必親手組裝容器的管路。更深入的發票與封存機制,住在下方連結的兩個相鄰頁面上;本頁談的是它們兩者所立足的那個容器。
實際範例
標題為「實際範例」的區段一個小巧、完整的程式。真正關鍵的那兩個呼叫,正是一個無型別關聯檔案與一個有型別關聯檔案之間的差別——而關係是一個你應當設定的明確引數。在這個引擎中,兩個呼叫都會產生一個關聯檔案:embedFile() 與 embedFileFromString() 一律會把檔案規格登錄到文件目錄的 /AF 陣列中,因此關係唯一改變的,就是該關聯的意義為何。它預設為 Unspecified,這會把檔案相關聯、卻不告訴機器原因;對一份電子發票酬載,你要把它設為 Data,好讓讀取器能找到它。
<?php
declare(strict_types=1);
use NextPDF\Core\Document;use NextPDF\Navigation\AFRelationship;
$document = Document::createStandalone();$document->addPage();$document->setFont('helvetica', 'B', 16);$document->cell(0, 12, 'Invoice INV-2026-0042', newLine: true);
// An UNTYPED associated file: the bytes are embedded AND the file spec is// added to the document catalog's /AF array, but the relationship says// nothing about why. A reader can open it; a machine cannot tell its role.// The relationship is left Unspecified (its default); the second argument is// the human-readable description. embedFile accepts the AFRelationship enum.$document->embedFile( '/srv/invoices/INV-2026-0042-source.docx', 'Original source document', AFRelationship::Unspecified,);
// A TYPED associated file: the invoice XML is declared as the DATA behind// the visible page. This is the relationship a hybrid e-invoice reader// looks for — the same intent the dedicated e-invoice embedder sets.// embedFileFromString takes the data, a filename, a description, and a// relationship as a PDF-name string ('/Data').$invoiceXml = $generateCiiXml(); // your ERP authors this; the engine never does$document->embedFileFromString( $invoiceXml, 'factur-x.xml', 'Factur-X invoice data', '/Data',);
$bytes = $document->getPdfData();'/Data' 這個關係毫不含糊。第一個附件——保留為 Unspecified——同樣被相關聯了,只是沒有陳述意義。對於這兩個呼叫,引擎都會寫入內嵌檔案串流、把檔案加入 EmbeddedFiles 名稱樹、把它的檔案規格列入文件目錄的 /AF 陣列,並記錄你所陳述的關係——它不會替你挑選一個。這個引擎沒有「僅限名稱樹」的模式:每一個你以此方式內嵌的檔案,都是一份文件關聯檔案,因此關係是你唯一掌控的槓桿。
常見的誤解
標題為「常見的誤解」的區段常見的假設是,「內嵌」與「關聯」是同一件事的兩個說法。它們不是。內嵌講的是儲存——位元組在 PDF 內部。關聯講的是繫結——檔案規格被列在文件某一部分的某個 /AF 陣列中,且它攜帶一個 AFRelationship。在抽象的 PDF 模型裡,一個檔案可以被內嵌進名稱樹卻從未被關聯;但 NextPDF 的 embedFile() 路徑不會把它停在那裡——它一律會寫入 /AF 關聯——因此對這個引擎而言,懸而未決的問題永遠不是某檔案是否被關聯,而是關係陳述了什麼。
第二個陷阱:假設檢視器會「自行判斷」哪個附件是發票。一個符合標準的讀取器不該去猜。它會尋找那個關係陳述為 Data 的檔案。把關係留為 Unspecified,你就把酬載相關聯了,卻沒對機器說出任何關於其角色的有用資訊。
限制與邊界
標題為「限制與邊界」的區段這套容器機制的強大之處,值得我們誠實以對:embedFile() 會讀取 PHP 行程所能讀取的任何路徑。那是功能——也同時是邊界。引擎會附上交給它的位元組;它不會、也無法替你判斷某個路徑是否是你有意要公開的。
| Edition | Availability |
|---|---|
| Core |
|
| Pro | Not in this edition |
| Enterprise | Not in this edition |
另有兩條值得直白陳述的限制:
- 內嵌不等於驗證。 引擎會攜帶你交給它的位元組。內嵌的 XML 是否是一份符合標準的發票酬載,是另一個問題,由驗證器來回答——參見發票頁面。
- 有型別的附件就其自身而言,並不是一份符合標準的封存檔案。 要讓那份混合式檔案成為一份合法的 PDF/A-3 文件,需要封存模式以及一次獨立的標準符合性檢查——參見封存頁面。
相關文件
標題為「相關文件」的區段- 發票與電子發票 — 這套機制所成就的使用情境:一份攜帶機器可讀發票、並以其作為
Data關聯檔案的混合式 PDF。 - 封存與 PDF/A — 為何載體是一份 PDF/A-3 檔案,以及標準符合性承諾了什麼、又不承諾什麼。
- PDF 檔案剖析 — 名稱樹與文件目錄在檔案結構中坐落於何處。
- 串流與篩選器 — 一個內嵌檔案的位元組如何在串流物件內部被儲存與壓縮。
詞彙表
標題為「詞彙表」的區段- 內嵌檔案串流 — 一個保存外部檔案位元組的 PDF 串流物件,帶有一份記錄其原始大小、日期與總和檢查碼的參數字典(ISO 32000-2 §7.11.4)。
- EmbeddedFiles 名稱樹 — 文件目錄中那張依名稱列出內嵌檔案的排序對應表,讓讀取器無須掃描整份文件就能逐一列舉附件。
- 關聯檔案 — 一個透過
/AF關聯(在文件目錄、某一頁或某個物件上)繫結到文件某一部分、並攜帶一個陳述其與內容如何相關之AFRelationship的內嵌檔案;本頁所聚焦的是文件層級的情形——文件目錄/AF陣列中的那份檔案規格(ISO 32000-2 §14.13.3)。 - AFRelationship — 那個其值用以命名關係的檔案規格鍵(ISO 32000-2 §7.11.3)。它取八個標準值之一(
Source、Data、Alternative、Supplement、EncryptedPayload、FormData、Schema、Unspecified),或一個自訂值;Data是混合式電子發票酬載所使用的值。 - PDF/A-3 — 那個允許任何格式檔案被內嵌、從而成就一份符合標準混合式文件的 ISO 19005-3 封存設定檔。
- 混合式發票 — 一份既是人類可讀頁面、又是機器可讀內嵌發票酬載的 PDF 檔案。