Source for: boxes.py [raw]

  1"""
  2Usage:
  3    boxes <input> <output> [--page-size=<WxH>] [--separation=<sep>]
  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
 59def layout(_boxes, pages, separation):
 60    """Layout boxes along pages.
 61
 62    Keep in mind that this function modifies the boxes themselves, so
 63    you should be very careful about trying to call layout() more than once
 64    on the same boxes.
 65
 66    Specifically, some spaces will become 0-width and not stretchy.
 67    """
 68
 69    # Because we modify the box list, we will work on a copy
 70    boxes = _boxes[:]
 71    # We start at page 0
 72    page = 0
 73    # The 1st box should be placed in the correct page
 74    previous = boxes.pop(0)
 75    previous.x = pages[page].x
 76    previous.y = pages[page].y
 77    row = []
 78    while boxes:
 79        # We take the new 1st box
 80        box = boxes.pop(0)
 81        # And put it next to the other
 82        box.x = previous.x + previous.w + separation
 83        # At the same vertical location
 84        box.y = previous.y
 85
 86        # Handle breaking on newlines
 87        break_line = False
 88        # But if it's a newline
 89        if (box.letter == "\n"):
 90            break_line = True
 91            # Newlines take no horizontal space ever
 92            box.w = 0
 93            box.stretchy = False
 94
 95        # Or if it's too far to the right, and is a
 96        # good place to break the line...
 97        elif (box.x + box.w) > (
 98            pages[page].x + pages[page].w
 99        ) and box.letter in (
100            " ", "\xad"
101        ):
102            if box.letter == "\xad":
103                # Add a visible hyphen in the row
104                h_b = hyphenbox()
105                h_b.x = previous.x + previous.w + separation
106                h_b.y = previous.y
107                _boxes.append(h_b)  # So it's drawn
108                row.append(h_b)  # So it's justified
109            break_line = True
110            # We adjust the row
111            # Remove all right-margin spaces
112            while row[-1].letter == " ":
113                row.pop()
114            slack = (pages[page].x + pages[page].w) - (
115                row[-1].x + row[-1].w
116            )
117            # Get a list of all the ones that are stretchy
118            stretchies = [b for b in row if b.stretchy]
119            if not stretchies:  # Nothing stretches do as before.
120                bump = slack / len(row)
121                # The 1st box gets 0 bumps, the 2nd gets 1 and so on
122                for i, b in enumerate(row):
123                    b.x += bump * i
124            else:
125                bump = slack / len(stretchies)
126                # Each stretchy gets wider
127                for b in stretchies:
128                    b.w += bump
129                # And we put each thing next to the previous one
130                for j, b in enumerate(row[1:], 1):
131                    b.x = row[j - 1].x + row[j - 1].w + separation
132
133        if break_line:
134            # We start a new row
135            row = []
136            # We go all the way left and a little down
137            box.x = pages[page].x
138            box.y = previous.y + previous.h + separation
139
140        # But if we go too far down
141        if box.y + box.h > pages[page].y + pages[page].h:
142            # We go to the next page
143            page += 1
144            # And put the box at the top-left
145            box.x = pages[page].x
146            box.y = pages[page].y
147
148        # Put the box in the row
149        row.append(box)
150
151        # Collapse all left-margin space
152        if all(b.letter == " " for b in row):
153            box.w = 0
154            box.stretchy = False
155            box.x = pages[page].x
156
157        previous = box
158
159    # Remove leftover boxes
160    del (pages[page + 1:])
161
162
163def draw_boxes(boxes, pages, fname, size, hide_boxes=False):
164    dwg = svgwrite.Drawing(fname, profile="full", size=size)
165    # Draw the pages
166    for page in pages:
167        dwg.add(
168            dwg.rect(
169                insert=(f"{page.x}cm", f"{page.y}cm"),
170                size=(f"{page.w}cm", f"{page.h}cm"),
171                fill="lightblue",
172            )
173        )
174    # Draw all the boxes
175    for box in boxes:
176        # The box color depends on its features
177        color = "green" if box.stretchy else "red"
178        # Make the colored boxes optional
179        if not hide_boxes:
180            dwg.add(
181                dwg.rect(
182                    insert=(f"{box.x}cm", f"{box.y}cm"),
183                    size=(f"{box.w}cm", f"{box.h}cm"),
184                    fill=color,
185                )
186            )
187        # Display the letter in the box
188        if box.letter:
189            dwg.add(
190                dwg.text(
191                    box.letter,
192                    insert=(f"{box.x}cm", f"{box.y + box.h}cm"),
193                    font_size=f"{box.h}cm",
194                    font_family="Arial",
195                )
196            )
197    dwg.save()
198
199
200def create_text_boxes(input_file):
201    p_and_p = open(input_file).read()
202    p_and_p = insert_soft_hyphens(p_and_p)  # Insert invisible hyphens
203    text_boxes = []
204    for letter in p_and_p:
205        text_boxes.append(Box(letter=letter, stretchy=letter == " "))
206    adjust_widths_by_letter(text_boxes)
207    return text_boxes
208
209
210def create_pages(page_size):
211    # A few pages all the same size
212    w, h = page_size
213    pages = [Box(i * (w + 5), 0, w, h) for i in range(1000)]
214    return pages
215
216
217def convert(input, output, page_size=(30, 50), separation=0.05):
218    pages = create_pages(page_size)
219    text_boxes = create_text_boxes(input)
220    layout(text_boxes, pages, separation)
221    draw_boxes(
222        text_boxes,
223        pages,
224        output,
225        (f"{pages[-1].w + pages[-1].x}cm", f"{pages[-1].h}cm"),
226        True,
227    )
228
229
230if __name__ == "__main__":
231    arguments = docopt(__doc__, version="Boxes 0.13")
232
233    if arguments["--page-size"]:
234        p_size = [int(x) for x in arguments["--page-size"].split("x")]
235    else:
236        p_size = (30, 50)
237
238    if arguments["--separation"]:
239        separation = float(arguments["--separation"])
240    else:
241        separation = 0.05
242
243    convert(
244        input=arguments["<input>"],
245        output=arguments["<output>"],
246        page_size=p_size,
247        separation=separation,
248    )
249