Retour à la Knowledge Base
Breakthrough

desktop-app-icons

Mars 2026·Projet : EVQuest (Tauri v2 + Windows 11)
TauriPython PillowImageMagickWindows

Vécu projet : EVQuest (Tauri v2 + Windows 11) Date : Mars 2026 Stack : Tauri v2, Python Pillow, ImageMagick, Windows 11


Contexte

Problème rencontré sur EVQuest : icône taskbar floue et noircie, alors que l'icône dans la barre de titre de la fenêtre rendait bien.

Diagnostic :

  • PNG source (Refactored-2.png, 628×628) avait alpha=255 partout — fond sombre 100% opaque
  • L'ICO contenait les bonnes tailles mais avec un fond opaque sombre
  • Windows affichait le fond noir dans la taskbar → aspect noirci

Résultat après fix : icône nette dans la taskbar (modes clair et sombre), favicon browser propre, logo sidebar cohérent.


Problème

1. Résolutions manquantes dans le .ico

La taskbar Windows demande du 24×24 (à 100% DPI) ou du 32×32 (à 125%+). Si le .ico ne contient que du 256 et du 32, Windows downscale lui-même avec un bête bilinéaire — résultat flou garanti.

Règle : le .ico doit embarquer explicitement 16, 24, 32, 48, 64, 128, 256.

2. Canal alpha mal géré

C'est la cause du "noirci". Si le PNG source n'a pas un vrai canal alpha propre (fond quasi-transparent mais pas totalement, ou pixels semi-transparents mal encodés), Windows remplace la transparence par du noir. L'icône paraît sombre et sale sur la taskbar.

Règle : le PNG source doit être en PNG-32 avec alpha binaire clean — fond à alpha=0 (100% transparent), éléments à alpha=255 (100% opaque). Pas de pixels à opacité 1-2% qui traînent.

3. Downscale automatique destructeur

Si le logo a des traits fins, du texte ou des effets subtils qui fonctionnent à 256px, ils deviennent une bouillie de pixels à 24px. Windows ne fait pas de "pixel hinting" intelligent.

Règle : pour les petites tailles (≤48px), prévoir une variante glyph épurée — juste le symbole central, traits épaissis, contrastes forts, pas de texte.


Solution

Étape 1 — Inspecter le .ico actuel

Vérifier les résolutions embarquées :

import struct
with open('icon.ico', 'rb') as f:
    _, _, count = struct.unpack('<HHH', f.read(6))
    print(f'{count} images')
    for i in range(count):
        entry = f.read(16)
        w, h = entry[0] or 256, entry[1] or 256
        sz = struct.unpack('<I', entry[8:12])[0]
        print(f'  {w}x{h} ({sz} bytes)')

Ou avec ImageMagick : magick identify icon.ico

Si pas de couche 24×24 → cause principale du flou taskbar.

Étape 2 — Préparer un PNG source irréprochable

  • Taille : 1024×1024 minimum (carré)
  • Format : PNG-32 avec canal alpha clean
  • Fond : 100% transparent (alpha=0), pas "presque transparent"

Pour nettoyer l'alpha d'un logo sur fond sombre, utiliser un BFS flood-fill depuis les bords :

from PIL import Image, ImageFilter
import numpy as np
from collections import deque

src = Image.open('logo.png').convert('RGBA')
arr = np.array(src)
h, w = arr.shape[:2]

# Luminance
lum = 0.299 * arr[:,:,0].astype(float) + 0.587 * arr[:,:,1].astype(float) + 0.114 * arr[:,:,2].astype(float)

# BFS flood-fill depuis les bords — ne supprime QUE le fond extérieur
dark_threshold = 32  # ajuster selon le logo
visited = np.zeros((h, w), dtype=bool)
queue = deque()

# Seed: pixels sombres sur les 4 bords
for y in range(h):
    for x in [0, w-1]:
        if lum[y, x] < dark_threshold:
            queue.append((y, x)); visited[y, x] = True
for x in range(w):
    for y in [0, h-1]:
        if lum[y, x] < dark_threshold and not visited[y, x]:
            queue.append((y, x)); visited[y, x] = True

outer_bg = np.zeros((h, w), dtype=bool)
while queue:
    cy, cx = queue.popleft()
    outer_bg[cy, cx] = True
    for dy, dx in [(-1,0),(1,0),(0,-1),(0,1)]:
        ny, nx = cy+dy, cx+dx
        if 0 <= ny < h and 0 <= nx < w and not visited[ny, nx] and lum[ny, nx] < dark_threshold:
            visited[ny, nx] = True
            queue.append((ny, nx))

# Alpha: 0 pour le fond, 255 pour le reste, feather sur les bords
alpha = np.where(outer_bg, 0, 255).astype(np.uint8)
alpha_img = Image.fromarray(alpha, 'L').filter(ImageFilter.GaussianBlur(radius=1.5))
arr[:,:,3] = np.array(alpha_img)

final = Image.fromarray(arr).resize((1024, 1024), Image.LANCZOS)
final.save('app-icon.png')

Pourquoi BFS et pas un seuil de luminance global ? Un seuil global (luminance < 25 → transparent) supprime aussi les zones sombres intérieures du design (ex: l'intérieur d'un cœur). Le flood-fill depuis les bords ne touche que le fond extérieur connecté aux bords de l'image.

Étape 3 — Générer avec npx tauri icon

npx tauri icon src-tauri/icons/app-icon.png

Cette commande génère automatiquement :

  • .ico multi-résolution (Windows)
  • .icns (macOS)
  • PNGs aux tailles standard (32, 64, 128, 256)
  • Icônes iOS et Android (bonus)

Toujours préférer cette commande à une génération manuelle avec Pillow.

Étape 4 — Si toujours moche en petit

Maintenir 2 variantes du logo :

  • Version complète pour 128px+ (avec détails, effets, texte)
  • Version glyph pour ≤48px (symbole seul, traits épaissis, contrastes forts)

Injecter manuellement les petites tailles avec ImageMagick :

magick convert icon-16.png icon-24.png icon-32.png icon-48.png icon-64.png icon-128.png icon-256.png icon.ico

Étape 5 — Tester le rendu réel

  • Vérifier la taskbar à 100%, 125%, 150% DPI
  • Vider le cache d'icônes Windows si l'ancien rendu persiste :
    ie4uinit.exe -show
    Ou redémarrer l'Explorer Windows.

Pourquoi

npx tauri icon est préféré à une génération manuelle Pillow car il produit automatiquement tous les formats requis (ICO multi-résolution, ICNS, PNGs standard). Le BFS flood-fill depuis les bords est préféré à un seuil de luminance global car ce dernier supprime aussi les zones sombres intérieures du design.


Points d'attention

Piège Détail
append_images ignoré pour ICO Fonctionne pour GIF/WebP/TIFF, mais silencieusement ignoré pour le format ICO. Résultat : un ICO avec une seule taille.
Utiliser sizes= img.save('icon.ico', format='ICO', sizes=[(16,16),...,(256,256)]) — Pillow auto-resize depuis la source.
Conversion RGBA obligatoire img.convert('RGBA') avant de sauver en ICO, sinon crash ou alpha manquant.
Vérification trompeuse Image.open('icon.ico').info['sizes'] ne rapporte que la 1re taille. Lire le header binaire pour vérifier toutes les couches.

Dernière mise à jour : mars 2026