toolgarden.xyz
中文
browser tool developmentICOICNSbinary formatsCanvas

How to Generate ICO and ICNS Files in the Browser

Render a source image at multiple sizes, write ICO directory and DIB bytes, build big-endian ICNS chunks, and export portable icon packages without a server.

ToolGarden tools prioritize browser-local processing, so files and text do not need to be uploaded to a server.

Published July 22, 202612 min readBy ToolGarden

ICO and ICNS are containers, not renamed PNG files. They store multiple representations for different display sizes, use different byte orders and directory structures, and require careful alpha, row order, and size metadata.

A browser implementation can decode the source image, apply one normalized crop and scale transform to every target size, encode PNG previews, write Windows DIB entries for smaller ICO layers, build macOS ICNS chunks, and package both combined and per-size outputs.

Render every target from one normalized transform

Load the selected image through an object URL, validate file bytes and decoded pixel count, and keep the decoded element only as long as the editor needs it. A normalized transform contains scale, x/y offsets, and corner radius, so preview and every exported resolution use identical composition.

For each configured size, create a fresh square canvas, clip a rounded rectangle if needed, enable high-quality smoothing, fit the source, apply user scale and offsets, then capture both ImageData and PNG bytes.

FormatTypical sizesContainer payload
ICO16 through 256DIB for small entries, PNG for 256
ICNS16 through 1024Typed PNG chunks
PNG ZIP16 through 1024Independent PNG files

Write Windows DIB pixels correctly

Small ICO entries can use a 40-byte BITMAPINFOHEADER followed by 32-bit pixel data and an AND mask. The stored height is doubled because it includes the XOR color bitmap and mask. Pixels are BGRA rather than Canvas RGBA, and rows are stored bottom-up.

Each AND-mask row is padded to a 32-bit boundary. With full alpha pixels the mask can remain zeroed, but its bytes and stride still belong to the DIB payload. For 256px entries, PNG bytes are widely supported and avoid the legacy width byte limit.

  • Write DIB numbers little-endian.
  • Reverse row order from Canvas top-down to bitmap bottom-up.
  • Swap red and blue channels to BGRA.
  • Store 256 width and height as zero in the one-byte ICO directory fields.

Build the ICO directory and offsets

An ICO begins with a six-byte header followed by one 16-byte directory record per image. Every record describes dimensions, color metadata, payload byte length, and absolute payload offset. Payloads are concatenated after the directory.

Compute offsets cumulatively from the full directory size. One incorrect byte length shifts every following image and can make the entire icon unreadable even when individual PNG or DIB data is valid.

const directorySize = 6 + entries.length * 16;
const header = new Uint8Array(directorySize);
const view = new DataView(header.buffer);

view.setUint16(0, 0, true);              // reserved
view.setUint16(2, 1, true);              // ICO image type
view.setUint16(4, entries.length, true); // image count

entries.forEach((entry, index) => {
  const offset = 6 + index * 16;
  header[offset] = entry.size >= 256 ? 0 : entry.size;
  header[offset + 1] = entry.size >= 256 ? 0 : entry.size;
  view.setUint16(offset + 6, 32, true);
  view.setUint32(offset + 8, entry.bytes.byteLength, true);
  view.setUint32(offset + 12, entry.dataOffset, true);
});

Build ICNS with big-endian typed chunks

ICNS starts with ASCII icns plus the total container length. Each representation is a chunk containing a four-character type, chunk length, and payload. Modern sizes can store PNG data directly under size-specific chunk types.

Unlike ICO fields, ICNS lengths are big-endian. Keep a size-to-chunk-type map, discard unsupported sizes deliberately, calculate total bytes before writing the header, and concatenate chunks in a predictable order.

function createIcnsChunk(type: string, png: Uint8Array) {
  const chunk = new Uint8Array(8 + png.byteLength);
  const view = new DataView(chunk.buffer);
  writeAscii(chunk, 0, type);
  view.setUint32(4, chunk.byteLength, false); // ICNS uses big-endian lengths
  chunk.set(png, 8);
  return chunk;
}

const chunks = entries.map(entry =>
  createIcnsChunk(chunkTypeForSize(entry.size), entry.pngBytes)
);
const header = createIcnsHeader(8 + sumByteLengths(chunks));
return new Blob([header, ...chunks], { type: 'image/icns' });

Offer combined files and inspectable archives

The primary ICO or ICNS should contain all supported sizes. A ZIP of per-size ICO, ICNS, or PNG files is useful for debugging and platforms that request individual assets. Build both from the same rendered entry list so they cannot drift visually.

Object URLs for source, combined output, and ZIP output need separate lifecycle tracking. Revoke old URLs when the user changes source, format, or transform, and revoke everything on unmount.

Test icons in real consumers

A preview canvas cannot validate the container. Test ICO in Windows Explorer, shortcuts, browser favicon handling, and an icon inspector; test ICNS through macOS Finder or icon tooling. Check 16px legibility, transparency, rounded corners, and high-resolution representations.

Do not upscale a tiny source and call it multi-resolution quality. Large exports preserve the same limited source detail, while small exports often need simpler shapes and stronger contrast than a single automatic downsample provides.

Key takeaways

Browser icon generation combines visual rendering with binary format engineering. Normalize one transform, render every target size, respect ICO DIB row and channel rules, switch endianness for ICNS, compute offsets exactly, and validate the container in actual operating-system consumers.

Frequently asked questions

Q.Can I create an ICO by renaming a PNG file?

No. An ICO has a header and image directory and may contain multiple PNG or DIB payloads. Renaming changes only the filename, not the container bytes.

Q.Why is a 256px ICO directory size stored as zero?

ICO directory width and height fields are one byte. The format defines zero as 256, allowing that special size without a wider field.

Q.Why do ICO DIB rows run bottom-up?

The legacy bitmap representation used by ICO follows Windows DIB conventions. Positive bitmap height stores the first row as the bottom row, unlike Canvas ImageData.

Q.Why does ICNS use different byte order from ICO?

They are independent platform formats. ICO structures use little-endian integer fields, while ICNS container and chunk lengths use big-endian values.