Análises Profundas

ICO Decodificado: Estrutura, Headers e Por Que Sobrevive

koboshiCo-founder
·17 min de leitura
ICO Decodificado: Estrutura, Headers e Por Que Sobrevive
Resumo

ICO é um container, não um codec de imagem. Inventado para o Windows 1.0 em 1985, armazena múltiplas resoluções do mesmo ícone em um único arquivo para que o SO possa escolher o tamanho certo em tempo de execução. Aqui está a análise técnica completa — do header de seis bytes ao motivo pelo qual os favicons SVG ainda não conseguiram eliminá-lo.

Abra um arquivo ICO em um editor hexadecimal. Os primeiros seis bytes:

00 00 01 00 03 00

Esse é o header completo do arquivo. 00 00 é o campo Reserved — sempre zero. 01 00 é o campo Type — 1 significa que este é um arquivo de ícone (arquivos de cursor usam 2). 03 00 é o campo Count — little-endian para 3, o que significa que este ICO contém três imagens separadas. Seis bytes para descrever um container que armazena três arquivos de imagem independentes. O restante do arquivo é metadados sobre essas imagens, seguido pelos dados da imagem em si.

ICO não é nem algoritmo de compressão, nem espaço de cor: é um container — o formato de container mais simples e primitivo ainda em uso diário em todas as máquinas Windows e em todos os principais navegadores.

De Onde Veio o ICO

A Microsoft introduziu o ICO com o Windows 1.0 em 1985. O ecossistema de PCs em 1985 não tinha conceito de arquivo de imagem padronizado. Não existia JPEG, PNG ou GIF. Os dados de bitmap eram arrays brutos de pixels, e cada programa lidava com seu próprio armazenamento. A Microsoft precisava de uma forma de entregar ícones para programas, pastas e elementos da interface do sistema. Os requisitos eram simples:

  • Um arquivo por ícone, independentemente de quantos tamanhos o SO precisasse
  • Busca rápida em tempo de execução — o SO não deveria precisar decodificar uma imagem para saber suas dimensões
  • Pequena pegada de memória em sistemas com 256 KB de RAM

A solução foi uma estrutura de diretório. O arquivo começa com um header que diz quantas imagens estão dentro. Depois vem um array de entradas, cada uma descrevendo a largura, altura, profundidade de bits e offset de uma imagem dentro do arquivo. O SO lê o diretório, escolhe a entrada que corresponde ao contexto de exibição atual, busca aquele offset e renderiza o bitmap.

Este design é anterior ao ZIP em dois anos, anterior ao GIF em dois anos e anterior ao JPEG em sete anos. O ICO era um sistema de arquivos em miniatura antes que sistemas de arquivos se tornassem interessantes.

Por que o ICO Existe

Um ícone não é uma única imagem, mas sim um conjunto de imagens em diferentes tamanhos. O Windows renderiza o mesmo ícone em 16 x 16 na lista de arquivos, em 32 x 32 na área de trabalho, em 48 x 48 no painel de detalhes do Explorer e em 256 x 256 na visualização "extra large". Uma tela de alta DPI em escala de 200% precisa de um ícone de 32 x 32 renderizado a partir de uma fonte de 64 x 64. O SO decide qual tamanho usar com base no contexto, e essa escolha tem de ser feita num instante.

Se os ícones fossem armazenados como arquivos PNG individuais, o SO precisaria abrir vários arquivos, decodificar cada um e armazenar os resultados em cache. Com o ICO, o SO abre um arquivo, lê uma entrada de diretório de 16 bytes e pula diretamente para os dados do bitmap. O diretório torna o formato autodescritivo. Nenhum decoder é necessário para metadados.

O mesmo vale para os favicons. Quando um navegador solicita /favicon.ico, recebe um arquivo que contém todos os tamanhos de que pode precisar — 16 x 16 para a aba, 32 x 32 para a barra de favoritos, 180 x 180 para atalhos da tela inicial do iOS. O navegador escolhe a entrada certa sem analisar headers de imagem.

O Header do Arquivo

O header do ICO tem duas partes: o ICONDIR e o array ICONDIRENTRY.

ICONDIR (6 bytes)

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)

Todo arquivo ICO começa com 00 00 01 00. Os dois bytes seguintes dizem quantas imagens vêm depois.

ICONDIRENTRY (16 bytes cada)

Cada imagem recebe uma entrada:

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)

