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