from PIL import Image, ImageDraw, ImageFont, ImageFilter config = { "ids": ["instagram_box", "alpha_gradient"], "name": "Box Styles", "panels": ["font", "size", "radius", "padding"], "default_font": "vazir" } frontend_template = """ {{WORD}} """ # تابع کمکی برای رسم کادرهای با گوشه گرد با کیفیت بسیار بالا و لبه‌های کاملاً نرم (Anti-aliased) def draw_aa_rounded_rectangle(img, xy, radius, fill): x1, y1, x2, y2 = xy w = int(x2 - x1) h = int(y2 - y1) if w <= 0 or h <= 0: return # رسم شکل با رزولوشن ۴ برابر برای انجام آنتی‌الیاسینگ (Supersampling) scale = 4 s_img = Image.new('RGBA', (w * scale, h * scale), fill) mask_large = Image.new('L', (w * scale, h * scale), 0) m_draw = ImageDraw.Draw(mask_large) m_draw.rounded_rectangle([0, 0, w * scale, h * scale], radius=radius * scale, fill=255) # سازگاری با نسخه‌های مختلف کتابخانه Pillow برای فیلتر تصویری LANCZOS try: resampling_filter = Image.Resampling.LANCZOS except AttributeError: resampling_filter = Image.LANCZOS mask_aa = mask_large.resize((w, h), resampling_filter) aa_img = s_img.resize((w, h), resampling_filter) # چسباندن تصویر هموار شده روی بوم اصلی img.paste(aa_img, (int(x1), int(y1)), mask_aa) def draw_frame(draw, img, width, height, style_config, lines, line_metrics, active_idx, font, color_parser, word_infos=None): style_name = style_config.name is_rtl = getattr(style_config, 'text_direction', 'rtl') != 'ltr' line_height_px = int(style_config.fontSize * 1.1) + int(style_config.paddingY * 2.2) total_block_height = len(lines) * line_height_px bottom_reference = height - style_config.marginV start_y_of_block = bottom_reference - total_block_height pad_x = style_config.paddingX pad_y = style_config.paddingY radius = style_config.radius if style_name == "instagram_box": pad_y = max(5, pad_y) current_line_y = start_y_of_block space_w = draw.textlength(" ", font=font) VERTICAL_CORRECTION = int(style_config.fontSize * 0.22) INSTA_CENTERING_OFFSET = 8 if style_name == "instagram_box" else 0 # محاسبه متریک‌های استاندارد فونت بر اساس کلمه مرجع جهت تراز یکدست عمودی تمام کلمات ref_word = "سلام" try: ref_bbox = draw.textbbox((0, 0), ref_word, font=font, direction=getattr(style_config, 'text_direction', 'rtl'), language=getattr(style_config, 'lang_code', 'fa')) standard_text_visual_center_y = (ref_bbox[1] + ref_bbox[3]) / 2.0 standard_text_offset_y = ref_bbox[1] standard_text_h = ref_bbox[3] - ref_bbox[1] except: standard_text_visual_center_y = style_config.fontSize / 2.0 standard_text_offset_y = 0 standard_text_h = style_config.fontSize global_word_counter = 0 words_to_draw = [] temp_y = current_line_y for line_idx, metrics in enumerate(line_metrics): line_start_idx = sum(len(m["words"]) for m in line_metrics[:line_idx]) is_active_on_line = (active_idx >= line_start_idx and active_idx < line_start_idx + len(metrics["words"])) dynamic_line_width = metrics["width"] + (pad_x * 2 if is_active_on_line else 0) if is_rtl: cursor_x = (width + dynamic_line_width) / 2 + style_config.x else: cursor_x = (width - dynamic_line_width) / 2 + style_config.x # مرکز عمودی دقیق و هندسی این خط فعلی line_center_y = temp_y + (line_height_px / 2.0) for w_i, word in enumerate(metrics["words"]): g_idx = line_start_idx + w_i w_len = metrics["word_widths"][w_i] # استفاده از مقدار ثابت برای جلوگیری از نوسان عمودی کلمات خط text_y_pos = line_center_y - standard_text_visual_center_y current_word_padding = pad_x if g_idx == active_idx else 0 if is_rtl: word_x = cursor_x - w_len - current_word_padding else: word_x = cursor_x + current_word_padding words_to_draw.append({ "text": word, "x": word_x, "y": text_y_pos, "width": w_len, "global_idx": global_word_counter, "line_center_y": line_center_y }) if is_rtl: cursor_x -= (w_len + space_w + (current_word_padding * 2)) else: cursor_x += (w_len + space_w + (current_word_padding * 2)) global_word_counter += 1 temp_y += line_height_px # --- رسم کادر اصلی سفید پشت کل متن برای استایل اینستاگرام --- if style_name == "instagram_box": max_w = 0 for m in line_metrics: if m["width"] > max_w: max_w = m["width"] cx = width / 2 + style_config.x # محاسبه مختصات باکس کلی main_box_x1 = cx - (max_w / 2) - pad_x - 10 # کمی فضای بیشتر (10px) main_box_x2 = cx + (max_w / 2) + pad_x + 10 # محاسبه دقیق سقف و کف بر اساس آرایه واقعی کلمات ذخیره شده برای تقارن مطلق پیکسلی first_line_y = words_to_draw[0]["y"] if words_to_draw else (start_y_of_block + pad_y) last_line_y = words_to_draw[-1]["y"] if words_to_draw else (start_y_of_block + pad_y) main_box_y1 = first_line_y - int(pad_y * 1.3) + VERTICAL_CORRECTION + INSTA_CENTERING_OFFSET - 8 main_box_y2 = last_line_y + style_config.fontSize + int(pad_y * 1.3) + VERTICAL_CORRECTION + INSTA_CENTERING_OFFSET + 8 main_box_hex = style_config.styleBgColors.get('instagram_box_main_box', '#ffffff') main_box_col = color_parser(main_box_hex, (255, 255, 255, 255)) # رسم کادر اصلی اینستاگرام با کیفیت بالا و لبه‌های نرم draw_aa_rounded_rectangle(img, [main_box_x1, main_box_y1, main_box_x2, main_box_y2], radius, main_box_col) # --- رسم کادرها (Highlight) --- for item in words_to_draw: is_active = (item["global_idx"] == active_idx) if is_active: box_color = (0,0,0,0) if style_name == "instagram_box": bg_hex = style_config.styleBgColors.get('instagram_box', '#1a1a1a') box_color = color_parser(bg_hex, (26, 26, 26, 255)) elif style_name == "alpha_gradient": box_color = (124, 77, 255, 255) rect_y1 = item["y"] - int(pad_y * 1.3) + VERTICAL_CORRECTION + INSTA_CENTERING_OFFSET rect_y2 = item["y"] + style_config.fontSize + int(pad_y * 1.3) + VERTICAL_CORRECTION + INSTA_CENTERING_OFFSET bx1, by1 = int(item["x"] - pad_x), int(rect_y1) bx2, by2 = int(item["x"] + item["width"] + pad_x), int(rect_y2) # اگر استایل آلفا بود، گرادینت بساز if style_name == "alpha_gradient": rw, rh = bx2 - bx1, by2 - by1 grad_img = Image.new('RGBA', (rw, rh), (0,0,0,0)) g_draw = ImageDraw.Draw(grad_img) base_hex = style_config.styleBgColors.get('alpha_gradient', '#7c4dff') r1, g1, b1, a1 = color_parser(base_hex, (124, 77, 255, 255)) factor = 0.35 r2 = int(r1 + (255 - r1) * factor) g2 = int(g1 + (255 - g1) * factor) b2 = int(b1 + (255 - b1) * factor) c1 = (r1, g1, b1) c2 = (r2, g2, b2) # رسم سایه (Glow) نرم پشت کادر shadow_pad = 20 shadow_layer = Image.new('RGBA', (rw + shadow_pad*2, rh + shadow_pad*2), (0,0,0,0)) s_draw = ImageDraw.Draw(shadow_layer) s_draw.rounded_rectangle([shadow_pad, shadow_pad, shadow_pad+rw, shadow_pad+rh], radius=radius, fill=(r1, g1, b1, 100)) shadow_layer = shadow_layer.filter(ImageFilter.GaussianBlur(4)) img.paste(shadow_layer, (bx1 - shadow_pad, by1 - shadow_pad + 4), shadow_layer) # پر کردن گرادینت روی بوم موقت for y in range(rh): for x in range(rw): ratio = (x + y) / (rw + rh) r = int(c1[0] * (1-ratio) + c2[0] * ratio) g = int(c1[1] * (1-ratio) + c2[1] * ratio) b = int(c1[2] * (1-ratio) + c2[2] * ratio) g_draw.point((x,y), fill=(r,g,b,255)) # ساخت ماسک با لبه‌های بسیار هموار و بدون پله‌پله شدن گوشه‌ها (Anti-aliased Mask) scale = 4 mask_large = Image.new('L', (rw * scale, rh * scale), 0) ImageDraw.Draw(mask_large).rounded_rectangle([0, 0, rw * scale, rh * scale], radius=radius * scale, fill=255) try: resampling_filter = Image.Resampling.LANCZOS except AttributeError: resampling_filter = Image.LANCZOS mask = mask_large.resize((rw, rh), resampling_filter) img.paste(grad_img, (bx1, by1), mask) else: # رسم کادر فعال استایل اینستاگرام با کیفیت بالا و لبه‌های نرم draw_aa_rounded_rectangle(img, [bx1, by1, bx2, by2], radius, box_color) # --- رسم متن --- for item in words_to_draw: is_active = (item["global_idx"] == active_idx) and getattr(style_config, 'useActiveColor', True) text_color = (255, 255, 255, 255) if style_name == "instagram_box": custom_hex = style_config.styleColors.get('instagram_box', '#000000') u_r, u_g, u_b, u_a = color_parser(custom_hex, (0, 0, 0, 255)) if is_active: active_hex = style_config.styleActiveColors.get('instagram_box', '#ffffff') text_color = color_parser(active_hex, (255, 255, 255, 255)) else: is_past = item["global_idx"] < active_idx fade_surr = getattr(style_config, 'fadeSurrounding', False) if is_past and fade_surr: factor = (u_a / 255.0 * 0.35) else: factor = (u_a / 255.0) if is_past else (u_a / 255.0 * 0.35) f_r = int((u_r * factor) + (255 * (1 - factor))) f_g = int((u_g * factor) + (255 * (1 - factor))) f_b = int((u_b * factor) + (255 * (1 - factor))) text_color = (f_r, f_g, f_b, 255) elif style_name == "alpha_gradient": base_alpha_color = (255, 255, 255, 255) custom_color = style_config.styleColors.get('alpha_gradient') if custom_color: base_alpha_color = color_parser(custom_color, base_alpha_color) r, g, b, a = base_alpha_color if is_active: active_hex = style_config.styleActiveColors.get('alpha_gradient', '#ffffff') text_color = color_parser(active_hex, (255, 255, 255, 255)) elif item["global_idx"] < active_idx: if getattr(style_config, 'fadeSurrounding', False): text_color = (r, g, b, int(a * 0.35)) else: text_color = (r, g, b, a) else: future_alpha = int(a * 0.35) text_color = (r, g, b, future_alpha) if word_infos and item["global_idx"] < len(word_infos): w_obj = word_infos[item["global_idx"]] if hasattr(w_obj, 'color') and w_obj.color: text_color = color_parser(w_obj.color, text_color) # محاسبه مرکز هندسی کادر رسم شده برای این کلمه جهت تراز بی‌نقص rect_y1 = item["y"] - int(pad_y * 1.3) + VERTICAL_CORRECTION + INSTA_CENTERING_OFFSET rect_y2 = item["y"] + style_config.fontSize + int(pad_y * 1.3) + VERTICAL_CORRECTION + INSTA_CENTERING_OFFSET rect_center_y = (rect_y1 + rect_y2) / 2.0 # تراز عمودی کاملاً هم‌سطح بر اساس مقادیر ثابت مرجع به جای مقادیر پویای کلمه‌ای final_text_y = rect_center_y - standard_text_offset_y - (standard_text_h / 2.0) draw.text((item["x"], final_text_y), item["text"], font=font, fill=text_color, direction=getattr(style_config, 'text_direction', 'rtl'), language=getattr(style_config, 'lang_code', 'fa'))