| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import sys |
| import tkinter as tk |
| from tkinter import filedialog |
| from PIL import Image, ImageTk |
| from pathlib import Path |
|
|
| IMG_EXT = ["jpg", "jpeg", "png", "webp"] |
|
|
| class CaptionedImage(): |
| def __init__(self, image_path): |
| self.base_path = image_path.parent |
| self.path = image_path |
| |
| def caption_path(self): |
| return self.base_path / (self.path.stem + '.txt') |
|
|
| def read_caption(self): |
| caption_path = self.caption_path() |
| if caption_path.exists(): |
| with open(caption_path, 'r', encoding='utf-8', newline='') as f: |
| return f.read() |
| return '' |
|
|
| def write_caption(self, caption): |
| caption_path = self.caption_path() |
| with open(str(caption_path), 'w', encoding='utf-8', newline='') as f: |
| f.write(caption) |
| |
| |
| def __lt__(self, other): |
| return str(self.path).lower() < str(other.path).lower() |
|
|
| class ImageView(tk.Frame): |
|
|
| def __init__(self, root): |
| tk.Frame.__init__(self, root) |
|
|
| self.root = root |
| self.base_path = None |
| self.images = [] |
| self.index = 0 |
|
|
| |
| self.image_frame = tk.Frame(self) |
| self.image_label = tk.Label(self.image_frame) |
| self.image_label.place(relx=0.5, rely=0.5, anchor=tk.CENTER) |
| self.image_frame.pack(expand=True, fill=tk.BOTH, side=tk.LEFT) |
| |
| |
| self.caption_frame = tk.Frame(self) |
| self.caption_field = tk.Text(self.caption_frame, wrap="word", width=50) |
| self.caption_field.pack(expand=True, fill=tk.BOTH) |
| self.caption_frame.pack(fill=tk.Y, side=tk.RIGHT) |
|
|
| def open_folder(self): |
| dir = filedialog.askdirectory() |
| if not dir: |
| return |
| self.base_path = Path(dir) |
| if self.base_path is None: |
| return |
| self.images.clear() |
| for ext in IMG_EXT: |
| for file in self.base_path.glob(f'*.{ext}'): |
| self.images.append(CaptionedImage(file)) |
| self.images.sort() |
| self.update_ui() |
| |
| def store_caption(self): |
| txt = self.caption_field.get(1.0, tk.END) |
| txt = txt.replace('\r', '').replace('\n', '').strip() |
| self.images[self.index].write_caption(txt) |
| |
| def set_index(self, index): |
| self.index = index % len(self.images) |
|
|
| def go_to_image(self, index): |
| if len(self.images) == 0: |
| return |
| self.store_caption() |
| self.set_index(index) |
| self.update_ui() |
|
|
| def next_image(self): |
| self.go_to_image(self.index + 1) |
|
|
| def prev_image(self): |
| self.go_to_image(self.index - 1) |
|
|
| |
| def delete_image(self): |
| if len(self.images) == 0: |
| return |
| img = self.images[self.index] |
|
|
| trash_path = self.base_path / '_deleted' |
| if not trash_path.exists(): |
| trash_path.mkdir() |
| img.path.rename(trash_path / img.path.name) |
| caption_path = img.caption_path() |
| if caption_path.exists(): |
| caption_path.rename(trash_path / caption_path.name) |
| del self.images[self.index] |
| self.set_index(self.index) |
| self.update_ui() |
| |
| def update_ui(self): |
| if (len(self.images)) == 0: |
| self.filename.set('') |
| self.caption_field.delete(1.0, tk.END) |
| self.image_label.configure(image=None) |
| return |
| img = self.images[self.index] |
| |
| title = self.images[self.index].path.name if len(self.images) > 0 else '' |
| self.root.title(title + f' ({self.index+1}/{len(self.images)})') |
| |
| self.caption_field.delete(1.0, tk.END) |
| self.caption_field.insert(tk.END, img.read_caption()) |
| |
| img = Image.open(self.images[self.index].path) |
| |
| |
| w = self.image_frame.winfo_width() |
| h = self.image_frame.winfo_height() |
| if img.width > w or img.height > h: |
| img.thumbnail((w, h)) |
| photoImage = ImageTk.PhotoImage(img) |
| self.image_label.configure(image=photoImage) |
| self.image_label.image = photoImage |
| |
| if __name__=='__main__': |
| root = tk.Tk() |
| root.geometry('1200x800') |
| root.title('Image Captions') |
|
|
| if sys.platform == 'darwin': |
| root.bind('<Command-o>', lambda e: view.open_folder()) |
| else: |
| root.bind('<Control-o>', lambda e: view.open_folder()) |
| root.bind('<Escape>', lambda e: root.destroy()) |
| root.bind('<Prior>', lambda e: view.prev_image()) |
| root.bind('<Next>', lambda e: view.next_image()) |
| root.bind('<Shift-Prior>', lambda e: view.go_to_image(view.index - 10)) |
| root.bind('<Shift-Next>', lambda e: view.go_to_image(view.index + 10)) |
| root.bind('<Shift-Home>', lambda e: view.go_to_image(0)) |
| root.bind('<Shift-End>', lambda e: view.go_to_image(len(view.images) - 1)) |
| root.bind('<Shift-Delete>', lambda e: view.delete_image()) |
|
|
| view = ImageView(root) |
| view.pack(side=tk.TOP, fill=tk.BOTH, expand=True) |
| root.mainloop() |
|
|