BOXES v0.9
In the previous lesson we fixed the handling of newlines in our layout engine, and noticed a problem with spaces. Let's fix it!
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.
126import svgwrite
127
128
129def draw_boxes(boxes, fname, size, hide_boxes=False):
130 dwg = svgwrite.Drawing(fname, profile="full", size=size)
131 # Draw the pages
132 for page in pages:
133 dwg.add(
134 dwg.rect(
135 insert=(f"{page.x}cm", f"{page.y}cm"),
136 size=(f"{page.w}cm", f"{page.h}cm"),
137 fill="lightblue",
138 )
139 )
140 # Draw all the boxes
141 for box in boxes:
142 # The box color depends on its features
143 color = "green" if box.stretchy else "red"
144 # Make the colored boxes optional
145 if not hide_boxes:
146 dwg.add(
147 dwg.rect(
148 insert=(f"{box.x}cm", f"{box.y}cm"),
149 size=(f"{box.w}cm", f"{box.h}cm"),
150 fill=color,
151 )
152 )
153 # Display the letter in the box
154 if box.letter:
155 dwg.add(
156 dwg.text(
157 box.letter,
158 insert=(f"{box.x}cm", f"{box.y + box.h}cm"),
159 font_size=f"{box.h}cm",
160 font_family="Arial",
161 )
162 )
163 dwg.save()
The more obvious problems are:
- It keeps spaces at the end of rows, making the right side ragged.
- White space at the beginning of rows is shown and it looks bad
You can see clearly, in the previous sample output where this happens in one of the latter paragraphs, "to see the place, " appears ragged when it should not. And a similar thing happens in an earlier paragraph where there is a hole against the left margin in " told me all about it".
In both cases, the cause is because the "empty" space is used by spaces!
So, one possible solution is, when justifying a row, to make all the spaces at the right margins 0-width and not stretchy. At the same time, when adding spaces at the beginning of a row, they should become 0-width and not stretchy.
BUT this means the list of boxes will need its width readjusted if they are to be laid out again on different pages! That's because some of the spaces will now be thin and "rigid" so they will work badly if they are not against the margin on a different layout.
It's not a big problem, but it's worth keeping in mind, since it's the kind of thing that becomes an obscure bug later on. So, we add it to the docstring.
30# We add a "separation" constant so you can see the boxes individually
31separation = .05
32
33
34def layout(_boxes):
35 """Layout boxes along pages.
36
37 Keep in mind that this function modifies the boxes themselves, so
38 you should be very careful about trying to call layout() more than once
39 on the same boxes.
40
41 Specifically, some spaces will become 0-width and not stretchy.
42 """
43
44 # Because we modify the box list, we will work on a copy
45 boxes = _boxes[:]
46 # We start at page 0
47 page = 0
48 # The 1st box should be placed in the correct page
49 previous = boxes.pop(0)
50 previous.x = pages[page].x
51 previous.y = pages[page].y
52 row = []
53 while boxes:
54 # We take the new 1st box
55 box = boxes.pop(0)
56 # And put it next to the other
57 box.x = previous.x + previous.w + separation
58 # At the same vertical location
59 box.y = previous.y
60
61 # Handle breaking on newlines
62 break_line = False
63 # But if it's a newline
64 if (box.letter == "\n"):
65 break_line = True
66 # Newlines take no horizontal space ever
67 box.w = 0
68 box.stretchy = False
69
70 # Or if it's too far to the right...
71 elif (box.x + box.w) > (pages[page].x + pages[page].w):
72 break_line = True
73 # We adjust the row
74 # Remove all right-margin spaces
75 while row[-1].letter == " ":
76 row.pop()
77 slack = (pages[page].x + pages[page].w) - (
78 row[-1].x + row[-1].w
79 )
80 # Get a list of all the ones that are stretchy
81 stretchies = [b for b in row if b.stretchy]
82 if not stretchies: # Nothing stretches do as before.
83 bump = slack / len(row)
84 # The 1st box gets 0 bumps, the 2nd gets 1 and so on
85 for i, b in enumerate(row):
86 b.x += bump * i
87 else:
88 bump = slack / len(stretchies)
89 # Each stretchy gets wider
90 for b in stretchies:
91 b.w += bump
92 # And we put each thing next to the previous one
93 for j, b in enumerate(row[1:], 1):
94 b.x = row[j - 1].x + row[j - 1].w + separation
95
96 if break_line:
97 # We start a new row
98 row = []
99 # We go all the way left and a little down
100 box.x = pages[page].x
101 box.y = previous.y + previous.h + separation
102
103 # But if we go too far down
104 if box.y + box.h > pages[page].y + pages[page].h:
105 # We go to the next page
106 page += 1
107 # And put the box at the top-left
108 box.x = pages[page].x
109 box.y = pages[page].y
110
111 # Put the box in the row
112 row.append(box)
113
114 # Collapse all left-margin space
115 if all(b.letter == " " for b in row):
116 box.w = 0
117 box.stretchy = False
118 box.x = pages[page].x
119
120 previous = box
121
122
123layout(text_boxes)
166draw_boxes(text_boxes, "lesson9.svg", ("30cm", "50cm"), hide_boxes=True)
As you can see, the justification now is absolutely tight where it needs to be. With that taken care of, we will consider the problem of breaking lines inside words and how to fix it using hyphenation in the next lesson.
Further references:
- Full source code for this lesson lesson9.py
- Difference with code from last lesson