🍱 Lunchbox Hands

icons

How to Make a macOS .icns and a Windows .ico From One PNG

Inside the .icns and .ico containers: the element types, the iconutil workflow, and the two gotchas that leave desktop icons blurry or garbled.

Ship a desktop app with Electron, Tauri, or a plain native toolchain and you’ll eventually need two files mobile developers never touch: a macOS .icns and a Windows .ico. Both are containers that hold many sizes of one picture, and both look easy to build by hand. That’s the trap. Most broken desktop icons come from a valid-looking container holding the wrong kind of image data for the smallest sizes, not from a bad export. Below: how each format is laid out byte by byte, how to build each from one master PNG, and the two gotchas that produce static-noise Finder icons and taskbar mush. (For the full cross-platform list of sizes, see the app icon sizes guide. For browser favicons, see the favicon guide.)

Who needs which file

ToolchainmacOSWindows
Electron (electron-builder)build/icon.icns, or a 1024 PNG/SVG it converts for youbuild/icon.ico, or a PNG/SVG of at least 256 px
Tauriicons/icon.icnsicons/icon.ico with 16, 24, 32, 48, 64 and 256 px layers
Xcode native appAsset catalog (AppIcon.appiconset) or Icon Composer file; no .icns neededn/a
WinUI / MSIX packagen/aPNG assets per scale and target size, declared in the manifest
Plain Win32n/a.ico compiled into the executable’s resources

electron-builder can generate both files from one image, but if you drop a ready-made icon.icns or icon.ico into build/, it skips conversion and uses yours as-is. Tauri’s docs add one detail most generators ignore: put the 32 px layer first in the .ico so the icon looks right during development.

Inside an .icns file

An .icns is a flat list of tagged chunks. It opens with an 8-byte header, the four ASCII bytes icns followed by the total file length as a big-endian 32-bit integer. After that come the elements. Each one has a 4-character type code, a big-endian length that includes its own 8-byte header, and then the image data.

You can see the header for yourself:

$ xxd -l 16 AppIcon.icns
00000000: 6963 6e73 0000 a4f5 6963 3132 0000 0149  icns....ic12...I

That’s icns, a file length of 0xA4F5 (42,229 bytes), and then the first element: ic12, 0x149 bytes long. The modern type codes look like this:

TypePixelsMeaningPayload
ic041616×16ARGB (or PNG)
ic053232×32ARGB (or PNG)
ic113216×16@2xPNG
ic126432×32@2xPNG
ic07128128×128PNG
ic13256128×128@2xPNG
ic08256256×256PNG
ic14512256×256@2xPNG
ic09512512×512PNG
ic101024512×512@2xPNG

Some pixel sizes show up twice: a 256 px image fills both ic08 (256 at 1x) and ic13 (128 at 2x). There are also older types, icp4, icp5 and icp6 (16, 32 and 48 px), which can hold PNG data. That’s where the first gotcha lives.

Gotcha 1: PNG data in the small slots shows up as noise

Writing an .icns encoder seems easy: render every size as PNG, give each one a tag, concatenate. For 64 px and up, that works. At 16 and 32 px it fails. Wikipedia’s ICNS article notes that PNG images in icp4/icp5 “will not display properly” when used as an application icon. electron-builder hit exactly this. Its 27.0.0-alpha.7 changelog bumps its icon toolset “so small ICNS sizes are written as ic04/ic05 ARGB instead of PNG-in-icp4/icp5/icp6, which macOS renders as noise at 16/32px (fixes corrupted Finder/DMG icons).”

The fix is to do what Apple’s own tool does. We ran iconutil on macOS 26.2 and listed the elements it wrote. Every slot holds PNG except ic04 and ic05, whose payloads start with the ASCII bytes ARGB. (It also adds an info element containing a property list.)

The ARGB format is simple. After the ARGB magic come four planes, not interleaved pixels: all the alpha bytes, then all the red, then green, then blue. Each plane is compressed with a PackBits-style run-length code:

  • A control byte from 0x00–0x7F means “copy the next n+1 bytes literally” (1 to 128 bytes).
  • A control byte from 0x80–0xFF means “repeat the next byte n−125 times” (3 to 130 copies).

Transparent margins and flat color compress well this way.

Building an .icns on a Mac

If you have a Mac, iconutil comes with the system. Put ten PNGs, named exactly like this, into a folder whose name ends in .iconset:

MyIcon.iconset/
  icon_16x16.png      [email protected]
  icon_32x32.png      [email protected]
  icon_128x128.png    [email protected]
  icon_256x256.png    [email protected]
  icon_512x512.png    [email protected]

Then run:

iconutil -c icns MyIcon.iconset

iconutil writes MyIcon.icns next to the folder, ARGB small sizes included. Running iconutil -c iconset MyIcon.icns goes the other way, which is handy for checking what’s inside an icon someone sent you.

