给浏览器拼贴工具加一个"A4预设"时,我以为最难的是布局计算。结果发现,真正的坑在于:画布上的A4只是一个形状,不是尺寸。如果不把这个说清楚,用户会在打印店才发现问题。

这里记录一下我踩过的坑,以及真正重要的数字。

打开网易新闻 查看精彩图片

画布没有物理尺寸

HTML画布是一个像素网格,只有宽高像素值,没有别的。纸张用毫米计量,连接两者的桥梁是DPI(每英寸像素数),而画布本身对此毫无概念。

所以当预设写着"A4"时,它真正能承诺的只有宽高比——1:1.414,这是所有ISO 216纸张共享的比例。像素最终在纸上变成什么,完全取决于你导出了多少像素。

我的预设导出1414×2000像素,正好是A4比例。换算一下:

  • A4 = 210×297毫米 = 8.27×11.69英寸
  • 1414像素 ÷ 8.27英寸 =171 DPI
  • 2000像素 ÷ 11.69英寸 =171 DPI

171 DPI,不是300。对于家用或办公打印机来说完全够用——正常观看距离下看不到单个像素。但对于照片冲印店,这个数据还不到他们预期的一半。

要达到打印店默认的300 DPI,同一页面需要:

  • 8.27英寸 × 300 =2480像素
  • 11.69英寸 × 300 =3508像素

2480×3508比1414×2000多出3.1倍像素。这就是整个取舍的核心。

为什么不直接按300 DPI导出?

因为内存,也因为用户实际上多在手机上操作。

一个2480×3508的画布是870万像素。浏览器以RGBA格式存储,每像素4字节,光位图就要约35MB,还没画任何内容。再加上源照片:一个12张照片的拼贴,每张是1200万像素的手机照片,如果全部以全尺寸解码,又要额外576MB。

这就是为什么崩溃总是发生在别人的安卓手机上。

让这个方案变得可用的修复方法是:在导入时、合成前对每张源图做降采样:

const MAX_SOURCE_EDGE = 2400;function fitSource(img) {const longest = Math.max(img.width, img.height);if (longest <= MAX_SOURCE_EDGE) return img;const scale = MAX_SOURCE_EDGE / longest;const c = document.createElement('canvas');c.width = Math.round(img.width * scale);c.height = Math.round(img.height * scale);c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);return c;

源图需要的像素,永远不会超过它在输出中占据的区域。如果一张照片落在1414×2000页面的四分之一区域,长边超过约1000像素的部分,都是解码后占用内存、最终被缩放器丢弃的。把长边限制在2400像素,视觉上没有任何损失,却消除了大部分内存压力。

值得知道的是:iOS Safari也会限制画布总面积(旧设备上历史上约为1670万像素)。超出限制不会报错——你会得到一张空白画布。静默失败是这一整片领域的主题。

非技术层面的问题

以上这些都不是用户抱怨的重点。他们抱怨,是因为他们期待的是……