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