diff --git a/icons/Makefile b/icons/Makefile index 71b43874..3e3ea456 100644 --- a/icons/Makefile +++ b/icons/Makefile @@ -13,6 +13,8 @@ PNGS = $(patsubst %.pam,%.png,$(PAMS)) MONOPNGS = $(patsubst %.pam,%.png,$(MONOPAMS)) TRUEPNGS = $(patsubst %.pam,%.png,$(TRUEPAMS)) +SVGS = $(patsubst %,%.svg,$(ICONS)) + ICOS = putty.ico puttygen.ico pscp.ico pageant.ico pageants.ico puttycfg.ico \ puttyins.ico pterm.ico ptermcfg.ico ICNS = PuTTY.icns Pterm.icns @@ -20,11 +22,12 @@ CICONS = xpmputty.c xpmpucfg.c xpmpterm.c xpmptcfg.c base: icos cicons -all: pngs monopngs base icns # truepngs currently disabled by default +all: pngs monopngs base icns svgs # truepngs currently disabled by default pngs: $(PNGS) monopngs: $(MONOPNGS) truepngs: $(TRUEPNGS) +svgs: $(SVGS) icos: $(ICOS) icns: $(ICNS) @@ -46,6 +49,9 @@ $(MONOPAMS): %.pam: mkicon.py $(TRUEPAMS): %.pam: mkicon.py ./mkicon.py -T $(MODE) $(join $(subst -, ,$(subst -true,,$(basename $@))),_icon) $@ +$(SVGS): %.svg: mksvg.py + ./mksvg.py $(patsubst %.svg,%_icon,$@) -o $@ + putty.ico: putty-16.png putty-32.png putty-48.png \ putty-16-mono.png putty-32-mono.png putty-48-mono.png ./icon.pl -4 $(filter-out %-mono.png, $^) -1 $(filter %-mono.png, $^) > $@ diff --git a/icons/mksvg.py b/icons/mksvg.py new file mode 100755 index 00000000..f29ff25b --- /dev/null +++ b/icons/mksvg.py @@ -0,0 +1,938 @@ +#!/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()