diff --git a/.gitignore b/.gitignore index bea01d861..48dceab44 100644 --- a/.gitignore +++ b/.gitignore @@ -80,6 +80,7 @@ /doc/*.hhk /icons/*.png /icons/*.ico +/icons/*.icns /icons/*.xpm /icons/*.c /macosx/Makefile diff --git a/icons/Makefile b/icons/Makefile index 1074303ef..5e845b27c 100644 --- a/icons/Makefile +++ b/icons/Makefile @@ -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 @@ -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 @@ -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 diff --git a/icons/macicon.py b/icons/macicon.py new file mode 100755 index 000000000..9dfc87ffd --- /dev/null +++ b/icons/macicon.py @@ -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) diff --git a/macosx/putty.icns b/macosx/putty.icns deleted file mode 100644 index 72eab295e..000000000 Binary files a/macosx/putty.icns and /dev/null differ diff --git a/mksrcarc.sh b/mksrcarc.sh index 87327df7c..a8b01b5bb 100755 --- a/mksrcarc.sh +++ b/mksrcarc.sh @@ -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() {