chiark / gitweb /
Just noticed another thing that could easily catch me out when
[sgt-puzzles.git] / print.py
1 #!/usr/bin/env python
2
3 # This program accepts a series of newline-separated game IDs on
4 # stdin and formats them into PostScript to be printed out. You
5 # specify using command-line options which game the IDs are for,
6 # and how many you want per page.
7
8 # Supported games are those which are sensibly solvable using
9 # pencil and paper: Rectangles, Pattern, Solo, Net.
10
11 # Command-line syntax is
12 #
13 #     print.py <game-name> <format>
14 #
15 # <game-name> is one of `rect', `rectangles', `pattern', `solo',
16 # `net', `dominosa'. <format> is two numbers separated by an x:
17 # `2x3', for example, means two columns by three rows.
18 #
19 # The program will then read game IDs from stdin until it sees EOF,
20 # and generate as many PostScript pages on stdout as it needs.
21 #
22 # The resulting PostScript will automatically adapt itself to the
23 # size of the clip rectangle, so that the puzzles are sensibly
24 # distributed across whatever paper size you decide to use.
25
26 import sys
27 import string
28 import re
29
30 class Holder:
31     pass
32
33 def psvprint(h, a):
34     for i in xrange(len(a)):
35         h.s = h.s + str(a[i])
36         if i < len(a)-1:
37             h.s = h.s + " "
38         else:
39             h.s = h.s + "\n"
40
41 def psprint(h, *a):
42     psvprint(h, a)
43
44 def rect_format(s):
45     # Parse the game ID.
46     ret = Holder()
47     ret.s = ""
48     params, seed = string.split(s, ":")
49     w, h = map(string.atoi, string.split(params, "x"))
50     grid = []
51     while len(seed) > 0:
52         if seed[0] in '_'+string.lowercase:
53             if seed[0] in string.lowercase:
54                 grid.extend([-1] * (ord(seed[0]) - ord('a') + 1))
55             seed = seed[1:]
56         elif seed[0] in string.digits:
57             ns = ""
58             while len(seed) > 0 and seed[0] in string.digits:
59                 ns = ns + seed[0]
60                 seed = seed[1:]
61             grid.append(string.atoi(ns))
62     assert w * h == len(grid)
63     # I'm going to arbitrarily choose to use 7pt text for the
64     # numbers, and a 14pt grid pitch.
65     textht = 7
66     gridpitch = 14
67     # Set up coordinate system.
68     pw = gridpitch * w
69     ph = gridpitch * h
70     ret.coords = (pw/2, pw/2, ph/2, ph/2)
71     psprint(ret, "%g %g translate" % (-ret.coords[0], -ret.coords[2]))
72     # Draw the internal grid lines, _very_ thin (the player will
73     # need to draw over them visibly).
74     psprint(ret, "newpath 0.01 setlinewidth")
75     for x in xrange(1,w):
76         psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, h * gridpitch))
77     for y in xrange(1,h):
78         psprint(ret, "0 %g moveto %g 0 rlineto" % (y * gridpitch, w * gridpitch))
79     psprint(ret, "stroke")
80     # Draw round the grid exterior, much thicker.
81     psprint(ret, "newpath 1.5 setlinewidth")
82     psprint(ret, "0 0 moveto 0 %g rlineto %g 0 rlineto 0 %g rlineto" % \
83     (h * gridpitch, w * gridpitch, -h * gridpitch))
84     psprint(ret, "closepath stroke")
85     # And draw the numbers.
86     psprint(ret, "/Helvetica findfont %g scalefont setfont" % textht)
87     for y in xrange(h):
88         for x in xrange(w):
89             n = grid[y*w+x]
90             if n > 0:
91                 psprint(ret, "%g %g (%d) ctshow" % \
92                 ((x+0.5)*gridpitch, (h-y-0.5)*gridpitch, n))
93     return ret.coords, ret.s
94
95 def net_format(s):
96     # Parse the game ID.
97     ret = Holder()
98     ret.s = ""
99     params, seed = string.split(s, ":")
100     wrapping = 0
101     if params[-1:] == "w":
102         wrapping = 1
103         params = params[:-1]
104     w, h = map(string.atoi, string.split(params, "x"))
105     grid = []
106     hbarriers = []
107     vbarriers = []
108     while len(seed) > 0:
109         n = string.atoi(seed[0], 16)
110         seed = seed[1:]
111         while len(seed) > 0 and seed[0] in 'hv':
112             x = len(grid) % w
113             y = len(grid) / w
114             if seed[0] == 'h':
115                 hbarriers.append((x, y+1))
116             else:
117                 vbarriers.append((x+1, y))
118             seed = seed[1:]
119         grid.append(n)
120     assert w * h == len(grid)
121     # I'm going to arbitrarily choose a 24pt grid pitch.
122     gridpitch = 24
123     scale = 0.25
124     bigoffset = 0.25
125     smalloffset = 0.17
126     squaresize = 0.25
127     # Set up coordinate system.
128     pw = gridpitch * w
129     ph = gridpitch * h
130     ret.coords = (pw/2, pw/2, ph/2, ph/2)
131     psprint(ret, "%g %g translate" % (-ret.coords[0], -ret.coords[2]))
132     # Draw the base grid lines.
133     psprint(ret, "newpath 0.02 setlinewidth")
134     for x in xrange(1,w):
135         psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, h * gridpitch))
136     for y in xrange(1,h):
137         psprint(ret, "0 %g moveto %g 0 rlineto" % (y * gridpitch, w * gridpitch))
138     psprint(ret, "stroke")
139     # Draw round the grid exterior.
140     psprint(ret, "newpath")
141     if not wrapping:
142         psprint(ret, "2 setlinewidth")
143     psprint(ret, "0 0 moveto 0 %g rlineto %g 0 rlineto 0 %g rlineto" % \
144     (h * gridpitch, w * gridpitch, -h * gridpitch))
145     psprint(ret, "closepath stroke")
146     # Draw any barriers.
147     psprint(ret, "newpath 2 setlinewidth 1 setlinecap")
148     for x, y in hbarriers:
149         psprint(ret, "%g %g moveto %g 0 rlineto" % \
150         (x * gridpitch, (h - y) * gridpitch, gridpitch))
151     for x, y in vbarriers:
152         psprint(ret, "%g %g moveto 0 -%g rlineto" % \
153         (x * gridpitch, (h - y) * gridpitch, gridpitch))
154     psprint(ret, "stroke")
155     # And draw the symbol in each box.
156     for i in xrange(len(grid)):
157         x = i % w
158         y = i / w
159         v = grid[i]
160         # Rotate to canonical form.
161         if v in (1,2,4,8):
162             v = 1
163         elif v in (5,10):
164             v = 5
165         elif v in (3,6,9,12):
166             v = 9
167         elif v in (7,11,13,14):
168             v = 13
169         # Centre on an area in the corner of the tile.
170         psprint(ret, "gsave")
171         if v & 4:
172             hoffset = bigoffset
173         else:
174             hoffset = smalloffset
175         if v & 2:
176             voffset = bigoffset
177         else:
178             voffset = smalloffset
179         psprint(ret, "%g %g translate" % \
180         ((x + hoffset) * gridpitch, (h - y - voffset) * gridpitch))
181         psprint(ret, "%g dup scale" % (float(gridpitch) * scale / 2))
182         psprint(ret, "newpath 0.07 setlinewidth")
183         # Draw the radial lines.
184         for dx, dy, z in ((1,0,1), (0,1,2), (-1,0,4), (0,-1,8)):
185             if v & z:
186                 psprint(ret, "0 0 moveto %d %d lineto" % (dx, dy))
187         psprint(ret, "stroke")
188         # Draw additional figures if desired.
189         if v == 1:
190             # Endpoints have a little empty square at the centre.
191             psprint(ret, "newpath %g %g moveto 0 -%g rlineto" % \
192             (squaresize, squaresize, squaresize * 2))
193             psprint(ret, "-%g 0 rlineto 0 %g rlineto closepath fill" % \
194             (squaresize * 2, squaresize * 2))
195         # Get back out of the centre section.
196         psprint(ret, "grestore")
197         # Draw the endpoint square in large in the middle.
198         if v == 1:
199             psprint(ret, "gsave")
200             psprint(ret, "%g %g translate" % \
201             ((x + 0.5) * gridpitch, (h - y - 0.5) * gridpitch))
202             psprint(ret, "%g dup scale" % (float(gridpitch) / 2))
203             psprint(ret, "newpath %g %g moveto 0 -%g rlineto" % \
204             (squaresize, squaresize, squaresize * 2))
205             psprint(ret, "-%g 0 rlineto 0 %g rlineto closepath fill" % \
206             (squaresize * 2, squaresize * 2))
207             psprint(ret, "grestore")
208     return ret.coords, ret.s
209
210 def pattern_format(s):
211     ret = Holder()
212     ret.s = ""
213     # Parse the game ID.
214     params, seed = string.split(s, ":")
215     w, h = map(string.atoi, string.split(params, "x"))
216     rowdata = map(lambda s: string.split(s, "."), string.split(seed, "/"))
217     assert len(rowdata) == w+h
218     # I'm going to arbitrarily choose to use 7pt text for the
219     # numbers, and a 14pt grid pitch.
220     textht = 7
221     gridpitch = 14
222     gutter = 8 # between the numbers and the grid
223     # Find the maximum number of numbers in each dimension, to
224     # determine the border size required.
225     xborder = reduce(max, map(len, rowdata[w:]))
226     yborder = reduce(max, map(len, rowdata[:w]))
227     # Set up coordinate system. I'm going to put the origin at the
228     # _top left_ of the grid, so that both sets of numbers get
229     # drawn the same way.
230     pw = (w + xborder) * gridpitch + gutter
231     ph = (h + yborder) * gridpitch + gutter
232     ret.coords = (xborder * gridpitch + gutter, w * gridpitch, \
233     yborder * gridpitch + gutter, h * gridpitch)
234     # Draw the internal grid lines. Every fifth one is thicker, as
235     # a visual aid.
236     psprint(ret, "newpath 0.1 setlinewidth")
237     for x in xrange(1,w):
238         if x % 5 != 0:
239             psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, -h * gridpitch))
240     for y in xrange(1,h):
241         if y % 5 != 0:
242             psprint(ret, "0 %g moveto %g 0 rlineto" % (-y * gridpitch, w * gridpitch))
243     psprint(ret, "stroke")
244     psprint(ret, "newpath 0.75 setlinewidth")
245     for x in xrange(5,w,5):
246         psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, -h * gridpitch))
247     for y in xrange(5,h,5):
248         psprint(ret, "0 %g moveto %g 0 rlineto" % (-y * gridpitch, w * gridpitch))
249     psprint(ret, "stroke")
250     # Draw round the grid exterior.
251     psprint(ret, "newpath 1.5 setlinewidth")
252     psprint(ret, "0 0 moveto 0 %g rlineto %g 0 rlineto 0 %g rlineto" % \
253     (-h * gridpitch, w * gridpitch, h * gridpitch))
254     psprint(ret, "closepath stroke")
255     # And draw the numbers.
256     psprint(ret, "/Helvetica findfont %g scalefont setfont" % textht)
257     for i in range(w+h):
258         ns = rowdata[i]
259         if i < w:
260             xo = (i + 0.5) * gridpitch
261             yo = (gutter + 0.5 * gridpitch)
262         else:
263             xo = -(gutter + 0.5 * gridpitch)
264             yo = ((i-w) + 0.5) * -gridpitch
265         for j in range(len(ns)-1, -1, -1):
266             psprint(ret, "%g %g (%s) ctshow" % (xo, yo, ns[j]))
267             if i < w:
268                 yo = yo + gridpitch
269             else:
270                 xo = xo - gridpitch
271     return ret.coords, ret.s
272
273 def solo_format(s):
274     ret = Holder()
275     ret.s = ""
276     # Parse the game ID.
277     params, seed = string.split(s, ":")
278     c, r = map(string.atoi, string.split(params, "x"))
279     cr = c*r
280     grid = []
281     while len(seed) > 0:
282         if seed[0] in '_'+string.lowercase:
283             if seed[0] in string.lowercase:
284                 grid.extend([-1] * (ord(seed[0]) - ord('a') + 1))
285             seed = seed[1:]
286         elif seed[0] in string.digits:
287             ns = ""
288             while len(seed) > 0 and seed[0] in string.digits:
289                 ns = ns + seed[0]
290                 seed = seed[1:]
291             grid.append(string.atoi(ns))
292     assert cr * cr == len(grid)
293     # I'm going to arbitrarily choose to use 9pt text for the
294     # numbers, and a 16pt grid pitch.
295     textht = 9
296     gridpitch = 16
297     # Set up coordinate system.
298     pw = ph = gridpitch * cr
299     ret.coords = (pw/2, pw/2, ph/2, ph/2)
300     psprint(ret, "%g %g translate" % (-ret.coords[0], -ret.coords[2]))
301     # Draw the thin internal grid lines.
302     psprint(ret, "newpath 0.1 setlinewidth")
303     for x in xrange(1,cr):
304         if x % r != 0:
305             psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, cr * gridpitch))
306     for y in xrange(1,cr):
307         if y % c != 0:
308             psprint(ret, "0 %g moveto %g 0 rlineto" % (y * gridpitch, cr * gridpitch))
309     psprint(ret, "stroke")
310     # Draw the thicker internal grid lines.
311     psprint(ret, "newpath 1 setlinewidth")
312     for x in xrange(r,cr,r):
313         psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, cr * gridpitch))
314     for y in xrange(c,cr,c):
315         psprint(ret, "0 %g moveto %g 0 rlineto" % (y * gridpitch, cr * gridpitch))
316     psprint(ret, "stroke")
317     # Draw round the grid exterior, thicker still.
318     psprint(ret, "newpath 1.5 setlinewidth")
319     psprint(ret, "0 0 moveto 0 %g rlineto %g 0 rlineto 0 %g rlineto" % \
320     (cr * gridpitch, cr * gridpitch, -cr * gridpitch))
321     psprint(ret, "closepath stroke")
322     # And draw the numbers.
323     psprint(ret, "/Helvetica findfont %g scalefont setfont" % textht)
324     for y in xrange(cr):
325         for x in xrange(cr):
326             n = grid[y*cr+x]
327             if n > 0:
328                 if n > 9:
329                     s = chr(ord('a') + n - 10)
330                 else:
331                     s = chr(ord('0') + n)
332                 psprint(ret, "%g %g (%s) ctshow" % \
333                 ((x+0.5)*gridpitch, (cr-y-0.5)*gridpitch, s))
334     return ret.coords, ret.s
335
336 def dominosa_format(s):
337     ret = Holder()
338     ret.s = ""
339     params, seed = string.split(s, ":")
340     n = string.atoi(params)
341     w = n+2
342     h = n+1
343     grid = []
344     while len(seed) > 0:
345         if seed[0] == '[': # XXX
346             d, seed = string.split(seed[1:], "]")
347             grid.append(string.atoi(d))
348         else:
349             assert seed[0] in string.digits
350             grid.append(string.atoi(seed[0:1]))
351             seed = seed[1:]
352     assert w*h == len(grid)
353     # I'm going to arbitrarily choose to use 9pt text for the
354     # numbers, and a 16pt grid pitch.
355     textht = 9
356     gridpitch = 16
357     pw = gridpitch * w
358     ph = gridpitch * h
359     psprint(ret, "/Helvetica findfont %g scalefont setfont" % textht)
360     ret.coords = (pw/2, pw/2, ph/2, ph/2)
361     psprint(ret, "%g %g translate" % (-ret.coords[0], -ret.coords[2]))
362     for y in xrange(h):
363         for x in xrange(w):
364             psprint(ret, "%g %g (%d) ctshow" % \
365                 ((x+0.5)*gridpitch, (h-y-0.5)*gridpitch, grid[y*w+x]))
366     return ret.coords, ret.s
367
368 def slant_format(s):
369     # Parse the game ID.
370     ret = Holder()
371     ret.s = ""
372     params, seed = string.split(s, ":")
373     w, h = map(string.atoi, string.split(params, "x"))
374     W = w+1
375     H = h+1
376     grid = []
377     while len(seed) > 0:
378         if seed[0] in string.lowercase:
379             grid.extend([-1] * (ord(seed[0]) - ord('a') + 1))
380             seed = seed[1:]
381         elif seed[0] in "01234":
382             grid.append(string.atoi(seed[0]))
383             seed = seed[1:]
384     assert W * H == len(grid)
385     # I'm going to arbitrarily choose to use 7pt text for the
386     # numbers, and a 14pt grid pitch.
387     textht = 7
388     gridpitch = 14
389     radius = textht * 2.0 / 3.0
390     # Set up coordinate system.
391     pw = gridpitch * w
392     ph = gridpitch * h
393     ret.coords = (pw/2, pw/2, ph/2, ph/2)
394     psprint(ret, "%g %g translate" % (-ret.coords[0], -ret.coords[2]))
395     # Draw round the grid exterior, thickly.
396     psprint(ret, "newpath 1 setlinewidth")
397     psprint(ret, "0 0 moveto 0 %g rlineto %g 0 rlineto 0 %g rlineto" % \
398     (h * gridpitch, w * gridpitch, -h * gridpitch))
399     psprint(ret, "closepath stroke")
400     # Draw the internal grid lines, _very_ thin (the player will
401     # need to draw over them visibly).
402     psprint(ret, "newpath 0.01 setlinewidth")
403     for x in xrange(1,w):
404         psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, h * gridpitch))
405     for y in xrange(1,h):
406         psprint(ret, "0 %g moveto %g 0 rlineto" % (y * gridpitch, w * gridpitch))
407     psprint(ret, "stroke")
408     # And draw the numbers.
409     psprint(ret, "/Helvetica findfont %g scalefont setfont" % textht)
410     for y in xrange(H):
411         for x in xrange(W):
412             n = grid[y*W+x]
413             if n >= 0:
414                 psprint(ret, "newpath %g %g %g 0 360 arc" % \
415                 ((x)*gridpitch, (h-y)*gridpitch, radius),
416                 "gsave 1 setgray fill grestore stroke")
417                 psprint(ret, "%g %g (%d) ctshow" % \
418                 ((x)*gridpitch, (h-y)*gridpitch, n))
419     return ret.coords, ret.s
420
421 def lightup_format(s):
422     # Parse the game ID.
423     ret = Holder()
424     ret.s = ""
425     params, seed = string.split(s, ":")
426     w, h = map(string.atoi, string.split(params, "x"))
427     grid = []
428     while len(seed) > 0:
429         if seed[0] in string.lowercase:
430             grid.extend([-2] * (ord(seed[0]) - ord('a') + 1))
431             seed = seed[1:]
432         elif seed[0] == "B":
433             grid.append(-1)
434             seed = seed[1:]
435         elif seed[0] in "01234":
436             grid.append(string.atoi(seed[0]))
437             seed = seed[1:]
438     assert w * h == len(grid)
439     # I'm going to arbitrarily choose to use 9pt text for the
440     # numbers, and a 14pt grid pitch.
441     textht = 10
442     gridpitch = 14
443     # Set up coordinate system.
444     pw = gridpitch * w
445     ph = gridpitch * h
446     ret.coords = (pw/2, pw/2, ph/2, ph/2)
447     psprint(ret, "%g %g translate" % (-ret.coords[0], -ret.coords[2]))
448     # Draw round the grid exterior, thickly.
449     psprint(ret, "newpath 1 setlinewidth")
450     psprint(ret, "0 0 moveto 0 %g rlineto %g 0 rlineto 0 %g rlineto" % \
451     (h * gridpitch, w * gridpitch, -h * gridpitch))
452     psprint(ret, "closepath stroke")
453     # Draw the internal grid lines.
454     psprint(ret, "newpath 0.02 setlinewidth")
455     for x in xrange(1,w):
456         psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, h * gridpitch))
457     for y in xrange(1,h):
458         psprint(ret, "0 %g moveto %g 0 rlineto" % (y * gridpitch, w * gridpitch))
459     psprint(ret, "stroke")
460     # And draw the black squares and numbers.
461     psprint(ret, "/Helvetica-Bold findfont %g scalefont setfont" % textht)
462     for y in xrange(h):
463         for x in xrange(w):
464             n = grid[y*w+x]
465             if n >= -1:
466                 psprint(ret, ("newpath %g %g moveto 0 %g rlineto " +
467                 "%g 0 rlineto 0 %g rlineto closepath fill") % \
468                 ((x)*gridpitch, (h-1-y)*gridpitch, gridpitch, gridpitch, \
469                 -gridpitch))
470                 if n >= 0:
471                     psprint(ret, "gsave 1 setgray %g %g (%d) ctshow grestore" % \
472                     ((x+0.5)*gridpitch, (h-y-0.5)*gridpitch, n))
473     return ret.coords, ret.s
474
475 formatters = {
476 "net": net_format,
477 "rect": rect_format,
478 "rectangles": rect_format,
479 "pattern": pattern_format,
480 "solo": solo_format,
481 "dominosa": dominosa_format,
482 "slant": slant_format,
483 "lightup": lightup_format
484 }
485
486 if len(sys.argv) < 3:
487     sys.stderr.write("print.py: expected two arguments (game and format)\n")
488     sys.exit(1)
489
490 formatter = formatters.get(sys.argv[1], None)
491 if formatter == None:
492     sys.stderr.write("print.py: unrecognised game name `%s'\n" % sys.argv[1])
493     sys.exit(1)
494
495 try:
496     format = map(string.atoi, string.split(sys.argv[2], "x"))
497 except ValueError, e:
498     format = []
499 if len(format) != 2:
500     sys.stderr.write("print.py: expected format such as `2x3' as second" \
501     + " argument\n")
502     sys.exit(1)
503
504 xx, yy = format
505 ppp = xx * yy # puzzles per page
506
507 ids = []
508 while 1:
509     s = sys.stdin.readline()
510     if s == "": break
511     if s[-1:] == "\n": s = s[:-1]
512     ids.append(s)
513
514 pages = int((len(ids) + ppp - 1) / ppp)
515
516 # Output initial DSC stuff.
517 print "%!PS-Adobe-3.0"
518 print "%%Creator: print.py from Simon Tatham's Puzzle Collection"
519 print "%%DocumentData: Clean7Bit"
520 print "%%LanguageLevel: 1"
521 print "%%Pages:", pages
522 print "%%DocumentNeededResources:"
523 print "%%+ font Helvetica"
524 print "%%DocumentSuppliedResources: procset Puzzles 0 0"
525 print "%%EndComments"
526 print "%%BeginProlog"
527 print "%%BeginResource: procset Puzzles 0 0"
528 print "/ctshow {"
529 print "  3 1 roll"
530 print "  newpath 0 0 moveto (X) true charpath flattenpath pathbbox"
531 print "  3 -1 roll add 2 div 3 1 roll pop pop sub moveto"
532 print "  dup stringwidth pop 0.5 mul neg 0 rmoveto show"
533 print "} bind def"
534 print "%%EndResource"
535 print "%%EndProlog"
536 print "%%BeginSetup"
537 print "%%IncludeResource: font Helvetica"
538 print "%%EndSetup"
539
540 # Now do each page.
541 puzzle_index = 0;
542
543 for i in xrange(1, pages+1):
544     print "%%Page:", i, i
545     print "save"
546
547     # Do the drawing for each puzzle, giving a set of PS fragments
548     # and bounding boxes.
549     fragments = [['' for i in xrange(xx)] for i in xrange(yy)]
550     lrbound = [(0,0) for i in xrange(xx)]
551     tbbound = [(0,0) for i in xrange(yy)]
552
553     for y in xrange(yy):
554         for x in xrange(xx):
555             if puzzle_index >= len(ids):
556                 break
557             coords, frag = formatter(ids[puzzle_index])
558             fragments[y][x] = frag
559             lb, rb = lrbound[x]
560             lrbound[x] = (max(lb, coords[0]), max(rb, coords[1]))
561             tb, bb = tbbound[y]
562             tbbound[y] = (max(tb, coords[2]), max(bb, coords[3]))
563             puzzle_index = puzzle_index + 1
564
565     # Now we know the sizes of everything, do the drawing in such a
566     # way that we provide equal gutter space at the page edges and
567     # between puzzle rows/columns.
568     for y in xrange(yy):
569         for x in xrange(xx):
570             if len(fragments[y][x]) > 0:
571                 print "gsave"
572                 print "clippath flattenpath pathbbox pop pop translate"
573                 print "clippath flattenpath pathbbox 4 2 roll pop pop"
574                 # Compute the total height of all puzzles, which
575                 # we'll use it to work out the amount of gutter
576                 # space below this puzzle.
577                 htotal = reduce(lambda a,b:a+b, map(lambda (a,b):a+b, tbbound), 0)
578                 # Now compute the total height of all puzzles
579                 # _below_ this one, plus the height-below-origin of
580                 # this one.
581                 hbelow = reduce(lambda a,b:a+b, map(lambda (a,b):a+b, tbbound[y+1:]), 0)
582                 hbelow = hbelow + tbbound[y][1]
583                 print "%g sub %d mul %d div %g add exch" % (htotal, yy-y, yy+1, hbelow)
584                 # Now do all the same computations for width,
585                 # except we need the total width of everything
586                 # _before_ this one since the coordinates work the
587                 # other way round.
588                 wtotal = reduce(lambda a,b:a+b, map(lambda (a,b):a+b, lrbound), 0)
589                 # Now compute the total height of all puzzles
590                 # _below_ this one, plus the height-below-origin of
591                 # this one.
592                 wleft = reduce(lambda a,b:a+b, map(lambda (a,b):a+b, lrbound[:x]), 0)
593                 wleft = wleft + lrbound[x][0]
594                 print "%g sub %d mul %d div %g add exch" % (wtotal, x+1, xx+1, wleft)
595                 print "translate"
596                 sys.stdout.write(fragments[y][x])
597                 print "grestore"
598
599     print "restore showpage"
600
601 print "%%EOF"