]> asedeno.scripts.mit.edu Git - PuTTY.git/blob - icons/mkicon.py
first pass
[PuTTY.git] / icons / mkicon.py
1 #!/usr/bin/env python
2
3 import math
4
5 # Python code which draws the PuTTY icon components at a range of
6 # sizes.
7
8 # TODO
9 # ----
10 #
11 #  - use of alpha blending
12 #     + try for variable-transparency borders
13 #
14 #  - can we integrate the Mac icons into all this? Do we want to?
15
16 def pixel(x, y, colour, canvas):
17     canvas[(int(x),int(y))] = colour
18
19 def overlay(src, x, y, dst):
20     x = int(x)
21     y = int(y)
22     for (sx, sy), colour in src.items():
23         dst[sx+x, sy+y] = blend(colour, dst.get((sx+x, sy+y), cT))
24
25 def finalise(canvas):
26     for k in canvas.keys():
27         canvas[k] = finalisepix(canvas[k])
28
29 def bbox(canvas):
30     minx, miny, maxx, maxy = None, None, None, None
31     for (x, y) in canvas.keys():
32         if minx == None:
33             minx, miny, maxx, maxy = x, y, x+1, y+1
34         else:
35             minx = min(minx, x)
36             miny = min(miny, y)
37             maxx = max(maxx, x+1)
38             maxy = max(maxy, y+1)
39     return (minx, miny, maxx, maxy)
40
41 def topy(canvas):
42     miny = {}
43     for (x, y) in canvas.keys():
44         miny[x] = min(miny.get(x, y), y)
45     return miny
46
47 def render(canvas, minx, miny, maxx, maxy):
48     w = maxx - minx
49     h = maxy - miny
50     ret = []
51     for y in range(h):
52         ret.append([outpix(cT)] * w)
53     for (x, y), colour in canvas.items():
54         if x >= minx and x < maxx and y >= miny and y < maxy:
55             ret[y-miny][x-minx] = outpix(colour)
56     return ret
57
58 # Code to actually draw pieces of icon. These don't generally worry
59 # about positioning within a canvas; they just draw at a standard
60 # location, return some useful coordinates, and leave composition
61 # to other pieces of code.
62
63 sqrthash = {}
64 def memoisedsqrt(x):
65     if not sqrthash.has_key(x):
66         sqrthash[x] = math.sqrt(x)
67     return sqrthash[x]
68
69 BR, TR, BL, TL = range(4) # enumeration of quadrants for border()
70
71 def border(canvas, thickness, squarecorners, out={}):
72     # I haven't yet worked out exactly how to do borders in a
73     # properly alpha-blended fashion.
74     #
75     # When you have two shades of dark available (half-dark H and
76     # full-dark F), the right sequence of circular border sections
77     # around a pixel x starts off with these two layouts:
78     #
79     #   H    F
80     #  HxH  FxF
81     #   H    F
82     #
83     # Where it goes after that I'm not entirely sure, but I'm
84     # absolutely sure those are the right places to start. However,
85     # every automated algorithm I've tried has always started off
86     # with the two layouts
87     #
88     #   H   HHH
89     #  HxH  HxH
90     #   H   HHH
91     #
92     # which looks much worse. This is true whether you do
93     # pixel-centre sampling (define an inner circle and an outer
94     # circle with radii differing by 1, set any pixel whose centre
95     # is inside the inner circle to F, any pixel whose centre is
96     # outside the outer one to nothing, interpolate between the two
97     # and round sensibly), _or_ whether you plot a notional circle
98     # of a given radius and measure the actual _proportion_ of each
99     # pixel square taken up by it.
100     #
101     # It's not clear what I should be doing to prevent this. One
102     # option is to attempt error-diffusion: Ian Jackson proved on
103     # paper that if you round each pixel's ideal value to the
104     # nearest of the available output values, then measure the
105     # error at each pixel, propagate that error outwards into the
106     # original values of the surrounding pixels, and re-round
107     # everything, you do get the correct second stage. However, I
108     # haven't tried it at a proper range of radii.
109     #
110     # Another option is that the automated mechanisms described
111     # above would be entirely adequate if it weren't for the fact
112     # that the human visual centres are adapted to detect
113     # horizontal and vertical lines in particular, so the only
114     # place you have to behave a bit differently is at the ends of
115     # the top and bottom row of pixels in the circle, and the top
116     # and bottom of the extreme columns.
117     #
118     # For the moment, what I have below is a very simple mechanism
119     # which always uses only one alpha level for any given border
120     # thickness, and which seems to work well enough for Windows
121     # 16-colour icons. Everything else will have to wait.
122
123     thickness = memoisedsqrt(thickness)
124
125     if thickness < 0.9:
126         darkness = 0.5
127     else:
128         darkness = 1
129     if thickness < 1: thickness = 1
130     thickness = round(thickness - 0.5) + 0.3
131
132     out["borderthickness"] = thickness
133
134     dmax = int(round(thickness))
135     if dmax < thickness: dmax = dmax + 1
136
137     cquadrant = [[0] * (dmax+1) for x in range(dmax+1)]
138     squadrant = [[0] * (dmax+1) for x in range(dmax+1)]
139
140     for x in range(dmax+1):
141         for y in range(dmax+1):
142             if max(x, y) < thickness:
143                 squadrant[x][y] = darkness
144             if memoisedsqrt(x*x+y*y) < thickness:
145                 cquadrant[x][y] = darkness
146
147     bvalues = {}
148     for (x, y), colour in canvas.items():
149         for dx in range(-dmax, dmax+1):
150             for dy in range(-dmax, dmax+1):
151                 quadrant = 2 * (dx < 0) + (dy < 0)
152                 if (x, y, quadrant) in squarecorners:
153                     bval = squadrant[abs(dx)][abs(dy)]
154                 else:
155                     bval = cquadrant[abs(dx)][abs(dy)]
156                 if bvalues.get((x+dx,y+dy),0) < bval:
157                     bvalues[(x+dx,y+dy)] = bval
158
159     for (x, y), value in bvalues.items():
160         if not canvas.has_key((x,y)):
161             canvas[(x,y)] = dark(value)
162
163 def sysbox(size, out={}):
164     canvas = {}
165
166     # The system box of the computer.
167
168     height = int(round(3.6*size))
169     width = int(round(16.51*size))
170     depth = int(round(2*size))
171     highlight = int(round(1*size))
172     bothighlight = int(round(1*size))
173
174     out["sysboxheight"] = height
175
176     floppystart = int(round(19*size)) # measured in half-pixels
177     floppyend = int(round(29*size)) # measured in half-pixels
178     floppybottom = height - bothighlight
179     floppyrheight = 0.7 * size
180     floppyheight = int(round(floppyrheight))
181     if floppyheight < 1:
182         floppyheight = 1
183     floppytop = floppybottom - floppyheight
184
185     # The front panel is rectangular.
186     for x in range(width):
187         for y in range(height):
188             grey = 3
189             if x < highlight or y < highlight:
190                 grey = grey + 1
191             if x >= width-highlight or y >= height-bothighlight:
192                 grey = grey - 1
193             if y < highlight and x >= width-highlight:
194                 v = (highlight-1-y) - (x-(width-highlight))
195                 if v < 0:
196                     grey = grey - 1
197                 elif v > 0:
198                     grey = grey + 1
199             if y >= floppytop and y < floppybottom and \
200             2*x+2 > floppystart and 2*x < floppyend:
201                 if 2*x >= floppystart and 2*x+2 <= floppyend and \
202                 floppyrheight >= 0.7:
203                     grey = 0
204                 else:
205                     grey = 2
206             pixel(x, y, greypix(grey/4.0), canvas)
207
208     # The side panel is a parallelogram.
209     for x in range(depth):
210         for y in range(height):
211             pixel(x+width, y-(x+1), greypix(0.5), canvas)
212
213     # The top panel is another parallelogram.
214     for x in range(width-1):
215         for y in range(depth):
216             grey = 3
217             if x >= width-1 - highlight:
218                 grey = grey + 1         
219             pixel(x+(y+1), -(y+1), greypix(grey/4.0), canvas)
220
221     # And draw a border.
222     border(canvas, size, [], out)
223
224     return canvas
225
226 def monitor(size):
227     canvas = {}
228
229     # The computer's monitor.
230
231     height = int(round(9.55*size))
232     width = int(round(11.49*size))
233     surround = int(round(1*size))
234     botsurround = int(round(2*size))
235     sheight = height - surround - botsurround
236     swidth = width - 2*surround
237     depth = int(round(2*size))
238     highlight = int(round(math.sqrt(size)))
239     shadow = int(round(0.55*size))
240
241     # The front panel is rectangular.
242     for x in range(width):
243         for y in range(height):
244             if x >= surround and y >= surround and \
245             x < surround+swidth and y < surround+sheight:
246                 # Screen.
247                 sx = (float(x-surround) - swidth/3) / swidth
248                 sy = (float(y-surround) - sheight/3) / sheight
249                 shighlight = 1.0 - (sx*sx+sy*sy)*0.27
250                 pix = bluepix(shighlight)
251                 if x < surround+shadow or y < surround+shadow:
252                     pix = blend(cD, pix) # sharp-edged shadow on top and left
253             else:
254                 # Complicated double bevel on the screen surround.
255
256                 # First, the outer bevel. We compute the distance
257                 # from this pixel to each edge of the front
258                 # rectangle.
259                 list = [
260                 (x, +1),
261                 (y, +1),
262                 (width-1-x, -1),
263                 (height-1-y, -1)
264                 ]
265                 # Now sort the list to find the distance to the
266                 # _nearest_ edge, or the two joint nearest.
267                 list.sort()
268                 # If there's one nearest edge, that determines our
269                 # bevel colour. If there are two joint nearest, our
270                 # bevel colour is their shared one if they agree,
271                 # and neutral otherwise.
272                 outerbevel = 0
273                 if list[0][0] < list[1][0] or list[0][1] == list[1][1]:
274                     if list[0][0] < highlight:
275                         outerbevel = list[0][1]
276
277                 # Now, the inner bevel. We compute the distance
278                 # from this pixel to each edge of the screen
279                 # itself.
280                 list = [
281                 (surround-1-x, -1),
282                 (surround-1-y, -1),
283                 (x-(surround+swidth), +1),
284                 (y-(surround+sheight), +1)
285                 ]
286                 # Now we sort to find the _maximum_ distance, which
287                 # conveniently ignores any less than zero.
288                 list.sort()
289                 # And now the strategy is pretty much the same as
290                 # above, only we're working from the opposite end
291                 # of the list.
292                 innerbevel = 0
293                 if list[-1][0] > list[-2][0] or list[-1][1] == list[-2][1]:
294                     if list[-1][0] >= 0 and list[-1][0] < highlight:
295                         innerbevel = list[-1][1]
296
297                 # Now we know the adjustment we want to make to the
298                 # pixel's overall grey shade due to the outer
299                 # bevel, and due to the inner one. We break a tie
300                 # in favour of a light outer bevel, but otherwise
301                 # add.
302                 grey = 3
303                 if outerbevel > 0 or outerbevel == innerbevel:
304                     innerbevel = 0
305                 grey = grey + outerbevel + innerbevel
306
307                 pix = greypix(grey / 4.0)
308
309             pixel(x, y, pix, canvas)
310
311     # The side panel is a parallelogram.
312     for x in range(depth):
313         for y in range(height):
314             pixel(x+width, y-x, greypix(0.5), canvas)
315
316     # The top panel is another parallelogram.
317     for x in range(width):
318         for y in range(depth-1):
319             pixel(x+(y+1), -(y+1), greypix(0.75), canvas)
320
321     # And draw a border.
322     border(canvas, size, [(0,int(height-1),BL)])
323
324     return canvas
325
326 def computer(size):
327     # Monitor plus sysbox.
328     out = {}
329     m = monitor(size)
330     s = sysbox(size, out)
331     x = int(round((2+size/(size+1))*size))
332     y = int(out["sysboxheight"] + out["borderthickness"])
333     mb = bbox(m)
334     sb = bbox(s)
335     xoff = sb[0] - mb[0] + x
336     yoff = sb[3] - mb[3] - y
337     overlay(m, xoff, yoff, s)
338     return s
339
340 def lightning(size):
341     canvas = {}
342
343     # The lightning bolt motif.
344
345     # We always want this to be an even number of pixels in height,
346     # and an odd number in width.
347     width = round(7*size) * 2 - 1
348     height = round(8*size) * 2
349
350     # The outer edge of each side of the bolt goes to this point.
351     outery = round(8.4*size)
352     outerx = round(11*size)
353
354     # And the inner edge goes to this point.
355     innery = height - 1 - outery
356     innerx = round(7*size)
357
358     for y in range(int(height)):
359         list = []
360         if y <= outery:
361             list.append(width-1-int(outerx * float(y) / outery + 0.3))
362         if y <= innery:
363             list.append(width-1-int(innerx * float(y) / innery + 0.3))
364         y0 = height-1-y
365         if y0 <= outery:
366             list.append(int(outerx * float(y0) / outery + 0.3))
367         if y0 <= innery:
368             list.append(int(innerx * float(y0) / innery + 0.3))
369         list.sort()
370         for x in range(int(list[0]), int(list[-1]+1)):
371             pixel(x, y, cY, canvas)
372
373     # And draw a border.
374     border(canvas, size, [(int(width-1),0,TR), (0,int(height-1),BL)])
375
376     return canvas
377
378 def document(size):
379     canvas = {}
380
381     # The document used in the PSCP/PSFTP icon.
382
383     width = round(13*size)
384     height = round(16*size)
385
386     lineht = round(1*size)
387     if lineht < 1: lineht = 1
388     linespc = round(0.7*size)
389     if linespc < 1: linespc = 1
390     nlines = int((height-linespc)/(lineht+linespc))
391     height = nlines*(lineht+linespc)+linespc # round this so it fits better
392
393     # Start by drawing a big white rectangle.
394     for y in range(int(height)):
395         for x in range(int(width)):
396             pixel(x, y, cW, canvas)
397
398     # Now draw lines of text.
399     for line in range(nlines):
400         # Decide where this line of text begins.
401         if line == 0:
402             start = round(4*size)
403         elif line < 5*nlines/7:
404             start = round((line - (nlines/7)) * size)
405         else:
406             start = round(1*size)
407         if start < round(1*size):
408             start = round(1*size)
409         # Decide where it ends.
410         endpoints = [10, 8, 11, 6, 5, 7, 5]
411         ey = line * 6.0 / (nlines-1)
412         eyf = math.floor(ey)
413         eyc = math.ceil(ey)
414         exf = endpoints[int(eyf)]
415         exc = endpoints[int(eyc)]
416         if eyf == eyc:
417             end = exf
418         else:
419             end = exf * (eyc-ey) + exc * (ey-eyf)
420         end = round(end * size)
421
422         liney = height - (lineht+linespc) * (line+1)
423         for x in range(int(start), int(end)):
424             for y in range(int(lineht)):
425                 pixel(x, y+liney, cK, canvas)
426
427     # And draw a border.
428     border(canvas, size, \
429     [(0,0,TL),(int(width-1),0,TR),(0,int(height-1),BL), \
430     (int(width-1),int(height-1),BR)])
431
432     return canvas
433
434 def hat(size):
435     canvas = {}
436
437     # The secret-agent hat in the Pageant icon.
438
439     topa = [6]*9+[5,3,1,0,0,1,2,2,1,1,1,9,9,10,10,11,11,12,12]
440     topa = [round(x*size) for x in topa]
441     botl = round(topa[0]+2.4*math.sqrt(size))
442     botr = round(topa[-1]+2.4*math.sqrt(size))
443     width = round(len(topa)*size)
444
445     # Line equations for the top and bottom of the hat brim, in the
446     # form y=mx+c. c, of course, needs scaling by size, but m is
447     # independent of size.
448     brimm = 1.0 / 3.75
449     brimtopc = round(4*size/3)
450     brimbotc = round(10*size/3)
451
452     for x in range(int(width)):
453         xs = float(x) * (len(topa)-1) / (width-1)
454         xf = math.floor(xs)
455         xc = math.ceil(xs)
456         topf = topa[int(xf)]
457         topc = topa[int(xc)]
458         if xf == xc:
459             top = topf
460         else:
461             top = topf * (xc-xs) + topc * (xs-xf)
462         top = math.floor(top)
463         bot = round(botl + (botr-botl) * x/(width-1))
464
465         for y in range(int(top), int(bot)):
466             pixel(x, y, cK, canvas)
467
468     # Now draw the brim.
469     for x in range(int(width)):
470         brimtop = brimtopc + brimm * x
471         brimbot = brimbotc + brimm * x
472         for y in range(int(math.floor(brimtop)), int(math.ceil(brimbot))):
473             tophere = max(min(brimtop - y, 1), 0)
474             bothere = max(min(brimbot - y, 1), 0)
475             grey = bothere - tophere
476             # Only draw brim pixels over pixels which are (a) part
477             # of the main hat, and (b) not right on its edge.
478             if canvas.has_key((x,y)) and \
479             canvas.has_key((x,y-1)) and \
480             canvas.has_key((x,y+1)) and \
481             canvas.has_key((x-1,y)) and \
482             canvas.has_key((x+1,y)):
483                 pixel(x, y, greypix(grey), canvas)
484
485     return canvas
486
487 def key(size):
488     canvas = {}
489
490     # The key in the PuTTYgen icon.
491
492     keyheadw = round(9.5*size)
493     keyheadh = round(12*size)
494     keyholed = round(4*size)
495     keyholeoff = round(2*size)
496     # Ensure keyheadh and keyshafth have the same parity.
497     keyshafth = round((2*size - (int(keyheadh)&1)) / 2) * 2 + (int(keyheadh)&1)
498     keyshaftw = round(18.5*size)
499     keyhead = [round(x*size) for x in [12,11,8,10,9,8,11,12]]
500
501     squarepix = []
502
503     # Ellipse for the key head, minus an off-centre circular hole.
504     for y in range(int(keyheadh)):
505         dy = (y-(keyheadh-1)/2.0) / (keyheadh/2.0)
506         dyh = (y-(keyheadh-1)/2.0) / (keyholed/2.0)
507         for x in range(int(keyheadw)):
508             dx = (x-(keyheadw-1)/2.0) / (keyheadw/2.0)
509             dxh = (x-(keyheadw-1)/2.0-keyholeoff) / (keyholed/2.0)
510             if dy*dy+dx*dx <= 1 and dyh*dyh+dxh*dxh > 1:
511                 pixel(x + keyshaftw, y, cy, canvas)
512
513     # Rectangle for the key shaft, extended at the bottom for the
514     # key head detail.
515     for x in range(int(keyshaftw)):
516         top = round((keyheadh - keyshafth) / 2)
517         bot = round((keyheadh + keyshafth) / 2)
518         xs = float(x) * (len(keyhead)-1) / round((len(keyhead)-1)*size)
519         xf = math.floor(xs)
520         xc = math.ceil(xs)
521         in_head = 0
522         if xc < len(keyhead):
523             in_head = 1
524             yf = keyhead[int(xf)]
525             yc = keyhead[int(xc)]
526             if xf == xc:
527                 bot = yf
528             else:
529                 bot = yf * (xc-xs) + yc * (xs-xf)
530         for y in range(int(top),int(bot)):
531             pixel(x, y, cy, canvas)
532             if in_head:
533                 last = (x, y)
534         if x == 0:
535             squarepix.append((x, int(top), TL))
536         if x == 0:
537             squarepix.append(last + (BL,))
538         if last != None and not in_head:
539             squarepix.append(last + (BR,))
540             last = None
541
542     # And draw a border.
543     border(canvas, size, squarepix)
544
545     return canvas
546
547 def linedist(x1,y1, x2,y2, x,y):
548     # Compute the distance from the point x,y to the line segment
549     # joining x1,y1 to x2,y2. Returns the distance vector, measured
550     # with x,y at the origin.
551
552     vectors = []
553
554     # Special case: if x1,y1 and x2,y2 are the same point, we
555     # don't attempt to extrapolate it into a line at all.
556     if x1 != x2 or y1 != y2:
557         # First, find the nearest point to x,y on the infinite
558         # projection of the line segment. So we construct a vector
559         # n perpendicular to that segment...
560         nx = y2-y1
561         ny = x1-x2
562         # ... compute the dot product of (x1,y1)-(x,y) with that
563         # vector...
564         nd = (x1-x)*nx + (y1-y)*ny
565         # ... multiply by the vector we first thought of...
566         ndx = nd * nx
567         ndy = nd * ny
568         # ... and divide twice by the length of n.
569         ndx = ndx / (nx*nx+ny*ny)
570         ndy = ndy / (nx*nx+ny*ny)
571         # That gives us a displacement vector from x,y to the
572         # nearest point. See if it's within the range of the line
573         # segment.
574         cx = x + ndx
575         cy = y + ndy
576         if cx >= min(x1,x2) and cx <= max(x1,x2) and \
577         cy >= min(y1,y2) and cy <= max(y1,y2):
578             vectors.append((ndx,ndy))
579
580     # Now we have up to three candidate result vectors: (ndx,ndy)
581     # as computed just above, and the two vectors to the ends of
582     # the line segment, (x1-x,y1-y) and (x2-x,y2-y). Pick the
583     # shortest.
584     vectors = vectors + [(x1-x,y1-y), (x2-x,y2-y)]
585     bestlen, best = None, None
586     for v in vectors:
587         vlen = v[0]*v[0]+v[1]*v[1]
588         if bestlen == None or bestlen > vlen:
589             bestlen = vlen
590             best = v
591     return best
592
593 def spanner(size):
594     canvas = {}
595
596     # The spanner in the config box icon.
597
598     headcentre = 0.5 + round(4*size)
599     headradius = headcentre + 0.1
600     headhighlight = round(1.5*size)
601     holecentre = 0.5 + round(3*size)
602     holeradius = round(2*size)
603     holehighlight = round(1.5*size)
604     shaftend = 0.5 + round(25*size)
605     shaftwidth = round(2*size)
606     shafthighlight = round(1.5*size)
607     cmax = shaftend + shaftwidth
608
609     # Define three line segments, such that the shortest distance
610     # vectors from any point to each of these segments determines
611     # everything we need to know about where it is on the spanner
612     # shape.
613     segments = [
614     ((0,0), (holecentre, holecentre)),
615     ((headcentre, headcentre), (headcentre, headcentre)),
616     ((headcentre+headradius/math.sqrt(2), headcentre+headradius/math.sqrt(2)),
617     (cmax, cmax))
618     ]
619
620     for y in range(int(cmax)):
621         for x in range(int(cmax)):
622             vectors = [linedist(a,b,c,d,x,y) for ((a,b),(c,d)) in segments]
623             dists = [memoisedsqrt(vx*vx+vy*vy) for (vx,vy) in vectors]
624
625             # If the distance to the hole line is less than
626             # holeradius, we're not part of the spanner.
627             if dists[0] < holeradius:
628                 continue
629             # If the distance to the head `line' is less than
630             # headradius, we are part of the spanner; likewise if
631             # the distance to the shaft line is less than
632             # shaftwidth _and_ the resulting shaft point isn't
633             # beyond the shaft end.
634             if dists[1] > headradius and \
635             (dists[2] > shaftwidth or x+vectors[2][0] >= shaftend):
636                 continue
637
638             # We're part of the spanner. Now compute the highlight
639             # on this pixel. We do this by computing a `slope
640             # vector', which points from this pixel in the
641             # direction of its nearest edge. We store an array of
642             # slope vectors, in polar coordinates.
643             angles = [math.atan2(vy,vx) for (vx,vy) in vectors]
644             slopes = []
645             if dists[0] < holeradius + holehighlight:
646                 slopes.append(((dists[0]-holeradius)/holehighlight,angles[0]))
647             if dists[1]/headradius < dists[2]/shaftwidth:
648                 if dists[1] > headradius - headhighlight and dists[1] < headradius:
649                     slopes.append(((headradius-dists[1])/headhighlight,math.pi+angles[1]))
650             else:
651                 if dists[2] > shaftwidth - shafthighlight and dists[2] < shaftwidth:
652                     slopes.append(((shaftwidth-dists[2])/shafthighlight,math.pi+angles[2]))
653             # Now we find the smallest distance in that array, if
654             # any, and that gives us a notional position on a
655             # sphere which we can use to compute the final
656             # highlight level.
657             bestdist = None
658             bestangle = 0
659             for dist, angle in slopes:
660                 if bestdist == None or bestdist > dist:
661                     bestdist = dist
662                     bestangle = angle
663             if bestdist == None:
664                 bestdist = 1.0
665             sx = (1.0-bestdist) * math.cos(bestangle)
666             sy = (1.0-bestdist) * math.sin(bestangle)
667             sz = math.sqrt(1.0 - sx*sx - sy*sy)
668             shade = sx-sy+sz / math.sqrt(3) # can range from -1 to +1
669             shade = 1.0 - (1-shade)/3
670
671             pixel(x, y, yellowpix(shade), canvas)
672
673     # And draw a border.
674     border(canvas, size, [])
675
676     return canvas
677
678 def box(size, back):
679     canvas = {}
680
681     # The back side of the cardboard box in the installer icon.
682
683     boxwidth = round(15 * size)
684     boxheight = round(12 * size)
685     boxdepth = round(4 * size)
686     boxfrontflapheight = round(5 * size)
687     boxrightflapheight = round(3 * size)
688
689     # Three shades of basically acceptable brown, all achieved by
690     # halftoning between two of the Windows-16 colours. I'm quite
691     # pleased that was feasible at all!
692     dark = halftone(cr, cK)
693     med = halftone(cr, cy)
694     light = halftone(cr, cY)
695     # We define our halftoning parity in such a way that the black
696     # pixels along the RHS of the visible part of the box back
697     # match up with the one-pixel black outline around the
698     # right-hand side of the box. In other words, we want the pixel
699     # at (-1, boxwidth-1) to be black, and hence the one at (0,
700     # boxwidth) too.
701     parityadjust = int(boxwidth) % 2
702
703     # The entire back of the box.
704     if back:
705         for x in range(int(boxwidth + boxdepth)):
706             ytop = max(-x-1, -boxdepth-1)
707             ybot = min(boxheight, boxheight+boxwidth-1-x)
708             for y in range(int(ytop), int(ybot)):
709                 pixel(x, y, dark[(x+y+parityadjust) % 2], canvas)
710
711     # Even when drawing the back of the box, we still draw the
712     # whole shape, because that means we get the right overall size
713     # (the flaps make the box front larger than the box back) and
714     # it'll all be overwritten anyway.
715
716     # The front face of the box.
717     for x in range(int(boxwidth)):
718         for y in range(int(boxheight)):
719             pixel(x, y, med[(x+y+parityadjust) % 2], canvas)
720     # The right face of the box.
721     for x in range(int(boxwidth), int(boxwidth+boxdepth)):
722         ybot = boxheight + boxwidth-x
723         ytop = ybot - boxheight
724         for y in range(int(ytop), int(ybot)):
725             pixel(x, y, dark[(x+y+parityadjust) % 2], canvas)
726     # The front flap of the box.
727     for y in range(int(boxfrontflapheight)):
728         xadj = int(round(-0.5*y))
729         for x in range(int(xadj), int(xadj+boxwidth)):
730             pixel(x, y, light[(x+y+parityadjust) % 2], canvas)
731     # The right flap of the box.
732     for x in range(int(boxwidth), int(boxwidth + boxdepth + boxrightflapheight + 1)):
733         ytop = max(boxwidth - 1 - x, x - boxwidth - 2*boxdepth - 1)
734         ybot = min(x - boxwidth - 1, boxwidth + 2*boxrightflapheight - 1 - x)
735         for y in range(int(ytop), int(ybot+1)):
736             pixel(x, y, med[(x+y+parityadjust) % 2], canvas)
737
738     # And draw a border.
739     border(canvas, size, [(0, int(boxheight)-1, BL)])
740
741     return canvas
742
743 def boxback(size):
744     return box(size, 1)
745 def boxfront(size):
746     return box(size, 0)
747
748 # Functions to draw entire icons by composing the above components.
749
750 def xybolt(c1, c2, size, boltoffx=0, boltoffy=0, aux={}):
751     # Two unspecified objects and a lightning bolt.
752
753     canvas = {}
754     w = h = round(32 * size)
755
756     bolt = lightning(size)
757
758     # Position c2 against the top right of the icon.
759     bb = bbox(c2)
760     assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
761     overlay(c2, w-bb[2], 0-bb[1], canvas)
762     aux["c2pos"] = (w-bb[2], 0-bb[1])
763     # Position c1 against the bottom left of the icon.
764     bb = bbox(c1)
765     assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
766     overlay(c1, 0-bb[0], h-bb[3], canvas)
767     aux["c1pos"] = (0-bb[0], h-bb[3])
768     # Place the lightning bolt artistically off-centre. (The
769     # rationale for this positioning is that it's centred on the
770     # midpoint between the centres of the two monitors in the PuTTY
771     # icon proper, but it's not really feasible to _base_ the
772     # calculation here on that.)
773     bb = bbox(bolt)
774     assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
775     overlay(bolt, (w-bb[0]-bb[2])/2 + round(boltoffx*size), \
776     (h-bb[1]-bb[3])/2 + round((boltoffy-2)*size), canvas)
777
778     return canvas
779
780 def putty_icon(size):
781     return xybolt(computer(size), computer(size), size)
782
783 def puttycfg_icon(size):
784     w = h = round(32 * size)
785     s = spanner(size)
786     canvas = putty_icon(size)
787     # Centre the spanner.
788     bb = bbox(s)
789     overlay(s, (w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2, canvas)
790     return canvas
791
792 def puttygen_icon(size):
793     return xybolt(computer(size), key(size), size, boltoffx=2)
794
795 def pscp_icon(size):
796     return xybolt(document(size), computer(size), size)
797
798 def puttyins_icon(size):
799     aret = {}
800     # The box back goes behind the lightning bolt.
801     canvas = xybolt(boxback(size), computer(size), size, boltoffx=-2, boltoffy=+1, aux=aret)
802     # But the box front goes over the top, so that the lightning
803     # bolt appears to come _out_ of the box. Here it's useful to
804     # know the exact coordinates where xybolt placed the box back,
805     # so we can overlay the box front exactly on top of it.
806     c1x, c1y = aret["c1pos"]
807     overlay(boxfront(size), c1x, c1y, canvas)
808     return canvas
809
810 def pterm_icon(size):
811     # Just a really big computer.
812
813     canvas = {}
814     w = h = round(32 * size)
815
816     c = computer(size * 1.4)
817
818     # Centre c in the return canvas.
819     bb = bbox(c)
820     assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
821     overlay(c, (w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2, canvas)
822
823     return canvas
824
825 def ptermcfg_icon(size):
826     w = h = round(32 * size)
827     s = spanner(size)
828     canvas = pterm_icon(size)
829     # Centre the spanner.
830     bb = bbox(s)
831     overlay(s, (w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2, canvas)
832     return canvas
833
834 def pageant_icon(size):
835     # A biggish computer, in a hat.
836
837     canvas = {}
838     w = h = round(32 * size)
839
840     c = computer(size * 1.2)
841     ht = hat(size)
842
843     cbb = bbox(c)
844     hbb = bbox(ht)
845
846     # Determine the relative y-coordinates of the computer and hat.
847     # We just centre the one on the other.
848     xrel = (cbb[0]+cbb[2]-hbb[0]-hbb[2])/2
849
850     # Determine the relative y-coordinates of the computer and hat.
851     # We do this by sitting the hat as low down on the computer as
852     # possible without any computer showing over the top. To do
853     # this we first have to find the minimum x coordinate at each
854     # y-coordinate of both components.
855     cty = topy(c)
856     hty = topy(ht)
857     yrelmin = None
858     for cx in cty.keys():
859         hx = cx - xrel
860         assert hty.has_key(hx)
861         yrel = cty[cx] - hty[hx]
862         if yrelmin == None:
863             yrelmin = yrel
864         else:
865             yrelmin = min(yrelmin, yrel)
866
867     # Overlay the hat on the computer.
868     overlay(ht, xrel, yrelmin, c)
869
870     # And centre the result in the main icon canvas.
871     bb = bbox(c)
872     assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
873     overlay(c, (w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2, canvas)
874
875     return canvas
876
877 # Test and output functions.
878
879 import os
880 import sys
881
882 def testrun(func, fname):
883     canvases = []
884     for size in [0.5, 0.6, 1.0, 1.2, 1.5, 4.0]:
885         canvases.append(func(size))
886     wid = 0
887     ht = 0
888     for canvas in canvases:
889         minx, miny, maxx, maxy = bbox(canvas)
890         wid = max(wid, maxx-minx+4)
891         ht = ht + maxy-miny+4
892     block = []
893     for canvas in canvases:
894         minx, miny, maxx, maxy = bbox(canvas)
895         block.extend(render(canvas, minx-2, miny-2, minx-2+wid, maxy+2))
896     with open(fname, "w") as f:
897         f.write(("P7\nWIDTH %d\nHEIGHT %d\nDEPTH 3\nMAXVAL 255\n" +
898                  "TUPLTYPE RGB\nENDHDR\n") % (wid, ht))
899         assert len(block) == ht
900         for line in block:
901             assert len(line) == wid
902             for r, g, b, a in line:
903                 # Composite on to orange.
904                 r = int(round((r * a + 255 * (255-a)) / 255.0))
905                 g = int(round((g * a + 128 * (255-a)) / 255.0))
906                 b = int(round((b * a +   0 * (255-a)) / 255.0))
907                 f.write("%c%c%c" % (r,g,b))
908
909 def drawicon(func, width, fname, orangebackground = 0):
910     canvas = func(width / 32.0)
911     finalise(canvas)
912     minx, miny, maxx, maxy = bbox(canvas)
913     assert minx >= 0 and miny >= 0 and maxx <= width and maxy <= width
914
915     block = render(canvas, 0, 0, width, width)
916     with open(fname, "w") as f:
917         f.write(("P7\nWIDTH %d\nHEIGHT %d\nDEPTH 4\nMAXVAL 255\n" +
918                  "TUPLTYPE RGB_ALPHA\nENDHDR\n") % (width, width))
919         assert len(block) == width
920         for line in block:
921             assert len(line) == width
922             for r, g, b, a in line:
923                 if orangebackground:
924                     # Composite on to orange.
925                     r = int(round((r * a + 255 * (255-a)) / 255.0))
926                     g = int(round((g * a + 128 * (255-a)) / 255.0))
927                     b = int(round((b * a +   0 * (255-a)) / 255.0))
928                     a = 255
929                 f.write("%c%c%c%c" % (r,g,b,a))
930
931 args = sys.argv[1:]
932
933 orangebackground = test = 0
934 colours = 1 # 0=mono, 1=16col, 2=truecol
935 doingargs = 1
936
937 realargs = []
938 for arg in args:
939     if doingargs and arg[0] == "-":
940         if arg == "-t":
941             test = 1
942         elif arg == "-it":
943             orangebackground = 1
944         elif arg == "-2":
945             colours = 0
946         elif arg == "-T":
947             colours = 2
948         elif arg == "--":
949             doingargs = 0
950         else:
951             sys.stderr.write("unrecognised option '%s'\n" % arg)
952             sys.exit(1)
953     else:
954         realargs.append(arg)
955
956 if colours == 0:
957     # Monochrome.
958     cK=cr=cg=cb=cm=cc=cP=cw=cR=cG=cB=cM=cC=cD = 0
959     cY=cy=cW = 1
960     cT = -1
961     def greypix(value):
962         return [cK,cW][int(round(value))]
963     def yellowpix(value):
964         return [cK,cW][int(round(value))]
965     def bluepix(value):
966         return cK
967     def dark(value):
968         return [cT,cK][int(round(value))]
969     def blend(col1, col2):
970         if col1 == cT:
971             return col2
972         else:
973             return col1
974     pixvals = [
975     (0x00, 0x00, 0x00, 0xFF), # cK
976     (0xFF, 0xFF, 0xFF, 0xFF), # cW
977     (0x00, 0x00, 0x00, 0x00), # cT
978     ]
979     def outpix(colour):
980         return pixvals[colour]
981     def finalisepix(colour):
982         return colour
983     def halftone(col1, col2):
984         return (col1, col2)
985 elif colours == 1:
986     # Windows 16-colour palette.
987     cK,cr,cg,cy,cb,cm,cc,cP,cw,cR,cG,cY,cB,cM,cC,cW = range(16)
988     cT = -1
989     cD = -2 # special translucent half-darkening value used internally
990     def greypix(value):
991         return [cK,cw,cw,cP,cW][int(round(4*value))]
992     def yellowpix(value):
993         return [cK,cy,cY][int(round(2*value))]
994     def bluepix(value):
995         return [cK,cb,cB][int(round(2*value))]
996     def dark(value):
997         return [cT,cD,cK][int(round(2*value))]
998     def blend(col1, col2):
999         if col1 == cT:
1000             return col2
1001         elif col1 == cD:
1002             return [cK,cK,cK,cK,cK,cK,cK,cw,cK,cr,cg,cy,cb,cm,cc,cw,cD,cD][col2]
1003         else:
1004             return col1
1005     pixvals = [
1006     (0x00, 0x00, 0x00, 0xFF), # cK
1007     (0x80, 0x00, 0x00, 0xFF), # cr
1008     (0x00, 0x80, 0x00, 0xFF), # cg
1009     (0x80, 0x80, 0x00, 0xFF), # cy
1010     (0x00, 0x00, 0x80, 0xFF), # cb
1011     (0x80, 0x00, 0x80, 0xFF), # cm
1012     (0x00, 0x80, 0x80, 0xFF), # cc
1013     (0xC0, 0xC0, 0xC0, 0xFF), # cP
1014     (0x80, 0x80, 0x80, 0xFF), # cw
1015     (0xFF, 0x00, 0x00, 0xFF), # cR
1016     (0x00, 0xFF, 0x00, 0xFF), # cG
1017     (0xFF, 0xFF, 0x00, 0xFF), # cY
1018     (0x00, 0x00, 0xFF, 0xFF), # cB
1019     (0xFF, 0x00, 0xFF, 0xFF), # cM
1020     (0x00, 0xFF, 0xFF, 0xFF), # cC
1021     (0xFF, 0xFF, 0xFF, 0xFF), # cW
1022     (0x00, 0x00, 0x00, 0x80), # cD
1023     (0x00, 0x00, 0x00, 0x00), # cT
1024     ]
1025     def outpix(colour):
1026         return pixvals[colour]
1027     def finalisepix(colour):
1028         # cD is used internally, but can't be output. Convert to cK.
1029         if colour == cD:
1030             return cK
1031         return colour
1032     def halftone(col1, col2):
1033         return (col1, col2)
1034 else:
1035     # True colour.
1036     cK = (0x00, 0x00, 0x00, 0xFF)
1037     cr = (0x80, 0x00, 0x00, 0xFF)
1038     cg = (0x00, 0x80, 0x00, 0xFF)
1039     cy = (0x80, 0x80, 0x00, 0xFF)
1040     cb = (0x00, 0x00, 0x80, 0xFF)
1041     cm = (0x80, 0x00, 0x80, 0xFF)
1042     cc = (0x00, 0x80, 0x80, 0xFF)
1043     cP = (0xC0, 0xC0, 0xC0, 0xFF)
1044     cw = (0x80, 0x80, 0x80, 0xFF)
1045     cR = (0xFF, 0x00, 0x00, 0xFF)
1046     cG = (0x00, 0xFF, 0x00, 0xFF)
1047     cY = (0xFF, 0xFF, 0x00, 0xFF)
1048     cB = (0x00, 0x00, 0xFF, 0xFF)
1049     cM = (0xFF, 0x00, 0xFF, 0xFF)
1050     cC = (0x00, 0xFF, 0xFF, 0xFF)
1051     cW = (0xFF, 0xFF, 0xFF, 0xFF)
1052     cD = (0x00, 0x00, 0x00, 0x80)
1053     cT = (0x00, 0x00, 0x00, 0x00)
1054     def greypix(value):
1055         value = max(min(value, 1), 0)
1056         return (int(round(0xFF*value)),) * 3 + (0xFF,)
1057     def yellowpix(value):
1058         value = max(min(value, 1), 0)
1059         return (int(round(0xFF*value)),) * 2 + (0, 0xFF)
1060     def bluepix(value):
1061         value = max(min(value, 1), 0)
1062         return (0, 0, int(round(0xFF*value)), 0xFF)
1063     def dark(value):
1064         value = max(min(value, 1), 0)
1065         return (0, 0, 0, int(round(0xFF*value)))
1066     def blend(col1, col2):
1067         r1,g1,b1,a1 = col1
1068         r2,g2,b2,a2 = col2
1069         r = int(round((r1*a1 + r2*(0xFF-a1)) / 255.0))
1070         g = int(round((g1*a1 + g2*(0xFF-a1)) / 255.0))
1071         b = int(round((b1*a1 + b2*(0xFF-a1)) / 255.0))
1072         a = int(round((255*a1 + a2*(0xFF-a1)) / 255.0))
1073         return r, g, b, a
1074     def outpix(colour):
1075         return colour
1076     if colours == 2:
1077         # True colour with no alpha blending: we still have to
1078         # finalise half-dark pixels to black.
1079         def finalisepix(colour):
1080             if colour[3] > 0:
1081                 return colour[:3] + (0xFF,)
1082             return colour
1083     else:
1084         def finalisepix(colour):
1085             return colour
1086     def halftone(col1, col2):
1087         r1,g1,b1,a1 = col1
1088         r2,g2,b2,a2 = col2
1089         colret = (int(r1+r2)/2, int(g1+g2)/2, int(b1+b2)/2, int(a1+a2)/2)
1090         return (colret, colret)
1091
1092 if test:
1093     testrun(eval(realargs[0]), realargs[1])
1094 else:
1095     drawicon(eval(realargs[0]), int(realargs[1]), realargs[2], orangebackground)