--- /dev/null
+#!/usr/bin/perl
+
+# Take a collection of input image files and convert them into a
+# multi-resolution Windows .ICO icon file.
+#
+# The input images can be treated as having four different colour
+# depths:
+#
+# - 24-bit true colour
+# - 8-bit with custom palette
+# - 4-bit using the Windows 16-colour palette (see comment below
+# for details)
+# - 1-bit using black and white only.
+#
+# The images can be supplied in any input format acceptable to
+# ImageMagick, but their actual colour usage must already be
+# appropriate for the specified mode; this script will not do any
+# substantive conversion. So if an image intended to be used in 4-
+# or 1-bit mode contains any colour not in the appropriate fixed
+# palette, that's a fatal error; if an image to be used in 8-bit
+# mode contains more than 256 distinct colours, that's also a fatal
+# error.
+#
+# Command-line syntax is:
+#
+# icon.pl -depth imagefile [imagefile...] [-depth imagefile [imagefile...]]
+#
+# where `-depth' is one of `-24', `-8', `-4' or `-1', and tells the
+# script how to treat all the image files given after that option
+# until the next depth option. For example, you might execute
+#
+# icon.pl -24 48x48x24.png 32x32x24.png -8 32x32x8.png -1 monochrome.png
+#
+# to build an icon file containing two differently sized 24-bit
+# images, one 8-bit image and one black and white image.
+#
+# Windows .ICO files support a 1-bit alpha channel on all these
+# image types. That is, any pixel can be either opaque or fully
+# transparent, but not partially transparent. The alpha channel is
+# separate from the main image data, meaning that `transparent' is
+# not required to take up a palette entry. (So an 8-bit image can
+# have 256 distinct _opaque_ colours, plus transparent pixels as
+# well.) If the input images have alpha channels, they will be used
+# to determine which pixels of the icon are transparent, by simple
+# quantisation half way up (e.g. in a PNG image with an 8-bit alpha
+# channel, alpha values of 00-7F will be mapped to transparent
+# pixels, and 80-FF will become opaque).
+
+# The Windows 16-colour palette consists of:
+# - the eight corners of the colour cube (000000, 0000FF, 00FF00,
+# 00FFFF, FF0000, FF00FF, FFFF00, FFFFFF)
+# - dim versions of the seven non-black corners, at 128/255 of the
+# brightness (000080, 008000, 008080, 800000, 800080, 808000,
+# 808080)
+# - light grey at 192/255 of full brightness (C0C0C0).
+%win16pal = (
+ "\x00\x00\x00\x00" => 0,
+ "\x00\x00\x80\x00" => 1,
+ "\x00\x80\x00\x00" => 2,
+ "\x00\x80\x80\x00" => 3,
+ "\x80\x00\x00\x00" => 4,
+ "\x80\x00\x80\x00" => 5,
+ "\x80\x80\x00\x00" => 6,
+ "\xC0\xC0\xC0\x00" => 7,
+ "\x80\x80\x80\x00" => 8,
+ "\x00\x00\xFF\x00" => 9,
+ "\x00\xFF\x00\x00" => 10,
+ "\x00\xFF\xFF\x00" => 11,
+ "\xFF\x00\x00\x00" => 12,
+ "\xFF\x00\xFF\x00" => 13,
+ "\xFF\xFF\x00\x00" => 14,
+ "\xFF\xFF\xFF\x00" => 15,
+);
+@win16pal = sort { $win16pal{$a} <=> $win16pal{$b} } keys %win16pal;
+
+# The black and white palette consists of black (000000) and white
+# (FFFFFF), obviously.
+%win2pal = (
+ "\x00\x00\x00\x00" => 0,
+ "\xFF\xFF\xFF\x00" => 1,
+);
+@win2pal = sort { $win16pal{$a} <=> $win2pal{$b} } keys %win2pal;
+
+@hdr = ();
+@dat = ();
+
+$depth = undef;
+foreach $_ (@ARGV) {
+ if (/^-(24|8|4|1)$/) {
+ $depth = $1;
+ } elsif (defined $depth) {
+ &readicon($_, $depth);
+ } else {
+ $usage = 1;
+ }
+}
+if ($usage || length @hdr == 0) {
+ print "usage: icon.pl ( -24 | -8 | -4 | -1 ) image [image...]\n";
+ print " [ ( -24 | -8 | -4 | -1 ) image [image...] ...]\n";
+ exit 0;
+}
+
+# Now write out the output icon file.
+print pack "vvv", 0, 1, scalar @hdr; # file-level header
+$filepos = 6 + 16 * scalar @hdr;
+for ($i = 0; $i < scalar @hdr; $i++) {
+ print $hdr[$i];
+ print pack "V", $filepos;
+ $filepos += length($dat[$i]);
+}
+for ($i = 0; $i < scalar @hdr; $i++) {
+ print $dat[$i];
+}
+
+sub readicon {
+ my $filename = shift @_;
+ my $depth = shift @_;
+ my $pix;
+ my $i;
+ my %pal;
+
+ # Determine the icon's width and height.
+ my $w = `identify -format %w $filename`;
+ my $h = `identify -format %h $filename`;
+
+ # Read the file in as RGBA data. We flip vertically at this
+ # point, to avoid having to do it ourselves (.BMP and hence
+ # .ICO are bottom-up).
+ my $data = [];
+ open IDATA, "convert -flip -depth 8 $filename rgba:- |";
+ push @$data, $rgb while (read IDATA,$rgb,4,0) == 4;
+ close IDATA;
+ # Check we have the right amount of data.
+ $xl = $w * $h;
+ $al = scalar @$data;
+ die "wrong amount of image data ($al, expected $xl) from $filename\n"
+ unless $al == $xl;
+
+ # Build the alpha channel now, so we can exclude transparent
+ # pixels from the palette analysis. We replace transparent
+ # pixels with undef in the data array.
+ #
+ # We quantise the alpha channel half way up, so that alpha of
+ # 0x80 or more is taken to be fully opaque and 0x7F or less is
+ # fully transparent. Nasty, but the best we can do without
+ # dithering (and don't even suggest we do that!).
+ my $x;
+ my $y;
+ my $alpha = "";
+
+ for ($y = 0; $y < $h; $y++) {
+ my $currbyte = 0, $currbits = 0;
+ for ($x = 0; $x < (($w+31)|31)-31; $x++) {
+ $pix = ($x < $w ? $data->[$y*$w+$x] : "\x00\x00\x00\xFF");
+ my @rgba = unpack "CCCC", $pix;
+ $currbyte <<= 1;
+ $currbits++;
+ if ($rgba[3] < 0x80) {
+ if ($x < $w) {
+ $data->[$y*$w+$x] = undef;
+ }
+ $currbyte |= 1; # MS has the alpha channel inverted :-)
+ } else {
+ # Might as well flip RGBA into BGR0 while we're here.
+ if ($x < $w) {
+ $data->[$y*$w+$x] = pack "CCCC",
+ $rgba[2], $rgba[1], $rgba[0], 0;
+ }
+ }
+ if ($currbits >= 8) {
+ $alpha .= pack "C", $currbyte;
+ $currbits -= 8;
+ }
+ }
+ }
+
+ # For an 8-bit image, check we have at most 256 distinct
+ # colours, and build the palette.
+ %pal = ();
+ if ($depth == 8) {
+ my $palindex = 0;
+ foreach $pix (@$data) {
+ next unless defined $pix;
+ $pal{$pix} = $palindex++ unless defined $pal{$pix};
+ }
+ die "too many colours in 8-bit image $filename\n" unless $palindex <= 256;
+ } elsif ($depth == 4) {
+ %pal = %win16pal;
+ } elsif ($depth == 1) {
+ %pal = %win2pal;
+ }
+
+ my $raster = "";
+ if ($depth < 24) {
+ # For a non-24-bit image, flatten the image into one palette
+ # index per pixel.
+ $pad = 32 / $depth; # number of pixels to pad scanline to 4-byte align
+ $pmask = $pad-1;
+ for ($y = 0; $y < $h; $y++) {
+ my $currbyte = 0, $currbits = 0;
+ for ($x = 0; $x < (($w+$pmask)|$pmask)-$pmask; $x++) {
+ $currbyte <<= $depth;
+ $currbits += $depth;
+ if ($x < $w && defined ($pix = $data->[$y*$w+$x])) {
+ if (!defined $pal{$pix}) {
+ die "illegal colour value $pix at pixel $i in $filename\n";
+ }
+ $currbyte |= $pal{$pix};
+ }
+ if ($currbits >= 8) {
+ $raster .= pack "C", $currbyte;
+ $currbits -= 8;
+ }
+ }
+ }
+ } else {
+ # For a 24-bit image, reverse the order of the R,G,B values
+ # and stick a padding zero on the end.
+ #
+ # (In this loop we don't need to bother padding the
+ # scanline out to a multiple of four bytes, because every
+ # pixel takes four whole bytes anyway.)
+ for ($i = 0; $i < scalar @$data; $i++) {
+ if (defined $data->[$i]) {
+ $raster .= $data->[$i];
+ } else {
+ $raster .= "\x00\x00\x00\x00";
+ }
+ }
+ $depth = 32; # and adjust this
+ }
+
+ # Prepare the icon data. First the header...
+ my $data = pack "VVVvvVVVVVV",
+ 40, # size of bitmap info header
+ $w, # icon width
+ $h*2, # icon height (x2 to indicate the subsequent alpha channel)
+ 1, # 1 plane (common to all MS image formats)
+ $depth, # bits per pixel
+ 0, # no compression
+ length $raster, # image size
+ 0, 0, 0, 0; # resolution, colours used, colours important (ignored)
+ # ... then the palette ...
+ if ($depth <= 8) {
+ my $ncols = (1 << $depth);
+ my $palette = "\x00\x00\x00\x00" x $ncols;
+ foreach $i (keys %pal) {
+ substr($palette, $pal{$i}*4, 4) = $i;
+ }
+ $data .= $palette;
+ }
+ # ... the raster data we already had ready ...
+ $data .= $raster;
+ # ... and the alpha channel we already had as well.
+ $data .= $alpha;
+
+ # Prepare the header which will represent this image in the
+ # icon file.
+ my $header = pack "CCCCvvV",
+ $w, $h, # width and height (this time the real height)
+ 1 << $depth, # number of colours, if less than 256
+ 0, # reserved
+ 1, # planes
+ $depth, # bits per pixel
+ length $data; # size of real icon data
+
+ push @hdr, $header;
+ push @dat, $data;
+}
--- /dev/null
+#!/usr/bin/env python
+
+import math
+
+# Python code which draws the PuTTY icon components at a range of
+# sizes.
+
+# TODO
+# ----
+#
+# - use of alpha blending
+# + try for variable-transparency borders
+#
+# - can we integrate the Mac icons into all this? Do we want to?
+
+def pixel(x, y, colour, canvas):
+ canvas[(int(x),int(y))] = colour
+
+def overlay(src, x, y, dst):
+ x = int(x)
+ y = int(y)
+ for (sx, sy), colour in src.items():
+ dst[sx+x, sy+y] = blend(colour, dst.get((sx+x, sy+y), cT))
+
+def finalise(canvas):
+ for k in canvas.keys():
+ canvas[k] = finalisepix(canvas[k])
+
+def bbox(canvas):
+ minx, miny, maxx, maxy = None, None, None, None
+ for (x, y) in canvas.keys():
+ if minx == None:
+ minx, miny, maxx, maxy = x, y, x+1, y+1
+ else:
+ minx = min(minx, x)
+ miny = min(miny, y)
+ maxx = max(maxx, x+1)
+ maxy = max(maxy, y+1)
+ return (minx, miny, maxx, maxy)
+
+def topy(canvas):
+ miny = {}
+ for (x, y) in canvas.keys():
+ miny[x] = min(miny.get(x, y), y)
+ return miny
+
+def render(canvas, minx, miny, maxx, maxy):
+ w = maxx - minx
+ h = maxy - miny
+ ret = []
+ for y in range(h):
+ ret.append([outpix(cT)] * w)
+ for (x, y), colour in canvas.items():
+ if x >= minx and x < maxx and y >= miny and y < maxy:
+ ret[y-miny][x-minx] = outpix(colour)
+ return ret
+
+# Code to actually draw pieces of icon. These don't generally worry
+# about positioning within a canvas; they just draw at a standard
+# location, return some useful coordinates, and leave composition
+# to other pieces of code.
+
+sqrthash = {}
+def memoisedsqrt(x):
+ if not sqrthash.has_key(x):
+ sqrthash[x] = math.sqrt(x)
+ return sqrthash[x]
+
+BR, TR, BL, TL = range(4) # enumeration of quadrants for border()
+
+def border(canvas, thickness, squarecorners):
+ # I haven't yet worked out exactly how to do borders in a
+ # properly alpha-blended fashion.
+ #
+ # When you have two shades of dark available (half-dark H and
+ # full-dark F), the right sequence of circular border sections
+ # around a pixel x starts off with these two layouts:
+ #
+ # H F
+ # HxH FxF
+ # H F
+ #
+ # Where it goes after that I'm not entirely sure, but I'm
+ # absolutely sure those are the right places to start. However,
+ # every automated algorithm I've tried has always started off
+ # with the two layouts
+ #
+ # H HHH
+ # HxH HxH
+ # H HHH
+ #
+ # which looks much worse. This is true whether you do
+ # pixel-centre sampling (define an inner circle and an outer
+ # circle with radii differing by 1, set any pixel whose centre
+ # is inside the inner circle to F, any pixel whose centre is
+ # outside the outer one to nothing, interpolate between the two
+ # and round sensibly), _or_ whether you plot a notional circle
+ # of a given radius and measure the actual _proportion_ of each
+ # pixel square taken up by it.
+ #
+ # It's not clear what I should be doing to prevent this. One
+ # option is to attempt error-diffusion: Ian Jackson proved on
+ # paper that if you round each pixel's ideal value to the
+ # nearest of the available output values, then measure the
+ # error at each pixel, propagate that error outwards into the
+ # original values of the surrounding pixels, and re-round
+ # everything, you do get the correct second stage. However, I
+ # haven't tried it at a proper range of radii.
+ #
+ # Another option is that the automated mechanisms described
+ # above would be entirely adequate if it weren't for the fact
+ # that the human visual centres are adapted to detect
+ # horizontal and vertical lines in particular, so the only
+ # place you have to behave a bit differently is at the ends of
+ # the top and bottom row of pixels in the circle, and the top
+ # and bottom of the extreme columns.
+ #
+ # For the moment, what I have below is a very simple mechanism
+ # which always uses only one alpha level for any given border
+ # thickness, and which seems to work well enough for Windows
+ # 16-colour icons. Everything else will have to wait.
+
+ thickness = memoisedsqrt(thickness)
+
+ if thickness < 0.9:
+ darkness = 0.5
+ else:
+ darkness = 1
+ if thickness < 1: thickness = 1
+ thickness = round(thickness - 0.5) + 0.3
+
+ dmax = int(round(thickness))
+ if dmax < thickness: dmax = dmax + 1
+
+ cquadrant = [[0] * (dmax+1) for x in range(dmax+1)]
+ squadrant = [[0] * (dmax+1) for x in range(dmax+1)]
+
+ for x in range(dmax+1):
+ for y in range(dmax+1):
+ if max(x, y) < thickness:
+ squadrant[x][y] = darkness
+ if memoisedsqrt(x*x+y*y) < thickness:
+ cquadrant[x][y] = darkness
+
+ bvalues = {}
+ for (x, y), colour in canvas.items():
+ for dx in range(-dmax, dmax+1):
+ for dy in range(-dmax, dmax+1):
+ quadrant = 2 * (dx < 0) + (dy < 0)
+ if (x, y, quadrant) in squarecorners:
+ bval = squadrant[abs(dx)][abs(dy)]
+ else:
+ bval = cquadrant[abs(dx)][abs(dy)]
+ if bvalues.get((x+dx,y+dy),0) < bval:
+ bvalues[(x+dx,y+dy)] = bval
+
+ for (x, y), value in bvalues.items():
+ if not canvas.has_key((x,y)):
+ canvas[(x,y)] = dark(value)
+
+def sysbox(size):
+ canvas = {}
+
+ # The system box of the computer.
+
+ height = int(round(3*size))
+ width = int(round(17*size))
+ depth = int(round(2*size))
+ highlight = int(round(1*size))
+ bothighlight = int(round(0.49*size))
+
+ floppystart = int(round(19*size)) # measured in half-pixels
+ floppyend = int(round(29*size)) # measured in half-pixels
+ floppybottom = height - bothighlight
+ floppyrheight = 0.7 * size
+ floppyheight = int(round(floppyrheight))
+ if floppyheight < 1:
+ floppyheight = 1
+ floppytop = floppybottom - floppyheight
+
+ # The front panel is rectangular.
+ for x in range(width):
+ for y in range(height):
+ grey = 3
+ if x < highlight or y < highlight:
+ grey = grey + 1
+ if x >= width-highlight or y >= height-bothighlight:
+ grey = grey - 1
+ if y < highlight and x >= width-highlight:
+ v = (highlight-1-y) - (x-(width-highlight))
+ if v < 0:
+ grey = grey - 1
+ elif v > 0:
+ grey = grey + 1
+ if y >= floppytop and y < floppybottom and \
+ 2*x+2 > floppystart and 2*x < floppyend:
+ if 2*x >= floppystart and 2*x+2 <= floppyend and \
+ floppyrheight >= 0.7:
+ grey = 0
+ else:
+ grey = 2
+ pixel(x, y, greypix(grey/4.0), canvas)
+
+ # The side panel is a parallelogram.
+ for x in range(depth):
+ for y in range(height+1):
+ pixel(x+width, y-(x+1), greypix(0.5), canvas)
+
+ # The top panel is another parallelogram.
+ for x in range(width-1):
+ for y in range(depth):
+ grey = 3
+ if x >= width-1 - highlight:
+ grey = grey + 1
+ pixel(x+(y+1), -(y+1), greypix(grey/4.0), canvas)
+
+ # And draw a border.
+ border(canvas, size, [])
+
+ return canvas
+
+def monitor(size):
+ canvas = {}
+
+ # The computer's monitor.
+
+ height = int(round(9.55*size))
+ width = int(round(11*size))
+ surround = int(round(1*size))
+ botsurround = int(round(2*size))
+ sheight = height - surround - botsurround
+ swidth = width - 2*surround
+ depth = int(round(2*size))
+ highlight = int(round(math.sqrt(size)))
+ shadow = int(round(0.55*size))
+
+ # The front panel is rectangular.
+ for x in range(width):
+ for y in range(height):
+ if x >= surround and y >= surround and \
+ x < surround+swidth and y < surround+sheight:
+ # Screen.
+ sx = (float(x-surround) - swidth/3) / swidth
+ sy = (float(y-surround) - sheight/3) / sheight
+ shighlight = 1.0 - (sx*sx+sy*sy)*0.27
+ pix = bluepix(shighlight)
+ if x < surround+shadow or y < surround+shadow:
+ pix = blend(cD, pix) # sharp-edged shadow on top and left
+ else:
+ # Complicated double bevel on the screen surround.
+
+ # First, the outer bevel. We compute the distance
+ # from this pixel to each edge of the front
+ # rectangle.
+ list = [
+ (x, +1),
+ (y, +1),
+ (width-1-x, -1),
+ (height-1-y, -1)
+ ]
+ # Now sort the list to find the distance to the
+ # _nearest_ edge, or the two joint nearest.
+ list.sort()
+ # If there's one nearest edge, that determines our
+ # bevel colour. If there are two joint nearest, our
+ # bevel colour is their shared one if they agree,
+ # and neutral otherwise.
+ outerbevel = 0
+ if list[0][0] < list[1][0] or list[0][1] == list[1][1]:
+ if list[0][0] < highlight:
+ outerbevel = list[0][1]
+
+ # Now, the inner bevel. We compute the distance
+ # from this pixel to each edge of the screen
+ # itself.
+ list = [
+ (surround-1-x, -1),
+ (surround-1-y, -1),
+ (x-(surround+swidth), +1),
+ (y-(surround+sheight), +1)
+ ]
+ # Now we sort to find the _maximum_ distance, which
+ # conveniently ignores any less than zero.
+ list.sort()
+ # And now the strategy is pretty much the same as
+ # above, only we're working from the opposite end
+ # of the list.
+ innerbevel = 0
+ if list[-1][0] > list[-2][0] or list[-1][1] == list[-2][1]:
+ if list[-1][0] >= 0 and list[-1][0] < highlight:
+ innerbevel = list[-1][1]
+
+ # Now we know the adjustment we want to make to the
+ # pixel's overall grey shade due to the outer
+ # bevel, and due to the inner one. We break a tie
+ # in favour of a light outer bevel, but otherwise
+ # add.
+ grey = 3
+ if outerbevel > 0 or outerbevel == innerbevel:
+ innerbevel = 0
+ grey = grey + outerbevel + innerbevel
+
+ pix = greypix(grey / 4.0)
+
+ pixel(x, y, pix, canvas)
+
+ # The side panel is a parallelogram.
+ for x in range(depth):
+ for y in range(height):
+ pixel(x+width, y-x, greypix(0.5), canvas)
+
+ # The top panel is another parallelogram.
+ for x in range(width):
+ for y in range(depth-1):
+ pixel(x+(y+1), -(y+1), greypix(0.75), canvas)
+
+ # And draw a border.
+ border(canvas, size, [(0,int(height-1),BL)])
+
+ return canvas
+
+def computer(size):
+ # Monitor plus sysbox.
+ m = monitor(size)
+ s = sysbox(size)
+ x = int(round((2+size/(size+1))*size))
+ y = int(round(4*size))
+ mb = bbox(m)
+ sb = bbox(s)
+ xoff = sb[0] - mb[0] + x
+ yoff = sb[3] - mb[3] - y
+ overlay(m, xoff, yoff, s)
+ return s
+
+def lightning(size):
+ canvas = {}
+
+ # The lightning bolt motif.
+
+ # We always want this to be an even number of pixels in span.
+ width = round(7*size) * 2
+ height = round(8*size) * 2
+
+ # The outer edge of each side of the bolt goes to this point.
+ outery = round(8.4*size)
+ outerx = round(11*size)
+
+ # And the inner edge goes to this point.
+ innery = height - 1 - outery
+ innerx = round(7*size)
+
+ for y in range(int(height)):
+ list = []
+ if y <= outery:
+ list.append(width-1-int(outerx * float(y) / outery + 0.3))
+ if y <= innery:
+ list.append(width-1-int(innerx * float(y) / innery + 0.3))
+ y0 = height-1-y
+ if y0 <= outery:
+ list.append(int(outerx * float(y0) / outery + 0.3))
+ if y0 <= innery:
+ list.append(int(innerx * float(y0) / innery + 0.3))
+ list.sort()
+ for x in range(int(list[0]), int(list[-1]+1)):
+ pixel(x, y, cY, canvas)
+
+ # And draw a border.
+ border(canvas, size, [(int(width-1),0,TR), (0,int(height-1),BL)])
+
+ return canvas
+
+def document(size):
+ canvas = {}
+
+ # The document used in the PSCP/PSFTP icon.
+
+ width = round(13*size)
+ height = round(16*size)
+
+ lineht = round(1*size)
+ if lineht < 1: lineht = 1
+ linespc = round(0.7*size)
+ if linespc < 1: linespc = 1
+ nlines = int((height-linespc)/(lineht+linespc))
+ height = nlines*(lineht+linespc)+linespc # round this so it fits better
+
+ # Start by drawing a big white rectangle.
+ for y in range(int(height)):
+ for x in range(int(width)):
+ pixel(x, y, cW, canvas)
+
+ # Now draw lines of text.
+ for line in range(nlines):
+ # Decide where this line of text begins.
+ if line == 0:
+ start = round(4*size)
+ elif line < 5*nlines/7:
+ start = round((line - (nlines/7)) * size)
+ else:
+ start = round(1*size)
+ if start < round(1*size):
+ start = round(1*size)
+ # Decide where it ends.
+ endpoints = [10, 8, 11, 6, 5, 7, 5]
+ ey = line * 6.0 / (nlines-1)
+ eyf = math.floor(ey)
+ eyc = math.ceil(ey)
+ exf = endpoints[int(eyf)]
+ exc = endpoints[int(eyc)]
+ if eyf == eyc:
+ end = exf
+ else:
+ end = exf * (eyc-ey) + exc * (ey-eyf)
+ end = round(end * size)
+
+ liney = height - (lineht+linespc) * (line+1)
+ for x in range(int(start), int(end)):
+ for y in range(int(lineht)):
+ pixel(x, y+liney, cK, canvas)
+
+ # And draw a border.
+ border(canvas, size, \
+ [(0,0,TL),(int(width-1),0,TR),(0,int(height-1),BL), \
+ (int(width-1),int(height-1),BR)])
+
+ return canvas
+
+def hat(size):
+ canvas = {}
+
+ # The secret-agent hat in the Pageant icon.
+
+ topa = [6]*9+[5,3,1,0,0,1,2,2,1,1,1,9,9,10,10,11,11,12,12]
+ topa = [round(x*size) for x in topa]
+ botl = round(topa[0]+2.4*math.sqrt(size))
+ botr = round(topa[-1]+2.4*math.sqrt(size))
+ width = round(len(topa)*size)
+
+ # Line equations for the top and bottom of the hat brim, in the
+ # form y=mx+c. c, of course, needs scaling by size, but m is
+ # independent of size.
+ brimm = 1.0 / 3.75
+ brimtopc = round(4*size/3)
+ brimbotc = round(10*size/3)
+
+ for x in range(int(width)):
+ xs = float(x) * (len(topa)-1) / (width-1)
+ xf = math.floor(xs)
+ xc = math.ceil(xs)
+ topf = topa[int(xf)]
+ topc = topa[int(xc)]
+ if xf == xc:
+ top = topf
+ else:
+ top = topf * (xc-xs) + topc * (xs-xf)
+ top = math.floor(top)
+ bot = round(botl + (botr-botl) * x/(width-1))
+
+ for y in range(int(top), int(bot)):
+ pixel(x, y, cK, canvas)
+
+ # Now draw the brim.
+ for x in range(int(width)):
+ brimtop = brimtopc + brimm * x
+ brimbot = brimbotc + brimm * x
+ for y in range(int(math.floor(brimtop)), int(math.ceil(brimbot))):
+ tophere = max(min(brimtop - y, 1), 0)
+ bothere = max(min(brimbot - y, 1), 0)
+ grey = bothere - tophere
+ # Only draw brim pixels over pixels which are (a) part
+ # of the main hat, and (b) not right on its edge.
+ if canvas.has_key((x,y)) and \
+ canvas.has_key((x,y-1)) and \
+ canvas.has_key((x,y+1)) and \
+ canvas.has_key((x-1,y)) and \
+ canvas.has_key((x+1,y)):
+ pixel(x, y, greypix(grey), canvas)
+
+ return canvas
+
+def key(size):
+ canvas = {}
+
+ # The key in the PuTTYgen icon.
+
+ keyheadw = round(9.5*size)
+ keyheadh = round(12*size)
+ keyholed = round(4*size)
+ keyholeoff = round(2*size)
+ # Ensure keyheadh and keyshafth have the same parity.
+ keyshafth = round((2*size - (int(keyheadh)&1)) / 2) * 2 + (int(keyheadh)&1)
+ keyshaftw = round(18.5*size)
+ keyhead = [round(x*size) for x in [12,11,8,10,9,8,11,12]]
+
+ squarepix = []
+
+ # Ellipse for the key head, minus an off-centre circular hole.
+ for y in range(int(keyheadh)):
+ dy = (y-(keyheadh-1)/2.0) / (keyheadh/2.0)
+ dyh = (y-(keyheadh-1)/2.0) / (keyholed/2.0)
+ for x in range(int(keyheadw)):
+ dx = (x-(keyheadw-1)/2.0) / (keyheadw/2.0)
+ dxh = (x-(keyheadw-1)/2.0-keyholeoff) / (keyholed/2.0)
+ if dy*dy+dx*dx <= 1 and dyh*dyh+dxh*dxh > 1:
+ pixel(x + keyshaftw, y, cy, canvas)
+
+ # Rectangle for the key shaft, extended at the bottom for the
+ # key head detail.
+ for x in range(int(keyshaftw)):
+ top = round((keyheadh - keyshafth) / 2)
+ bot = round((keyheadh + keyshafth) / 2)
+ xs = float(x) * (len(keyhead)-1) / round((len(keyhead)-1)*size)
+ xf = math.floor(xs)
+ xc = math.ceil(xs)
+ in_head = 0
+ if xc < len(keyhead):
+ in_head = 1
+ yf = keyhead[int(xf)]
+ yc = keyhead[int(xc)]
+ if xf == xc:
+ bot = yf
+ else:
+ bot = yf * (xc-xs) + yc * (xs-xf)
+ for y in range(int(top),int(bot)):
+ pixel(x, y, cy, canvas)
+ if in_head:
+ last = (x, y)
+ if x == 0:
+ squarepix.append((x, int(top), TL))
+ if x == 0:
+ squarepix.append(last + (BL,))
+ if last != None and not in_head:
+ squarepix.append(last + (BR,))
+ last = None
+
+ # And draw a border.
+ border(canvas, size, squarepix)
+
+ return canvas
+
+def linedist(x1,y1, x2,y2, x,y):
+ # Compute the distance from the point x,y to the line segment
+ # joining x1,y1 to x2,y2. Returns the distance vector, measured
+ # with x,y at the origin.
+
+ vectors = []
+
+ # Special case: if x1,y1 and x2,y2 are the same point, we
+ # don't attempt to extrapolate it into a line at all.
+ if x1 != x2 or y1 != y2:
+ # First, find the nearest point to x,y on the infinite
+ # projection of the line segment. So we construct a vector
+ # n perpendicular to that segment...
+ nx = y2-y1
+ ny = x1-x2
+ # ... compute the dot product of (x1,y1)-(x,y) with that
+ # vector...
+ nd = (x1-x)*nx + (y1-y)*ny
+ # ... multiply by the vector we first thought of...
+ ndx = nd * nx
+ ndy = nd * ny
+ # ... and divide twice by the length of n.
+ ndx = ndx / (nx*nx+ny*ny)
+ ndy = ndy / (nx*nx+ny*ny)
+ # That gives us a displacement vector from x,y to the
+ # nearest point. See if it's within the range of the line
+ # segment.
+ cx = x + ndx
+ cy = y + ndy
+ if cx >= min(x1,x2) and cx <= max(x1,x2) and \
+ cy >= min(y1,y2) and cy <= max(y1,y2):
+ vectors.append((ndx,ndy))
+
+ # Now we have up to three candidate result vectors: (ndx,ndy)
+ # as computed just above, and the two vectors to the ends of
+ # the line segment, (x1-x,y1-y) and (x2-x,y2-y). Pick the
+ # shortest.
+ vectors = vectors + [(x1-x,y1-y), (x2-x,y2-y)]
+ bestlen, best = None, None
+ for v in vectors:
+ vlen = v[0]*v[0]+v[1]*v[1]
+ if bestlen == None or bestlen > vlen:
+ bestlen = vlen
+ best = v
+ return best
+
+def spanner(size):
+ canvas = {}
+
+ # The spanner in the config box icon.
+
+ headcentre = 0.5 + round(4*size)
+ headradius = headcentre + 0.1
+ headhighlight = round(1.5*size)
+ holecentre = 0.5 + round(3*size)
+ holeradius = round(2*size)
+ holehighlight = round(1.5*size)
+ shaftend = 0.5 + round(25*size)
+ shaftwidth = round(2*size)
+ shafthighlight = round(1.5*size)
+ cmax = shaftend + shaftwidth
+
+ # Define three line segments, such that the shortest distance
+ # vectors from any point to each of these segments determines
+ # everything we need to know about where it is on the spanner
+ # shape.
+ segments = [
+ ((0,0), (holecentre, holecentre)),
+ ((headcentre, headcentre), (headcentre, headcentre)),
+ ((headcentre+headradius/math.sqrt(2), headcentre+headradius/math.sqrt(2)),
+ (cmax, cmax))
+ ]
+
+ for y in range(int(cmax)):
+ for x in range(int(cmax)):
+ vectors = [linedist(a,b,c,d,x,y) for ((a,b),(c,d)) in segments]
+ dists = [memoisedsqrt(vx*vx+vy*vy) for (vx,vy) in vectors]
+
+ # If the distance to the hole line is less than
+ # holeradius, we're not part of the spanner.
+ if dists[0] < holeradius:
+ continue
+ # If the distance to the head `line' is less than
+ # headradius, we are part of the spanner; likewise if
+ # the distance to the shaft line is less than
+ # shaftwidth _and_ the resulting shaft point isn't
+ # beyond the shaft end.
+ if dists[1] > headradius and \
+ (dists[2] > shaftwidth or x+vectors[2][0] >= shaftend):
+ continue
+
+ # We're part of the spanner. Now compute the highlight
+ # on this pixel. We do this by computing a `slope
+ # vector', which points from this pixel in the
+ # direction of its nearest edge. We store an array of
+ # slope vectors, in polar coordinates.
+ angles = [math.atan2(vy,vx) for (vx,vy) in vectors]
+ slopes = []
+ if dists[0] < holeradius + holehighlight:
+ slopes.append(((dists[0]-holeradius)/holehighlight,angles[0]))
+ if dists[1]/headradius < dists[2]/shaftwidth:
+ if dists[1] > headradius - headhighlight and dists[1] < headradius:
+ slopes.append(((headradius-dists[1])/headhighlight,math.pi+angles[1]))
+ else:
+ if dists[2] > shaftwidth - shafthighlight and dists[2] < shaftwidth:
+ slopes.append(((shaftwidth-dists[2])/shafthighlight,math.pi+angles[2]))
+ # Now we find the smallest distance in that array, if
+ # any, and that gives us a notional position on a
+ # sphere which we can use to compute the final
+ # highlight level.
+ bestdist = None
+ bestangle = 0
+ for dist, angle in slopes:
+ if bestdist == None or bestdist > dist:
+ bestdist = dist
+ bestangle = angle
+ if bestdist == None:
+ bestdist = 1.0
+ sx = (1.0-bestdist) * math.cos(bestangle)
+ sy = (1.0-bestdist) * math.sin(bestangle)
+ sz = math.sqrt(1.0 - sx*sx - sy*sy)
+ shade = sx-sy+sz / math.sqrt(3) # can range from -1 to +1
+ shade = 1.0 - (1-shade)/3
+
+ pixel(x, y, yellowpix(shade), canvas)
+
+ # And draw a border.
+ border(canvas, size, [])
+
+ return canvas
+
+# Functions to draw entire icons by composing the above components.
+
+def xybolt(c1, c2, size, boltoffx=0, boltoffy=0):
+ # Two unspecified objects and a lightning bolt.
+
+ canvas = {}
+ w = h = round(32 * size)
+
+ bolt = lightning(size)
+
+ # Position c2 against the top right of the icon.
+ bb = bbox(c2)
+ assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
+ overlay(c2, w-bb[2], 0-bb[1], canvas)
+ # Position c1 against the bottom left of the icon.
+ bb = bbox(c1)
+ assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
+ overlay(c1, 0-bb[0], h-bb[3], canvas)
+ # Place the lightning bolt artistically off-centre. (The
+ # rationale for this positioning is that it's centred on the
+ # midpoint between the centres of the two monitors in the PuTTY
+ # icon proper, but it's not really feasible to _base_ the
+ # calculation here on that.)
+ bb = bbox(bolt)
+ assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
+ overlay(bolt, (w-bb[0]-bb[2])/2 - round((1-boltoffx)*size), \
+ (h-bb[1]-bb[3])/2 - round((2-boltoffy)*size), canvas)
+
+ return canvas
+
+def putty_icon(size):
+ return xybolt(computer(size), computer(size), size)
+
+def puttycfg_icon(size):
+ w = h = round(32 * size)
+ s = spanner(size)
+ canvas = putty_icon(size)
+ # Centre the spanner.
+ bb = bbox(s)
+ overlay(s, (w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2, canvas)
+ return canvas
+
+def puttygen_icon(size):
+ return xybolt(computer(size), key(size), size, boltoffx=2)
+
+def pscp_icon(size):
+ return xybolt(document(size), computer(size), size, boltoffx=1)
+
+def pterm_icon(size):
+ # Just a really big computer.
+
+ canvas = {}
+ w = h = round(32 * size)
+
+ c = computer(size * 1.4)
+
+ # Centre c in the return canvas.
+ bb = bbox(c)
+ assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
+ overlay(c, (w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2, canvas)
+
+ return canvas
+
+def ptermcfg_icon(size):
+ w = h = round(32 * size)
+ s = spanner(size)
+ canvas = pterm_icon(size)
+ # Centre the spanner.
+ bb = bbox(s)
+ overlay(s, (w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2, canvas)
+ return canvas
+
+def pageant_icon(size):
+ # A biggish computer, in a hat.
+
+ canvas = {}
+ w = h = round(32 * size)
+
+ c = computer(size * 1.3)
+ ht = hat(size)
+
+ cbb = bbox(c)
+ hbb = bbox(ht)
+
+ # Determine the relative y-coordinates of the computer and hat.
+ # We just centre the one on the other.
+ xrel = (cbb[0]+cbb[2]-hbb[0]-hbb[2])/2
+
+ # Determine the relative y-coordinates of the computer and hat.
+ # We do this by sitting the hat as low down on the computer as
+ # possible without any computer showing over the top. To do
+ # this we first have to find the minimum x coordinate at each
+ # y-coordinate of both components.
+ cty = topy(c)
+ hty = topy(ht)
+ yrelmin = None
+ for cx in cty.keys():
+ hx = cx - xrel
+ assert hty.has_key(hx)
+ yrel = cty[cx] - hty[hx]
+ if yrelmin == None:
+ yrelmin = yrel
+ else:
+ yrelmin = min(yrelmin, yrel)
+
+ # Overlay the hat on the computer.
+ overlay(ht, xrel, yrelmin, c)
+
+ # And centre the result in the main icon canvas.
+ bb = bbox(c)
+ assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
+ overlay(c, (w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2, canvas)
+
+ return canvas
+
+# Test and output functions.
+
+import os
+import sys
+
+def testrun(func, fname):
+ canvases = []
+ for size in [0.5, 0.6, 1.0, 1.2, 1.5, 4.0]:
+ canvases.append(func(size))
+ wid = 0
+ ht = 0
+ for canvas in canvases:
+ minx, miny, maxx, maxy = bbox(canvas)
+ wid = max(wid, maxx-minx+4)
+ ht = ht + maxy-miny+4
+ block = []
+ for canvas in canvases:
+ minx, miny, maxx, maxy = bbox(canvas)
+ block.extend(render(canvas, minx-2, miny-2, minx-2+wid, maxy+2))
+ p = os.popen("convert -depth 8 -size %dx%d rgb:- %s" % (wid,ht,fname), "w")
+ assert len(block) == ht
+ for line in block:
+ assert len(line) == wid
+ for r, g, b, a in line:
+ # Composite on to orange.
+ r = int(round((r * a + 255 * (255-a)) / 255.0))
+ g = int(round((g * a + 128 * (255-a)) / 255.0))
+ b = int(round((b * a + 0 * (255-a)) / 255.0))
+ p.write("%c%c%c" % (r,g,b))
+ p.close()
+
+def drawicon(func, width, fname, orangebackground = 0):
+ canvas = func(width / 32.0)
+ finalise(canvas)
+ minx, miny, maxx, maxy = bbox(canvas)
+ assert minx >= 0 and miny >= 0 and maxx <= width and maxy <= width
+
+ block = render(canvas, 0, 0, width, width)
+ p = os.popen("convert -depth 8 -size %dx%d rgba:- %s" % (width,width,fname), "w")
+ assert len(block) == width
+ for line in block:
+ assert len(line) == width
+ for r, g, b, a in line:
+ if orangebackground:
+ # Composite on to orange.
+ r = int(round((r * a + 255 * (255-a)) / 255.0))
+ g = int(round((g * a + 128 * (255-a)) / 255.0))
+ b = int(round((b * a + 0 * (255-a)) / 255.0))
+ a = 255
+ p.write("%c%c%c%c" % (r,g,b,a))
+ p.close()
+
+args = sys.argv[1:]
+
+orangebackground = test = 0
+colours = 1 # 0=mono, 1=16col, 2=truecol
+doingargs = 1
+
+realargs = []
+for arg in args:
+ if doingargs and arg[0] == "-":
+ if arg == "-t":
+ test = 1
+ elif arg == "-it":
+ orangebackground = 1
+ elif arg == "-2":
+ colours = 0
+ elif arg == "-T":
+ colours = 2
+ elif arg == "--":
+ doingargs = 0
+ else:
+ sys.stderr.write("unrecognised option '%s'\n" % arg)
+ sys.exit(1)
+ else:
+ realargs.append(arg)
+
+if colours == 0:
+ # Monochrome.
+ cK=cr=cg=cb=cm=cc=cP=cw=cR=cG=cB=cM=cC=cD = 0
+ cY=cy=cW = 1
+ cT = -1
+ def greypix(value):
+ return [cK,cW][int(round(value))]
+ def yellowpix(value):
+ return [cK,cW][int(round(value))]
+ def bluepix(value):
+ return cK
+ def dark(value):
+ return [cT,cK][int(round(value))]
+ def blend(col1, col2):
+ if col1 == cT:
+ return col2
+ else:
+ return col1
+ pixvals = [
+ (0x00, 0x00, 0x00, 0xFF), # cK
+ (0xFF, 0xFF, 0xFF, 0xFF), # cW
+ (0x00, 0x00, 0x00, 0x00), # cT
+ ]
+ def outpix(colour):
+ return pixvals[colour]
+ def finalisepix(colour):
+ return colour
+elif colours == 1:
+ # Windows 16-colour palette.
+ cK,cr,cg,cy,cb,cm,cc,cP,cw,cR,cG,cY,cB,cM,cC,cW = range(16)
+ cT = -1
+ cD = -2 # special translucent half-darkening value used internally
+ def greypix(value):
+ return [cK,cw,cw,cP,cW][int(round(4*value))]
+ def yellowpix(value):
+ return [cK,cy,cY][int(round(2*value))]
+ def bluepix(value):
+ return [cK,cb,cB][int(round(2*value))]
+ def dark(value):
+ return [cT,cD,cK][int(round(2*value))]
+ def blend(col1, col2):
+ if col1 == cT:
+ return col2
+ elif col1 == cD:
+ return [cK,cK,cK,cK,cK,cK,cK,cw,cK,cr,cg,cy,cb,cm,cc,cw,cD,cD][col2]
+ else:
+ return col1
+ pixvals = [
+ (0x00, 0x00, 0x00, 0xFF), # cK
+ (0x80, 0x00, 0x00, 0xFF), # cr
+ (0x00, 0x80, 0x00, 0xFF), # cg
+ (0x80, 0x80, 0x00, 0xFF), # cy
+ (0x00, 0x00, 0x80, 0xFF), # cb
+ (0x80, 0x00, 0x80, 0xFF), # cm
+ (0x00, 0x80, 0x80, 0xFF), # cc
+ (0xC0, 0xC0, 0xC0, 0xFF), # cP
+ (0x80, 0x80, 0x80, 0xFF), # cw
+ (0xFF, 0x00, 0x00, 0xFF), # cR
+ (0x00, 0xFF, 0x00, 0xFF), # cG
+ (0xFF, 0xFF, 0x00, 0xFF), # cY
+ (0x00, 0x00, 0xFF, 0xFF), # cB
+ (0xFF, 0x00, 0xFF, 0xFF), # cM
+ (0x00, 0xFF, 0xFF, 0xFF), # cC
+ (0xFF, 0xFF, 0xFF, 0xFF), # cW
+ (0x00, 0x00, 0x00, 0x80), # cD
+ (0x00, 0x00, 0x00, 0x00), # cT
+ ]
+ def outpix(colour):
+ return pixvals[colour]
+ def finalisepix(colour):
+ # cD is used internally, but can't be output. Convert to cK.
+ if colour == cD:
+ return cK
+ return colour
+else:
+ # True colour.
+ cK = (0x00, 0x00, 0x00, 0xFF)
+ cr = (0x80, 0x00, 0x00, 0xFF)
+ cg = (0x00, 0x80, 0x00, 0xFF)
+ cy = (0x80, 0x80, 0x00, 0xFF)
+ cb = (0x00, 0x00, 0x80, 0xFF)
+ cm = (0x80, 0x00, 0x80, 0xFF)
+ cc = (0x00, 0x80, 0x80, 0xFF)
+ cP = (0xC0, 0xC0, 0xC0, 0xFF)
+ cw = (0x80, 0x80, 0x80, 0xFF)
+ cR = (0xFF, 0x00, 0x00, 0xFF)
+ cG = (0x00, 0xFF, 0x00, 0xFF)
+ cY = (0xFF, 0xFF, 0x00, 0xFF)
+ cB = (0x00, 0x00, 0xFF, 0xFF)
+ cM = (0xFF, 0x00, 0xFF, 0xFF)
+ cC = (0x00, 0xFF, 0xFF, 0xFF)
+ cW = (0xFF, 0xFF, 0xFF, 0xFF)
+ cD = (0x00, 0x00, 0x00, 0x80)
+ cT = (0x00, 0x00, 0x00, 0x00)
+ def greypix(value):
+ value = max(min(value, 1), 0)
+ return (int(round(0xFF*value)),) * 3 + (0xFF,)
+ def yellowpix(value):
+ value = max(min(value, 1), 0)
+ return (int(round(0xFF*value)),) * 2 + (0, 0xFF)
+ def bluepix(value):
+ value = max(min(value, 1), 0)
+ return (0, 0, int(round(0xFF*value)), 0xFF)
+ def dark(value):
+ value = max(min(value, 1), 0)
+ return (0, 0, 0, int(round(0xFF*value)))
+ def blend(col1, col2):
+ r1,g1,b1,a1 = col1
+ r2,g2,b2,a2 = col2
+ r = int(round((r1*a1 + r2*(0xFF-a1)) / 255.0))
+ g = int(round((g1*a1 + g2*(0xFF-a1)) / 255.0))
+ b = int(round((b1*a1 + b2*(0xFF-a1)) / 255.0))
+ a = int(round((255*a1 + a2*(0xFF-a1)) / 255.0))
+ return r, g, b, a
+ def outpix(colour):
+ return colour
+ if colours == 2:
+ # True colour with no alpha blending: we still have to
+ # finalise half-dark pixels to black.
+ def finalisepix(colour):
+ if colour[3] > 0:
+ return colour[:3] + (0xFF,)
+ return colour
+ else:
+ def finalisepix(colour):
+ return colour
+
+if test:
+ testrun(eval(realargs[0]), realargs[1])
+else:
+ drawicon(eval(realargs[0]), int(realargs[1]), realargs[2], orangebackground)