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