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":

lesson7_pride_and_prejudice.svg

Let's count the problems:

  1. It totally ignores newlines everywhere
  2. It keeps spaces at the end of rows, making the right side ragged (see "said his " in the seventh line)
  3. White space at the beginning of rows is shown and it looks bad (see " a neigh" at the beginning of the fifth line)
  4. 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)

lesson8.svg


Further references:

results matching ""

    No results matching ""