mksvg.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939
  1. #!/usr/bin/env python3
  2. import argparse
  3. import itertools
  4. import math
  5. import os
  6. import sys
  7. from fractions import Fraction
  8. import xml.etree.cElementTree as ET
  9. # Python code which draws the PuTTY icon components in SVG.
  10. def makegroup(*objects):
  11. if len(objects) == 1:
  12. return objects[0]
  13. g = ET.Element("g")
  14. for obj in objects:
  15. g.append(obj)
  16. return g
  17. class Container:
  18. "Empty class for keeping things in."
  19. pass
  20. class SVGthing(object):
  21. def __init__(self):
  22. self.fillc = "none"
  23. self.strokec = "none"
  24. self.strokewidth = 0
  25. self.strokebehind = False
  26. self.clipobj = None
  27. self.props = Container()
  28. def fmt_colour(self, rgb):
  29. return "#{0:02x}{1:02x}{2:02x}".format(*rgb)
  30. def fill(self, colour):
  31. self.fillc = self.fmt_colour(colour)
  32. def stroke(self, colour, width=1, behind=False):
  33. self.strokec = self.fmt_colour(colour)
  34. self.strokewidth = width
  35. self.strokebehind = behind
  36. def clip(self, obj):
  37. self.clipobj = obj
  38. def styles(self, elt, styles):
  39. elt.attrib["style"] = ";".join("{}:{}".format(k,v)
  40. for k,v in sorted(styles.items()))
  41. def add_clip_paths(self, container, idents, X, Y):
  42. if self.clipobj:
  43. self.clipobj.identifier = next(idents)
  44. clipelt = self.clipobj.render_thing(X, Y)
  45. clippath = ET.Element("clipPath")
  46. clippath.attrib["id"] = self.clipobj.identifier
  47. clippath.append(clipelt)
  48. container.append(clippath)
  49. return True
  50. return False
  51. def render(self, X, Y, with_styles=True):
  52. elt = self.render_thing(X, Y)
  53. if self.clipobj:
  54. elt.attrib["clip-path"] = "url(#{})".format(
  55. self.clipobj.identifier)
  56. estyles = {"fill": self.fillc}
  57. sstyles = {"stroke": self.strokec}
  58. if self.strokewidth:
  59. sstyles["stroke-width"] = "{:g}".format(self.strokewidth)
  60. sstyles["stroke-linecap"] = "round"
  61. sstyles["stroke-linejoin"] = "round"
  62. if not self.strokebehind:
  63. estyles.update(sstyles)
  64. if with_styles:
  65. self.styles(elt, estyles)
  66. if not self.strokebehind:
  67. return elt
  68. selt = self.render_thing(X, Y)
  69. if with_styles:
  70. self.styles(selt, sstyles)
  71. return makegroup(selt, elt)
  72. def bbox(self):
  73. it = self.bb_iter()
  74. xmin, ymin = xmax, ymax = next(it)
  75. for x, y in it:
  76. xmin = min(x, xmin)
  77. xmax = max(x, xmax)
  78. ymin = min(y, ymin)
  79. ymax = max(y, ymax)
  80. r = self.strokewidth / 2.0
  81. xmin -= r
  82. ymin -= r
  83. xmax += r
  84. ymax += r
  85. if self.clipobj:
  86. x0, y0, x1, y1 = self.clipobj.bbox()
  87. xmin = max(x0, xmin)
  88. xmax = min(x1, xmax)
  89. ymin = max(y0, ymin)
  90. ymax = min(y1, ymax)
  91. return xmin, ymin, xmax, ymax
  92. class SVGpath(SVGthing):
  93. def __init__(self, pointlists, closed=True):
  94. super().__init__()
  95. self.pointlists = pointlists
  96. self.closed = closed
  97. def bb_iter(self):
  98. for points in self.pointlists:
  99. for x,y,on in points:
  100. yield x,y
  101. def render_thing(self, X, Y):
  102. pathcmds = []
  103. for points in self.pointlists:
  104. while not points[-1][2]:
  105. points = points[1:] + [points[0]]
  106. piter = iter(points)
  107. if self.closed:
  108. xp, yp, _ = points[-1]
  109. pathcmds.extend(["M", X+xp, Y-yp])
  110. else:
  111. xp, yp, on = next(piter)
  112. assert on, "Open paths must start with an on-curve point"
  113. pathcmds.extend(["M", X+xp, Y-yp])
  114. for x, y, on in piter:
  115. if isinstance(on, type(())):
  116. assert on[0] == "arc"
  117. _, rx, ry, rotation, large, sweep = on
  118. pathcmds.extend(["a",
  119. rx, ry, rotation,
  120. 1 if large else 0,
  121. 1 if sweep else 0,
  122. x-xp, -(y-yp)])
  123. elif not on:
  124. x0, y0 = x, y
  125. x1, y1, on = next(piter)
  126. assert not on
  127. x, y, on = next(piter)
  128. assert on
  129. pathcmds.extend(["c", x0-xp, -(y0-yp),
  130. ",", x1-xp, -(y1-yp),
  131. ",", x-xp, -(y-yp)])
  132. elif x == xp:
  133. pathcmds.extend(["v", -(y-yp)])
  134. elif x == xp:
  135. pathcmds.extend(["h", x-xp])
  136. else:
  137. pathcmds.extend(["l", x-xp, -(y-yp)])
  138. xp, yp = x, y
  139. if self.closed:
  140. pathcmds.append("z")
  141. path = ET.Element("path")
  142. path.attrib["d"] = " ".join(str(cmd) for cmd in pathcmds)
  143. return path
  144. class SVGrect(SVGthing):
  145. def __init__(self, x0, y0, x1, y1):
  146. super().__init__()
  147. self.points = x0, y0, x1, y1
  148. def bb_iter(self):
  149. x0, y0, x1, y1 = self.points
  150. return iter([(x0,y0), (x1,y1)])
  151. def render_thing(self, X, Y):
  152. x0, y0, x1, y1 = self.points
  153. rect = ET.Element("rect")
  154. rect.attrib["x"] = "{:g}".format(min(X+x0,X+x1))
  155. rect.attrib["y"] = "{:g}".format(min(Y-y0,Y-y1))
  156. rect.attrib["width"] = "{:g}".format(abs(x0-x1))
  157. rect.attrib["height"] = "{:g}".format(abs(y0-y1))
  158. return rect
  159. class SVGpoly(SVGthing):
  160. def __init__(self, points):
  161. super().__init__()
  162. self.points = points
  163. def bb_iter(self):
  164. return iter(self.points)
  165. def render_thing(self, X, Y):
  166. poly = ET.Element("polygon")
  167. poly.attrib["points"] = " ".join("{:g},{:g}".format(X+x,Y-y)
  168. for x,y in self.points)
  169. return poly
  170. class SVGgroup(object):
  171. def __init__(self, objects, translations=[]):
  172. translations = translations + (
  173. [(0,0)] * (len(objects)-len(translations)))
  174. self.contents = list(zip(objects, translations))
  175. self.props = Container()
  176. def render(self, X, Y):
  177. return makegroup(*[obj.render(X+x, Y-y)
  178. for obj, (x,y) in self.contents])
  179. def add_clip_paths(self, container, idents, X, Y):
  180. toret = False
  181. for obj, (x,y) in self.contents:
  182. if obj.add_clip_paths(container, idents, X+x, Y-y):
  183. toret = True
  184. return toret
  185. def bbox(self):
  186. it = ((x,y) + obj.bbox() for obj, (x,y) in self.contents)
  187. x, y, xmin, ymin, xmax, ymax = next(it)
  188. xmin = x+xmin
  189. ymin = y+ymin
  190. xmax = x+xmax
  191. ymax = y+ymax
  192. for x, y, x0, y0, x1, y1 in it:
  193. xmin = min(x+x0, xmin)
  194. xmax = max(x+x1, xmax)
  195. ymin = min(y+y0, ymin)
  196. ymax = max(y+y1, ymax)
  197. return (xmin, ymin, xmax, ymax)
  198. class SVGtranslate(object):
  199. def __init__(self, obj, translation):
  200. self.obj = obj
  201. self.tx, self.ty = translation
  202. def render(self, X, Y):
  203. return self.obj.render(X+self.tx, Y+self.ty)
  204. def add_clip_paths(self, container, idents, X, Y):
  205. return self.obj.add_clip_paths(container, idents, X+self.tx, Y-self.ty)
  206. def bbox(self):
  207. xmin, ymin, xmax, ymax = self.obj.bbox()
  208. return xmin+self.tx, ymin+self.ty, xmax+self.tx, ymax+self.ty
  209. # Code to actually draw pieces of icon. These don't generally worry
  210. # about positioning within a rectangle; they just draw at a standard
  211. # location, return some useful coordinates, and leave composition
  212. # to other pieces of code.
  213. def sysbox(size):
  214. # The system box of the computer.
  215. height = 3.6*size
  216. width = 16.51*size
  217. depth = 2*size
  218. highlight = 1*size
  219. floppystart = 19*size # measured in half-pixels
  220. floppyend = 29*size # measured in half-pixels
  221. floppybottom = highlight
  222. floppyrheight = 0.7 * size
  223. floppyheight = floppyrheight
  224. if floppyheight < 1:
  225. floppyheight = 1
  226. floppytop = floppybottom + floppyheight
  227. background_coords = [
  228. (0,0), (width,0), (width+depth,depth),
  229. (width+depth,height+depth), (depth,height+depth), (0,height)]
  230. background = SVGpoly(background_coords)
  231. background.fill(greypix(0.75))
  232. hl_dark = SVGpoly([
  233. (highlight,0), (highlight,highlight), (width-highlight,highlight),
  234. (width-highlight,height-highlight), (width+depth,height+depth),
  235. (width+depth,depth), (width,0)])
  236. hl_dark.fill(greypix(0.5))
  237. hl_light = SVGpoly([
  238. (0,highlight), (highlight,highlight), (highlight,height-highlight),
  239. (width-highlight,height-highlight), (width+depth,height+depth),
  240. (width+depth-highlight,height+depth), (width-highlight,height),
  241. (0,height)])
  242. hl_light.fill(cW)
  243. floppy = SVGrect(floppystart/2.0, floppybottom,
  244. floppyend/2.0, floppytop)
  245. floppy.fill(cK)
  246. outline = SVGpoly(background_coords)
  247. outline.stroke(cK, width=0.5)
  248. toret = SVGgroup([background, hl_dark, hl_light, floppy, outline])
  249. toret.props.sysboxheight = height
  250. toret.props.borderthickness = 1 # FIXME
  251. return toret
  252. def monitor(size):
  253. # The computer's monitor.
  254. height = 9.5*size
  255. width = 11.5*size
  256. surround = 1*size
  257. botsurround = 2*size
  258. sheight = height - surround - botsurround
  259. swidth = width - 2*surround
  260. depth = 2*size
  261. highlight = surround/2
  262. shadow = 0.5*size
  263. background_coords = [
  264. (0,0), (width,0), (width+depth,depth),
  265. (width+depth,height+depth), (depth,height+depth), (0,height)]
  266. background = SVGpoly(background_coords)
  267. background.fill(greypix(0.75))
  268. hl0_dark = SVGpoly([
  269. (0,0), (highlight,highlight), (width-highlight,highlight),
  270. (width-highlight,height-highlight), (width+depth,height+depth),
  271. (width+depth,depth), (width,0)])
  272. hl0_dark.fill(greypix(0.5))
  273. hl0_light = SVGpoly([
  274. (0,0), (highlight,highlight), (highlight,height-highlight),
  275. (width-highlight,height-highlight), (width,height), (0,height)])
  276. hl0_light.fill(greypix(1))
  277. hl1_dark = SVGpoly([
  278. (surround-highlight,botsurround-highlight), (surround,botsurround),
  279. (surround,height-surround), (width-surround,height-surround),
  280. (width-surround+highlight,height-surround+highlight),
  281. (surround-highlight,height-surround+highlight)])
  282. hl1_dark.fill(greypix(0.5))
  283. hl1_light = SVGpoly([
  284. (surround-highlight,botsurround-highlight), (surround,botsurround),
  285. (width-surround,botsurround), (width-surround,height-surround),
  286. (width-surround+highlight,height-surround+highlight),
  287. (width-surround+highlight,botsurround-highlight)])
  288. hl1_light.fill(greypix(1))
  289. screen = SVGrect(surround, botsurround, width-surround, height-surround)
  290. screen.fill(bluepix(1))
  291. screenshadow = SVGpoly([
  292. (surround,botsurround), (surround+shadow,botsurround),
  293. (surround+shadow,height-surround-shadow),
  294. (width-surround,height-surround-shadow),
  295. (width-surround,height-surround), (surround,height-surround)])
  296. screenshadow.fill(bluepix(0.5))
  297. outline = SVGpoly(background_coords)
  298. outline.stroke(cK, width=0.5)
  299. toret = SVGgroup([background, hl0_dark, hl0_light, hl1_dark, hl1_light,
  300. screen, screenshadow, outline])
  301. # Give the centre of the screen (for lightning-bolt positioning purposes)
  302. # as the centre of the _light_ area of the screen, not counting the
  303. # shadow on the top and left. I think that looks very slightly nicer.
  304. sbb = (surround+shadow, botsurround, width-surround, height-surround-shadow)
  305. toret.props.screencentre = ((sbb[0]+sbb[2])/2, (sbb[1]+sbb[3])/2)
  306. return toret
  307. def computer(size):
  308. # Monitor plus sysbox.
  309. m = monitor(size)
  310. s = sysbox(size)
  311. x = (2+size/(size+1))*size
  312. y = int(s.props.sysboxheight + s.props.borderthickness)
  313. mb = m.bbox()
  314. sb = s.bbox()
  315. xoff = mb[0] - sb[0] + x
  316. yoff = mb[1] - sb[1] + y
  317. toret = SVGgroup([s, m], [(0,0), (xoff,yoff)])
  318. toret.props.screencentre = (m.props.screencentre[0]+xoff,
  319. m.props.screencentre[1]+yoff)
  320. return toret
  321. def lightning(size):
  322. # The lightning bolt motif.
  323. # Compute the right size of a lightning bolt to exactly connect
  324. # the centres of the two screens in the main PuTTY icon. We'll use
  325. # that size of bolt for all the other icons too, for consistency.
  326. iconw = iconh = 32 * size
  327. cbb = computer(size).bbox()
  328. assert cbb[2]-cbb[0] <= iconw and cbb[3]-cbb[1] <= iconh
  329. width, height = iconw-(cbb[2]-cbb[0]), iconh-(cbb[3]-cbb[1])
  330. degree = math.pi/180
  331. centrethickness = 2*size # top-to-bottom thickness of centre bar
  332. innerangle = 46 * degree # slope of the inner slanting line
  333. outerangle = 39 * degree # slope of the outer one
  334. innery = (height - centrethickness) / 2
  335. outery = (height + centrethickness) / 2
  336. innerx = innery / math.tan(innerangle)
  337. outerx = outery / math.tan(outerangle)
  338. points = [(innerx, innery), (0,0), (outerx, outery)]
  339. points.extend([(width-x, height-y) for x,y in points])
  340. # Fill and stroke the lightning bolt.
  341. #
  342. # Most of the filled-and-stroked objects in these icons are filled
  343. # first, and then stroked with width 0.5, so that the edge of the
  344. # filled area runs down the centre line of the stroke. Put another
  345. # way, half the stroke covers what would have been the filled
  346. # area, and the other half covers the background. This seems like
  347. # the normal way to fill-and-stroke a shape of a given size, and
  348. # SVG makes it easy by allowing us to specify the polygon just
  349. # once with both 'fill' and 'stroke' CSS properties.
  350. #
  351. # But if we did that in this case, then the tips of the lightning
  352. # bolt wouldn't have lightning-colour anywhere near them, because
  353. # the two edges are so close together in angle that the point
  354. # where the strokes would first _not_ overlap would be miles away
  355. # from the logical endpoint.
  356. #
  357. # So, for this one case, we stroke the polygon first at double the
  358. # width, and then fill it on top of that, requiring two copies of
  359. # it in the SVG (though my construction class here hides that
  360. # detail). The effect is that we still get a stroke of visible
  361. # width 0.5, but it's entirely outside the filled area of the
  362. # polygon, so the tips of the yellow interior of the lightning
  363. # bolt are exactly at the logical endpoints.
  364. poly = SVGpoly(points)
  365. poly.fill(cY)
  366. poly.stroke(cK, width=1, behind=True)
  367. poly.props.end1 = (0,0)
  368. poly.props.end2 = (width,height)
  369. return poly
  370. def document(size):
  371. # The document used in the PSCP/PSFTP icon.
  372. width = 13*size
  373. height = 16*size
  374. lineht = 0.875*size
  375. linespc = 1.125*size
  376. nlines = int((height-linespc)/(lineht+linespc))
  377. height = nlines*(lineht+linespc)+linespc # round this so it fits better
  378. paper = SVGrect(0, 0, width, height)
  379. paper.fill(cW)
  380. paper.stroke(cK, width=0.5)
  381. objs = [paper]
  382. # Now draw lines of text.
  383. for line in range(nlines):
  384. # Decide where this line of text begins.
  385. if line == 0:
  386. start = 4*size
  387. elif line < 5*nlines/7:
  388. start = (line * 4/5) * size
  389. else:
  390. start = 1*size
  391. # Decide where it ends.
  392. endpoints = [10, 8, 11, 6, 5, 7, 5]
  393. ey = line * 6.0 / (nlines-1)
  394. eyf = math.floor(ey)
  395. eyc = math.ceil(ey)
  396. exf = endpoints[int(eyf)]
  397. exc = endpoints[int(eyc)]
  398. if eyf == eyc:
  399. end = exf
  400. else:
  401. end = exf * (eyc-ey) + exc * (ey-eyf)
  402. end = end * size
  403. liney = (lineht+linespc) * (line+1)
  404. line = SVGrect(start, liney-lineht, end, liney)
  405. line.fill(cK)
  406. objs.append(line)
  407. return SVGgroup(objs)
  408. def hat(size):
  409. # The secret-agent hat in the Pageant icon.
  410. leftend = (0, -6*size)
  411. rightend = (28*size, -12*size)
  412. dx = rightend[0]-leftend[0]
  413. dy = rightend[1]-leftend[1]
  414. tcentre = (leftend[0] + 0.5*dx - 0.3*dy, leftend[1] + 0.5*dy + 0.3*dx)
  415. hatpoints = [leftend + (True,),
  416. (7.5*size, -6*size, True),
  417. (12*size, 0, True),
  418. (14*size, 3*size, False),
  419. (tcentre[0] - 0.1*dx, tcentre[1] - 0.1*dy, False),
  420. tcentre + (True,)]
  421. for x, y, on in list(reversed(hatpoints))[1:]:
  422. vx, vy = x-tcentre[0], y-tcentre[1]
  423. coeff = float(vx*dx + vy*dy) / float(dx*dx + dy*dy)
  424. rx, ry = x - 2*coeff*dx, y - 2*coeff*dy
  425. hatpoints.append((rx, ry, on))
  426. mainhat = SVGpath([hatpoints])
  427. mainhat.fill(cK)
  428. band = SVGpoly([
  429. (leftend[0] - 0.1*dy, leftend[1] + 0.1*dx),
  430. (rightend[0] - 0.1*dy, rightend[1] + 0.1*dx),
  431. (rightend[0] - 0.15*dy, rightend[1] + 0.15*dx),
  432. (leftend[0] - 0.15*dy, leftend[1] + 0.15*dx)])
  433. band.fill(cW)
  434. band.clip(SVGpath([hatpoints]))
  435. outline = SVGpath([hatpoints])
  436. outline.stroke(cK, width=1)
  437. return SVGgroup([mainhat, band, outline])
  438. def key(size):
  439. # The key in the PuTTYgen icon.
  440. keyheadw = 9.5*size
  441. keyheadh = 12*size
  442. keyholed = 4*size
  443. keyholeoff = 2*size
  444. # Ensure keyheadh and keyshafth have the same parity.
  445. keyshafth = (2*size - (int(keyheadh)&1)) / 2 * 2 + (int(keyheadh)&1)
  446. keyshaftw = 18.5*size
  447. keyheaddetail = [x*size for x in [12,11,8,10,9,8,11,12]]
  448. squarepix = []
  449. keyheadcx = keyshaftw + keyheadw / 2.0
  450. keyheadcy = keyheadh / 2.0
  451. keyshafttop = keyheadcy + keyshafth / 2.0
  452. keyshaftbot = keyheadcy - keyshafth / 2.0
  453. keyhead = [(0, keyshafttop, True), (keyshaftw, keyshafttop, True),
  454. (keyshaftw, keyshaftbot,
  455. ("arc", keyheadw/2.0, keyheadh/2.0, 0, True, True)),
  456. (len(keyheaddetail)*size, keyshaftbot, True)]
  457. for i, h in reversed(list(enumerate(keyheaddetail))):
  458. keyhead.append(((i+1)*size, keyheadh-h, True))
  459. keyhead.append(((i)*size, keyheadh-h, True))
  460. keyholecx = keyheadcx + keyholeoff
  461. keyholecy = keyheadcy
  462. keyholer = keyholed / 2.0
  463. keyhole = [(keyholecx + keyholer, keyholecy,
  464. ("arc", keyholer, keyholer, 0, False, False)),
  465. (keyholecx - keyholer, keyholecy,
  466. ("arc", keyholer, keyholer, 0, False, False))]
  467. outline = SVGpath([keyhead, keyhole])
  468. outline.fill(cy)
  469. outline.stroke(cK, width=0.5)
  470. return outline
  471. def linedist(x1,y1, x2,y2, x,y):
  472. # Compute the distance from the point x,y to the line segment
  473. # joining x1,y1 to x2,y2. Returns the distance vector, measured
  474. # with x,y at the origin.
  475. vectors = []
  476. # Special case: if x1,y1 and x2,y2 are the same point, we
  477. # don't attempt to extrapolate it into a line at all.
  478. if x1 != x2 or y1 != y2:
  479. # First, find the nearest point to x,y on the infinite
  480. # projection of the line segment. So we construct a vector
  481. # n perpendicular to that segment...
  482. nx = y2-y1
  483. ny = x1-x2
  484. # ... compute the dot product of (x1,y1)-(x,y) with that
  485. # vector...
  486. nd = (x1-x)*nx + (y1-y)*ny
  487. # ... multiply by the vector we first thought of...
  488. ndx = nd * nx
  489. ndy = nd * ny
  490. # ... and divide twice by the length of n.
  491. ndx = ndx / (nx*nx+ny*ny)
  492. ndy = ndy / (nx*nx+ny*ny)
  493. # That gives us a displacement vector from x,y to the
  494. # nearest point. See if it's within the range of the line
  495. # segment.
  496. cx = x + ndx
  497. cy = y + ndy
  498. if cx >= min(x1,x2) and cx <= max(x1,x2) and \
  499. cy >= min(y1,y2) and cy <= max(y1,y2):
  500. vectors.append((ndx,ndy))
  501. # Now we have up to three candidate result vectors: (ndx,ndy)
  502. # as computed just above, and the two vectors to the ends of
  503. # the line segment, (x1-x,y1-y) and (x2-x,y2-y). Pick the
  504. # shortest.
  505. vectors = vectors + [(x1-x,y1-y), (x2-x,y2-y)]
  506. bestlen, best = None, None
  507. for v in vectors:
  508. vlen = v[0]*v[0]+v[1]*v[1]
  509. if bestlen == None or bestlen > vlen:
  510. bestlen = vlen
  511. best = v
  512. return best
  513. def spanner(size):
  514. # The spanner in the config box icon.
  515. # Coordinate definitions.
  516. headcentre = 0.5 + 4*size
  517. headradius = headcentre + 0.1
  518. headhighlight = 1.5*size
  519. holecentre = 0.5 + 3*size
  520. holeradius = 2*size
  521. holehighlight = 1.5*size
  522. shaftend = 0.5 + 25*size
  523. shaftwidth = 2*size
  524. shafthighlight = 1.5*size
  525. cmax = shaftend + shaftwidth
  526. # The spanner head is a circle centred at headcentre*(1,1) with
  527. # radius headradius, minus a circle at holecentre*(1,1) with
  528. # radius holeradius, and also minus every translate of that circle
  529. # by a negative real multiple of (1,1).
  530. #
  531. # The spanner handle is a diagonally oriented rectangle, of width
  532. # shaftwidth, with the centre of the far end at shaftend*(1,1),
  533. # and the near end terminating somewhere inside the spanner head
  534. # (doesn't really matter exactly where).
  535. #
  536. # Hence, in SVG we can represent the shape using a path of
  537. # straight lines and circular arcs. But first we need to calculate
  538. # the points where the straight lines meet the spanner head circle.
  539. headpt = lambda a, on=True: (headcentre+headradius*math.cos(a),
  540. -headcentre+headradius*math.sin(a), on)
  541. holept = lambda a, on=True: (holecentre+holeradius*math.cos(a),
  542. -holecentre+holeradius*math.sin(a), on)
  543. # Now we can specify the path.
  544. spannercoords = [[
  545. holept(math.pi*5/4),
  546. holept(math.pi*1/4, ("arc", holeradius,holeradius,0, False, False)),
  547. headpt(math.pi*3/4 - math.asin(holeradius/headradius)),
  548. headpt(math.pi*7/4 + math.asin(shaftwidth/headradius),
  549. ("arc", headradius,headradius,0, False, True)),
  550. (shaftend+math.sqrt(0.5)*shaftwidth,
  551. -shaftend+math.sqrt(0.5)*shaftwidth, True),
  552. (shaftend-math.sqrt(0.5)*shaftwidth,
  553. -shaftend-math.sqrt(0.5)*shaftwidth, True),
  554. headpt(math.pi*7/4 - math.asin(shaftwidth/headradius)),
  555. headpt(math.pi*3/4 + math.asin(holeradius/headradius),
  556. ("arc", headradius,headradius,0, False, True)),
  557. ]]
  558. base = SVGpath(spannercoords)
  559. base.fill(cY)
  560. shadowthickness = 2*size
  561. sx, sy, _ = holept(math.pi*5/4)
  562. sx += math.sqrt(0.5) * shadowthickness/2
  563. sy += math.sqrt(0.5) * shadowthickness/2
  564. sr = holeradius - shadowthickness/2
  565. shadow = SVGpath([
  566. [(sx, sy, sr),
  567. holept(math.pi*1/4, ("arc", sr, sr, 0, False, False)),
  568. headpt(math.pi*3/4 - math.asin(holeradius/headradius))],
  569. [(shaftend-math.sqrt(0.5)*shaftwidth,
  570. -shaftend-math.sqrt(0.5)*shaftwidth, True),
  571. headpt(math.pi*7/4 - math.asin(shaftwidth/headradius)),
  572. headpt(math.pi*3/4 + math.asin(holeradius/headradius),
  573. ("arc", headradius,headradius,0, False, True))],
  574. ], closed=False)
  575. shadow.clip(SVGpath(spannercoords))
  576. shadow.stroke(cy, width=shadowthickness)
  577. outline = SVGpath(spannercoords)
  578. outline.stroke(cK, width=0.5)
  579. return SVGgroup([base, shadow, outline])
  580. def box(size, wantback):
  581. # The back side of the cardboard box in the installer icon.
  582. boxwidth = 15 * size
  583. boxheight = 12 * size
  584. boxdepth = 4 * size
  585. boxfrontflapheight = 5 * size
  586. boxrightflapheight = 3 * size
  587. # Three shades of basically acceptable brown, all achieved by
  588. # halftoning between two of the Windows-16 colours. I'm quite
  589. # pleased that was feasible at all!
  590. dark = halftone(cr, cK)
  591. med = halftone(cr, cy)
  592. light = halftone(cr, cY)
  593. # We define our halftoning parity in such a way that the black
  594. # pixels along the RHS of the visible part of the box back
  595. # match up with the one-pixel black outline around the
  596. # right-hand side of the box. In other words, we want the pixel
  597. # at (-1, boxwidth-1) to be black, and hence the one at (0,
  598. # boxwidth) too.
  599. parityadjust = int(boxwidth) % 2
  600. # The back of the box.
  601. if wantback:
  602. back = SVGpoly([
  603. (0,0), (boxwidth,0), (boxwidth+boxdepth,boxdepth),
  604. (boxwidth+boxdepth,boxheight+boxdepth),
  605. (boxdepth,boxheight+boxdepth), (0,boxheight)])
  606. back.fill(dark)
  607. back.stroke(cK, width=0.5)
  608. return back
  609. # The front face of the box.
  610. front = SVGrect(0, 0, boxwidth, boxheight)
  611. front.fill(med)
  612. front.stroke(cK, width=0.5)
  613. # The right face of the box.
  614. right = SVGpoly([
  615. (boxwidth,0), (boxwidth+boxdepth,boxdepth),
  616. (boxwidth+boxdepth,boxheight+boxdepth), (boxwidth,boxheight)])
  617. right.fill(dark)
  618. right.stroke(cK, width=0.5)
  619. frontflap = SVGpoly([
  620. (0,boxheight), (boxwidth,boxheight),
  621. (boxwidth-boxfrontflapheight/2, boxheight-boxfrontflapheight),
  622. (-boxfrontflapheight/2, boxheight-boxfrontflapheight)])
  623. frontflap.stroke(cK, width=0.5)
  624. frontflap.fill(light)
  625. rightflap = SVGpoly([
  626. (boxwidth,boxheight), (boxwidth+boxdepth,boxheight+boxdepth),
  627. (boxwidth+boxdepth+boxrightflapheight,
  628. boxheight+boxdepth-boxrightflapheight),
  629. (boxwidth+boxrightflapheight,boxheight-boxrightflapheight)])
  630. rightflap.stroke(cK, width=0.5)
  631. rightflap.fill(med)
  632. return SVGgroup([front, right, frontflap, rightflap])
  633. def boxback(size):
  634. return box(size, 1)
  635. def boxfront(size):
  636. return box(size, 0)
  637. # Functions to draw entire icons by composing the above components.
  638. def xybolt(c1, c2, size, boltoffx=0, boltoffy=0, c1bb=None, c2bb=None):
  639. # Two unspecified objects and a lightning bolt.
  640. w = h = 32 * size
  641. bolt = lightning(size)
  642. objs = [c2, c1, bolt]
  643. origins = [None] * 3
  644. # Position c2 against the top right of the icon.
  645. bb = c2bb if c2bb is not None else c2.bbox()
  646. assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
  647. origins[0] = w-bb[2], h-bb[3]
  648. # Position c1 against the bottom left of the icon.
  649. bb = c1bb if c1bb is not None else c1.bbox()
  650. assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
  651. origins[1] = 0-bb[0], 0-bb[1]
  652. # Place the lightning bolt so that it ends precisely at the centre
  653. # of the monitor, in whichever of the two sub-pictures has one.
  654. # (In the case of the PuTTY icon proper, in which _both_
  655. # sub-pictures are computers, it should line up correctly for both.)
  656. origin1 = origin2 = None
  657. if hasattr(c1.props, "screencentre"):
  658. origin1 = (
  659. c1.props.screencentre[0] + origins[1][0] - bolt.props.end1[0],
  660. c1.props.screencentre[1] + origins[1][1] - bolt.props.end1[1])
  661. if hasattr(c2.props, "screencentre"):
  662. origin2 = (
  663. c2.props.screencentre[0] + origins[0][0] - bolt.props.end2[0],
  664. c2.props.screencentre[1] + origins[0][1] - bolt.props.end2[1])
  665. if origin1 is not None and origin2 is not None:
  666. assert math.hypot(origin1[0]-origin2[0],origin1[1]-origin2[1]<1e-5), (
  667. "Lightning bolt didn't line up! Off by {}*size".format(
  668. ((origin1[0]-origin2[0])/size,
  669. (origin1[1]-origin2[1])/size)))
  670. origins[2] = origin1 if origin1 is not None else origin2
  671. assert origins[2] is not None, "Need at least one computer to line up bolt"
  672. toret = SVGgroup(objs, origins)
  673. toret.props.c1pos = origins[1]
  674. toret.props.c2pos = origins[0]
  675. return toret
  676. def putty_icon(size):
  677. return xybolt(computer(size), computer(size), size)
  678. def puttycfg_icon(size):
  679. w = h = 32 * size
  680. s = spanner(size)
  681. b = putty_icon(size)
  682. bb = s.bbox()
  683. return SVGgroup([b, s], [(0,0), ((w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2)])
  684. def puttygen_icon(size):
  685. k = key(size)
  686. # Manually move the key around, by pretending to xybolt that its
  687. # bounding box is offset from where it really is.
  688. kbb = SVGtranslate(k,(2*size,5*size)).bbox()
  689. return xybolt(computer(size), k, size, boltoffx=2, c2bb=kbb)
  690. def pscp_icon(size):
  691. return xybolt(document(size), computer(size), size)
  692. def puttyins_icon(size):
  693. boxfront = box(size, False)
  694. boxback = box(size, True)
  695. # The box back goes behind the lightning bolt.
  696. most = xybolt(boxback, computer(size), size, c1bb=boxfront.bbox(),
  697. boltoffx=-2, boltoffy=+1)
  698. # But the box front goes over the top, so that the lightning
  699. # bolt appears to come _out_ of the box. Here it's useful to
  700. # know the exact coordinates where xybolt placed the box back,
  701. # so we can overlay the box front exactly on top of it.
  702. c1x, c1y = most.props.c1pos
  703. return SVGgroup([most, boxfront], [(0,0), most.props.c1pos])
  704. def pterm_icon(size):
  705. # Just a really big computer.
  706. w = h = 32 * size
  707. c = computer(size * 1.4)
  708. # Centre c in the output rectangle.
  709. bb = c.bbox()
  710. assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
  711. return SVGgroup([c], [((w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2)])
  712. def ptermcfg_icon(size):
  713. w = h = 32 * size
  714. s = spanner(size)
  715. b = pterm_icon(size)
  716. bb = s.bbox()
  717. return SVGgroup([b, s], [(0,0), ((w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2)])
  718. def pageant_icon(size):
  719. # A biggish computer, in a hat.
  720. w = h = 32 * size
  721. c = computer(size * 1.2)
  722. ht = hat(size)
  723. cbb = c.bbox()
  724. hbb = ht.bbox()
  725. # Determine the relative coordinates of the computer and hat. We
  726. # do this by first centring one on the other, then adjusting by
  727. # hand.
  728. xrel = (cbb[0]+cbb[2]-hbb[0]-hbb[2])/2 + 2*size
  729. yrel = (cbb[1]+cbb[3]-hbb[1]-hbb[3])/2 + 12*size
  730. both = SVGgroup([c, ht], [(0,0), (xrel,yrel)])
  731. # Mostly-centre the result in the output rectangle. We want
  732. # everything to fit in frame, but we also want to make it look as
  733. # if the computer is more x-centred than the hat.
  734. # Coordinates that would centre the whole group.
  735. bb = both.bbox()
  736. assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
  737. grx, gry = (w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2
  738. # Coords that would centre just the computer.
  739. bb = c.bbox()
  740. crx, cry = (w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2
  741. # Use gry unchanged, but linear-combine grx with crx.
  742. return SVGgroup([both], [(grx+0.6*(crx-grx), gry)])
  743. # Test and output functions.
  744. cK = (0x00, 0x00, 0x00, 0xFF)
  745. cr = (0x80, 0x00, 0x00, 0xFF)
  746. cg = (0x00, 0x80, 0x00, 0xFF)
  747. cy = (0x80, 0x80, 0x00, 0xFF)
  748. cb = (0x00, 0x00, 0x80, 0xFF)
  749. cm = (0x80, 0x00, 0x80, 0xFF)
  750. cc = (0x00, 0x80, 0x80, 0xFF)
  751. cP = (0xC0, 0xC0, 0xC0, 0xFF)
  752. cw = (0x80, 0x80, 0x80, 0xFF)
  753. cR = (0xFF, 0x00, 0x00, 0xFF)
  754. cG = (0x00, 0xFF, 0x00, 0xFF)
  755. cY = (0xFF, 0xFF, 0x00, 0xFF)
  756. cB = (0x00, 0x00, 0xFF, 0xFF)
  757. cM = (0xFF, 0x00, 0xFF, 0xFF)
  758. cC = (0x00, 0xFF, 0xFF, 0xFF)
  759. cW = (0xFF, 0xFF, 0xFF, 0xFF)
  760. cD = (0x00, 0x00, 0x00, 0x80)
  761. cT = (0x00, 0x00, 0x00, 0x00)
  762. def greypix(value):
  763. value = max(min(value, 1), 0)
  764. return (int(round(0xFF*value)),) * 3 + (0xFF,)
  765. def yellowpix(value):
  766. value = max(min(value, 1), 0)
  767. return (int(round(0xFF*value)),) * 2 + (0, 0xFF)
  768. def bluepix(value):
  769. value = max(min(value, 1), 0)
  770. return (0, 0, int(round(0xFF*value)), 0xFF)
  771. def dark(value):
  772. value = max(min(value, 1), 0)
  773. return (0, 0, 0, int(round(0xFF*value)))
  774. def blend(col1, col2):
  775. r1,g1,b1,a1 = col1
  776. r2,g2,b2,a2 = col2
  777. r = int(round((r1*a1 + r2*(0xFF-a1)) / 255.0))
  778. g = int(round((g1*a1 + g2*(0xFF-a1)) / 255.0))
  779. b = int(round((b1*a1 + b2*(0xFF-a1)) / 255.0))
  780. a = int(round((255*a1 + a2*(0xFF-a1)) / 255.0))
  781. return r, g, b, a
  782. def halftone(col1, col2):
  783. r1,g1,b1,a1 = col1
  784. r2,g2,b2,a2 = col2
  785. return ((r1+r2)//2, (g1+g2)//2, (b1+b2)//2, (a1+a2)//2)
  786. def drawicon(func, width, fname):
  787. icon = func(width / 32.0)
  788. minx, miny, maxx, maxy = icon.bbox()
  789. #assert minx >= 0 and miny >= 0 and maxx <= width and maxy <= width
  790. svgroot = ET.Element("svg")
  791. svgroot.attrib["xmlns"] = "http://www.w3.org/2000/svg"
  792. svgroot.attrib["viewBox"] = "0 0 {w:d} {w:d}".format(w=width)
  793. defs = ET.Element("defs")
  794. idents = ("iconid{:d}".format(n) for n in itertools.count())
  795. if icon.add_clip_paths(defs, idents, 0, width):
  796. svgroot.append(defs)
  797. svgroot.append(icon.render(0,width))
  798. ET.ElementTree(svgroot).write(fname)
  799. def main():
  800. parser = argparse.ArgumentParser(description='Generate PuTTY SVG icons.')
  801. parser.add_argument("icon", help="Which icon to generate.")
  802. parser.add_argument("-s", "--size", type=int, default=48,
  803. help="Notional pixel size to base the SVG on.")
  804. parser.add_argument("-o", "--output", required=True,
  805. help="Output file name.")
  806. args = parser.parse_args()
  807. drawicon(eval(args.icon), args.size, args.output)
  808. if __name__ == '__main__':
  809. main()