Spaces:
Paused
Paused
| 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 = """ | |
| <span style="display: inline-block; vertical-align: middle; margin: -{{PAD_Y}}px calc(5px - {{PAD_X}}px); padding: {{PAD_Y}}px {{PAD_X}}px; background: {{BG_COLOR}}; color: {{TEXT_COLOR}} !important; border-radius: {{RADIUS}}px; position: relative; z-index: {{Z_INDEX}}; white-space: nowrap; transition: all 0.1s;">{{WORD}}</span> | |
| """ | |
| # تابع کمکی برای رسم کادرهای با گوشه گرد با کیفیت بسیار بالا و لبههای کاملاً نرم (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 | |
| 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='rtl', language='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) | |
| 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 | |
| word_x = cursor_x - w_len - 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 | |
| }) | |
| 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='rtl', language='fa') |