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