ICO 和 ICNS 是容器格式,不是改了扩展名的 PNG。它们会存储多个显示尺寸,使用不同字节序与目录结构,并对透明度、行顺序和尺寸元数据有明确要求。
浏览器可以解码源图,把统一裁剪与缩放变换应用到所有目标尺寸,编码 PNG 预览,为较小 ICO 层写入 Windows DIB,再构造 macOS ICNS Chunk,同时导出组合文件与独立尺寸包。
所有目标尺寸使用同一个标准化变换
通过 Object URL 加载图片,先校验文件字节和解码后像素数,只在编辑期间保留图片元素。标准化 transform 包含 scale、x/y offset 与圆角半径,预览和每个输出分辨率都使用完全相同的构图。
对每个配置尺寸创建独立正方形 Canvas,需要时裁剪圆角,开启高质量平滑,先适配源图,再叠加用户缩放与偏移,最后同时读取 ImageData 与 PNG 字节。
| 格式 | 常见尺寸 | 容器内容 |
|---|---|---|
| ICO | 16 到 256 | 小尺寸使用 DIB,256 使用 PNG |
| ICNS | 16 到 1024 | 带类型的 PNG Chunk |
| PNG ZIP | 16 到 1024 | 独立 PNG 文件 |
正确写入 Windows DIB 像素
小尺寸 ICO 可以使用 40 字节 BITMAPINFOHEADER,后面跟 32 位像素和 AND mask。存储高度需要乘以二,因为同时包括 XOR 颜色位图与 mask。像素通道是 BGRA 而不是 Canvas RGBA,行顺序则是从下到上。
每一行 AND mask 都要补齐到 32 位边界。使用完整 alpha 时 mask 可以全零,但它的字节和 stride 仍然属于 DIB。256px 条目通常直接保存 PNG,可以避开 ICO 单字节宽高上限。
- DIB 数字使用小端序。
- 把 Canvas 从上到下的行反转成位图从下到上。
- 交换红蓝通道,生成 BGRA。
- ICO 目录中 256 的宽高字段写成零。
构造 ICO 目录与数据偏移
ICO 从六字节 header 开始,后面每张图片占一个 16 字节目录项。目录项描述宽高、颜色信息、payload 长度和绝对 offset,全部 payload 则依次拼在目录之后。
offset 要从完整目录总长度开始累计。任何一个字节长度写错,都会让后面全部图片错位,即使单独的 PNG 或 DIB 数据本身正确,整个图标也可能无法读取。
const directorySize = 6 + entries.length * 16;
const header = new Uint8Array(directorySize);
const view = new DataView(header.buffer);
view.setUint16(0, 0, true); // reserved
view.setUint16(2, 1, true); // ICO image type
view.setUint16(4, entries.length, true); // image count
entries.forEach((entry, index) => {
const offset = 6 + index * 16;
header[offset] = entry.size >= 256 ? 0 : entry.size;
header[offset + 1] = entry.size >= 256 ? 0 : entry.size;
view.setUint16(offset + 6, 32, true);
view.setUint32(offset + 8, entry.bytes.byteLength, true);
view.setUint32(offset + 12, entry.dataOffset, true);
});使用大端序 Typed Chunk 构造 ICNS
ICNS 以 ASCII 字符 icns 和容器总长度开头。每个图像表示是一个 Chunk,包含四字符类型、Chunk 长度与 payload。现代尺寸可以在对应类型下直接存储 PNG 数据。
与 ICO 相反,ICNS 长度是大端序。维护尺寸到 Chunk Type 的映射,明确忽略不支持的尺寸,写 header 前先计算总字节,并按稳定顺序拼接 Chunk。
function createIcnsChunk(type: string, png: Uint8Array) {
const chunk = new Uint8Array(8 + png.byteLength);
const view = new DataView(chunk.buffer);
writeAscii(chunk, 0, type);
view.setUint32(4, chunk.byteLength, false); // ICNS uses big-endian lengths
chunk.set(png, 8);
return chunk;
}
const chunks = entries.map(entry =>
createIcnsChunk(chunkTypeForSize(entry.size), entry.pngBytes)
);
const header = createIcnsHeader(8 + sumByteLengths(chunks));
return new Blob([header, ...chunks], { type: 'image/icns' });同时提供组合文件和可检查归档
主 ICO 或 ICNS 应包含全部支持尺寸。单尺寸 ICO、ICNS 或 PNG ZIP 适合调试,也能满足部分平台要求独立资产的场景。两种产物应来自同一份渲染 entry,避免视觉漂移。
源图、组合输出和 ZIP 输出需要分别追踪 Object URL 生命周期。用户更换源图、格式或变换后 revoke 旧 URL,组件卸载时全部释放。
必须在真实消费者中测试
预览 Canvas 无法验证容器。ICO 应在 Windows Explorer、快捷方式、浏览器 favicon 和图标检查器中测试;ICNS 则要用 macOS Finder 或图标工具检查。重点关注 16px 可读性、透明、圆角和高分辨率层。
不要把小源图放大后称为多分辨率质量。大尺寸输出仍然只有有限源细节,而小尺寸图标往往需要比自动缩放更简单的形状和更强对比度。
总结
浏览器图标生成同时涉及视觉渲染与二进制格式工程。标准化一份变换,渲染全部目标尺寸,遵守 ICO DIB 行与通道规则,为 ICNS 切换字节序,准确计算 offset,并在真实操作系统消费者中验证。