DEV Community

Hardi
Hardi

Posted on

PNG to ICO Converter: The Complete Guide to Favicon and Windows Icon Creation

Favicons. Desktop icons. Browser tabs. App shortcuts. These tiny images are everywhere, yet many developers still struggle with creating proper ICO files. Let's dive into the technical details of PNG to ICO conversion and why it matters more than you might think.

Why ICO Format Still Matters in 2025

You might be thinking: "Can't I just use a PNG for my favicon?" Well, yes and no. While modern browsers support PNG favicons, ICO files have unique advantages:

Multi-Resolution in One File

favicon.ico contains:
├─ 16x16 pixels (browser tabs)
├─ 32x32 pixels (bookmark bars)
├─ 48x48 pixels (Windows desktop)
└─ 64x64 pixels (Windows taskbar)

Total: 1 file, 4 sizes = ~10KB
Enter fullscreen mode Exit fullscreen mode

Compared to:

<!-- PNG approach: Multiple files -->
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="48x48" href="/favicon-48x48.png">
<!-- Requires 3+ separate HTTP requests -->
Enter fullscreen mode Exit fullscreen mode

Browser Compatibility

ICO Support:

  • Internet Explorer (all versions)
  • Edge (all versions)
  • Chrome, Firefox, Safari (all versions)
  • Literally every browser since 1999

PNG Favicon Support:

  • Modern browsers only
  • Some mobile browsers inconsistent
  • Older systems ignore it completely

Windows Integration

Windows looks for .ico files for:
├─ Desktop shortcuts
├─ Taskbar icons
├─ Start menu tiles
├─ File explorer icons
└─ System tray applications
Enter fullscreen mode Exit fullscreen mode

PNG won't work for native Windows applications. Only ICO.

Understanding ICO File Format

Technical Structure

ICO File Format:
┌─────────────────────────────┐
│ Header (6 bytes)            │
├─────────────────────────────┤
│ Directory Entries           │
│ ├─ Image 1 (16 bytes)      │
│ ├─ Image 2 (16 bytes)      │
│ └─ Image N (16 bytes)      │
├─────────────────────────────┤
│ Image Data                  │
│ ├─ Bitmap/PNG data 1       │
│ ├─ Bitmap/PNG data 2       │
│ └─ Bitmap/PNG data N       │
└─────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

What Makes ICO Special

  1. Multiple images in one file: Different resolutions for different contexts
  2. Both BMP and PNG: Modern ICO files can embed PNG data
  3. Transparency support: Alpha channel for non-rectangular icons
  4. Indexed and true color: Supports various color depths

When You Need PNG to ICO Conversion

1. Website Favicons

The Old Way (incorrect):

<link rel="icon" href="logo.png">
<!-- Works in modern browsers, fails in IE/older systems -->
Enter fullscreen mode Exit fullscreen mode

The Right Way:

<link rel="icon" href="/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
Enter fullscreen mode Exit fullscreen mode

The .ico file is your fallback for maximum compatibility.

2. Electron Applications

// main.js - Electron app
const { app, BrowserWindow } = require('electron');
const path = require('path');

function createWindow() {
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    icon: path.join(__dirname, 'assets/icon.ico'), // ICO required for Windows
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  });
}

app.whenReady().then(() => {
  createWindow();
});
Enter fullscreen mode Exit fullscreen mode

Platform requirements:

  • Windows: .ico file
  • macOS: .icns file
  • Linux: .png file

3. Progressive Web Apps (PWA)

