HotPDF 的 THotPDF.CompressDocument 是一个开关,让 BeginDoc 产出这个组件能写出的最小无损 PDF:最高档的 FlateDecode、带 object stream 的交叉引用流、字体子集化,以及紧凑字体子集——把保留的字形重新编号,藏在显式的 /CIDToGIDMap 后面。EndDoc 随后把你自己的设置放回去。一份三页的 Arial 和 SimSun 测试文档从 10.2 MB 掉到 20 KB,渲染完全一致
CompressDocument 到底打开了什么?
CompressDocument 为一份文档覆盖六项写出设置外加 object stream 上限,事后全部还原。在 BeginDoc、PDF 版本还没定下来之前,HotPDF 记下你的值,把 Compression 设为 cmFlateDecode、CompressionLevel 设为 clMaximum,打开 EnableFontSubsetting 和 CompactFontSubsetting,并启用 UseXRefStream 与 UseObjectStreams(ISO 32000-1 §7.5.7 和 §7.5.8)。object stream 需要 PDF 1.5,所以较旧的 Version 在未被锁定时抬到 1.5。PDF/A-1 禁止这两种结构,所以 PDF/A-1 文档保留经典交叉引用表,只做 Flate 和字体这两件事。图像则原样保留,你当初怎么嵌的就怎么在
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'invoice-2026-1042.pdf';
Pdf.CompressDocument := True; // 由 BeginDoc 应用,由 EndDoc 撤销
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-1042');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
还原发生在 EndDoc 最外层的 finally 里,所以报表写到一半抛异常,也不会让一个长寿组件在下一个任务里卡在最高压缩上。CompressDocument 属性本身保持 True;只有它借走的六项设置回去。版本的处理更细心。只有文档最终仍停在 1.5 时,HotPDF 才撤销自己抬到 1.5 的动作,所以当另一个特性在运行期间把文件推到 1.6(比如一个内嵌的 OpenType 字体),较高的版本保持不变,和不压缩时一模一样
不压缩的字体子集为什么还是大?
经典的 TrueType 子集丢掉你从不绘制的轮廓,但每个字形 ID 都留在原位,正是这个编号让它沉。内容流里出现的 CID 等于原始 GID,所以子集必须为最高保留字形之下的每个槽位——不管空不空——保留一个 loca 偏移和一个 hmtx 条目。对拉丁字体这份开销是噪音;对 SimSun 这类 CJK 字体,它的汉字深居一张巨大的字形表,两个中文字符拖着一整套按整个字体尺寸准备的表。整形字形的字体子集闭包规则决定哪些字形活下来;紧凑化关心的是幸存者要花多少钱
CompactFontSubsetting 把保留的字形重新编号成从 0 起步的稠密区间,并在 CIDFont 上写一个 /CIDToGIDMap 流,ISO 32000-1 §9.7.4.2 把它定义为以 CID 索引、每项两字节 GID 的表。这张表就是全部戏法。内容流、/W 宽度数组和 ToUnicode CMap 都保留原始 CID,已写出的东西一个都不用改;变的只是 CID 到字形的查表,挪进了这张 map。在催生这个特性的测试里,两个字符的 SimSun 从 24.8 KB 字体数据降到 3.1 KB
紧凑化有硬边界,而且它安静降级而不是失败。HotPDF 只为 Type 0 TrueType 字体构建紧凑子集——既包括经 SetFont 设置且开着子集化的,也包括经 RegisterUnicodeTTF 注册的。简单 TrueType 字体靠字体程序内部的 cmap 找字形,重新编号会破坏它,所以保留稀疏子集。OpenType-CFF 字体也没有紧凑路径。紧凑构建失败时回落到稀疏子集而不是抛异常。属性默认关闭,既有输出保持逐字节一致;而在 PDF/A 下,注册的 Unicode 字体总是得到紧凑子集
Pdf.EnableFontSubsetting := True;
Pdf.CompactFontSubsetting := True; // 不开 CompressDocument 也能用
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('SimSun', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, WideString('Total: '#$4E2D#$6587));
Pdf.EndDoc;
打包写出器怎么挤压文件结构?
字体和流都小了之后,字典和交叉引用数据成了剩下的最大开销,所以 CompressDocument 背后的 object stream 写出器把这两样也修剪。object stream 与增量更新指南讲容器格式本身;压缩路径在其上加了四项细化:
- 按 ISO 32000-1 §7.2.2 的紧凑语法:只有当两个 token 不加空格就会连成普通字符时才写空格,于是
/Type /Page变成/Type/Page - 交叉引用流字段取 §7.5.8.2 允许的任意宽度,所以 16 MB 以下的文件每个偏移存 3 字节而不是 4 字节
- 每个 object stream 装最多 250 个对象而不是通常的 100,除非你经
ConfigureAdaptiveObjectStreamPacking设了自己的上限 - 文件未加密时,Catalog 和 Info 字典也打包进 object stream;加密输出把它们留在顶层
紧凑语法有个坑,扩展写出器的人值得知道。签名在文件写完后通过在字节里搜索字面占位符 /ByteRange ( 和 /Contents < 来填值,紧凑拼法会把它们变成 /ByteRange( 和 /Contents<,搜索永远找不到。因此签名字典(Type Sig 或 DocTimeStamp、FT Sig)和加密字典保持带空格的版式。还有一个相关缺陷影响 v2.766.41 之前的构建:每一次 object stream 保存——CompressDocument 也在内——都以两行 %PDF- 头开始,严格校验器如果挑你的输出,就升级
已加载的 PDF 也能压缩吗?
能,走选项重载 CompressLoadedDocument(Options, Info),它对既有文件跑同一套无损步骤。用 THPDFLoadedDocumentCompressionOptions.Default 时,它移除未用的页面资源、合并相同字体和表单、以内嵌紧凑子集做字体子集化、对未过滤、Flate、LZW、ASCII 和 RunLength 流在结果更小时用 Flate 重压,并让下一次保存使用 object stream。HighRatioFlate 默认关闭;PDF/A-1 和增量保存跳过 object stream。无参的 CompressLoadedDocument 重载是更老、更窄的调用,只对未压缩流做 Flate 压缩
var
Doc: THotPDF;
Options: THPDFLoadedDocumentCompressionOptions;
Info: THPDFLoadedDocumentCompressionInfo;
begin
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
Doc.LoadFromFile('quarterly-report.pdf');
Options := THPDFLoadedDocumentCompressionOptions.Default;
Doc.CompressLoadedDocument(Options, Info);
if Info.RefusedBySignaturePolicy then
Writeln(Format('Left untouched: %d signature fields', [Info.SignatureCount]))
else
begin
Writeln(Format('Compact fonts: %d, stream bytes saved: %d',
[Info.Fonts.CompactSubsetFontCount, Info.BytesSaved]));
Doc.SaveLoadedDocument('quarterly-report-compact.pdf');
end;
finally
Doc.Free;
end;
end;
已加载路径上有两条边界。每一步都会重写签名覆盖的字节,所以带签字段的文档整体被拒:调用返回 0、置 RefusedBySignaturePolicy、什么都不改,除非你设 AllowSignatureInvalidation,此后 Info.SignaturesInvalidated 告诉你放弃了什么。紧凑化在这里也比创建路径保守。HotPDF 只紧凑化那些仅被 Identity /CIDToGIDMap(CID 等于 GID)的 CIDFontType2 字体使用的字体程序,跳过已带 map 流、/CIDSet 或彩色字形表(COLR、sbix、CBDT、SVG)的程序,因为紧凑重建会丢掉彩色图层。还要注意 Info.BytesSaved 只合计资源、字体和流这几步;object stream 的收益在文件写出时才显现
实际能期待什么结果?
收益取决于一份文件里有多少未压缩结构和超重字体数据,而不是它有几页。三页的 Arial 和 SimSun 样例在用 CompressDocument 生成时从 10.2 MB 缩到 20 KB,未压缩原件走 CompressLoadedDocument 时从 10.2 MB 缩到 19.8 KB,两种方式渲染完全一致。已经紧凑的 PDF 几乎不动:回归集里这类文件的保存结果在原尺寸的 -0.07% 到 +0.06% 之间。照片多的文件收益很小,因为两条路径都不碰图像数据
如果你每晚生成同样的 CJK 报表,把紧凑子集和磁盘上的持久字体子集缓存配对,子集化工作就不必每次运行重来;压缩输出的 diff 按对象内容比而不是按字节比,因为一个改动的字段就会让整个 object stream 重新 Flate。完整的属性与记录参考见 HotPDF Delphi PDF component 产品页