扫描的医疗幻灯片,航测图块,或者在全动态范围内存档的胶片帧。这就是作为 JPEG 2000 送达的图片,它们以此种格式送达是有原因的。该格式每通道保留 12 或 16 位,使用小波变换而不是 JPEG 使用的块 DCT 进行压缩,并且可以从一个代码流中以无损或有损方式对同一张图片进行编码。当由这些来源构建的文档必须生成为 PDF 时,图像必须通过 PDF 规范专为此编解码器保留的滤镜进行传递
HotPDF v2.228.0 恢复了该路径正常工作的 JPEG 2000 解码引擎。早期的版本附带了返回 nil 的存根函数的单元,因此 API 虽然存在却什么也解不出。当前的引擎静态绑定了 OpenJPEG 2.5.4,并将 JP2 或 J2K 来源转换为 HotPDF 可放在页面上的像素
PDF 中的 JPXDecode 滤镜
ISO 32000-1 在第 7.4.9 节定义了 JPXDecode 滤镜。PDF 图像 XObject 在其流字典的 /Filter 条目中指明其压缩方式,JPXDecode 这个值表示该流数据是 JPEG 2000 代码流,而不是 /DCTDecode 所携带的基线 JPEG。该滤镜使得 PDF 能够容纳具有高位深的小波压缩图像数据,并接纳了编解码器的无损和有损模式,因为模式本身是代码流的一个属性,而非其周围包裹物的属性
最后一点值得牢记。JPEG 2000 是一个带有无损特例的单一算法,并非两种独立的格式。可逆的 5/3 小波能精准重建原始样本;不可逆的 9/7 小波则用这份精准换取了更小的文件。解码器在读取时对两者一视同仁,这就是为什么 HotPDF 只需要一条解码路径,就能接纳 JPXDecode 流抛给它的任何内容
解码器对像素做了什么
在通常情况下,PDF 图像 XObject 期望在 DeviceGray 或 DeviceRGB 中每分量有 8 位。JPEG 2000 往往超出这个范围,并且其分量模型比打包光栅(packed raster)更为通用,因此在将数据用作正常图像之前,解码器要完成三项工作
第一,高位深分量会被重采样到 8 位。12 位或 16 位样本会按比例缩小至 0 到 255 的范围,使结果成为普通的 8 位光栅。有符号分量会先被移动至无符号范围。这个细节至关重要,因为它本身是有损的:当一幅 16 位灰度扫描图像变成 8 位的 PDF 图像时,就会丢失其深刻的色调范围,这对于屏幕显示和打印输出而言是正确的权衡,但并不适合进行再次存档
第二,YCbCr(该编解码器称之为 SYCC)色彩空间会被转换为 RGB。为了压缩效率,JPEG 2000 经常在亮度和色度空间中存储色彩,这与基线 JPEG 使用的思路相同,解码器则应用标准逆变换,让页面获取真实的 RGB 色彩
第三,被子采样的分量会通过最近邻复制(nearest-neighbor replication)被上采样。色度通道经常以半分辨率存储,因此解码器会以各自的尺寸及采样率读取每个分量,随后复制样本,让每个通道在进行交错处理之前都恢复到完整的图像尺寸。最近邻让这一步代价极小;它要填充的色度起初就是低频的,所以可见的损耗微乎其微
JP2 box 与原始 J2K 代码流
JPEG 2000 文件有这两种形态,而 HotPDF 从前几个字节来检测其正在读取哪种,并非依赖文件扩展名。JP2 文件是一个以 box 构建的容器:它以十二个字节的签名 box 00 00 00 0C 6A 50 20 20 作为开头,将代码流与描述色彩空间、分辨率以及元数据的 box 封装在一起。原始 J2K 代码流则未附带任何容器,以 SOC 标记 FF 4F FF 51 开始。解码器会读取这些前导字节,识别出签名,并为每种情况选取相匹配的 OpenJPEG 编解码器
两种形态都被处理了,因为两者在自然状态下都会出现。需要附带元数据的捕获设备和存档会发出 JP2;追求载荷极小化的工具则发出纯代码流。格式类型被建模为枚举 TJpeg2000FileType,其成员有 jtInvalid、jtJP2、jtJ2K 以及 jtJPT。JPT 成员指定 JPIP 流变体;字节签名检测器可解析它能解码的两种形态(JP2 和 J2K),并将所有其它东西都报告为 jtInvalid,如此一来,不被支持的输入就会干脆地失败,而不会生成垃圾信息
uses
HPDFJpeg2000;
var
Decoder: THPDFJpeg2000Decoder;
Pixels: TJpeg2000ByteArray;
begin
Decoder := THPDFJpeg2000Decoder.Create;
try
if Decoder.LoadFromStream(Input) then // JP2 or J2K, auto-detected
if Decoder.GetImageData(Pixels) then
// Pixels is 8-bit interleaved, ColorComponents channels wide,
// row-major top to bottom: ready for a DeviceGray/DeviceRGB XObject.
ProcessRaster(Decoder.Width, Decoder.Height,
Decoder.ColorComponents, Pixels);
finally
Decoder.Free;
end;
end;
编码端的无损与有损
解码器在没被告知其处于何种模式的情况下会读取这两种模式。只有当你反其道而行之,去生成一个 JPEG 2000 文件时,这种选择才成为一个参数,而 HotPDF 也能通过 TJpeg2000Bitmap 类做到这一点,该类是 TBitmap 的后代,将光栅数据作为 JP2 来加载和保存。两个属性掌管着输出。LosslessCompression 是一个布尔值,为 true 时选择可逆小波;CompressionQuality 是 TJpeg2000QualityRange,这是一个从 1 到 100 的整数,其中 1 代表文件小而画质差,100 代表文件大而画质保真。默认值被置于命名常量中:Jpeg2000DefaultLosslessCompression 是 False,Jpeg2000DefaultLossyQuality 是 80
这个决定是一个基于内容的决定。无损模式适合主副本(master copy)、医疗或法律扫描件,以及任何日后可能会重新编码且决不能累积世代损耗的内容。质量为 80 的有损模式则适合要在屏幕显示或打印输出的图片,小波优雅的降级能以文件体积极小为代价,且读者察觉不到任何伪影。有一个 CMYK 的警告需要标明:位图公开了 SetCMYK 来将四通道数据标记为 CMYK 而不是 RGBA,这对于保持分色完好无缺的打印管线来说很重要
uses
HPDFJpeg2000;
var
Bmp: TJpeg2000Bitmap;
begin
Bmp := TJpeg2000Bitmap.Create;
try
Bmp.LoadFromStream(Source); // decode an existing JP2/J2K
Bmp.LosslessCompression := True; // reversible 5/3 wavelet
// or, for a smaller lossy file:
// Bmp.LosslessCompression := False;
// Bmp.CompressionQuality := 80; // matches the default
Bmp.SaveToStream(Output); // always writes a JP2 file
finally
Bmp.Free;
end;
end;
为什么没有装载时解码(decode-on-load)图像过滤管线
一个架构方面的事实塑造了你对这一切的使用方式,而人们很容易假设出相反的情况。HotPDF 没有通用的装载时解码图像滤镜。当你打开一个已经包含 JPXDecode 图像的 PDF 时,引擎并不会去解码那个流。它会将那些 JPEG 2000 字节完好无损地保留下来,所以页面复制或文档合并会将被触动丝毫的图像原封不动地搬运过去。解码器只有一个入口点,且在创建侧:基于文件的 AddImage,依据文件扩展名分派去处理 .jp2、.j2k、.jpt 和 .jpc 资源
这种拆分是正确的设计,而非局限。若是在装载时去解码被嵌入的 JPX 流,只为在保存时重新对其编码,这会把无损的存档图像转换为有损的,并使每次合并都发生膨胀,所有这一切仅仅是因为你意图将一张图片从一个 PDF 挪进另一个 PDF。将这个流原样直通(pass through)是一个无损操作,而且速度极快。解码会被推迟到它确实被需要的唯一时刻:当你从磁盘上向引擎交接一份 JPEG 2000 文件,并请求它为了将此图置于新页面而把该图光栅化时。在那个时点,文件必须变成像素,解码器才会运转
注册支持并放置图像
JPEG 2000 图片注册是一个可选项,隐藏在 HPDF_REGISTER_JPEG2000_PICTURE 编译开关背后,且该开关默认为关闭。原因在于一次真实的冲突,而非出于谨慎考虑:通过 TPicture 全局注册 jp2、j2k 以及 jpc 文件格式会干涉 ReportBuilder 的 TppDBImage 赖以运行的 BLOB 格式检测机制。在没有这种集成的环境中,定义这个开关,那么 TPicture 便能识别所注册的文件格式;如果不定义该开关,AddImage 的扩展名分派仍旧能够直接对 JPEG 2000 文件解码,因为那条路径完全不通过 TPicture
了解了这一点后,放置 JPEG 2000 图片就像放置任何其它 HotPDF 图像一样,同样也是那三个调用的节奏。递给 AddImage 一个 .jp2 路径,以及一种指示图片应以何种压缩方式储存在输出中的类型,随后使用 ShowImage 将返回的图像索引置于页面上
var
Pdf: THotPDF;
ImgIndex: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginDoc;
Pdf.AddPage;
// The .jp2 source is decoded through the OpenJPEG backend, then
// re-embedded with the compression you request here.
ImgIndex := Pdf.AddImage('Scan_16bit.jp2', icJpeg);
// x, y, width, height in points; final 0 is the rotation angle.
Pdf.ShowImage(ImgIndex, 72, 72, 400, 300, 0);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
你传递给 AddImage 的压缩方式主导着解码后的图片怎样被重新存储,而非其被读取的方式。被解码为位图的 JPEG 2000 文件可作为 DCTDecode JPEG、Flate 光栅或者另一受支持的滤镜再次输出,只要它适配文档即可。不论如何,来自 JP2 或 J2K 的解码总是最先发生,因而同一个调用接纳了一个经过小波压缩的源码,然后以你的管线剩余部分所期望的任何形态将其嵌进其中
关于图像和字体怎样抵达生成的输出这幅更广阔的图景,请参考我们在关于带有字体和图像的报告输出中(在 Delphi 内)的笔记。当你要拼装的文档从现有的 PDF 里重用内容时,此处所述的直通行为就与对象流和增量更新里的合并与修改机制结对生效了。JPEG 2000 解码引擎作为供 Delphi 以及 C++Builder 使用的 HotPDF Component 的一部分被交付,并且与在这个博客其余地方提及的图像、字体以及文档 API 并列提供