Os campos Width e Height têm um byte cada. O valor máximo explicitamente armazenável é 255. Quando uma imagem tem 256 x 256 pixels, o campo armazena 0x00, que os decoders interpretam como 256. Essa peculiaridade faz parte da especificação desde 1985.

Veja como fica o diretório de um ICO com três imagens — um BMP de 16 x 16, um BMP de 32 x 32 e um PNG de 256 x 256:

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

O SO lê o diretório, encontra a entrada que corresponde às suas necessidades e busca o ImageOffset — acesso direto à memória, sem parsing ou suposições.

Por que PNG, WebP e JPEG Nunca Substituíram o ICO

Por todas as métricas modernas, PNG é um formato melhor que a codificação interna de bitmap do ICO. PNG tem melhor compressão, transparência alpha verdadeira e ferramentas amplamente disponíveis. WebP é ainda menor. JPEG lida com fotografias. Mesmo assim, o ICO persiste. Três razões:

1. Arquivo único com múltiplas resoluções. PNG armazena uma imagem. ICO armazena um número arbitrário. Um site poderia servir um ZIP de PNGs, mas nenhum navegador saberia como escolher o certo para um favicon. A estrutura de diretório do ICO resolve isso em seis bytes.

2. Vínculo com API do sistema. As funções LoadIcon, ExtractIcon e SHGetFileInfo do Windows esperam dados ICO. A API Win32 não tem equivalente para containers de ícones PNG. Mudar isso quebraria todos os aplicativos Windows compilados desde 1985. A Microsoft nunca abre mão da compatibilidade retroativa.

3. O padrão favicon. A tag HTML <link rel="icon"> aceita PNG, SVG e ICO, mas a solicitação implícita padrão por /favicon.ico é anterior a essas opções. Todo navegador desde o Internet Explorer 5 (1999) solicita /favicon.ico por padrão. Sites que não declaram explicitamente um link de favicon ainda precisam de um ICO nesse caminho, ou o navegador recebe um 404.

PNG não substituiu o ICO porque o ICO nunca competiu em qualidade de imagem. Ele competia em semântica de container, e nenhum outro formato oferece o mesmo modelo de diretório-em-arquivo com trinta anos de suporte do sistema operacional.

O que Há Dentro do Container

Cada entrada em um arquivo ICO aponta para uma imagem independente. Os dados da imagem podem estar em um de dois formatos:

Codificação BMP (Legado)

Antes do Windows Vista, todas as imagens ICO eram bitmaps BMP sem compressão ou comprimidos com RLE. Os dados armazenados são um DIB (Device Independent Bitmap) — um BMP sem o BITMAPFILEHEADER. Começa diretamente com o 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

O campo Height no header DIB é o dobro da altura real do ícone. Um ícone de 32 x 32 armazena 64 no campo Height. A metade superior é a máscara XOR (a imagem colorida real), e a metade inferior é a máscara AND (um bitmap de transparência de 1 bit). Para ícones de 32 bits com canal alpha, a máscara AND é tipicamente ignorada.

Para ícones codificados em BMP de 24 bits ou menos sem alpha, a máscara AND é o único mecanismo de transparência. Um 1 na máscara AND significa transparente; um 0 significa opaco. Foi assim que o Windows conseguiu transparência antes que a cor de 32 bits se tornasse padrão.

Codificação PNG (Windows Vista+)

A partir do Windows Vista, arquivos ICO podem armazenar imagens codificadas em PNG. Os dados da imagem no offset especificado em ICONDIRENTRY são um arquivo PNG raw — completo com sua própria assinatura 89 50 4E 47 e estrutura completa de chunks PNG.

Este é o formato que você quer para ícones de 256 x 256. Um BMP de 256 x 256 em 32 bits teria aproximadamente 262 KB sem compressão. A mesma imagem como PNG costuma ter de 20 a 60 KB. Ferramentas modernas de ícones geram entradas codificadas em PNG para 256 x 256 e entradas codificadas em BMP para tamanhos menores, oferecendo o melhor equilíbrio entre compatibilidade e tamanho de arquivo.

Comparação de Codificações

CodificaçãoIntroduzidoCompressãoSuporte AlphaIdeal Para
BMPWindows 1.0 (1985)Nenhuma ou RLEMáscara AND de 1 bit16 x 16 a 48 x 48
PNGWindows Vista (2006)DEFLATEAlpha de 8 bits64 x 64 a 256 x 256

Detectando e Inspecionando Arquivos ICO

Não confie na extensão .ico. Leia os primeiros seis bytes e analise o diretório.

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,
    ];
}

