透過壓縮與子集化縮減 PDF 檔案大小
你想要在不損失保真度的前提下,得到內容所允許的最小 PDF。NextPDF 提供兩個大小調節項,而且兩者都預設開啟:
- 串流壓縮。 寫入器會把每個頁面內容串流與每個嵌入的字型程式都封裝成一個 FlateDecode(zlib)串流。
NextPDF\Core\Config的compress旗標承載這項設定。當你建構串流文件時,可透過withCompress()wither 調整它。 - 字型子集化。 當你嵌入一個 TrueType 或 CFF 字型時,寫入器會重建字型程式,使其只攜帶文件實際用到的字形,然後再以 FlateDecode 壓縮結果。這會自動發生——沒有旗標要設定,也沒有方法要呼叫。一個有
20,000個字形的 CJK 字面,若只貢獻幾百個字形給某份文件,嵌入後的大小只占其磁碟上大小的一小部分。
先說清楚:NextPDF Core 並未提供影像重新取樣、影像品質旋鈕、物件串流開關或資源去重設定。現有的大小控制項就是上述那兩個。本範例的其餘部分會說明如何正確使用它們,以及每一項各自不會做什麼。
前置需求:一份 Core 安裝(composer require nextpdf/core:^3),以及用於子集化路徑、你有授權嵌入的字型檔。
composer require nextpdf/core:^3概念綜覽
標題為「概念綜覽」的區段一份 PDF 是一棵物件樹。最大的物件通常是內容串流(每個頁面的繪圖運算子)與字型程式(嵌入的字形外框)。兩者都是高度可壓縮的文字與二進位資料,因此最有效的單一大小調節項,就是以 FlateDecode 壓縮它們。FlateDecode 是 PDF 2.0 對 zlib 包裝的 DEFLATE 串流所用的名稱(ISO 32000-2:2020 §7.4.4),也是 NextPDF 輸出的過濾器。
寫入器透過 NextPDF\Writer\PinnedZlibCompressor 把 DEFLATE 壓縮等級固定在 9,也就是 RFC 1951 的最大值。等級 9 以一點額外的 CPU 換取最小的串流。固定等級也讓輸出維持確定性,因為 zlib 標頭會編碼這個等級;等級若飄移,就會改變位元組。你不需要挑選等級——引擎會把它固定下來,讓對同一份輸入的兩次執行產生位元組完全相同的串流。
第二個調節項是字型子集化。磁碟上的字型檔會攜帶該字體所定義的每一個字形,但一份印出「Invoice 2026」的文件只需要其中十幾個。NextPDF\Typography\FontSubsetter(用於 TrueType)與 NextPDF\Typography\CffSubsetter(用於 CFF / OpenType)會走訪文件實際繪製的碼位、resolve(解析)複合字形相依性,並只重建所需的字型表。它們會輸出一個有效的子集字型二進位,並帶有一個確定性的六字母子集前綴標籤(ISO 32000-2:2020 §9.9)。只要某個嵌入字型的已用字形集合是已知的,寫入器就會套用這項處理,然後以 FlateDecode 壓縮該子集。如果對某個特定字面進行子集化所省下的不到一成,子集器就會改為傳回原始字型程式,因為重建的開銷不值得換取那一點邊際效益。
重點是:要讓 PDF 維持精簡,靠的是讓壓縮保持開啟(也就是預設值),以及嵌入真正的字型檔(讓子集化有東西可以縮減),而不是靠調整一長串選項。
API 介面
標題為「API 介面」的區段你唯一需要設定的大小旋鈕在組態物件上。
NextPDF\Core\Config 是一個不可變的 final readonly 值物件,並提供具型別的 wither 方法。相關的成員是:
compress(bool,預設true)——啟用 FlateDecode 壓縮。用withCompress(bool $compress): self調整它時,方法會傳回一個新的Config,其中旗標已變更,其餘每個欄位都保留。
在建構時把一個 Config 附加到文件上:
NextPDF\Core\Document::createStandalone(?Config $config = null): self會建構一份帶有臨時登錄表的文件,適用於 CLI 指令稿或短生命週期的行程,並套用你的Config。
有兩個成員會影響大小調節項可運作的素材,但兩者本身都不是壓縮控制項:
imageCacheBytes(int,預設52_428_800)會為記憶體內的影像快取設定上限,而withImageCacheBytes(int $bytes): self會變更它。這會限制一次建構期間的尖峰記憶體。它不會重新取樣、重新壓縮,也不會以其他方式縮小你嵌入的影像——它是記憶體上限,不是輸出大小控制項。fontsDirectory(string)與withFontsDirectory(string $dir): self會設定字型檔的預設搜尋路徑,供子集化路徑使用。
字型相關工作透過文件上的排版介面進行:
setFont(string $family, string $style = '', float $size = 12.0): static會選取一個字面。當字族解析為一個可嵌入的字型檔時,寫入器會記錄你繪製的碼位,以便在儲存時對該字面進行子集化。addFontDirectory(string $directory): static會註冊另一個用來搜尋字型檔的目錄。
輸出是標準的三件組:getPdfData(): string 傳回位元組,save(string $path): void 以原子方式寫入它們,而 output(?string $filename, OutputDestination $dest): string 處理 HTTP 傳遞。
子集化沒有公開方法,也沒有旗標。它是嵌入字型並繪製文字所衍生的行為——寫入器會替你驅動 FontSubsetter / CffSubsetter,就在 NextPDF\Writer\PdfFontWriter 內部。
程式碼範例——快速上手
標題為「程式碼範例——快速上手」的區段這會建構一份明確啟用壓縮、並包含已嵌入且已子集化字型的文件,然後寫出位元組。它略去了錯誤處理以呈現呼叫形式;下方的正式環境範例則補上完整的防護。
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;use NextPDF\Core\Document;
// compress defaults to true; setting it explicitly documents intent.$config = (new Config())->withCompress(true);
$doc = Document::createStandalone($config);$doc->addFontDirectory(__DIR__ . '/fonts');$doc->addPage();
// Selecting an embeddable face records the glyphs used, so the writer// subsets this font automatically when the document is built.$doc->setFont('LiberationSans', '', 12);$doc->cell(0, 10, 'Invoice 2026 - subsetted, compressed output.', newLine: true);
$pdf = $doc->getPdfData();
file_put_contents(__DIR__ . '/small.pdf', $pdf);
printf("Wrote %d bytes.\n", strlen($pdf));程式碼範例——正式環境
標題為「程式碼範例——正式環境」的區段這是一支自成一體的程式。它會建構一份開啟壓縮的文件,從一個你控管的目錄嵌入字型,繪製文字讓子集器取得一組已用字形集合,並以原子方式寫出結果。它會捕捉建構與儲存路徑所拋出、最具體的 NextPDF 例外,然後為每一個例外附加脈絡後重新拋出,而不是吞掉它。把 NEXTPDF_FONT_DIR 指向一個放有 TrueType 或 CFF 字面、且你有授權嵌入的目錄;程式會在嵌入前先驗證該路徑。
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;use NextPDF\Core\Document;use NextPDF\Exception\CompressionException;use NextPDF\Exception\InvalidConfigException;
/** * Resolve and validate the font directory from a server-controlled source. * * Reading the directory from the environment keeps the path off the request * surface. The function rejects a missing or unreadable directory so the * embedding path never runs against untrusted or absent input. */function resolveFontDirectory(): string{ $configured = getenv('NEXTPDF_FONT_DIR'); $dir = $configured !== false && $configured !== '' ? $configured : __DIR__ . '/fonts';
$real = realpath($dir); if ($real === false || !is_dir($real) || !is_readable($real)) { throw new RuntimeException(sprintf('Font directory "%s" is not a readable directory.', $dir)); }
return $real;}
/** * Build a compressed, font-subsetted document and return its bytes. * * @param non-empty-string $fontDirectory Validated directory of embeddable fonts. * * @return string Raw PDF bytes. */function buildCompactPdf(string $fontDirectory): string{ // compress is true by default; pin it so the intent is explicit and the // streaming writer path honours it regardless of any wrapper defaults. $config = (new Config()) ->withCompress(true) ->withFontsDirectory($fontDirectory) // Bound the image cache so a build cannot exhaust memory. This is a // memory ceiling, not an output-size control. ->withImageCacheBytes(16 * 1024 * 1024);
$doc = Document::createStandalone($config); $doc->addFontDirectory($fontDirectory); $doc->addPage();
// Rendering with an embeddable face records the used codepoints, which the // writer turns into a font subset at build time. $doc->setFont('LiberationSans', '', 12); $doc->cell(0, 10, 'Invoice 2026', newLine: true); $doc->cell(0, 10, 'Compressed streams plus an automatic font subset.', newLine: true);
// getPdfData() triggers the build: page streams and the subset font program // are FlateDecode-compressed before the bytes are returned. return $doc->getPdfData();}
try { $fontDirectory = resolveFontDirectory(); $pdf = buildCompactPdf($fontDirectory);} catch (CompressionException $e) { // Raised if the zlib encoder hard-fails while compressing a stream. throw new RuntimeException( sprintf('Compression failed for a %s stream.', $e->getAlgorithm()), previous: $e, );} catch (InvalidConfigException $e) { // Raised by the output path for an invalid destination configuration. throw new RuntimeException( sprintf('Output configuration "%s" was rejected.', $e->getConfigKey()), previous: $e, );}
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');$path = $out !== false && $out !== '' ? $out : __DIR__ . '/small.pdf';
if (file_put_contents($path, $pdf) === false) { throw new RuntimeException(sprintf('Could not write PDF to "%s".', $path));}
printf("Wrote %d bytes to %s.\n", strlen($pdf), $path);預期的 STDOUT(位元組數取決於字型與該次建構):
Wrote <n> bytes to <path>.邊界情況與陷阱
標題為「邊界情況與陷阱」的區段- 壓縮預設開啟。 一個全新的
Config的compress設為true。你幾乎不需要用到withCompress()。只在想記錄意圖時才明確設定它;或是在除錯建構中需要讀取原始串流時,用它停用壓縮。 - 關掉壓縮會讓檔案變大,而不是變小。
withCompress(false)是用來檢視未壓縮串流的診斷輔助。它絕不是一種大小最佳化。上線時請保持壓縮開啟。 - 子集化需要一個真正嵌入的字型。 Base14 標準字型(Helvetica、Times、Courier 及其相關字型)是以名稱參照的,在一般文件中不攜帶嵌入的字型程式,因此沒有東西可以子集化。子集化只會縮減你從字型檔嵌入的字面。
- 子集化是自動且無聲的。 沒有旗標、沒有方法,也沒有確認訊息。如果你嵌入了一個字型並用它繪製了文字,寫入器就會對它做子集化。嵌入的字型程式會攜帶一個六字母的子集前綴標籤(例如
ABCDEF+LiberationSans),讓閱讀器能分辨子集與完整嵌入。 - 省得太少就保留完整字型。 當子集化所省下的不到字型程式大小的一成時,子集器會傳回原始字型程式。這是一道刻意設下的下限:重建不值得換取那一點邊際效益。嵌入一個本來就很小的字面,或繪製了它幾乎所有的字形,都可能落入這種情況。
imageCacheBytes不是影像大小旋鈕。 它限制的是記憶體,不是輸出位元組。NextPDF Core 會嵌入你給它的影像資料;沒有重新取樣、降取樣或重新編碼的步驟。如果你需要更小的影像,請在嵌入它們之前先調整大小並重新編碼。- 沒有物件串流或去重設定存在。 NextPDF Core 並未提供 PDF 2.0 物件串流或資源去重的開關。不用尋找這種開關——大小調節項就是串流壓縮與字型子集化。
等級 9 的壓縮是寫入一個串流時主要的 CPU 成本。它以幾個百分點的建構時間換取最小的輸出。成本與未壓縮位元組數成線性關係,因此頁面數與嵌入字型資料量就決定了這項預算。子集化會為每個嵌入字面額外增加一次處理:剖析字型的表目錄、解析已用字形閉包,並重建所需的字型表。對一個大型 CJK 字面而言,這是兩個調節項中成本較高的一個,但它每個字型只執行一次,而非每頁一次。那道省幅一成的下限之所以存在,部分原因就是要在不划算時,讓這次處理離開熱路徑。一份帶有單一嵌入子集的小型文件,能輕鬆落在 1500 ms 的牆鐘時間與 96 MB 的尖峰預算之內。請把 imageCacheBytes 設成你真正的上限,讓一個嵌入大量影像的建構在記憶體上快速失敗,而不是發生置換。
安全性注意事項
標題為「安全性注意事項」的區段建構作業在行程內執行;沒有任何文件位元組離開主機,也不會發出任何網路呼叫。請把任何外部提供的字型或影像都當成不可信任的輸入:
- 驗證字型目錄。 正式環境範例會從一個由伺服器控管的環境變數讀取字型路徑,並在嵌入前拒絕缺漏或不可讀的目錄。絕不要從請求欄位推導出字型路徑。
- 只嵌入你有授權再散布的字型。 子集仍然是一個嵌入的字型程式。在你交付一份攜帶該字面的文件之前,請確認授權允許嵌入。
- 格式錯誤的字型會拋出例外,而不是無聲地損毀。 一個剖析失敗的字型檔會拋出
NextPDF\Exception\FontParsingException,而一個硬性的 zlib 失敗會拋出NextPDF\Exception\CompressionException。請捕捉最具體的例外並據以行動。絕不要把建構包進一個空的catch裡。 - 絕不要把使用者輸入內插進輸出路徑。 範例會寫入一個固定路徑或一個由伺服器控管的旁路通道,並透過
save()中的原子寫入器拒絕串流包裝器與 null 位元組。請從由伺服器控管的值推導輸出路徑,以避免路徑遍歷。 - 文件中不放任何密鑰。 不要在一份你要回傳給用戶端的產生文件中嵌入憑證、權杖或內部識別碼。
符合性
標題為「符合性」的區段本範例本身不提出任何規範性的標準主張。它所使用的機制是由 PDF 2.0 規格定義的:FlateDecode 串流壓縮(ISO 32000-2:2020 §7.4.4),以及帶有六字元子集前綴的字型子集命名(ISO 32000-2:2020 §9.9)。NextPDF 會將兩者都作為標準寫入路徑的一部分輸出;除了 compress 旗標之外,你不必再為它們做任何設定。本頁宣告的 structural 可重現性設定檔,反映出寫入器固定了 DEFLATE 等級,因此壓縮後的串流是確定性的,而文件層級的識別碼如果你未一併設定確定性設定,仍可能在不同執行之間變動。關於子集化背後的嵌入機制,請參閱下方連結的嵌入與子集化範例。
另請參閱
標題為「另請參閱」的區段- 嵌入並子集化一個 TrueType 字型——註冊一個字面、用它繪製,並檢視嵌入的子集標籤。
- 組合文字與字型——更廣泛的文字與字型組合介面,並供應子集化路徑。
- 組態模組參考——完整的
Config值物件、它的 wither 方法,以及它們的預設值。 - 例外感知的錯誤處理——位於
CompressionException、FontParsingException與InvalidConfigException背後的 NextPDF 例外階層。