BOXES v0.7

So far in our previous lessons we have worked in an abstract world of boxes. Some hints of a direction were visible, like organizing our boxes in pages and trying to achieve a justified layout among others.

So, let's just say it, we are going to be doing text layout. But not the easy one. No, sir. No monospaced fonts for us. We want to do the whole enchilada, we are going to have variable-width fonts with kerning, and multi-page, fully-justified text layouts with hyphenation.

OK, perhaps about 50% of the enchilada, because it will have no bidirectional support, only work in English, only read UTF-8 encoded files, and so on a lot of things. But it's still a lot of Mexican food!

And we are going to do that in lessons not much longer than the ones you have been seeing so far. Except this one. This one is much longer. So let's get started.

Clearly, we want our boxes to have letters. And our "stretchy" boxes are special because they have things like spaces. In fact, let's just say they have spaces.

We will now expand our Box class to support letters inside the boxes.

 1class Box():
 3    def __init__(self, x=0, y=0, w=1, h=1, stretchy=False, letter="x"):
 4        """Accept arguments to define our box, and store them."""
 5        self.x = x
 6        self.y = y
 7        self.w = w
 8        self.h = h
 9        self.stretchy = stretchy
10        self.letter = letter
12    def __repr__(self):
13        return 'Box(%s, %s, %s, %s, "%s")' % (
14            self.x, self.y, self.w, self.h, self.letter
15        )
18# Many boxes, all the same width, with an x in them
19text_boxes = [Box() for i in range(5000)]
21# A few pages all the same size
22pages = [Box(i * 35, 0, 30, 50) for i in range(10)]

We can keep using the exact same layout function, so no need to show it here.

The drawing function needs tweaking to show us letters, and to make the colored boxes optional.

89import svgwrite
92def draw_boxes(boxes, fname, size, hide_boxes=False):
93    dwg = svgwrite.Drawing(fname, profile="full", size=size)
94    # Draw the pages
95    for page in pages:
96        dwg.add(
97            dwg.rect(
98                insert=(f"{page.x}cm", f"{page.y}cm"),
99                size=(f"{page.w}cm", f"{page.h}cm"),
100                fill="lightblue",
101            )
102        )
103    # Draw all the boxes
104    for box in boxes:
105        # The box color depends on its features
106        color = "green" if box.stretchy else "red"
107        # Make the colored boxes optional
108        if not hide_boxes:
109            dwg.add(
110                dwg.rect(
111                    insert=(f"{box.x}cm", f"{box.y}cm"),
112                    size=(f"{box.w}cm", f"{box.h}cm"),
113                    fill=color,
114                )
115            )
116        # Display the letter in the box
117        if box.letter:
118            dwg.add(
119                dwg.text(
120                    box.letter,
121                    insert=(f"{box.x}cm", f"{box.y + box.h}cm"),
122                    font_size=f"{box.h}cm",
123                    font_family="Arial",
124                )
125            )
129draw_boxes(text_boxes, "lesson7.svg", ("30cm", "20cm"))


Of course this is very boring, so we need to spice up our data a little. We can use different letters, and then make the right ones stretchy. That is easy!

132from random import choice
134for box in text_boxes:
135    # More than one space so they appear often
136    box.letter = choice("     abcdefghijklmnopqrstuvwxyz")
137    if box.letter == " ":  # Spaces are stretchy
138        box.stretchy = True
142    text_boxes, "lesson7_different_letters.svg", ("30cm", "20cm")


As you can see, there are very minor horizontal shifts and stretches, since all boxes are the same size.

But as a text layout engine we have a major failure: we are ignoring the size of the letters we are laying out!

This is a very complex thing to do called text shaping. You need to understand the content of the font you are using to display the text, and more subtle things like what happens if you put specific letters next to each other (kerning) and much more.

The good news is that it's already done for us, in libraries called Harfbuzz and Freetype.

This paragraph is perhaps the most important one in this book. I am about to show you some obscure code. And I will tell you the secret of how it got here: I copied it from the documentation for the libraries I am using. Sometimes you will need to do something complicated only once in your life. It's perfectly OK to just google how to do it. And as long as you are confident you can find it again if needed, it's OK to just forget about it.

I will show you this code, and then put it in a separate file called and from now on I will not show it in the lessons, because we are not going to change it, ever.

 1import harfbuzz as hb
 2import freetype2 as ft
 5def adjust_widths_by_letter(boxes):
 6    """Takes a list of boxes as arguments, and uses harfbuzz to
 7    adjust the width of each box to match the harfbuzz text shaping."""
 8    buf = hb.Buffer.create()
 9    buf.add_str("".join(b.letter for b in boxes))
10    buf.guess_segment_properties()
11    font_lib = ft.get_default_lib()
12    face = font_lib.find_face("Arial")
13    face.set_char_size(size=1, resolution=64)
14    font = hb.Font.ft_create(face)
15    hb.shape(font, buf)
16    # at this point buf.glyph_positions has all the data we need
17    for box, position in zip(boxes, buf.glyph_positions):
18        box.w = position.x_advance

And now we will pretend we know what that does, based on its docstring and use it.

146from code import fonts
149separation = .05
152draw_boxes(text_boxes, "lesson7_adjusted_letters.svg", ("30cm", "20cm"))


And nicer, without the boxes:

158    text_boxes,
159    "lesson7_adjusted_letters_no_boxes.svg",
160    ("30cm", "20cm"),
161    hide_boxes=True,


And of course, we can just load text there instead of random letters. For example, here we load what is going to be our example test from now on, Jane Austen's Pride and Prejudice from Project Gutenberg

165p_and_p = open("pride-and-prejudice.txt").read()
166text_boxes = []
167for l in p_and_p:
168    text_boxes.append(Box(letter=l, stretchy=l == " "))
172    text_boxes,
173    "lesson7_pride_and_prejudice.svg",
174    ("30cm", "20cm"),
175    hide_boxes=True,


And that is ... maybe disappointing? While we spent a lot of time on things like justifying text, we have not even looked at newlines!

Also, spaces at the end of lines make the line appear ragged again, now that they are not boxes.

So, we know what to hit in the next lesson.

Further references:

results matching ""

    No results matching ""