用十六进制编辑器打开一个 ICO 文件。前六个字节:
00 00 01 00 03 00
这就是整个文件头。00 00 是 Reserved 字段——永远为零。01 00 是 Type 字段——1 表示这是一个图标文件(光标文件用 2)。03 00 是 Count 字段——小端序表示 3,意味着这个 ICO 内包含三幅独立图像。六个字节,描述了一个容纳三份独立图像文件的容器。文件的其余部分是关于这些图像的元数据,以及图像数据本身。
ICO 既不是压缩算法,也不是色彩空间,而是一个容器——它是当今每一台 Windows 电脑和每一个主流浏览器上仍在日常使用的最小、最原始的容器格式。
ICO 的起源
微软在 1985 年的 Windows 1.0 中引入了 ICO。当时的 PC 生态根本没有标准化图像文件的概念。没有 JPEG,没有 PNG,没有 GIF。位图数据就是原始像素数组,每个程序各自管理存储。微软需要一种为程序、文件夹和系统 UI 元素分发图标的方式。需求很简单:
- 一个图标一个文件,无论操作系统需要多少种尺寸
- 运行时快速查找——操作系统不应为了获知图像尺寸而解码整张图
- 在仅有 256 KB RAM 的系统上保持极小的内存占用
解决方案是目录结构。文件以头部开头,说明内部有多少幅图像。随后是一组条目数组,每个条目描述一幅图像的宽度、高度、位深度以及在文件中的偏移量。操作系统读取目录,找到匹配当前显示上下文的条目,跳转到对应偏移,直接渲染位图。
这一设计比 ZIP 早两年,比 GIF 早两年,比 JPEG 早七年。ICO 是一种微型文件系统,远在文件系统本身变得复杂之前就已出现。
ICO 存在的根本原因
图标从来不是单张图像,而是一套不同尺寸的图像组合。Windows 在文件列表中将同一图标渲染为 16 x 16,在桌面渲染为 32 x 32,在资源管理器详细信息窗格渲染为 48 x 48,在"超大图标"视图中渲染为 256 x 256。200% 缩放的高 DPI 显示器需要从 64 x 64 源渲染出 32 x 32 图标。操作系统必须根据上下文在瞬间做出判断,选出最合适的尺寸。
如果图标以独立 PNG 文件存储,操作系统需要打开多个文件、分别解码、缓存结果。而使用 ICO,操作系统打开一个文件,读取一个 16 字节的目录条目,直接跳转到位图数据。目录让格式具备自描述能力。元数据无需解码器。
网站图标(favicon)的情况也一样。当浏览器请求 /favicon.ico 时,它获得的是一个包含所有可能尺寸的文件——16 x 16 用于标签页,32 x 32 用于书签栏,180 x 180 用于 iOS 主屏幕快捷方式。浏览器无需解析图像头部,就能选出正确条目。
文件头结构
ICO 文件头分为两部分:ICONDIR 和 ICONDIRENTRY 数组。
ICONDIR(6 字节)
Bytes 0-1: Reserved (must be 0)
Bytes 2-3: Type (1 = icon, 2 = cursor)
Bytes 4-5: Count (number of images, little-endian uint16)
每个 ICO 文件都以 00 00 01 00 开头。接下来两个字节告诉你后面有多少幅图像。
ICONDIRENTRY(每个 16 字节)
每幅图像对应一个条目:
Bytes 0: Width (in pixels; 0 means 256)
Bytes 1: Height (in pixels; 0 means 256)
Bytes 2: ColorCount (0 if more than 256 colors)
Bytes 3: Reserved (always 0)
Bytes 4-5: Planes (1 for icons)
Bytes 6-7: BitCount (bits per pixel: 1, 4, 8, 24, or 32)
Bytes 8-11: BytesInRes (size of image data in bytes, uint32)
Bytes 12-15: ImageOffset (byte offset to image data from file start, uint32)
Width 和 Height 字段各占一个字节。显式可存储的最大值是 255。当图像为 256 x 256 像素时,字段存储 0x00,解码器将其解释为 256。这一怪异设计自 1985 年起就是规范的一部分。
以下是一个包含三幅图像的 ICO 目录示例——16 x 16 BMP、32 x 32 BMP 和 256 x 256 PNG:
Offset 0x00: 00 00 01 00 03 00 -- ICONDIR: 3 images
Offset 0x06: 10 10 00 00 01 00 18 00 2C 01 00 00 16 00 00 00 -- 16x16, 24bpp, BMP, 300 bytes at 0x16
Offset 0x16: 20 20 00 00 01 00 18 00 A8 02 00 00 42 01 00 00 -- 32x32, 24bpp, BMP, 680 bytes at 0x142
Offset 0x26: 00 00 00 00 01 00 20 00 B4 1C 00 00 EA 03 00 00 -- 256x256, 32bpp, PNG, 7348 bytes at 0x3EA
操作系统读取目录,找到满足需求的条目,然后跳转到 ImageOffset。不需要解析,不需要猜测,直接进行内存访问。
为什么 PNG、WebP 和 JPEG 从未取代 ICO
按任何现代指标衡量,PNG 都比 ICO 内部的位图编码更优秀。PNG 压缩率更高,支持真正的 Alpha 透明,工具链成熟。WebP 体积更小。JPEG 适合照片。然而 ICO 依然存在。三个原因:
1. 单文件多分辨率。 PNG 存储单张图像。ICO 可存储任意数量。网站当然可以提供一个 PNG 压缩包,但没有浏览器知道如何从中为 favicon 挑选正确尺寸。ICO 的目录结构用六个字节解决了这个问题。
2. 系统 API 绑定。 Windows 的 LoadIcon、ExtractIcon 和 SHGetFileInfo 都期望 ICO 数据。Win32 API 没有对应 PNG 图标容器的等价物。改变这一点将破坏自 1985 年以来编译的每一个 Windows 应用程序,而微软从不会在向后兼容上让步。
3. Favicon 标准。 HTML 的 <link rel="icon"> 标签接受 PNG、SVG 和 ICO,但浏览器隐式默认请求 /favicon.ico 的做法早于这些选项。自 Internet Explorer 5(1999 年)以来,每个浏览器都会默认请求 /favicon.ico。未显式声明 favicon 链接的网站仍需要在该路径放置 ICO,否则浏览器会收到 404。
PNG 没有取代 ICO,因为 ICO 从来就不是在图像质量层面竞争。它竞争的是容器语义,而且没有其他格式提供同样的"文件中的目录"模型,外加三十年的操作系统支持。
容器内部是什么
ICO 文件中的每个条目指向一幅独立图像。图像数据可以是以下两种格式之一:
BMP 编码(遗留方案)
在 Windows Vista 之前,所有 ICO 图像都是未压缩或 RLE 压缩的 BMP 位图。存储的数据是 DIB(设备无关位图)——即没有 BITMAPFILEHEADER 的 BMP。它直接从 BITMAPINFOHEADER 开始:
Bytes 0-3: Header size (40 for BITMAPINFOHEADER)
Bytes 4-7: Width (uint32)
Bytes 8-11: Height (uint32, doubled for XOR + AND masks)
Bytes 12-13: Planes (1)
Bytes 14-15: BitCount (1, 4, 8, 24, or 32)
Bytes 16-19: Compression (0 for uncompressed, 1 or 2 for RLE)
Bytes 20-23: Image size (0 if uncompressed)
Bytes 24-27: X pixels per meter
Bytes 28-31: Y pixels per meter
Bytes 32-35: Colors used
Bytes 36-39: Important colors
DIB 头中的 Height 字段是实际图标高度的两倍。一个 32 x 32 的图标在 Height 字段中存储 64。上半部分是 XOR 掩码(实际彩色图像),下半部分是 AND 掩码(1 位透明位图)。对于带 Alpha 通道的 32 位图标,AND 掩码通常被忽略。
对于 24 位及更低、不含 Alpha 的 BMP 编码图标,AND 掩码是唯一的透明机制。AND 掩码中的 1 表示透明,0 表示不透明。这就是 Windows 在 32 位色彩成为标准之前实现透明的方式。
PNG 编码(Windows Vista 起)
从 Windows Vista 开始,ICO 文件可以存储 PNG 编码的图像。ICONDIRENTRY 中指定偏移量处的图像数据是一个完整的 PNG 文件——包含自身的 89 50 4E 47 签名和完整的 PNG 块结构。
这是 256 x 256 图标应该使用的格式。一个 256 x 256 的 32 位 BMP 未压缩时约 262 KB。同一张图作为 PNG 通常只有 20-60 KB。现代图标工具对 256 x 256 生成 PNG 编码条目,对小尺寸生成 BMP 编码条目,在兼容性和文件大小之间取得最佳平衡。
编码对比
| 编码 | 引入时间 | 压缩方式 | Alpha 通道支持 | 适用尺寸 |
|---|---|---|---|---|
| BMP | Windows 1.0 (1985) | 无或 RLE | 1 位 AND 掩码 | 16 x 16 至 48 x 48 |
| PNG | Windows Vista (2006) | DEFLATE | 8 位 Alpha | 64 x 64 至 256 x 256 |
检测与解析 ICO 文件
不要相信 .ico 扩展名。读取前六个字节并解析目录。
TypeScript
interface IcoEntry {
width: number
height: number
bitCount: number
bytesInRes: number
imageOffset: number
}
interface IcoInfo {
valid: boolean
type: "icon" | "cursor" | "unknown"
count: number
entries: IcoEntry[]
}
async function inspectIco(file: File): Promise<IcoInfo> {
const buffer = await file.slice(0, 1024).arrayBuffer()
const bytes = new Uint8Array(buffer)
if (bytes.length < 6)
return { valid: false, type: "unknown", count: 0, entries: [] }
const reserved = bytes[0] | (bytes[1] << 8)
const type = bytes[2] | (bytes[3] << 8)
const count = bytes[4] | (bytes[5] << 8)
if (reserved !== 0 || (type !== 1 && type !== 2)) {
return { valid: false, type: "unknown", count: 0, entries: [] }
}
const entries: IcoEntry[] = []
const dirSize = 6 + count * 16
if (bytes.length < dirSize) {
return { valid: false, type: "unknown", count: 0, entries: [] }
}
for (let i = 0; i < count; i++) {
const off = 6 + i * 16
entries.push({
width: bytes[off] === 0 ? 256 : bytes[off],
height: bytes[off + 1] === 0 ? 256 : bytes[off + 1],
bitCount: bytes[off + 6] | (bytes[off + 7] << 8),
bytesInRes:
bytes[off + 8] |
(bytes[off + 9] << 8) |
(bytes[off + 10] << 16) |
(bytes[off + 11] << 24),
imageOffset:
bytes[off + 12] |
(bytes[off + 13] << 8) |
(bytes[off + 14] << 16) |
(bytes[off + 15] << 24),
})
}
return {
valid: true,
type: type === 1 ? "icon" : "cursor",
count,
entries,
}
}
Python
import struct
from typing import TypedDict
class IcoEntry(TypedDict):
width: int
height: int
bit_count: int
bytes_in_res: int
image_offset: int
class IcoInfo(TypedDict):
valid: bool
type: str
count: int
entries: list[IcoEntry]
def inspect_ico(path: str) -> IcoInfo:
with open(path, "rb") as f:
header = f.read(1024)
if len(header) < 6:
return {"valid": False, "type": "unknown", "count": 0, "entries": []}
reserved, type_, count = struct.unpack("<HHH", header[:6])
if reserved != 0 or type_ not in (1, 2):
return {"valid": False, "type": "unknown", "count": 0, "entries": []}
dir_size = 6 + count * 16
if len(header) < dir_size:
return {"valid": False, "type": "unknown", "count": 0, "entries": []}
entries: list[IcoEntry] = []
for i in range(count):
off = 6 + i * 16
w, h, colors, _, planes, bit_count = struct.unpack("<BBBBHH", header[off:off+8])
bytes_in_res, image_offset = struct.unpack("<II", header[off+8:off+16])
entries.append({
"width": 256 if w == 0 else w,
"height": 256 if h == 0 else h,
"bit_count": bit_count,
"bytes_in_res": bytes_in_res,
"image_offset": image_offset,
})
return {
"valid": True,
"type": "icon" if type_ == 1 else "cursor",
"count": count,
"entries": entries,
}
Go
package main
import (
"encoding/binary"
"fmt"
"os"
)
type IcoEntry struct {
Width int
Height int
BitCount int
BytesInRes uint32
ImageOffset uint32
}
type IcoInfo struct {
Valid bool
Type string
Count int
Entries []IcoEntry
}
func inspectIco(path string) (IcoInfo, error) {
f, err := os.Open(path)
if err != nil {
return IcoInfo{}, err
}
defer f.Close()
buf := make([]byte, 1024)
n, _ := f.Read(buf)
buf = buf[:n]
if n < 6 {
return IcoInfo{Valid: false, Type: "unknown"}, nil
}
reserved := binary.LittleEndian.Uint16(buf[0:2])
typ := binary.LittleEndian.Uint16(buf[2:4])
count := binary.LittleEndian.Uint16(buf[4:6])
if reserved != 0 || (typ != 1 && typ != 2) {
return IcoInfo{Valid: false, Type: "unknown"}, nil
}
dirSize := 6 + int(count)*16
if n < dirSize {
return IcoInfo{Valid: false, Type: "unknown"}, nil
}
entries := make([]IcoEntry, 0, count)
for i := 0; i < int(count); i++ {
off := 6 + i*16
w := buf[off]
h := buf[off+1]
bitCount := binary.LittleEndian.Uint16(buf[off+6 : off+8])
bytesInRes := binary.LittleEndian.Uint32(buf[off+8 : off+12])
imageOffset := binary.LittleEndian.Uint32(buf[off+12 : off+16])
if w == 0 { w = 256 }
if h == 0 { h = 256 }
entries = append(entries, IcoEntry{
Width: int(w),
Height: int(h),
BitCount: int(bitCount),
BytesInRes: bytesInRes,
ImageOffset: imageOffset,
})
}
typStr := "icon"
if typ == 2 {
typStr = "cursor"
}
return IcoInfo{Valid: true, Type: typStr, Count: int(count), Entries: entries}, nil
}
PHP
function inspectIco(string $path): array {
$header = file_get_contents($path, false, null, 0, 1024);
if (strlen($header) < 6) {
return ["valid" => false, "type" => "unknown", "count" => 0, "entries" => []];
}
$reserved = unpack("v", substr($header, 0, 2))[1];
$type = unpack("v", substr($header, 2, 2))[1];
$count = unpack("v", substr($header, 4, 2))[1];
if ($reserved !== 0 || ($type !== 1 && $type !== 2)) {
return ["valid" => false, "type" => "unknown", "count" => 0, "entries" => []];
}
$dirSize = 6 + $count * 16;
if (strlen($header) < $dirSize) {
return ["valid" => false, "type" => "unknown", "count" => 0, "entries" => []];
}
$entries = [];
for ($i = 0; $i < $count; $i++) {
$off = 6 + $i * 16;
$w = ord($header[$off]);
$h = ord($header[$off + 1]);
$bitCount = unpack("v", substr($header, $off + 6, 2))[1];
$bytesInRes = unpack("V", substr($header, $off + 8, 4))[1];
$imageOffset = unpack("V", substr($header, $off + 12, 4))[1];
$entries[] = [
"width" => $w === 0 ? 256 : $w,
"height" => $h === 0 ? 256 : $h,
"bit_count" => $bitCount,
"bytes_in_res" => $bytesInRes,
"image_offset" => $imageOffset,
];
}
return [
"valid" => true,
"type" => $type === 1 ? "icon" : "cursor",
"count" => $count,
"entries" => $entries,
];
}
ImageMagick CLI
magick identify -verbose favicon.ico | grep -E "(Print size|Resolution|Colorspace|Type)"
列出所有内部图像:
magick identify favicon.ico
提取特定尺寸:
magick favicon.ico[2] extracted-256x256.png
或者简单地:
file favicon.ico
# favicon.ico: MS Windows icon resource - 5 icons, 16x16, 32 bits/pixel, 32x32, 32 bits/pixel
最佳实践与使用场景
ICO 不是通用图像格式。它是针对特定上下文的部署产物。在需要它的场景中使用,其余情况一律用 PNG。
Favicons
对于现代网站,同时提供两种格式:
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
浏览器会选取它们支持的第一种格式。支持 SVG 的浏览器获得矢量图标。旧版浏览器和书签工具回退到 ICO。ICO 文件应包含:
| 尺寸 | 编码方式 | 用途 |
|---|---|---|
| 16 x 16 | BMP | 浏览器标签页、旧版 IE |
| 32 x 32 | BMP | 任务栏、书签栏 |
| 48 x 48 | BMP | Windows 快捷方式 |
| 180 x 180 | PNG | iOS 主屏幕图标 |
| 256 x 256 | PNG | Windows 资源管理器超大图标视图 |
包含这五个条目的 favicon ICO 通常在 30-50 KB。如果不包含 256 x 256 的 PNG,可降至 10 KB 以下。
Windows 应用程序图标
Windows 应用程序的可执行文件会嵌入 ICO 资源。操作系统在运行时从嵌入目录中加载对应尺寸。对于面向 Windows 10 和 11 的桌面应用,应包含:
- 16 x 16、24 x 24、32 x 32、48 x 48(BMP,用于旧版和标准 DPI)
- 64 x 64、128 x 128、256 x 256(PNG,用于高 DPI 显示器)
Windows 会在缺少精确尺寸时自动缩小,但缩放的视觉效果比直接渲染原生小尺寸更差。你省略的每一个尺寸都在牺牲视觉质量。
何时不使用 ICO
- 网页内容图像:使用 WebP、JPEG 或 PNG。ICO 在压缩上没有优势,对内联图像也没有浏览器原生渲染收益。
- 照片:ICO 并非为高色深连续色调图像设计。文件体积会爆炸。
- 跨平台资源:macOS 使用
.icns,而非 ICO。Linux 使用 PNG 或 SVG。ICO 是 Windows 原生格式,只是恰好能在 Web 上工作。
ICO 的未来
ICO 的寿命会比大多数唱衰它的预言更长。原因与其起源相同:向后兼容。
SVG favicon 在技术层面更优越。它们可以缩放到任意分辨率,压缩率优于任何栅格格式,还支持动画和交互。Chrome、Firefox 和 Safari 都支持 SVG favicon。然而 ICO 依然存在,因为数百万台设备、企业系统和旧版浏览器仍在默认请求 /favicon.ico。没有 ICO 文件的网站会在服务器日志中产生 404,并在旧软件中显示破损图标。
Windows 11 仍将 ICO 用于应用程序图标、文件夹图标和系统 UI。Win32 API 仍期望 ICO 数据。微软对这一格式没有任何替换兴趣——没有业务理由去破坏三十年的应用程序兼容性。
真正发生变化的,是 ICO 文件的生成方式。现代工具链——IconKitchen、Figma 插件、在线生成器——会自动输出包含 PNG 编码高分辨率条目的 ICO 文件。容器本身没变,变的是里面的载荷。
ICO 谈不上让人喜欢,但它无处不在、不会破坏任何东西、支持成本几乎为零。这正是它到 2035 年仍将存在的原因。
如果你需要从现有图像创建 ICO 文件,JPG 转 ICO 可以直接在浏览器中将 JPG、PNG 和 WebP 源文件转换为多分辨率 ICO——无需上传,无需服务器处理。它会生成标准尺寸矩阵,并自动为 256 x 256 选择 PNG 编码、为小尺寸选择 BMP 编码,让你在兼容性和文件大小之间获得最佳平衡。



