Tiefenblick

ICO-Format entschlüsselt: Struktur, Header und sein Überleben

koboshiCo-founder
·15 min Lesezeit
ICO-Format entschlüsselt: Struktur, Header und sein Überleben
Zusammenfassung

ICO ist ein Container, kein Bild-Codec. Für Windows 1.0 im Jahr 1985 erfunden, speichert es mehrere Auflösungen desselben Icons in einer Datei, damit das Betriebssystem zur Laufzeit die passende Größe auswählen kann. Hier ist die vollständige technische Aufschlüsselung — vom sechs Byte langen Header bis zur Frage, warum SVG-Favicons es immer noch nicht verdrängt haben.

Öffne eine ICO-Datei in einem Hex-Editor. Die ersten sechs Bytes:

00 00 01 00 03 00

Das ist der gesamte Datei-Header. 00 00 ist das Reserved-Feld — immer Null. 01 00 ist das Type-Feld — 1 bedeutet, dass dies eine Icon-Datei ist (Cursor-Dateien verwenden 2). 03 00 ist das Count-Feld — Little-Endian für 3, was bedeutet, dass diese ICO drei separate Bilder enthält. Sechs Bytes, um einen Container zu beschreiben, der drei unabhängige Bilddateien aufnimmt. Der Rest der Datei besteht aus Metadaten über diese Bilder, gefolgt von den Bilddaten selbst.

ICO ist weder ein Komprimierungsalgorithmus noch ein Farbraum — es ist ein Container, und zwar der kleinste, primitivste Container-Format, das auf jedem Windows-Rechner und in jedem gängigen Browser noch täglich im Einsatz ist.

Woher ICO kommt

Microsoft führte ICO mit Windows 1.0 im Jahr 1985 ein. Das PC-Ökosystem von 1985 kannte kein standardisiertes Bilddateiformat. Es gab kein JPEG, kein PNG, kein GIF. Bitmap-Daten waren rohe Pixel-Arrays, und jedes Programm verwaltete seinen eigenen Speicher. Microsoft brauchte eine Möglichkeit, Icons für Programme, Ordner und System-UI-Elemente auszuliefern. Die Anforderungen waren einfach:

  • Eine Datei pro Icon, unabhängig davon, wie viele Größen das Betriebssystem benötigte
  • Schnelle Suche zur Laufzeit — das OS sollte kein Bild dekodieren müssen, um seine Abmessungen zu kennen
  • Geringer Speicherbedarf auf Systemen mit 256 KB RAM

Die Lösung war eine Verzeichnisstruktur. Die Datei beginnt mit einem Header, der angibt, wie viele Bilder enthalten sind. Dann folgt ein Array von Einträgen, die jeweils Breite, Höhe, Farbtiefe und Offset eines Bildes innerhalb der Datei beschreiben. Das OS liest das Verzeichnis, wählt den Eintrag, der zum aktuellen Anzeigekontext passt, springt zu diesem Offset und rendert die Bitmap.

Dieses Design ist zwei Jahre älter als ZIP, zwei Jahre älter als GIF und sieben Jahre älter als JPEG. ICO war ein Miniatur-Dateisystem, bevor Dateisysteme interessant wurden.

Warum ICO überhaupt existiert

Ein Icon ist kein einzelnes Bild. Es ist eine Sammlung derselben Grafik in verschiedenen Auflösungen. Windows rendert dasselbe Icon als 16 x 16 in einer Dateiliste, als 32 x 32 auf dem Desktop, als 48 x 48 im Explorer-Detailbereich und als 256 x 256 in der Ansicht "Extra große Symbole". Ein High-DPI-Display mit 200 % Skalierung benötigt ein 32 x 32 Icon, das aus einer 64 x 64 Quelle gerendert wird. Das OS entscheidet anhand des Kontexts, welche Größe es verwendet, und muss blitzschnell die passende Größe auswählen.

Wären Icons als einzelne PNG-Dateien gespeichert, müsste das OS mehrere Dateien öffnen, jede dekodieren und die Ergebnisse cachen. Mit ICO öffnet das OS eine Datei, liest einen 16-Byte-Verzeichniseintrag und springt direkt zu den Bitmap-Daten. Das Verzeichnis macht das Format selbstbeschreibend. Kein Decoder für Metadaten nötig.

