生成带真实物理尺寸的PNG以实现打印机自动识别打印尺寸的方法
解决方案
核心结论
- 添加PNG标准定义的
pHYschunk是实现打印自动识别物理尺寸的通用方案,主流操作系统、打印软件、办公软件均支持读取该字段获取DPI信息,无需用户手动调整打印尺寸 - 你当前代码的像素计算逻辑是正确的(英寸数值*DPI得到像素宽高),只需要在导出PNG时手动插入
pHYschunk即可
实现原理
PNG的pHYs chunk存储的是图片的物理像素密度,单位为像素每米,300DPI换算为像素每米的公式为:300 / 0.0254 ≈ 11811,把这个值写入pHYs chunk后,打印程序就会自动用像素宽高除以DPI得到实际打印尺寸,和你预设的2×1英寸完全匹配。
修改后的完整代码
你只需要替换原有JS中canvas.toBlob的部分逻辑,给导出的PNG插入pHYs chunk即可:
JS代码
const canvas = document.getElementById("preview"), ctx = canvas.getContext("2d"), width = document.getElementById("width"), height = document.getElementById("height"), link = document.getElementById("download"), dpi = 300, offset = 0.5; //make lines true 1px wide by shifting it 0.5 pixels // 插入pHYs chunk的工具函数 function insertPngPhysChunk(blob, dpi) { return new Promise((resolve) => { const reader = new FileReader(); reader.onload = () => { const arr = new Uint8Array(reader.result); // PNG文件头是8字节,之后是IHDR chunk const physChunk = new Uint8Array(21); // 4长度 + 4标识 + 9数据 + 4校验和 // 写入chunk长度:9字节数据 physChunk[0] = 0; physChunk[1] = 0; physChunk[2] = 0; physChunk[3] = 9; // 写入pHYs标识 physChunk.set([0x70, 0x48, 0x59, 0x73], 4); // 写入X方向像素每米 const pxPerM = Math.round(dpi / 0.0254); physChunk[8] = (pxPerM >>> 24) & 0xff; physChunk[9] = (pxPerM >>> 16) & 0xff; physChunk[10] = (pxPerM >>> 8) & 0xff; physChunk[11] = pxPerM & 0xff; // 写入Y方向像素每米和X一致 physChunk.set(physChunk.slice(8, 12), 12); // 写入单位标识:1表示单位为米 physChunk[16] = 1; // 计算CRC校验和 let crc = 0; for (let i = 4; i < 17; i++) { crc ^= physChunk[i]; for (let j = 0; j < 8; j++) { crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1; } } // 写入CRC physChunk[17] = (crc >>> 24) & 0xff; physChunk[18] = (crc >>> 16) & 0xff; physChunk[19] = (crc >>> 8) & 0xff; physChunk[20] = crc & 0xff; // 把pHYs chunk插入到IHDR chunk之后,IDAT chunk之前 const newArr = new Uint8Array(arr.length + physChunk.length); // 先写8字节PNG头 + 25字节IHDR chunk newArr.set(arr.slice(0, 33), 0); // 插入pHYs chunk newArr.set(physChunk, 33); // 写入剩余的原有内容 newArr.set(arr.slice(33), 33 + physChunk.length); resolve(new Blob([newArr], { type: 'image/png' })); }; reader.readAsArrayBuffer(blob); }); } function generate() { const w = width.value * dpi, h = height.value * dpi, text = width.value + "x" + height.value; canvas.width = w + offset * 2; canvas.height = h + offset * 2; ctx.strokeStyle = "black"; ctx.fillStyle = "white"; ctx.lineWidth = 1; ctx.fillRect(0, 0, canvas.width, canvas.height); //fill background ctx.rect(offset, offset, w, h); // diagonal lines ctx.moveTo(offset, offset); ctx.lineTo(w, h); ctx.moveTo(w + offset, offset); ctx.lineTo(offset, h); // vertical line ctx.moveTo(w / 2 + offset, offset + offset); ctx.lineTo(w / 2, h); // horizontal line ctx.moveTo(offset, h / 2 + offset); ctx.lineTo(w, h / 2); // circle ctx.moveTo(w / 2 + Math.min(w, h) / 2 + offset, h / 2 + offset); ctx.arc(w / 2 + offset, h / 2 + offset, Math.min(w, h) / 2, 0, Math.PI * 2); // text ctx.font = Math.min(w, h) / (text.length / 1.5) + "px monospace"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.strokeText(text, w / 2 + offset, h / 2 + offset); ctx.stroke(); //generate download link canvas.toBlob(async data => { // 插入pHYs chunk const pngWithPhys = await insertPngPhysChunk(data, dpi); link.setAttribute("download", `${text}_(${dpi}dpi).png`); link.href = URL.createObjectURL(pngWithPhys); }); } generate();
CSS代码(无修改)
input { width: 3em; } div { padding: 1em; background-color: pink; display: inline-block; } div:first-of-type { padding-bottom: 0; }
HTML代码(无修改)
<div> Size (in inches): <input id="width" type="number" oninput="generate()" value="2"> x <input id="height" type="number" oninput="generate()" value="1"> </div> <a id="download">download PNG</a> <br> <div> <canvas id="preview"></canvas> </div>
注意事项
- 打印时请确认打印软件的「缩放」选项设置为「实际大小」/「100%」,部分软件默认会开启「适应纸张」缩放,会覆盖pHYs chunk的尺寸设置
- 极个别老旧打印软件不支持读取pHYs chunk,这种场景才需要手动设置打印尺寸,主流场景下均可以自动识别
内容的提问来源于stack exchange,提问作者vanowm
相关产品推荐
相关产品推荐

