| 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> |
| """ |
|
|
| |
| 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 |
| |
| |
| 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) |
| |
| |
| 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 |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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)) |
| |
| |
| 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')) |