mirror of
https://git.tartarus.org/simon/putty.git
synced 2025-01-09 01:18:00 +00:00
a101444d40
This gets us scalable icons that will go to extremely large sizes without the problems that arise from scaling up the output of mkicon.py, in which outlines become too thin because the script was mostly concerned with trying to squeeze all the desired detail into _tiny_ sizes. The SVG icons are generated by mksvg.py, which is a conversion of the existing mkicon.py. So the SVG files themselves are not committed in this repo; 'make svg' in the icons subdir will generate them. (I haven't decided yet whether this state of affairs should be permanent. Perhaps _having_ generated the SVGs via a similar program to the bitmap icons, we should regard the script as a discardable booster stage and redesignate the SVGs themselves as the source format for future modifications, so that they can be edited in Inkscape or similar rather than by tinkering with Python. On the other hand, perhaps keeping the script will make it easier to keep the icon family consistent, e.g. if changing the style of one of the shared visual components.) My plan is that we should stick with the output of the previous bitmap-generating script for all the _small_ icons, up to and including 48 pixels, because it does a better job at low resolution. (That was really what it was for in the first place: you can think of it as an analogue of a scalable-font hinting system, to tune the scaling for very low res so that all the important features are still visible.) I think probably I want to switch the 128-pixel icons used in the Mac icon file over to being rendered from the SVG (though in this commit I haven't gone that far, not least because I'll also need to prepare a corresponding black and white version). I haven't done extensive research yet to decide where I think the crossover point in between is.
939 lines
32 KiB
Python
Executable File
939 lines
32 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import itertools
|
|
import math
|
|
import os
|
|
import sys
|
|
from fractions import Fraction
|
|
|
|
import xml.etree.cElementTree as ET
|
|
|
|
# Python code which draws the PuTTY icon components in SVG.
|
|
|
|
def makegroup(*objects):
|
|
if len(objects) == 1:
|
|
return objects[0]
|
|
g = ET.Element("g")
|
|
for obj in objects:
|
|
g.append(obj)
|
|
return g
|
|
|
|
class Container:
|
|
"Empty class for keeping things in."
|
|
pass
|
|
|
|
class SVGthing(object):
|
|
def __init__(self):
|
|
self.fillc = "none"
|
|
self.strokec = "none"
|
|
self.strokewidth = 0
|
|
self.strokebehind = False
|
|
self.clipobj = None
|
|
self.props = Container()
|
|
def fmt_colour(self, rgb):
|
|
return "#{0:02x}{1:02x}{2:02x}".format(*rgb)
|
|
def fill(self, colour):
|
|
self.fillc = self.fmt_colour(colour)
|
|
def stroke(self, colour, width=1, behind=False):
|
|
self.strokec = self.fmt_colour(colour)
|
|
self.strokewidth = width
|
|
self.strokebehind = behind
|
|
def clip(self, obj):
|
|
self.clipobj = obj
|
|
def styles(self, elt, styles):
|
|
elt.attrib["style"] = ";".join("{}:{}".format(k,v)
|
|
for k,v in sorted(styles.items()))
|
|
def add_clip_paths(self, container, idents, X, Y):
|
|
if self.clipobj:
|
|
self.clipobj.identifier = next(idents)
|
|
clipelt = self.clipobj.render_thing(X, Y)
|
|
clippath = ET.Element("clipPath")
|
|
clippath.attrib["id"] = self.clipobj.identifier
|
|
clippath.append(clipelt)
|
|
container.append(clippath)
|
|
return True
|
|
return False
|
|
def render(self, X, Y, with_styles=True):
|
|
elt = self.render_thing(X, Y)
|
|
if self.clipobj:
|
|
elt.attrib["clip-path"] = "url(#{})".format(
|
|
self.clipobj.identifier)
|
|
estyles = {"fill": self.fillc}
|
|
sstyles = {"stroke": self.strokec}
|
|
if self.strokewidth:
|
|
sstyles["stroke-width"] = "{:g}".format(self.strokewidth)
|
|
sstyles["stroke-linecap"] = "round"
|
|
sstyles["stroke-linejoin"] = "round"
|
|
if not self.strokebehind:
|
|
estyles.update(sstyles)
|
|
if with_styles:
|
|
self.styles(elt, estyles)
|
|
if not self.strokebehind:
|
|
return elt
|
|
selt = self.render_thing(X, Y)
|
|
if with_styles:
|
|
self.styles(selt, sstyles)
|
|
return makegroup(selt, elt)
|
|
def bbox(self):
|
|
it = self.bb_iter()
|
|
xmin, ymin = xmax, ymax = next(it)
|
|
for x, y in it:
|
|
xmin = min(x, xmin)
|
|
xmax = max(x, xmax)
|
|
ymin = min(y, ymin)
|
|
ymax = max(y, ymax)
|
|
r = self.strokewidth / 2.0
|
|
xmin -= r
|
|
ymin -= r
|
|
xmax += r
|
|
ymax += r
|
|
if self.clipobj:
|
|
x0, y0, x1, y1 = self.clipobj.bbox()
|
|
xmin = max(x0, xmin)
|
|
xmax = min(x1, xmax)
|
|
ymin = max(y0, ymin)
|
|
ymax = min(y1, ymax)
|
|
return xmin, ymin, xmax, ymax
|
|
|
|
class SVGpath(SVGthing):
|
|
def __init__(self, pointlists, closed=True):
|
|
super().__init__()
|
|
self.pointlists = pointlists
|
|
self.closed = closed
|
|
def bb_iter(self):
|
|
for points in self.pointlists:
|
|
for x,y,on in points:
|
|
yield x,y
|
|
def render_thing(self, X, Y):
|
|
pathcmds = []
|
|
|
|
for points in self.pointlists:
|
|
while not points[-1][2]:
|
|
points = points[1:] + [points[0]]
|
|
|
|
piter = iter(points)
|
|
|
|
if self.closed:
|
|
xp, yp, _ = points[-1]
|
|
pathcmds.extend(["M", X+xp, Y-yp])
|
|
else:
|
|
xp, yp, on = next(piter)
|
|
assert on, "Open paths must start with an on-curve point"
|
|
pathcmds.extend(["M", X+xp, Y-yp])
|
|
|
|
for x, y, on in piter:
|
|
if isinstance(on, type(())):
|
|
assert on[0] == "arc"
|
|
_, rx, ry, rotation, large, sweep = on
|
|
pathcmds.extend(["a",
|
|
rx, ry, rotation,
|
|
1 if large else 0,
|
|
1 if sweep else 0,
|
|
x-xp, -(y-yp)])
|
|
elif not on:
|
|
x0, y0 = x, y
|
|
x1, y1, on = next(piter)
|
|
assert not on
|
|
x, y, on = next(piter)
|
|
assert on
|
|
pathcmds.extend(["c", x0-xp, -(y0-yp),
|
|
",", x1-xp, -(y1-yp),
|
|
",", x-xp, -(y-yp)])
|
|
elif x == xp:
|
|
pathcmds.extend(["v", -(y-yp)])
|
|
elif x == xp:
|
|
pathcmds.extend(["h", x-xp])
|
|
else:
|
|
pathcmds.extend(["l", x-xp, -(y-yp)])
|
|
|
|
xp, yp = x, y
|
|
|
|
if self.closed:
|
|
pathcmds.append("z")
|
|
|
|
path = ET.Element("path")
|
|
path.attrib["d"] = " ".join(str(cmd) for cmd in pathcmds)
|
|
return path
|
|
|
|
class SVGrect(SVGthing):
|
|
def __init__(self, x0, y0, x1, y1):
|
|
super().__init__()
|
|
self.points = x0, y0, x1, y1
|
|
def bb_iter(self):
|
|
x0, y0, x1, y1 = self.points
|
|
return iter([(x0,y0), (x1,y1)])
|
|
def render_thing(self, X, Y):
|
|
x0, y0, x1, y1 = self.points
|
|
rect = ET.Element("rect")
|
|
rect.attrib["x"] = "{:g}".format(min(X+x0,X+x1))
|
|
rect.attrib["y"] = "{:g}".format(min(Y-y0,Y-y1))
|
|
rect.attrib["width"] = "{:g}".format(abs(x0-x1))
|
|
rect.attrib["height"] = "{:g}".format(abs(y0-y1))
|
|
return rect
|
|
|
|
class SVGpoly(SVGthing):
|
|
def __init__(self, points):
|
|
super().__init__()
|
|
self.points = points
|
|
def bb_iter(self):
|
|
return iter(self.points)
|
|
def render_thing(self, X, Y):
|
|
poly = ET.Element("polygon")
|
|
poly.attrib["points"] = " ".join("{:g},{:g}".format(X+x,Y-y)
|
|
for x,y in self.points)
|
|
return poly
|
|
|
|
class SVGgroup(object):
|
|
def __init__(self, objects, translations=[]):
|
|
translations = translations + (
|
|
[(0,0)] * (len(objects)-len(translations)))
|
|
self.contents = list(zip(objects, translations))
|
|
self.props = Container()
|
|
def render(self, X, Y):
|
|
return makegroup(*[obj.render(X+x, Y-y)
|
|
for obj, (x,y) in self.contents])
|
|
def add_clip_paths(self, container, idents, X, Y):
|
|
toret = False
|
|
for obj, (x,y) in self.contents:
|
|
if obj.add_clip_paths(container, idents, X+x, Y-y):
|
|
toret = True
|
|
return toret
|
|
def bbox(self):
|
|
it = ((x,y) + obj.bbox() for obj, (x,y) in self.contents)
|
|
x, y, xmin, ymin, xmax, ymax = next(it)
|
|
xmin = x+xmin
|
|
ymin = y+ymin
|
|
xmax = x+xmax
|
|
ymax = y+ymax
|
|
for x, y, x0, y0, x1, y1 in it:
|
|
xmin = min(x+x0, xmin)
|
|
xmax = max(x+x1, xmax)
|
|
ymin = min(y+y0, ymin)
|
|
ymax = max(y+y1, ymax)
|
|
return (xmin, ymin, xmax, ymax)
|
|
|
|
class SVGtranslate(object):
|
|
def __init__(self, obj, translation):
|
|
self.obj = obj
|
|
self.tx, self.ty = translation
|
|
def render(self, X, Y):
|
|
return self.obj.render(X+self.tx, Y+self.ty)
|
|
def add_clip_paths(self, container, idents, X, Y):
|
|
return self.obj.add_clip_paths(container, idents, X+self.tx, Y-self.ty)
|
|
def bbox(self):
|
|
xmin, ymin, xmax, ymax = self.obj.bbox()
|
|
return xmin+self.tx, ymin+self.ty, xmax+self.tx, ymax+self.ty
|
|
|
|
# Code to actually draw pieces of icon. These don't generally worry
|
|
# about positioning within a rectangle; they just draw at a standard
|
|
# location, return some useful coordinates, and leave composition
|
|
# to other pieces of code.
|
|
|
|
def sysbox(size):
|
|
# The system box of the computer.
|
|
|
|
height = 3.6*size
|
|
width = 16.51*size
|
|
depth = 2*size
|
|
highlight = 1*size
|
|
|
|
floppystart = 19*size # measured in half-pixels
|
|
floppyend = 29*size # measured in half-pixels
|
|
floppybottom = highlight
|
|
floppyrheight = 0.7 * size
|
|
floppyheight = floppyrheight
|
|
if floppyheight < 1:
|
|
floppyheight = 1
|
|
floppytop = floppybottom + floppyheight
|
|
|
|
background_coords = [
|
|
(0,0), (width,0), (width+depth,depth),
|
|
(width+depth,height+depth), (depth,height+depth), (0,height)]
|
|
background = SVGpoly(background_coords)
|
|
background.fill(greypix(0.75))
|
|
|
|
hl_dark = SVGpoly([
|
|
(highlight,0), (highlight,highlight), (width-highlight,highlight),
|
|
(width-highlight,height-highlight), (width+depth,height+depth),
|
|
(width+depth,depth), (width,0)])
|
|
hl_dark.fill(greypix(0.5))
|
|
|
|
hl_light = SVGpoly([
|
|
(0,highlight), (highlight,highlight), (highlight,height-highlight),
|
|
(width-highlight,height-highlight), (width+depth,height+depth),
|
|
(width+depth-highlight,height+depth), (width-highlight,height),
|
|
(0,height)])
|
|
hl_light.fill(cW)
|
|
|
|
floppy = SVGrect(floppystart/2.0, floppybottom,
|
|
floppyend/2.0, floppytop)
|
|
floppy.fill(cK)
|
|
|
|
outline = SVGpoly(background_coords)
|
|
outline.stroke(cK, width=0.5)
|
|
|
|
toret = SVGgroup([background, hl_dark, hl_light, floppy, outline])
|
|
toret.props.sysboxheight = height
|
|
toret.props.borderthickness = 1 # FIXME
|
|
return toret
|
|
|
|
def monitor(size):
|
|
# The computer's monitor.
|
|
|
|
height = 9.5*size
|
|
width = 11.5*size
|
|
surround = 1*size
|
|
botsurround = 2*size
|
|
sheight = height - surround - botsurround
|
|
swidth = width - 2*surround
|
|
depth = 2*size
|
|
highlight = surround/2
|
|
shadow = 0.5*size
|
|
|
|
background_coords = [
|
|
(0,0), (width,0), (width+depth,depth),
|
|
(width+depth,height+depth), (depth,height+depth), (0,height)]
|
|
background = SVGpoly(background_coords)
|
|
background.fill(greypix(0.75))
|
|
|
|
hl0_dark = SVGpoly([
|
|
(0,0), (highlight,highlight), (width-highlight,highlight),
|
|
(width-highlight,height-highlight), (width+depth,height+depth),
|
|
(width+depth,depth), (width,0)])
|
|
hl0_dark.fill(greypix(0.5))
|
|
|
|
hl0_light = SVGpoly([
|
|
(0,0), (highlight,highlight), (highlight,height-highlight),
|
|
(width-highlight,height-highlight), (width,height), (0,height)])
|
|
hl0_light.fill(greypix(1))
|
|
|
|
hl1_dark = SVGpoly([
|
|
(surround-highlight,botsurround-highlight), (surround,botsurround),
|
|
(surround,height-surround), (width-surround,height-surround),
|
|
(width-surround+highlight,height-surround+highlight),
|
|
(surround-highlight,height-surround+highlight)])
|
|
hl1_dark.fill(greypix(0.5))
|
|
|
|
hl1_light = SVGpoly([
|
|
(surround-highlight,botsurround-highlight), (surround,botsurround),
|
|
(width-surround,botsurround), (width-surround,height-surround),
|
|
(width-surround+highlight,height-surround+highlight),
|
|
(width-surround+highlight,botsurround-highlight)])
|
|
hl1_light.fill(greypix(1))
|
|
|
|
screen = SVGrect(surround, botsurround, width-surround, height-surround)
|
|
screen.fill(bluepix(1))
|
|
|
|
screenshadow = SVGpoly([
|
|
(surround,botsurround), (surround+shadow,botsurround),
|
|
(surround+shadow,height-surround-shadow),
|
|
(width-surround,height-surround-shadow),
|
|
(width-surround,height-surround), (surround,height-surround)])
|
|
screenshadow.fill(bluepix(0.5))
|
|
|
|
outline = SVGpoly(background_coords)
|
|
outline.stroke(cK, width=0.5)
|
|
|
|
toret = SVGgroup([background, hl0_dark, hl0_light, hl1_dark, hl1_light,
|
|
screen, screenshadow, outline])
|
|
# Give the centre of the screen (for lightning-bolt positioning purposes)
|
|
# as the centre of the _light_ area of the screen, not counting the
|
|
# shadow on the top and left. I think that looks very slightly nicer.
|
|
sbb = (surround+shadow, botsurround, width-surround, height-surround-shadow)
|
|
toret.props.screencentre = ((sbb[0]+sbb[2])/2, (sbb[1]+sbb[3])/2)
|
|
return toret
|
|
|
|
def computer(size):
|
|
# Monitor plus sysbox.
|
|
m = monitor(size)
|
|
s = sysbox(size)
|
|
x = (2+size/(size+1))*size
|
|
y = int(s.props.sysboxheight + s.props.borderthickness)
|
|
mb = m.bbox()
|
|
sb = s.bbox()
|
|
xoff = mb[0] - sb[0] + x
|
|
yoff = mb[1] - sb[1] + y
|
|
toret = SVGgroup([s, m], [(0,0), (xoff,yoff)])
|
|
toret.props.screencentre = (m.props.screencentre[0]+xoff,
|
|
m.props.screencentre[1]+yoff)
|
|
return toret
|
|
|
|
def lightning(size):
|
|
# The lightning bolt motif.
|
|
|
|
# Compute the right size of a lightning bolt to exactly connect
|
|
# the centres of the two screens in the main PuTTY icon. We'll use
|
|
# that size of bolt for all the other icons too, for consistency.
|
|
iconw = iconh = 32 * size
|
|
cbb = computer(size).bbox()
|
|
assert cbb[2]-cbb[0] <= iconw and cbb[3]-cbb[1] <= iconh
|
|
width, height = iconw-(cbb[2]-cbb[0]), iconh-(cbb[3]-cbb[1])
|
|
|
|
degree = math.pi/180
|
|
|
|
centrethickness = 2*size # top-to-bottom thickness of centre bar
|
|
innerangle = 46 * degree # slope of the inner slanting line
|
|
outerangle = 39 * degree # slope of the outer one
|
|
|
|
innery = (height - centrethickness) / 2
|
|
outery = (height + centrethickness) / 2
|
|
innerx = innery / math.tan(innerangle)
|
|
outerx = outery / math.tan(outerangle)
|
|
|
|
points = [(innerx, innery), (0,0), (outerx, outery)]
|
|
points.extend([(width-x, height-y) for x,y in points])
|
|
|
|
# Fill and stroke the lightning bolt.
|
|
#
|
|
# Most of the filled-and-stroked objects in these icons are filled
|
|
# first, and then stroked with width 0.5, so that the edge of the
|
|
# filled area runs down the centre line of the stroke. Put another
|
|
# way, half the stroke covers what would have been the filled
|
|
# area, and the other half covers the background. This seems like
|
|
# the normal way to fill-and-stroke a shape of a given size, and
|
|
# SVG makes it easy by allowing us to specify the polygon just
|
|
# once with both 'fill' and 'stroke' CSS properties.
|
|
#
|
|
# But if we did that in this case, then the tips of the lightning
|
|
# bolt wouldn't have lightning-colour anywhere near them, because
|
|
# the two edges are so close together in angle that the point
|
|
# where the strokes would first _not_ overlap would be miles away
|
|
# from the logical endpoint.
|
|
#
|
|
# So, for this one case, we stroke the polygon first at double the
|
|
# width, and then fill it on top of that, requiring two copies of
|
|
# it in the SVG (though my construction class here hides that
|
|
# detail). The effect is that we still get a stroke of visible
|
|
# width 0.5, but it's entirely outside the filled area of the
|
|
# polygon, so the tips of the yellow interior of the lightning
|
|
# bolt are exactly at the logical endpoints.
|
|
poly = SVGpoly(points)
|
|
poly.fill(cY)
|
|
poly.stroke(cK, width=1, behind=True)
|
|
poly.props.end1 = (0,0)
|
|
poly.props.end2 = (width,height)
|
|
return poly
|
|
|
|
def document(size):
|
|
# The document used in the PSCP/PSFTP icon.
|
|
|
|
width = 13*size
|
|
height = 16*size
|
|
|
|
lineht = 0.875*size
|
|
linespc = 1.125*size
|
|
nlines = int((height-linespc)/(lineht+linespc))
|
|
height = nlines*(lineht+linespc)+linespc # round this so it fits better
|
|
|
|
paper = SVGrect(0, 0, width, height)
|
|
paper.fill(cW)
|
|
paper.stroke(cK, width=0.5)
|
|
|
|
objs = [paper]
|
|
|
|
# Now draw lines of text.
|
|
for line in range(nlines):
|
|
# Decide where this line of text begins.
|
|
if line == 0:
|
|
start = 4*size
|
|
elif line < 5*nlines/7:
|
|
start = (line * 4/5) * size
|
|
else:
|
|
start = 1*size
|
|
# Decide where it ends.
|
|
endpoints = [10, 8, 11, 6, 5, 7, 5]
|
|
ey = line * 6.0 / (nlines-1)
|
|
eyf = math.floor(ey)
|
|
eyc = math.ceil(ey)
|
|
exf = endpoints[int(eyf)]
|
|
exc = endpoints[int(eyc)]
|
|
if eyf == eyc:
|
|
end = exf
|
|
else:
|
|
end = exf * (eyc-ey) + exc * (ey-eyf)
|
|
end = end * size
|
|
|
|
liney = (lineht+linespc) * (line+1)
|
|
line = SVGrect(start, liney-lineht, end, liney)
|
|
line.fill(cK)
|
|
objs.append(line)
|
|
|
|
return SVGgroup(objs)
|
|
|
|
def hat(size):
|
|
# The secret-agent hat in the Pageant icon.
|
|
|
|
leftend = (0, -6*size)
|
|
rightend = (28*size, -12*size)
|
|
dx = rightend[0]-leftend[0]
|
|
dy = rightend[1]-leftend[1]
|
|
tcentre = (leftend[0] + 0.5*dx - 0.3*dy, leftend[1] + 0.5*dy + 0.3*dx)
|
|
|
|
hatpoints = [leftend + (True,),
|
|
(7.5*size, -6*size, True),
|
|
(12*size, 0, True),
|
|
(14*size, 3*size, False),
|
|
(tcentre[0] - 0.1*dx, tcentre[1] - 0.1*dy, False),
|
|
tcentre + (True,)]
|
|
for x, y, on in list(reversed(hatpoints))[1:]:
|
|
vx, vy = x-tcentre[0], y-tcentre[1]
|
|
coeff = float(vx*dx + vy*dy) / float(dx*dx + dy*dy)
|
|
rx, ry = x - 2*coeff*dx, y - 2*coeff*dy
|
|
hatpoints.append((rx, ry, on))
|
|
|
|
mainhat = SVGpath([hatpoints])
|
|
mainhat.fill(cK)
|
|
|
|
band = SVGpoly([
|
|
(leftend[0] - 0.1*dy, leftend[1] + 0.1*dx),
|
|
(rightend[0] - 0.1*dy, rightend[1] + 0.1*dx),
|
|
(rightend[0] - 0.15*dy, rightend[1] + 0.15*dx),
|
|
(leftend[0] - 0.15*dy, leftend[1] + 0.15*dx)])
|
|
band.fill(cW)
|
|
band.clip(SVGpath([hatpoints]))
|
|
|
|
outline = SVGpath([hatpoints])
|
|
outline.stroke(cK, width=1)
|
|
|
|
return SVGgroup([mainhat, band, outline])
|
|
|
|
def key(size):
|
|
# The key in the PuTTYgen icon.
|
|
|
|
keyheadw = 9.5*size
|
|
keyheadh = 12*size
|
|
keyholed = 4*size
|
|
keyholeoff = 2*size
|
|
# Ensure keyheadh and keyshafth have the same parity.
|
|
keyshafth = (2*size - (int(keyheadh)&1)) / 2 * 2 + (int(keyheadh)&1)
|
|
keyshaftw = 18.5*size
|
|
keyheaddetail = [x*size for x in [12,11,8,10,9,8,11,12]]
|
|
|
|
squarepix = []
|
|
|
|
keyheadcx = keyshaftw + keyheadw / 2.0
|
|
keyheadcy = keyheadh / 2.0
|
|
keyshafttop = keyheadcy + keyshafth / 2.0
|
|
keyshaftbot = keyheadcy - keyshafth / 2.0
|
|
|
|
keyhead = [(0, keyshafttop, True), (keyshaftw, keyshafttop, True),
|
|
(keyshaftw, keyshaftbot,
|
|
("arc", keyheadw/2.0, keyheadh/2.0, 0, True, True)),
|
|
(len(keyheaddetail)*size, keyshaftbot, True)]
|
|
for i, h in reversed(list(enumerate(keyheaddetail))):
|
|
keyhead.append(((i+1)*size, keyheadh-h, True))
|
|
keyhead.append(((i)*size, keyheadh-h, True))
|
|
|
|
keyholecx = keyheadcx + keyholeoff
|
|
keyholecy = keyheadcy
|
|
keyholer = keyholed / 2.0
|
|
|
|
keyhole = [(keyholecx + keyholer, keyholecy,
|
|
("arc", keyholer, keyholer, 0, False, False)),
|
|
(keyholecx - keyholer, keyholecy,
|
|
("arc", keyholer, keyholer, 0, False, False))]
|
|
|
|
outline = SVGpath([keyhead, keyhole])
|
|
outline.fill(cy)
|
|
outline.stroke(cK, width=0.5)
|
|
return outline
|
|
|
|
def linedist(x1,y1, x2,y2, x,y):
|
|
# Compute the distance from the point x,y to the line segment
|
|
# joining x1,y1 to x2,y2. Returns the distance vector, measured
|
|
# with x,y at the origin.
|
|
|
|
vectors = []
|
|
|
|
# Special case: if x1,y1 and x2,y2 are the same point, we
|
|
# don't attempt to extrapolate it into a line at all.
|
|
if x1 != x2 or y1 != y2:
|
|
# First, find the nearest point to x,y on the infinite
|
|
# projection of the line segment. So we construct a vector
|
|
# n perpendicular to that segment...
|
|
nx = y2-y1
|
|
ny = x1-x2
|
|
# ... compute the dot product of (x1,y1)-(x,y) with that
|
|
# vector...
|
|
nd = (x1-x)*nx + (y1-y)*ny
|
|
# ... multiply by the vector we first thought of...
|
|
ndx = nd * nx
|
|
ndy = nd * ny
|
|
# ... and divide twice by the length of n.
|
|
ndx = ndx / (nx*nx+ny*ny)
|
|
ndy = ndy / (nx*nx+ny*ny)
|
|
# That gives us a displacement vector from x,y to the
|
|
# nearest point. See if it's within the range of the line
|
|
# segment.
|
|
cx = x + ndx
|
|
cy = y + ndy
|
|
if cx >= min(x1,x2) and cx <= max(x1,x2) and \
|
|
cy >= min(y1,y2) and cy <= max(y1,y2):
|
|
vectors.append((ndx,ndy))
|
|
|
|
# Now we have up to three candidate result vectors: (ndx,ndy)
|
|
# as computed just above, and the two vectors to the ends of
|
|
# the line segment, (x1-x,y1-y) and (x2-x,y2-y). Pick the
|
|
# shortest.
|
|
vectors = vectors + [(x1-x,y1-y), (x2-x,y2-y)]
|
|
bestlen, best = None, None
|
|
for v in vectors:
|
|
vlen = v[0]*v[0]+v[1]*v[1]
|
|
if bestlen == None or bestlen > vlen:
|
|
bestlen = vlen
|
|
best = v
|
|
return best
|
|
|
|
def spanner(size):
|
|
# The spanner in the config box icon.
|
|
|
|
# Coordinate definitions.
|
|
headcentre = 0.5 + 4*size
|
|
headradius = headcentre + 0.1
|
|
headhighlight = 1.5*size
|
|
holecentre = 0.5 + 3*size
|
|
holeradius = 2*size
|
|
holehighlight = 1.5*size
|
|
shaftend = 0.5 + 25*size
|
|
shaftwidth = 2*size
|
|
shafthighlight = 1.5*size
|
|
cmax = shaftend + shaftwidth
|
|
|
|
# The spanner head is a circle centred at headcentre*(1,1) with
|
|
# radius headradius, minus a circle at holecentre*(1,1) with
|
|
# radius holeradius, and also minus every translate of that circle
|
|
# by a negative real multiple of (1,1).
|
|
#
|
|
# The spanner handle is a diagonally oriented rectangle, of width
|
|
# shaftwidth, with the centre of the far end at shaftend*(1,1),
|
|
# and the near end terminating somewhere inside the spanner head
|
|
# (doesn't really matter exactly where).
|
|
#
|
|
# Hence, in SVG we can represent the shape using a path of
|
|
# straight lines and circular arcs. But first we need to calculate
|
|
# the points where the straight lines meet the spanner head circle.
|
|
headpt = lambda a, on=True: (headcentre+headradius*math.cos(a),
|
|
-headcentre+headradius*math.sin(a), on)
|
|
holept = lambda a, on=True: (holecentre+holeradius*math.cos(a),
|
|
-holecentre+holeradius*math.sin(a), on)
|
|
|
|
# Now we can specify the path.
|
|
spannercoords = [[
|
|
holept(math.pi*5/4),
|
|
holept(math.pi*1/4, ("arc", holeradius,holeradius,0, False, False)),
|
|
headpt(math.pi*3/4 - math.asin(holeradius/headradius)),
|
|
headpt(math.pi*7/4 + math.asin(shaftwidth/headradius),
|
|
("arc", headradius,headradius,0, False, True)),
|
|
(shaftend+math.sqrt(0.5)*shaftwidth,
|
|
-shaftend+math.sqrt(0.5)*shaftwidth, True),
|
|
(shaftend-math.sqrt(0.5)*shaftwidth,
|
|
-shaftend-math.sqrt(0.5)*shaftwidth, True),
|
|
headpt(math.pi*7/4 - math.asin(shaftwidth/headradius)),
|
|
headpt(math.pi*3/4 + math.asin(holeradius/headradius),
|
|
("arc", headradius,headradius,0, False, True)),
|
|
]]
|
|
|
|
base = SVGpath(spannercoords)
|
|
base.fill(cY)
|
|
|
|
shadowthickness = 2*size
|
|
sx, sy, _ = holept(math.pi*5/4)
|
|
sx += math.sqrt(0.5) * shadowthickness/2
|
|
sy += math.sqrt(0.5) * shadowthickness/2
|
|
sr = holeradius - shadowthickness/2
|
|
|
|
shadow = SVGpath([
|
|
[(sx, sy, sr),
|
|
holept(math.pi*1/4, ("arc", sr, sr, 0, False, False)),
|
|
headpt(math.pi*3/4 - math.asin(holeradius/headradius))],
|
|
[(shaftend-math.sqrt(0.5)*shaftwidth,
|
|
-shaftend-math.sqrt(0.5)*shaftwidth, True),
|
|
headpt(math.pi*7/4 - math.asin(shaftwidth/headradius)),
|
|
headpt(math.pi*3/4 + math.asin(holeradius/headradius),
|
|
("arc", headradius,headradius,0, False, True))],
|
|
], closed=False)
|
|
shadow.clip(SVGpath(spannercoords))
|
|
shadow.stroke(cy, width=shadowthickness)
|
|
|
|
outline = SVGpath(spannercoords)
|
|
outline.stroke(cK, width=0.5)
|
|
|
|
return SVGgroup([base, shadow, outline])
|
|
|
|
def box(size, wantback):
|
|
# The back side of the cardboard box in the installer icon.
|
|
|
|
boxwidth = 15 * size
|
|
boxheight = 12 * size
|
|
boxdepth = 4 * size
|
|
boxfrontflapheight = 5 * size
|
|
boxrightflapheight = 3 * size
|
|
|
|
# Three shades of basically acceptable brown, all achieved by
|
|
# halftoning between two of the Windows-16 colours. I'm quite
|
|
# pleased that was feasible at all!
|
|
dark = halftone(cr, cK)
|
|
med = halftone(cr, cy)
|
|
light = halftone(cr, cY)
|
|
# We define our halftoning parity in such a way that the black
|
|
# pixels along the RHS of the visible part of the box back
|
|
# match up with the one-pixel black outline around the
|
|
# right-hand side of the box. In other words, we want the pixel
|
|
# at (-1, boxwidth-1) to be black, and hence the one at (0,
|
|
# boxwidth) too.
|
|
parityadjust = int(boxwidth) % 2
|
|
|
|
# The back of the box.
|
|
if wantback:
|
|
back = SVGpoly([
|
|
(0,0), (boxwidth,0), (boxwidth+boxdepth,boxdepth),
|
|
(boxwidth+boxdepth,boxheight+boxdepth),
|
|
(boxdepth,boxheight+boxdepth), (0,boxheight)])
|
|
back.fill(dark)
|
|
back.stroke(cK, width=0.5)
|
|
return back
|
|
|
|
# The front face of the box.
|
|
front = SVGrect(0, 0, boxwidth, boxheight)
|
|
front.fill(med)
|
|
front.stroke(cK, width=0.5)
|
|
# The right face of the box.
|
|
right = SVGpoly([
|
|
(boxwidth,0), (boxwidth+boxdepth,boxdepth),
|
|
(boxwidth+boxdepth,boxheight+boxdepth), (boxwidth,boxheight)])
|
|
right.fill(dark)
|
|
right.stroke(cK, width=0.5)
|
|
frontflap = SVGpoly([
|
|
(0,boxheight), (boxwidth,boxheight),
|
|
(boxwidth-boxfrontflapheight/2, boxheight-boxfrontflapheight),
|
|
(-boxfrontflapheight/2, boxheight-boxfrontflapheight)])
|
|
frontflap.stroke(cK, width=0.5)
|
|
frontflap.fill(light)
|
|
rightflap = SVGpoly([
|
|
(boxwidth,boxheight), (boxwidth+boxdepth,boxheight+boxdepth),
|
|
(boxwidth+boxdepth+boxrightflapheight,
|
|
boxheight+boxdepth-boxrightflapheight),
|
|
(boxwidth+boxrightflapheight,boxheight-boxrightflapheight)])
|
|
rightflap.stroke(cK, width=0.5)
|
|
rightflap.fill(med)
|
|
|
|
return SVGgroup([front, right, frontflap, rightflap])
|
|
|
|
def boxback(size):
|
|
return box(size, 1)
|
|
def boxfront(size):
|
|
return box(size, 0)
|
|
|
|
# Functions to draw entire icons by composing the above components.
|
|
|
|
def xybolt(c1, c2, size, boltoffx=0, boltoffy=0, c1bb=None, c2bb=None):
|
|
# Two unspecified objects and a lightning bolt.
|
|
|
|
w = h = 32 * size
|
|
|
|
bolt = lightning(size)
|
|
|
|
objs = [c2, c1, bolt]
|
|
origins = [None] * 3
|
|
|
|
# Position c2 against the top right of the icon.
|
|
bb = c2bb if c2bb is not None else c2.bbox()
|
|
assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
|
|
origins[0] = w-bb[2], h-bb[3]
|
|
# Position c1 against the bottom left of the icon.
|
|
bb = c1bb if c1bb is not None else c1.bbox()
|
|
assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
|
|
origins[1] = 0-bb[0], 0-bb[1]
|
|
|
|
# Place the lightning bolt so that it ends precisely at the centre
|
|
# of the monitor, in whichever of the two sub-pictures has one.
|
|
# (In the case of the PuTTY icon proper, in which _both_
|
|
# sub-pictures are computers, it should line up correctly for both.)
|
|
origin1 = origin2 = None
|
|
if hasattr(c1.props, "screencentre"):
|
|
origin1 = (
|
|
c1.props.screencentre[0] + origins[1][0] - bolt.props.end1[0],
|
|
c1.props.screencentre[1] + origins[1][1] - bolt.props.end1[1])
|
|
if hasattr(c2.props, "screencentre"):
|
|
origin2 = (
|
|
c2.props.screencentre[0] + origins[0][0] - bolt.props.end2[0],
|
|
c2.props.screencentre[1] + origins[0][1] - bolt.props.end2[1])
|
|
if origin1 is not None and origin2 is not None:
|
|
assert math.hypot(origin1[0]-origin2[0],origin1[1]-origin2[1]<1e-5), (
|
|
"Lightning bolt didn't line up! Off by {}*size".format(
|
|
((origin1[0]-origin2[0])/size,
|
|
(origin1[1]-origin2[1])/size)))
|
|
origins[2] = origin1 if origin1 is not None else origin2
|
|
assert origins[2] is not None, "Need at least one computer to line up bolt"
|
|
|
|
toret = SVGgroup(objs, origins)
|
|
toret.props.c1pos = origins[1]
|
|
toret.props.c2pos = origins[0]
|
|
return toret
|
|
|
|
def putty_icon(size):
|
|
return xybolt(computer(size), computer(size), size)
|
|
|
|
def puttycfg_icon(size):
|
|
w = h = 32 * size
|
|
s = spanner(size)
|
|
b = putty_icon(size)
|
|
bb = s.bbox()
|
|
return SVGgroup([b, s], [(0,0), ((w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2)])
|
|
|
|
def puttygen_icon(size):
|
|
k = key(size)
|
|
# Manually move the key around, by pretending to xybolt that its
|
|
# bounding box is offset from where it really is.
|
|
kbb = SVGtranslate(k,(2*size,5*size)).bbox()
|
|
return xybolt(computer(size), k, size, boltoffx=2, c2bb=kbb)
|
|
|
|
def pscp_icon(size):
|
|
return xybolt(document(size), computer(size), size)
|
|
|
|
def puttyins_icon(size):
|
|
boxfront = box(size, False)
|
|
boxback = box(size, True)
|
|
# The box back goes behind the lightning bolt.
|
|
most = xybolt(boxback, computer(size), size, c1bb=boxfront.bbox(),
|
|
boltoffx=-2, boltoffy=+1)
|
|
# But the box front goes over the top, so that the lightning
|
|
# bolt appears to come _out_ of the box. Here it's useful to
|
|
# know the exact coordinates where xybolt placed the box back,
|
|
# so we can overlay the box front exactly on top of it.
|
|
c1x, c1y = most.props.c1pos
|
|
return SVGgroup([most, boxfront], [(0,0), most.props.c1pos])
|
|
|
|
def pterm_icon(size):
|
|
# Just a really big computer.
|
|
|
|
w = h = 32 * size
|
|
|
|
c = computer(size * 1.4)
|
|
|
|
# Centre c in the output rectangle.
|
|
bb = c.bbox()
|
|
assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
|
|
|
|
return SVGgroup([c], [((w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2)])
|
|
|
|
def ptermcfg_icon(size):
|
|
w = h = 32 * size
|
|
s = spanner(size)
|
|
b = pterm_icon(size)
|
|
bb = s.bbox()
|
|
return SVGgroup([b, s], [(0,0), ((w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2)])
|
|
|
|
def pageant_icon(size):
|
|
# A biggish computer, in a hat.
|
|
|
|
w = h = 32 * size
|
|
|
|
c = computer(size * 1.2)
|
|
ht = hat(size)
|
|
|
|
cbb = c.bbox()
|
|
hbb = ht.bbox()
|
|
|
|
# Determine the relative coordinates of the computer and hat. We
|
|
# do this by first centring one on the other, then adjusting by
|
|
# hand.
|
|
xrel = (cbb[0]+cbb[2]-hbb[0]-hbb[2])/2 + 2*size
|
|
yrel = (cbb[1]+cbb[3]-hbb[1]-hbb[3])/2 + 12*size
|
|
|
|
both = SVGgroup([c, ht], [(0,0), (xrel,yrel)])
|
|
|
|
# Mostly-centre the result in the output rectangle. We want
|
|
# everything to fit in frame, but we also want to make it look as
|
|
# if the computer is more x-centred than the hat.
|
|
|
|
# Coordinates that would centre the whole group.
|
|
bb = both.bbox()
|
|
assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h
|
|
grx, gry = (w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2
|
|
|
|
# Coords that would centre just the computer.
|
|
bb = c.bbox()
|
|
crx, cry = (w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2
|
|
|
|
# Use gry unchanged, but linear-combine grx with crx.
|
|
return SVGgroup([both], [(grx+0.6*(crx-grx), gry)])
|
|
|
|
# Test and output functions.
|
|
|
|
cK = (0x00, 0x00, 0x00, 0xFF)
|
|
cr = (0x80, 0x00, 0x00, 0xFF)
|
|
cg = (0x00, 0x80, 0x00, 0xFF)
|
|
cy = (0x80, 0x80, 0x00, 0xFF)
|
|
cb = (0x00, 0x00, 0x80, 0xFF)
|
|
cm = (0x80, 0x00, 0x80, 0xFF)
|
|
cc = (0x00, 0x80, 0x80, 0xFF)
|
|
cP = (0xC0, 0xC0, 0xC0, 0xFF)
|
|
cw = (0x80, 0x80, 0x80, 0xFF)
|
|
cR = (0xFF, 0x00, 0x00, 0xFF)
|
|
cG = (0x00, 0xFF, 0x00, 0xFF)
|
|
cY = (0xFF, 0xFF, 0x00, 0xFF)
|
|
cB = (0x00, 0x00, 0xFF, 0xFF)
|
|
cM = (0xFF, 0x00, 0xFF, 0xFF)
|
|
cC = (0x00, 0xFF, 0xFF, 0xFF)
|
|
cW = (0xFF, 0xFF, 0xFF, 0xFF)
|
|
cD = (0x00, 0x00, 0x00, 0x80)
|
|
cT = (0x00, 0x00, 0x00, 0x00)
|
|
def greypix(value):
|
|
value = max(min(value, 1), 0)
|
|
return (int(round(0xFF*value)),) * 3 + (0xFF,)
|
|
def yellowpix(value):
|
|
value = max(min(value, 1), 0)
|
|
return (int(round(0xFF*value)),) * 2 + (0, 0xFF)
|
|
def bluepix(value):
|
|
value = max(min(value, 1), 0)
|
|
return (0, 0, int(round(0xFF*value)), 0xFF)
|
|
def dark(value):
|
|
value = max(min(value, 1), 0)
|
|
return (0, 0, 0, int(round(0xFF*value)))
|
|
def blend(col1, col2):
|
|
r1,g1,b1,a1 = col1
|
|
r2,g2,b2,a2 = col2
|
|
r = int(round((r1*a1 + r2*(0xFF-a1)) / 255.0))
|
|
g = int(round((g1*a1 + g2*(0xFF-a1)) / 255.0))
|
|
b = int(round((b1*a1 + b2*(0xFF-a1)) / 255.0))
|
|
a = int(round((255*a1 + a2*(0xFF-a1)) / 255.0))
|
|
return r, g, b, a
|
|
def halftone(col1, col2):
|
|
r1,g1,b1,a1 = col1
|
|
r2,g2,b2,a2 = col2
|
|
return ((r1+r2)//2, (g1+g2)//2, (b1+b2)//2, (a1+a2)//2)
|
|
|
|
def drawicon(func, width, fname):
|
|
icon = func(width / 32.0)
|
|
minx, miny, maxx, maxy = icon.bbox()
|
|
#assert minx >= 0 and miny >= 0 and maxx <= width and maxy <= width
|
|
|
|
svgroot = ET.Element("svg")
|
|
svgroot.attrib["xmlns"] = "http://www.w3.org/2000/svg"
|
|
svgroot.attrib["viewBox"] = "0 0 {w:d} {w:d}".format(w=width)
|
|
|
|
defs = ET.Element("defs")
|
|
idents = ("iconid{:d}".format(n) for n in itertools.count())
|
|
if icon.add_clip_paths(defs, idents, 0, width):
|
|
svgroot.append(defs)
|
|
|
|
svgroot.append(icon.render(0,width))
|
|
|
|
ET.ElementTree(svgroot).write(fname)
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Generate PuTTY SVG icons.')
|
|
parser.add_argument("icon", help="Which icon to generate.")
|
|
parser.add_argument("-s", "--size", type=int, default=48,
|
|
help="Notional pixel size to base the SVG on.")
|
|
parser.add_argument("-o", "--output", required=True,
|
|
help="Output file name.")
|
|
args = parser.parse_args()
|
|
|
|
drawicon(eval(args.icon), args.size, args.output)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|