Source for: original.py [raw]

  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.
797