mkicon.py 36 KB

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