From 73835f0151f75b95cfedbd9cccf0a6139b478fe6 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 30 Nov 2025 07:02:18 +0000 Subject: [PATCH] feat: Update thumbnail generation to match Anime Flicker style - Replaces procedural honeycomb background with `background.jpg`. - Uses `plp.jpg` as a mask for the anime poster integration. - Adds a gradient overlay for improved text readability on the left. - Adjusts text positioning, fonts, and UI elements to match reference design. - Updates font loading logic to support `fonts/` directory. --- thumbnail.py | 241 ++++++++++++++++++++++++--------------------------- 1 file changed, 111 insertions(+), 130 deletions(-) diff --git a/thumbnail.py b/thumbnail.py index f56f0bc..f317238 100644 --- a/thumbnail.py +++ b/thumbnail.py @@ -14,14 +14,21 @@ # Fonts FONTS_DIR = "fonts" try: - # Use root BebasNeue-Regular.ttf for title (the one in fonts/ is empty) - TITLE_FONT = ImageFont.truetype("BebasNeue-Regular.ttf", 160) - # Use available Roboto variants (Bold, Medium, Regular are empty in fonts/) - BOLD_FONT = ImageFont.truetype(os.path.join(FONTS_DIR, "Roboto-SemiBold.ttf"), 42) - MEDIUM_FONT = ImageFont.truetype(os.path.join(FONTS_DIR, "Roboto-SemiBold.ttf"), 36) + # Use root BebasNeue-Regular.ttf for title + if os.path.exists("BebasNeue-Regular.ttf"): + TITLE_FONT_PATH = "BebasNeue-Regular.ttf" + else: + TITLE_FONT_PATH = os.path.join(FONTS_DIR, "BebasNeue-Regular.ttf") + + TITLE_FONT = ImageFont.truetype(TITLE_FONT_PATH, 160) + + # Use available Roboto variants from fonts/ + BOLD_FONT = ImageFont.truetype(os.path.join(FONTS_DIR, "Roboto-Bold.ttf"), 42) + MEDIUM_FONT = ImageFont.truetype(os.path.join(FONTS_DIR, "Roboto-Medium.ttf"), 36) REG_FONT = ImageFont.truetype(os.path.join(FONTS_DIR, "Roboto-Light.ttf"), 30) - GENRE_FONT = ImageFont.truetype(os.path.join(FONTS_DIR, "Roboto-SemiBold.ttf"), 38) -except: + GENRE_FONT = ImageFont.truetype(os.path.join(FONTS_DIR, "Roboto-Medium.ttf"), 38) +except Exception as e: + print(f"Font loading error: {e}") TITLE_FONT = ImageFont.load_default() BOLD_FONT = ImageFont.load_default() MEDIUM_FONT = ImageFont.load_default() @@ -33,62 +40,11 @@ CANVAS_WIDTH, CANVAS_HEIGHT = 1280, 720 # Colors -BG_COLOR = (20, 35, 60) # Bluish Dark Background -HEX_OUTLINE = (40, 55, 85) # Lighter blue outline for background grid TEXT_COLOR = (255, 255, 255) SUBTEXT_COLOR = (210, 210, 210) GENRE_COLOR = (140, 150, 190) # Bluish grey BUTTON_BG = (40, 60, 100) # Blue button bg LOGO_COLOR = (255, 255, 255) -HONEYCOMB_OUTLINE_COLOR = (255, 255, 255) -HONEYCOMB_STROKE = 6 - -# Helper Functions -def draw_regular_polygon(draw, center, radius, n_sides=6, rotation=30, fill=None, outline=None, width=1): - points = [] - for i in range(n_sides): - angle = math.radians(rotation + 360 / n_sides * i) - x = center[0] + radius * math.cos(angle) - y = center[1] + radius * math.sin(angle) - points.append((x, y)) - if fill: - draw.polygon(points, fill=fill) - if outline: - draw.polygon(points, outline=outline, width=width) - -def generate_hex_background(): - # Use the hex_bg.png from fonts folder if available - hex_bg_path = os.path.join(FONTS_DIR, "hex_bg.png") - if os.path.exists(hex_bg_path): - try: - return Image.open(hex_bg_path).convert("RGBA") - except Exception: - pass # Fall through to programmatic generation - - # Otherwise generate programmatically - img = Image.new("RGBA", (CANVAS_WIDTH, CANVAS_HEIGHT), BG_COLOR) - draw = ImageDraw.Draw(img) - - # Background Grid - hex_radius = 55 - import math - dx = math.sqrt(3) * hex_radius - dy = 1.5 * hex_radius - - cols = int(CANVAS_WIDTH / dx) + 2 - rows = int(CANVAS_HEIGHT / dy) + 2 - - for row in range(rows): - for col in range(cols): - cx = col * dx - cy = row * dy - if row % 2 == 1: - cx += dx / 2 - - # Draw faint outline - draw_regular_polygon(draw, (cx, cy), hex_radius, outline=HEX_OUTLINE, width=2) - - return img def wrap_text(text, font, max_width): avg_char_width = font.getlength('x') @@ -96,50 +52,135 @@ def wrap_text(text, font, max_width): wrapped = textwrap.fill(text, width=chars_per_line) return wrapped.split('\n') +def add_gradient(image): + # Add a horizontal gradient from black (left) to transparent (right) + # to make text readable on the left side + width, height = image.size + gradient = Image.new('L', (width, height), color=0) + draw = ImageDraw.Draw(gradient) + + # Create gradient: 0 to 255 (left to right) + # But we want opaque black on left (255 alpha) fading to transparent (0 alpha) + # So we draw opacity mask + + for x in range(width): + # Opacity decreases as x increases + # Start fully opaque (255) until x=600, then fade to 0 by x=1000 + if x < 400: + alpha = 240 # Very dark on left + elif x < 900: + # Linear fade + alpha = int(240 * (1 - (x - 400) / 500)) + else: + alpha = 0 + + draw.line([(x, 0), (x, height)], fill=alpha) + + # Create black layer + black_layer = Image.new("RGBA", (width, height), (0, 0, 0, 0)) + # Apply gradient as alpha + black_layer.putalpha(gradient) + + # Paste/Composite + image.paste(black_layer, (0, 0), black_layer) + return image + def generate_thumbnail(anime): title = anime['title']['english'] or anime['title']['romaji'] poster_url = anime['coverImage']['extraLarge'] score = anime['averageScore'] genres = anime['genres'][:3] desc = (anime['description'] or "").replace("
", " ").replace("", "").replace("", "") - desc = " ".join(desc.split()[:40]) + "..." # Shorter desc for larger text + desc = " ".join(desc.split()[:40]) + "..." - # Background - bg = generate_hex_background() + # 1. Prepare Background + if os.path.exists("background.jpg"): + bg = Image.open("background.jpg").convert("RGBA") + bg = bg.resize((CANVAS_WIDTH, CANVAS_HEIGHT), Image.Resampling.LANCZOS) + else: + # Fallback to dark blue if missing + bg = Image.new("RGBA", (CANVAS_WIDTH, CANVAS_HEIGHT), (20, 35, 60)) + + # Apply gradient to background for text visibility + bg = add_gradient(bg) canvas = bg.copy() draw = ImageDraw.Draw(canvas) - # 1. Logo (Top Left) + # 2. Prepare Poster & Mask + try: + poster_resp = requests.get(poster_url) + poster = Image.open(BytesIO(poster_resp.content)).convert("RGBA") + + # Resize poster to fill canvas (keeping aspect ratio, crop center/top) + # We want the poster to fill the mask area which is mostly on the right/center + aspect = poster.width / poster.height + target_aspect = CANVAS_WIDTH / CANVAS_HEIGHT + + if aspect > target_aspect: + new_height = CANVAS_HEIGHT + new_width = int(new_height * aspect) + poster = poster.resize((new_width, new_height), Image.Resampling.LANCZOS) + # Center crop + left = (new_width - CANVAS_WIDTH) // 2 + poster = poster.crop((left, 0, left + CANVAS_WIDTH, new_height)) + else: + new_width = CANVAS_WIDTH + new_height = int(new_width / aspect) + poster = poster.resize((new_width, new_height), Image.Resampling.LANCZOS) + # Top crop (faces are usually at top) + poster = poster.crop((0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)) + + # Load PLP mask + if os.path.exists("plp.jpg"): + mask = Image.open("plp.jpg").convert("L") + mask = mask.resize((CANVAS_WIDTH, CANVAS_HEIGHT), Image.Resampling.LANCZOS) + + # Apply mask + # Paste poster onto canvas using mask + canvas.paste(poster, (0, 0), mask) + else: + print("Warning: plp.jpg not found, skipping masking") + # Just paste poster on right side as fallback? No, let's keep background. + pass + + except Exception as e: + print(f"Error loading poster: {e}") + + # 3. Draw Text & UI (Left Side) + + # Logo (Top Left) icon_x, icon_y = 50, 40 sz = 18 + # Simple logo icon draw.polygon([(icon_x, icon_y+sz), (icon_x+sz, icon_y), (icon_x+2*sz, icon_y+sz), (icon_x+sz, icon_y+2*sz)], outline=LOGO_COLOR, width=3) draw.polygon([(icon_x+10, icon_y+sz), (icon_x+sz+10, icon_y), (icon_x+2*sz+10, icon_y+sz), (icon_x+sz+10, icon_y+2*sz)], outline=LOGO_COLOR, width=3) draw.text((icon_x + 65, icon_y + 2), "ANIME FLICKER", font=LOGO_FONT, fill=LOGO_COLOR) - # 2. Rating (Below Logo) + # Rating if score: rating_text = f"{score/10:.1f}+ Rating" draw.text((50, 140), rating_text, font=REG_FONT, fill=TEXT_COLOR) - # 3. Title (Large, Below Rating) + # Title + # Use max width for title title_lines = wrap_text(title.upper(), TITLE_FONT, 700) title_y = 180 - for line in title_lines[:2]: + for line in title_lines[:2]: # Max 2 lines draw.text((50, title_y), line, font=TITLE_FONT, fill=TEXT_COLOR) - title_y += 140 # Height + title_y += 140 - # 4. Genres (Below Title) + # Genres genre_text = ", ".join(genres).upper() draw.text((50, title_y + 10), genre_text, font=GENRE_FONT, fill=GENRE_COLOR) - # 5. Description (Below Genres) + # Description desc_y = title_y + 60 desc_lines = wrap_text(desc, REG_FONT, 580) - for line in desc_lines[:4]: # Fewer lines because text is bigger + for line in desc_lines[:4]: # Max 4 lines draw.text((50, desc_y), line, font=REG_FONT, fill=SUBTEXT_COLOR) desc_y += 42 - # 6. Buttons (Bottom Left) + # Buttons btn_y = 620 btn_width = 210 btn_height = 65 @@ -157,66 +198,6 @@ def generate_thumbnail(anime): text_x = btn2_x + (btn_width - text_w) / 2 draw.text((text_x, btn_y + 12), "JOIN NOW", font=BOLD_FONT, fill=TEXT_COLOR) - - # 7. Right Side Honeycomb Poster - poster_resp = requests.get(poster_url) - poster = Image.open(BytesIO(poster_resp.content)).convert("RGBA") - - start_x = 550 - width = CANVAS_WIDTH - start_x + 100 - height = CANVAS_HEIGHT - - # Resize poster - aspect = poster.width / poster.height - target_aspect = width / height - - if aspect > target_aspect: - new_height = height - new_width = int(new_height * aspect) - poster = poster.resize((new_width, new_height), Image.Resampling.LANCZOS) - left = (new_width - width) // 2 - poster = poster.crop((left, 0, left + width, new_height)) - else: - new_width = width - new_height = int(new_width / aspect) - poster = poster.resize((new_width, new_height), Image.Resampling.LANCZOS) - top = (new_height - height) // 2 - poster = poster.crop((0, top, width, top + height)) - - # Mask Generation - mask = Image.new("L", (width, height), 0) - mask_draw = ImageDraw.Draw(mask) - - overlay = Image.new("RGBA", (width, height), (0,0,0,0)) - overlay_draw = ImageDraw.Draw(overlay) - - hex_radius = 160 - gap = 8 - - dx = math.sqrt(3) * hex_radius - dy = 1.5 * hex_radius - - cols = int(CANVAS_WIDTH / dx) + 2 - rows = int(CANVAS_HEIGHT / dy) + 2 - - for row in range(-1, rows): - for col in range(-1, cols): - global_cx = col * dx - if row % 2 == 1: - global_cx += dx / 2 - global_cy = row * dy - - local_cx = global_cx - start_x - local_cy = global_cy - - if global_cx > 650: - draw_regular_polygon(mask_draw, (local_cx, local_cy), hex_radius - gap, fill=255) - draw_regular_polygon(overlay_draw, (local_cx, local_cy), hex_radius - gap, outline=HONEYCOMB_OUTLINE_COLOR, width=HONEYCOMB_STROKE) - - poster.putalpha(mask) - canvas.paste(poster, (start_x, 0), poster) - canvas.paste(overlay, (start_x, 0), overlay) - final = BytesIO() canvas.convert("RGB").save(final, "PNG") final.seek(0)