Rename project to ICRS and add folder browsing
This commit is contained in:
+2
-2
@@ -1,5 +1,5 @@
|
||||
"""Application package."""
|
||||
|
||||
from .app import ColorCalcApp, start_app
|
||||
from .app import ICRSApp, start_app
|
||||
|
||||
__all__ = ["ColorCalcApp", "start_app"]
|
||||
__all__ = ["ICRSApp", "start_app"]
|
||||
|
||||
+6
-4
@@ -8,7 +8,7 @@ from .gui import ColorPickerMixin, ExclusionMixin, ThemeMixin, UIBuilderMixin
|
||||
from .logic import DEFAULTS, ImageProcessingMixin, ResetMixin
|
||||
|
||||
|
||||
class ColorCalcApp(
|
||||
class ICRSApp(
|
||||
ThemeMixin,
|
||||
UIBuilderMixin,
|
||||
ImageProcessingMixin,
|
||||
@@ -20,7 +20,7 @@ class ColorCalcApp(
|
||||
|
||||
def __init__(self, root: tk.Tk):
|
||||
self.root = root
|
||||
self.root.title("ColorCalc — Bild + Overlay")
|
||||
self.root.title("ICRS — Interactive Color Range Analyzer")
|
||||
try:
|
||||
self.root.state("zoomed")
|
||||
except Exception:
|
||||
@@ -56,6 +56,8 @@ class ColorCalcApp(
|
||||
self.preview_img = None
|
||||
self.preview_tk = None
|
||||
self.overlay_tk = None
|
||||
self.image_paths = []
|
||||
self.current_image_index = -1
|
||||
|
||||
# Build UI
|
||||
self.setup_ui()
|
||||
@@ -66,8 +68,8 @@ class ColorCalcApp(
|
||||
def start_app() -> None:
|
||||
"""Entry point used by the CLI script."""
|
||||
root = tk.Tk()
|
||||
app = ColorCalcApp(root)
|
||||
app = ICRSApp(root)
|
||||
root.mainloop()
|
||||
|
||||
|
||||
__all__ = ["ColorCalcApp", "start_app"]
|
||||
__all__ = ["ICRSApp", "start_app"]
|
||||
|
||||
@@ -15,6 +15,9 @@ class UIBuilderMixin:
|
||||
toolbar.pack(fill=tk.X, padx=12, pady=8)
|
||||
buttons = [
|
||||
("📂 Bild laden", self.load_image),
|
||||
("📁 Ordner laden", self.load_folder),
|
||||
("⬅️ Bild zurück", self.show_previous_image),
|
||||
("➡️ Nächstes Bild", self.show_next_image),
|
||||
("🎨 Farbe wählen", self.choose_color),
|
||||
("🖱️Farbe aus Bild klicken", self.enable_pick_mode),
|
||||
("💾 Overlay speichern", self.save_overlay),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Logic utilities and mixins for processing and configuration."""
|
||||
|
||||
from .constants import BASE_DIR, DEFAULTS, IMAGES_DIR, PREVIEW_MAX_SIZE
|
||||
from .constants import BASE_DIR, DEFAULTS, IMAGES_DIR, PREVIEW_MAX_SIZE, SUPPORTED_IMAGE_EXTENSIONS
|
||||
from .image_processing import ImageProcessingMixin
|
||||
from .reset import ResetMixin
|
||||
|
||||
@@ -9,6 +9,7 @@ __all__ = [
|
||||
"DEFAULTS",
|
||||
"IMAGES_DIR",
|
||||
"PREVIEW_MAX_SIZE",
|
||||
"SUPPORTED_IMAGE_EXTENSIONS",
|
||||
"ImageProcessingMixin",
|
||||
"ResetMixin",
|
||||
]
|
||||
|
||||
@@ -29,6 +29,8 @@ _DEFAULTS_BASE = {
|
||||
"alpha": 120,
|
||||
}
|
||||
|
||||
SUPPORTED_IMAGE_EXTENSIONS = (".webp", ".png", ".jpg", ".jpeg", ".bmp")
|
||||
|
||||
_DEFAULT_TYPES: dict[str, Callable[[Any], Any]] = {
|
||||
"hue_min": float,
|
||||
"hue_max": float,
|
||||
|
||||
@@ -4,13 +4,13 @@ from __future__ import annotations
|
||||
|
||||
import colorsys
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Tuple
|
||||
from typing import Iterable, Sequence, Tuple
|
||||
|
||||
from tkinter import filedialog, messagebox
|
||||
|
||||
from PIL import Image, ImageDraw, ImageTk
|
||||
|
||||
from .constants import IMAGES_DIR, PREVIEW_MAX_SIZE
|
||||
from .constants import IMAGES_DIR, PREVIEW_MAX_SIZE, SUPPORTED_IMAGE_EXTENSIONS
|
||||
|
||||
|
||||
class ImageProcessingMixin:
|
||||
@@ -22,6 +22,9 @@ class ImageProcessingMixin:
|
||||
preview_tk: ImageTk.PhotoImage | None
|
||||
overlay_tk: ImageTk.PhotoImage | None
|
||||
|
||||
image_paths: list[Path]
|
||||
current_image_index: int
|
||||
|
||||
def load_image(self) -> None:
|
||||
default_dir = IMAGES_DIR if IMAGES_DIR.exists() else Path.cwd()
|
||||
path = filedialog.askopenfilename(
|
||||
@@ -31,21 +34,88 @@ class ImageProcessingMixin:
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
self.image_path = Path(path)
|
||||
self._set_image_collection([Path(path)], 0)
|
||||
|
||||
def load_folder(self) -> None:
|
||||
default_dir = IMAGES_DIR if IMAGES_DIR.exists() else Path.cwd()
|
||||
directory = filedialog.askdirectory(
|
||||
title="Ordner mit Bildern wählen",
|
||||
initialdir=str(default_dir),
|
||||
)
|
||||
if not directory:
|
||||
return
|
||||
folder = Path(directory)
|
||||
if not folder.exists():
|
||||
messagebox.showerror("Fehler", "Der Ordner wurde nicht gefunden.")
|
||||
return
|
||||
image_files = sorted(
|
||||
(
|
||||
path
|
||||
for path in folder.iterdir()
|
||||
if path.suffix.lower() in SUPPORTED_IMAGE_EXTENSIONS and path.is_file()
|
||||
),
|
||||
key=lambda item: item.name.lower(),
|
||||
)
|
||||
if not image_files:
|
||||
messagebox.showinfo("Info", "Keine unterstützten Bilder im Ordner gefunden.")
|
||||
return
|
||||
self._set_image_collection(image_files, 0)
|
||||
|
||||
def show_next_image(self, event=None) -> None:
|
||||
if not getattr(self, "image_paths", None):
|
||||
return
|
||||
next_index = getattr(self, "current_image_index", -1) + 1
|
||||
if next_index < len(self.image_paths):
|
||||
self._display_image_by_index(next_index)
|
||||
|
||||
def show_previous_image(self, event=None) -> None:
|
||||
if not getattr(self, "image_paths", None):
|
||||
return
|
||||
prev_index = getattr(self, "current_image_index", -1) - 1
|
||||
if prev_index >= 0:
|
||||
self._display_image_by_index(prev_index)
|
||||
|
||||
def _set_image_collection(self, paths: Sequence[Path], start_index: int) -> None:
|
||||
self.image_paths = list(paths)
|
||||
if not self.image_paths:
|
||||
return
|
||||
self.current_image_index = -1
|
||||
self._display_image_by_index(max(0, start_index))
|
||||
|
||||
def _display_image_by_index(self, index: int) -> None:
|
||||
if not self.image_paths:
|
||||
return
|
||||
if index < 0 or index >= len(self.image_paths):
|
||||
return
|
||||
path = self.image_paths[index]
|
||||
if not path.exists():
|
||||
messagebox.showerror("Fehler", f"Datei nicht gefunden: {path}")
|
||||
return
|
||||
try:
|
||||
image = Image.open(path).convert("RGBA")
|
||||
except Exception as exc:
|
||||
messagebox.showerror("Fehler", f"Bild konnte nicht geladen werden: {exc}")
|
||||
return
|
||||
|
||||
self.image_path = path
|
||||
self.orig_img = image
|
||||
self.exclude_rects = []
|
||||
self._rubber_start = None
|
||||
self._rubber_id = None
|
||||
self.pick_mode = False
|
||||
|
||||
self.prepare_preview()
|
||||
self.update_preview()
|
||||
|
||||
dimensions = f"{self.orig_img.width}x{self.orig_img.height}"
|
||||
status_text = f"Geladen: {self.image_path.name} — {dimensions}"
|
||||
suffix = f" [{index + 1}/{len(self.image_paths)}]" if len(self.image_paths) > 1 else ""
|
||||
status_text = f"Geladen: {path.name} — {dimensions}{suffix}"
|
||||
self.status.config(text=status_text)
|
||||
self.status_default_text = status_text
|
||||
if hasattr(self, "filename_label"):
|
||||
self.filename_label.config(text=f"{self.image_path.name} — {dimensions}")
|
||||
self.filename_label.config(text=f"{path.name} — {dimensions}{suffix}")
|
||||
|
||||
self.current_image_index = index
|
||||
|
||||
def save_overlay(self) -> None:
|
||||
if self.orig_img is None:
|
||||
|
||||
Reference in New Issue
Block a user