def to_textmap( self, layout: bool = False, layout_width: T_num = 0, layout_height: T_num = 0, layout_width_chars: int = 0, layout_height_chars: int = 0, layout_bbox: T_bbox = (0, 0, 0, 0), x_density: T_num = DEFAULT_X_DENSITY, y_density: T_num = DEFAULT_Y_DENSITY, x_shift: T_num = 0, y_shift: T_num = 0, y_tolerance: T_num = DEFAULT_Y_TOLERANCE, line_dir: T_dir = DEFAULT_LINE_DIR, char_dir: T_dir = DEFAULT_CHAR_DIR, line_dir_rotated: Optional[T_dir] = None, char_dir_rotated: Optional[T_dir] = None, char_dir_render: Optional[T_dir] = None, line_dir_render: Optional[T_dir] = None, use_text_flow: bool = False, presorted: bool = False, expand_ligatures: bool = True, ) -> TextMap: """ Given a list of (word, chars) tuples (i.e., a WordMap), return a list of (char-text, char) tuples (i.e., a TextMap) that can be used to mimic the structural layout of the text on the page(s), using the following approach for top-to-bottom, left-to-right text: - Sort the words by (top, x0) if not already sorted. - Cluster the words by top (taking `y_tolerance` into account), and iterate through them. - For each cluster, divide (top - y_shift) by `y_density` to calculate the minimum number of newlines that should come before this cluster. Append that number of newlines *minus* the number of newlines already appended, with a minimum of one. - Then for each cluster, iterate through each word in it. Divide each word's x0, minus `x_shift`, by `x_density` to calculate the minimum number of characters that should come before this cluster. Append that number of spaces *minus* the number of characters and spaces already appended, with a minimum of one. Then append the word's text. - At the termination of each line, add more spaces if necessary to mimic `layout_width`. - Finally, add newlines to the end if necessary to mimic to `layout_height`. For other line/character directions (e.g., bottom-to-top, right-to-left), these steps are adjusted. """ _textmap: List[Tuple[str, Optional[T_obj]]] = [] if not len(self.tuples): return TextMap( _textmap, line_dir_render=line_dir_render or line_dir, char_dir_render=char_dir_render or char_dir, ) expansions = LIGATURES if expand_ligatures else {} if layout: if layout_width_chars: if layout_width: raise ValueError( "`layout_width` and `layout_width_chars` cannot both be set." ) else: layout_width_chars = int(round(layout_width / x_density)) if layout_height_chars: if layout_height: raise ValueError( "`layout_height` and `layout_height_chars` cannot both be set." ) else: layout_height_chars = int(round(layout_height / y_density)) blank_line = [(" ", None)] * layout_width_chars else: blank_line = [] num_newlines = 0 line_cluster_key = get_line_cluster_key(line_dir) char_sort_key = get_char_sort_key(char_dir) line_position_key = POSITION_KEYS[line_dir] char_position_key = POSITION_KEYS[char_dir] y_origin = BBOX_ORIGIN_KEYS[line_dir](layout_bbox) x_origin = BBOX_ORIGIN_KEYS[char_dir](layout_bbox) words_sorted_line_dir = ( self.tuples if presorted or use_text_flow else sorted(self.tuples, key=lambda x: line_cluster_key(x[0])) ) tuples_by_line = cluster_objects( words_sorted_line_dir, lambda x: line_cluster_key(x[0]), y_tolerance, preserve_order=presorted or use_text_flow, ) for i, line_tuples in enumerate(tuples_by_line): if layout: line_position = line_position_key(line_tuples[0][0]) y_dist_raw = line_position - (y_origin + y_shift) adj = -1 if line_dir in ["btt", "rtl"] else 1 y_dist = y_dist_raw * adj / y_density else: y_dist = 0 num_newlines_prepend = max( # At least one newline, unless this iis the first line int(i > 0), # ... or as many as needed to get the imputed "distance" from the top round(y_dist) - num_newlines, ) for i in range(num_newlines_prepend): if not len(_textmap) or _textmap[-1][0] == "\n": _textmap += blank_line _textmap.append(("\n", None)) num_newlines += num_newlines_prepend line_len = 0 line_tuples_sorted = ( line_tuples if presorted or use_text_flow else sorted(line_tuples, key=lambda x: char_sort_key(x[0])) ) for word, chars in line_tuples_sorted: if layout: char_position = char_position_key(word) x_dist_raw = char_position - (x_origin + x_shift) adj = -1 if char_dir in ["btt", "rtl"] else 1 x_dist = x_dist_raw * adj / x_density else: x_dist = 0 num_spaces_prepend = max(min(1, line_len), round(x_dist) - line_len) _textmap += [(" ", None)] * num_spaces_prepend line_len += num_spaces_prepend for c in chars: letters = expansions.get(c["text"], c["text"]) for letter in letters: _textmap.append((letter, c)) line_len += 1 # Append spaces at end of line if layout: _textmap += [(" ", None)] * (layout_width_chars - line_len) # Append blank lines at end of text if layout: num_newlines_append = layout_height_chars - (num_newlines + 1) for i in range(num_newlines_append): if i > 0: _textmap += blank_line _textmap.append(("\n", None)) # Remove terminal newline if _textmap[-1] == ("\n", None): _textmap = _textmap[:-1] return TextMap( _textmap, line_dir_render=line_dir_render or line_dir, char_dir_render=char_dir_render or char_dir, )