#!/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, out={}): # 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 out["borderthickness"] = thickness 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, out={}): canvas = {} # The system box of the computer. height = int(round(3.6*size)) width = int(round(16.51*size)) depth = int(round(2*size)) highlight = int(round(1*size)) bothighlight = int(round(1*size)) out["sysboxheight"] = height 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): 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, [], out) return canvas def monitor(size): canvas = {} # The computer's monitor. height = int(round(9.55*size)) width = int(round(11.49*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. out = {} m = monitor(size) s = sysbox(size, out) x = int(round((2+size/(size+1))*size)) y = int(out["sysboxheight"] + out["borderthickness"]) 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 height, # and an odd number in width. width = round(7*size) * 2 - 1 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 def box(size, back): canvas = {} # The back side of the cardboard box in the installer icon. boxwidth = round(15 * size) boxheight = round(12 * size) boxdepth = round(4 * size) boxfrontflapheight = round(5 * size) boxrightflapheight = round(3 * size) # Three shades of basically acceptable brown, all achieved by # halftoning between two of the Windows-16 colours. I'm quite # pleased that was feasible at all! dark = halftone(cr, cK) med = halftone(cr, cy) light = halftone(cr, cY) # We define our halftoning parity in such a way that the black # pixels along the RHS of the visible part of the box back # match up with the one-pixel black outline around the # right-hand side of the box. In other words, we want the pixel # at (-1, boxwidth-1) to be black, and hence the one at (0, # boxwidth) too. parityadjust = int(boxwidth) % 2 # The entire back of the box. if back: for x in range(int(boxwidth + boxdepth)): ytop = max(-x-1, -boxdepth-1) ybot = min(boxheight, boxheight+boxwidth-1-x) for y in range(int(ytop), int(ybot)): pixel(x, y, dark[(x+y+parityadjust) % 2], canvas) # Even when drawing the back of the box, we still draw the # whole shape, because that means we get the right overall size # (the flaps make the box front larger than the box back) and # it'll all be overwritten anyway. # The front face of the box. for x in range(int(boxwidth)): for y in range(int(boxheight)): pixel(x, y, med[(x+y+parityadjust) % 2], canvas) # The right face of the box. for x in range(int(boxwidth), int(boxwidth+boxdepth)): ybot = boxheight + boxwidth-x ytop = ybot - boxheight for y in range(int(ytop), int(ybot)): pixel(x, y, dark[(x+y+parityadjust) % 2], canvas) # The front flap of the box. for y in range(int(boxfrontflapheight)): xadj = int(round(-0.5*y)) for x in range(int(xadj), int(xadj+boxwidth)): pixel(x, y, light[(x+y+parityadjust) % 2], canvas) # The right flap of the box. for x in range(int(boxwidth), int(boxwidth + boxdepth + boxrightflapheight + 1)): ytop = max(boxwidth - 1 - x, x - boxwidth - 2*boxdepth - 1) ybot = min(x - boxwidth - 1, boxwidth + 2*boxrightflapheight - 1 - x) for y in range(int(ytop), int(ybot+1)): pixel(x, y, med[(x+y+parityadjust) % 2], canvas) # And draw a border. border(canvas, size, [(0, int(boxheight)-1, BL)]) return canvas def boxback(size): return box(size, 1) def boxfront(size): return box(size, 0) # Functions to draw entire icons by composing the above components. def xybolt(c1, c2, size, boltoffx=0, boltoffy=0, aux={}): # 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) aux["c2pos"] = (w-bb[2], 0-bb[1]) # 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) aux["c1pos"] = (0-bb[0], h-bb[3]) # 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(boltoffx*size), \ (h-bb[1]-bb[3])/2 + round((boltoffy-2)*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) def puttyins_icon(size): aret = {} # The box back goes behind the lightning bolt. canvas = xybolt(boxback(size), computer(size), size, boltoffx=-2, boltoffy=+1, aux=aret) # But the box front goes over the top, so that the lightning # bolt appears to come _out_ of the box. Here it's useful to # know the exact coordinates where xybolt placed the box back, # so we can overlay the box front exactly on top of it. c1x, c1y = aret["c1pos"] overlay(boxfront(size), c1x, c1y, canvas) return canvas 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.2) 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 def halftone(col1, col2): return (col1, col2) 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 def halftone(col1, col2): return (col1, col2) 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 def halftone(col1, col2): r1,g1,b1,a1 = col1 r2,g2,b2,a2 = col2 colret = (int(r1+r2)/2, int(g1+g2)/2, int(b1+b2)/2, int(a1+a2)/2) return (colret, colret) if test: testrun(eval(realargs[0]), realargs[1]) else: drawicon(eval(realargs[0]), int(realargs[1]), realargs[2], orangebackground)