以下は『寸影律』のソースコード全文です:
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()