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
70 # If the line ends in newline, and is underfull, do nothing.
71 if row[-1].letter == "\n" and (row[-1].x + row[-1].w) < (
72 page.x + page.w
73 ):
74 return
75
76 # If the line ends with a soft-hyphen, replace it with a real hyphen
77 elif row[-1].letter == "\xad":
78 # This looks pretty bad, doesn't it?
79 hyphen = hyphenbox()
80 row[-1].letter = hyphen.letter
81 row[-1].w = hyphen.w
82
83 slack = (page.x + page.w) - (row[-1].x + row[-1].w)
84 # Get a list of all the ones that are stretchy
85 stretchies = [b for b in row if b.stretchy]
86 if not stretchies:
87 # Nothing stretches, spread slack on everything
88 stretchies = row
89 bump = (slack / len(stretchies))
90 # Each stretchy gets wider
91 for b in stretchies:
92 b.w += bump
93 # And we put each thing next to the previous one
94 for j, b in enumerate(row[1:], 1):
95 b.x = row[j - 1].x + row[j - 1].w + separation
96
97
98BREAKING_CHARS = (" ", "\xad")
99
100
101def is_breaking(box, page):
102 """Decide if 'box' is a good candidate to be the end of a row
103 in the page."""
104 # If it's a newline
105 if box.letter == "\n":
106 return True
107
108 # If we are too much to the right
109 if (box.x + box.w) > (page.x + page.w):
110 # And it's a breaking character:
111 if box.letter in BREAKING_CHARS:
112 return True
113
114 return False
115
116
117def fill_row(boxes, page, separation):
118 """Fill a row with elements removed from boxes.
119
120 The elements put in the row should be a good fit for laying out on
121 page considering separation.
122 """
123
124 row = []
125 x = page.x
126 while boxes:
127 b = boxes.pop(0)
128 row.append(b)
129 b.x = x
130 if is_breaking(b, page):
131 break
132
133 x = x + b.w + separation
134 return row
135
136
137def layout(_boxes, _pages, separation):
138 """Layout boxes along pages."""
139
140 # We modify these lists, so use copies
141 boxes = _boxes[:]
142 pages = _pages[:]
143
144 # Start in page 0
145 current_page = pages.pop(0)
146 y = current_page.y
147
148 # If we run out of boxes or pages, stop
149 while boxes and pages:
150 # If this row would end below the page, advance to next page
151 if (y + boxes[0].h) > (current_page.y + current_page.h):
152 current_page = pages.pop(0)
153 y = current_page.y
154 # Put "enough" letters into row and out of boxes
155 row = fill_row(boxes, current_page, separation)
156 # Adjust box positions to fill the page width
157 justify_row(row, current_page, separation)
158 # Put all the letters in the right vertical position
159 y = y + row[0].h + separation
160 for b in row:
161 b.y = y
162 # Remove unused pages
163 del (_pages[-len(pages):])
164
165
166def draw_boxes(boxes, pages, fname, size, hide_boxes=False):
167 dwg = svgwrite.Drawing(fname, profile="full", size=size)
168 # Draw the pages
169 for page in pages:
170 dwg.add(
171 dwg.rect(
172 insert=(f"{page.x}cm", f"{page.y}cm"),
173 size=(f"{page.w}cm", f"{page.h}cm"),
174 fill="lightblue",
175 )
176 )
177 # Draw all the boxes
178 for box in boxes:
179 # The box color depends on its features
180 color = "green" if box.stretchy else "red"
181 # Make the colored boxes optional
182 if not hide_boxes:
183 dwg.add(
184 dwg.rect(
185 insert=(f"{box.x}cm", f"{box.y}cm"),
186 size=(f"{box.w}cm", f"{box.h}cm"),
187 fill=color,
188 )
189 )
190 # Display the letter in the box
191 if box.letter:
192 dwg.add(
193 dwg.text(
194 box.letter,
195 insert=(f"{box.x}cm", f"{box.y + box.h}cm"),
196 font_size=f"{box.h}cm",
197 font_family="Arial",
198 )
199 )
200 dwg.save()
201
202
203def create_text_boxes(input_file):
204 p_and_p = open(input_file).read()
205 p_and_p = insert_soft_hyphens(p_and_p) # Insert invisible hyphens
206 if p_and_p[-1] != "\n":
207 p_and_p += "\n"
208 text_boxes = []
209 for letter in p_and_p:
210 text_boxes.append(Box(letter=letter, stretchy=letter == " "))
211 adjust_widths_by_letter(text_boxes)
212 return text_boxes
213
214
215def create_pages(page_size):
216 # A few pages all the same size
217 w, h = page_size
218 pages = [Box(i * (w + 5), 0, w, h) for i in range(1000)]
219 return pages
220
221
222def convert(input, output, page_size=(30, 50), separation=0.05):
223 pages = create_pages(page_size)
224 text_boxes = create_text_boxes(input)
225 layout(text_boxes, pages, separation)
226 draw_boxes(
227 text_boxes,
228 pages,
229 output,
230 (f"{pages[-1].w + pages[-1].x}cm", f"{pages[-1].h}cm"),
231 True,
232 )
233
234
235if __name__ == "__main__":
236 arguments = docopt(__doc__, version="Boxes 0.13")
237
238 if arguments["--page-size"]:
239 p_size = [int(x) for x in arguments["--page-size"].split("x")]
240 else:
241 p_size = (30, 50)
242
243 if arguments["--separation"]:
244 separation = float(arguments["--separation"])
245 else:
246 separation = 0.05
247
248 convert(
249 input=arguments["<input>"],
250 output=arguments["<output>"],
251 page_size=p_size,
252 separation=separation,
253 )
254