""" Usage: boxes [--page-size=] [--separation=] 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 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[""], output=arguments[""], page_size=p_size, separation=separation, )