Skip to content

Commit

Permalink
New script to generate OS X icon files.
Browse files Browse the repository at this point in the history
The Xcode icon composer doesn't seem to exist any more in modern
versions of Xcode, or at least if it does then it's well hidden and
certainly doesn't live at the top-level path at /Developer where web
pages still claim it can be found.

There is a free software 'libicns' and associated command-line tools,
but they're large, complicated, picky about the exact format of PNGs
they get as input, and in any case a needless extra build dependency
when it turns out the important parts of the file format can be done
in a few dozen lines of Python. So here's a new macicon.py, and
icons/Makefile additions to build a demo icon for OS X PuTTY, as and
when I finally get it working.

Also I've deleted the static icon file in the neglected 'macosx'
source directory, because this one is better anyway - the old one was
appalling quality, and must have been autogenerated from a single
image in some way.
  • Loading branch information
sgtatham committed Sep 6, 2015
1 parent 82824e1 commit b08895f
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 4 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
/doc/*.hhk
/icons/*.png
/icons/*.ico
/icons/*.icns
/icons/*.xpm
/icons/*.c
/macosx/Makefile
Expand Down
18 changes: 15 additions & 3 deletions icons/Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Makefile for the PuTTY icon suite.

ICONS = putty puttycfg puttygen pscp pageant pterm ptermcfg puttyins
SIZES = 16 32 48
SIZES = 16 32 48 128

MODE = # override to -it on command line for opaque testing

Expand All @@ -11,17 +11,19 @@ TRUEPNGS = $(foreach I,$(ICONS),$(foreach S,$(SIZES),$(I)-$(S)-true.png))

ICOS = putty.ico puttygen.ico pscp.ico pageant.ico pageants.ico puttycfg.ico \
puttyins.ico
ICNS = PuTTY.icns
CICONS = xpmputty.c xpmpucfg.c xpmpterm.c xpmptcfg.c

base: icos cicons

all: pngs monopngs base # truepngs currently disabled by default
all: pngs monopngs base icns # truepngs currently disabled by default

pngs: $(PNGS)
monopngs: $(MONOPNGS)
truepngs: $(TRUEPNGS)

icos: $(ICOS)
icns: $(ICNS)
cicons: $(CICONS)

install: icos cicons
Expand Down Expand Up @@ -88,5 +90,15 @@ xpmpterm.c: pterm-16.png pterm-32.png pterm-48.png
xpmptcfg.c: ptermcfg-16.png ptermcfg-32.png ptermcfg-48.png
./cicon.pl cfg_icon $^ > $@

PuTTY.icns: putty-16-mono.png putty-16.png \
putty-32-mono.png putty-32.png \
putty-48-mono.png putty-48.png \
putty-128.png
./macicon.py mono:putty-16-mono.png colour:putty-16.png \
mono:putty-32-mono.png colour:putty-32.png \
mono:putty-48-mono.png colour:putty-48.png \
colour:putty-128.png \
output:$@

clean:
rm -f *.png *.ico *.c
rm -f *.png *.ico *.icns *.c
150 changes: 150 additions & 0 deletions icons/macicon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#!/usr/bin/env python

# Generate Mac OS X .icns files, or at least the simple subformats
# that don't involve JPEG encoding and the like.
#
# Sources: https://en.wikipedia.org/wiki/Apple_Icon_Image_format and
# some details implicitly documented by the source code of 'libicns'.

import sys
import struct
import subprocess

# The file format has a typical IFF-style (type, length, data) chunk
# structure, with one outer chunk containing subchunks for various
# different icon sizes and formats.
def make_chunk(chunkid, data):
assert len(chunkid) == 4
return chunkid + struct.pack(">I", len(data) + 8) + data

# Monochrome icons: a single chunk containing a 1 bpp image followed
# by a 1 bpp transparency mask. Both uncompressed, unless you count
# packing the bits into bytes.
def make_mono_icon(size, rgba):
assert len(rgba) == size * size

# We assume our input image was monochrome, so that the R,G,B
# channels are all the same; we want the image and then the mask,
# so we take the R channel followed by the alpha channel. However,
# we have to flip the former, because in the output format the
# image has 0=white and 1=black, while the mask has 0=transparent
# and 1=opaque.
pixels = [rgba[index][chan] ^ flip for (chan, flip) in [(0,0xFF),(3,0)]
for index in range(len(rgba))]

# Encode in 1-bit big-endian format.
data = ""
for i in range(0, len(pixels), 8):
byte = 0
for j in range(8):
if pixels[i+j] >= 0x80:
byte |= 0x80 >> j
data += chr(byte)

# This size-32 chunk id is an anomaly in what would otherwise be a
# consistent system of using {s,l,h,t} for {16,32,48,128}-pixel
# icon sizes.
chunkid = { 16: "ics#", 32: "ICN#", 48: "ich#" }[size]
return make_chunk(chunkid, data)

# Mask for full-colour icons: a chunk containing an 8 bpp alpha
# bitmap, uncompressed. The RGB data appears in a separate chunk.
def make_colour_mask(size, rgba):
assert len(rgba) == size * size

data = "".join(map(lambda pix: chr(pix[3]), rgba))

chunkid = { 16: "s8mk", 32: "l8mk", 48: "h8mk", 128: "t8mk" }[size]
return make_chunk(chunkid, data)

# Helper routine for deciding when to start and stop run-length
# encoding.
def runof3(string, position):
return (position < len(string) and
string[position:position+3] == string[position] * 3)

# RGB data for full-colour icons: a chunk containing 8 bpp red, green
# and blue images, each run-length encoded (see comment inside the
# function), and then concatenated.
def make_colour_icon(size, rgba):
assert len(rgba) == size * size

data = ""

# Mysterious extra zero header word appearing only in the size-128
# icon chunk. libicns doesn't know what it's for, and neither do
# I.
if size == 128:
data += "\0\0\0\0"

# Handle R,G,B channels in sequence. (Ignore the alpha channel; it
# goes into the separate mask chunk constructed above.)
for chan in range(3):
pixels = "".join([chr(rgba[index][chan])
for index in range(len(rgba))])

# Run-length encode each channel using the following format:
# * byte 0x80-0xFF followed by one literal byte means repeat
# that byte 3-130 times
# * byte 0x00-0x7F followed by n+1 literal bytes means emit
# those bytes once each.
pos = 0
while pos < len(pixels):
start = pos
if runof3(pixels, start):
pos += 3
pixval = pixels[start]
while (pos - start < 130 and
pos < len(pixels) and
pixels[pos] == pixval):
pos += 1
data += chr(0x80 + pos-start - 3) + pixval
else:
while (pos - start < 128 and
pos < len(pixels) and
not runof3(pixels, pos)):
pos += 1
data += chr(0x00 + pos-start - 1) + pixels[start:pos]

chunkid = { 16: "is32", 32: "il32", 48: "ih32", 128: "it32" }[size]
return make_chunk(chunkid, data)

# Load an image file from disk and turn it into a simple list of
# 4-tuples giving 8-bit R,G,B,A values for each pixel.
#
# My icon-building makefile already depends on ImageMagick, so I use
# identify and convert here in place of more sensible Python libraries
# so as to add no build dependency that wasn't already needed.
def load_rgba(filename):
size = subprocess.check_output(["identify", "-format", "%wx%h", filename])
width, height = map(int, size.split("x"))
assert width == height
data = subprocess.check_output(["convert", "-depth", "8",
filename, "rgba:-"])
assert len(data) == width*height*4
rgba = [map(ord, data[i:i+4]) for i in range(0, len(data), 4)]
return width, rgba

data = ""

# Trivial argument format: each argument is a filename prefixed with
# "mono:", "colour:" or "output:". The first two indicate image files
# to use as part of the icon, and the last gives the output file name.
# Icon subformat chunks are written out in the order of the arguments.
for arg in sys.argv[1:]:
kind, filename = arg.split(":", 2)
if kind == "output":
outfile = filename
else:
size, rgba = load_rgba(filename)
if kind == "mono":
data += make_mono_icon(size, rgba)
elif kind == "colour":
data += make_colour_icon(size, rgba) + make_colour_mask(size, rgba)
else:
assert False, "bad argument '%s'" % arg

data = make_chunk("icns", data)

with open(outfile, "w") as f:
f.write(data)
Binary file removed macosx/putty.icns
Binary file not shown.
2 changes: 1 addition & 1 deletion mksrcarc.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ text=`{ find . -name CVS -prune -o \
# files.
bintext=testdata/*.txt
# These are actual binary files which we don't want transforming.
bin=`{ ls -1 windows/*.ico windows/putty.iss windows/website.url macosx/*.icns; \
bin=`{ ls -1 windows/*.ico windows/putty.iss windows/website.url; \
find . -name '*.dsp' -print -o -name '*.dsw' -print; }`

verbosely() {
Expand Down

0 comments on commit b08895f

Please sign in to comment.