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

lesson8.svg

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)

lesson9.svg

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:

results matching ""

    No results matching ""