diff --git a/icons/Makefile b/icons/Makefile new file mode 100644 index 00000000..ef55d498 --- /dev/null +++ b/icons/Makefile @@ -0,0 +1,71 @@ +# Makefile for the PuTTY icon suite. + +ICONS = putty puttycfg puttygen pscp pageant pterm ptermcfg +SIZES = 16 32 48 + +MODE = # override to -it on command line for opaque testing + +PNGS = $(foreach I,$(ICONS),$(foreach S,$(SIZES),$(I)-$(S).png)) +MONOPNGS = $(foreach I,$(ICONS),$(foreach S,$(SIZES),$(I)-$(S)-mono.png)) +TRUEPNGS = $(foreach I,$(ICONS),$(foreach S,$(SIZES),$(I)-$(S)-true.png)) + +ICOS = putty.ico puttygen.ico pscp.ico pageant.ico pageants.ico puttycfg.ico +CICONS = xpmputty.c xpmpucfg.c xpmpterm.c xpmptcfg.c + +base: icos cicons + +all: pngs monopngs base # truepngs currently disabled by default + +pngs: $(PNGS) +monopngs: $(MONOPNGS) +truepngs: $(TRUEPNGS) + +icos: $(ICOS) +cicons: $(CICONS) + +$(PNGS): %.png: mkicon.py + ./mkicon.py $(MODE) $(join $(subst -, ,$(basename $@)),_icon) $@ + +$(MONOPNGS): %.png: mkicon.py + ./mkicon.py -2 $(MODE) $(join $(subst -, ,$(subst -mono,,$(basename $@))),_icon) $@ + +$(TRUEPNGS): %.png: mkicon.py + ./mkicon.py -T $(MODE) $(join $(subst -, ,$(subst -true,,$(basename $@))),_icon) $@ + +putty.ico: putty-16.png putty-32.png putty-48.png \ + putty-16-mono.png putty-32-mono.png putty-48-mono.png + ./icon.pl -4 $(filter-out %-mono.png, $^) -1 $(filter %-mono.png, $^) > $@ + +puttycfg.ico: puttycfg-16.png puttycfg-32.png puttycfg-48.png \ + puttycfg-16-mono.png puttycfg-32-mono.png puttycfg-48-mono.png + ./icon.pl -4 $(filter-out %-mono.png, $^) -1 $(filter %-mono.png, $^) > $@ + +puttygen.ico: puttygen-16.png puttygen-32.png puttygen-48.png \ + puttygen-16-mono.png puttygen-32-mono.png puttygen-48-mono.png + ./icon.pl -4 $(filter-out %-mono.png, $^) -1 $(filter %-mono.png, $^) > $@ + +pageant.ico: pageant-16.png pageant-32.png pageant-48.png \ + pageant-16-mono.png pageant-32-mono.png pageant-48-mono.png + ./icon.pl -4 $(filter-out %-mono.png, $^) -1 $(filter %-mono.png, $^) > $@ + +pageants.ico: pageant-16.png pageant-16-mono.png + ./icon.pl -4 $(filter-out %-mono.png, $^) -1 $(filter %-mono.png, $^) > $@ + +pscp.ico: pscp-16.png pscp-32.png pscp-48.png \ + pscp-16-mono.png pscp-32-mono.png pscp-48-mono.png + ./icon.pl -4 $(filter-out %-mono.png, $^) -1 $(filter %-mono.png, $^) > $@ + +xpmputty.c: putty-16.png putty-32.png putty-48.png + ./cicon.pl main_icon $^ > $@ + +xpmpucfg.c: puttycfg-16.png puttycfg-32.png puttycfg-48.png + ./cicon.pl cfg_icon $^ > $@ + +xpmpterm.c: pterm-16.png pterm-32.png pterm-48.png + ./cicon.pl main_icon $^ > $@ + +xpmptcfg.c: ptermcfg-16.png ptermcfg-32.png ptermcfg-48.png + ./cicon.pl cfg_icon $^ > $@ + +clean: + rm -f *.png *.ico *.c diff --git a/icons/cicon.pl b/icons/cicon.pl new file mode 100755 index 00000000..08e9bcb9 --- /dev/null +++ b/icons/cicon.pl @@ -0,0 +1,28 @@ +#!/usr/bin/perl + +# Given a list of input PNGs, create a C source file containing a +# const array of XPMs, named by a given C identifier. + +$id = shift @ARGV; +$k = 0; +@xpms = (); +foreach $f (@ARGV) { + # XPM format is generated directly by ImageMagick, so that's easy + # enough. We just have to adjust the declaration line so that it + # has the right name, linkage and storage class. + @lines = (); + open XPM, "convert $f xpm:- |"; + push @lines, $_ while ; + close XPM; + die "XPM from $f in unexpected format\n" unless $lines[1] =~ /^static.*\{$/; + $lines[1] = "static const char *const ${id}_$k"."[] = {\n"; + $k++; + push @xpms, @lines, "\n"; +} + +# Now output. +foreach $line (@xpms) { print $line; } +print "const char *const *const ${id}[] = {\n"; +for ($i = 0; $i < $k; $i++) { print " ${id}_$i,\n"; } +print "};\n"; +print "const int n_${id} = $k;\n"; diff --git a/icons/icon.pl b/icons/icon.pl new file mode 100755 index 00000000..b9cce7e3 --- /dev/null +++ b/icons/icon.pl @@ -0,0 +1,269 @@ +#!/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; +} diff --git a/icons/mkicon.py b/icons/mkicon.py new file mode 100755 index 00000000..5bb6a1c3 --- /dev/null +++ b/icons/mkicon.py @@ -0,0 +1,994 @@ +#!/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) diff --git a/windows/pageant.ico b/windows/pageant.ico index ba19e0a1..cdd378a8 100644 Binary files a/windows/pageant.ico and b/windows/pageant.ico differ diff --git a/windows/pageants.ico b/windows/pageants.ico index b563925b..cd39a0db 100644 Binary files a/windows/pageants.ico and b/windows/pageants.ico differ diff --git a/windows/pscp.ico b/windows/pscp.ico index 203ece8a..88a77de8 100644 Binary files a/windows/pscp.ico and b/windows/pscp.ico differ diff --git a/windows/putty.ico b/windows/putty.ico index c0c7cfc6..984a6022 100644 Binary files a/windows/putty.ico and b/windows/putty.ico differ diff --git a/windows/puttycfg.ico b/windows/puttycfg.ico index d90bfc2b..0da6c02c 100644 Binary files a/windows/puttycfg.ico and b/windows/puttycfg.ico differ diff --git a/windows/puttygen.ico b/windows/puttygen.ico index 537a0302..95cf45c2 100644 Binary files a/windows/puttygen.ico and b/windows/puttygen.ico differ