|
2 | 2 | from PIL import Image, ImageDraw, ImageFont |
3 | 3 |
|
4 | 4 | def create_favicon(): |
5 | | - size = 1024 |
6 | | - bg_color = '#020617' # Site primary background (bg-darker) |
7 | | - text_color = '#ffffff' |
| 5 | + target_dir = os.path.join("assets", "favicon") |
| 6 | + os.makedirs(target_dir, exist_ok=True) |
| 7 | + |
| 8 | + bg_color = (2, 6, 23) # #020617 |
| 9 | + text_color = (255, 255, 255) # #ffffff |
| 10 | + |
| 11 | + # --------------------------------------------------------- |
| 12 | + # 1) Generate the 16x16 PIXEL-PERFECT grid to avoid ALL blur |
| 13 | + # --------------------------------------------------------- |
| 14 | + pixel_map = [ |
| 15 | + "0000000000000000", |
| 16 | + "0000000000000000", |
| 17 | + "0000000000000000", |
| 18 | + "0000000000000000", |
| 19 | + "0000000000000000", |
| 20 | + "0011101110011100", |
| 21 | + "0001001000100000", |
| 22 | + "0001001110101100", |
| 23 | + "0001000010100100", |
| 24 | + "0001001110011100", |
| 25 | + "0000000000000000", |
| 26 | + "0000000000000000", |
| 27 | + "0000000000000000", |
| 28 | + "0000000000000000", |
| 29 | + "0000000000000000", |
| 30 | + "0000000000000000" |
| 31 | + ] |
| 32 | + |
| 33 | + img16 = Image.new('RGB', (16, 16), color=bg_color) |
| 34 | + for y, row in enumerate(pixel_map): |
| 35 | + for x, char in enumerate(row): |
| 36 | + if char == '1': |
| 37 | + img16.putpixel((x, y), text_color) |
| 38 | + |
| 39 | + # Save the perfect 16x16 |
| 40 | + img16.save(os.path.join(target_dir, "favicon-16x16.png")) |
8 | 41 |
|
| 42 | + # Scale up mathematically exactly for 32x32 so it remains perfectly crisp |
| 43 | + img32 = img16.resize((32, 32), Image.Resampling.NEAREST) |
| 44 | + img32.save(os.path.join(target_dir, "favicon-32x32.png")) |
| 45 | + |
| 46 | + # --------------------------------------------------------- |
| 47 | + # 2) Generate the 512x512 High-Res Icons using standard vectors |
| 48 | + # --------------------------------------------------------- |
| 49 | + size = 1024 |
9 | 50 | img = Image.new('RGB', (size, size), color=bg_color) |
10 | 51 | draw = ImageDraw.Draw(img) |
11 | 52 |
|
12 | | - # Try Verdana Bold for maximum clarity at tiny sizes |
13 | 53 | try: |
14 | | - font = ImageFont.truetype("C:\\Windows\\Fonts\\verdanab.ttf", int(size * 0.45)) |
15 | | - except IOError: |
16 | 54 | font = ImageFont.truetype("C:\\Windows\\Fonts\\arialbd.ttf", int(size * 0.45)) |
| 55 | + except IOError: |
| 56 | + font = ImageFont.load_default() |
17 | 57 |
|
18 | | - text = "TSG" |
19 | | - |
20 | | - # Get accurate bounding box |
| 58 | + text = "T S G" |
21 | 59 | left, top, right, bottom = draw.textbbox((0, 0), text, font=font) |
22 | 60 | text_w = right - left |
23 | 61 | text_h = bottom - top |
24 | 62 |
|
25 | | - # Center mathematically exactly on the text pixels |
| 63 | + if text_w > size * 0.9: |
| 64 | + scale = (size * 0.9) / text_w |
| 65 | + font = ImageFont.truetype("C:\\Windows\\Fonts\\arialbd.ttf", int(size * 0.45 * scale)) |
| 66 | + left, top, right, bottom = draw.textbbox((0, 0), text, font=font) |
| 67 | + text_w = right - left |
| 68 | + text_h = bottom - top |
| 69 | + |
26 | 70 | x = (size - text_w) / 2 - left |
27 | 71 | y = (size - text_h) / 2 - top |
28 | | - |
29 | 72 | draw.text((x, y), text, fill=text_color, font=font) |
30 | | - |
31 | | - target_dir = os.path.join("assets", "favicon") |
32 | | - os.makedirs(target_dir, exist_ok=True) |
33 | 73 |
|
34 | 74 | resample = Image.Resampling.LANCZOS |
35 | 75 |
|
36 | 76 | img.resize((512, 512), resample).save(os.path.join(target_dir, "android-chrome-512x512.png")) |
37 | 77 | img.resize((192, 192), resample).save(os.path.join(target_dir, "android-chrome-192x192.png")) |
38 | 78 | img.resize((180, 180), resample).save(os.path.join(target_dir, "apple-touch-icon.png")) |
39 | | - img.resize((32, 32), resample).save(os.path.join(target_dir, "favicon-32x32.png")) |
40 | 79 |
|
41 | | - # Sharp downscale for 16x16 |
42 | | - img16 = img.resize((128, 128), resample).resize((16, 16), resample) |
43 | | - img16.save(os.path.join(target_dir, "favicon-16x16.png")) |
44 | | - |
45 | | - # generate ico |
| 80 | + # Multi-size ICO: 16 (pixel art), 32 (pixel art), 48 (lanczos) |
| 81 | + img48 = img.resize((48, 48), resample) |
46 | 82 | icon_sizes = [(16, 16), (32, 32), (48, 48)] |
47 | | - # Use the 48x48 from lanczos for the base of ico |
48 | | - icon_img = img.resize((48, 48), resample) |
49 | | - img.save(os.path.join(target_dir, "favicon.ico"), format='ICO', sizes=icon_sizes) |
50 | | - |
51 | | - # Re-write the manifest to match the new background color |
52 | | - manifest = """{ |
53 | | - "name": "TechStack Global", |
54 | | - "short_name": "TSG", |
55 | | - "icons": [ |
56 | | - { |
57 | | - "src": "/assets/favicon/android-chrome-192x192.png", |
58 | | - "sizes": "192x192", |
59 | | - "type": "image/png" |
60 | | - }, |
61 | | - { |
62 | | - "src": "/assets/favicon/android-chrome-512x512.png", |
63 | | - "sizes": "512x512", |
64 | | - "type": "image/png" |
65 | | - } |
66 | | - ], |
67 | | - "theme_color": "#020617", |
68 | | - "background_color": "#020617", |
69 | | - "display": "standalone" |
70 | | -}""" |
71 | | - with open(os.path.join(target_dir, "site.webmanifest"), "w") as f: |
72 | | - f.write(manifest) |
73 | | - |
| 83 | + |
| 84 | + # We can create a multi-sized ICO by passing multiple images to the save method, |
| 85 | + # or let PIL downscale from img. Since we want our custom 16 and 32, we should use append_images. |
| 86 | + img16.save(os.path.join(target_dir, "favicon.ico"), format='ICO', sizes=icon_sizes, append_images=[img32, img48]) |
| 87 | + |
74 | 88 | if __name__ == "__main__": |
75 | 89 | create_favicon() |
76 | | - print("Favicons re-generated perfectly!") |
| 90 | + print("Favicons generated with True Pixel-Perfect precision!") |
0 commit comments