|
| 1 | +import base64 |
| 2 | +import io |
| 3 | +from PIL import Image, ImageDraw |
| 4 | + |
| 5 | +# 1. Load source images |
| 6 | +logo_black = Image.open('public/ClientEcho_logo.png').convert('RGBA') |
| 7 | +logo_white = Image.open('public/ClientEcho_logo_white.png').convert('RGBA') |
| 8 | +w, h = logo_black.size |
| 9 | + |
| 10 | +# 2. Build High-DPI Obsidian Squircle Icons (Optimized for Google Search 48x48 multiples, Chrome, & Apple Touch) |
| 11 | +def create_squircle_favicon(size, output_path): |
| 12 | + scale = 4 |
| 13 | + hr_size = size * scale |
| 14 | + bg = Image.new('RGBA', (hr_size, hr_size), (0, 0, 0, 0)) |
| 15 | + draw = ImageDraw.Draw(bg) |
| 16 | + radius = int(hr_size * 0.225) |
| 17 | + |
| 18 | + # Modern dark obsidian tile (#111116) |
| 19 | + draw.rounded_rectangle([(0, 0), (hr_size - 1, hr_size - 1)], radius=radius, fill=(17, 17, 22, 255)) |
| 20 | + |
| 21 | + # White logo glyph with ~15% optical margin |
| 22 | + glyph_size = int(hr_size * 0.70) |
| 23 | + glyph = logo_white.resize((glyph_size, glyph_size), Image.Resampling.LANCZOS) |
| 24 | + offset = (hr_size - glyph_size) // 2 |
| 25 | + bg.paste(glyph, (offset, offset), glyph) |
| 26 | + |
| 27 | + final_img = bg.resize((size, size), Image.Resampling.LANCZOS) |
| 28 | + final_img.save(output_path, 'PNG', optimize=True) |
| 29 | + print(f'Created {output_path} ({size}x{size})') |
| 30 | + return final_img |
| 31 | + |
| 32 | +# Google Search multiples of 48px + standard web sizes |
| 33 | +create_squircle_favicon(48, 'public/favicon-48x48.png') |
| 34 | +create_squircle_favicon(96, 'public/favicon-96x96.png') |
| 35 | +create_squircle_favicon(144, 'public/favicon-144x144.png') |
| 36 | +create_squircle_favicon(192, 'public/favicon-192x192.png') |
| 37 | +create_squircle_favicon(512, 'public/favicon-512x512.png') |
| 38 | +ico_source = create_squircle_favicon(180, 'public/apple-touch-icon.png') |
| 39 | + |
| 40 | +# Generate multi-resolution .ico for Googlebot & standard browser root |
| 41 | +ico_source.save('public/favicon.ico', format='ICO', sizes=[(16, 16), (32, 32), (48, 48)]) |
| 42 | +print('Created public/favicon.ico (16/32/48)') |
| 43 | + |
| 44 | +# 3. Create Adaptive SVG favicon for modern browsers |
| 45 | +buf_black = io.BytesIO() |
| 46 | +logo_black.save(buf_black, format='PNG') |
| 47 | +b64_black = base64.b64encode(buf_black.getvalue()).decode('utf-8') |
| 48 | + |
| 49 | +buf_white = io.BytesIO() |
| 50 | +logo_white.save(buf_white, format='PNG') |
| 51 | +b64_white = base64.b64encode(buf_white.getvalue()).decode('utf-8') |
| 52 | + |
| 53 | +svg_content = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" width="{w}" height="{h}"> |
| 54 | + <style> |
| 55 | + .icon-light {{ display: block; }} |
| 56 | + .icon-dark {{ display: none; }} |
| 57 | + @media (prefers-color-scheme: dark) {{ |
| 58 | + .icon-light {{ display: none; }} |
| 59 | + .icon-dark {{ display: block; }} |
| 60 | + }} |
| 61 | + </style> |
| 62 | + <image class="icon-light" href="data:image/png;base64,{b64_black}" width="{w}" height="{h}" /> |
| 63 | + <image class="icon-dark" href="data:image/png;base64,{b64_white}" width="{w}" height="{h}" /> |
| 64 | +</svg>''' |
| 65 | + |
| 66 | +with open('public/favicon.svg', 'w', encoding='utf-8') as f: |
| 67 | + f.write(svg_content) |
| 68 | +print('Created public/favicon.svg (Adaptive SVG)') |
0 commit comments