Dasselbe Prinzip gilt für Favicons. Wenn ein Browser /favicon.ico anfordert, erhält er eine Datei, die jede Größe enthält, die er brauchen könnte — 16 x 16 für den Tab, 32 x 32 für die Lesezeichenleiste, 180 x 180 für iOS-Home-Bildschirm-Verknüpfungen. Der Browser wählt den richtigen Eintrag, ohne Bild-Header parsen zu müssen.

Der Datei-Header

Der ICO-Header hat zwei Teile: den ICONDIR und das ICONDIRENTRY-Array.

ICONDIR (6 Bytes)

Bytes 0-1: Reserved     (muss 0 sein)
Bytes 2-3: Type         (1 = Icon, 2 = Cursor)
Bytes 4-5: Count        (Anzahl der Bilder, Little-Endian uint16)

Jede ICO-Datei beginnt mit 00 00 01 00. Die nächsten zwei Bytes sagen dir, wie viele Bilder folgen.

ICONDIRENTRY (16 Bytes pro Eintrag)

Jedes Bild erhält einen Eintrag:

Bytes 0:    Width         (in Pixeln; 0 bedeutet 256)
Bytes 1:    Height        (in Pixeln; 0 bedeutet 256)
Bytes 2:    ColorCount    (0 wenn mehr als 256 Farben)
Bytes 3:    Reserved      (immer 0)
Bytes 4-5:  Planes        (1 für Icons)
Bytes 6-7:  BitCount      (Bits pro Pixel: 1, 4, 8, 24 oder 32)
Bytes 8-11: BytesInRes    (Größe der Bilddaten in Bytes, uint32)
Bytes 12-15: ImageOffset  (Byte-Offset zu den Bilddaten vom Dateianfang, uint32)

Die Width- und Height-Felder sind jeweils ein Byte lang. Der maximal explizit speicherbare Wert ist 255. Wenn ein Bild 256 x 256 Pixel groß ist, speichert das Feld 0x00, was Decoder als 256 interpretieren. Diese Eigenheit ist seit 1985 Teil der Spezifikation.

So sieht das Verzeichnis für eine ICO mit drei Bildern aus — einem 16 x 16 BMP, einem 32 x 32 BMP und einem 256 x 256 PNG:

Offset 0x00:  00 00 01 00 03 00                    -- ICONDIR: 3 Bilder
Offset 0x06:  10 10 00 00 01 00 18 00 2C 01 00 00 16 00 00 00  -- 16x16, 24bpp, BMP, 300 Bytes bei 0x16
Offset 0x16:  20 20 00 00 01 00 18 00 A8 02 00 00 42 01 00 00  -- 32x32, 24bpp, BMP, 680 Bytes bei 0x142
Offset 0x26:  00 00 00 00 01 00 20 00 B4 1C 00 00 EA 03 00 00  -- 256x256, 32bpp, PNG, 7348 Bytes bei 0x3EA

Das OS liest das Verzeichnis, findet den passenden Eintrag und springt zu ImageOffset. Es muss nichts geparst, nichts geraten werden — der Speicherzugriff erfolgt direkt.

Warum PNG, WebP und JPEG ICO nie ersetzt haben

Nach jedem modernen Maßstab ist PNG ein besseres Format als die interne Bitmap-Kodierung von ICO. PNG hat bessere Komprimierung, echte Alpha-Transparenz und weit verbreitete Tools. WebP ist noch kleiner. JPEG behandelt Fotos. Und doch hält ICO sich. Drei Gründe:

1. Multi-Resolution in einer Datei. PNG speichert ein Bild. ICO speichert eine beliebige Anzahl. Eine Website könnte ein ZIP mit PNGs ausliefern, aber kein Browser wüsste, wie er das richtige für ein Favicon auswählt. Die ICO-Verzeichnisstruktur löst das in sechs Bytes.

2. System-API-Bindung. Windows-LoadIcon, ExtractIcon und SHGetFileInfo erwarten alle ICO-Daten. Die Win32-API hat kein Äquivalent für PNG-Icon-Container. Das zu ändern würde jede Windows-Anwendung zerstören, die seit 1985 kompiliert wurde. Microsoft würde die Abwärtskompatibilität dafür niemals opfern.

