What Is an ICO File?
ICO is the Windows icon file format, used to store small images that represent applications, files, folders, and shortcuts in the Windows operating system. A single ICO file is a container that holds multiple image variants at different sizes and color depths — Windows automatically selects the most appropriate size for each display context (taskbar, desktop, Explorer thumbnail, title bar, etc.).
The format was introduced with Windows 1.0 in 1985 and has evolved significantly: modern ICO files can embed PNG-compressed images alongside the traditional BMP-encoded layers, and Windows Vista introduced support for 256×256 pixel icons with alpha transparency.
ICO files are also used in web development as favicons — the small icons displayed in browser tabs, bookmarks, and address bars. The traditional approach was to name the file favicon.ico and place it in the website's root directory.
ICO File Structure
An ICO file begins with a 6-byte header followed by a directory listing all embedded images, and then the image data itself:
ICO Header (6 bytes):
[2 bytes] Reserved = 0x0000
[2 bytes] Type = 0x0001 (ICO) or 0x0002 (CUR for cursors)
[2 bytes] Count = number of images in the file
For each image, a 16-byte directory entry:
[1 byte] Width = image width in pixels (0 = 256)
[1 byte] Height = image height in pixels (0 = 256)
[1 byte] ColorCount = number of colors (0 if more than 256)
[1 byte] Reserved = 0x00
[2 bytes] Planes = color planes (typically 1)
[2 bytes] BitCount = bits per pixel (1, 4, 8, 24, 32)
[4 bytes] BytesInRes = size of image data in bytes
[4 bytes] ImageOffset = byte offset from start of file to image data
Image data:
Either BMP DIB (without the BITMAPFILEHEADER)
Or PNG data (for sizes >= 256x256 in modern ICO files)
Standard Icon Sizes
Windows uses different icon sizes for different contexts. A comprehensive ICO file should contain at minimum:
| Size | Context |
|---|---|
| 16×16 | Title bar, small list view, taskbar (small) |
| 24×24 | Small toolbar buttons |
| 32×32 | Desktop (low DPI), Explorer list, classic menu |
| 48×48 | Desktop (medium), large list view |
| 64×64 | Some custom dialogs |
| 128×128 | Folder thumbnail view |
| 256×256 | Desktop (high DPI), Explorer large icons |
For modern HiDPI/Retina displays, Windows scales icons. A 48×48 icon at 125% DPI appears at the same physical size as a 60×60 would at 100%, but the OS uses the next-largest available size and scales it. Including 256×256 ensures crisp rendering at all DPI settings.
For favicons, the minimum is 16×16 (required) plus 32×32 and 48×48 for better display across browsers and operating systems.
Creating ICO Files
Using Python with Pillow
pip install Pillow
from PIL import Image
import io
def create_ico_from_png(
source_png: str,
output_ico: str,
sizes: list[int] = None,
) -> None:
"""
Convert a PNG to ICO with multiple embedded sizes.
Args:
source_png: Path to source PNG (ideally 256x256 or larger)
output_ico: Output ICO file path
sizes: List of pixel sizes to embed (default: standard set)
"""
if sizes is None:
sizes = [16, 24, 32, 48, 64, 128, 256]
# Load source image (must be RGBA for alpha transparency)
img = Image.open(source_png).convert('RGBA')
# Pillow's save with format='ICO' and sizes parameter
img.save(
output_ico,
format='ICO',
sizes=[(s, s) for s in sizes],
)
print(f"ICO saved: {output_ico} ({len(sizes)} sizes: {sizes})")
# Basic usage
create_ico_from_png('logo_256.png', 'app.ico')
# Favicon (minimal sizes)
create_ico_from_png('favicon_source.png', 'favicon.ico', sizes=[16, 32, 48])
Batch Converting PNG to ICO
from PIL import Image
import glob
import os
def batch_png_to_ico(
png_directory: str,
output_directory: str,
sizes: list[int] = None,
) -> None:
"""Convert all PNGs in a directory to ICO files."""
if sizes is None:
sizes = [16, 32, 48, 64, 128, 256]
os.makedirs(output_directory, exist_ok=True)
png_files = glob.glob(os.path.join(png_directory, '*.png'))
for png_path in png_files:
filename = os.path.splitext(os.path.basename(png_path))[0]
ico_path = os.path.join(output_directory, f'{filename}.ico')
try:
img = Image.open(png_path).convert('RGBA')
img.save(ico_path, format='ICO', sizes=[(s, s) for s in sizes])
print(f" {filename}.png → {filename}.ico")
except Exception as e:
print(f" FAILED: {filename}.png — {e}")
batch_png_to_ico('./source_icons', './output_icons')
Extracting Images from an ICO File
from PIL import Image, IcoImagePlugin
import os
def extract_ico_frames(ico_path: str, output_dir: str) -> None:
"""Extract all embedded size variants from an ICO file as PNG."""
os.makedirs(output_dir, exist_ok=True)
ico = Image.open(ico_path)
# Pillow exposes ICO sizes via the info dict
sizes = ico.info.get('sizes', [(ico.width, ico.height)])
print(f"ICO contains {len(sizes)} embedded sizes: {sorted(sizes)}")
for size in sorted(sizes):
try:
ico.size = size # select specific size
frame = ico.copy().convert('RGBA')
out_path = os.path.join(output_dir, f'icon_{size[0]}x{size[1]}.png')
frame.save(out_path, 'PNG')
print(f" Extracted: {out_path}")
except Exception as e:
print(f" Skipped {size}: {e}")
extract_ico_frames('application.ico', './extracted_icons')
ICO and Favicons for the Web
The Modern Favicon Stack
Browser support for different favicon formats has evolved considerably. The modern recommended approach:
<!-- favicon.ico (root) — serves IE, old browsers, and bookmarks -->
<link rel="shortcut icon" href="/favicon.ico">
<!-- PNG favicons for modern browsers -->
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<!-- Apple touch icon (iOS home screen shortcut) -->
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<!-- Android Chrome icons (via Web App Manifest) -->
<link rel="manifest" href="/site.webmanifest">
<!-- SVG favicon for scalable rendering (modern Chrome/Firefox) -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
The favicon.ico at the root is still important because browsers fetch it automatically without needing the <link> tag, and it serves all environments that don't support PNG favicons. However, SVG favicons are increasingly preferred for their lossless scaling at any size.
Generating a Complete Favicon Package
from PIL import Image
import os
def generate_favicon_package(source_image: str, output_dir: str) -> None:
"""
Generate a complete favicon package from a single source image.
Produces: favicon.ico, favicon-16x16.png, favicon-32x32.png,
apple-touch-icon.png (180x180), and android-chrome files.
"""
os.makedirs(output_dir, exist_ok=True)
src = Image.open(source_image).convert('RGBA')
# favicon.ico — multi-size
src.save(
os.path.join(output_dir, 'favicon.ico'),
format='ICO',
sizes=[(16,16), (32,32), (48,48)],
)
# PNG sizes
for size, name in [
(16, 'favicon-16x16.png'),
(32, 'favicon-32x32.png'),
(48, 'favicon-48x48.png'),
(192, 'android-chrome-192x192.png'),
(512, 'android-chrome-512x512.png'),
(180, 'apple-touch-icon.png'),
]:
resized = src.resize((size, size), Image.LANCZOS)
resized.save(os.path.join(output_dir, name), 'PNG')
print(f" Generated: {name}")
print(f"\nFavicon package ready in: {output_dir}")
generate_favicon_package('logo.png', './favicons')
Reading ICO Metadata with Python
from PIL import Image
import struct
import os
def inspect_ico(ico_path: str) -> dict:
"""Read ICO directory and report all embedded image metadata."""
file_size = os.path.getsize(ico_path)
with open(ico_path, 'rb') as f:
# Read header
reserved, type_, count = struct.unpack('<HHH', f.read(6))
images = []
for _ in range(count):
width, height, color_count, reserved_b = struct.unpack('BBBB', f.read(4))
planes, bit_count = struct.unpack('<HH', f.read(4))
bytes_in_res, image_offset = struct.unpack('<II', f.read(8))
# Width/height of 0 means 256
w = width if width > 0 else 256
h = height if height > 0 else 256
# Check if image data is PNG (starts with PNG magic bytes)
pos = f.tell()
f.seek(image_offset)
magic = f.read(8)
is_png = magic[:4] == b'\x89PNG'
f.seek(pos)
images.append({
'size': f'{w}x{h}',
'bit_depth': bit_count,
'encoding': 'PNG' if is_png else 'BMP DIB',
'data_size_kb': round(bytes_in_res / 1024, 1),
})
return {
'file_size_kb': round(file_size / 1024, 1),
'image_count': count,
'images': sorted(images, key=lambda i: int(i['size'].split('x')[0])),
}
info = inspect_ico('app.ico')
print(f"ICO file: {info['file_size_kb']} KB, {info['image_count']} embedded images")
for img in info['images']:
print(f" {img['size']} @ {img['bit_depth']}bpp ({img['encoding']}, {img['data_size_kb']} KB)")
ICO Best Practices
Always include alpha channel — ICO images with 32-bit color (RGBA) support full alpha transparency, essential for icons on any background color. Avoid 8-bit or 24-bit ICO for modern Windows applications.
Use PNG compression for large sizes — Modern ICO files (Vista+) embed PNG data for the 256×256 size, dramatically reducing file size vs BMP encoding.
Source from vector art — Generate ICO from SVG or high-resolution PNG (512×512+) rather than scaling up small raster images. Downscaling maintains sharpness; upscaling creates blurry icons.
Test on multiple DPI settings — Check icons at 96 DPI (100%), 120 DPI (125%), and 144 DPI (150%) to ensure crisp rendering at Windows' common DPI scaling levels.
Favicon ICO vs PNG — For web, a multi-size favicon.ico in the root covers all environments. Add explicit PNG and SVG favicon links for modern browsers that prefer those formats.
Related conversions
Frequent conversions across the catalogue: