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