// manifest.json
{
  "name": "My App",
  "short_name": "App",
  "icons": [
    {
      "src": "/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "theme_color": "#ffffff",
  "background_color": "#ffffff",
  "display": "standalone"
}
Enter fullscreen mode Exit fullscreen mode

Plus you still need favicon.ico in your root directory for browser compatibility.

4. Desktop Applications

Any Windows desktop app (C++, C#, Java, Python with GUI) needs ICO files:

// C# WinForms example
this.Icon = new Icon("app_icon.ico");

// Or load from resources
this.Icon = Properties.Resources.ApplicationIcon;
Enter fullscreen mode Exit fullscreen mode

Methods for PNG to ICO Conversion

1. Command Line with ImageMagick

# Install ImageMagick
sudo apt-get install imagemagick  # Linux
brew install imagemagick          # macOS

# Basic conversion
convert input.png output.ico

# Multiple sizes in one ICO
convert input.png -define icon:auto-resize=64,48,32,16 output.ico

# With transparency
convert input.png -background none -define icon:auto-resize=256,128,64,48,32,16 favicon.ico

# Specific sizes
convert icon-16.png icon-32.png icon-48.png icon-64.png favicon.ico
Enter fullscreen mode Exit fullscreen mode

Pro tip: Create different sized PNGs first for better quality:

# Generate multiple sizes
for size in 16 32 48 64; do
  convert input.png -resize ${size}x${size} icon-${size}.png
done

# Combine into ICO
convert icon-*.png -colors 256 favicon.ico
Enter fullscreen mode Exit fullscreen mode

2. Node.js Implementation

const sharp = require('sharp');
const toIco = require('to-ico');
const fs = require('fs');

async function pngToIco(inputPath, outputPath) {
  try {
    // Generate multiple sizes
    const sizes = [16, 32, 48, 64];
    const buffers = await Promise.all(
      sizes.map(size =>
        sharp(inputPath)
          .resize(size, size, {
            fit: 'contain',
            background: { r: 0, g: 0, b: 0, alpha: 0 }
          })
          .png()
          .toBuffer()
      )
    );

    // Create ICO file
    const icoBuffer = await toIco(buffers);
    fs.writeFileSync(outputPath, icoBuffer);

    console.log(`✓ ICO created: ${outputPath}`);
    return outputPath;
  } catch (error) {
    console.error('Conversion error:', error);
    throw error;
  }
}

// Usage
pngToIco('./logo.png', './favicon.ico');
Enter fullscreen mode Exit fullscreen mode

3. Express API Endpoint

const express = require('express');
const multer = require('multer');
const sharp = require('sharp');
const toIco = require('to-ico');

const app = express();
const upload = multer({ storage: multer.memoryStorage() });

app.post('/api/convert-to-ico', upload.single('image'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'No file uploaded' });
    }

    // Generate ICO with multiple sizes
    const sizes = [16, 24, 32, 48, 64];
    const buffers = await Promise.all(
      sizes.map(size =>
        sharp(req.file.buffer)
          .resize(size, size, {
            fit: 'contain',
            background: { r: 0, g: 0, b: 0, alpha: 0 }
          })
          .png()
          .toBuffer()
      )
    );

    const icoBuffer = await toIco(buffers);

    // Send as download
    res.set({
      'Content-Type': 'image/x-icon',
      'Content-Disposition': 'attachment; filename="favicon.ico"',
      'Content-Length': icoBuffer.length
    });

    res.send(icoBuffer);
  } catch (error) {
    console.error('Conversion error:', error);
    res.status(500).json({ error: 'Conversion failed' });
  }
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
Enter fullscreen mode Exit fullscreen mode

4. Python Implementation

from PIL import Image
import struct

def create_ico(png_path, ico_path, sizes=[16, 32, 48, 64]):
    """Convert PNG to ICO with multiple sizes"""
    img = Image.open(png_path)

    # Ensure RGBA mode for transparency
    if img.mode != 'RGBA':
        img = img.convert('RGBA')

    # Create images at different sizes
    images = []
    for size in sizes:
        resized = img.resize((size, size), Image.Resampling.LANCZOS)
        images.append(resized)

    # Save as ICO
    images[0].save(
        ico_path,
        format='ICO',
        sizes=[(s, s) for s in sizes],
        append_images=images[1:]
    )

    print(f"✓ ICO created: {ico_path}")

# Usage
create_ico('logo.png', 'favicon.ico', [16, 32, 48, 64])
Enter fullscreen mode Exit fullscreen mode

5. Quick Online Conversion

For rapid development or client projects where you need ICO files quickly, using a PNG to ICO converter streamlines the process. This is particularly useful when:

  • Prototyping: Need favicons quickly for demos
  • Client assets: Converting logos they provide
  • Quick fixes: Updating favicons without rebuilding
  • Testing: Trying different icon variations

Once you've validated your icons work correctly, you can integrate automated conversion into your build pipeline.

Best Practices for ICO Creation

1. Design for Small Sizes

// Bad: Detailed logo won't work at 16x16
const complexLogo = {
  text: "MyCompany",
  tagline: "Innovation in Tech",
  details: ["Est. 2020", "Premium Quality"]
};

// Good: Simple, recognizable shape
const simpleIcon = {
  shape: "M", // First letter
  style: "bold",
  background: "#007bff"
};
Enter fullscreen mode Exit fullscreen mode

Tips:

  • Use bold, simple shapes
  • High contrast colors
  • Remove fine details at small sizes
  • Test at 16x16 pixels first

2. Optimize Each Size Separately

// Don't just scale - optimize for each size
async function createOptimizedIco(sourcePng) {
  const sizes = [
    { size: 16, detail: 'minimal', strokeWidth: 2 },
    { size: 32, detail: 'low', strokeWidth: 2 },
    { size: 48, detail: 'medium', strokeWidth: 1.5 },
    { size: 64, detail: 'high', strokeWidth: 1 }
  ];

  const buffers = await Promise.all(
    sizes.map(config => generateOptimizedVersion(sourcePng, config))
  );

  return toIco(buffers);
}
Enter fullscreen mode Exit fullscreen mode

3. Maintain Transparency

// Ensure transparency is preserved
async function convertWithTransparency(input, output) {
  const buffers = await Promise.all(
    [16, 32, 48, 64].map(size =>
      sharp(input)
        .resize(size, size, {
          fit: 'contain',
          background: { r: 0, g: 0, b: 0, alpha: 0 } // Transparent
        })
        .png({ compressionLevel: 9 })
        .toBuffer()
    )
  );

  const ico = await toIco(buffers);
  fs.writeFileSync(output, ico);
}
Enter fullscreen mode Exit fullscreen mode

4. File Size Considerations

// Check ICO file size
async function analyzeIcoSize(icoPath) {
  const stats = fs.statSync(icoPath);
  const sizeKB = (stats.size / 1024).toFixed(2);

  if (stats.size > 50000) { // 50KB
    console.warn(`⚠️ ICO is large: ${sizeKB}KB`);
    console.log('Consider:');
    console.log('- Reducing color count');
    console.log('- Using fewer sizes');
    console.log('- Optimizing source PNG');
  } else {
    console.log(`✓ ICO size: ${sizeKB}KB`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Automated Build Integration

Webpack Plugin

// webpack.config.js
const path = require('path');
const sharp = require('sharp');
const toIco = require('to-ico');

class FaviconPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync('FaviconPlugin', async (compilation, callback) => {
      try {
        const sourcePath = path.resolve(__dirname, 'src/assets/logo.png');

        // Generate ICO
        const buffers = await Promise.all(
          [16, 32, 48].map(size =>
            sharp(sourcePath)
              .resize(size, size)
              .png()
              .toBuffer()
          )
        );

        const ico = await toIco(buffers);

        // Add to compilation
        compilation.assets['favicon.ico'] = {
          source: () => ico,
          size: () => ico.length
        };

        callback();
      } catch (error) {
        callback(error);
      }
    });
  }
}

module.exports = {
  // ... other config
  plugins: [
    new FaviconPlugin()
  ]
};
Enter fullscreen mode Exit fullscreen mode

Gulp Task

const gulp = require('gulp');
const sharp = require('sharp');
const toIco = require('to-ico');
const through2 = require('through2');

gulp.task('generate-favicon', () => {
  return gulp.src('src/logo.png')
    .pipe(through2.obj(async (file, _, cb) => {
      if (file.isBuffer()) {
        try {
          const buffers = await Promise.all(
            [16, 32, 48, 64].map(size =>
              sharp(file.contents)
                .resize(size, size)
                .png()
                .toBuffer()
            )
          );

          const ico = await toIco(buffers);
          file.contents = ico;
          file.extname = '.ico';

          cb(null, file);
        } catch (error) {
          cb(error);
        }
      }
    }))
    .pipe(gulp.dest('dist'));
});
Enter fullscreen mode Exit fullscreen mode

npm Script Automation

{
  "scripts": {
    "build:favicon": "node scripts/generate-favicon.js",
    "build": "npm run build:favicon && webpack",
    "prebuild": "npm run build:favicon"
  }
}
Enter fullscreen mode Exit fullscreen mode
// scripts/generate-favicon.js
const sharp = require('sharp');
const toIco = require('to-ico');
const fs = require('fs');

(async () => {
  console.log('Generating favicon.ico...');

  const buffers = await Promise.all(
    [16, 32, 48, 64].map(size =>
      sharp('src/assets/logo.png')
        .resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
        .png()
        .toBuffer()
    )
  );

  const ico = await toIco(buffers);
  fs.writeFileSync('public/favicon.ico', ico);

  console.log('✓ favicon.ico generated successfully');
})();
Enter fullscreen mode Exit fullscreen mode

Complete Favicon Setup

Modern Cross-Platform Approach

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <!-- ICO for legacy browsers -->
  <link rel="icon" href="/favicon.ico" sizes="any">

  <!-- SVG for modern browsers -->
  <link rel="icon" href="/favicon.svg" type="image/svg+xml">

  <!-- PNG for various sizes -->
  <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 -->
  <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">

  <!-- Android Chrome -->
  <link rel="manifest" href="/site.webmanifest">

  <!-- Microsoft Tiles -->
  <meta name="msapplication-TileColor" content="#da532c">
  <meta name="msapplication-config" content="/browserconfig.xml">

  <meta name="theme-color" content="#ffffff">

  <title>My App</title>
</head>
<body>
  <!-- Content -->
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Complete Build Script

const sharp = require('sharp');
const toIco = require('to-ico');
const fs = require('fs').promises;

async function generateAllIcons(sourcePng) {
  const output = {
    ico: [],
    png: [],
    apple: null
  };

  // Generate ICO (16, 32, 48, 64)
  const icoBuffers = await Promise.all(
    [16, 32, 48, 64].map(size =>
      sharp(sourcePng)
        .resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
        .png()
        .toBuffer()
    )
  );
  const ico = await toIco(icoBuffers);
  await fs.writeFile('public/favicon.ico', ico);
  console.log('✓ favicon.ico');

  // Generate PNG favicons
  await sharp(sourcePng)
    .resize(32, 32)
    .png()
    .toFile('public/favicon-32x32.png');
  console.log('✓ favicon-32x32.png');

  await sharp(sourcePng)
    .resize(16, 16)
    .png()
    .toFile('public/favicon-16x16.png');
  console.log('✓ favicon-16x16.png');

  // Generate Apple Touch Icon
  await sharp(sourcePng)
    .resize(180, 180)
    .png()
    .toFile('public/apple-touch-icon.png');
  console.log('✓ apple-touch-icon.png');

  // Generate Android icons
  await sharp(sourcePng)
    .resize(192, 192)
    .png()
    .toFile('public/android-chrome-192x192.png');
  console.log('✓ android-chrome-192x192.png');

  await sharp(sourcePng)
    .resize(512, 512)
    .png()
    .toFile('public/android-chrome-512x512.png');
  console.log('✓ android-chrome-512x512.png');

  console.log('\n🎉 All icons generated successfully!');
}

// Run
generateAllIcons('src/logo.png').catch(console.error);
Enter fullscreen mode Exit fullscreen mode

Testing Your ICO Files

// Verify ICO file structure
function verifyIco(icoPath) {
  const buffer = fs.readFileSync(icoPath);

  // Check magic number (first 4 bytes should be 00 00 01 00)
  if (buffer[0] !== 0 || buffer[1] !== 0 || buffer[2] !== 1 || buffer[3] !== 0) {
    throw new Error('Invalid ICO file format');
  }

  // Get number of images
  const numImages = buffer.readUInt16LE(4);
  console.log(`ICO contains ${numImages} image(s)`);

  // Parse directory entries
  for (let i = 0; i < numImages; i++) {
    const offset = 6 + (i * 16);
    const width = buffer[offset] || 256;
    const height = buffer[offset + 1] || 256;
    const colorCount = buffer[offset + 2];
    const planes = buffer.readUInt16LE(offset + 4);
    const bitCount = buffer.readUInt16LE(offset + 6);
    const size = buffer.readUInt32LE(offset + 8);

    console.log(`  Image ${i + 1}: ${width}x${height}, ${bitCount}-bit, ${size} bytes`);
  }
}

verifyIco('favicon.ico');
Enter fullscreen mode Exit fullscreen mode

Common Issues and Solutions

Issue 1: Transparency Lost

// Problem: ICO shows black background instead of transparency

// Solution: Ensure PNG has alpha channel
async function fixTransparency(input, output) {
  await sharp(input)
    .ensureAlpha() // Add alpha channel if missing
    .png()
    .toFile('temp.png');

  // Then convert to ICO
  await pngToIco('temp.png', output);
}
Enter fullscreen mode Exit fullscreen mode

Issue 2: Blurry Icons

// Problem: Icons look blurry at small sizes

// Solution: Design or optimize for each size separately
const sizes = {
  16: { blur: 0, sharpen: 2 },
  32: { blur: 0, sharpen: 1.5 },
  48: { blur: 0, sharpen: 1 },
  64: { blur: 0, sharpen: 0.5 }
};

async function generateSharpIcons(input) {
  const buffers = await Promise.all(
    Object.entries(sizes).map(([size, config]) =>
      sharp(input)
        .resize(parseInt(size), parseInt(size))
        .sharpen(config.sharpen)
        .png()
        .toBuffer()
    )
  );

  return toIco(buffers);
}
Enter fullscreen mode Exit fullscreen mode

Issue 3: File Size Too Large

// Problem: ICO file is 100KB+ (should be <50KB)

// Solution: Reduce colors and optimize
async function optimizeIco(input, output) {
  const buffers = await Promise.all(
    [16, 32, 48].map(size => // Use fewer sizes
      sharp(input)
        .resize(size, size)
        .png({
          compressionLevel: 9,
          palette: true // Use indexed color
        })
        .toBuffer()
    )
  );

  const ico = await toIco(buffers);
  fs.writeFileSync(output, ico);

  const sizeKB = (ico.length / 1024).toFixed(2);
  console.log(`Optimized ICO: ${sizeKB}KB`);
}
Enter fullscreen mode Exit fullscreen mode

Performance Impact

// Benchmark different approaches
async function benchmarkConversion(pngPath) {
  console.time('ImageMagick');
  exec(`convert ${pngPath} -define icon:auto-resize=16,32,48 favicon1.ico`);
  console.timeEnd('ImageMagick');

  console.time('Sharp + to-ico');
  await pngToIco(pngPath, 'favicon2.ico');
  console.timeEnd('Sharp + to-ico');

  console.time('Pillow (Python)');
  exec(`python convert.py ${pngPath} favicon3.ico`);
  console.timeEnd('Pillow (Python)');
}

// Results (typical):
// ImageMagick: 250ms
// Sharp + to-ico: 120ms (fastest)
// Pillow: 180ms
Enter fullscreen mode Exit fullscreen mode

Conclusion: ICO Files Still Matter

Despite being a decades-old format, ICO files remain essential for web development and desktop applications. They provide:

Universal compatibility across all browsers and Windows versions

Multi-resolution support in a single file

Smaller total size compared to multiple PNG files

Native Windows integration for desktop apps

Quick Checklist:

  • [ ] Create ICO with sizes: 16, 32, 48, 64
  • [ ] Preserve transparency
  • [ ] Keep file size under 50KB
  • [ ] Test in multiple browsers
  • [ ] Include in build pipeline
  • [ ] Place in website root as /favicon.ico

Start converting your PNGs to ICO today and ensure your brand looks perfect everywhere, from browser tabs to Windows desktops.


How do you handle favicons in your projects? Share your setup in the comments!

webdev #favicon #frontend #windows #icons

Top comments (0)