I think this change to Recipe was accidentally omitted from r7064.
[u/mdw/putty] / 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):
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 dmax = int(round(thickness))
133 if dmax < thickness: dmax = dmax + 1
134
135 cquadrant = [[0] * (dmax+1) for x in range(dmax+1)]
136 squadrant = [[0] * (dmax+1) for x in range(dmax+1)]
137
138 for x in range(dmax+1):
139 for y in range(dmax+1):
140 if max(x, y) < thickness:
141 squadrant[x][y] = darkness
142 if memoisedsqrt(x*x+y*y) < thickness:
143 cquadrant[x][y] = darkness
144
145 bvalues = {}
146 for (x, y), colour in canvas.items():
147 for dx in range(-dmax, dmax+1):
148 for dy in range(-dmax, dmax+1):
149 quadrant = 2 * (dx < 0) + (dy < 0)
150 if (x, y, quadrant) in squarecorners:
151 bval = squadrant[abs(dx)][abs(dy)]
152 else:
153 bval = cquadrant[abs(dx)][abs(dy)]
154 if bvalues.get((x+dx,y+dy),0) < bval:
155 bvalues[(x+dx,y+dy)] = bval
156
157 for (x, y), value in bvalues.items():
158 if not canvas.has_key((x,y)):
159 canvas[(x,y)] = dark(value)
160
161 def sysbox(size):
162 canvas = {}
163
164 # The system box of the computer.
165
166 height = int(round(3*size))
167 width = int(round(17*size))
168 depth = int(round(2*size))
169 highlight = int(round(1*size))
170 bothighlight = int(round(0.49*size))
171
172 floppystart = int(round(19*size)) # measured in half-pixels
173 floppyend = int(round(29*size)) # measured in half-pixels
174 floppybottom = height - bothighlight
175 floppyrheight = 0.7 * size
176 floppyheight = int(round(floppyrheight))
177 if floppyheight < 1:
178 floppyheight = 1
179 floppytop = floppybottom - floppyheight
180
181 # The front panel is rectangular.
182 for x in range(width):
183 for y in range(height):
184 grey = 3
185 if x < highlight or y < highlight:
186 grey = grey + 1
187 if x >= width-highlight or y >= height-bothighlight:
188 grey = grey - 1
189 if y < highlight and x >= width-highlight:
190 v = (highlight-1-y) - (x-(width-highlight))
191 if v < 0:
192 grey = grey - 1
193 elif v > 0:
194 grey = grey + 1
195 if y >= floppytop and y < floppybottom and \
196 2*x+2 > floppystart and 2*x < floppyend:
197 if 2*x >= floppystart and 2*x+2 <= floppyend and \
198 floppyrheight >= 0.7:
199 grey = 0
200 else:
201 grey = 2
202 pixel(x, y, greypix(grey/4.0), canvas)
203
204 # The side panel is a parallelogram.
205 for x in range(depth):
206 for y in range(height+1):
207 pixel(x+width, y-(x+1), greypix(0.5), canvas)
208
209 # The top panel is another parallelogram.
210 for x in range(width-1):
211 for y in range(depth):
212 grey = 3
213 if x >= width-1 - highlight:
214 grey = grey + 1
215 pixel(x+(y+1), -(y+1), greypix(grey/4.0), canvas)
216
217 # And draw a border.
218 border(canvas, size, [])
219
220 return canvas
221
222 def monitor(size):
223 canvas = {}
224
225 # The computer's monitor.
226
227 height = int(round(9.55*size))
228 width = int(round(11*size))
229 surround = int(round(1*size))
230 botsurround = int(round(2*size))
231 sheight = height - surround - botsurround
232 swidth = width - 2*surround
233 depth = int(round(2*size))
234 highlight = int(round(math.sqrt(size)))
235 shadow = int(round(0.55*size))
236
237 # The front panel is rectangular.
238 for x in range(width):
239 for y in range(height):
240 if x >= surround and y >= surround and \
241 x < surround+swidth and y < surround+sheight:
242 # Screen.
243 sx = (float(x-surround) - swidth/3) / swidth
244 sy = (float(y-surround) - sheight/3) / sheight
245 shighlight = 1.0 - (sx*sx+sy*sy)*0.27
246 pix = bluepix(shighlight)
247 if x < surround+shadow or y < surround+shadow:
248 pix = blend(cD, pix) # sharp-edged shadow on top and left
249 else:
250 # Complicated double bevel on the screen surround.
251
252 # First, the outer bevel. We compute the distance
253 # from this pixel to each edge of the front
254 # rectangle.
255 list = [
256 (x, +1),
257 (y, +1),
258 (width-1-x, -1),
259 (height-1-y, -1)
260 ]
261 # Now sort the list to find the distance to the
262 # _nearest_ edge, or the two joint nearest.
263 list.sort()
264 # If there's one nearest edge, that determines our
265 # bevel colour. If there are two joint nearest, our
266 # bevel colour is their shared one if they agree,
267 # and neutral otherwise.
268 outerbevel = 0
269 if list[0][0] < list[1][0] or list[0][1] == list[1][1]:
270 if list[0][0] < highlight:
271 outerbevel = list[0][1]
272
273 # Now, the inner bevel. We compute the distance
274 # from this pixel to each edge of the screen
275 # itself.
276 list = [
277 (surround-1-x, -1),
278 (surround-1-y, -1),
279 (x-(surround+swidth), +1),
280 (y-(surround+sheight), +1)
281 ]
282 # Now we sort to find the _maximum_ distance, which
283 # conveniently ignores any less than zero.
284 list.sort()
285 # And now the strategy is pretty much the same as
286 # above, only we're working from the opposite end
287 # of the list.
288 innerbevel = 0
289 if list[-1][0] > list[-2][0] or list[-1][1] == list[-2][1]:
290 if list[-1][0] >= 0 and list[-1][0] < highlight:
291 innerbevel = list[-1][1]
292
293 # Now we know the adjustment we want to make to the
294 # pixel's overall grey shade due to the outer
295 # bevel, and due to the inner one. We break a tie
296 # in favour of a light outer bevel, but otherwise
297 # add.
298 grey = 3
299 if outerbevel > 0 or outerbevel == innerbevel:
300 innerbevel = 0
301 grey = grey + outerbevel + innerbevel
302
303 pix = greypix(grey / 4.0)
304
305 pixel(x, y, pix, canvas)
306
307 # The side panel is a parallelogram.
308 for x in range(depth):
309 for y in range(height):
310 pixel(x+width, y-x, greypix(0.5), canvas)
311
312 # The top panel is another parallelogram.
313 for x in range(width):
314 for y in range(depth-1):
315 pixel(x+(y+1), -(y+1), greypix(0.75), canvas)
316
317 # And draw a border.
318 border(canvas, size, [(0,int(height-1),BL)])
319
320 return canvas
321
322 def computer(size):
323 # Monitor plus sysbox.
324 m = monitor(size)
325 s = sysbox(size)
326 x = int(round((2+size/(size+1))*size))
327 y = int(round(4*size))
328 mb = bbox(m)
329 sb = bbox(s)
330 xoff = sb[0] - mb[0] + x
331 yoff = sb[3] - mb[3] - y
332 overlay(m, xoff, yoff, s)
333 return s
334
335 def lightning(size):
336 canvas = {}
337
338 # The lightning bolt motif.
339
340 # We always want this to be an even number of pixels in span.
341 width = round(7*size) * 2
342 height = round(8*size) * 2
343
344 # The outer edge of each side of the bolt goes to this point.
345 outery = round(8.4*size)
346 outerx = round(11*size)
347
348 # And the inner edge goes to this point.
349 innery = height - 1 - outery
350 innerx = round(7*size)
351
352 for y in range(int(height)):
353 list = []
354 if y <= outery:
355 list.append(width-1-int(outerx * float(y) / outery + 0.3))
356 if y <= innery:
357 list.append(width-1-int(innerx * float(y) / innery + 0.3))
358 y0 = height-1-y
359 if y0 <= outery:
360 list.append(int(outerx * float(y0) / outery + 0.3))
361 if y0 <= innery:
362 list.append(int(innerx * float(y0) / innery + 0.3))
363 list.sort()
364 for x in range(int(list[0]), int(list[-1]+1)):
365 pixel(x, y, cY, canvas)
366
367 # And draw a border.
368 border(canvas, size, [(int(width-1),0,TR), (0,int(height-1),BL)])
369
370 return canvas
371
372 def document(size):
373 canvas = {}
374
375 # The document used in the PSCP/PSFTP icon.
376
377 width = round(13*size)
378 height = round(16*size)
379
380 lineht = round(1*size)
381 if lineht < 1: lineht = 1
382 linespc = round(0.7*size)
383 if linespc < 1: linespc = 1
384 nlines = int((height-linespc)/(lineht+linespc))
385 height = nlines*(lineht+linespc)+linespc # round this so it fits better
386
387 # Start by drawing a big white rectangle.
388 for y in range(int(height)):
389 for x in range(int(width)):
390 pixel(x, y, cW, canvas)
391
392 # Now draw lines of text.
393 for line in range(nlines):
394 # Decide where this line of text begins.
395 if line == 0:
396 start = round(4*size)
397 elif line < 5*nlines/7:
398 start = round((line - (nlines/7)) * size)
399 else:
400 start = round(1*size)
401 if start < round(1*size):
402 start = round(1*size)
403 # Decide where it ends.
404 endpoints = [10, 8, 11, 6, 5, 7, 5]
405 ey = line * 6.0 / (nlines-1)
406 eyf = math.floor(ey)
407 eyc = math.ceil(ey)
408 exf = endpoints[int(eyf)]
409 exc = endpoints[int(eyc)]
410 if eyf == eyc:
411 end = exf
412 else:
413 end = exf * (eyc-ey) + exc * (ey-eyf)
414 end = round(end * size)
415
416 liney = height - (lineht+linespc) * (line+1)
417 for x in range(int(start), int(end)):
418 for y in range(int(lineht)):
419 pixel(x, y+liney, cK, canvas)
420
421 # And draw a border.
422 border(canvas, size, \
423 [(0,0,TL),(int(width-1),0,TR),(0,int(height-1),BL), \
424 (int(width-1),int(height-1),BR)])
425
426 return canvas
427
428 def hat(size):
429 canvas = {}
430
431 # The secret-agent hat in the Pageant icon.
432
433 topa = [6]*9+[5,3,1,0,0,1,2,2,1,1,1,9,9,10,10,11,11,12,12]
434 topa = [round(x*size) for x in topa]
435 botl = round(topa[0]+2.4*math.sqrt(size))
436 botr = round(topa[-1]+2.4*math.sqrt(size))
437 width = round(len(topa)*size)
438
439 # Line equations for the top and bottom of the hat brim, in the
440 # form y=mx+c. c, of course, needs scaling by size, but m is
441 # independent of size.
442 brimm = 1.0 / 3.75
443 brimtopc = round(4*size/3)
444 brimbotc = round(10*size/3)
445
446 for x in range(int(width)):
447 xs = float(x) * (len(topa)-1) / (width-1)
448 xf = math.floor(xs)
449 xc = math.ceil(xs)
450 topf = topa[int(xf)]
451 topc = topa[int(xc)]
452 if xf == xc:
453 top = topf
454 else:
455 top = topf * (xc-xs) + topc * (xs-xf)
456 top = math.floor(top)
457 bot = round(botl + (botr-botl) * x/(width-1))
458
459 for y in range(int(top), int(bot)):
460 pixel(x, y, cK, canvas)
461
462 # Now draw the brim.
463 for x in range(int(width)):
464 brimtop = brimtopc + brimm * x
465 brimbot = brimbotc + brimm * x
466 for y in range(int(math.floor(brimtop)), int(math.ceil(brimbot))):
467 tophere = max(min(brimtop - y, 1), 0)
468 bothere = max(min(brimbot - y, 1), 0)
469 grey = bothere - tophere
470 # Only draw brim pixels over pixels which are (a) part
471 # of the main hat, and (b) not right on its edge.
472 if canvas.has_key((x,y)) and \
473 canvas.has_key((x,y-1)) and \
474 canvas.has_key((x,y+1)) and \
475 canvas.has_key((x-1,y)) and \
476 canvas.has_key((x+1,y)):
477 pixel(x, y, greypix(grey), canvas)
478
479 return canvas
480
481 def key(size):
482 canvas = {}
483
484 # The key in the PuTTYgen icon.
485
486 keyheadw = round(9.5*size)
487 keyheadh = round(12*size)
488 keyholed = round(4*size)
489 keyholeoff = round(2*size)
490 # Ensure keyheadh and keyshafth have the same parity.
491 keyshafth = round((2*size - (int(keyheadh)&1)) / 2) * 2 + (int(keyheadh)&1)
492 keyshaftw = round(18.5*size)
493 keyhead = [round(x*size) for x in [12,11,8,10,9,8,11,12]]
494
495 squarepix = []
496
497 # Ellipse for the key head, minus an off-centre circular hole.
498 for y in range(int(keyheadh)):
499 dy = (y-(keyheadh-1)/2.0) / (keyheadh/2.0)
500 dyh = (y-(keyheadh-1)/2.0) / (keyholed/2.0)
501 for x in range(int(keyheadw)):
502 dx = (x-(keyheadw-1)/2.0) / (keyheadw/2.0)
503 dxh = (x-(keyheadw-1)/2.0-keyholeoff) / (keyholed/2.0)
504 if dy*dy+dx*dx <= 1 and dyh*dyh+dxh*dxh > 1:
505 pixel(x + keyshaftw, y, cy, canvas)
506
507 # Rectangle for the key shaft, extended at the bottom for the
508 # key head detail.
509 for x in range(int(keyshaftw)):
510 top = round((keyheadh - keyshafth) / 2)
511 bot = round((keyheadh + keyshafth) / 2)
512 xs = float(x) * (len(keyhead)-1) / round((len(keyhead)-1)*size)
513 xf = math.floor(xs)
514 xc = math.ceil(xs)
515 in_head = 0
516 if xc < len(keyhead):
517 in_head = 1
518 yf = keyhead[int(xf)]
519 yc = keyhead[int(xc)]
520 if xf == xc:
521 bot = yf
522 else:
523 bot = yf * (xc-xs) + yc * (xs-xf)
524 for y in range(int(top),int(bot)):
525 pixel(x, y, cy, canvas)
526 if in_head:
527 last = (x, y)
528 if x == 0:
529 squarepix.append((x, int(top), TL))
530 if x == 0:
531 squarepix.append(last + (BL,))
532 if last != None and not in_head:
533 squarepix.append(last + (BR,))
534 last = None
535
536 # And draw a border.
537 border(canvas, size, squarepix)
538
539 return canvas
540
541 def linedist(x1,y1, x2,y2, x,y):
542 # Compute the distance from the point x,y to the line segment
543 # joining x1,y1 to x2,y2. Returns the distance vector, measured
544 # with x,y at the origin.
545
546 vectors = []
547
548 # Special case: if x1,y1 and x2,y2 are the same point, we
549 # don't attempt to extrapolate it into a line at all.
550 if x1 != x2 or y1 != y2:
551 # First, find the nearest point to x,y on the infinite
552 # projection of the line segment. So we construct a vector
553 # n perpendicular to that segment...
554 nx = y2-y1
555 ny = x1-x2
556 # ... compute the dot product of (x1,y1)-(x,y) with that
557 # vector...
558 nd = (x1-x)*nx + (y1-y)*ny
559 # ... multiply by the vector we first thought of...
560 ndx = nd * nx
561 ndy = nd * ny
562 # ... and divide twice by the length of n.
563 ndx = ndx / (nx*nx+ny*ny)
564 ndy = ndy / (nx*nx+ny*ny)
565 # That gives us a displacement vector from x,y to the
566 # nearest point. See if it's within the range of the line
567 # segment.
568 cx = x + ndx
569 cy = y + ndy
570 if cx >= min(x1,x2) and cx <= max(x1,x2) and \
571 cy >= min(y1,y2) and cy <= max(y1,y2):
572 vectors.append((ndx,ndy))
573
574 # Now we have up to three candidate result vectors: (ndx,ndy)
575 # as computed just above, and the two vectors to the ends of
576 # the line segment, (x1-x,y1-y) and (x2-x,y2-y). Pick the
577 # shortest.
578 vectors = vectors + [(x1-x,y1-y), (x2-x,y2-y)]
579 bestlen, best = None, None
580 for v in vectors:
581 vlen = v[0]*v[0]+v[1]*v[1]
582 if bestlen == None or bestlen > vlen:
583 bestlen = vlen
584 best = v
585 return best
586
587 def spanner(size):
588 canvas = {}
589
590 # The spanner in the config box icon.
591
592 headcentre = 0.5 + round(4*size)
593 headradius = headcentre + 0.1
594 headhighlight = round(1.5*size)
595 holecentre = 0.5 + round(3*size)
596 holeradius = round(2*size)
597 holehighlight = round(1.5*size)
598 shaftend = 0.5 + round(25*size)
599 shaftwidth = round(2*size)
600 shafthighlight = round(1.5*size)
601 cmax = shaftend + shaftwidth
602
603 # Define three line segments, such that the shortest distance
604 # vectors from any point to each of these segments determines
605 # everything we need to know about where it is on the spanner
606 # shape.
607 segments = [
608 ((0,0), (holecentre, holecentre)),
609 ((headcentre, headcentre), (headcentre, headcentre)),
610 ((headcentre+headradius/math.sqrt(2), headcentre+headradius/math.sqrt(2)),
611 (cmax, cmax))
612 ]
613
614 for y in range(int(cmax)):
615 for x in range(int(cmax)):
616 vectors = [linedist(a,b,c,d,x,y) for ((a,b),(c,d)) in segments]
617 dists = [memoisedsqrt(vx*vx+vy*vy) for (vx,vy) in vectors]
618
619 # If the distance to the hole line is less than
620 # holeradius, we're not part of the spanner.
621 if dists[0] < holeradius:
622 continue
623 # If the distance to the head `line' is less than
624 # headradius, we are part of the spanner; likewise if
625 # the distance to the shaft line is less than
626 # shaftwidth _and_ the resulting shaft point isn't
627 # beyond the shaft end.
628 if dists[1] > headradius and \
629 (dists[2] > shaftwidth or x+vectors[2][0] >= shaftend):
630 continue
631
632 # We're part of the spanner. Now compute the highlight
633 # on this pixel. We do this by computing a `slope
634 # vector', which points from this pixel in the
635 # direction of its nearest edge. We store an array of
636 # slope vectors, in polar coordinates.
637 angles = [math.atan2(vy,vx) for (vx,vy) in vectors]
638 slopes = []
639 if dists[0] < holeradius + holehighlight:
640 slopes.append(((dists[0]-holeradius)/holehighlight,angles[0]))
641 if dists[1]/headradius < dists[2]/shaftwidth:
642 if dists[1] > headradius - headhighlight and dists[1] < headradius:
643 slopes.append(((headradius-dists[1])/headhighlight,math.pi+angles[1]))
644 else:
645 if dists[2] > shaftwidth - shafthighlight and dists[2] < shaftwidth:
646 slopes.append(((shaftwidth-dists[2])/shafthighlight,math.pi+angles[2]))
647 # Now we find the smallest distance in that array, if
648 # any, and that gives us a notional position on a
649 # sphere which we can use to compute the final
650 # highlight level.
651 bestdist = None
652 bestangle = 0
653 for dist, angle in slopes:
654 if bestdist == None or bestdist > dist:
655 bestdist = dist
656 bestangle = angle
657 if bestdist == None:
658 bestdist = 1.0
659 sx = (1.0-bestdist) * math.cos(bestangle)
660 sy = (1.0-bestdist) * math.sin(bestangle)
661 sz = math.sqrt(1.0 - sx*sx - sy*sy)
662 shade = sx-sy+sz / math.sqrt(3) # can range from -1 to +1
663 shade = 1.0 - (1-shade)/3
664
665 pixel(x, y, yellowpix(shade), canvas)
666
667 # And draw a border.
668 border(canvas, size, [])
669
670 return canvas
671
672 # Functions to draw entire icons by composing the above components.
673
674 def xybolt(c1, c2, size, boltoffx=0, boltoffy=0):
675 # Two unspecified objects and a lightning bolt.
676
677 canvas = {}
678 w = h = round(32 * size)
679
680 bolt = lightning(size)
681
682 # Position c2 against the top right of the icon.
683 bb = bbox(c2)
684 assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
685 overlay(c2, w-bb[2], 0-bb[1], canvas)
686 # Position c1 against the bottom left of the icon.
687 bb = bbox(c1)
688 assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
689 overlay(c1, 0-bb[0], h-bb[3], canvas)
690 # Place the lightning bolt artistically off-centre. (The
691 # rationale for this positioning is that it's centred on the
692 # midpoint between the centres of the two monitors in the PuTTY
693 # icon proper, but it's not really feasible to _base_ the
694 # calculation here on that.)
695 bb = bbox(bolt)
696 assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
697 overlay(bolt, (w-bb[0]-bb[2])/2 - round((1-boltoffx)*size), \
698 (h-bb[1]-bb[3])/2 - round((2-boltoffy)*size), canvas)
699
700 return canvas
701
702 def putty_icon(size):
703 return xybolt(computer(size), computer(size), size)
704
705 def puttycfg_icon(size):
706 w = h = round(32 * size)
707 s = spanner(size)
708 canvas = putty_icon(size)
709 # Centre the spanner.
710 bb = bbox(s)
711 overlay(s, (w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2, canvas)
712 return canvas
713
714 def puttygen_icon(size):
715 return xybolt(computer(size), key(size), size, boltoffx=2)
716
717 def pscp_icon(size):
718 return xybolt(document(size), computer(size), size, boltoffx=1)
719
720 def pterm_icon(size):
721 # Just a really big computer.
722
723 canvas = {}
724 w = h = round(32 * size)
725
726 c = computer(size * 1.4)
727
728 # Centre c in the return canvas.
729 bb = bbox(c)
730 assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
731 overlay(c, (w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2, canvas)
732
733 return canvas
734
735 def ptermcfg_icon(size):
736 w = h = round(32 * size)
737 s = spanner(size)
738 canvas = pterm_icon(size)
739 # Centre the spanner.
740 bb = bbox(s)
741 overlay(s, (w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2, canvas)
742 return canvas
743
744 def pageant_icon(size):
745 # A biggish computer, in a hat.
746
747 canvas = {}
748 w = h = round(32 * size)
749
750 c = computer(size * 1.3)
751 ht = hat(size)
752
753 cbb = bbox(c)
754 hbb = bbox(ht)
755
756 # Determine the relative y-coordinates of the computer and hat.
757 # We just centre the one on the other.
758 xrel = (cbb[0]+cbb[2]-hbb[0]-hbb[2])/2
759
760 # Determine the relative y-coordinates of the computer and hat.
761 # We do this by sitting the hat as low down on the computer as
762 # possible without any computer showing over the top. To do
763 # this we first have to find the minimum x coordinate at each
764 # y-coordinate of both components.
765 cty = topy(c)
766 hty = topy(ht)
767 yrelmin = None
768 for cx in cty.keys():
769 hx = cx - xrel
770 assert hty.has_key(hx)
771 yrel = cty[cx] - hty[hx]
772 if yrelmin == None:
773 yrelmin = yrel
774 else:
775 yrelmin = min(yrelmin, yrel)
776
777 # Overlay the hat on the computer.
778 overlay(ht, xrel, yrelmin, c)
779
780 # And centre the result in the main icon canvas.
781 bb = bbox(c)
782 assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
783 overlay(c, (w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2, canvas)
784
785 return canvas
786
787 # Test and output functions.
788
789 import os
790 import sys
791
792 def testrun(func, fname):
793 canvases = []
794 for size in [0.5, 0.6, 1.0, 1.2, 1.5, 4.0]:
795 canvases.append(func(size))
796 wid = 0
797 ht = 0
798 for canvas in canvases:
799 minx, miny, maxx, maxy = bbox(canvas)
800 wid = max(wid, maxx-minx+4)
801 ht = ht + maxy-miny+4
802 block = []
803 for canvas in canvases:
804 minx, miny, maxx, maxy = bbox(canvas)
805 block.extend(render(canvas, minx-2, miny-2, minx-2+wid, maxy+2))
806 p = os.popen("convert -depth 8 -size %dx%d rgb:- %s" % (wid,ht,fname), "w")
807 assert len(block) == ht
808 for line in block:
809 assert len(line) == wid
810 for r, g, b, a in line:
811 # Composite on to orange.
812 r = int(round((r * a + 255 * (255-a)) / 255.0))
813 g = int(round((g * a + 128 * (255-a)) / 255.0))
814 b = int(round((b * a + 0 * (255-a)) / 255.0))
815 p.write("%c%c%c" % (r,g,b))
816 p.close()
817
818 def drawicon(func, width, fname, orangebackground = 0):
819 canvas = func(width / 32.0)
820 finalise(canvas)
821 minx, miny, maxx, maxy = bbox(canvas)
822 assert minx >= 0 and miny >= 0 and maxx <= width and maxy <= width
823
824 block = render(canvas, 0, 0, width, width)
825 p = os.popen("convert -depth 8 -size %dx%d rgba:- %s" % (width,width,fname), "w")
826 assert len(block) == width
827 for line in block:
828 assert len(line) == width
829 for r, g, b, a in line:
830 if orangebackground:
831 # Composite on to orange.
832 r = int(round((r * a + 255 * (255-a)) / 255.0))
833 g = int(round((g * a + 128 * (255-a)) / 255.0))
834 b = int(round((b * a + 0 * (255-a)) / 255.0))
835 a = 255
836 p.write("%c%c%c%c" % (r,g,b,a))
837 p.close()
838
839 args = sys.argv[1:]
840
841 orangebackground = test = 0
842 colours = 1 # 0=mono, 1=16col, 2=truecol
843 doingargs = 1
844
845 realargs = []
846 for arg in args:
847 if doingargs and arg[0] == "-":
848 if arg == "-t":
849 test = 1
850 elif arg == "-it":
851 orangebackground = 1
852 elif arg == "-2":
853 colours = 0
854 elif arg == "-T":
855 colours = 2
856 elif arg == "--":
857 doingargs = 0
858 else:
859 sys.stderr.write("unrecognised option '%s'\n" % arg)
860 sys.exit(1)
861 else:
862 realargs.append(arg)
863
864 if colours == 0:
865 # Monochrome.
866 cK=cr=cg=cb=cm=cc=cP=cw=cR=cG=cB=cM=cC=cD = 0
867 cY=cy=cW = 1
868 cT = -1
869 def greypix(value):
870 return [cK,cW][int(round(value))]
871 def yellowpix(value):
872 return [cK,cW][int(round(value))]
873 def bluepix(value):
874 return cK
875 def dark(value):
876 return [cT,cK][int(round(value))]
877 def blend(col1, col2):
878 if col1 == cT:
879 return col2
880 else:
881 return col1
882 pixvals = [
883 (0x00, 0x00, 0x00, 0xFF), # cK
884 (0xFF, 0xFF, 0xFF, 0xFF), # cW
885 (0x00, 0x00, 0x00, 0x00), # cT
886 ]
887 def outpix(colour):
888 return pixvals[colour]
889 def finalisepix(colour):
890 return colour
891 elif colours == 1:
892 # Windows 16-colour palette.
893 cK,cr,cg,cy,cb,cm,cc,cP,cw,cR,cG,cY,cB,cM,cC,cW = range(16)
894 cT = -1
895 cD = -2 # special translucent half-darkening value used internally
896 def greypix(value):
897 return [cK,cw,cw,cP,cW][int(round(4*value))]
898 def yellowpix(value):
899 return [cK,cy,cY][int(round(2*value))]
900 def bluepix(value):
901 return [cK,cb,cB][int(round(2*value))]
902 def dark(value):
903 return [cT,cD,cK][int(round(2*value))]
904 def blend(col1, col2):
905 if col1 == cT:
906 return col2
907 elif col1 == cD:
908 return [cK,cK,cK,cK,cK,cK,cK,cw,cK,cr,cg,cy,cb,cm,cc,cw,cD,cD][col2]
909 else:
910 return col1
911 pixvals = [
912 (0x00, 0x00, 0x00, 0xFF), # cK
913 (0x80, 0x00, 0x00, 0xFF), # cr
914 (0x00, 0x80, 0x00, 0xFF), # cg
915 (0x80, 0x80, 0x00, 0xFF), # cy
916 (0x00, 0x00, 0x80, 0xFF), # cb
917 (0x80, 0x00, 0x80, 0xFF), # cm
918 (0x00, 0x80, 0x80, 0xFF), # cc
919 (0xC0, 0xC0, 0xC0, 0xFF), # cP
920 (0x80, 0x80, 0x80, 0xFF), # cw
921 (0xFF, 0x00, 0x00, 0xFF), # cR
922 (0x00, 0xFF, 0x00, 0xFF), # cG
923 (0xFF, 0xFF, 0x00, 0xFF), # cY
924 (0x00, 0x00, 0xFF, 0xFF), # cB
925 (0xFF, 0x00, 0xFF, 0xFF), # cM
926 (0x00, 0xFF, 0xFF, 0xFF), # cC
927 (0xFF, 0xFF, 0xFF, 0xFF), # cW
928 (0x00, 0x00, 0x00, 0x80), # cD
929 (0x00, 0x00, 0x00, 0x00), # cT
930 ]
931 def outpix(colour):
932 return pixvals[colour]
933 def finalisepix(colour):
934 # cD is used internally, but can't be output. Convert to cK.
935 if colour == cD:
936 return cK
937 return colour
938 else:
939 # True colour.
940 cK = (0x00, 0x00, 0x00, 0xFF)
941 cr = (0x80, 0x00, 0x00, 0xFF)
942 cg = (0x00, 0x80, 0x00, 0xFF)
943 cy = (0x80, 0x80, 0x00, 0xFF)
944 cb = (0x00, 0x00, 0x80, 0xFF)
945 cm = (0x80, 0x00, 0x80, 0xFF)
946 cc = (0x00, 0x80, 0x80, 0xFF)
947 cP = (0xC0, 0xC0, 0xC0, 0xFF)
948 cw = (0x80, 0x80, 0x80, 0xFF)
949 cR = (0xFF, 0x00, 0x00, 0xFF)
950 cG = (0x00, 0xFF, 0x00, 0xFF)
951 cY = (0xFF, 0xFF, 0x00, 0xFF)
952 cB = (0x00, 0x00, 0xFF, 0xFF)
953 cM = (0xFF, 0x00, 0xFF, 0xFF)
954 cC = (0x00, 0xFF, 0xFF, 0xFF)
955 cW = (0xFF, 0xFF, 0xFF, 0xFF)
956 cD = (0x00, 0x00, 0x00, 0x80)
957 cT = (0x00, 0x00, 0x00, 0x00)
958 def greypix(value):
959 value = max(min(value, 1), 0)
960 return (int(round(0xFF*value)),) * 3 + (0xFF,)
961 def yellowpix(value):
962 value = max(min(value, 1), 0)
963 return (int(round(0xFF*value)),) * 2 + (0, 0xFF)
964 def bluepix(value):
965 value = max(min(value, 1), 0)
966 return (0, 0, int(round(0xFF*value)), 0xFF)
967 def dark(value):
968 value = max(min(value, 1), 0)
969 return (0, 0, 0, int(round(0xFF*value)))
970 def blend(col1, col2):
971 r1,g1,b1,a1 = col1
972 r2,g2,b2,a2 = col2
973 r = int(round((r1*a1 + r2*(0xFF-a1)) / 255.0))
974 g = int(round((g1*a1 + g2*(0xFF-a1)) / 255.0))
975 b = int(round((b1*a1 + b2*(0xFF-a1)) / 255.0))
976 a = int(round((255*a1 + a2*(0xFF-a1)) / 255.0))
977 return r, g, b, a
978 def outpix(colour):
979 return colour
980 if colours == 2:
981 # True colour with no alpha blending: we still have to
982 # finalise half-dark pixels to black.
983 def finalisepix(colour):
984 if colour[3] > 0:
985 return colour[:3] + (0xFF,)
986 return colour
987 else:
988 def finalisepix(colour):
989 return colour
990
991 if test:
992 testrun(eval(realargs[0]), realargs[1])
993 else:
994 drawicon(eval(realargs[0]), int(realargs[1]), realargs[2], orangebackground)