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