跳转到内容

通过压缩与子集化缩减 PDF 文件大小

你的目标是在不损失保真度的前提下,生成内容所允许的最小 PDF。NextPDF 提供两个大小调节项,而且两者都默认开启:

  • 流压缩。 写入器会把每个页面内容流与每个嵌入的字体程序封装进一个 FlateDecode(zlib)流。NextPDF\Core\Configcompress 标志承载这项设置。构建流式文档时,可用 withCompress() 这个 wither 方法显式设置它。
  • 字体子集化。 当你嵌入一个 TrueType 或 CFF 字体时,写入器会重建字体程序,使其只携带文档实际用到的字形,然后再以 FlateDecode 压缩结果。这会自动发生——没有标志要设置,也没有方法要调用。一个包含 20,000 个字形的 CJK 字面,若只为某份文档贡献几百个字形,嵌入后的大小只占其磁盘上大小的一小部分。

需要先说明的是:NextPDF Core 并未提供图像重新采样、图像质量旋钮、对象流开关或资源去重设置。现有的大小控制项就是上述两个。本范例的其余部分会说明如何正确使用它们,以及每一项各自不会做什么。

前置需求:一份 Core 安装(composer require nextpdf/core:^3),以及用于子集化路径、你有授权嵌入的字体文件。

Terminal window
composer require nextpdf/core:^3

一份 PDF 是一棵对象树。最大的对象通常是内容流(每个页面的绘图运算符)与字体程序(嵌入的字形轮廓)。两者都是高度可压缩的文本与二进制数据,因此最有效的单一大小调节项,就是以 FlateDecode 压缩它们。在 PDF 2.0 中,FlateDecode 是 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 保持精简,靠的是让压缩保持开启(也就是默认值),以及嵌入真正的字体文件(让子集化有东西可以缩减),而不是靠调整一长串选项。

唯一需要设置的大小旋钮在配置对象上。

NextPDF\Core\Config 是一个不可变的 final readonly 值对象,搭配带类型声明的 wither 方法。相关成员是:

  • compressbool,默认 true)——启用 FlateDecode 压缩。用 withCompress(bool $compress): self 变更该值,它会返回一个新的 Config,其中标志已变更,其余每个字段都保留。

构建时将一个 Config 附加到文档上:

  • NextPDF\Core\Document::createStandalone(?Config $config = null): self 会构建一份带有临时注册表的文档,适用于 CLI 脚本或短生命周期的进程,并应用你的 Config

有两个成员会影响大小调节项可处理的素材,但两者本身都不是压缩控制项:

  • imageCacheBytesint,默认 52_428_800)会为内存中的图像缓存设置上限,而 withImageCacheBytes(int $bytes): self 会变更它。这会限制一次构建期间的峰值内存。它不会重新采样、重新压缩,也不会以其他方式缩小你嵌入的图像——它是内存上限,不是输出大小控制项。
  • fontsDirectorystring)与 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 等级,因此压缩后的流是确定性的;如果你没有同时配置确定性设置,文档级别的标识符仍可能在不同运行之间变动。关于子集化背后的嵌入机制,请参阅下方链接的嵌入与子集化范例。