Spaces:
Sleeping
Sleeping
| from typing import Tuple | |
| from PIL import Image, ImageDraw, ImageFont, ImageEnhance | |
| from io import BytesIO | |
| import boto3 | |
| s3_client = boto3.client("s3") | |
| STYLE_CONFIG = { | |
| "default": { | |
| "patch": { | |
| "background_color": (10, 10, 10, 255), | |
| "text_color": (255, 255, 255, 255), | |
| "opacity": 1, | |
| "top_height": 100, | |
| "bottom_height": 300, | |
| }, | |
| "fade": {"color": (10, 10, 10, 255), "strength": 1.7}, | |
| "image": {"contrast": 1, "brightness": 1, "saturation": 1}, | |
| } | |
| } | |
| def wrap_text( | |
| text: str, font: ImageFont.FreeTypeFont, max_width: int, padding: int = 60 | |
| ) -> list: | |
| """Wrap text to fit within the specified width.""" | |
| max_width -= 2 * padding | |
| words = text.split() | |
| lines, current_line = [], [] | |
| for word in words: | |
| test_line = " ".join(current_line + [word]) | |
| text_width = ImageDraw.Draw( | |
| Image.new("RGBA", (max_width + 2 * padding, 1000)) | |
| ).textbbox((0, 0), test_line, font)[2] | |
| if text_width <= max_width: | |
| current_line.append(word) | |
| else: | |
| lines.append(" ".join(current_line)) | |
| current_line = [word] | |
| if current_line: | |
| lines.append(" ".join(current_line)) | |
| return lines | |
| def apply_style_effects( | |
| image: Image.Image, visual_style: str = "default" | |
| ) -> Image.Image: | |
| """Apply image style effects such as contrast, brightness, and saturation.""" | |
| style_config = STYLE_CONFIG.get(visual_style, STYLE_CONFIG["default"]) | |
| enhancers = [ | |
| (ImageEnhance.Contrast, style_config["image"]["contrast"]), | |
| (ImageEnhance.Brightness, style_config["image"]["brightness"]), | |
| (ImageEnhance.Color, style_config["image"]["saturation"]), | |
| ] | |
| for enhancer_class, factor in enhancers: | |
| image = enhancer_class(image).enhance(factor) | |
| return image.convert("RGBA") | |
| def create_fade_mask( | |
| size: Tuple[int, int], visual_style: str = "default" | |
| ) -> Image.Image: | |
| """Create a fade mask with a gradient effect.""" | |
| width, height = size | |
| style_config = STYLE_CONFIG.get(visual_style, STYLE_CONFIG["default"]) | |
| fade_height = int(height * 0.25) | |
| mask = Image.new("L", size, 255) | |
| gradient_data = [ | |
| int(255 * (i / fade_height) * style_config["fade"]["strength"]) | |
| for i in range(fade_height) | |
| ] | |
| gradient = Image.new("L", (1, fade_height)) | |
| gradient.putdata(gradient_data) | |
| gradient = gradient.resize((width, fade_height)) | |
| mask.paste(gradient, (0, 0)) | |
| mask.paste(gradient.transpose(Image.FLIP_TOP_BOTTOM), (0, height - fade_height)) | |
| return mask | |
| def create_text_patch( | |
| text: str, width: int, height: int, font_path: str, visual_style: str | |
| ) -> Image.Image: | |
| """Create a text patch with wrapping and styling.""" | |
| style_config = STYLE_CONFIG.get(visual_style, STYLE_CONFIG["default"]) | |
| patch = Image.new( | |
| "RGBA", (width, height), style_config["patch"]["background_color"] | |
| ) | |
| draw = ImageDraw.Draw(patch) | |
| font_size = 40 | |
| font = None | |
| while font_size > 10: | |
| try: | |
| font = ImageFont.truetype(font_path, font_size) | |
| lines = wrap_text(text, font, width - 40, 60) | |
| font_height = font.getbbox("A")[3] - font.getbbox("A")[1] | |
| total_text_height = len(lines) * (font_height + 10) | |
| if total_text_height <= height - 40 and len(lines) > 0: | |
| break | |
| except Exception: | |
| pass | |
| font_size -= 2 | |
| font = font or ImageFont.load_default() | |
| lines = wrap_text(text, font, width - 40, 60) | |
| font_height = font.getbbox("A")[3] - font.getbbox("A")[1] | |
| total_text_height = len(lines) * (font_height + 10) | |
| y = (height - total_text_height) // 2 | |
| for line in lines: | |
| bbox = draw.textbbox((0, 0), line, font=font) | |
| text_width = bbox[2] - bbox[0] | |
| x = (width - text_width) // 2 | |
| draw.text((x, y), line, font=font, fill=style_config["patch"]["text_color"]) | |
| y += font_height + 10 | |
| return patch | |
| def process_single_image( | |
| image_data: bytes, | |
| text: str, | |
| font_path: str, | |
| target_width: int = 1024, | |
| target_height: int = 1024, | |
| visual_style: str = "default", | |
| ) -> BytesIO: | |
| """Process a single image with text overlay and styling.""" | |
| img = Image.open(BytesIO(image_data)) | |
| style_config = STYLE_CONFIG.get(visual_style, STYLE_CONFIG["default"]) | |
| top_height, bottom_height = ( | |
| style_config["patch"]["top_height"], | |
| style_config["patch"]["bottom_height"], | |
| ) | |
| # Process the main image | |
| processed = apply_style_effects(img, visual_style).resize( | |
| (target_width, target_height) | |
| ) | |
| # Apply fade effect | |
| mask = create_fade_mask(processed.size, visual_style) | |
| background = Image.new("RGBA", processed.size, style_config["fade"]["color"]) | |
| processed = Image.composite(processed, background, mask) | |
| # Create text patches | |
| bottom_patch = create_text_patch( | |
| text, target_width, bottom_height, font_path, visual_style | |
| ) | |
| top_patch = Image.new( | |
| "RGBA", (target_width, top_height), style_config["patch"]["background_color"] | |
| ) | |
| # Combine all elements | |
| final_image = Image.new( | |
| "RGBA", (target_width, target_height + top_height + bottom_height) | |
| ) | |
| final_image.paste(top_patch, (0, 0)) | |
| final_image.paste(processed, (0, top_height)) | |
| final_image.paste(bottom_patch, (0, top_height + target_height)) | |
| # Save to BytesIO | |
| output_buffer = BytesIO() | |
| final_image.save(output_buffer, format="PNG") | |
| output_buffer.seek(0) | |
| return output_buffer |