Original Code
In the intro to Part 1 I mentioned that I doodle in a throwaway python file. Well, here is one I did not throw away, which turned into the seed for the code in this book.
The code has not been improved in any way other than format it via black.
It has comments written as "we" because it was meant to be a blog post, and then it grew.
The code in the book is a lightly cleaned up version of this.
1import svgwrite
2
3
4class Box():
5
6 def __init__(
7 self, x=0, y=0, w=1, h=1, red=False, blue=False, yellow=False, letter=None
8 ):
9 self.x = x
10 self.y = y
11 self.w = w
12 self.h = h
13 self.red = red
14 self.blue = blue
15 self.yellow = yellow
16 self.letter = letter
17
18 def __str__(self):
19 return self.letter or 'Box'
20
21 def __repr__(self):
22 return self.letter or 'Box'
23
24
25big_box = Box(0, 0, 80, 1000)
26pages = [big_box]
27
28
29def draw_boxes(boxes, with_boxes=True):
30 STYLES = """
31 .red { fill: red;}
32 .green { fill: green;}
33 .blue {fill: blue;}
34 .yellow {fill: yellow;}
35 .bigbox {fill: cyan; }
36 """
37 dwg = svgwrite.Drawing(
38 'test.svg', profile='full', size=('%d' % big_box.w, '%d' % big_box.w)
39 )
40 dwg.defs.add(dwg.style(STYLES))
41 for bb in pages:
42 dwg.add(dwg.rect(insert=(bb.x, bb.y), size=(bb.w, bb.h), class_='bigbox'))
43 for box in boxes:
44 if with_boxes:
45 if box.red:
46 dwg.add(
47 dwg.rect(insert=(box.x, box.y), size=(box.w, box.h), class_='red')
48 )
49 elif box.blue:
50 dwg.add(
51 dwg.rect(insert=(box.x, box.y), size=(box.w, box.h), class_='blue')
52 )
53 elif box.yellow:
54 dwg.add(
55 dwg.rect(
56 insert=(box.x, box.y), size=(box.w, box.h), class_='yellow'
57 )
58 )
59 else:
60 dwg.add(
61 dwg.rect(insert=(box.x, box.y), size=(box.w, box.h), class_='green')
62 )
63 if box.letter:
64 dwg.add(
65 dwg.text(
66 box.letter,
67 insert=(box.x, box.y + box.h),
68 font_size=box.h,
69 font_family='Arial',
70 )
71 )
72 dwg.save()
73
74
75# So, how could we layout the many boxes inside the big_box?
76many_boxes = [Box(0, 0, 1, 1) for _ in range(5000)]
77
78# Do nothing ...
79# They are all in the same place
80# draw_boxes(many_boxes)
81
82# Try to lay them out side by side
83# They just go too wide
84# We add a "separation" constant so you can see the boxes individually
85separation = .2
86
87
88def layout1(boxes):
89 for i, box in enumerate(boxes):
90 box.x = i * (1 + separation)
91
92
93# Put each one next to the other until they reach a width,
94# then go down. This is a monospaced layout.
95
96
97def layout2(boxes):
98 for i, box in enumerate(boxes[1:]):
99 box.x = boxes[i - 1].x + 1 + separation
100 box.y = boxes[i - 1].y
101 if box.x > big_box.w:
102 box.x = 0
103 box.y = boxes[i - 1].y + 1.1
104
105
106# Now, what happens if some boxes are wider than the others?
107# layout2 will make them overlap or have wide breaks between them
108import random
109
110many_boxes = [Box(0, 0, 1 + random.randint(-2, 2) / 10, 1) for _ in range(5000)]
111
112# So, we use each box's width instead of a fixed width
113
114
115def layout3(boxes):
116 for i, box in enumerate(boxes[1:], 1):
117 prev_box = boxes[i - 1]
118 box.x = prev_box.x + prev_box.w + separation
119 box.y = prev_box.y
120 if box.x > big_box.w:
121 box.x = 0
122 box.y = prev_box.y + 1.1
123
124
125# layout3(many_boxes)
126# But the right side is ragged
127
128# We can, when we break, put the remaining space spread between boxes
129# This is a "justified" layout
130
131
132def layout4(boxes):
133 last_break = 0
134 for i, box in enumerate(boxes[1:], 1):
135 prev_box = boxes[i - 1]
136 box.x = prev_box.x + prev_box.w + separation
137 box.y = prev_box.y
138 if box.x > big_box.w:
139 box.x = 0
140 box.y = prev_box.y + 1.1
141 slack = big_box.w - (prev_box.x + prev_box.w)
142 mini_slack = slack / (i - last_break)
143 for j, b in enumerate(boxes[last_break:i]):
144 b.x += j * mini_slack
145 last_break = i
146
147
148# layout4(many_boxes)
149
150# But what happens if some boxes are red?
151for box in many_boxes:
152 if random.randint(1, 6) > 5:
153 box.red = True
154# Well, nothing much, other than they are red
155
156# But what if red means "stretchy" and only those can stretch?
157
158
159def layout5(boxes):
160 last_break = 0
161 for i, box in enumerate(boxes[1:], 1):
162 prev_box = boxes[i - 1]
163 box.x = prev_box.x + prev_box.w + separation
164 box.y = prev_box.y
165 if box.x > big_box.w:
166 box.x = 0
167 box.y = prev_box.y + 1.1
168 slack = big_box.w - (prev_box.x + prev_box.w)
169 row = boxes[last_break:i]
170 reds = [b for b in row if b.red]
171 # sometimes there is no red in the row. Do nothing.
172 if reds:
173 mini_slack = slack / len(reds)
174 for b in reds:
175 b.w += mini_slack
176 for j, b in enumerate(row[1:], 1):
177 b.x = row[j - 1].x + row[j - 1].w + separation
178 last_break = i
179
180
181# But what happens if a few are blue?
182for box in many_boxes:
183 if random.randint(1, 150) > 149:
184 box.blue = True
185
186# Well, nothing much, other than they are blue
187
188# But what if blue means "this row ends here"?
189
190
191def layout6(boxes):
192 last_break = 0
193 for i, box in enumerate(boxes[1:], 1):
194 prev_box = boxes[i - 1]
195 box.x = prev_box.x + prev_box.w + separation
196 box.y = prev_box.y
197 if prev_box.blue or box.x > big_box.w:
198 box.x = 0
199 box.y = prev_box.y + 1.1
200 slack = big_box.w - (prev_box.x + prev_box.w)
201 row = boxes[last_break:i]
202 reds = [b for b in row if b.red]
203 # sometimes there is no red in the row. Do nothing.
204 if reds:
205 mini_slack = slack / len(reds)
206 for b in reds:
207 b.w += mini_slack
208 for j, b in enumerate(row[1:], 1):
209 b.x = row[j - 1].x + row[j - 1].w + separation
210 last_break = i
211
212
213# Some reds get reeeeeeealy stretchy! That is because rows that
214# end because of a blue have very few boxes. So maybe we don't stretch those?
215
216
217def layout7(boxes):
218 last_break = 0
219 for i, box in enumerate(boxes[1:], 1):
220 prev_box = boxes[i - 1]
221 box.x = prev_box.x + prev_box.w + separation
222 box.y = prev_box.y
223 if prev_box.blue or box.x > big_box.w:
224 box.x = 0
225 box.y = prev_box.y + 1.1
226 if not prev_box.blue:
227 row = boxes[last_break:i]
228 slack = big_box.w - (row[-1].x + row[-1].w)
229 reds = [b for b in row if b.red]
230 # sometimes there is no red in the row. Do nothing.
231 if reds:
232 mini_slack = slack / len(reds)
233 for b in reds:
234 b.w += mini_slack
235 for j, b in enumerate(row[1:], 1):
236 b.x = row[j - 1].x + row[j - 1].w + separation
237 last_break = i
238
239
240# What if we want blue boxes break lines but also separate lines a little?
241
242
243def layout8(boxes):
244 last_break = 0
245 for i, box in enumerate(boxes[1:], 1):
246 prev_box = boxes[i - 1]
247 box.x = prev_box.x + prev_box.w + separation
248 box.y = prev_box.y
249 if prev_box.blue or box.x > big_box.w:
250 box.x = 0
251 if prev_box.blue:
252 box.y = prev_box.y + 2.1
253 else: # not blue
254 box.y = prev_box.y + 1.1
255 row = boxes[last_break:i]
256 slack = big_box.w - (row[-1].x + row[-1].w)
257 reds = [b for b in row if b.red]
258 # sometimes there is no red in the row. Do nothing.
259 if reds:
260 mini_slack = slack / len(reds)
261 for b in reds:
262 b.w += mini_slack
263 for j, b in enumerate(row[1:], 1):
264 b.x = row[j - 1].x + row[j - 1].w + separation
265 last_break = i
266
267
268# So ... what if each box has a letter or a space inside it?
269for box in many_boxes:
270 # More than one space so they appear often
271 box.letter = random.choice(' abcdefghijklmnopqrstuvwxyz')
272
273# Maybe we should make the box sizes depend on the letter inside it?
274# This is complicated, sorry
275import harfbuzz as hb
276import freetype2 as ft
277
278
279def adjust_widths_by_letter(boxes):
280 buf = hb.Buffer.create()
281 buf.add_str(''.join(b.letter for b in boxes))
282 buf.guess_segment_properties()
283 font_lib = ft.get_default_lib()
284 face = font_lib.find_face('Arial')
285 face.set_char_size(size=1, resolution=64)
286 font = hb.Font.ft_create(face)
287 hb.shape(font, buf)
288 # at this point buf.glyph_positions has all the data we need
289 for box, position in zip(boxes, buf.glyph_positions):
290 box.w = position.x_advance
291
292
293adjust_widths_by_letter(many_boxes)
294# layout8(many_boxes)
295
296# There is all that space between letters we added when they were boxes. Let's remove it.
297
298separation = 0
299layout8(many_boxes)
300
301# How about we get the letters from a text instead of randomly?
302p_and_p = open('pride-and-prejudice.txt').read()
303text_boxes = []
304for l in p_and_p:
305 text_boxes.append(Box(letter=l))
306adjust_widths_by_letter(text_boxes)
307# layout8(text_boxes)
308# Oh, it's all green now, and it's all one thing after another. We should make newlines blue!
309
310for b in text_boxes:
311 if b.letter == '\n':
312 b.blue = True
313# layout8(text_boxes)
314
315# Better, but newlines should not really take any space should they?
316
317
318def add_blue(boxes):
319 for b in boxes:
320 if b.letter == '\n':
321 b.blue = True
322 b.w = 0
323
324
325add_blue(text_boxes)
326# layout8(text_boxes)
327
328# Our big_box is very wide now, that is why we have long lines. Let's make it narrower
329big_box = Box(0, 0, 30, 1000)
330# layout8(text_boxes)
331
332# But our right side is ragged again! We should make spaces red.
333
334
335def add_red(boxes):
336 for b in boxes:
337 if b.letter == ' ':
338 b.red = True
339
340
341add_red(text_boxes)
342# layout8(text_boxes)
343
344# The second paragraph of Chapter 1 shows a red space as first thing in the row, and that looks bad!
345# So, when the 1st letter in a row is a space, let's make it take no width and not stretch
346
347
348def layout9(boxes):
349 last_break = 0
350 for i, box in enumerate(boxes[1:], 1):
351 prev_box = boxes[i - 1]
352 box.x = prev_box.x + prev_box.w + separation
353 box.y = prev_box.y
354 if prev_box.blue or box.x > big_box.w:
355 box.x = 0
356 if box.red:
357 box.w = 0
358 if prev_box.blue:
359 box.y = prev_box.y + 2.1
360 else: # not blue
361 box.y = prev_box.y + 1.1
362 row = boxes[last_break:i]
363 slack = big_box.w - (row[-1].x + row[-1].w)
364 # If the 1st thing is a red, that one doesn't stretch
365 reds = [b for b in row[1:] if b.red]
366 # sometimes there is no red in the row. Do nothing.
367 if reds:
368 mini_slack = slack / len(reds)
369 for b in reds:
370 b.w += mini_slack
371 for j, b in enumerate(row[1:], 1):
372 b.x = row[j - 1].x + row[j - 1].w + separation
373 last_break = i
374
375
376# Just for fun, let's draw it without the colored boxes
377# layout9(text_boxes)
378# draw_boxes(text_boxes, False)
379
380# XXX layout9 rewritten using pop(), need to backport this version
381
382
383def layout10(_boxes):
384 boxes = _boxes[:] # Work on a copy
385 prev_box = boxes.pop(0)
386 row = [prev_box]
387 while (boxes):
388 box = boxes.pop(0)
389 row.append(box)
390 box.x = prev_box.x + prev_box.w + separation
391 box.y = prev_box.y
392 if prev_box.blue or box.x > big_box.w:
393 box.x = 0
394 row.pop() # our box will go in the next row
395 if box.red:
396 box.w = 0
397 if prev_box.blue:
398 box.y = prev_box.y + 2.1
399
400 else: # not blue
401 box.y = prev_box.y + 1.1
402 slack = big_box.w - (row[-1].x + row[-1].w)
403 # If the 1st thing is a red, that one doesn't stretch
404 reds = [b for b in row[1:] if b.red]
405 # sometimes there is no red in the row. Do nothing.
406 if reds:
407 mini_slack = slack / len(reds)
408 for b in reds:
409 b.w += mini_slack
410 for j, b in enumerate(row[1:], 1):
411 b.x = row[j - 1].x + row[j - 1].w + separation
412 row = [box]
413 prev_box = box
414
415
416# layout10(text_boxes)
417
418# Looks good, except that the words are broken wrong. You can't break good like: g
419# ood!
420
421# What if we only break on spaces?
422
423
424def layout11(_boxes):
425 boxes = _boxes[:] # Work on a copy
426 prev_box = boxes.pop(0)
427 row = [prev_box]
428 while (boxes):
429 box = boxes.pop(0)
430 row.append(box)
431 box.x = prev_box.x + prev_box.w + separation
432 box.y = prev_box.y
433 if prev_box.blue or (box.x > big_box.w and box.red):
434 row.pop() # our box will go in the next row
435 box.x = 0
436 if box.red:
437 box.w = 0
438 if prev_box.blue:
439 box.y = prev_box.y + 2.1
440
441 else: # not blue
442 box.y = prev_box.y + 1.1
443 slack = big_box.w - (row[-1].x + row[-1].w)
444 # If the 1st thing is a red, that one doesn't stretch
445 reds = [b for b in row[1:] if b.red]
446 # sometimes there is no red in the row. Do nothing.
447 if reds:
448 mini_slack = slack / len(reds)
449 for b in reds:
450 b.w += mini_slack
451 for j, b in enumerate(row[1:], 1):
452 b.x = row[j - 1].x + row[j - 1].w + separation
453 row = [box]
454 prev_box = box
455
456
457# layout11(text_boxes)
458
459# That actually ... worked? Except that when we need to fit something wider than
460# big_box because we did not break the row, the slack is NEGATIVE and words get
461# smushed together!
462
463# What we actually need is hyphenation.
464# We can use pyphen to insert soft-hyphen characters wherever words can break.
465# And we can mark those positions yellow.
466
467import pyphen
468
469hyphenator = pyphen.Pyphen(lang='en_GB') # These things are language dependent
470
471p_and_p = open('pride-and-prejudice.txt').readlines()
472for i, l in enumerate(p_and_p):
473 words = l.split(' ')
474 p_and_p[i] = ' '.join(hyphenator.inserted(w, '\u00AD') for w in words)
475p_and_p = ''.join(p_and_p)
476
477text_boxes = []
478for l in p_and_p:
479 text_boxes.append(Box(letter=l))
480
481
482# This makes the characters '\u00AD' (soft-hyphen) yellow.
483
484
485def add_yellow(boxes):
486 for b in boxes:
487 if b.letter == '\u00AD':
488 b.yellow = True
489
490
491add_blue(text_boxes)
492add_red(text_boxes)
493add_yellow(text_boxes)
494adjust_widths_by_letter(text_boxes)
495
496# And create a new layout function that also breaks on yellow boxes.
497
498
499def layout12(_boxes):
500 boxes = _boxes[:] # Work on a copy
501 prev_box = boxes.pop(0)
502 row = [prev_box]
503 while (boxes):
504 box = boxes.pop(0)
505 row.append(box)
506 box.x = prev_box.x + prev_box.w + separation
507 box.y = prev_box.y
508 if prev_box.blue or (box.x > big_box.w and (box.red or box.yellow)):
509 row.pop() # our box will go in the next row
510 box.x = 0
511 if box.red:
512 box.w = 0
513 if prev_box.blue:
514 box.y = prev_box.y + 2.1
515
516 else: # not blue
517 box.y = prev_box.y + 1.1
518 slack = big_box.w - (row[-1].x + row[-1].w)
519 # If the 1st thing is a red, that one doesn't stretch
520 reds = [b for b in row[1:] if b.red]
521 # sometimes there is no red in the row. Do nothing.
522 if reds:
523 mini_slack = slack / len(reds)
524 for b in reds:
525 b.w += mini_slack
526 for j, b in enumerate(row[1:], 1):
527 b.x = row[j - 1].x + row[j - 1].w + separation
528 row = [box]
529 prev_box = box
530
531
532# layout12(text_boxes)
533
534# Better! Since we have more break chances, there is less word-smushing.
535# However our typography is wrong, because we are hyphenating but not showing a hyphen!
536# So, we need to ADD a box when we hyphenate. That special box is hyphenbox():
537
538
539def hyphenbox(): # Yes, this is not optimal
540 b = Box(letter='-', yellow=True)
541 adjust_widths_by_letter([b])
542 return b
543
544
545def layout13(_boxes):
546 boxes = _boxes[:] # Work on a copy
547 prev_box = boxes.pop(0)
548 row = [prev_box]
549 while (boxes):
550 box = boxes.pop(0)
551 row.append(box)
552 box.x = prev_box.x + prev_box.w + separation
553 box.y = prev_box.y
554 if prev_box.blue or (box.x > big_box.w and (box.red or box.yellow)):
555 row.pop() # our box will go in the next row
556 if box.yellow: # We need to insert the hyphen!
557 h_b = hyphenbox()
558 h_b.x = prev_box.x + prev_box.w + separation
559 h_b.y = prev_box.y
560 _boxes.append(h_b) # So it's drawn
561 row.append(h_b) # So it's justified
562 box.x = 0
563 if box.red:
564 box.w = 0
565 if prev_box.blue:
566 box.y = prev_box.y + 2.1
567
568 else: # not blue
569 box.y = prev_box.y + 1.1
570 slack = big_box.w - (row[-1].x + row[-1].w)
571 # If the 1st thing is a red, that one doesn't stretch
572 reds = [b for b in row[1:] if b.red]
573 # sometimes there is no red in the row. Do nothing.
574 if reds:
575 mini_slack = slack / len(reds)
576 for b in reds:
577 b.w += mini_slack
578 for j, b in enumerate(row[1:], 1):
579 b.x = row[j - 1].x + row[j - 1].w + separation
580 row = [box]
581 prev_box = box
582
583
584# layout13(text_boxes)
585
586# Good. However, we still have smushing. It's usually considered better to make spaces
587# between words grow, rather than shrink. So, what we should do is, instead of breaking
588# in the 1st yellow/red PAST the break, go back and break in the last BEFORE the break!
589
590# XXX backport simplifications from layout14: separation of blue case
591
592
593def layout14(_boxes):
594 boxes = _boxes[:] # Work on a copy
595 row = [boxes.pop(0)]
596 while (boxes):
597 prev_box = row[-1]
598 box = boxes.pop(0)
599 row.append(box)
600 box.x = prev_box.x + prev_box.w + separation
601 box.y = prev_box.y
602 if prev_box.blue:
603 box.x = 0
604 box.y = prev_box.y + 2.1
605 row.pop()
606 row = [box]
607
608 elif box.x > big_box.w:
609 while not (box.red or box.yellow): # backtrack onw
610 boxes.insert(0, row.pop())
611 # tip of row is now box, previous one is prev_box
612 box = row[-1]
613 prev_box = row[-2]
614
615 row.pop() # Breaking box goes in next row
616 if box.yellow: # We need to insert the hyphen!
617 h_b = hyphenbox()
618 h_b.x = prev_box.x + prev_box.w + separation
619 h_b.y = prev_box.y
620 _boxes.append(h_b) # So it's drawn
621 row.append(h_b) # So it's justified
622 box.x = 0
623 box.y = prev_box.y + 1.1
624 if box.red:
625 box.w = 0
626 slack = big_box.w - (row[-1].x + row[-1].w)
627 # If the 1st thing is a red, that one doesn't stretch
628 reds = [b for b in row[1:] if b.red]
629 # sometimes there is no red in the row. Do nothing.
630 if reds:
631 mini_slack = slack / len(reds)
632 for b in reds:
633 b.w += mini_slack
634 for j, b in enumerate(row[1:], 1):
635 b.x = row[j - 1].x + row[j - 1].w + separation
636 row = [box]
637
638
639# With a little separation for niceness, the result is not half bad!
640separation = 0.05
641# layout14(text_boxes)
642
643# So far, our big_box has been very, very tall. What would happen if it were short?
644big_box = Box(0, 0, 30, 60)
645# layout14(text_boxes)
646
647# Well, that our text flows straight out of the box! And our initial problem was
648# "So, how could we layout the many boxes inside the big_box?" !
649# That won't do. Clearly, if we have more text than fits in a box, we need more than one
650# box. Like pages.
651
652# So, let's make many big boxes.
653
654
655pages = [Box(0, i * 65, 30, 60) for i in range(50)]
656
657# layout14(text_boxes)
658
659# Of course our layout engine does nothing with those pages. Let's fix that.
660
661
662def layout15(_boxes):
663 boxes = _boxes[:] # Work on a copy
664 row = [boxes.pop(0)]
665
666 # Put the 1st box in the beginning of the 1st page
667 page = 0
668 row[0].x = pages[page].x
669 row[0].y = pages[page].y
670
671 while (boxes):
672 prev_box = row[-1]
673 box = boxes.pop(0)
674 row.append(box)
675 box.x = prev_box.x + prev_box.w + separation
676 box.y = prev_box.y
677 if prev_box.blue:
678 box.x = 0
679 box.y = prev_box.y + 2.1
680 row.pop()
681 row = [box]
682
683 elif box.x > pages[page].w:
684 while not (box.red or box.yellow): # backtrack onw
685 boxes.insert(0, row.pop())
686 # tip of row is now box, previous one is prev_box
687 box = row[-1]
688 prev_box = row[-2]
689
690 row.pop() # Breaking box goes in next row
691 if box.yellow: # We need to insert the hyphen!
692 h_b = hyphenbox()
693 h_b.x = prev_box.x + prev_box.w + separation
694 h_b.y = prev_box.y
695 _boxes.append(h_b) # So it's drawn
696 row.append(h_b) # So it's justified
697 box.x = 0
698 box.y = prev_box.y + 1.1
699 if box.red:
700 box.w = 0
701 slack = pages[page].w - (row[-1].x + row[-1].w)
702 # If the 1st thing is a red, that one doesn't stretch
703 reds = [b for b in row[1:] if b.red]
704 # sometimes there is no red in the row. Do nothing.
705 if reds:
706 mini_slack = slack / len(reds)
707 for b in reds:
708 b.w += mini_slack
709 for j, b in enumerate(row[1:], 1):
710 b.x = row[j - 1].x + row[j - 1].w + separation
711 row = [box]
712
713 if box.y + box.h > pages[page].y + pages[page].h:
714 # We need to go to the next page
715 page = page + 1
716 box.y = pages[page].y
717
718
719# layout15(text_boxes)
720# And there you go, pagination
721
722# What happens if pages are organized differently?
723# Let's put them left-to-right
724pages = [Box(i * 35, 0, 30, 60) for i in range(50)]
725# layout15(text_boxes)
726
727# Everything is in the same page, because we are resetting x to 0 on line breaks.
728# You always need to find your hidden assumptions.
729
730
731def layout16(_boxes):
732 boxes = _boxes[:] # Work on a copy
733 row = [boxes.pop(0)]
734
735 # Put the 1st box in the beginning of the 1st page
736 page = 0
737 row[0].x = pages[page].x
738 row[0].y = pages[page].y
739
740 while (boxes):
741 prev_box = row[-1]
742 box = boxes.pop(0)
743 row.append(box)
744 box.x = prev_box.x + prev_box.w + separation
745 box.y = prev_box.y
746 if prev_box.blue:
747 box.x = pages[page].x
748 box.y = prev_box.y + 2.1
749 row.pop()
750 row = [box]
751
752 elif box.x > pages[page].w + pages[page].x:
753 while not (box.red or box.yellow): # backtrack onw
754 boxes.insert(0, row.pop())
755 # tip of row is now box, previous one is prev_box
756 box = row[-1]
757 prev_box = row[-2]
758
759 row.pop() # Breaking box goes in next row
760 if box.yellow: # We need to insert the hyphen!
761 h_b = hyphenbox()
762 h_b.x = prev_box.x + prev_box.w + separation
763 h_b.y = prev_box.y
764 _boxes.append(h_b) # So it's drawn
765 row.append(h_b) # So it's justified
766 box.x = pages[page].x
767 box.y = prev_box.y + 1.1
768 if box.red:
769 box.w = 0
770 slack = (pages[page].w + pages[page].x) - (row[-1].x + row[-1].w)
771 # If the 1st thing is a red, that one doesn't stretch
772 reds = [b for b in row[1:] if b.red]
773 # sometimes there is no red in the row. Do nothing.
774 if reds:
775 mini_slack = slack / len(reds)
776 for b in reds:
777 b.w += mini_slack
778 for j, b in enumerate(row[1:], 1):
779 b.x = row[j - 1].x + row[j - 1].w + separation
780 row = [box]
781
782 # We may need to go to the next page
783 if box.y + box.h > pages[page].y + pages[page].h:
784 page = page + 1
785 # Since the location of a page is arbitrary, both coordinates are reset
786 box.y = pages[page].y
787 box.x = pages[page].x
788
789
790# This may even work with oddly-sized pages!
791pages = [Box(i * 35, 0, 20, 60 + random.randint(-20, 20)) for i in range(50)]
792
793layout16(text_boxes)
794draw_boxes(text_boxes, False)
795
796# This is a good point to STOP. Look at the code, and clean house.