❭ ../part1/code/lesson11.py
 ` 12 3456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 ` ``` from fonts import adjust_widths_by_letter from hyphen import insert_soft_hyphens class Box(): def __init__(self, x=0, y=0, w=1, h=1, stretchy=False, letter="x"): """Accept arguments to define our box, and store them.""" self.x = x self.y = y self.w = w self.h = h self.stretchy = stretchy self.letter = letter def __repr__(self): return 'Box(%s, %s, %s, %s, "%s")' % ( self.x, self.y, self.w, self.h, self.letter ) # Three runs of "a" with spaces between them. # The ideal breaking point is the second space. text_boxes = [Box(letter="a") for a in range(72)] for i in (20, 50): text_boxes[i].letter = " " text_boxes[i].stretchy = True adjust_widths_by_letter(text_boxes) # A few pages all the same size pages = [Box(i * 35, 0, 30, 50) for i in range(10)] def hyphenbox(): b = Box(letter="-") adjust_widths_by_letter([b]) return b def badness(page_width, row): """Calculate how 'bad' a position to break is. bigger is worse. """ # Yes, this is suboptimal. It's easier to optimize working code # than fixing fast code. row_width = (row[-1].x + row[-1].w) - row.x slack = page_width - row_width stretchies = [b for b in row if b.stretchy] if len(stretchies) > 0: stretchies_width = sum(s.w for s in stretchies) # More stetchy space is good. More slack is bad. badness = slack / stretchies_width else: # Nothing to stretch. Not good. badness = 1000 if slack < 0: # Arbitrary fudge factor, negative slack is THIS much worse badness *= -2 return badness # We add a "separation" constant so you can see the boxes individually separation = .05 def layout(_boxes): """Layout boxes along pages. Keep in mind that this function modifies the boxes themselves, so you should be very careful about trying to call layout() more than once on the same boxes. Specifically, some spaces will become 0-width and not stretchy. """ # Because we modify the box list, we will work on a copy boxes = _boxes[:] # We start at page 0 page = 0 # The 1st box should be placed in the correct page previous = boxes.pop(0) previous.x = pages[page].x previous.y = pages[page].y row = [] while boxes: # We take the new 1st box box = boxes.pop(0) # And put it next to the other box.x = previous.x + previous.w + separation # At the same vertical location box.y = previous.y # Handle breaking on newlines break_line = False # But if it's a newline if (box.letter == "\n"): break_line = True # Newlines take no horizontal space ever box.w = 0 box.stretchy = False # Or if it's too far to the right, and is a # good place to break the line... elif (box.x + box.w) > ( pages[page].x + pages[page].w ) and box.letter in ( " ", "\xad" ): if box.letter == "\xad": # Add a visible hyphen in the row h_b = hyphenbox() h_b.x = previous.x + previous.w + separation h_b.y = previous.y _boxes.append(h_b) # So it's drawn row.append(h_b) # So it's justified break_line = True # We adjust the row # Remove all right-margin spaces while row[-1].letter == " ": row.pop() slack = (pages[page].x + pages[page].w) - ( row[-1].x + row[-1].w ) # Get a list of all the ones that are stretchy stretchies = [b for b in row if b.stretchy] if not stretchies: # Nothing stretches do as before. bump = slack / len(row) # The 1st box gets 0 bumps, the 2nd gets 1 and so on for i, b in enumerate(row): b.x += bump * i else: bump = slack / len(stretchies) # Each stretchy gets wider for b in stretchies: b.w += bump # And we put each thing next to the previous one for j, b in enumerate(row[1:], 1): b.x = row[j - 1].x + row[j - 1].w + separation if break_line: # We start a new row row = [] # We go all the way left and a little down box.x = pages[page].x box.y = previous.y + previous.h + separation # But if we go too far down if box.y + box.h > pages[page].y + pages[page].h: # We go to the next page page += 1 # And put the box at the top-left box.x = pages[page].x box.y = pages[page].y # Put the box in the row row.append(box) # Collapse all left-margin space if all(b.letter == " " for b in row): box.w = 0 box.stretchy = False box.x = pages[page].x previous = box layout(text_boxes) import svgwrite def draw_boxes(boxes, fname, size, hide_boxes=False): dwg = svgwrite.Drawing(fname, profile="full", size=size) # Draw the pages for page in pages: dwg.add( dwg.rect( insert=(f"{page.x}cm", f"{page.y}cm"), size=(f"{page.w}cm", f"{page.h}cm"), fill="lightblue", ) ) # Draw all the boxes for box in boxes: # The box color depends on its features color = "green" if box.stretchy else "red" # Make the colored boxes optional if not hide_boxes: dwg.add( dwg.rect( insert=(f"{box.x}cm", f"{box.y}cm"), size=(f"{box.w}cm", f"{box.h}cm"), fill=color, ) ) # Display the letter in the box if box.letter: dwg.add( dwg.text( box.letter, insert=(f"{box.x}cm", f"{box.y + box.h}cm"), font_size=f"{box.h}cm", font_family="Arial", ) ) dwg.save() draw_boxes(text_boxes, "lesson11.svg", ("33cm", "5cm"), hide_boxes=True) ```
