1313
1414from PIL import Image , ImageFilter
1515
16- # Different raster sizes to include in the ICO file.
17- SIZES = [16 , 24 , 32 , 48 , 64 , 128 , 256 ]
16+ # Colors matching the SVG design
17+ BG = (224 , 232 , 240 , 255 ) # #e0e8f0
18+ FG = (42 , 53 , 69 , 255 ) # #2a3545
19+
20+ RASTER_SIZES = [16 , 24 , 32 , 48 , 64 , 128 , 256 ]
1821
19- # Sharpen smaller sizes to improve ledgibility.
2022SHARPEN_AMOUNT = {
21- 16 : 200 ,
22- 24 : 200 ,
23- 32 : 100 ,
24- 48 : 50 ,
25- 64 : 50 ,
23+ 24 : 75 ,
24+ 32 : 75 ,
25+ 48 : 75 ,
26+ 64 : 100 ,
27+ 128 : 150 ,
28+ 256 : 150 ,
2629}
2730
31+ SHARPEN_RADIUS = 0.5
32+
2833DIST_DIR = Path (__file__ ).resolve ().parent
29- SVG_PATH = DIST_DIR / "pyscenedetect.svg "
34+ LOGO_DIR = DIST_DIR / "logo "
3035ICO_PATH = DIST_DIR / "pyscenedetect.ico"
3136
37+ SVG_FOR_SIZE : dict [int , Path ] = {
38+ 24 : LOGO_DIR / "pyscenedetect-24-48.svg" ,
39+ 32 : LOGO_DIR / "pyscenedetect-32.svg" ,
40+ 48 : LOGO_DIR / "pyscenedetect-24-48.svg" ,
41+ 64 : LOGO_DIR / "pyscenedetect-64+.svg" ,
42+ 128 : LOGO_DIR / "pyscenedetect-64+.svg" ,
43+ 256 : LOGO_DIR / "pyscenedetect-64+.svg" ,
44+ }
45+
46+
47+ def make_icon_16 () -> Image .Image :
48+ """Create a hand-crafted 16x16 clapperboard icon."""
49+ img = Image .new ("RGBA" , (16 , 16 ), FG )
50+ px = img .load ()
51+
52+ # Clear 1px padding on all sides
53+ for i in range (16 ):
54+ px [0 , i ] = BG
55+ px [15 , i ] = BG
56+ px [i , 0 ] = BG
57+ px [i , 15 ] = BG
58+
59+ # Arm stripe gaps (rows 2–4): clear pixels not part of a complete stripe.
60+ # A stripe x+y=s spans all 3 arm rows only when 5 <= s <= 16.
61+ for y in range (2 , 5 ):
62+ for x in range (1 , 15 ):
63+ if y < 4 and x < 3 :
64+ continue
65+ if y > 2 and x > 12 :
66+ continue
67+ if not ((x + y ) % 4 < 2 and 5 <= (x + y ) <= 16 ):
68+ px [x , y ] = BG
69+
70+ # Slate interior (rows 8–12, cols 3–12)
71+ for y in range (8 , 13 ):
72+ for x in range (3 , 13 ):
73+ px [x , y ] = BG
74+
75+ return img
76+
3277
3378def find_inkscape () -> str :
3479 """Find the Inkscape executable."""
@@ -55,15 +100,21 @@ def render_svg(inkscape: str, svg: Path, output: Path, size: int):
55100def render_all_sizes (inkscape : str , work_dir : Path ) -> list [Image .Image ]:
56101 """Render the SVG at all icon sizes, applying sharpening where configured."""
57102 images = []
58- for size in SIZES :
103+ for size in RASTER_SIZES :
59104 png_path = work_dir / f"icon_{ size } .png"
60- print (f" Rendering { size } x{ size } ..." )
61- render_svg (inkscape , SVG_PATH , png_path , size )
62- img = Image .open (png_path ).copy ()
63- if size in SHARPEN_AMOUNT :
64- img = img .filter (ImageFilter .UnsharpMask (radius = 0.5 , percent = SHARPEN_AMOUNT [size ], threshold = 0 ))
65- print (f" Sharpened { size } x{ size } (USM { SHARPEN_AMOUNT [size ]} %)" )
105+ if size == 16 :
106+ print (f" Using hand-crafted { size } x{ size } icon..." )
107+ img = make_icon_16 ()
66108 img .save (png_path )
109+ else :
110+ svg_path = SVG_FOR_SIZE [size ]
111+ print (f" Rendering { size } x{ size } using { svg_path .name } ..." )
112+ render_svg (inkscape , svg_path , png_path , size )
113+ img = Image .open (png_path ).copy ()
114+ if size in SHARPEN_AMOUNT :
115+ img = img .filter (ImageFilter .UnsharpMask (radius = SHARPEN_RADIUS , percent = SHARPEN_AMOUNT [size ], threshold = 0 ))
116+ print (f" Sharpened { size } x{ size } (USM { SHARPEN_AMOUNT [size ]} %)" )
117+ img .save (png_path )
67118 images .append (img )
68119 return images
69120
@@ -76,7 +127,7 @@ def main():
76127
77128 inkscape = find_inkscape ()
78129 print (f"Using Inkscape: { inkscape } " )
79- print (f"Input SVG : { SVG_PATH } " )
130+ print (f"Logo directory : { LOGO_DIR } " )
80131
81132 ctx = contextlib .nullcontext (str (persist_dir )) if persist_dir else tempfile .TemporaryDirectory ()
82133 with ctx as work :
0 commit comments