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