mkicon.py 37 KB

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