What about macOS 26 and Icon Composer? Apple’s new tool makes a layered icon file that Xcode consumes directly. It’s a separate format, not a new kind of .icns. Electron, Tauri, and any app bundled outside Xcode still ship a classic .icns, so the steps above still apply.

Inside an .ico file

The .ico format is older and little-endian. Microsoft’s original documentation lays it out as three parts:

  1. ICONDIR (6 bytes): a reserved zero, a type (1 = icon), and the number of images.
  2. ICONDIRENTRY × N (16 bytes each): width, height, color count, reserved, planes, bit count, byte size, and the offset where that image’s data starts. Width and height are single bytes, so a value of 0 means 256.
  3. Image data, one blob per entry.

Each blob is one of two things. The classic option is a DIB: a BITMAPINFOHEADER followed by the color pixels (the “XOR mask”) and a 1-bit transparency “AND mask.” The header’s height is double the real height because it covers both masks stacked together, and forgetting to halve it is a classic parser bug. The other option, added in Windows Vista, is a complete PNG file embedded as-is, with no bitmap header, no doubled height, and no AND mask.

Gotcha 2: PNG frames and old readers

The format doesn’t limit PNG to any one size, but Microsoft’s guidance does. Its Windows design guidelines (written for Vista and 7) say “only a 32-bit copy of the 256x256 pixel image should be included, and only the 256x256 pixel image should be compressed.” The convention that followed is a PNG for 256 and uncompressed 32-bit bitmaps for everything smaller. Windows XP can’t read PNG frames, so keeping the small sizes as bitmaps keeps the file working there.

The bigger risk today is software that reads icons, not Windows itself. Microsoft documents that in .NET Framework 4.5.2 and earlier, Icon.ToBitmap throws an ArgumentOutOfRangeException if the icon contains PNG frames. Version 4.6 fixed it, but older apps and build tools built on that stack are still around. If an installer, resource editor, or legacy WinForms app rejects your icon, a PNG frame is a likely cause.

Sizes are the other half. Microsoft’s current app icon guidance says apps should include at least 16, 24, 32, 48 and 256 px, so that Windows only ever scales down. Windows looks for an exact match first, then shrinks the next size up. That’s why the Windows 11 scale table is worth reading: at 125% scaling, the title bar asks for 20 px, and at 250% it asks for 40. Without those sizes, Windows shrinks 24 to 20 or 48 to 40, and the icon goes soft. Adding 20, 40 and 64 (plus 96 for Start pins at 300%) covers the common scale steps with exact matches.

The design gotcha: 1024 px doesn’t shrink to 16 px gracefully

Even with a perfect container, a detailed 1024 px master shrunk to 16 px turns into a smudge. Microsoft’s guidelines say it plainly: “Simply scaling down from the 256x256 size does not work.” They tell designers to exaggerate the defining details as icons get smaller and to drop shadows at 24 px and below. Apple makes a related point: a 16×16@2x image is not a substitute for a true 32×32 design, even though both are 32 pixels wide.

A few practical steps help:

  • Check 16 and 32 px at actual size before shipping. If the mark isn’t recognizable, make a simplified version (thicker strokes, fewer elements) and use it for the smallest slots only.
  • Use a light sharpen on small downscales if your editor offers one. Smooth resampling softens edges.
  • Mind the shape difference. macOS icons have the shape built into the file: a rounded square with a transparent margin. Our generator places an 824 px rounded square on a 1024 px canvas. Windows has no required shape, so a full-bleed square or a free-form mark is fine. One master usually needs a macOS version and a Windows version.

How our tools handle it

The App Icon Generator builds both files from one image. Its macOS output includes an Xcode AppIcon.appiconset plus AppIcon.icns, written with the same ten element types as the table above. The 16 and 32 px slots are ic04/ic05 ARGB with PackBits planes, matching iconutil, and every other slot is PNG. The icon is drawn in the macOS rounded-square shape. Its Windows option writes app.ico with ten sizes: 16, 20, 24, 32, 40, 48, 64, 96, 128 and 256 px. That’s Microsoft’s minimum plus the Windows 11 scale steps.

To be upfront: every frame in our .ico files is PNG, including the small ones. Windows has read PNG frames since Vista, and PNG keeps files small. If you’re targeting XP, or a .NET Framework 4.5.2-era tool that reads icons, convert it with a tool that writes BMP frames for the small sizes. The App Icon Generator’s app.ico stores the 32 px frame first, the order Tauri asks for; the PNG → ICO converter keeps its frames in ascending size order, which Windows doesn’t care about either way.

If you just need a quick multi-size .ico from a logo, PNG to ICO lets you pick from 16, 32, 48, 64, 128 and 256 px (16/32/48 by default). It fits non-square images onto a transparent square canvas and shows each size before you download. Both tools run entirely in your browser.