]> asedeno.scripts.mit.edu Git - PuTTY.git/commitdiff
New script to generate OS X icon files.
authorSimon Tatham <anakin@pobox.com>
Sun, 6 Sep 2015 08:50:09 +0000 (09:50 +0100)
committerSimon Tatham <anakin@pobox.com>
Sun, 6 Sep 2015 09:12:15 +0000 (10:12 +0100)
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.

.gitignore
icons/Makefile
icons/macicon.py [new file with mode: 0755]
macosx/putty.icns [deleted file]
mksrcarc.sh

index bea01d86130ffce1d57dcdfc452e12abebee61ee..48dceab4417f37935c8567297880e8c86fb04b16 100644 (file)
@@ -80,6 +80,7 @@
 /doc/*.hhk
 /icons/*.png
 /icons/*.ico
+/icons/*.icns
 /icons/*.xpm
 /icons/*.c
 /macosx/Makefile
index 1074303eff506e313307e1190ccdb0b3563dc98f..5e845b27c88929b434ba81cb23cad7c291259e74 100644 (file)
@@ -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 (executable)
index 0000000..9dfc87f
--- /dev/null
@@ -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 (file)
index 72eab29..0000000
Binary files a/macosx/putty.icns and /dev/null differ
index 87327df7c8d97c8b6b47330126835bcf9d585816..a8b01b5bbeed7f84934d15ee7e29081fdc52a8b7 100755 (executable)
@@ -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() {