import cv2 import pytesseract from PIL import Image, ImageDraw, ImageFont import numpy as np import argparse import io import base64 class AndroidEditor: def __init__(self, font_path="Roboto-Regular.ttf"): self.font_path = font_path @staticmethod def find_text_position(text_list, target_text, start_idx=0): """Mencari posisi teks target dalam list teks""" for i in range(start_idx, len(text_list)): if target_text in text_list[i]: return i return None @staticmethod def get_position_data(extracted_data, idx): """Mendapatkan data posisi dari indeks tertentu""" if idx is None or idx >= len(extracted_data['left']): return None return { "left": extracted_data['left'][idx], "top": extracted_data['top'][idx], "width": extracted_data['width'][idx], "height": extracted_data['height'][idx], } @staticmethod def is_dark_mode(bg_color): """Mendeteksi apakah background menggunakan dark mode berdasarkan kecerahan warna""" r, g, b = float(bg_color[0]), float(bg_color[1]), float(bg_color[2]) brightness = (r * 299 + g * 587 + b * 114) / 1000 return brightness < 128 def process_image(self, image_path, anggota): image = cv2.imread(image_path) if image is None: return result = self._process_core(image, anggota, show_preview=True) return result def process_image_bytes(self, image_bytes, anggota): image_stream = io.BytesIO(image_bytes) pil_image = Image.open(image_stream).convert('RGB') image = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR) result, theme = self._process_core(image, anggota, show_preview=False, return_theme=True) if result is not None: pil_result = Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB)) output_io = io.BytesIO() pil_result.save(output_io, format='PNG') img_b64 = base64.b64encode(output_io.getvalue()).decode('utf-8') return img_b64, theme return None, None def _process_core(self, image, anggota, show_preview=False, return_theme=False): # Ekstrak teks dari gambar extracted_data = pytesseract.image_to_data(image, output_type=pytesseract.Output.DICT) text_list = extracted_data['text'] # Inisialisasi variabel posisi group_position = None split_position = None member_position = None member_count_position = None second_member_position = None second_member_count_position = None lang = '' group_idx = self.find_text_position(text_list, "Grup") if group_idx is None: group_idx = self.find_text_position(text_list, "Group") if group_idx is not None: lang = 'id' if "Grup" in text_list[group_idx] else 'en' group_position = self.get_position_data(extracted_data, group_idx) split_idx = self.find_text_position(text_list, "·", group_idx) if split_idx is None: for i in range(group_idx, min(group_idx + 4, len(text_list))): if "-" in text_list[i]: split_idx = i break split_position = self.get_position_data(extracted_data, split_idx) member_idx = self.find_text_position(text_list, "anggota", group_idx) if member_idx is None: member_idx = self.find_text_position(text_list, "member", group_idx) member_position = self.get_position_data(extracted_data, member_idx) for i in range(group_idx, min(group_idx + 5, len(text_list))): if text_list[i].isdigit(): member_count_position = self.get_position_data(extracted_data, i) break second_member_idx = self.find_text_position(text_list, "Anggota", group_idx + 4) if second_member_idx is not None: second_member_position = self.get_position_data(extracted_data, second_member_idx) for i in range(second_member_idx - 3, second_member_idx): if i >= 0 and text_list[i].isdigit(): second_member_count_position = self.get_position_data(extracted_data, i) break else: return None if member_position is None: return None x = member_position['left'] + member_position['width'] + 100 y = member_position['top'] + member_position['height'] - 100 bg_color = image[y, x] rgb = (int(bg_color[0]), int(bg_color[1]), int(bg_color[2])) is_dark = self.is_dark_mode(bg_color) theme = 'Dark Mode' if is_dark else 'Light Mode' text_color = (147,151,154,255) if is_dark else (90, 94, 95, 255) for position in [group_position, split_position, member_position, member_count_position, second_member_position, second_member_count_position]: if position: margin_horizontal = 10 cv2.rectangle( image, (position['left'] - margin_horizontal, position['top']), (position['left'] + position['width'] + margin_horizontal, position['top'] + position['height']), rgb, -1, ) if member_count_position: updated_member_count = { 'id': f"Grup · {anggota} anggota", 'en': f"Group · {anggota} members" }.get(lang) original_height = member_count_position['height'] original_width = member_count_position['width'] font_size = int(original_height * 2.0) font = ImageFont.truetype(self.font_path, font_size) image_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) draw = ImageDraw.Draw(image_pil) min_size = int(original_height * 1.5) max_size = int(original_height * 2.5) max_iterations = 10 iteration = 0 while min_size <= max_size and iteration < max_iterations: font = ImageFont.truetype(self.font_path, font_size) text_bbox = draw.textbbox((0, 0), updated_member_count, font=font) text_height = text_bbox[3] - text_bbox[1] text_width = text_bbox[2] - text_bbox[0] if abs(text_height - original_height) <= 2 and text_width <= original_width * 1.5: break if text_height > original_height or text_width > original_width * 1.5: font_size = int(font_size * 0.95) else: font_size = int(font_size * 1.05) font_size = max(min_size, min(max_size, font_size)) iteration += 1 text_width = text_bbox[2] - text_bbox[0] image_width = image.shape[1] text_x = (image_width - text_width) // 2 text_y = member_count_position['top'] - 2 draw.text((text_x, text_y), updated_member_count, font=font, fill=text_color) image = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR) if second_member_count_position: updated_second_member_count = { 'id': f"{anggota} Anggota", 'en': f"{anggota} Members" }.get(lang) original_height = second_member_count_position['height'] original_width = second_member_count_position['width'] font_size = int(original_height * 2.0) font = ImageFont.truetype(self.font_path, font_size) image_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) draw = ImageDraw.Draw(image_pil) min_size = int(original_height * 1.5) max_size = int(original_height * 2.5) max_iterations = 10 iteration = 0 while min_size <= max_size and iteration < max_iterations: font = ImageFont.truetype(self.font_path, font_size) text_bbox = draw.textbbox((0, 0), updated_second_member_count, font=font) text_height = text_bbox[3] - text_bbox[1] text_width = text_bbox[2] - text_bbox[0] if abs(text_height - original_height) <= 2 and text_width <= original_width * 1.5: break if text_height > original_height or text_width > original_width * 1.5: font_size = int(font_size * 0.95) else: font_size = int(font_size * 1.05) font_size = max(min_size, min(max_size, font_size)) iteration += 1 text_width = text_bbox[2] - text_bbox[0] text_x = second_member_count_position['left'] text_y = second_member_count_position['top'] - 2 draw.text((text_x, text_y), updated_second_member_count, font=font, fill=text_color) image = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR) cv2.imwrite('output.png', image) if show_preview: cv2.imshow('Preview (Tekan q untuk keluar)', image) while True: key = cv2.waitKey(1) & 0xFF if key == ord('q'): break cv2.destroyAllWindows() if return_theme: return image, theme return image if __name__ == '__main__': parser = argparse.ArgumentParser(description='Proses gambar grup') parser.add_argument('image_path', help='Path ke file gambar') parser.add_argument('anggota', help='Jumlah anggota') args = parser.parse_args() editor = AndroidEditor() editor.process_image(args.image_path, args.anggota)