Motto:计算生成 PDF 时嵌入图片的尺寸

2024-06-06, 星期四, 19:50

本文是 motto-html 项目的一部分

制作 HTML 模版时,通常会依据印刷制品的实际尺寸确定元素的大小,比如在证书上添加一寸照什么的。考虑到我们的场景,print CSS 是受到支持的,这个时候可以使用 pt 作为单位。

pt,又称 point,是一个大小固定的单位。1 pt 等于 1/72 英寸,用国际单位制的话,1 pt 约为 0.35mm(iOS 里还有个 pt 的概念,不过不在本次讨论范围内)。

img#photo {
    width: 228pt;
    height: 128pt;
}

这个样式规定了图像的长为 79.8mm,宽 44.8mm,可以打印出来用尺子量下。

不过这张图片会整个嵌入到生成的文件中。也就是说,如果笔者使用了一张 6MB 的图片素材,目标文件就会增大 6 MB,因此需要在嵌入前先进行压缩。

在 motto-html 中,图片压缩是通过 java.awt.image.BufferedImagejava.awt.image.AffineTransformOp 实现的,压缩动作需要提供以像素为单位的目标尺寸。

Flying Saucer 的 org.xhtmlrenderer.pdf.ITextRenderer 中有两个魔法数字:

float DEFAULT_DOTS_PER_POINT = 20f * 4f / 3f;
int DEFAULT_DOTS_PER_PIXEL = 20;

1 inch 含有 1920 个 dot,96 个 pixel。Dot 表示一个印刷颜色墨点,Pixel 表示一个像素点,这两个都是相对大小的单位,对应的物理尺寸依具体设备而不同。也就是说 ITextRenderer 的默认 DPI(Dots per Inch)为 1920,PPI(Pixels per Inch)为 96。

如此这般,228 pt × 128pt 似乎应当换算成 304px × 171 px。

要是真这么简单就没有本文了。

笔者选用了 2022 年 3 月 31 日的 Bing 搜索背景(摄影师 Susanne Kremer)做测试,复杂的钢架结构可以很好地展示图像质量的变化。

<img src="${COMPRESSED}"></img>
<img style="width: 228pt; height: 128pt;" src="${ORIGINAL}"></img>

左侧的图片进行了 scaleWithPixel(304, 171) 的操作,右侧的图片没有经过压缩,通过 CSS 样式指定了大小。使用 DevTools 查看这两个元素,虽然左边的图片惨不忍睹,其分辨率确实是 304 × 171 px。右边的元素则有这样的说明:

Rendered size: 304 × 171 px
Intrinsic size: 8000 × 4500 px

虽然图片的尺寸是 304px × 171 px,其分辨率却远超这个数值。

理论上笔者应该通过修改 DOTS_PER_POINTDOTS_PER_PIXEL 的值,获得 pixel 与 point 的正确换算关系,然而这两个值影响了最终渲染结果的方方面面,可谓牵一发动全身。笔者并不是很清楚该如何测量和计算得到理想的数值,所以另一种方案是要显示 4 × 4 的图像,先提供 8 × 8 的素材,通过 CSS 设置其尺寸为 4 × 4,如果把 DPR 的概念套入这个场景,可以称 DPR = 2。

设备像素比率(Device Pixel Ratio,DPR)是设备的物理像素与逻辑像素之间的关系,代表在屏幕的每个方向,一个 CSS 像素会用多少个物理像素来显示。浏览器可以使用 window.devicePixelRatio 获取 DPR。

尝试了下 DPR = 4 的时候才会有比较好的效果,到 DPR = 8 的时候就几乎无法分辨了。

motto-html:1.1.0 提供了 EmbeddedImage#setDevicePixelRatio,对每个 EmbeddedImage 实例,默认的 DPR 为 1。