Откройте файл ICO в шестнадцатеричном редакторе. Первые шесть байтов:
00 00 01 00 03 00
Это весь заголовок файла. 00 00 — поле Reserved, всегда ноль. 01 00 — поле Type: 1 означает, что это файл иконки (файлы курсоров используют 2). 03 00 — поле Count, little-endian для 3, то есть этот ICO содержит три отдельных изображения. Шесть байтов, описывающих контейнер, в котором лежат три независимых файла изображений. Остаток файла — метаданные об этих изображениях, а за ними сами данные изображений.
ICO не сжимает данные и не определяет цветовое пространство — это контейнер, причём самый простой и минималистичный формат подобного рода, который до сих пор используется ежедневно на каждой машине с Windows и в каждом крупном браузере.
Откуда взялся ICO
Microsoft представила ICO вместе с Windows 1.0 в 1985 году. В экосистеме ПК в 1985 году не существовало понятия стандартизированного файла изображения. Не было JPEG, не было PNG, не было GIF. Данные битмапа представляли собой сырые массивы пикселей, и каждая программа обрабатывала хранение самостоятельно. Microsoft нужен был способ поставлять иконки для программ, папок и системных элементов интерфейса. Требования были простыми:
- Один файл на иконку, независимо от того, сколько размеров требовалось ОС
- Быстрый поиск во время выполнения — ОС не должна была декодировать изображение, чтобы узнать его размеры
- Маленький объём памяти на системах с 256 КБ ОЗУ
Решением стала структура каталога. Файл начинается с заголовка, который говорит, сколько изображений внутри. Затем следует массив записей, каждая из которых описывает ширину, высоту, глубину цвета и смещение одного изображения внутри файла. ОС читает каталог, выбирает запись, соответствующую текущему контексту отображения, переходит к заданному смещению и рендерит битмап.
Эта конструкция на два года старше ZIP, на два года старше GIF и на семь лет старше JPEG. ICO был миниатюрной файловой системой до того, как файловые системы стали интересными.
Зачем вообще существует ICO
Иконка — это не одно изображение, а набор копий разного разрешения. Windows отображает одну и ту же иконку в размере 16 x 16 в списке файлов, 32 x 32 на рабочем столе, 48 x 48 в панели подробностей Проводника и 256 x 256 в режиме «extra large». Дисплей с высоким DPI при масштабе 200 % требует иконку 32 x 32, отрендеренную из источника 64 x 64. ОС выбирает подходящий размер в зависимости от контекста, и этот выбор должен происходить мгновенно.
Если бы иконки хранились в виде отдельных файлов PNG, ОС пришлось бы открывать несколько файлов, декодировать каждый и кешировать результаты. С ICO ОС открывает один файл, читает 16-байтовую запись каталога и сразу переходит к данным битмапа. Каталог делает формат самоописываемым. Для метаданных декодер не нужен.
С фавиконами ситуация аналогичная. Когда браузер запрашивает /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 лучше сжатие, настоящая альфа-прозрачность и широкая инфраструктура инструментов. WebP ещё компактнее. JPEG справляется с фотографиями. Тем не менее ICO живёт. Три причины:
1. Многорезолюционность в одном файле. PNG хранит одно изображение. ICO хранит произвольное количество. Сайт мог бы отдавать ZIP с PNG, но ни один браузер не знал бы, как выбрать правильный для фавикона. Структура каталога ICO решает это за шесть байтов.
2. Привязка к системному API. LoadIcon, ExtractIcon и SHGetFileInfo в Windows ожидают данные ICO. У Win32 API нет аналога для контейнеров иконок PNG. Изменение этого сломало бы каждое приложение Windows, скомпилированное с 1985 года. Microsoft никогда не идёт на нарушение обратной совместимости.
3. Стандарт фавикона. HTML-тег <link rel="icon"> принимает PNG, SVG и ICO, но неявный запрос по умолчанию на /favicon.ico появился раньше этих вариантов. Каждый браузер, начиная с Internet Explorer 5 (1999), запрашивает /favicon.ico по умолчанию. Сайты, которые явно не объявляют ссылку на фавикон, всё равно нуждаются в ICO по этому пути, иначе браузер получит 404.
PNG не заменил ICO, потому что ICO никогда не конкурировал по качеству изображения. Он конкурировал по семантике контейнера, и ни один другой формат не предлагает ту же модель «каталог в файле» при тридцати годах поддержки операционных систем.
Что внутри контейнера
Каждая запись в файле ICO указывает на независимое изображение. Данные изображения могут быть в одном из двух форматов:
Кодирование BMP (устаревшее)
До Windows Vista все изображения ICO были несжатыми или сжатыми по RLE битмапами BMP. Хранимые данные представляют собой DIB (Device Independent Bitmap) — BMP без BITMAPFILEHEADER. Он начинается напрямую с 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
Поле Height в заголовке DIB вдвое превышает реальную высоту иконки. Иконка 32 x 32 хранит 64 в поле Height. Верхняя половина — XOR-маска (собственно цветное изображение), нижняя половина — AND-маска (однобитовый битмап прозрачности). Для 32-битных иконок с альфа-каналом AND-маска обычно игнорируется.
Для иконок с кодированием BMP в 24 бита и ниже без альфа-канала AND-маска — единственный механизм прозрачности. 1 в AND-маске означает прозрачность, 0 — непрозрачность. Именно так Windows обеспечивала прозрачность до того, как 32-битный цвет стал стандартом.
Кодирование PNG (Windows Vista+)
Начиная с Windows Vista, файлы ICO могут хранить изображения в кодировании PNG. Данные изображения по смещению, указанному в ICONDIRENTRY, представляют собой сырой файл PNG — со своей сигнатурой 89 50 4E 47 и полной структурой PNG-чанков.
Это тот формат, который нужен для иконок 256 x 256. 32-битный BMP 256 x 256 без сжатия занял бы примерно 262 КБ. То же изображение в PNG обычно занимает 20–60 КБ. Современные инструменты для иконок генерируют записи в кодировании PNG для 256 x 256 и в кодировании BMP для меньших размеров, обеспечивая оптимальный баланс совместимости и размера файла.
Сравнение форматов
| Кодирование | Год появления | Сжатие | Альфа-канал | Оптимально для |
|---|---|---|---|---|
| BMP | Windows 1.0 (1985) | None or RLE | 1-bit AND mask | 16 x 16 to 48 x 48 |
| PNG | Windows Vista (2006) | DEFLATE | 8-bit alpha | 64 x 64 to 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.
Фавиконы
Для современных сайтов отдавайте оба формата:
<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 | Режим «extra large» в Проводнике Windows |
Фавикон ICO с этими пятью записями обычно занимает 30–50 КБ. Без PNG 256 x 256 размер падает ниже 10 КБ.
Иконки приложений 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, который случайно работает и в вебе.
Будущее ICO
Большинство прогнозов о скорой смерти ICO так и не сбудутся. Причина та же, что и при его появлении: обратная совместимость.
SVG-фавиконы технически превосходят ICO. Они масштабируются до любого разрешения, сжимаются лучше любого растрового формата и поддерживают анимацию и интерактивность. Chrome, Firefox и Safari все поддерживают SVG-фавиконы. Тем не менее ICO сохраняется, потому что миллионы устройств, корпоративных систем и устаревших браузеров по-прежнему запрашивают /favicon.ico по умолчанию. Сайт без файла ICO генерирует 404 в логах сервера и показывает сломанную иконку в старом ПО.
Windows 11 по-прежнему использует ICO для иконок приложений, папок и системного интерфейса. Win32 API по-прежнему ожидает данные ICO. Microsoft не проявляет интереса к замене этого формата — нет бизнес-причин нарушать тридцать лет совместимости приложений.
По-настоящему изменяется не сам формат, а инструменты для его создания. Современные тулчейны — IconKitchen, плагины Figma, онлайн-генераторы — автоматически выдают файлы ICO с записями высокого разрешения в кодировании PNG. Контейнер остаётся прежним; полезная нагрузка становится лучше.
ICO нельзя назвать любимым форматом разработчиков. Это формат, который работает везде, ничего не ломает и ничего не стоит в поддержке. Именно поэтому он всё ещё будет с нами в 2035 году.
Если вам нужно создавать файлы ICO из существующих изображений, JPG в ICO конвертирует исходники JPG, PNG и WebP в многорезолюционные файлы ICO прямо в браузере — без загрузок, без серверной обработки. Он генерирует стандартную матрицу размеров и автоматически выбирает кодирование PNG для 256 x 256 и BMP для меньших размеров, обеспечивая оптимальный баланс совместимости и размера файла.



