Task 303: .ICO File Format
Task 303: .ICO File Format
The .ICO file format is a container for one or more icon images used primarily in Windows. It supports multiple image sizes (up to 256x256 pixels) and color depths (from 1-bit to 32-bit with alpha transparency). Images within an .ICO file can be in BMP format (without the BITMAPFILEHEADER) or PNG format (for Windows Vista and later). The format is little-endian.
The properties intrinsic to the .ICO file format (derived from its binary structure) are as follows. These include the main header fields and the per-image directory entry fields. Note that "intrinsic to its file system" is interpreted as the core structural properties embedded in the file itself, excluding external file system metadata like timestamps or permissions:
- Reserved (header, WORD, offset 0): Must always be 0.
- Type (header, WORD, offset 2): Specifies the resource type; must be 1 for .ICO files (2 for .CUR cursor files).
- Image Count (header, WORD, offset 4): The number of icon images in the file (1 or more).
- Width (per-image, BYTE, offset 0 in entry): Image width in pixels (1-255; 0 represents 256 pixels).
- Height (per-image, BYTE, offset 1 in entry): Image height in pixels (1-255; 0 represents 256 pixels).
- Color Count (per-image, BYTE, offset 2 in entry): Number of colors in the palette (2, 4, 16, etc.); 0 if the image does not use a palette (e.g., for 24-bit or higher).
- Reserved (per-image, BYTE, offset 3 in entry): Must always be 0.
- Color Planes (per-image, WORD, offset 4 in entry): Number of color planes; must be 0 or 1 for .ICO files.
- Bits Per Pixel (per-image, WORD, offset 6 in entry): Color depth (1, 4, 8, 16, 24, or 32 bits per pixel).
- Image Data Size (per-image, DWORD, offset 8 in entry): Size of the image data in bytes.
- Image Data Offset (per-image, DWORD, offset 12 in entry): Offset from the start of the file to the image data.
- Image Format (derived per-image): Type of embedded image data ("BMP" if data starts with signature 0x42 0x4D; "PNG" if data starts with signature 0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A; otherwise "Unknown").
Two direct download links for .ICO files:
- https://filesamples.com/samples/image/ico/sample_640×426.ico
- https://filesamples.com/samples/image/ico/sample_1280×853.ico
Below is an embeddable HTML/JavaScript snippet suitable for a Ghost blog (or any HTML page). It creates a drag-and-drop area where a user can drop a .ICO file. The script reads the file as an ArrayBuffer, parses it using DataView (little-endian), extracts the properties from the list above, and dumps them to the screen in a formatted element. It handles multiple images and detects BMP/PNG formats. Error handling is included for invalid files.
Below is a Python class IcoHandler
that can open a .ICO file, decode/read its binary data, extract and print the properties to console, and write a modified or new .ICO file (e.g., it can read, modify properties like adding a dummy image entry, and write back). It uses the struct
module for parsing (little-endian). For simplicity, writing assumes preserving original image data; it doesn't generate new image data. Run with handler = IcoHandler('example.ico'); handler.print_properties(); handler.write('output.ico')
.
import struct
import os
class IcoHandler:
def __init__(self, filename):
self.filename = filename
self.header = None
self.entries = []
self.image_data = []
self._read()
def _read(self):
with open(self.filename, 'rb') as f:
data = f.read()
if len(data) < 6:
raise ValueError('Invalid ICO file')
# Unpack header
reserved, type_, count = struct.unpack('<HHH', data[:6])
if reserved != 0 or type_ != 1:
raise ValueError('Invalid ICO header')
self.header = {'reserved': reserved, 'type': type_, 'count': count}
offset = 6
for i in range(count):
if offset + 16 > len(data):
raise ValueError('Truncated ICO entry')
entry = struct.unpack('<BBBBHHII', data[offset:offset+16])
width = entry[0] if entry[0] != 0 else 256
height = entry[1] if entry[1] != 0 else 256
self.entries.append({
'width': width,
'height': height,
'color_count': entry[2],
'reserved': entry[3],
'planes': entry[4],
'bit_count': entry[5],
'data_size': entry[6],
'data_offset': entry[7]
})
# Extract image data
img_start = entry[7]
img_end = img_start + entry[6]
if img_end > len(data):
raise ValueError('Invalid image data offset/size')
img_bytes = data[img_start:img_end]
self.image_data.append(img_bytes)
# Detect format
if len(img_bytes) >= 2 and img_bytes[:2] == b'BM':
self.entries[-1]['format'] = 'BMP'
elif len(img_bytes) >= 8 and img_bytes[:8] == b'\x89PNG\r\n\x1A\n':
self.entries[-1]['format'] = 'PNG'
else:
self.entries[-1]['format'] = 'Unknown'
offset += 16
def print_properties(self):
print('ICO Properties:')
print(f"- Reserved: {self.header['reserved']}")
print(f"- Type: {self.header['type']}")
print(f"- Image Count: {self.header['count']}\n")
for i, entry in enumerate(self.entries):
print(f"Image {i+1}:")
print(f" - Width: {entry['width']}")
print(f" - Height: {entry['height']}")
print(f" - Color Count: {entry['color_count']}")
print(f" - Reserved: {entry['reserved']}")
print(f" - Color Planes: {entry['planes']}")
print(f" - Bits Per Pixel: {entry['bit_count']}")
print(f" - Image Data Size: {entry['data_size']}")
print(f" - Image Data Offset: {entry['data_offset']}")
print(f" - Image Format: {entry['format']}\n")
def write(self, output_filename):
# Recompute offsets for writing
header_size = 6 + len(self.entries) * 16
current_offset = header_size
for entry in self.entries:
entry['data_offset'] = current_offset
entry['data_size'] = len(self.image_data[self.entries.index(entry)])
current_offset += entry['data_size']
with open(output_filename, 'wb') as f:
# Write header
f.write(struct.pack('<HHH', self.header['reserved'], self.header['type'], len(self.entries)))
# Write entries
for entry in self.entries:
width = entry['width'] if entry['width'] < 256 else 0
height = entry['height'] if entry['height'] < 256 else 0
f.write(struct.pack('<BBBBHHII', width, height, entry['color_count'], entry['reserved'],
entry['planes'], entry['bit_count'], entry['data_size'], entry['data_offset']))
# Write image data
for img in self.image_data:
f.write(img)
print(f'Wrote ICO to {output_filename}')
Below is a Java class IcoHandler
that can open a .ICO file, decode/read its binary data, extract and print the properties to console, and write a modified or new .ICO file (similar to Python: reads, allows modification, writes back preserving image data). It uses ByteBuffer
for little-endian parsing. Compile and run with IcoHandler handler = new IcoHandler("example.ico"); handler.printProperties(); handler.write("output.ico");
.
import java.io.*;
import java.nio.*;
import java.util.*;
public class IcoHandler {
private String filename;
private Map<String, Integer> header;
private List<Map<String, Object>> entries;
private List<byte[]> imageData;
public IcoHandler(String filename) throws IOException {
this.filename = filename;
this.header = new HashMap<>();
this.entries = new ArrayList<>();
this.imageData = new ArrayList<>();
read();
}
private void read() throws IOException {
File file = new File(filename);
byte[] data = new byte[(int) file.length()];
try (FileInputStream fis = new FileInputStream(file)) {
fis.read(data);
}
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
int reserved = bb.getShort(0) & 0xFFFF;
int type = bb.getShort(2) & 0xFFFF;
int count = bb.getShort(4) & 0xFFFF;
if (reserved != 0 || type != 1) {
throw new IOException("Invalid ICO header");
}
header.put("reserved", reserved);
header.put("type", type);
header.put("count", count);
int offset = 6;
for (int i = 0; i < count; i++) {
Map<String, Object> entry = new HashMap<>();
int width = bb.get(offset) & 0xFF;
width = (width == 0) ? 256 : width;
int height = bb.get(offset + 1) & 0xFF;
height = (height == 0) ? 256 : height;
entry.put("width", width);
entry.put("height", height);
entry.put("color_count", bb.get(offset + 2) & 0xFF);
entry.put("reserved", bb.get(offset + 3) & 0xFF);
entry.put("planes", bb.getShort(offset + 4) & 0xFFFF);
entry.put("bit_count", bb.getShort(offset + 6) & 0xFFFF);
int dataSize = bb.getInt(offset + 8);
int dataOffset = bb.getInt(offset + 12);
entry.put("data_size", dataSize);
entry.put("data_offset", dataOffset);
byte[] imgBytes = new byte[dataSize];
System.arraycopy(data, dataOffset, imgBytes, 0, dataSize);
imageData.add(imgBytes);
String format = "Unknown";
if (dataSize >= 2 && imgBytes[0] == 0x42 && imgBytes[1] == 0x4D) {
format = "BMP";
} else if (dataSize >= 8 && imgBytes[0] == (byte)0x89 && imgBytes[1] == 0x50 && imgBytes[2] == 0x4E && imgBytes[3] == 0x47 &&
imgBytes[4] == 0x0D && imgBytes[5] == 0x0A && imgBytes[6] == 0x1A && imgBytes[7] == 0x0A) {
format = "PNG";
}
entry.put("format", format);
entries.add(entry);
offset += 16;
}
}
public void printProperties() {
System.out.println("ICO Properties:");
System.out.println("- Reserved: " + header.get("reserved"));
System.out.println("- Type: " + header.get("type"));
System.out.println("- Image Count: " + header.get("count") + "\n");
for (int i = 0; i < entries.size(); i++) {
Map<String, Object> entry = entries.get(i);
System.out.println("Image " + (i + 1) + ":");
System.out.println(" - Width: " + entry.get("width"));
System.out.println(" - Height: " + entry.get("height"));
System.out.println(" - Color Count: " + entry.get("color_count"));
System.out.println(" - Reserved: " + entry.get("reserved"));
System.out.println(" - Color Planes: " + entry.get("planes"));
System.out.println(" - Bits Per Pixel: " + entry.get("bit_count"));
System.out.println(" - Image Data Size: " + entry.get("data_size"));
System.out.println(" - Image Data Offset: " + entry.get("data_offset"));
System.out.println(" - Image Format: " + entry.get("format") + "\n");
}
}
public void write(String outputFilename) throws IOException {
// Recompute offsets
int headerSize = 6 + entries.size() * 16;
int currentOffset = headerSize;
for (Map<String, Object> entry : entries) {
entry.put("data_offset", currentOffset);
entry.put("data_size", imageData.get(entries.indexOf(entry)).length);
currentOffset += (int) entry.get("data_size");
}
ByteBuffer bb = ByteBuffer.allocate(currentOffset).order(ByteOrder.LITTLE_ENDIAN);
// Write header
bb.putShort((short) header.get("reserved").intValue());
bb.putShort((short) header.get("type").intValue());
bb.putShort((short) entries.size());
// Write entries
for (Map<String, Object> entry : entries) {
int width = (int) entry.get("width");
bb.put((byte) (width < 256 ? width : 0));
int height = (int) entry.get("height");
bb.put((byte) (height < 256 ? height : 0));
bb.put((byte) ((int) entry.get("color_count")));
bb.put((byte) ((int) entry.get("reserved")));
bb.putShort((short) ((int) entry.get("planes")));
bb.putShort((short) ((int) entry.get("bit_count")));
bb.putInt((int) entry.get("data_size"));
bb.putInt((int) entry.get("data_offset"));
}
// Write image data
for (byte[] img : imageData) {
bb.put(img);
}
try (FileOutputStream fos = new FileOutputStream(outputFilename)) {
fos.write(bb.array());
}
System.out.println("Wrote ICO to " + outputFilename);
}
}
Below is a JavaScript class IcoHandler
(Node.js compatible) that can open a .ICO file, decode/read its binary data, extract and print the properties to console, and write a modified or new .ICO file (reads via fs, allows modification, writes back). It uses Buffer
for little-endian parsing. Run with Node.js: const IcoHandler = require('./ico-handler'); const handler = new IcoHandler('example.ico'); handler.printProperties(); handler.write('output.ico');
.
const fs = require('fs');
class IcoHandler {
constructor(filename) {
this.filename = filename;
this.header = {};
this.entries = [];
this.imageData = [];
this.read();
}
read() {
const data = fs.readFileSync(this.filename);
if (data.length < 6) throw new Error('Invalid ICO file');
this.header.reserved = data.readUInt16LE(0);
this.header.type = data.readUInt16LE(2);
this.header.count = data.readUInt16LE(4);
if (this.header.reserved !== 0 || this.header.type !== 1) {
throw new Error('Invalid ICO header');
}
let offset = 6;
for (let i = 0; i < this.header.count; i++) {
const entry = {};
entry.width = data[offset] || 256;
entry.height = data[offset + 1] || 256;
entry.color_count = data[offset + 2];
entry.reserved = data[offset + 3];
entry.planes = data.readUInt16LE(offset + 4);
entry.bit_count = data.readUInt16LE(offset + 6);
entry.data_size = data.readUInt32LE(offset + 8);
entry.data_offset = data.readUInt32LE(offset + 12);
const imgStart = entry.data_offset;
const imgEnd = imgStart + entry.data_size;
if (imgEnd > data.length) throw new Error('Invalid image data');
const imgBytes = data.slice(imgStart, imgEnd);
this.imageData.push(imgBytes);
let format = 'Unknown';
if (imgBytes.length >= 2 && imgBytes[0] === 0x42 && imgBytes[1] === 0x4D) {
format = 'BMP';
} else if (imgBytes.length >= 8 && imgBytes[0] === 0x89 && imgBytes[1] === 0x50 && imgBytes[2] === 0x4E && imgBytes[3] === 0x47 &&
imgBytes[4] === 0x0D && imgBytes[5] === 0x0A && imgBytes[6] === 0x1A && imgBytes[7] === 0x0A) {
format = 'PNG';
}
entry.format = format;
this.entries.push(entry);
offset += 16;
}
}
printProperties() {
console.log('ICO Properties:');
console.log(`- Reserved: ${this.header.reserved}`);
console.log(`- Type: ${this.header.type}`);
console.log(`- Image Count: ${this.header.count}\n`);
this.entries.forEach((entry, i) => {
console.log(`Image ${i + 1}:`);
console.log(` - Width: ${entry.width}`);
console.log(` - Height: ${entry.height}`);
console.log(` - Color Count: ${entry.color_count}`);
console.log(` - Reserved: ${entry.reserved}`);
console.log(` - Color Planes: ${entry.planes}`);
console.log(` - Bits Per Pixel: ${entry.bit_count}`);
console.log(` - Image Data Size: ${entry.data_size}`);
console.log(` - Image Data Offset: ${entry.data_offset}`);
console.log(` - Image Format: ${entry.format}\n`);
});
}
write(outputFilename) {
// Recompute offsets
const headerSize = 6 + this.entries.length * 16;
let currentOffset = headerSize;
this.entries.forEach(entry => {
entry.data_offset = currentOffset;
entry.data_size = this.imageData[this.entries.indexOf(entry)].length;
currentOffset += entry.data_size;
});
const buffer = Buffer.alloc(currentOffset);
// Write header
buffer.writeUInt16LE(this.header.reserved, 0);
buffer.writeUInt16LE(this.header.type, 2);
buffer.writeUInt16LE(this.entries.length, 4);
// Write entries
let offset = 6;
this.entries.forEach(entry => {
const width = entry.width < 256 ? entry.width : 0;
const height = entry.height < 256 ? entry.height : 0;
buffer[offset] = width;
buffer[offset + 1] = height;
buffer[offset + 2] = entry.color_count;
buffer[offset + 3] = entry.reserved;
buffer.writeUInt16LE(entry.planes, offset + 4);
buffer.writeUInt16LE(entry.bit_count, offset + 6);
buffer.writeUInt32LE(entry.data_size, offset + 8);
buffer.writeUInt32LE(entry.data_offset, offset + 12);
offset += 16;
});
// Write image data
this.imageData.forEach((img, i) => {
img.copy(buffer, this.entries[i].data_offset);
});
fs.writeFileSync(outputFilename, buffer);
console.log(`Wrote ICO to ${outputFilename}`);
}
}
module.exports = IcoHandler;
Below is a C++ class IcoHandler
(since C doesn't natively support classes; this uses struct with methods for class-like behavior) that can open a .ICO file, decode/read its binary data, extract and print the properties to console, and write a modified or new .ICO file. It uses fread
/ fwrite
for I/O and handles little-endian (assuming host is little-endian or adjust if needed). Compile with g++ ico_handler.cpp -o ico_handler
and run ./ico_handler example.ico output.ico
(prints automatically; writes if output provided).
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <cstdint>
#include <cstring>
struct IcoEntry {
uint8_t width; // adjusted to 256 if 0
uint8_t height;
uint8_t color_count;
uint8_t reserved;
uint16_t planes;
uint16_t bit_count;
uint32_t data_size;
uint32_t data_offset;
std::string format;
std::vector<uint8_t> image_data;
};
class IcoHandler {
private:
std::string filename;
uint16_t reserved;
uint16_t type;
uint16_t count;
std::vector<IcoEntry> entries;
public:
IcoHandler(const std::string& fn) : filename(fn) {
read();
}
void read() {
std::ifstream file(filename, std::ios::binary | std::ios::ate);
if (!file) {
throw std::runtime_error("Cannot open file");
}
size_t size = file.tellg();
file.seekg(0);
std::vector<uint8_t> data(size);
file.read(reinterpret_cast<char*>(data.data()), size);
file.close();
if (size < 6) throw std::runtime_error("Invalid ICO file");
memcpy(&reserved, data.data(), 2);
memcpy(&type, data.data() + 2, 2);
memcpy(&count, data.data() + 4, 2);
if (reserved != 0 || type != 1) {
throw std::runtime_error("Invalid ICO header");
}
size_t offset = 6;
for (uint16_t i = 0; i < count; ++i) {
if (offset + 16 > size) throw std::runtime_error("Truncated entry");
IcoEntry entry;
entry.width = data[offset];
entry.width = (entry.width == 0) ? 256 : entry.width;
entry.height = data[offset + 1];
entry.height = (entry.height == 0) ? 256 : entry.height;
entry.color_count = data[offset + 2];
entry.reserved = data[offset + 3];
memcpy(&entry.planes, data.data() + offset + 4, 2);
memcpy(&entry.bit_count, data.data() + offset + 6, 2);
memcpy(&entry.data_size, data.data() + offset + 8, 4);
memcpy(&entry.data_offset, data.data() + offset + 12, 4);
size_t img_start = entry.data_offset;
size_t img_end = img_start + entry.data_size;
if (img_end > size) throw std::runtime_error("Invalid image data");
entry.image_data.assign(data.begin() + img_start, data.begin() + img_end);
entry.format = "Unknown";
if (entry.data_size >= 2 && entry.image_data[0] == 0x42 && entry.image_data[1] == 0x4D) {
entry.format = "BMP";
} else if (entry.data_size >= 8 && entry.image_data[0] == 0x89 && entry.image_data[1] == 0x50 && entry.image_data[2] == 0x4E && entry.image_data[3] == 0x47 &&
entry.image_data[4] == 0x0D && entry.image_data[5] == 0x0A && entry.image_data[6] == 0x1A && entry.image_data[7] == 0x0A) {
entry.format = "PNG";
}
entries.push_back(entry);
offset += 16;
}
}
void printProperties() const {
std::cout << "ICO Properties:" << std::endl;
std::cout << "- Reserved: " << reserved << std::endl;
std::cout << "- Type: " << type << std::endl;
std::cout << "- Image Count: " << count << std::endl << std::endl;
for (size_t i = 0; i < entries.size(); ++i) {
const auto& entry = entries[i];
std::cout << "Image " << (i + 1) << ":" << std::endl;
std::cout << " - Width: " << static_cast<unsigned>(entry.width) << std::endl;
std::cout << " - Height: " << static_cast<unsigned>(entry.height) << std::endl;
std::cout << " - Color Count: " << static_cast<unsigned>(entry.color_count) << std::endl;
std::cout << " - Reserved: " << static_cast<unsigned>(entry.reserved) << std::endl;
std::cout << " - Color Planes: " << entry.planes << std::endl;
std::cout << " - Bits Per Pixel: " << entry.bit_count << std::endl;
std::cout << " - Image Data Size: " << entry.data_size << std::endl;
std::cout << " - Image Data Offset: " << entry.data_offset << std::endl;
std::cout << " - Image Format: " << entry.format << std::endl << std::endl;
}
}
void write(const std::string& outputFilename) {
// Recompute offsets
size_t header_size = 6 + entries.size() * 16;
uint32_t current_offset = static_cast<uint32_t>(header_size);
for (auto& entry : entries) {
entry.data_offset = current_offset;
entry.data_size = static_cast<uint32_t>(entry.image_data.size());
current_offset += entry.data_size;
}
std::vector<uint8_t> buffer(current_offset);
// Write header
memcpy(buffer.data(), &reserved, 2);
memcpy(buffer.data() + 2, &type, 2);
uint16_t new_count = static_cast<uint16_t>(entries.size());
memcpy(buffer.data() + 4, &new_count, 2);
// Write entries
size_t offset = 6;
for (const auto& entry : entries) {
uint8_t w = (entry.width < 256) ? entry.width : 0;
uint8_t h = (entry.height < 256) ? entry.height : 0;
buffer[offset] = w;
buffer[offset + 1] = h;
buffer[offset + 2] = entry.color_count;
buffer[offset + 3] = entry.reserved;
memcpy(buffer.data() + offset + 4, &entry.planes, 2);
memcpy(buffer.data() + offset + 6, &entry.bit_count, 2);
memcpy(buffer.data() + offset + 8, &entry.data_size, 4);
memcpy(buffer.data() + offset + 12, &entry.data_offset, 4);
offset += 16;
}
// Write image data
for (const auto& entry : entries) {
memcpy(buffer.data() + entry.data_offset, entry.image_data.data(), entry.data_size);
}
std::ofstream out_file(outputFilename, std::ios::binary);
if (!out_file) {
throw std::runtime_error("Cannot write file");
}
out_file.write(reinterpret_cast<const char*>(buffer.data()), buffer.size());
out_file.close();
std::cout << "Wrote ICO to " << outputFilename << std::endl;
}
};
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "Usage: " << argv[0] << " input.ico [output.ico]" << std::endl;
return 1;
}
try {
IcoHandler handler(argv[1]);
handler.printProperties();
if (argc > 2) {
handler.write(argv[2]);
}
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return 0;
}