Source for: boxes.py [raw]
1"""
2Usage:
3 boxes <input> <output>
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
59# We add a "separation" constant so you can see the boxes individually
60separation = .05
61
62
63def layout(_boxes, pages):
64 """Layout boxes along pages.
65
66 Keep in mind that this function modifies the boxes themselves, so
67 you should be very careful about trying to call layout() more than once
68 on the same boxes.
69
70 Specifically, some spaces will become 0-width and not stretchy.
71 """
72
73 # Because we modify the box list, we will work on a copy
74 boxes = _boxes[:]
75 # We start at page 0
76 page = 0
77 # The 1st box should be placed in the correct page
78 previous = boxes.pop(0)
79 previous.x = pages[page].x
80 previous.y = pages[page].y
81 row = []
82 while boxes:
83 # We take the new 1st box
84 box = boxes.pop(0)
85 # And put it next to the other
86 box.x = previous.x + previous.w + separation
87 # At the same vertical location
88 box.y = previous.y
89
90 # Handle breaking on newlines
91 break_line = False
92 # But if it's a newline
93 if (box.letter == "\n"):
94 break_line = True
95 # Newlines take no horizontal space ever
96 box.w = 0
97 box.stretchy = False
98
99 # Or if it's too far to the right, and is a
100 # good place to break the line...
101 elif (box.x + box.w) > (
102 pages[page].x + pages[page].w
103 ) and box.letter in (
104 " ", "\xad"
105 ):
106 if box.letter == "\xad":
107 # Add a visible hyphen in the row
108 h_b = hyphenbox()
109 h_b.x = previous.x + previous.w + separation
110 h_b.y = previous.y
111 _boxes.append(h_b) # So it's drawn
112 row.append(h_b) # So it's justified
113 break_line = True
114 # We adjust the row
115 # Remove all right-margin spaces
116 while row[-1].letter == " ":
117 row.pop()
118 slack = (pages[page].x + pages[page].w) - (
119 row[-1].x + row[-1].w
120 )
121 # Get a list of all the ones that are stretchy
122 stretchies = [b for b in row if b.stretchy]
123 if not stretchies: # Nothing stretches do as before.
124 bump = slack / len(row)
125 # The 1st box gets 0 bumps, the 2nd gets 1 and so on
126 for i, b in enumerate(row):
127 b.x += bump * i
128 else:
129 bump = slack / len(stretchies)
130 # Each stretchy gets wider
131 for b in stretchies:
132 b.w += bump
133 # And we put each thing next to the previous one
134 for j, b in enumerate(row[1:], 1):
135 b.x = row[j - 1].x + row[j - 1].w + separation
136
137 if break_line:
138 # We start a new row
139 row = []
140 # We go all the way left and a little down
141 box.x = pages[page].x
142 box.y = previous.y + previous.h + separation
143
144 # But if we go too far down
145 if box.y + box.h > pages[page].y + pages[page].h:
146 # We go to the next page
147 page += 1
148 # And put the box at the top-left
149 box.x = pages[page].x
150 box.y = pages[page].y
151
152 # Put the box in the row
153 row.append(box)
154
155 # Collapse all left-margin space
156 if all(b.letter == " " for b in row):
157 box.w = 0
158 box.stretchy = False
159 box.x = pages[page].x
160
161 previous = box
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 text_boxes = []
205 for letter in p_and_p:
206 text_boxes.append(Box(letter=letter, stretchy=letter == " "))
207 adjust_widths_by_letter(text_boxes)
208 return text_boxes
209
210
211def create_pages():
212 # A few pages all the same size
213 pages = [Box(i * 35, 0, 30, 50) for i in range(10)]
214 return pages
215
216
217def convert(input, output):
218 pages = create_pages()
219 text_boxes = create_text_boxes(input)
220 layout(text_boxes, pages)
221 draw_boxes(text_boxes, pages, output, ("100cm", "50cm"), True)
222
223
224if __name__ == "__main__":
225 arguments = docopt(__doc__, version="Boxes 0.12")
226 convert(input=arguments["<input>"], output=arguments["<output>"])
227