# Core FreeType-based text rendering # Ported from manga_translator/rendering/text_render.py import os import re import cv2 import numpy as np import freetype import functools from pathlib import Path from typing import Tuple, Optional, List from ..utils import BASE_PATH, is_punctuation, get_logger logger = get_logger("text_render") try: from hyphen import Hyphenator from hyphen.dictools import LANGUAGES as HYPHENATOR_LANGUAGES from langcodes import standardize_tag HAS_HYPHEN = True try: HYPHENATOR_LANGUAGES.remove('fr') HYPHENATOR_LANGUAGES.append('fr_FR') except Exception: pass except ImportError: HAS_HYPHEN = False # --------------------------------------------------------------------------- # CJK horizontal-to-vertical character mapping # --------------------------------------------------------------------------- CJK_H2V = { "‥": "︰", "—": "︱", "―": "|", "–": "︲", "_": "︳", "(": "︵", ")": "︶", "(": "︵", ")": "︶", "{": "︷", "}": "︸", "〔": "︹", "〕": "︺", "【": "︻", "】": "︼", "《": "︽", "》": "︾", "〈": "︿", "〉": "﹀", "「": "﹁", "」": "﹂", "『": "﹃", "』": "﹄", "[": "﹇", "]": "﹈", "…": "⋮", "⋯": "︙", "\u201c": "﹁", "\u201d": "﹂", # " " "\u2018": "﹁", "\u2019": "﹂", # ' ' "~": "︴", "〜": "︴", "~": "︴", "!": "︕", "?": "︖", ".": "︒", "。": "︒", ";": "︔", ";": "︔", ":": "︓", ":": "︓", ",": "︐", ",": "︐", "-": "︲", "−": "︲", "・": "·", } CJK_V2H = {v: k for k, v in CJK_H2V.items()} def CJK_Compatibility_Forms_translate(cdpt: str, direction: int): if cdpt == 'ー' and direction == 1: return 'ー', 90 if cdpt in CJK_V2H and direction == 0: return CJK_V2H[cdpt], 0 if cdpt in CJK_H2V and direction == 1: return CJK_H2V[cdpt], 0 return cdpt, 0 def compact_special_symbols(text: str) -> str: text = text.replace('...', '…').replace('..', '…') text = re.sub(r'([^\w\s])[ \u3000]+', r'\1', text) return text # --------------------------------------------------------------------------- # Font management # --------------------------------------------------------------------------- FALLBACK_FONTS = [ os.path.join(BASE_PATH, 'fonts/Arial-Unicode-Regular.ttf'), # System fonts (filtered by os.path.isfile in set_font) 'C:/Windows/Fonts/arial.ttf', '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', os.path.join(BASE_PATH, 'fonts/msyh.ttc'), os.path.join(BASE_PATH, 'fonts/msgothic.ttc'), ] FONT_SELECTION: List[freetype.Face] = [] _font_cache = {} def _get_cached_font(path: str) -> freetype.Face: path = path.replace('\\', '/') if path not in _font_cache: _font_cache[path] = freetype.Face(Path(path).open('rb')) return _font_cache[path] def set_font(font_path: str): global FONT_SELECTION selection = ([font_path] + FALLBACK_FONTS) if font_path else FALLBACK_FONTS FONT_SELECTION = [_get_cached_font(p) for p in selection if os.path.isfile(p)] class _NS: """Minimal namespace for glyph caching.""" pass class Glyph: def __init__(self, glyph): self.bitmap = _NS() self.bitmap.buffer = glyph.bitmap.buffer self.bitmap.rows = glyph.bitmap.rows self.bitmap.width = glyph.bitmap.width self.advance = _NS() self.advance.x = glyph.advance.x self.advance.y = glyph.advance.y self.bitmap_left = glyph.bitmap_left self.bitmap_top = glyph.bitmap_top self.metrics = _NS() self.metrics.vertBearingX = glyph.metrics.vertBearingX self.metrics.vertBearingY = glyph.metrics.vertBearingY self.metrics.horiBearingX = glyph.metrics.horiBearingX self.metrics.horiBearingY = glyph.metrics.horiBearingY self.metrics.horiAdvance = glyph.metrics.horiAdvance self.metrics.vertAdvance = glyph.metrics.vertAdvance @functools.lru_cache(maxsize=1024, typed=True) def get_char_glyph(cdpt: str, font_size: int, direction: int) -> Glyph: for i, face in enumerate(FONT_SELECTION): if face.get_char_index(cdpt) == 0 and i != len(FONT_SELECTION) - 1: continue if direction == 0: face.set_pixel_sizes(0, font_size) else: face.set_pixel_sizes(font_size, 0) face.load_char(cdpt) return Glyph(face.glyph) def get_char_border(cdpt: str, font_size: int, direction: int): for i, face in enumerate(FONT_SELECTION): if face.get_char_index(cdpt) == 0 and i != len(FONT_SELECTION) - 1: continue if direction == 0: face.set_pixel_sizes(0, font_size) else: face.set_pixel_sizes(font_size, 0) face.load_char(cdpt, freetype.FT_LOAD_DEFAULT | freetype.FT_LOAD_NO_BITMAP) return face.glyph.get_glyph() # --------------------------------------------------------------------------- # Colorization # --------------------------------------------------------------------------- def add_color(bw_char_map, color, stroke_char_map, stroke_color): if bw_char_map.size == 0: return np.zeros((bw_char_map.shape[0], bw_char_map.shape[1], 4), dtype=np.uint8) if stroke_color is None: x, y, w, h = cv2.boundingRect(bw_char_map) else: x, y, w, h = cv2.boundingRect(stroke_char_map) fg = np.zeros((h, w, 4), dtype=np.uint8) fg[:, :, 0] = color[0] fg[:, :, 1] = color[1] fg[:, :, 2] = color[2] fg[:, :, 3] = bw_char_map[y:y + h, x:x + w] if stroke_color is None: stroke_color = color bg = np.zeros((stroke_char_map.shape[0], stroke_char_map.shape[1], 4), dtype=np.uint8) bg[:, :, 0] = stroke_color[0] bg[:, :, 1] = stroke_color[1] bg[:, :, 2] = stroke_color[2] bg[:, :, 3] = stroke_char_map fg_alpha = fg[:, :, 3:4] / 255.0 bg_alpha = 1.0 - fg_alpha bg[y:y + h, x:x + w] = (fg_alpha * fg + bg_alpha * bg[y:y + h, x:x + w]).astype(np.uint8) return bg # --------------------------------------------------------------------------- # Character offset helpers # --------------------------------------------------------------------------- def get_char_offset_x(font_size: int, cdpt: str): c, _ = CJK_Compatibility_Forms_translate(cdpt, 0) glyph = get_char_glyph(c, font_size, 0) bm = glyph.bitmap if bm.rows * bm.width == 0 or len(bm.buffer) != bm.rows * bm.width: return glyph.advance.x >> 6 return glyph.metrics.horiAdvance >> 6 def get_string_width(font_size: int, text: str): return sum(get_char_offset_x(font_size, c) for c in text) # --------------------------------------------------------------------------- # Horizontal character rendering # --------------------------------------------------------------------------- def put_char_horizontal(font_size: int, cdpt: str, pen_l, canvas_text, canvas_border, border_size: int): pen = list(pen_l) cdpt, _ = CJK_Compatibility_Forms_translate(cdpt, 0) slot = get_char_glyph(cdpt, font_size, 0) bitmap = slot.bitmap if slot.metrics.horiAdvance: char_offset_x = slot.metrics.horiAdvance >> 6 elif slot.advance.x: char_offset_x = slot.advance.x >> 6 else: char_offset_x = font_size // 2 if bitmap.rows * bitmap.width == 0 or len(bitmap.buffer) != bitmap.rows * bitmap.width: return char_offset_x bm_char = np.array(bitmap.buffer, dtype=np.uint8).reshape((bitmap.rows, bitmap.width)) cx = pen[0] + slot.bitmap_left cy = pen[1] - slot.bitmap_top y0 = max(0, cy); x0 = max(0, cx) y1 = min(canvas_text.shape[0], cy + bitmap.rows) x1 = min(canvas_text.shape[1], cx + bitmap.width) if y0 < y1 and x0 < x1: sl = bm_char[y0 - cy:y1 - cy, x0 - cx:x1 - cx] if sl.size > 0: canvas_text[y0:y1, x0:x1] = sl if border_size > 0: _render_border_horizontal(cdpt, font_size, bitmap, cx, cy, canvas_border) return char_offset_x def _render_border_horizontal(cdpt, font_size, bitmap, cx, cy, canvas_border): glyph_b = get_char_border(cdpt, font_size, 0) stroker = freetype.Stroker() stroker.set(64 * max(int(0.07 * font_size), 1), freetype.FT_STROKER_LINEJOIN_ROUND, freetype.FT_STROKER_LINECAP_ROUND, 0) glyph_b.stroke(stroker, destroy=True) blyph = glyph_b.to_bitmap(freetype.FT_RENDER_MODE_NORMAL, freetype.Vector(0, 0), True) bm_b = blyph.bitmap if bm_b.rows * bm_b.width == 0 or len(bm_b.buffer) != bm_b.rows * bm_b.width: return arr = np.array(bm_b.buffer, dtype=np.uint8).reshape((bm_b.rows, bm_b.width)) # Center-align border with character bx = int(round(cx + bitmap.width / 2 - bm_b.width / 2)) by = int(round(cy + bitmap.rows / 2 - bm_b.rows / 2)) y0a = max(0, by); x0a = max(0, bx) y1a = min(canvas_border.shape[0], by + bm_b.rows) x1a = min(canvas_border.shape[1], bx + bm_b.width) if y0a < y1a and x0a < x1a: sl = arr[y0a - by:y1a - by, x0a - bx:x1a - bx] tgt = canvas_border[y0a:y1a, x0a:x1a] if sl.shape == tgt.shape: canvas_border[y0a:y1a, x0a:x1a] = cv2.add(tgt, sl) # --------------------------------------------------------------------------- # Vertical character rendering # --------------------------------------------------------------------------- def put_char_vertical(font_size: int, cdpt: str, pen_l, canvas_text, canvas_border, border_size: int): pen = list(pen_l) cdpt, _ = CJK_Compatibility_Forms_translate(cdpt, 1) slot = get_char_glyph(cdpt, font_size, 1) bitmap = slot.bitmap if bitmap.rows * bitmap.width == 0 or len(bitmap.buffer) != bitmap.rows * bitmap.width: if slot.metrics.vertAdvance: return slot.metrics.vertAdvance >> 6 return font_size char_offset_y = slot.metrics.vertAdvance >> 6 bm_char = np.array(bitmap.buffer, dtype=np.uint8).reshape((bitmap.rows, bitmap.width)) cx = pen[0] + (slot.metrics.vertBearingX >> 6) cy = pen[1] + (slot.metrics.vertBearingY >> 6) y0 = max(0, cy); x0 = max(0, cx) y1 = min(canvas_text.shape[0], cy + bitmap.rows) x1 = min(canvas_text.shape[1], cx + bitmap.width) if y0 < y1 and x0 < x1: sl = bm_char[y0 - cy:y1 - cy, x0 - cx:x1 - cx] if sl.size > 0: canvas_text[y0:y1, x0:x1] = sl if border_size > 0: _render_border_vertical(cdpt, font_size, bitmap, cx, cy, canvas_border) return char_offset_y def _render_border_vertical(cdpt, font_size, bitmap, cx, cy, canvas_border): glyph_b = get_char_border(cdpt, font_size, 1) stroker = freetype.Stroker() stroker.set(64 * max(int(0.07 * font_size), 1), freetype.FT_STROKER_LINEJOIN_ROUND, freetype.FT_STROKER_LINECAP_ROUND, 0) glyph_b.stroke(stroker, destroy=True) blyph = glyph_b.to_bitmap(freetype.FT_RENDER_MODE_NORMAL, freetype.Vector(0, 0), True) bm_b = blyph.bitmap if bm_b.rows * bm_b.width == 0 or len(bm_b.buffer) != bm_b.rows * bm_b.width: return arr = np.array(bm_b.buffer, dtype=np.uint8).reshape((bm_b.rows, bm_b.width)) bx = int(round(cx + bitmap.width / 2 - bm_b.width / 2)) by = int(round(cy + bitmap.rows / 2 - bm_b.rows / 2)) y0a = max(0, by); x0a = max(0, bx) y1a = min(canvas_border.shape[0], by + bm_b.rows) x1a = min(canvas_border.shape[1], bx + bm_b.width) if y0a < y1a and x0a < x1a: sl = arr[y0a - by:y1a - by, x0a - bx:x1a - bx] tgt = canvas_border[y0a:y1a, x0a:x1a] if sl.shape == tgt.shape: canvas_border[y0a:y1a, x0a:x1a] = cv2.add(tgt, sl) # --------------------------------------------------------------------------- # Layout calculation # --------------------------------------------------------------------------- def calc_vertical(font_size: int, text: str, max_height: int): line_text_list = [] line_height_list = [] line_str = "" line_height = 0 for cdpt in text: if line_height == 0 and cdpt == ' ': continue cdpt, _ = CJK_Compatibility_Forms_translate(cdpt, 1) ckpt = get_char_glyph(cdpt, font_size, 1) bm = ckpt.bitmap if bm.rows * bm.width == 0 or len(bm.buffer) != bm.rows * bm.width: char_offset_y = ckpt.metrics.vertBearingY >> 6 else: char_offset_y = ckpt.metrics.vertAdvance >> 6 if line_height + char_offset_y > max_height: line_text_list.append(line_str) line_height_list.append(line_height) line_str = "" line_height = 0 line_height += char_offset_y line_str += cdpt line_text_list.append(line_str) line_height_list.append(line_height) return line_text_list, line_height_list def select_hyphenator(lang: str): if not HAS_HYPHEN: return None lang = standardize_tag(lang) if lang not in HYPHENATOR_LANGUAGES: for avail in reversed(HYPHENATOR_LANGUAGES): if avail.startswith(lang): lang = avail break else: return None try: return Hyphenator(lang) except Exception: return None def _is_cjk_language(lang: str) -> bool: """Check if language should skip hyphenation (CJK, Thai, etc.).""" lang_lower = lang.lower() # CJK and other languages that don't use spaces/hyphenation cjk_codes = ['ja', 'jp', 'zh', 'ko', 'th', 'lo', 'my', 'km'] return any(lang_lower.startswith(code) for code in cjk_codes) def _syllabify_word(word: str, font_size: int, hyphenator=None, lang: str = 'en_US'): """ Smart word breaking that avoids crude character-level splits. Incorporates: - Language awareness (skip hyphenation for CJK) - Hyphenation for long words (10+ chars) - Natural grouping for mid-length words (4-9 chars) - Keeps short words intact (1-3 chars) """ # Skip hyphenation for CJK languages if _is_cjk_language(lang): return [word] # Keep short words intact if len(word) <= 3: return [word] # Try hyphenation for long words if hyphenator and len(word) >= 10 and len(word) <= 100: try: syls = hyphenator.syllables(word) if syls: # Only use if hyphenation succeeded return syls except Exception: pass # For 4-9 char words: prefer keeping whole or smart grouping if len(word) <= 9: # Simple heuristic: break after vowels when followed by consonants # This gives more natural breaks like "me-di-um" vs "m-e-d-i-u-m" result = [] current = "" vowels = "aeiouAEIOU" for i, c in enumerate(word): current += c # Break after vowel if next is consonant (and we have 2+ chars) if len(current) >= 2 and c in vowels and (i + 1 < len(word) and word[i+1] not in vowels): result.append(current) current = "" if current: result.append(current) # Only use smart grouping if it produces 2-4 clean chunks if result and 2 <= len(result) <= 4: return result # Otherwise, keep whole word (better than char-by-char) return [word] # For very long words without hyphenation: chunk into groups of 3-4 chars if len(word) > 15: chunk_size = 3 return [word[i:i+chunk_size] for i in range(0, len(word), chunk_size)] # Default: keep whole word return [word] def calc_horizontal(font_size: int, text: str, max_width: int, max_height: int, language: str = 'en_US', hyphenate: bool = True): max_width = max(max_width, 2 * font_size) whitespace_offset_x = get_char_offset_x(font_size, ' ') hyphen_offset_x = get_char_offset_x(font_size, '-') words = re.split(r'\s+', text) word_widths = [get_string_width(font_size, w) for w in words] # Expand width if overflow is unavoidable while True: max_lines = max_height // font_size + 1 expected = sum(word_widths) + max((len(word_widths) - 1) * whitespace_offset_x - (max_lines - 1) * hyphen_offset_x, 0) if max_width * max_lines < expected: m = max(np.sqrt(expected / (max_width * max_lines)), 1.05) max_width *= m max_height *= m else: break # Split into syllables using smart word breaking syllables = [] hyphenator = select_hyphenator(language) if hyphenate else None for word in words: # Use new smart syllabification syls = _syllabify_word(word, font_size, hyphenator, language) # Handle edge case: if a syllable is still too wide, split it normed = [] for s in syls: if get_string_width(font_size, s) > max_width: # Last resort: character-level split for extremely narrow boxes normed.extend(list(s)) else: normed.append(s) syllables.append(normed) line_words_list: list[list[int]] = [] line_width_list: list[int] = [] hyphenation_idx_list: list[int] = [] line_words: list[int] = [] line_width = 0 hyph_idx = 0 def break_line(): nonlocal line_words, line_width, hyph_idx line_words_list.append(line_words) line_width_list.append(line_width) hyphenation_idx_list.append(hyph_idx) line_words = [] line_width = 0 hyph_idx = 0 # Step 1: arrange without hyphenation i = 0 while True: if i >= len(words): if line_width > 0: break_line() break cw = whitespace_offset_x if line_width > 0 else 0 if line_width + cw + word_widths[i] <= max_width + hyphen_offset_x: line_words.append(i) line_width += cw + word_widths[i] i += 1 elif word_widths[i] > max_width: j = 0 hyph_idx = 0 while j < len(syllables[i]): sw = get_string_width(font_size, syllables[i][j]) if line_width + cw + sw <= max_width: cw += sw j += 1 hyph_idx = j else: if hyph_idx > 0: line_words.append(i) line_width += cw cw = 0 break_line() line_words.append(i) line_width += cw i += 1 else: break_line() # Step 2: Backwards hyphenation to fill lines def get_present_syllables_range(li, wp): while wp < 0: wp += len(line_words_list[li]) wi = line_words_list[li][wp] ss = 0 se = len(syllables[wi]) if li > 0 and wp == 0 and line_words_list[li - 1][-1] == wi: ss = hyphenation_idx_list[li - 1] if li < len(line_words_list) - 1 and wp == len(line_words_list[li]) - 1 \ and line_words_list[li + 1][0] == wi: se = hyphenation_idx_list[li] return ss, se max_lines = max_height // font_size + 1 if hyphenate and hyphenator and len(line_words_list) > max_lines: li = 0 while li < len(line_words_list) - 1: lw1 = line_words_list[li] lw2 = line_words_list[li + 1] left = max_width - line_width_list[li] first_word = True while lw2: wi = lw2[0] if first_word and wi == lw1[-1]: ss = hyphenation_idx_list[li] se = hyphenation_idx_list[li + 1] if (li < len(line_width_list) - 2 and wi == line_words_list[li + 2][0]) else len(syllables[wi]) else: left -= whitespace_offset_x ss = 0 se = len(syllables[wi]) if len(lw2) > 1 else hyphenation_idx_list[li + 1] first_word = False cw = 0 for si in range(ss, se): sw = get_string_width(font_size, syllables[wi][si]) if left > cw + sw: cw += sw else: if cw > 0: left -= cw line_width_list[li] = max_width - left hyphenation_idx_list[li] = si lw1.append(wi) break else: left -= cw line_width_list[li] = max_width - left lw1.append(wi) lw2.pop(0) continue break if not lw2: line_words_list.pop(li + 1) line_width_list.pop(li + 1) hyphenation_idx_list.pop(li) else: li += 1 # Step 3: Move tiny fragments between lines li = 0 while li < len(line_words_list) - 1: lw1 = line_words_list[li] lw2 = line_words_list[li + 1] if lw1[-1] == lw2[0]: ss1, se1 = get_present_syllables_range(li, -1) t1 = ''.join(syllables[lw1[-1]][ss1:se1]) ss2, se2 = get_present_syllables_range(li + 1, 0) t2 = ''.join(syllables[lw2[0]][ss2:se2]) w1 = get_string_width(font_size, t1) w2 = get_string_width(font_size, t2) if len(t2) == 1 or w2 < font_size: lw2.pop(0) line_width_list[li] += w2 line_width_list[li + 1] -= w2 + whitespace_offset_x elif len(t1) == 1 or w1 < font_size: lw1.pop(-1) line_width_list[li] -= w1 + whitespace_offset_x line_width_list[li + 1] += w1 if not lw1: line_words_list.pop(li); line_width_list.pop(li); hyphenation_idx_list.pop(li) elif not lw2: line_words_list.pop(li + 1); line_width_list.pop(li + 1); hyphenation_idx_list.pop(li) else: li += 1 # Step 4: Assemble use_hyphens = hyphenate and hyphenator and max_width > 1.5 * font_size and len(words) > 1 line_text_list = [] for i, line in enumerate(line_words_list): lt = '' for j, wi in enumerate(line): ss, se = get_present_syllables_range(i, j) lt += ''.join(syllables[wi][ss:se]) if not lt: continue if j == 0 and i > 0 and line_text_list[-1][-1] == '-' and lt[0] == '-': lt = lt[1:] line_width_list[i] -= hyphen_offset_x if j < len(line) - 1 and lt: lt += ' ' elif use_hyphens and se != len(syllables[wi]) and len(words[wi]) > 3 and lt[-1] != '-' \ and not (se < len(syllables[wi]) and not re.search(r'\w', syllables[wi][se][0])): lt += '-' line_width_list[i] += hyphen_offset_x line_width_list[i] = get_string_width(font_size, lt) line_text_list.append(lt) return line_text_list, line_width_list # --------------------------------------------------------------------------- # High-level text rendering # --------------------------------------------------------------------------- def put_text_vertical(font_size: int, text: str, h: int, alignment: str, fg, bg, line_spacing: int): text = compact_special_symbols(text) if not text: return None bg_size = int(max(font_size * 0.07, 1)) if bg is not None else 0 # Improved line spacing: 10% for vertical text line_spacing_ratio = line_spacing if line_spacing else 0.10 spacing_x = max(int(font_size * line_spacing_ratio), 2) # Minimum 2px num_char_y = max(h // font_size, 1) num_char_x = len(text) // num_char_y + 1 canvas_x = font_size * num_char_x + spacing_x * (num_char_x - 1) + (font_size + bg_size) * 2 canvas_y = font_size * num_char_y + (font_size + bg_size) * 2 line_text_list, line_height_list = calc_vertical(font_size, text, h) canvas_text = np.zeros((canvas_y, canvas_x), dtype=np.uint8) canvas_border = canvas_text.copy() pen_orig = [canvas_text.shape[1] - (font_size + bg_size), font_size + bg_size] for lt, lh in zip(line_text_list, line_height_list): pen_line = pen_orig.copy() if alignment == 'center': pen_line[1] += (max(line_height_list) - lh) // 2 elif alignment == 'right': pen_line[1] += max(line_height_list) - lh for c in lt: pen_line[1] += put_char_vertical(font_size, c, pen_line, canvas_text, canvas_border, border_size=bg_size) pen_orig[0] -= spacing_x + font_size canvas_border = np.clip(canvas_border, 0, 255) box = add_color(canvas_text, fg, canvas_border, bg) x, y, w, h = cv2.boundingRect(canvas_border) return box[y:y + h, x:x + w] def put_text_horizontal(font_size: int, text: str, width: int, height: int, alignment: str, reversed_direction: bool, fg, bg, lang: str = 'en_US', hyphenate: bool = True, line_spacing: int = 0): text = compact_special_symbols(text) if not text: return None bg_size = int(max(font_size * 0.07, 1)) if bg is not None else 0 # Improved line spacing: 15% of font size (professional typography standard) line_spacing_ratio = line_spacing if line_spacing else 0.15 spacing_y = max(int(font_size * line_spacing_ratio), 3) # Minimum 3px line_text_list, line_width_list = calc_horizontal(font_size, text, width, height, lang, hyphenate) canvas_w = max(line_width_list) + (font_size + bg_size) * 2 canvas_h = font_size * len(line_width_list) + spacing_y * (len(line_width_list) - 1) + (font_size + bg_size) * 2 canvas_text = np.zeros((canvas_h, canvas_w), dtype=np.uint8) canvas_border = canvas_text.copy() pen_orig = [font_size + bg_size, font_size + bg_size] if reversed_direction: pen_orig[0] = canvas_w - bg_size - 10 for lt, lw in zip(line_text_list, line_width_list): pen_line = pen_orig.copy() if alignment == 'center': pen_line[0] += (max(line_width_list) - lw) // 2 * (-1 if reversed_direction else 1) elif alignment == 'right' and not reversed_direction: pen_line[0] += max(line_width_list) - lw elif alignment == 'left' and reversed_direction: pen_line[0] -= max(line_width_list) - lw pen_line[0] = max(lw, pen_line[0]) for c in lt: if reversed_direction: cdpt, _ = CJK_Compatibility_Forms_translate(c, 0) g = get_char_glyph(cdpt, font_size, 0) pen_line[0] -= g.metrics.horiAdvance >> 6 offset_x = put_char_horizontal(font_size, c, pen_line, canvas_text, canvas_border, border_size=bg_size) if not reversed_direction: pen_line[0] += offset_x pen_orig[1] += spacing_y + font_size canvas_border = np.clip(canvas_border, 0, 255) box = add_color(canvas_text, fg, canvas_border, bg) x, y, w, h = cv2.boundingRect(canvas_border) return box[y:y + h, x:x + w]