❭ code/lesson1/boxes.py
 `123456789101112131415161718192021222324252627282930 313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226` ```""" Usage: boxes boxes --version """ from fonts import adjust_widths_by_letter from hyphen import insert_soft_hyphens import svgwrite from docopt import docopt class Box(): def __init__(self, x=0, y=0, w=1, h=1, stretchy=False, letter="x"): """Accept arguments to define our box, and store them.""" self.x = x self.y = y self.w = w self.h = h self.stretchy = stretchy self.letter = letter def __repr__(self): return 'Box(%s, %s, %s, %s, "%s")' % ( self.x, self.y, self.w, self.h, self.letter ) def hyphenbox(): b = Box(letter="-") adjust_widths_by_letter([b]) return b def badness(page_width, row): """Calculate how 'bad' a position to break is. bigger is worse. """ # Yes, this is suboptimal. It's easier to optimize working code # than fixing fast code. row_width = (row[-1].x + row[-1].w) - row.x slack = page_width - row_width stretchies = [b for b in row if b.stretchy] if len(stretchies) > 0: stretchies_width = sum(s.w for s in stretchies) # More stetchy space is good. More slack is bad. badness = slack / stretchies_width else: # Nothing to stretch. Not good. badness = 1000 if slack < 0: # Arbitrary fudge factor, negative slack is THIS much worse badness *= -2 return badness # We add a "separation" constant so you can see the boxes individually separation = .05 def layout(_boxes, pages): """Layout boxes along pages. Keep in mind that this function modifies the boxes themselves, so you should be very careful about trying to call layout() more than once on the same boxes. Specifically, some spaces will become 0-width and not stretchy. """ # Because we modify the box list, we will work on a copy boxes = _boxes[:] # We start at page 0 page = 0 # The 1st box should be placed in the correct page previous = boxes.pop(0) previous.x = pages[page].x previous.y = pages[page].y row = [] while boxes: # We take the new 1st box box = boxes.pop(0) # And put it next to the other box.x = previous.x + previous.w + separation # At the same vertical location box.y = previous.y # Handle breaking on newlines break_line = False # But if it's a newline if (box.letter == "\n"): break_line = True # Newlines take no horizontal space ever box.w = 0 box.stretchy = False # Or if it's too far to the right, and is a # good place to break the line... elif (box.x + box.w) > ( pages[page].x + pages[page].w ) and box.letter in ( " ", "\xad" ): if box.letter == "\xad": # Add a visible hyphen in the row h_b = hyphenbox() h_b.x = previous.x + previous.w + separation h_b.y = previous.y _boxes.append(h_b) # So it's drawn row.append(h_b) # So it's justified break_line = True # We adjust the row # Remove all right-margin spaces while row[-1].letter == " ": row.pop() slack = (pages[page].x + pages[page].w) - ( row[-1].x + row[-1].w ) # Get a list of all the ones that are stretchy stretchies = [b for b in row if b.stretchy] if not stretchies: # Nothing stretches do as before. bump = slack / len(row) # The 1st box gets 0 bumps, the 2nd gets 1 and so on for i, b in enumerate(row): b.x += bump * i else: bump = slack / len(stretchies) # Each stretchy gets wider for b in stretchies: b.w += bump # And we put each thing next to the previous one for j, b in enumerate(row[1:], 1): b.x = row[j - 1].x + row[j - 1].w + separation if break_line: # We start a new row row = [] # We go all the way left and a little down box.x = pages[page].x box.y = previous.y + previous.h + separation # But if we go too far down if box.y + box.h > pages[page].y + pages[page].h: # We go to the next page page += 1 # And put the box at the top-left box.x = pages[page].x box.y = pages[page].y # Put the box in the row row.append(box) # Collapse all left-margin space if all(b.letter == " " for b in row): box.w = 0 box.stretchy = False box.x = pages[page].x previous = box def draw_boxes(boxes, pages, fname, size, hide_boxes=False): dwg = svgwrite.Drawing(fname, profile="full", size=size) # Draw the pages for page in pages: dwg.add( dwg.rect( insert=(f"{page.x}cm", f"{page.y}cm"), size=(f"{page.w}cm", f"{page.h}cm"), fill="lightblue", ) ) # Draw all the boxes for box in boxes: # The box color depends on its features color = "green" if box.stretchy else "red" # Make the colored boxes optional if not hide_boxes: dwg.add( dwg.rect( insert=(f"{box.x}cm", f"{box.y}cm"), size=(f"{box.w}cm", f"{box.h}cm"), fill=color, ) ) # Display the letter in the box if box.letter: dwg.add( dwg.text( box.letter, insert=(f"{box.x}cm", f"{box.y + box.h}cm"), font_size=f"{box.h}cm", font_family="Arial", ) ) dwg.save() def create_text_boxes(input_file): p_and_p = open(input_file).read() p_and_p = insert_soft_hyphens(p_and_p) # Insert invisible hyphens text_boxes = [] for letter in p_and_p: text_boxes.append(Box(letter=letter, stretchy=letter == " ")) adjust_widths_by_letter(text_boxes) return text_boxes def create_pages(): # A few pages all the same size pages = [Box(i * 35, 0, 30, 50) for i in range(10)] return pages def convert(input, output): pages = create_pages() text_boxes = create_text_boxes(input) layout(text_boxes, pages) draw_boxes(text_boxes, pages, output, ("100cm", "50cm"), True) if __name__ == "__main__": arguments = docopt(__doc__, version="Boxes 0.12") convert(input=arguments[""], output=arguments[""]) ```