BOXES v0.8
In the previous lesson, we started using our layout engine to display text, and ran into some limitations. Let's get rid of them.
We have no changes in our Box class, or the page setup, or how we load and adjust the boxes' sizes.
1from fonts import adjust_widths_by_letter
2
3
4class Box():
5
6 def __init__(self, x=0, y=0, w=1, h=1, stretchy=False, letter="x"):
7 """Accept arguments to define our box, and store them."""
8 self.x = x
9 self.y = y
10 self.w = w
11 self.h = h
12 self.stretchy = stretchy
13 self.letter = letter
14
15 def __repr__(self):
16 return 'Box(%s, %s, %s, %s, "%s")' % (
17 self.x, self.y, self.w, self.h, self.letter
18 )
19
20
21p_and_p = open("pride-and-prejudice.txt").read()
22text_boxes = []
23for l in p_and_p:
24 text_boxes.append(Box(letter=l, stretchy=l == " "))
25adjust_widths_by_letter(text_boxes)
26
27# A few pages all the same size
28pages = [Box(i * 35, 0, 30, 50) for i in range(10)]
Also unchanged is the drawing code.
107import svgwrite
108
109
110def draw_boxes(boxes, fname, size, hide_boxes=False):
111 dwg = svgwrite.Drawing(fname, profile="full", size=size)
112 # Draw the pages
113 for page in pages:
114 dwg.add(
115 dwg.rect(
116 insert=(f"{page.x}cm", f"{page.y}cm"),
117 size=(f"{page.w}cm", f"{page.h}cm"),
118 fill="lightblue",
119 )
120 )
121 # Draw all the boxes
122 for box in boxes:
123 # The box color depends on its features
124 color = "green" if box.stretchy else "red"
125 # Make the colored boxes optional
126 if not hide_boxes:
127 dwg.add(
128 dwg.rect(
129 insert=(f"{box.x}cm", f"{box.y}cm"),
130 size=(f"{box.w}cm", f"{box.h}cm"),
131 fill=color,
132 )
133 )
134 # Display the letter in the box
135 if box.letter:
136 dwg.add(
137 dwg.text(
138 box.letter,
139 insert=(f"{box.x}cm", f"{box.y + box.h}cm"),
140 font_size=f"{box.h}cm",
141 font_family="Arial",
142 )
143 )
144 dwg.save()
But we need to work on our layout engine, a lot. Here is the image of our attempt at displaying "Pride and Prejudice":
Let's count the problems:
- It totally ignores newlines everywhere
- It keeps spaces at the end of rows, making the right side ragged (see "said his " in the seventh line)
- White space at the beginning of rows is shown and it looks bad (see " a neigh" at the beginning of the fifth line)
- Words are split between lines haphazardly, but this is for later and leads to some serious code that needs its own lesson.
Let's hit the issues in order. First, newlines.
The idea is: if we find a newline, we need to break the line. Doesn't sound particularly complex, specially since lines that are broken intentionally are never fully justified.
The changes are minor:
- Create a flag
break_line
set to True if we encounter a newline or overflow the page. - In case of newline, make that box invisible by making it 0-wide and not stretchy.
- When the break_line flag is set, handle as usual by moving to the left, etc.
30# We add a "separation" constant so you can see the boxes individually
31separation = .05
32
33
34def layout(_boxes):
35 # Because we modify the box list, we will work on a copy
36 boxes = _boxes[:]
37 # We start at page 0
38 page = 0
39 # The 1st box should be placed in the correct page
40 previous = boxes.pop(0)
41 previous.x = pages[page].x
42 previous.y = pages[page].y
43 row = []
44 while boxes:
45 # We take the new 1st box
46 box = boxes.pop(0)
47 # And put it next to the other
48 box.x = previous.x + previous.w + separation
49 # At the same vertical location
50 box.y = previous.y
51
52 # Handle breaking on newlines
53 break_line = False
54 # But if it's a newline
55 if (box.letter == "\n"):
56 break_line = True
57 # Newlines take no horizontal space ever
58 box.w = 0
59 box.stretchy = False
60
61 # Or if it's too far to the right...
62 elif (box.x + box.w) > (pages[page].x + pages[page].w):
63 break_line = True
64 # We adjust the row
65 slack = (pages[page].x + pages[page].w) - (
66 row[-1].x + row[-1].w
67 )
68 # Get a list of all the ones that are stretchy
69 stretchies = [b for b in row if b.stretchy]
70 if not stretchies: # Nothing stretches do as before.
71 bump = slack / len(row)
72 # The 1st box gets 0 bumps, the 2nd gets 1 and so on
73 for i, b in enumerate(row):
74 b.x += bump * i
75 else:
76 bump = slack / len(stretchies)
77 # Each stretchy gets wider
78 for b in stretchies:
79 b.w += bump
80 # And we put each thing next to the previous one
81 for j, b in enumerate(row[1:], 1):
82 b.x = row[j - 1].x + row[j - 1].w + separation
83
84 if break_line:
85 # We start a new row
86 row = []
87 # We go all the way left and a little down
88 box.x = pages[page].x
89 box.y = previous.y + previous.h + separation
90
91 # But if we go too far down
92 if box.y + box.h > pages[page].y + pages[page].h:
93 # We go to the next page
94 page += 1
95 # And put the box at the top-left
96 box.x = pages[page].x
97 box.y = pages[page].y
98
99 # Put the box in the row
100 row.append(box)
101 previous = box
102
103
104layout(text_boxes)
The code changes are small, but the output now looks radically different.
147draw_boxes(text_boxes, "lesson8.svg", ("30cm", "50cm"), hide_boxes=True)
Further references:
- Full source code for this lesson lesson8.py
- Difference with code from last lesson