CLI do ImageMagick

magick identify -verbose favicon.ico | grep -E "(Print size|Resolution|Colorspace|Type)"

Lista todas as imagens internas:

magick identify favicon.ico

Extrai um tamanho específico:

magick favicon.ico[2] extracted-256x256.png

Ou simplesmente:

file favicon.ico
# favicon.ico: MS Windows icon resource - 5 icons, 16x16, 32 bits/pixel, 32x32, 32 bits/pixel

Boas Práticas e Casos de Uso

ICO não é um formato de imagem de uso geral. É um artefato de implantação para contextos específicos. Use-o onde o contexto exigir, e use PNG em todos os outros lugares.

Favicons

Para sites modernos, sirva ambos os formatos:

<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />

Os navegadores escolhem o primeiro formato que suportam. Navegadores com suporte a SVG recebem o ícone vetorial. Navegadores legados e utilitários de favoritos recorrem ao ICO. O arquivo ICO deve conter:

TamanhoCodificaçãoFinalidade
16 x 16BMPAba do navegador, IE legado
32 x 32BMPBarra de tarefas, barra de favoritos
48 x 48BMPAtalhos do Windows
180 x 180PNGÍcone da tela inicial do iOS
256 x 256PNGVisualização extra-large do Windows Explorer

Um favicon ICO com essas cinco entradas costuma ter de 30 a 50 KB. Sem o PNG de 256 x 256, cai para menos de 10 KB.

Ícones de Aplicativos Windows

Os executáveis de aplicativos Windows incorporam um recurso ICO. O SO carrega o tamanho apropriado do diretório incorporado em tempo de execução. Para aplicativos de desktop destinados ao Windows 10 e 11, inclua:

  • 16 x 16, 24 x 24, 32 x 32, 48 x 48 (BMP, para legado e DPI padrão)
  • 64 x 64, 128 x 128, 256 x 256 (PNG, para telas de alta DPI)

O Windows escala automaticamente para baixo se um tamanho exato estiver ausente, mas o scaling fica pior do que renderizar um tamanho nativo menor. Cada tamanho que você omite custa qualidade visual.

Quando Não Usar ICO

  • Imagens de conteúdo web: Use WebP, JPEG ou PNG. ICO não tem vantagem de compressão nem benefício de renderização nativa do navegador para imagens inline.
  • Fotografias: ICO não foi projetado para imagens contínuas de alta cor. Os tamanhos de arquivo explodem.
  • Recursos multiplataforma: macOS usa .icns, não ICO. Linux usa PNG ou SVG. ICO é um formato nativo do Windows que por acaso funciona na web.

O Futuro do ICO

O ICO resistirá à maioria dos prognósticos que anunciam sua extinção. O motivo é o mesmo de sua origem: compatibilidade retroativa.

Favicons SVG são tecnicamente superiores. Eles escalam para qualquer resolução, comprimem melhor do que qualquer formato raster e suportam animação e interatividade. Chrome, Firefox e Safari suportam favicons SVG. Mesmo assim, o ICO persiste porque milhões de dispositivos, sistemas corporativos e navegadores legados ainda solicitam /favicon.ico por padrão. Um site sem um arquivo ICO gera 404s nos logs do servidor e mostra um ícone quebrado em softwares mais antigos.

O Windows 11 ainda usa ICO para ícones de aplicativos, ícones de pastas e interface do sistema. A API Win32 ainda espera dados ICO. A Microsoft não demonstrou interesse em substituir este formato — não há razão de negócio para quebrar trinta anos de compatibilidade de aplicativos.

A verdadeira mudança está na forma como os arquivos ICO são produzidos. Toolchains modernas — IconKitchen, plugins do Figma, geradores online — geram arquivos ICO com entradas de alta resolução codificadas em PNG automaticamente. O container permanece o mesmo; o payload fica melhor.

O ICO não é um formato que desperta paixão. É um formato que funciona em toda parte, não quebra nada e não custa nada para manter. É exatamente por isso que ele ainda estará por aí em 2035.

Se você precisar criar arquivos ICO a partir de imagens existentes, JPG to ICO converte fontes JPG, PNG e WebP em arquivos ICO multi-resolução diretamente no seu navegador — sem uploads, sem processamento no servidor. Ele gera a matriz de tamanhos padrão e escolhe automaticamente a codificação PNG para 256 x 256 e a codificação BMP para tamanhos menores, oferecendo o equilíbrio ideal entre compatibilidade e tamanho de arquivo.

Mais posts do blog