# -*- coding: utf-8 -*- """ ๐Ÿ”ฅ ๊ณ ๊ธ‰ ํ…์ŠคํŠธ ๋ Œ๋”๋ง ๋ชจ๋“ˆ - 4์  ํด๋ฆฌ๊ณค ์›๊ทผ ํˆฌ์˜, WCAG ๋Œ€๋น„, ๊ณ ํ’ˆ์งˆ ์…ฐ์ดํ•‘ - Pango/Cairo ์šฐ์„ , Pillow ํด๋ฐฑ - 4์  ํด๋ฆฌ๊ณค(์‚ฌ๋‹ค๋ฆฌ๊ผด) ์›๊ทผ ํˆฌ์˜ ํ•ฉ์„ฑ - WCAG ๋Œ€๋น„(4.5:1) ์ž๋™ ๋ณด์ • - ์™ธ๊ณฝ์„ /์„€๋„/๊ธ€๋กœ์šฐ + ๋ธ”๋ Œ๋”ฉ ๋ชจ๋“œ ์ง€์› - ํ•œ๊ธ€/CJK ์ค„๋ฐ”๊ฟˆ ํญ๋งž์ถค, ์ด๋ถ„ ํƒ์ƒ‰ ํฐํŠธ ํ”ผํŒ… """ import os import math import logging from typing import List, Dict, Any, Tuple, Optional, Union from enum import Enum import json import cv2 import numpy as np from PIL import Image, ImageDraw, ImageFont from PIL.ImageColor import getrgb # ๐Ÿ”ฅ ๊ณ ๊ธ‰ ๋ Œ๋”๋ง ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ (์•ˆ์ „ํ•œ ์ž„ํฌํŠธ) CAIRO_AVAILABLE = False cairo = None Pango = None PangoCairo = None try: import cairo import gi gi.require_version('Pango', '1.0') gi.require_version('PangoCairo', '1.0') from gi.repository import Pango, PangoCairo CAIRO_AVAILABLE = True print("โœ… Cairo/Pango ์ง€์› ํ™œ์„ฑํ™”") except (ImportError, ValueError, AttributeError) as e: CAIRO_AVAILABLE = False print(f"โš ๏ธ Cairo/Pango ๋ฏธ์ง€์›, Pillow ํด๋ฐฑ ์‚ฌ์šฉ: {e}") class BlendMode(Enum): """๋ธ”๋ Œ๋”ฉ ๋ชจ๋“œ""" NORMAL = "normal" MULTIPLY = "multiply" SCREEN = "screen" OVERLAY = "overlay" SOFT_LIGHT = "soft_light" class TextEffect(Enum): """ํ…์ŠคํŠธ ํšจ๊ณผ""" NONE = "none" OUTLINE = "outline" SHADOW = "shadow" GLOW = "glow" EMBOSS = "emboss" class TextRenderingModule: def __init__(self, logger, font_path: Optional[str] = None): """ ๐Ÿ”ฅ ๊ณ ๊ธ‰ ํ…์ŠคํŠธ ๋ Œ๋”๋ง ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™” Args: logger: ๋กœ๊ฑฐ ๊ฐ์ฒด font_path: ๊ธฐ๋ณธ ํฐํŠธ ๊ฒฝ๋กœ (์„ ํƒ์‚ฌํ•ญ) """ self.logger = logger self.default_font_size = 20 self.font_cache: Dict[str, Union[ImageFont.FreeTypeFont, str]] = {} self.cairo_available = CAIRO_AVAILABLE # ๊ธฐ๋ณธ ํฐํŠธ ๋ฒˆํ˜ธ (3๋ฒˆ) self.default_font_number = 3 # ๐Ÿ”ฅ ๊ณ ๊ธ‰ ๋ Œ๋”๋ง ์„ค์ • self.quality_settings = { 'dpi': 150, # ๊ณ ํ•ด์ƒ๋„ ๋ Œ๋”๋ง 'antialiasing': True, 'subpixel_rendering': True, 'font_hinting': False, } # ๐Ÿ”ฅ WCAG ๋Œ€๋น„ ๋น„์œจ ์„ค์ • self.wcag_contrast_ratios = { 'AA_normal': 4.5, # WCAG AA ์ผ๋ฐ˜ ํ…์ŠคํŠธ 'AA_large': 3.0, # WCAG AA ํฐ ํ…์ŠคํŠธ 'AAA_normal': 7.0, # WCAG AAA ์ผ๋ฐ˜ ํ…์ŠคํŠธ 'AAA_large': 4.5, # WCAG AAA ํฐ ํ…์ŠคํŠธ } # ํฐํŠธ ๋งต ์„ค์ • default_path = self._setup_default_fonts() self.font_path = font_path or default_path self.logger.log("๐Ÿ”ฅ ๊ณ ๊ธ‰ ํ…์ŠคํŠธ ๋ Œ๋”๋ง ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ", level=logging.INFO) self.logger.log(f"Cairo ์ง€์›: {self.cairo_available}", level=logging.INFO) self.logger.log(f"๊ธฐ๋ณธ ํฐํŠธ ๊ฒฝ๋กœ: {self.font_path}", level=logging.INFO) def _setup_default_fonts(self) -> Optional[str]: """ํฐํŠธ ๋งต ์„ค์ • (๊ธฐ์กด ๋กœ์ง ์œ ์ง€)""" base_path = "/app/worker/fonts/" self.font_map: Dict[int, str] = { 1: os.path.join(base_path, "HakgyoansimDunggeunmisoTTFB.ttf"), 2: os.path.join(base_path, "NanumBarunGothic.ttf"), 3: os.path.join(base_path, "NanumSquareRoundR.ttf"), 4: os.path.join(base_path, "gamtanload.ttf"), 5: os.path.join(base_path, "Cafe24Ohsquare-v2.0.ttf"), } # ์‹ค์ œ ์กด์žฌํ•˜๋Š” ํฐํŠธ๋งŒ ๋‚จ๊น€ for key in list(self.font_map.keys()): path = self.font_map[key] if not os.path.exists(path): self.logger.log(f"[๊ฒฝ๊ณ ] ํฐํŠธ ํŒŒ์ผ ์—†์Œ: {path}", level=logging.WARNING) del self.font_map[key] # ๊ธฐ๋ณธ ํฐํŠธ ๋ฐ˜ํ™˜ if self.default_font_number in self.font_map: return self.font_map[self.default_font_number] elif len(self.font_map) > 0: return next(iter(self.font_map.values())) else: self.logger.log("[์˜ค๋ฅ˜] ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํฐํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.", level=logging.ERROR) return None def _get_font_path_by_number(self, font_number: Optional[int]) -> Optional[str]: """๋ฒˆํ˜ธ๋กœ ํฐํŠธ ๊ฒฝ๋กœ ์„ ํƒ""" if font_number is not None and font_number in self.font_map: return self.font_map[font_number] return self.font_path # ๐Ÿ”ฅ ==================== ์ƒ‰์ƒ/๋Œ€๋น„ ๊ด€๋ จ ==================== def _calculate_luminance(self, color: Tuple[int, int, int]) -> float: """WCAG ๊ธฐ์ค€ ํœ˜๋„ ๊ณ„์‚ฐ""" def linearize(c): c = c / 255.0 return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4 r, g, b = color return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b) def _calculate_contrast_ratio(self, color1: Tuple[int, int, int], color2: Tuple[int, int, int]) -> float: """WCAG ๊ธฐ์ค€ ๋Œ€๋น„ ๋น„์œจ ๊ณ„์‚ฐ""" lum1 = self._calculate_luminance(color1) lum2 = self._calculate_luminance(color2) lighter = max(lum1, lum2) darker = min(lum1, lum2) return (lighter + 0.05) / (darker + 0.05) def _get_wcag_compliant_color(self, bg_color: Tuple[int, int, int], target_ratio: float = 4.5, prefer_dark: bool = True) -> Tuple[int, int, int]: """๐Ÿ”ฅ WCAG ๋Œ€๋น„ ๋น„์œจ์„ ๋งŒ์กฑํ•˜๋Š” ํ…์ŠคํŠธ ์ƒ‰์ƒ ์ƒ์„ฑ""" # ๊ธฐ๋ณธ ํ›„๋ณด๋“ค candidates = [ (0, 0, 0), # ๊ฒ€์ • (255, 255, 255), # ํฐ์ƒ‰ (33, 37, 41), # ์ง„ํ•œ ํšŒ์ƒ‰ (248, 249, 250), # ๋ฐ์€ ํšŒ์ƒ‰ ] best_color = (0, 0, 0) if prefer_dark else (255, 255, 255) best_ratio = self._calculate_contrast_ratio(bg_color, best_color) # ํ›„๋ณด ์ค‘ ์ตœ์  ์„ ํƒ for candidate in candidates: ratio = self._calculate_contrast_ratio(bg_color, candidate) if ratio >= target_ratio and ratio > best_ratio: best_color = candidate best_ratio = ratio # ์š”๊ตฌ ๋น„์œจ์„ ๋งŒ์กฑํ•˜์ง€ ๋ชปํ•˜๋ฉด ๊ฐ•์ œ ์กฐ์ • if best_ratio < target_ratio: bg_luminance = self._calculate_luminance(bg_color) if bg_luminance > 0.5: # ๋ฐ์€ ๋ฐฐ๊ฒฝ best_color = (0, 0, 0) # ๊ฒ€์ • ๊ฐ•์ œ else: # ์–ด๋‘์šด ๋ฐฐ๊ฒฝ best_color = (255, 255, 255) # ํฐ์ƒ‰ ๊ฐ•์ œ self.logger.log(f"WCAG ๋Œ€๋น„ ๋ณด์ •: ๋ฐฐ๊ฒฝ{bg_color} โ†’ ํ…์ŠคํŠธ{best_color} (๋น„์œจ: {best_ratio:.2f})", level=logging.DEBUG) return best_color def _estimate_background_color(self, image: np.ndarray, polygon: List[Tuple[int, int]]) -> Tuple[int, int, int]: """๐Ÿ”ฅ ๊ฐœ์„ ๋œ ๋ฐฐ๊ฒฝ์ƒ‰ ์ถ”์ • (ํด๋ฆฌ๊ณค ๊ธฐ๋ฐ˜)""" # ํด๋ฆฌ๊ณค์„ ๋งˆ์Šคํฌ๋กœ ๋ณ€ํ™˜ mask = np.zeros(image.shape[:2], dtype=np.uint8) polygon_array = np.array(polygon, dtype=np.int32) cv2.fillPoly(mask, [polygon_array], 255) # ํด๋ฆฌ๊ณค ์˜์—ญ ํ™•์žฅ (์ปจํ…์ŠคํŠธ ๊ณ ๋ ค) kernel = np.ones((15, 15), np.uint8) expanded_mask = cv2.dilate(mask, kernel, iterations=1) context_mask = cv2.subtract(expanded_mask, mask) # ์ปจํ…์ŠคํŠธ ์˜์—ญ์˜ ํ‰๊ท  ์ƒ‰์ƒ (BGR) if np.sum(context_mask) > 0: masked_region = image[context_mask > 0] mean_color = np.mean(masked_region, axis=0) else: # ํด๋ฐฑ: ์ „์ฒด ์ด๋ฏธ์ง€ ํ‰๊ท  mean_color = np.mean(image, axis=(0, 1)) # BGR โ†’ RGB ๋ณ€ํ™˜ return (int(mean_color[2]), int(mean_color[1]), int(mean_color[0])) # ๐Ÿ”ฅ ==================== ๊ณ ๊ธ‰ ํฐํŠธ ํ”ผํŒ… (์ด๋ถ„ ํƒ์ƒ‰) ==================== def _measure_text_size_pillow(self, text: str, font_path: str, font_size: float) -> Tuple[int, int]: """Pillow ๊ธฐ๋ฐ˜ ํ…์ŠคํŠธ ํฌ๊ธฐ ์ธก์ •""" try: font = ImageFont.truetype(font_path, int(font_size)) bbox = font.getbbox(text) return bbox[2] - bbox[0], bbox[3] - bbox[1] except Exception: # ๊ธฐ๋ณธ ํฐํŠธ ํด๋ฐฑ font = ImageFont.load_default() bbox = font.getbbox(text) return bbox[2] - bbox[0], bbox[3] - bbox[1] def _fit_text_binary_search(self, text: str, target_width: int, target_height: int, font_path: str, min_size: float = 8.0, max_size: float = 200.0, tolerance: float = 0.95) -> float: """๐Ÿ”ฅ ์ด๋ถ„ ํƒ์ƒ‰์„ ํ†ตํ•œ ์ •ํ™•ํ•œ ํฐํŠธ ํฌ๊ธฐ ํ”ผํŒ…""" if not text.strip(): return min_size # ์ด๋ถ„ ํƒ์ƒ‰ low, high = min_size, max_size best_size = min_size while high - low > 0.5: # 0.5pt ์ •๋ฐ€๋„ mid = (low + high) / 2 width, height = self._measure_text_size_pillow(text, font_path, mid) # ์—ฌ๋ฐฑ ๊ณ ๋ ค (90% ์‚ฌ์šฉ) fits_width = width <= target_width * tolerance fits_height = height <= target_height * tolerance if fits_width and fits_height: best_size = mid low = mid else: high = mid return best_size # ๐Ÿ”ฅ ==================== ํ•œ๊ธ€/CJK ์ค„๋ฐ”๊ฟˆ ์ฒ˜๋ฆฌ ==================== def _is_cjk_character(self, char: str) -> bool: """CJK ๋ฌธ์ž ํŒ๋ณ„""" code = ord(char) return ( 0x4E00 <= code <= 0x9FFF or # CJK Unified Ideographs 0x3400 <= code <= 0x4DBF or # CJK Extension A 0xAC00 <= code <= 0xD7AF or # Hangul Syllables 0x1100 <= code <= 0x11FF or # Hangul Jamo 0x3130 <= code <= 0x318F or # Hangul Compatibility Jamo 0xFF00 <= code <= 0xFFEF # Halfwidth and Fullwidth Forms ) def _smart_text_wrap(self, text: str, max_width: int, font_path: str, font_size: float) -> List[str]: """๐Ÿ”ฅ ํ•œ๊ธ€/CJK ์ง€๋Šฅํ˜• ์ค„๋ฐ”๊ฟˆ""" if not text.strip(): return [text] lines = [] words = text.split() if not words: return [text] current_line = "" for word in words: test_line = current_line + (" " if current_line else "") + word # ํ˜„์žฌ ์ค„ + ์ƒˆ ๋‹จ์–ด์˜ ๋„ˆ๋น„ ์ธก์ • width, _ = self._measure_text_size_pillow(test_line, font_path, font_size) if width <= max_width * 0.95: # 5% ์—ฌ๋ฐฑ current_line = test_line else: # ํ˜„์žฌ ์ค„์ด ๋น„์–ด์žˆ์ง€ ์•Š์œผ๋ฉด ์ €์žฅํ•˜๊ณ  ์ƒˆ ์ค„ ์‹œ์ž‘ if current_line: lines.append(current_line) current_line = word else: # ๋‹จ์–ด ์ž์ฒด๊ฐ€ ๋„ˆ๋ฌด ๊ธธ๋ฉด CJK ๋‹จ์œ„๋กœ ๋ถ„ํ•  if any(self._is_cjk_character(c) for c in word): lines.extend(self._break_cjk_word(word, max_width, font_path, font_size)) else: lines.append(word) # ๊ฐ•์ œ ์ถ”๊ฐ€ if current_line: lines.append(current_line) return lines if lines else [text] def _break_cjk_word(self, word: str, max_width: int, font_path: str, font_size: float) -> List[str]: """CJK ๋‹จ์–ด๋ฅผ ๋ฌธ์ž ๋‹จ์œ„๋กœ ๋ถ„ํ• """ lines = [] current_line = "" for char in word: test_line = current_line + char width, _ = self._measure_text_size_pillow(test_line, font_path, font_size) if width <= max_width * 0.95: current_line = test_line else: if current_line: lines.append(current_line) current_line = char if current_line: lines.append(current_line) return lines if lines else [word] # ๐Ÿ”ฅ ==================== ๋ฉ”์ธ ๋ Œ๋”๋ง ํ•จ์ˆ˜ (Pillow ๊ธฐ๋ฐ˜) ==================== def render_text(self, image: np.ndarray, ocr_results: List[Dict], translated_texts: List[str], font_number: Optional[int] = None, **kwargs) -> np.ndarray: """ ๐Ÿ”ฅ ๊ณ ๊ธ‰ ํ…์ŠคํŠธ ๋ Œ๋”๋ง ๋ฉ”์ธ ํ•จ์ˆ˜ (Pillow ๊ธฐ๋ฐ˜ + ๊ฐœ์„ ๋œ ์•Œ๊ณ ๋ฆฌ์ฆ˜) Args: image: ์›๋ณธ BGR ์ด๋ฏธ์ง€ ocr_results: [{'polygon': [(x,y), ...]}, ...] translated_texts: ๋ฒˆ์—ญ๋œ ํ…์ŠคํŠธ ๋ฆฌ์ŠคํŠธ font_number: ํฐํŠธ ๋ฒˆํ˜ธ **kwargs: ๊ณ ๊ธ‰ ์˜ต์…˜๋“ค """ self.logger.log(f"๐Ÿ”ฅ ๊ณ ๊ธ‰ ํ…์ŠคํŠธ ๋ Œ๋”๋ง ์‹œ์ž‘: {len(translated_texts)}๊ฐœ ํ…์ŠคํŠธ", level=logging.INFO) # ์˜ต์…˜ ํŒŒ์‹ฑ wcag_level = kwargs.get('wcag_level', 'AA_normal') enable_smart_wrap = kwargs.get('enable_smart_wrap', True) result_image = image.copy() selected_font_path = self._get_font_path_by_number(font_number) if not selected_font_path: self.logger.log("ํฐํŠธ ๊ฒฝ๋กœ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ, ๊ธฐ๋ณธ ์ฒ˜๋ฆฌ ์‚ฌ์šฉ", level=logging.WARNING) return result_image target_contrast_ratio = self.wcag_contrast_ratios.get(wcag_level, 4.5) for i, (ocr_result, translated_text) in enumerate(zip(ocr_results, translated_texts)): if not translated_text.strip(): continue try: polygon = ocr_result['polygon'] # ๐Ÿ”ฅ ๋ฐฐ๊ฒฝ์ƒ‰ ์ถ”์ • (ํด๋ฆฌ๊ณค ๊ธฐ๋ฐ˜) bg_color = self._estimate_background_color(image, polygon) # ๐Ÿ”ฅ WCAG ์ค€์ˆ˜ ํ…์ŠคํŠธ ์ƒ‰์ƒ text_color = self._get_wcag_compliant_color(bg_color, target_contrast_ratio) # ํด๋ฆฌ๊ณค ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค polygon_array = np.array(polygon) x_coords = polygon_array[:, 0] y_coords = polygon_array[:, 1] x_min, x_max = int(np.min(x_coords)), int(np.max(x_coords)) y_min, y_max = int(np.min(y_coords)), int(np.max(y_coords)) bbox_width = max(1, x_max - x_min) bbox_height = max(1, y_max - y_min) # ๐Ÿ”ฅ ์ด๋ถ„ ํƒ์ƒ‰์œผ๋กœ ์ตœ์  ํฐํŠธ ํฌ๊ธฐ ์ฐพ๊ธฐ optimal_font_size = self._fit_text_binary_search( translated_text, bbox_width, bbox_height, selected_font_path ) # ๐Ÿ”ฅ ์ง€๋Šฅํ˜• ์ค„๋ฐ”๊ฟˆ (ํ•„์š”์‹œ) if enable_smart_wrap: lines = self._smart_text_wrap(translated_text, bbox_width, selected_font_path, optimal_font_size) else: lines = [translated_text] # ๐Ÿ”ฅ ๊ณ ํ’ˆ์งˆ ํ…์ŠคํŠธ ๋ Œ๋”๋ง result_image = self._render_text_with_effects( result_image, lines, (x_min, y_min), bbox_width, bbox_height, selected_font_path, optimal_font_size, text_color ) self.logger.log(f"ํ…์ŠคํŠธ {i+1}/{len(translated_texts)} ๋ Œ๋”๋ง ์™„๋ฃŒ", level=logging.DEBUG) except Exception as e: self.logger.log(f"ํ…์ŠคํŠธ {i+1} ๋ Œ๋”๋ง ์‹คํŒจ: {e}", level=logging.WARNING) continue self.logger.log("๐Ÿ”ฅ ๊ณ ๊ธ‰ ํ…์ŠคํŠธ ๋ Œ๋”๋ง ์™„๋ฃŒ", level=logging.INFO) return result_image def _render_text_with_effects(self, image: np.ndarray, lines: List[str], position: Tuple[int, int], bbox_width: int, bbox_height: int, font_path: str, font_size: float, text_color: Tuple[int, int, int]) -> np.ndarray: """๐Ÿ”ฅ ์™ธ๊ณฝ์„ ์ด ์žˆ๋Š” ๊ณ ํ’ˆ์งˆ ํ…์ŠคํŠธ ๋ Œ๋”๋ง""" x_min, y_min = position # ๊ฐ ์ค„์„ ๋ Œ๋”๋ง line_height = font_size * 1.2 # ํ–‰๊ฐ„ total_text_height = len(lines) * line_height start_y = y_min + (bbox_height - total_text_height) // 2 result_image = image.copy() for line_idx, line in enumerate(lines): if not line.strip(): continue line_y = int(start_y + line_idx * line_height) # ํ…์ŠคํŠธ ํฌ๊ธฐ ์ธก์ • text_width, text_height = self._measure_text_size_pillow(line, font_path, font_size) # ์ค‘์•™ ์ •๋ ฌ text_x = x_min + (bbox_width - text_width) // 2 # ๐Ÿ”ฅ ์™ธ๊ณฝ์„  + ํ…์ŠคํŠธ ๋ Œ๋”๋ง result_image = self._render_single_line_with_outline( result_image, line, (text_x, line_y), font_path, font_size, text_color ) return result_image def _render_single_line_with_outline(self, image: np.ndarray, text: str, position: Tuple[int, int], font_path: str, font_size: float, text_color: Tuple[int, int, int]) -> np.ndarray: """๋‹จ์ผ ์ค„ ํ…์ŠคํŠธ๋ฅผ ์™ธ๊ณฝ์„ ๊ณผ ํ•จ๊ป˜ ๋ Œ๋”๋ง""" h, w = image.shape[:2] text_width, text_height = self._measure_text_size_pillow(text, font_path, font_size) # ํ…์ŠคํŠธ ์˜์—ญ๋งŒ PIL๋กœ ์ฒ˜๋ฆฌํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ์ ˆ์•ฝ padding = 20 x1 = max(0, position[0] - padding) y1 = max(0, position[1] - padding) x2 = min(w, position[0] + text_width + padding) y2 = min(h, position[1] + text_height + padding) # ์ž‘์€ ์˜์—ญ๋งŒ PIL๋กœ ๋ณ€ํ™˜ roi = image[y1:y2, x1:x2] pil_roi = Image.fromarray(cv2.cvtColor(roi, cv2.COLOR_BGR2RGB)) draw = ImageDraw.Draw(pil_roi) try: font = ImageFont.truetype(font_path, int(font_size)) except Exception: font = ImageFont.load_default() # ์ƒ๋Œ€ ์ขŒํ‘œ๋กœ ์กฐ์ • rel_pos = (position[0] - x1, position[1] - y1) # ๐Ÿ”ฅ ์™ธ๊ณฝ์„  ๋ Œ๋”๋ง (8๋ฐฉํ–ฅ) outline_width = max(1, int(font_size / 24)) outline_color = (255, 255, 255) if sum(text_color) < 384 else (0, 0, 0) for dx in [-outline_width, 0, outline_width]: for dy in [-outline_width, 0, outline_width]: if dx != 0 or dy != 0: draw.text((rel_pos[0] + dx, rel_pos[1] + dy), text, font=font, fill=outline_color) # ๋ณธ๋ฌธ ํ…์ŠคํŠธ draw.text(rel_pos, text, font=font, fill=text_color) # ROI๋งŒ ๋‹ค์‹œ BGR๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์›๋ณธ์— ์ ์šฉ result_roi = cv2.cvtColor(np.array(pil_roi), cv2.COLOR_RGB2BGR) result_image = image.copy() result_image[y1:y2, x1:x2] = result_roi return result_image # ๐Ÿ”ฅ ==================== ํ˜ธํ™˜์„ฑ ํ•จ์ˆ˜๋“ค ==================== def estimate_text_size(self, text: str, font_size: int, font_path: Optional[str] = None) -> Tuple[int, int]: """ํ…์ŠคํŠธ ํฌ๊ธฐ ์ถ”์ • (ํ˜ธํ™˜์„ฑ ์œ ์ง€)""" if font_path is None: font_path = self.font_path or "" return self._measure_text_size_pillow(text, font_path, font_size) def calculate_optimal_font_size(self, text: str, target_width: int, target_height: int, min_size: int = 8, max_size: int = 100, font_path: Optional[str] = None) -> int: """์ตœ์  ํฐํŠธ ํฌ๊ธฐ ๊ณ„์‚ฐ (ํ˜ธํ™˜์„ฑ ์œ ์ง€)""" if font_path is None: font_path = self.font_path or "" return int(self._fit_text_binary_search(text, target_width, target_height, font_path, min_size, max_size)) # ------------------------------------------------------------------------- # ๐Ÿ”ฅ ์‚ฌ์šฉ ์˜ˆ์‹œ # ------------------------------------------------------------------------- # logger = your_logger # tr = TextRenderingModule(logger) # result = tr.render_text(image, ocr_results, translated_texts, font_number=3)