画像リサイズツール『寸影律』 ソースコード

以下は『寸影律』のソースコード全文です:


        import tkinter as tk
        from tkinter import filedialog, messagebox
        from PIL import ImageTk, Image
        import os
        import glob
        import threading
        import datetime
        
        class ImageResizerGUI:
            def __init__(self, master):
                self.master = master
                master.title("画像リサイズツール - 寸影律")
        
                self.path = ""
                self.save_dir = ""
        
                self.add_timestamp = tk.BooleanVar(value=False)
                self.save_log = tk.BooleanVar(value=False)
        
                self.label_width = 50
                self.mode = tk.StringVar(value="file")
                self.rule = tk.StringVar(value="percent")
        
                self.build_interface()
        
            def build_interface(self):
                row = 0
                tk.Label(self.master, text="処理対象:").grid(row=row, column=0, sticky="w", padx=5)
                tk.Radiobutton(self.master, text="ファイル", variable=self.mode, value="file").grid(row=row, column=1, sticky="w")
                tk.Radiobutton(self.master, text="フォルダ", variable=self.mode, value="folder").grid(row=row, column=2, sticky="w")
        
                row += 1
                tk.Label(self.master, text="ルール:").grid(row=row, column=0, sticky="w", padx=5)
                tk.Radiobutton(self.master, text="パーセント", variable=self.rule, value="percent").grid(row=row, column=1, sticky="w")
                tk.Radiobutton(self.master, text="ピクセル", variable=self.rule, value="pixel").grid(row=row, column=2, sticky="w")
        
                row += 1
                tk.Label(self.master, text="サイズ横:").grid(row=row, column=0, sticky="w", padx=5)
                self.width_entry = tk.Entry(self.master, width=8)
                self.width_entry.grid(row=row, column=1, sticky="w")
        
                row += 1
                tk.Label(self.master, text="サイズ縦:").grid(row=row, column=0, sticky="w", padx=5)
                self.height_entry = tk.Entry(self.master, width=8)
                self.height_entry.grid(row=row, column=1, sticky="w")
        
                row += 1
                tk.Button(self.master, text="ファイル/フォルダ選択", command=self.select_path).grid(row=row, column=0, pady=5, padx=5, sticky="w")
                self.path_label = tk.Label(self.master, text="", anchor="w", width=self.label_width, fg="gray")
                self.path_label.grid(row=row, column=1, columnspan=2, sticky="w")
        
                row += 1
                tk.Button(self.master, text="保存先選択", command=self.select_save_dir).grid(row=row, column=0, pady=5, padx=5, sticky="w")
                self.save_label = tk.Label(self.master, text="", anchor="w", width=self.label_width, fg="gray")
                self.save_label.grid(row=row, column=1, columnspan=2, sticky="w")
        
                row += 1
                tk.Checkbutton(self.master, text="保存名に日時をつける", variable=self.add_timestamp).grid(row=row, column=0, columnspan=2, sticky="w", padx=5)
                row += 1
                tk.Checkbutton(self.master, text="ログを保存する", variable=self.save_log).grid(row=row, column=0, columnspan=2, sticky="w", padx=5)
        
                row += 1
                tk.Button(self.master, text="プレビュー表示", command=self.preview_image).grid(row=row, column=0, columnspan=3, pady=5)
                row += 1
                tk.Button(self.master, text="実行", command=self.confirm).grid(row=row, column=0, columnspan=3, pady=10)
        
            def shorten_path(self, path, max_len=45):
                return path if len(path) <= max_len else "..." + path[-(max_len - 3):]
        
            def select_path(self):
                if self.mode.get() == "file":
                    path = filedialog.askopenfilename(filetypes=[("画像ファイル", "*.png;*.jpg;*.jpeg;*.bmp")])
                else:
                    path = filedialog.askdirectory()
        
                if path:
                    self.path = path
                    short_path = self.shorten_path(path)
                    self.path_label.config(text=short_path)
        
            def select_save_dir(self):
                path = filedialog.askdirectory()
                if path:
                    self.save_dir = path
                    short_path = self.shorten_path(path)
                    self.save_label.config(text=short_path)
        
            def preview_image(self):
                try:
                    if self.mode.get() == "file":
                        img_path = self.path
                    else:
                        images = glob.glob(os.path.join(self.path, "*.png")) + \
                                 glob.glob(os.path.join(self.path, "*.jpg")) + \
                                 glob.glob(os.path.join(self.path, "*.jpeg")) + \
                                 glob.glob(os.path.join(self.path, "*.bmp"))
                        if not images:
                            return
                        img_path = images[0]
        
                    img = Image.open(img_path)
                    w, h = img.size
                    new_w, new_h = self.calculate_new_size(w, h)
                    resized = img.resize((new_w, new_h), Image.LANCZOS)
                    self.show_preview_thumbnails(img_path, resized)
        
                except Exception as e:
                    messagebox.showerror("プレビューエラー", f"プレビューに失敗しました\n{e}")
        
            def show_preview_thumbnails(self, original_path, resized_img):
                preview_win = tk.Toplevel(self.master)
                preview_win.title("プレビュー:処理前 / 処理後")
        
                original = Image.open(original_path)
                original.thumbnail((200, 200))
                after = resized_img.copy()
                after.thumbnail((200, 200))
        
                tk.Label(preview_win, text="処理前").grid(row=0, column=0, padx=10, pady=5)
                tk.Label(preview_win, text="処理後").grid(row=0, column=1, padx=10, pady=5)
        
                tk_original = ImageTk.PhotoImage(original)
                tk_after = ImageTk.PhotoImage(after)
        
                label1 = tk.Label(preview_win, image=tk_original)
                label2 = tk.Label(preview_win, image=tk_after)
                label1.grid(row=1, column=0, padx=10, pady=10)
                label2.grid(row=1, column=1, padx=10, pady=10)
        
                label1.image = tk_original
                label2.image = tk_after
        
            def confirm(self):
                if not self.path or not self.save_dir:
                    messagebox.showwarning("警告", "対象や保存先が選択されていません。")
                    return
        
                width_input = self.width_entry.get().strip()
                height_input = self.height_entry.get().strip()
        
                if not width_input and not height_input:
                    messagebox.showwarning("警告", "サイズ横またはサイズ縦のいずれかを入力してください。")
                    return
        
                try:
                    _ = self.calculate_new_size(100, 100)  # テスト計算
                except Exception as e:
                    messagebox.showerror("エラー", f"サイズの指定に問題があります。\n{e}")
                    return
        
                rule = self.rule.get()
                unit = "%" if rule == "percent" else "px"
                msg = f"対象: {self.path}\n保存先: {self.save_dir}\n"
                msg += f"サイズ指定: 横 {width_input or '自動'} / 縦 {height_input or '自動'}({unit})\n\n実行してよろしいですか?"
        
                if messagebox.askokcancel("確認", msg):
                    threading.Thread(target=self.process_images).start()
        
            def calculate_new_size(self, original_w, original_h):
                rule = self.rule.get()
                width_str = self.width_entry.get().strip()
                height_str = self.height_entry.get().strip()
        
                if rule == "percent":
                    w_scale = float(width_str)/100 if width_str else None
                    h_scale = float(height_str)/100 if height_str else None
        
                    if w_scale and not h_scale:
                        h_scale = w_scale
                    elif h_scale and not w_scale:
                        w_scale = h_scale
                    elif not w_scale and not h_scale:
                        raise ValueError("サイズが未指定です。")
        
                    return int(original_w * w_scale), int(original_h * h_scale)
                else:
                    w = int(width_str) if width_str else None
                    h = int(height_str) if height_str else None
        
                    if w and not h:
                        h = int(original_h * (w / original_w))
                    elif h and not w:
                        w = int(original_w * (h / original_h))
                    elif not w and not h:
                        raise ValueError("サイズが未指定です。")
        
                    return w, h
        
            def process_images(self):
                try:
                    dir_mode = self.mode.get()
                    timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M")
                    log_entries = []
        
                    if dir_mode == "file":
                        files = [self.path]
                    else:
                        files = []
                        for ext in ("*.png", "*.jpg", "*.jpeg", "*.bmp"):
                            files.extend(glob.glob(os.path.join(self.path, ext)))
        
                    if not files:
                        messagebox.showinfo("情報", "対象画像が見つかりませんでした。")
                        return
        
                    status = tk.Toplevel(self.master)
                    status.title("処理中")
                    label = tk.Label(status, text="処理中です... 0 / {}".format(len(files)))
                    label.pack(padx=20, pady=20)
        
                    for i, file in enumerate(files, 1):
                        img = Image.open(file)
                        w, h = img.size
                        new_w, new_h = self.calculate_new_size(w, h)
                        resized = img.resize((new_w, new_h), Image.LANCZOS)
        
                        base = os.path.basename(file)
                        name, ext = os.path.splitext(base)
                        name += "-resized"
                        if self.add_timestamp.get():
                            name += f"-{timestamp}"
        
                        save_path = os.path.join(self.save_dir, name + ext)
                        resized.save(save_path)
        
                        log_entries.append(f"{base} -> {name + ext} ({w}x{h} → {new_w}x{new_h})")
        
                        label.config(text="処理中です... {} / {}".format(i, len(files)))
                        label.update()
        
                    status.destroy()
                    messagebox.showinfo("完了", "全画像の処理が完了しました。")
        
                    if self.save_log.get():
                        log_file = os.path.join(self.save_dir, f"resize_log_{timestamp}.txt")
                        with open(log_file, "w", encoding="utf-8") as f:
                            for line in log_entries:
                                f.write(line + "\n")
        
                except Exception as e:
                    messagebox.showerror("エラー", f"処理中にエラーが発生しました\n{e}")
        
        if __name__ == "__main__":
            root = tk.Tk()
            app = ImageResizerGUI(root)
            root.mainloop()