Scaling Favicons


Scaling Favicons

When I redid my site to use Hugo I also redid my favicon. I wanted this favicon to also serve as the Add to Home Screen functionality provided by Chrome. I expected Chrome to automatically scale up the favicon. When I designed it I made it pixelated on purpose, so some very crude scaling was just what I wanted. Unfortunately Chrome doesn’t scale automatically, so my add to home screen icon was just a blue square with a white S set in the center.

It turns out these are called Touch Icons, and they’re more complicated than I expected. This blog post does a good job with an overview. Basically, Chrome uses a conventional <link rel="icon" sizes="192x192x" href="/touch-icon-192x192.png"> syntax and Apple uses a number of rel="apple-touch-icon-precomposed" with different sizes values.

The Problem

I wanted a simple command to scale up my favicon, keeping it pixelated. All the tools I could find to scale up an icon tried to smooth it out and ended up making it look trippy. First I used Preview to export the favicon as a PNG so that tools would have an easier time interacting with it.

ImageMagick’s convert command:

convert -resize 192x192 favicon.png favicon_imagemagick.png
imagemagick image

Scale2x’s scalex command, installed from their github, tried to round off the edges, which isn’t what I wanted.

scalex -k 4 favicon.png favicon_scalex.png
scale2x rounds the edges of the favicon

Note that because scalex only permits certain scaling factors, the inline image above is further scaled to 192x192 using the technique described below. The original output of the command had dimensions of 16 * 4 = 64, and can be seen here.

Can you do this with something like Inkscape or Preview? Probably, but it’s a lot of annoying dragging and dropping.

The Solution

Despite my best efforts, I couldn’t find a simple “scale and keep it looking pixelated” solution online. It must exist, possibly even as an ImageMagick filter, but that documentation is confusing. It was easy enough to make my own. All I needed to do was access the pixels of the PNG and amplify each one by my scaling factor, making it essentially take up a new square pseudo-pixel in the image. This new pseudo-pixel would be a square of factor true pixels on each side.

Prepare virtualenv

To access PNG byte’s I used PyPNG. I wasn’t able to install it globally for some reason, so I used virtualenv.

mkdir virtualenv_png
cd virtualenv_png
virtualenv .
source bin/activate
pip install pypng

Now you can import png. Don’t forget that to exit the virtual environment and get back to regular Python you need to type deactivate.

The Script

This script loads the PNG and amplifies each pixel as described above. The result is a perfectly scaled, pixelated icon.

# -*- coding: utf-8 -*-
"""
Scale a PNG by an integer factor, keeping it pixelated.
"""

import png
from sys import argv

# We're assuming rgba for for ints per pixel. Looking at the pypng docs there
# are other options this might be, but this is all I needed.
VAL_PER_PIXEL = 4  # rgba

if __name__ == '__main__':
    # we expect scale_factor, in_file, out_file
    if len(argv) != 4:
        raise ValueError('Usage: scale_factor, in_file, out_file')

    factor = int(argv[1])
    in_file = argv[2]
    out_file = argv[3]

    reader = png.Reader(in_file)
    content = reader.asDirect()
    rows = content[2]

    # content[2][0] assumes at least one row. There are original_width pixels in
    # this row, and each pixel has VAL_PER_PIXEL ints for each pixel.
    original_width = len(content[2][0]) / VAL_PER_PIXEL
    original_height = len(content[2])

    out_width = original_width * factor
    out_height = original_height * factor

    all_rows = []

    # Iterate over each row. Each pixel is represented by several ints (the
    # exact number depends on the file and is defined by VAL_PER_PIXEL). Each
    # pixel must be grown by factor, and each row must then be appended factor
    # number of times to all_rows.
    for row in rows:
        new_row = []
        for num_pixel in range(len(row) / VAL_PER_PIXEL):
            # / 4 for rgba
            # if a single pixel row was [255, 128, 25, 0], we would want to
            # expand that times 4.
            pixel_start = num_pixel * VAL_PER_PIXEL
            pixel_values = row[pixel_start:pixel_start + VAL_PER_PIXEL]
            larger_values = list(pixel_values) * factor
            new_row = new_row + larger_values
        for i in range(factor):
            # we've enlarged the row, now add it.
            all_rows.append(new_row)

    with open(out_file, 'w') as f:
        # alpha = True for rgba. This will have to be updated for PNGs without
        # alpha.
        w = png.Writer(out_width, out_height, alpha=True)
        w.write(f, all_rows)

With this script, we can take the 16x16 favicon png and scale it up by a factor of 12. First copy or download the script and save it in your virtualenv directory that has pypng installed, then run it. In this case, we’ve saved the scrip as scale.py. Here is the same script in a gist. Note that I’ve only tested this with my single favicon, but for that it works just fine. PNGs in general, non-square PNGs, etc – no promises.

python scale.py 12 favicon.png favicon_scaled.png

And there we have it:

favicon scaled up with script

Other Sizes

Now that we have the largest size and we know it’s perfect, we can use ImageMagick to convert to the other sizes. 120x120, or instance, is:

convert -resize 120x120 favicon_scaled.png favicon_120.png

Then add the <link> tags to your <head> and be happy they’re so pretty.


comments powered by Disqus