3. Der Favicon-Standard. Das HTML-<link rel="icon">-Tag akzeptiert PNG, SVG und ICO, aber die implizite Standardanfrage für /favicon.ico ist älter als diese Optionen. Jeder Browser seit Internet Explorer 5 (1999) fordert /favicon.ico standardmäßig an. Seiten, die kein Favicon-Link deklarieren, brauchen dennoch eine ICO unter diesem Pfad — sonst bekommt der Browser einen 404.

PNG hat ICO nicht ersetzt, weil ICO nie um Bildqualität konkurriert hat. Es konkurrierte um Container-Semantik, und kein anderes Format bietet dasselbe Verzeichnis-in-einer-Datei-Modell mit dreißig Jahren Betriebssystem-Unterstützung.

Was im Container steckt

Jeder Eintrag in einer ICO-Datei zeigt auf ein unabhängiges Bild. Die Bilddaten können eines von zwei Formaten sein:

BMP-Kodierung (Legacy)

Vor Windows Vista waren alle ICO-Bilder unkomprimierte oder RLE-komprimierte BMP-Bitmaps. Die gespeicherten Daten sind eine DIB (Device Independent Bitmap) — ein BMP ohne den BITMAPFILEHEADER. Sie beginnen direkt mit dem BITMAPINFOHEADER:

Bytes 0-3:   Header size      (40 für BITMAPINFOHEADER)
Bytes 4-7:   Width            (uint32)
Bytes 8-11:  Height           (uint32, verdoppelt für XOR + AND Masken)
Bytes 12-13: Planes           (1)
Bytes 14-15: BitCount         (1, 4, 8, 24 oder 32)
Bytes 16-19: Compression      (0 für unkomprimiert, 1 oder 2 für RLE)
Bytes 20-23: Image size       (0 wenn unkomprimiert)
Bytes 24-27: X pixels per meter
Bytes 28-31: Y pixels per meter
Bytes 32-35: Colors used
Bytes 36-39: Important colors

Das Height-Feld im DIB-Header ist doppelt so hoch wie die tatsächliche Icon-Höhe. Ein 32 x 32 Icon speichert 64 im Height-Feld. Die obere Hälfte ist die XOR-Maske (das eigentliche Farbbild), die untere Hälfte ist die AND-Maske (eine 1-Bit-Transparenz-Bitmap). Bei 32-Bit-Icons mit Alphakanal wird die AND-Maske typischerweise ignoriert.

Für 24-Bit- und kleinere BMP-kodierte Icons ohne Alpha ist die AND-Maske der einzige Transparenzmechanismus. Eine 1 in der AND-Maske bedeutet transparent; eine 0 bedeutet opak. So hat Windows Transparenz erreicht, bevor 32-Bit-Farbe Standard wurde.

PNG-Kodierung (Windows Vista+)

Seit Windows Vista können ICO-Dateien PNG-kodierte Bilder speichern. Die Bilddaten am in ICONDIRENTRY angegebenen Offset sind eine rohe PNG-Datei — komplett mit ihrer eigenen 89 50 4E 47-Signatur und voller PNG-Chunk-Struktur.

Das ist das Format, das du für 256 x 256 Icons willst. Ein 256 x 256 32-Bit-BMP wäre unkomprimiert etwa 262 KB groß. Dasselbe Bild als PNG ist typischerweise 20-60 KB. Moderne Icon-Tools generieren PNG-kodierte Einträge für 256 x 256 und BMP-kodierte Einträge für kleinere Größen und erzielen so die beste Balance aus Kompatibilität und Dateigröße.

Vergleich der Kodierungen

KodierungEingeführtKomprimierungAlpha-UnterstützungBestens geeignet für
BMPWindows 1.0 (1985)Keine oder RLE1-Bit AND-Maske16 x 16 bis 48 x 48
PNGWindows Vista (2006)DEFLATE8-Bit Alpha64 x 64 bis 256 x 256

ICO-Dateien erkennen und analysieren

Vertraue nicht der .ico-Erweiterung. Lies die ersten sechs Bytes und parse das Verzeichnis.

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)"

Alle internen Bilder auflisten:

magick identify favicon.ico

Eine bestimmte Größe extrahieren:

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

Oder einfach:

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

Best Practices und Anwendungsfälle

ICO ist kein allgemeines Bildformat. Es ist ein Bereitstellungsartefakt für spezifische Kontexte. Verwende es, wo der Kontext es verlangt, und überall sonst PNG.

Favicons

Für moderne Websites solltest du beide Formate ausliefern:

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

