305 lines
9.5 KiB
Python
305 lines
9.5 KiB
Python
"""
|
||
fcco-Converter V2 – Icon-Modul
|
||
|
||
Erzeugt saubere, professionelle Icons programmatisch mit PIL.
|
||
Keine externen Bilddateien nötig – alle Icons sind Vektorgrafiken,
|
||
die in beliebiger Größe gerendert werden können.
|
||
"""
|
||
|
||
from PIL import Image, ImageDraw
|
||
import customtkinter as ctk
|
||
|
||
|
||
def _create_canvas(size: int = 24, bg: str | None = None) -> tuple[Image.Image, ImageDraw.ImageDraw]:
|
||
"""Erstellt eine transparente Zeichenfläche."""
|
||
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||
draw = ImageDraw.Draw(img, "RGBA")
|
||
return img, draw
|
||
|
||
|
||
def _hex_to_rgba(hex_color: str, alpha: int = 255) -> tuple[int, int, int, int]:
|
||
"""Konvertiert einen Hex-Farbwert zu RGBA."""
|
||
hex_color = hex_color.lstrip("#")
|
||
r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
|
||
return (r, g, b, alpha)
|
||
|
||
|
||
def make_ctk_image(light_img: Image.Image, dark_img: Image.Image | None = None, size: tuple[int, int] = (20, 20)) -> ctk.CTkImage:
|
||
"""Erzeugt ein CTkImage aus PIL-Bildern."""
|
||
return ctk.CTkImage(
|
||
light_image=light_img,
|
||
dark_image=dark_img or light_img,
|
||
size=size,
|
||
)
|
||
|
||
|
||
# =========================================================================
|
||
# Icon-Generatoren
|
||
# =========================================================================
|
||
|
||
def icon_folder(color: str = "#e2e8f0", size: int = 24) -> Image.Image:
|
||
"""Ordner-Icon."""
|
||
img, draw = _create_canvas(size)
|
||
c = _hex_to_rgba(color)
|
||
s = size
|
||
# Ordner-Körper
|
||
draw.rounded_rectangle(
|
||
[s * 0.08, s * 0.25, s * 0.92, s * 0.85],
|
||
radius=s * 0.08, fill=c
|
||
)
|
||
# Tab oben
|
||
draw.rounded_rectangle(
|
||
[s * 0.08, s * 0.15, s * 0.45, s * 0.35],
|
||
radius=s * 0.06, fill=c
|
||
)
|
||
return img
|
||
|
||
|
||
def icon_file_add(color: str = "#e2e8f0", size: int = 24) -> Image.Image:
|
||
"""Datei-hinzufügen-Icon."""
|
||
img, draw = _create_canvas(size)
|
||
c = _hex_to_rgba(color)
|
||
s = size
|
||
lw = max(1, int(s * 0.08))
|
||
# Dokument
|
||
draw.rounded_rectangle(
|
||
[s * 0.17, s * 0.04, s * 0.71, s * 0.96],
|
||
radius=s * 0.06, outline=c, width=lw
|
||
)
|
||
# Plus-Zeichen
|
||
cx, cy = s * 0.44, s * 0.50
|
||
arm = s * 0.15
|
||
draw.line([(cx - arm, cy), (cx + arm, cy)], fill=c, width=lw)
|
||
draw.line([(cx, cy - arm), (cx, cy + arm)], fill=c, width=lw)
|
||
return img
|
||
|
||
|
||
def icon_play(color: str = "#e2e8f0", size: int = 24) -> Image.Image:
|
||
"""Play/Start-Icon (Dreieck)."""
|
||
img, draw = _create_canvas(size)
|
||
c = _hex_to_rgba(color)
|
||
s = size
|
||
# Dreieck (nach rechts zeigend)
|
||
points = [
|
||
(s * 0.25, s * 0.15),
|
||
(s * 0.80, s * 0.50),
|
||
(s * 0.25, s * 0.85),
|
||
]
|
||
draw.polygon(points, fill=c)
|
||
return img
|
||
|
||
|
||
def icon_settings(color: str = "#e2e8f0", size: int = 24) -> Image.Image:
|
||
"""Zahnrad/Einstellungen-Icon."""
|
||
img, draw = _create_canvas(size)
|
||
c = _hex_to_rgba(color)
|
||
s = size
|
||
lw = max(1, int(s * 0.08))
|
||
cx, cy = s * 0.5, s * 0.5
|
||
# Äußerer Ring
|
||
r_outer = s * 0.38
|
||
draw.ellipse(
|
||
[cx - r_outer, cy - r_outer, cx + r_outer, cy + r_outer],
|
||
outline=c, width=lw
|
||
)
|
||
# Innerer Punkt
|
||
r_inner = s * 0.12
|
||
draw.ellipse(
|
||
[cx - r_inner, cy - r_inner, cx + r_inner, cy + r_inner],
|
||
fill=c
|
||
)
|
||
# Zähne (6 Stück als kurze Linien)
|
||
import math
|
||
for i in range(6):
|
||
angle = math.pi * i / 3
|
||
x1 = cx + r_outer * math.cos(angle)
|
||
y1 = cy + r_outer * math.sin(angle)
|
||
x2 = cx + (r_outer + s * 0.12) * math.cos(angle)
|
||
y2 = cy + (r_outer + s * 0.12) * math.sin(angle)
|
||
draw.line([(x1, y1), (x2, y2)], fill=c, width=lw)
|
||
return img
|
||
|
||
|
||
def icon_refresh(color: str = "#e2e8f0", size: int = 24) -> Image.Image:
|
||
"""Refresh/Update-Icon (kreisförmiger Pfeil)."""
|
||
img, draw = _create_canvas(size)
|
||
c = _hex_to_rgba(color)
|
||
s = size
|
||
lw = max(1, int(s * 0.08))
|
||
# Kreisbogen
|
||
draw.arc(
|
||
[s * 0.15, s * 0.15, s * 0.85, s * 0.85],
|
||
start=30, end=330, fill=c, width=lw
|
||
)
|
||
# Pfeilspitze
|
||
import math
|
||
angle = math.radians(30)
|
||
cx, cy = s * 0.5, s * 0.5
|
||
r = s * 0.35
|
||
tip_x = cx + r * math.cos(angle)
|
||
tip_y = cy - r * math.sin(angle)
|
||
arrow_size = s * 0.15
|
||
points = [
|
||
(tip_x, tip_y),
|
||
(tip_x + arrow_size, tip_y + arrow_size * 0.3),
|
||
(tip_x + arrow_size * 0.3, tip_y + arrow_size),
|
||
]
|
||
draw.polygon(points, fill=c)
|
||
return img
|
||
|
||
|
||
def icon_save(color: str = "#e2e8f0", size: int = 24) -> Image.Image:
|
||
"""Speichern-Icon (Diskette)."""
|
||
img, draw = _create_canvas(size)
|
||
c = _hex_to_rgba(color)
|
||
s = size
|
||
lw = max(1, int(s * 0.08))
|
||
# Äußeres Rechteck
|
||
draw.rounded_rectangle(
|
||
[s * 0.10, s * 0.10, s * 0.90, s * 0.90],
|
||
radius=s * 0.06, outline=c, width=lw
|
||
)
|
||
# Inneres Rechteck (Etikett)
|
||
draw.rectangle(
|
||
[s * 0.30, s * 0.55, s * 0.70, s * 0.85],
|
||
outline=c, width=lw
|
||
)
|
||
# Oberer Bereich (Schieber)
|
||
draw.rectangle(
|
||
[s * 0.30, s * 0.10, s * 0.70, s * 0.38],
|
||
fill=c
|
||
)
|
||
return img
|
||
|
||
|
||
def icon_browse(color: str = "#e2e8f0", size: int = 24) -> Image.Image:
|
||
"""Durchsuchen-Icon (Lupe)."""
|
||
img, draw = _create_canvas(size)
|
||
c = _hex_to_rgba(color)
|
||
s = size
|
||
lw = max(1, int(s * 0.08))
|
||
# Kreis
|
||
draw.ellipse(
|
||
[s * 0.12, s * 0.08, s * 0.62, s * 0.58],
|
||
outline=c, width=lw
|
||
)
|
||
# Griff
|
||
draw.line(
|
||
[(s * 0.55, s * 0.55), (s * 0.88, s * 0.88)],
|
||
fill=c, width=max(2, int(s * 0.10))
|
||
)
|
||
return img
|
||
|
||
|
||
def icon_check(color: str = "#34d399", size: int = 24) -> Image.Image:
|
||
"""Häkchen-Icon."""
|
||
img, draw = _create_canvas(size)
|
||
c = _hex_to_rgba(color)
|
||
s = size
|
||
lw = max(2, int(s * 0.10))
|
||
draw.line(
|
||
[(s * 0.18, s * 0.52), (s * 0.40, s * 0.75), (s * 0.82, s * 0.25)],
|
||
fill=c, width=lw, joint="curve"
|
||
)
|
||
return img
|
||
|
||
|
||
def icon_cross(color: str = "#f87171", size: int = 24) -> Image.Image:
|
||
"""Kreuz/Fehler-Icon."""
|
||
img, draw = _create_canvas(size)
|
||
c = _hex_to_rgba(color)
|
||
s = size
|
||
lw = max(2, int(s * 0.10))
|
||
draw.line([(s * 0.22, s * 0.22), (s * 0.78, s * 0.78)], fill=c, width=lw)
|
||
draw.line([(s * 0.78, s * 0.22), (s * 0.22, s * 0.78)], fill=c, width=lw)
|
||
return img
|
||
|
||
|
||
def icon_music(color: str = "#e2e8f0", size: int = 24) -> Image.Image:
|
||
"""Musik/Audio-Icon (Notensymbol)."""
|
||
img, draw = _create_canvas(size)
|
||
c = _hex_to_rgba(color)
|
||
s = size
|
||
lw = max(1, int(s * 0.08))
|
||
# Notenhals
|
||
draw.line([(s * 0.60, s * 0.15), (s * 0.60, s * 0.70)], fill=c, width=lw)
|
||
# Notenkopf
|
||
draw.ellipse(
|
||
[s * 0.35, s * 0.60, s * 0.65, s * 0.85],
|
||
fill=c
|
||
)
|
||
# Fähnchen
|
||
draw.arc(
|
||
[s * 0.55, s * 0.12, s * 0.88, s * 0.45],
|
||
start=270, end=90, fill=c, width=lw
|
||
)
|
||
return img
|
||
|
||
|
||
def icon_info(color: str = "#e2e8f0", size: int = 24) -> Image.Image:
|
||
"""Info-Icon (i im Kreis)."""
|
||
img, draw = _create_canvas(size)
|
||
c = _hex_to_rgba(color)
|
||
s = size
|
||
lw = max(1, int(s * 0.08))
|
||
# Kreis
|
||
draw.ellipse(
|
||
[s * 0.10, s * 0.10, s * 0.90, s * 0.90],
|
||
outline=c, width=lw
|
||
)
|
||
# Punkt oben
|
||
draw.ellipse(
|
||
[s * 0.44, s * 0.22, s * 0.56, s * 0.34],
|
||
fill=c
|
||
)
|
||
# Strich unten
|
||
draw.line([(s * 0.50, s * 0.42), (s * 0.50, s * 0.75)], fill=c, width=lw)
|
||
return img
|
||
|
||
|
||
def icon_download(color: str = "#e2e8f0", size: int = 24) -> Image.Image:
|
||
"""Download-Icon (Pfeil nach unten)."""
|
||
img, draw = _create_canvas(size)
|
||
c = _hex_to_rgba(color)
|
||
s = size
|
||
lw = max(1, int(s * 0.08))
|
||
# Vertikale Linie
|
||
draw.line([(s * 0.50, s * 0.12), (s * 0.50, s * 0.62)], fill=c, width=lw)
|
||
# Pfeilspitze
|
||
draw.line([(s * 0.30, s * 0.48), (s * 0.50, s * 0.68)], fill=c, width=lw)
|
||
draw.line([(s * 0.70, s * 0.48), (s * 0.50, s * 0.68)], fill=c, width=lw)
|
||
# Unterstrich
|
||
draw.line([(s * 0.20, s * 0.85), (s * 0.80, s * 0.85)], fill=c, width=lw)
|
||
return img
|
||
|
||
|
||
# =========================================================================
|
||
# Vorgefertigte CTkImage-Sets (Light + Dark)
|
||
# =========================================================================
|
||
|
||
class IconSet:
|
||
"""Hält alle Icons als CTkImage-Instanzen.
|
||
Muss nach CTk-Initialisierung erstellt werden.
|
||
"""
|
||
|
||
def __init__(self, size: tuple[int, int] = (18, 18)):
|
||
s = size[0]
|
||
light = "#3f3f46" # Für hellen Modus
|
||
dark = "#e2e8f0" # Für dunklen Modus
|
||
accent = "#8b7cf6"
|
||
success = "#34d399"
|
||
error = "#f87171"
|
||
|
||
self.folder = make_ctk_image(icon_folder(light, s), icon_folder(dark, s), size)
|
||
self.file_add = make_ctk_image(icon_file_add(light, s), icon_file_add(dark, s), size)
|
||
self.play = make_ctk_image(icon_play(light, s), icon_play(dark, s), size)
|
||
self.settings = make_ctk_image(icon_settings(light, s), icon_settings(dark, s), size)
|
||
self.refresh = make_ctk_image(icon_refresh(light, s), icon_refresh(dark, s), size)
|
||
self.save = make_ctk_image(icon_save(light, s), icon_save(dark, s), size)
|
||
self.browse = make_ctk_image(icon_browse(light, s), icon_browse(dark, s), size)
|
||
self.check = make_ctk_image(icon_check(success, s), icon_check(success, s), size)
|
||
self.cross = make_ctk_image(icon_cross(error, s), icon_cross(error, s), size)
|
||
self.music = make_ctk_image(icon_music(light, s), icon_music(dark, s), size)
|
||
self.info = make_ctk_image(icon_info(light, s), icon_info(dark, s), size)
|
||
self.download = make_ctk_image(icon_download(light, s), icon_download(dark, s), size)
|