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