Browser wählen das erste Format, das sie unterstützen. SVG-fähige Browser bekommen das Vektor-Icon. Legacy-Browser und Lesezeichen-Tools greifen auf ICO zurück. Die ICO-Datei sollte enthalten:

GrößeKodierungZweck
16 x 16BMPBrowser-Tab, Legacy-IE
32 x 32BMPTaskleiste, Lesezeichenleiste
48 x 48BMPWindows-Verknüpfungen
180 x 180PNGiOS-Home-Bildschirm-Icon
256 x 256PNGWindows Explorer extra-große Ansicht

Eine Favicon-ICO mit diesen fünf Einträgen ist typischerweise 30-50 KB groß. Ohne die 256 x 256 PNG sinkt sie auf unter 10 KB.

Windows-Anwendungs-Icons

Windows-Anwendungs-Executables betten eine ICO-Ressource ein. Das OS lädt zur Laufzeit die passende Größe aus dem eingebetteten Verzeichnis. Für Desktop-Anwendungen, die Windows 10 und 11 ansprechen, solltest du enthalten:

  • 16 x 16, 24 x 24, 32 x 32, 48 x 48 (BMP, für Legacy und Standard-DPI)
  • 64 x 64, 128 x 128, 256 x 256 (PNG, für High-DPI-Displays)

Windows skaliert automatisch herunter, wenn eine exakte Größe fehlt, aber Skalierung sieht schlechter aus als das Rendern einer nativen kleineren Größe. Jede Größe, die du weglässt, kostet Bildqualität.

Wann man ICO nicht verwenden sollte

  • Web-Content-Bilder: Verwende WebP, JPEG oder PNG. ICO hat keinen Komprimierungsvorteil und keinen browser-nativen Rendering-Vorteil für Inline-Bilder.
  • Fotografien: ICO ist nicht für High-Color-Kontinuierlichkeitsbilder konzipiert. Dateigrößen explodieren.
  • Plattformübergreifende Assets: macOS verwendet .icns, nicht ICO. Linux verwendet PNG oder SVG. ICO ist ein Windows-natives Format, das zufällig im Web funktioniert.

Die Zukunft von ICO

ICO wird alle Vorhersagen von seinem baldigen Tod überdauern. Der Grund ist derselbe wie bei seiner Entstehung: Abwärtskompatibilität.

SVG-Favicons sind technisch überlegen. Sie skalieren auf jede Auflösung, komprimieren besser als jedes Rasterformat und unterstützen Animation und Interaktivität. Chrome, Firefox und Safari unterstützen alle SVG-Favicons. Und doch hält ICO sich, weil Millionen Geräte, Unternehmenssysteme und Legacy-Browser /favicon.ico weiterhin standardmäßig anfordern. Eine Seite ohne ICO-Datei erzeugt 404s in Server-Logs und zeigt in älterer Software ein defektes Icon an.

Windows 11 verwendet ICO weiterhin für Anwendungs-Icons, Ordner-Icons und System-UI. Die Win32-API erwartet weiterhin ICO-Daten. Microsoft hat kein Interesse gezeigt, dieses Format zu ersetzen — es gibt keinen geschäftlichen Grund, dreißig Jahre Anwendungskompatibilität zu zerstören.

Verändert hat sich lediglich, wie ICO-Dateien erstellt werden. Moderne Toolchains — IconKitchen, Figma-Plugins, Online-Generatoren — geben ICO-Dateien mit automatisch PNG-kodierten Hochauflösungs-Einträgen aus. Der Container bleibt gleich, nur die Nutzlast wird besser.

ICO ist kein Format, das jemand aus Leidenschaft verwendet. Es funktioniert einfach überall, zerbricht nichts und kostet nichts, um es zu unterstützen. Genau deshalb wird es auch 2035 noch existieren.

Wenn du ICO-Dateien aus bestehenden Bildern erstellen musst, wandelt JPG to ICO JPG-, PNG- und WebP-Quellen direkt im Browser in Multi-Resolution-ICO-Dateien um — keine Uploads, keine Server-Verarbeitung. Es generiert die Standard-Größen-Matrix und wählt automatisch PNG-Kodierung für 256 x 256 und BMP-Kodierung für kleinere Größen, sodass du die optimale Balance aus Kompatibilität und Dateigröße erhältst.

Mehr Blog-Posts zum Lesen