❭ code/lesson8/boxes.py
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071 72737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249 | """ Usage: boxes <input> <output> [--page-size=<WxH>] [--separation=<sep>] 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[0].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 def justify_row(row, page, separation): """Given a row and a page, adjust position of elements in the row so it fits the page width. It modifies the contents of the row in place, so returns nothing. """ # Remove all right-margin spaces while row[-1].letter == " ": row.pop() # If the line ends in newline, do nothing. if row[-1].letter == "\n": return # If the line ends with a soft-hyphen, replace it with a real hyphen elif row[-1].letter == "\xad": # This looks pretty bad, doesn't it? hyphen = hyphenbox() row[-1].letter = hyphen.letter row[-1].w = hyphen.w slack = (page.x + 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, spread slack on everything stretchies = row 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 BREAKING_CHARS = (" ", "\xad") def is_breaking(box, page): """Decide if 'box' is a good candidate to be the end of a row in the page.""" # If it's a newline if box.letter == "\n": return True # If we are too much to the right if (box.x + box.w) > (page.x + page.w): # And it's a breaking character: if box.letter in BREAKING_CHARS: return True return False def fill_row(boxes, page, separation): """Fill a row with elements removed from boxes. The elements put in the row should be a good fit for laying out on page considering separation. """ row = [] x = page.x while boxes: b = boxes.pop(0) row.append(b) b.x = x if is_breaking(b, page): break x = x + b.w + separation return row def layout(_boxes, _pages, separation): """Layout boxes along pages.""" # We modify these lists, so use copies boxes = _boxes[:] pages = _pages[:] # Start in page 0 current_page = pages.pop(0) y = current_page.y # If we run out of boxes or pages, stop while boxes and pages: # If this row would end below the page, advance to next page if (y + boxes[0].h) > (current_page.y + current_page.h): current_page = pages.pop(0) y = current_page.y # Put "enough" letters into row and out of boxes row = fill_row(boxes, current_page, separation) # Adjust box positions to fill the page width justify_row(row, current_page, separation) # Put all the letters in the right vertical position y = y + row[0].h + separation for b in row: b.y = y # Remove unused pages del (_pages[-len(pages):]) 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(page_size): # A few pages all the same size w, h = page_size pages = [Box(i * (w + 5), 0, w, h) for i in range(1000)] return pages def convert(input, output, page_size=(30, 50), separation=0.05): pages = create_pages(page_size) text_boxes = create_text_boxes(input) layout(text_boxes, pages, separation) draw_boxes( text_boxes, pages, output, (f"{pages[-1].w + pages[-1].x}cm", f"{pages[-1].h}cm"), True, ) if __name__ == "__main__": arguments = docopt(__doc__, version="Boxes 0.13") if arguments["--page-size"]: p_size = [int(x) for x in arguments["--page-size"].split("x")] else: p_size = (30, 50) if arguments["--separation"]: separation = float(arguments["--separation"]) else: separation = 0.05 convert( input=arguments["<input>"], output=arguments["<output>"], page_size=p_size, separation=separation, ) |
❭ code/lesson9.1/boxes.py
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 | """ Usage: boxes <input> <output> [--page-size=<WxH>] [--separation=<sep>] 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[0].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 def justify_row(row, page, separation): """Given a row and a page, adjust position of elements in the row so it fits the page width. It modifies the contents of the row in place, so returns nothing. """ # Remove all right-margin spaces while row[-1].letter == " ": row.pop() # If the line ends in newline, and is underfull, do nothing. if row[-1].letter == "\n" and (row[-1].x + row[-1].w) < ( page.x + page.w ): return # If the line ends with a soft-hyphen, replace it with a real hyphen elif row[-1].letter == "\xad": # This looks pretty bad, doesn't it? hyphen = hyphenbox() row[-1].letter = hyphen.letter row[-1].w = hyphen.w slack = (page.x + 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, spread slack on everything stretchies = row 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 BREAKING_CHARS = (" ", "\xad") def is_breaking(box, page): """Decide if 'box' is a good candidate to be the end of a row in the page.""" # If it's a newline if box.letter == "\n": return True # If we are too much to the right if (box.x + box.w) > (page.x + page.w): # And it's a breaking character: if box.letter in BREAKING_CHARS: return True return False def fill_row(boxes, page, separation): """Fill a row with elements removed from boxes. The elements put in the row should be a good fit for laying out on page considering separation. """ row = [] x = page.x while boxes: b = boxes.pop(0) row.append(b) b.x = x if is_breaking(b, page): break x = x + b.w + separation return row def layout(_boxes, _pages, separation): """Layout boxes along pages.""" # We modify these lists, so use copies boxes = _boxes[:] pages = _pages[:] # Start in page 0 current_page = pages.pop(0) y = current_page.y # If we run out of boxes or pages, stop while boxes and pages: # If this row would end below the page, advance to next page if (y + boxes[0].h) > (current_page.y + current_page.h): current_page = pages.pop(0) y = current_page.y # Put "enough" letters into row and out of boxes row = fill_row(boxes, current_page, separation) # Adjust box positions to fill the page width justify_row(row, current_page, separation) # Put all the letters in the right vertical position y = y + row[0].h + separation for b in row: b.y = y # Remove unused pages del (_pages[-len(pages):]) 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 if p_and_p[-1] != "\n": p_and_p += "\n" 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(page_size): # A few pages all the same size w, h = page_size pages = [Box(i * (w + 5), 0, w, h) for i in range(1000)] return pages def convert(input, output, page_size=(30, 50), separation=0.05): pages = create_pages(page_size) text_boxes = create_text_boxes(input) layout(text_boxes, pages, separation) draw_boxes( text_boxes, pages, output, (f"{pages[-1].w + pages[-1].x}cm", f"{pages[-1].h}cm"), True, ) if __name__ == "__main__": arguments = docopt(__doc__, version="Boxes 0.13") if arguments["--page-size"]: p_size = [int(x) for x in arguments["--page-size"].split("x")] else: p_size = (30, 50) if arguments["--separation"]: separation = float(arguments["--separation"]) else: separation = 0.05 convert( input=arguments["<input>"], output=arguments["<output>"], page_size=p_size, separation=separation, ) |