Source for: boxes.py [raw]

  1"""
  2Usage:
  3    boxes <input> <output>
  4    boxes --version
  5"""
  6
  7from fonts import adjust_widths_by_letter
  8from hyphen import insert_soft_hyphens
  9
 10import svgwrite
 11from docopt import docopt
 12
 13
 14class Box():
 15
 16    def __init__(self, x=0, y=0, w=1, h=1, stretchy=False, letter="x"):
 17        """Accept arguments to define our box, and store them."""
 18        self.x = x
 19        self.y = y
 20        self.w = w
 21        self.h = h
 22        self.stretchy = stretchy
 23        self.letter = letter
 24
 25    def __repr__(self):
 26        return 'Box(%s, %s, %s, %s, "%s")' % (
 27            self.x, self.y, self.w, self.h, self.letter
 28        )
 29
 30
 31def hyphenbox():
 32    b = Box(letter="-")
 33    adjust_widths_by_letter([b])
 34    return b
 35
 36
 37def badness(page_width, row):
 38    """Calculate how 'bad' a position to break is.
 39
 40    bigger is worse.
 41    """
 42    # Yes, this is suboptimal. It's easier to optimize working code
 43    # than fixing fast code.
 44    row_width = (row[-1].x + row[-1].w) - row[0].x
 45    slack = page_width - row_width
 46    stretchies = [b for b in row if b.stretchy]
 47    if len(stretchies) > 0:
 48        stretchies_width = sum(s.w for s in stretchies)
 49        # More stetchy space is good. More slack is bad.
 50        badness = slack / stretchies_width
 51    else:  # Nothing to stretch. Not good.
 52        badness = 1000
 53    if slack < 0:
 54        # Arbitrary fudge factor, negative slack is THIS much worse
 55        badness *= -2
 56    return badness
 57
 58
 59# We add a "separation" constant so you can see the boxes individually
 60separation = .05
 61
 62
 63def layout(_boxes, pages):
 64    """Layout boxes along pages.
 65
 66    Keep in mind that this function modifies the boxes themselves, so
 67    you should be very careful about trying to call layout() more than once
 68    on the same boxes.
 69
 70    Specifically, some spaces will become 0-width and not stretchy.
 71    """
 72
 73    # Because we modify the box list, we will work on a copy
 74    boxes = _boxes[:]
 75    # We start at page 0
 76    page = 0
 77    # The 1st box should be placed in the correct page
 78    previous = boxes.pop(0)
 79    previous.x = pages[page].x
 80    previous.y = pages[page].y
 81    row = []
 82    while boxes:
 83        # We take the new 1st box
 84        box = boxes.pop(0)
 85        # And put it next to the other
 86        box.x = previous.x + previous.w + separation
 87        # At the same vertical location
 88        box.y = previous.y
 89
 90        # Handle breaking on newlines
 91        break_line = False
 92        # But if it's a newline
 93        if (box.letter == "\n"):
 94            break_line = True
 95            # Newlines take no horizontal space ever
 96            box.w = 0
 97            box.stretchy = False
 98
 99        # Or if it's too far to the right, and is a
100        # good place to break the line...
101        elif (box.x + box.w) > (
102            pages[page].x + pages[page].w
103        ) and box.letter in (
104            " ", "\xad"
105        ):
106            if box.letter == "\xad":
107                # Add a visible hyphen in the row
108                h_b = hyphenbox()
109                h_b.x = previous.x + previous.w + separation
110                h_b.y = previous.y
111                _boxes.append(h_b)  # So it's drawn
112                row.append(h_b)  # So it's justified
113            break_line = True
114            # We adjust the row
115            # Remove all right-margin spaces
116            while row[-1].letter == " ":
117                row.pop()
118            slack = (pages[page].x + pages[page].w) - (
119                row[-1].x + row[-1].w
120            )
121            # Get a list of all the ones that are stretchy
122            stretchies = [b for b in row if b.stretchy]
123            if not stretchies:  # Nothing stretches do as before.
124                bump = slack / len(row)
125                # The 1st box gets 0 bumps, the 2nd gets 1 and so on
126                for i, b in enumerate(row):
127                    b.x += bump * i
128            else:
129                bump = slack / len(stretchies)
130                # Each stretchy gets wider
131                for b in stretchies:
132                    b.w += bump
133                # And we put each thing next to the previous one
134                for j, b in enumerate(row[1:], 1):
135                    b.x = row[j - 1].x + row[j - 1].w + separation
136
137        if break_line:
138            # We start a new row
139            row = []
140            # We go all the way left and a little down
141            box.x = pages[page].x
142            box.y = previous.y + previous.h + separation
143
144        # But if we go too far down
145        if box.y + box.h > pages[page].y + pages[page].h:
146            # We go to the next page
147            page += 1
148            # And put the box at the top-left
149            box.x = pages[page].x
150            box.y = pages[page].y
151
152        # Put the box in the row
153        row.append(box)
154
155        # Collapse all left-margin space
156        if all(b.letter == " " for b in row):
157            box.w = 0
158            box.stretchy = False
159            box.x = pages[page].x
160
161        previous = box
162
163
164def draw_boxes(boxes, pages, fname, size, hide_boxes=False):
165    dwg = svgwrite.Drawing(fname, profile="full", size=size)
166    # Draw the pages
167    for page in pages:
168        dwg.add(
169            dwg.rect(
170                insert=(f"{page.x}cm", f"{page.y}cm"),
171                size=(f"{page.w}cm", f"{page.h}cm"),
172                fill="lightblue",
173            )
174        )
175    # Draw all the boxes
176    for box in boxes:
177        # The box color depends on its features
178        color = "green" if box.stretchy else "red"
179        # Make the colored boxes optional
180        if not hide_boxes:
181            dwg.add(
182                dwg.rect(
183                    insert=(f"{box.x}cm", f"{box.y}cm"),
184                    size=(f"{box.w}cm", f"{box.h}cm"),
185                    fill=color,
186                )
187            )
188        # Display the letter in the box
189        if box.letter:
190            dwg.add(
191                dwg.text(
192                    box.letter,
193                    insert=(f"{box.x}cm", f"{box.y + box.h}cm"),
194                    font_size=f"{box.h}cm",
195                    font_family="Arial",
196                )
197            )
198    dwg.save()
199
200
201def create_text_boxes(input_file):
202    p_and_p = open(input_file).read()
203    p_and_p = insert_soft_hyphens(p_and_p)  # Insert invisible hyphens
204    text_boxes = []
205    for letter in p_and_p:
206        text_boxes.append(Box(letter=letter, stretchy=letter == " "))
207    adjust_widths_by_letter(text_boxes)
208    return text_boxes
209
210
211def create_pages():
212    # A few pages all the same size
213    pages = [Box(i * 35, 0, 30, 50) for i in range(10)]
214    return pages
215
216
217def convert(input, output):
218    pages = create_pages()
219    text_boxes = create_text_boxes(input)
220    layout(text_boxes, pages)
221    draw_boxes(text_boxes, pages, output, ("100cm", "50cm"), True)
222
223
224if __name__ == "__main__":
225    arguments = docopt(__doc__, version="Boxes 0.12")
226    convert(input=arguments["<input>"], output=arguments["<output>"])
227