技术文章

从 Delphi 在 PDF 中嵌入 AVIF、HEIF 和 JPEG XL 图像

PDF Library for Delphi 通过 AddModernImageFromFile 及其流和字符串变体接受 AVIF、HEIF 和 JPEG XL 图像作为输入,在图像进入 PDF 图像对象的过程中保留 alpha 通道、嵌入的 ICC 色彩配置文件和 16 位通道。格式检测基于一次有界的魔数读取,解码则通过一个可替换的后端完成,因此对于一个实际上不属于这些格式的文件,不会调用任何外部程序

这些格式是通过手机进入文档工作流的。iOS 多年来一直默认生成 HEIC,Android 设备生成 AVIF,而一名现场技术人员拍摄一个损坏部件后发来的图像,一个写于 2015 年的 PDF 报表生成器根本打不开。通用的回退路径,即通过平台位图解码,得到的结果稳定是 8 位色彩,并在过程中丢失 alpha 通道和色彩配置文件

现代图像路径保留了哪些位图转换会丢失的内容?

三样东西,每一样都对应着一种依赖它的工作流。alpha 通道被保留下来,这对叠加在页面内容上的标志和产品抠图很重要。ICC 色彩配置文件被保留下来,这对任何要印刷或做色彩匹配的内容都很重要。16 位通道被保留下来,这对医学和科研影像很重要,因为 8 位量化会破坏影像本要捕捉的那些细微色阶

让图像经过一次平台位图,会在这一步里把这三样东西全部丢掉,而且丢得悄无声息:生成的 PDF 看起来大致没问题,直到有一天印刷厂问企业标准红为什么不对,才有人注意到。现代图像调用中的选项值 8,正是同时保留 alpha、ICC 和 16 位通道的开关,而且它是这些调用的默认值

把一张图像加到页面上

该调用返回一个图像标识符,之后既可以先选中再绘制,也可以一步绘制并释放:

uses
  PDFlibrary, PDFlibModernImage;

var
  Lib: TPDFlib;
  ImageID: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.NewDocument;
    Lib.SetPageSize('A4');
    Lib.NewPage;

    // Options = 8 保留 alpha、ICC 和 16 位通道
    ImageID := Lib.AddModernImageFromFile('site-photo.heic', 8);
    if ImageID > 0 then
      Lib.DrawImageAndRelease(ImageID, 40, 40, 515, 340)
    else
      Lib.DrawText(40, 40, 'image could not be decoded');

    Lib.SaveToFile('inspection-report.pdf');
  finally
    Lib.Free;
  end;
end;

检测在解码之前进行,而且刻意做得很窄。库会读取一段有界的头部,识别标识 AVIF 和 HEIF 的 ISO 基础媒体文件格式品牌,并识别 JPEG XL 的原始签名和容器签名两种形式,然后恢复调用方的流位置。未知或伪装的输入永远不会到达外部编解码器,这样一个改了扩展名的可执行文件就不会被当成图片交给解码器

解码实际发生在哪里?

现代图像格式都是庞大而复杂的编解码器,把其中一个塞进 PDF 库里会是个怪异的设计选择。默认后端会在进程内动态加载一个可部署的 MagickWand 模块,并按一个明确记录的顺序去寻找它:一个你显式设置的文件或目录、环境变量、可执行文件所在目录,以及系统搜索路径

已经自带解码器、或者绝不能加载任何外部模块的应用,可以改为注册自己的回调。契约很简单:读取输入流,把 PNG 写入输出流,遵循请求的方向信息:

function MyDecoder(InStream, OutPNG: TStream;
  ImageFormat: TPDFlibModernImageFormat;
  ApplyOrientation: Boolean): Boolean;
begin
  // 用你自己的编解码器解码 InStream,把 PNG 字节写入 OutPNG
  Result := DecodeWithBundledCodec(InStream, OutPNG,
    ImageFormat, ApplyOrientation);
end;

begin
  RegisterModernImageDecoderBackend(MyDecoder);
  // ... 添加图像 ...
  ClearModernImageDecoderBackend;    // 回到默认后端
end;

部署这一步得到一项便利,也保留了一处刻意的克制。如果编解码器目录下含有 modules\coders 子目录,库会为这种布局所需的编解码器环境变量补上取值,但仅在宿主应用尚未自行设置它们的情况下才会这样做。已有自己一套运行时部署策略的应用,可以继续沿用它自己的策略

为什么中间要转成 PNG?

通过一份内存中的 PNG 来搭桥,而不是直接用原始像素缓冲区,看起来像是多绕了一步,实际上却是最省钱的正确做法。PNG 能表达所有必须保留的信息:alpha、色彩类型、位深和一份嵌入的 ICC 配置文件,而库早已有一条成熟且经过充分测试的路径,能把 PNG 转换成带正确滤镜和色彩空间的 PDF 图像对象。复用这条路径,意味着现代格式能继承多年积累下来的正确性成果,而不必再造一套并行实现

这座桥完全在内存中完成,因此不会创建临时文件,崩溃后也无需清理。有一处细节需要专门处理:某些转换会在切换格式时丢弃 ICC 配置文件。后端因此会在格式切换之前先捕获源配置文件,用 Flate 压缩它,构建一个带重新计算过 CRC 的合法 iCCP 数据块,并移除任何会与之冲突的 sRGB 数据块。在测试中,一份解码后的 AVIF 保留了 16 位 RGBA 并带 16 位 alpha 通道,从生成的 PDF 中提取出的配置文件与源配置文件逐字节一致,共 60,960 字节

在生产环境启用前的一些实用提示

请在启动时检查可用性,而不是等到第一张照片进来才检查。ModernImageCodecAvailable 报告是否有后端可用,当部署环境把编解码器放在非标准位置时,SetModernImageCodecLibrary 用来指定一个显式的文件或目录:

Lib.SetModernImageCodecLibrary('C:\MyApp\codecs');
if Lib.ModernImageCodecAvailable = 0 then
  Log('modern image input unavailable - HEIC and AVIF will be refused');

请留意结果的文件大小。一张带嵌入配置文件的 16 位 RGBA 图像会生成一个较大的 PDF 图像对象,一份带四十张这类图像的报表体积会相当可观。当文档面向屏幕查看而不是印刷时,在嵌入前先降采样才是正确的取舍,通用的体积控制手段见 PDF 文件体积优化

最后,请有意识地决定色彩策略。对归档和印刷工作来说,保留源配置文件是正确的;当一批混合来源的照片必须看起来一致时,转换到统一的文档级色彩空间才是正确的,转换方式见 把文档重新着色到另一个色彩空间。如果你需要确认最终写入文件的到底是什么,文本、图像与字体提取 中的检视路径会报告文档所携带的图像对象

现代图像输入、色彩管理和图像优化都是适用于 Delphi、C++Builder 和 Free Pascal 的同一个库的一部分;完整功能列表见 PDF Library for Delphi 页面