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