色調変換ツール『Φωτοκρίτης』 ソースコード

以下は『Φωτοκρίτης』のソースコード全文です:

 
        import tkinter as tk
        from tkinter import filedialog, messagebox
        from tkinter import ttk
        from PIL import Image, ImageTk, ImageOps
        import colorsys
        import os
        
        class ImageAdjuster:
            def __init__(self, root):
                self.root = root
                self.root.title("色調変換ツール - Φωτοκρίτης")
        
                self.image_path = None
                self.original_image = None
                self.thumb_size = (200, 200)
                self.save_dir = None  # 保存先パス
                self.save_format = tk.StringVar(value="png")  # 保存形式
        
                ttk.Button(root, text="画像選択", command=self.select_image).pack(pady=5)
        
                self.path_label = ttk.Label(root, text="画像が未選択です")
                self.path_label.pack()
        
                self.thumbnail_label = ttk.Label(root)
                self.thumbnail_label.pack(pady=5)
        
                # チェックボックス
                self.chk_invert = tk.BooleanVar()
                self.chk_brightness = tk.BooleanVar()
                self.chk_grayscale = tk.BooleanVar()
        
                ttk.Checkbutton(root, text="階調の反転", variable=self.chk_invert).pack()
                ttk.Checkbutton(root, text="光度の反転", variable=self.chk_brightness).pack()
                ttk.Checkbutton(root, text="モノクロ化", variable=self.chk_grayscale).pack()
        
                # プレビュー
                ttk.Button(root, text="プレビュー", command=self.preview_images).pack(pady=10)
                self.preview_frame = ttk.Frame(root)
                self.preview_frame.pack()
        
                # 保存形式選択(ラジオボタン)
                ttk.Label(root, text="保存形式:").pack(pady=(10, 0))
                fmt_frame = ttk.Frame(root)
                fmt_frame.pack()
                for fmt in ["png", "jpg", "bmp"]:
                    ttk.Radiobutton(fmt_frame, text=fmt, variable=self.save_format, value=fmt).pack(side=tk.LEFT, padx=5)
        
                # 保存先選択
                ttk.Button(root, text="保存先選択", command=self.select_save_dir).pack(pady=5)
                self.save_dir_label = ttk.Label(root, text="保存先:未指定(元画像と同じフォルダ)")
                self.save_dir_label.pack()
        
                # 保存ボタン
                ttk.Button(root, text="保存", command=self.save_images).pack(pady=5)
        
            def select_image(self):
                path = filedialog.askopenfilename(filetypes=[("画像ファイル", "*.png *.jpg *.jpeg *.bmp")])
                if path:
                    self.image_path = path
                    self.original_image = Image.open(path)
                    self.path_label.config(text=self.image_path)
        
                    thumbnail = self.original_image.copy()
                    thumbnail.thumbnail(self.thumb_size)
                    self.tk_thumb = ImageTk.PhotoImage(thumbnail)
                    self.thumbnail_label.config(image=self.tk_thumb)
        
            def select_save_dir(self):
                dir_path = filedialog.askdirectory()
                if dir_path:
                    self.save_dir = dir_path
                    self.save_dir_label.config(text=f"保存先:{self.save_dir}")
                else:
                    self.save_dir = None
                    self.save_dir_label.config(text="保存先:未指定(元画像と同じフォルダ)")
        
            def preview_images(self):
                for widget in self.preview_frame.winfo_children():
                    widget.destroy()
        
                if not self.original_image:
                    return
        
                col = 0
        
                if self.chk_invert.get():
                    inv_img = self.invert_with_alpha(self.original_image)
                    self.display_preview(inv_img, "階調の反転", col)
                    col += 1
        
                if self.chk_brightness.get():
                    bright_img = self.invert_brightness(self.original_image)
                    self.display_preview(bright_img, "光度の反転", col)
                    col += 1
        
                if self.chk_grayscale.get():
                    gray_img = self.original_image.convert("L")
                    self.display_preview(gray_img, "モノクロ化", col)
        
            def display_preview(self, img, label_text, col):
                preview = img.copy()
                preview.thumbnail(self.thumb_size)
                tk_img = ImageTk.PhotoImage(preview)
        
                frame = ttk.Frame(self.preview_frame)
                frame.grid(row=0, column=col, padx=10)
        
                label = ttk.Label(frame, text=label_text)
                label.pack()
        
                img_label = ttk.Label(frame, image=tk_img)
                img_label.image = tk_img
                img_label.pack()
        
            def invert_brightness(self, img):
                if img.mode in ("RGBA", "LA"):
                    rgb_img = img.convert("RGBA")
                else:
                    rgb_img = img.convert("RGB")
        
                pixels = rgb_img.load()
                width, height = rgb_img.size
        
                result = Image.new("RGBA" if "A" in rgb_img.mode else "RGB", (width, height))
                result_pixels = result.load()
        
                for y in range(height):
                    for x in range(width):
                        r, g, b, *a = pixels[x, y]
                        h, l, s = colorsys.rgb_to_hls(r / 255, g / 255, b / 255)
                        l = 1.0 - l
                        r2, g2, b2 = colorsys.hls_to_rgb(h, l, s)
                        result_pixels[x, y] = (int(r2 * 255), int(g2 * 255), int(b2 * 255)) + tuple(a)
        
                return result
        
            def invert_with_alpha(self, img):
                if img.mode == "RGBA":
                    r, g, b, a = img.split()
                    rgb_img = Image.merge("RGB", (r, g, b))
                    inverted = ImageOps.invert(rgb_img)
                    return Image.merge("RGBA", (*inverted.split(), a))
                else:
                    return ImageOps.invert(img.convert("RGB"))
        
            def save_images(self):
                if not self.original_image or not self.image_path:
                    return
        
                base_dir = self.save_dir if self.save_dir else os.path.dirname(self.image_path)
                base_name = os.path.splitext(os.path.basename(self.image_path))[0]
                ext = self.save_format.get()
                count = 0
        
                if self.chk_invert.get():
                    img = self.invert_with_alpha(self.original_image)
                    self.save_image(img, base_dir, f"{base_name}_invert.{ext}", ext)
                    count += 1
        
                if self.chk_brightness.get():
                    img = self.invert_brightness(self.original_image)
                    self.save_image(img, base_dir, f"{base_name}_brightness_invert.{ext}", ext)
                    count += 1
        
                if self.chk_grayscale.get():
                    img = self.original_image.convert("L")
                    self.save_image(img, base_dir, f"{base_name}_grayscale.{ext}", ext)
                    count += 1
        
                messagebox.showinfo("保存完了", f"{count} 枚の画像を保存しました。")
        
            def save_image(self, img, path, filename, fmt):
                full_path = os.path.join(path, filename)
                save_params = {}
                if fmt == "jpg":
                    img = img.convert("RGB")  # JPEGは透明非対応
                elif fmt == "png" and "A" in img.getbands():
                    save_params["compress_level"] = 1  # PNG圧縮軽め
        
                img.save(full_path, format=fmt.upper(), **save_params)
        
        if __name__ == "__main__":
            root = tk.Tk()
            app = ImageAdjuster(root)
            root.mainloop()