11import os
22import math
3+ import warnings
34from PIL import Image , ImageDraw , ImageFont , ImageFilter
45
6+
7+ def load_fonts ():
8+ """Load title/subtitle fonts with a robust, OS-independent fallback chain.
9+
10+ Tries a bundled font first (works identically on every OS), then falls
11+ back to common Windows/Linux system fonts. If nothing is found, warns
12+ loudly instead of silently rendering unreadable bitmap text.
13+ """
14+ script_dir = os .path .dirname (os .path .abspath (__file__ ))
15+ bundled_font = os .path .join (script_dir , "assets" , "fonts" , "DejaVuSans-Bold.ttf" )
16+
17+ candidates = [
18+ bundled_font , # 1. Repo-bundled font (preferred)
19+ "segoeui.ttf" , # 2. Windows
20+ "arial.ttf" , # 3. Windows fallback
21+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" , # 4. Common Linux path
22+ ]
23+
24+ for path in candidates :
25+ try :
26+ font_title = ImageFont .truetype (path , 36 )
27+ font_subtitle = ImageFont .truetype (path , 16 )
28+ return font_title , font_subtitle
29+ except IOError :
30+ continue
31+
32+ warnings .warn (
33+ "No usable .ttf font found (checked bundled, Windows, and common Linux paths). "
34+ "Falling back to PIL's default bitmap font - banner text will be unreadable. "
35+ "Add a .ttf to assets/fonts/ to fix this." ,
36+ RuntimeWarning ,
37+ stacklevel = 2 ,
38+ )
39+ return ImageFont .load_default (), ImageFont .load_default ()
40+
41+
542def create_radial_gradient (color_center , color_edge ):
643 """Create a high-fidelity smooth radial gradient by resizing a eased 24x24 radial dot."""
744 w , h = 24 , 24
@@ -44,6 +81,12 @@ def draw_perspective_grid(draw, color):
4481
4582def generate_banner (name , category , filename ):
4683 """Generate a highly customized, vector-styled banner for the card."""
84+ # Load fonts once, up front, so they're available to every branch below
85+ # (previously font_title was only loaded near the end, which crashed
86+ # the "matrix" branch with a NameError since it referenced font_title
87+ # before it existed).
88+ font_title , font_subtitle = load_fonts ()
89+
4790 # Tailor color scheme by category
4891 if category == "games" :
4992 color_center = (25 , 10 , 55 )
@@ -485,6 +528,15 @@ def draw_o(ox, oy):
485528 v_draw .ellipse ([cx - 35 , cy - 40 , cx - 15 , cy - 20 ], fill = color_accent ) # toe 1
486529 v_draw .ellipse ([cx - 10 , cy - 50 , cx + 10 , cy - 30 ], fill = color_accent ) # toe 2
487530 v_draw .ellipse ([cx + 15 , cy - 40 , cx + 35 , cy - 20 ], fill = color_accent ) # toe 3
531+ elif "budget" in n_lower :
532+ # Wallet + bar chart for budget tracker
533+ cx , cy = 400 , 225
534+ v_draw .rounded_rectangle ([cx - 90 , cy - 50 , cx + 90 , cy + 50 ], radius = 16 , fill = (255 ,255 ,255 ,12 ), outline = color_accent , width = 3 )
535+ v_draw .rounded_rectangle ([cx + 40 , cy - 15 , cx + 90 , cy + 15 ], radius = 8 , fill = color_accent )
536+ bar_heights = [30 , 55 , 40 , 70 ]
537+ for i , h in enumerate (bar_heights ):
538+ x = 230 + i * 50
539+ v_draw .rectangle ([x , 320 - h , x + 30 , 320 ], fill = color_accent )
488540 else :
489541 # Default nice abstract waves
490542 points = []
@@ -502,18 +554,6 @@ def draw_o(ox, oy):
502554 glass_layer = Image .new ("RGBA" , (800 , 450 ))
503555 gl_draw = ImageDraw .Draw (glass_layer )
504556 gl_draw .rounded_rectangle ([150 , 110 , 650 , 340 ], radius = 24 , fill = (255 , 255 , 255 , 14 ), outline = (255 , 255 , 255 , 45 ), width = 2 )
505-
506- # Load fonts cleanly
507- try :
508- font_title = ImageFont .truetype ("segoeui.ttf" , 36 )
509- font_subtitle = ImageFont .truetype ("segoeui.ttf" , 16 )
510- except IOError :
511- try :
512- font_title = ImageFont .truetype ("arial.ttf" , 36 )
513- font_subtitle = ImageFont .truetype ("arial.ttf" , 16 )
514- except IOError :
515- font_title = ImageFont .load_default ()
516- font_subtitle = ImageFont .load_default ()
517557
518558 # Draw Text Labels
519559 # Shadow text
@@ -527,9 +567,10 @@ def draw_o(ox, oy):
527567
528568 final_img = Image .alpha_composite (composite , glass_layer ).convert ("RGB" )
529569
530- # Save Image
570+ # Save Image (true WebP encoding — filenames use .webp, so save as WebP,
571+ # not JPEG bytes inside a .webp container)
531572 os .makedirs (os .path .dirname (filename ), exist_ok = True )
532- final_img .save (filename , "JPEG " , quality = 90 )
573+ final_img .save (filename , "WEBP " , quality = 90 , method = 6 )
533574 print (f"Generated HD banner: { filename } " )
534575
535576# Project category directory mappings
@@ -540,7 +581,7 @@ def draw_o(ox, oy):
540581 ("Coin Flip" , "games" , "coin-flip.webp" ),
541582 ("Dice Rolling" , "games" , "dice-rolling.webp" ),
542583 ("Dots & Boxes AI" , "games" , "dots-boxes.webp" ),
543- ("Emoji Memory Game" , "games" , "emoji-memory-game.webp" ), # FIXED filename
584+ ("Emoji Memory Game" , "games" , "emoji-memory-game.webp" ),
544585 ("FLAMES Game" , "games" , "flames.webp" ),
545586 ("Flappy Game" , "games" , "flappy-game.webp" ),
546587 ("Hangman" , "games" , "hangman.webp" ),
@@ -580,9 +621,12 @@ def draw_o(ox, oy):
580621 ("Fourier Series" , "math" , "fourier-series.webp" ),
581622
582623 # UTILITIES
624+ # NOTE: "Tower of Hanoi" under utilities removed — it was a duplicate of
625+ # the math entry above and wrote to the same "tower-of-hanoi.webp" path,
626+ # silently overwriting it. If a utilities-specific banner is wanted,
627+ # give it a distinct filename instead (e.g. "tower-of-hanoi-util.webp").
583628 ("Morse Code" , "utilities" , "morse-code.webp" ),
584629 ("Number Converter" , "utilities" , "number-converter.webp" ),
585- ("Tower of Hanoi" , "utilities" , "tower-of-hanoi.webp" ),
586630 ("Typing Speed Tester" , "utilities" , "typing-speed-tester.webp" ),
587631 ("Color Palette Suggestor" , "utilities" , "color-palette.webp" ),
588632 ("AI Resume Analyzer" , "utilities" , "resume-analyzer.webp" ),
@@ -598,4 +642,4 @@ def draw_o(ox, oy):
598642 dest_path = os .path .join (banners_dir , filename )
599643 generate_banner (name , cat , dest_path )
600644
601- print ("All HD banner images generated successfully!" )
645+ print ("All HD banner images generated successfully!" )
0 commit comments