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.

results matching ""

    No results matching ""