Initial Tkinter GUI
This commit is contained in:
parent
a6b0298253
commit
725b3c351e
|
|
@ -9,17 +9,22 @@ Tkinter-based GUI for WImageEdit, using wimage_core for:
|
||||||
- Simple per-file save/load of settings (imgedit_proc.json).
|
- Simple per-file save/load of settings (imgedit_proc.json).
|
||||||
- Directory defaults.
|
- Directory defaults.
|
||||||
|
|
||||||
|
Layout:
|
||||||
|
- Left panel: directory selection, file list, navigation, info.
|
||||||
|
- Right panel: top canvas = Original, bottom canvas = Preview, controls on the right.
|
||||||
|
|
||||||
This version:
|
This version:
|
||||||
- Shows TWO views:
|
- Shows TWO views stacked vertically:
|
||||||
Left = original (with rotation applied, no processing except resize)
|
Top = original (rotated, no processing except resize)
|
||||||
Right = preview with current options applied (Pillow approximation).
|
Bottom = preview with current options applied (Pillow approximation).
|
||||||
- Includes parametric controls for:
|
- Includes parametric controls for:
|
||||||
- Sigmoidal contrast (contrast, midpoint)
|
- Sigmoidal contrast (contrast, midpoint)
|
||||||
- Sharpen (radius, amount, threshold)
|
- Sharpen (radius, amount, threshold)
|
||||||
- Includes rotation controls:
|
- Includes rotation controls:
|
||||||
- None / 90 / 180 / 270 / Custom
|
- None / 90 / 180 / 270 / Custom
|
||||||
|
- Displays a live ImageMagick command string based on current options.
|
||||||
"""
|
"""
|
||||||
|
import sys, os
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
@ -49,6 +54,8 @@ from wimage_core import (
|
||||||
save_directory_defaults,
|
save_directory_defaults,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
# ---------------------- helpers / file listing / EXIF ----------------------
|
# ---------------------- helpers / file listing / EXIF ----------------------
|
||||||
|
|
||||||
|
|
@ -111,8 +118,11 @@ class WImageEditTk(tk.Tk):
|
||||||
|
|
||||||
self.state = AppState()
|
self.state = AppState()
|
||||||
|
|
||||||
|
# IM command string (for display/copy)
|
||||||
|
self.current_cmd_str: str = ""
|
||||||
|
|
||||||
# UI variables
|
# UI variables
|
||||||
self.dir_var = tk.StringVar()
|
self.dir_var = tk.StringVar(value="~/Downloads")
|
||||||
|
|
||||||
self.color_var = tk.BooleanVar(value=True)
|
self.color_var = tk.BooleanVar(value=True)
|
||||||
self.sigmoidal_var = tk.BooleanVar(value=True)
|
self.sigmoidal_var = tk.BooleanVar(value=True)
|
||||||
|
|
@ -145,19 +155,23 @@ class WImageEditTk(tk.Tk):
|
||||||
# --------------------------- UI building ---------------------------------
|
# --------------------------- UI building ---------------------------------
|
||||||
|
|
||||||
def _build_ui(self):
|
def _build_ui(self):
|
||||||
top = ttk.Frame(self, padding=5)
|
# Main frame splits left (files) and right (images + controls)
|
||||||
top.pack(side=tk.TOP, fill=tk.X)
|
main = ttk.Frame(self, padding=5)
|
||||||
|
main.pack(fill=tk.BOTH, expand=True)
|
||||||
# Left: file list + nav
|
|
||||||
left = ttk.Frame(self, padding=5)
|
|
||||||
ttk.Label(left, text="Dir:").pack(side=tk.LEFT)
|
|
||||||
ttk.Entry(left, textvariable=self.dir_var, width=30).pack(side=tk.LEFT, padx=4)
|
|
||||||
ttk.Button(left, text="Browse...", command=self.select_directory).pack(side=tk.LEFT)
|
|
||||||
|
|
||||||
|
# LEFT PANEL: directory picker + file list + nav + info
|
||||||
|
left = ttk.Frame(main, padding=5)
|
||||||
left.pack(side=tk.LEFT, fill=tk.Y)
|
left.pack(side=tk.LEFT, fill=tk.Y)
|
||||||
|
|
||||||
|
# Directory picker at top *within* left panel
|
||||||
|
dir_frame = ttk.Frame(left)
|
||||||
|
dir_frame.pack(fill=tk.X, pady=(0, 5))
|
||||||
|
ttk.Label(dir_frame, text="Directory:").pack(side=tk.LEFT)
|
||||||
|
ttk.Entry(dir_frame, textvariable=self.dir_var, width=30).pack(side=tk.LEFT, padx=4)
|
||||||
|
ttk.Button(dir_frame, text="...", width=3, command=self.select_directory).pack(side=tk.LEFT)
|
||||||
|
|
||||||
ttk.Label(left, text="Files:").pack(anchor=tk.W)
|
ttk.Label(left, text="Files:").pack(anchor=tk.W)
|
||||||
self.file_listbox = tk.Listbox(left, width=40, height=25)
|
self.file_listbox = tk.Listbox(left, width=35, height=24)
|
||||||
self.file_listbox.pack(fill=tk.BOTH, expand=True)
|
self.file_listbox.pack(fill=tk.BOTH, expand=True)
|
||||||
self.file_listbox.bind("<<ListboxSelect>>", self.on_file_select)
|
self.file_listbox.bind("<<ListboxSelect>>", self.on_file_select)
|
||||||
|
|
||||||
|
|
@ -166,39 +180,38 @@ class WImageEditTk(tk.Tk):
|
||||||
ttk.Button(nav_frame, text="Prev", command=self.prev_image).pack(side=tk.LEFT, expand=True, fill=tk.X)
|
ttk.Button(nav_frame, text="Prev", command=self.prev_image).pack(side=tk.LEFT, expand=True, fill=tk.X)
|
||||||
ttk.Button(nav_frame, text="Next", command=self.next_image).pack(side=tk.LEFT, expand=True, fill=tk.X)
|
ttk.Button(nav_frame, text="Next", command=self.next_image).pack(side=tk.LEFT, expand=True, fill=tk.X)
|
||||||
|
|
||||||
# Center: two canvases (Original + Preview)
|
ttk.Label(left, textvariable=self.info_var, wraplength=260, justify=tk.LEFT).pack(fill=tk.X, pady=5)
|
||||||
center = ttk.Frame(self, padding=5)
|
|
||||||
center.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
canvas_labels = ttk.Frame(center)
|
# RIGHT PANEL: top canvas (original), bottom canvas (preview), controls on the right
|
||||||
canvas_labels.pack(fill=tk.X)
|
right = ttk.Frame(main, padding=5)
|
||||||
#ttk.Label(canvas_labels, text="Original").pack(side=tk.LEFT, expand=True)
|
right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
ttk.Label(canvas_labels, text="Original").pack(side=tk.TOP, expand=True)
|
|
||||||
#ttk.Label(canvas_labels, text="Preview").pack(side=tk.RIGHT, expand=True)
|
|
||||||
ttk.Label(canvas_labels, text="Preview").pack(side=tk.BOTTOM, expand=True)
|
|
||||||
|
|
||||||
canvases = ttk.Frame(center)
|
# Right is split: left area = canvases (stacked), right area = controls
|
||||||
canvases.pack(fill=tk.BOTH, expand=True)
|
right_canvases = ttk.Frame(right)
|
||||||
|
right_canvases.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
self.canvas_orig = tk.Canvas(canvases, bg="gray", width=600, height=400)
|
right_controls = ttk.Frame(right, padding=(5, 0))
|
||||||
#self.canvas_orig.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 2))
|
right_controls.pack(side=tk.LEFT, fill=tk.Y)
|
||||||
self.canvas_orig.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=(0, 2))
|
|
||||||
|
|
||||||
self.canvas_prev = tk.Canvas(canvases, bg="gray", width=600, height=400)
|
# Canvases stacked: Original (top) and Preview (bottom)
|
||||||
#self.canvas_prev.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(2, 0))
|
orig_label = ttk.Label(right_canvases, text="Original")
|
||||||
self.canvas_prev.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True, padx=(2, 0))
|
orig_label.pack(anchor=tk.W)
|
||||||
|
self.canvas_orig = tk.Canvas(right_canvases, bg="gray", width=600, height=300)
|
||||||
|
self.canvas_orig.pack(fill=tk.BOTH, expand=True, pady=(0, 4))
|
||||||
|
|
||||||
|
prev_label = ttk.Label(right_canvases, text="Preview")
|
||||||
|
prev_label.pack(anchor=tk.W)
|
||||||
|
self.canvas_prev = tk.Canvas(right_canvases, bg="gray", width=600, height=300)
|
||||||
|
self.canvas_prev.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
# Crop is defined by dragging on the ORIGINAL canvas
|
# Crop is defined by dragging on the ORIGINAL canvas
|
||||||
self.canvas_orig.bind("<ButtonPress-1>", self.on_canvas_press)
|
self.canvas_orig.bind("<ButtonPress-1>", self.on_canvas_press)
|
||||||
self.canvas_orig.bind("<B1-Motion>", self.on_canvas_drag)
|
self.canvas_orig.bind("<B1-Motion>", self.on_canvas_drag)
|
||||||
self.canvas_orig.bind("<ButtonRelease-1>", self.on_canvas_release)
|
self.canvas_orig.bind("<ButtonRelease-1>", self.on_canvas_release)
|
||||||
|
|
||||||
# Right: controls
|
# Controls on the right
|
||||||
right = ttk.Frame(self, padding=5)
|
|
||||||
right.pack(side=tk.RIGHT, fill=tk.Y)
|
|
||||||
|
|
||||||
# Actions frame
|
# Actions frame
|
||||||
actions_frame = ttk.LabelFrame(right, text="Actions", padding=5)
|
actions_frame = ttk.LabelFrame(right_controls, text="Actions", padding=5)
|
||||||
actions_frame.pack(fill=tk.X)
|
actions_frame.pack(fill=tk.X)
|
||||||
|
|
||||||
ttk.Button(actions_frame, text="Color Pos",
|
ttk.Button(actions_frame, text="Color Pos",
|
||||||
|
|
@ -211,7 +224,7 @@ class WImageEditTk(tk.Tk):
|
||||||
command=lambda: self.apply_action("BW Neg")).pack(fill=tk.X, pady=1)
|
command=lambda: self.apply_action("BW Neg")).pack(fill=tk.X, pady=1)
|
||||||
|
|
||||||
# Processing options
|
# Processing options
|
||||||
opts_frame = ttk.LabelFrame(right, text="Processing", padding=5)
|
opts_frame = ttk.LabelFrame(right_controls, text="Processing", padding=5)
|
||||||
opts_frame.pack(fill=tk.X, pady=(5, 0))
|
opts_frame.pack(fill=tk.X, pady=(5, 0))
|
||||||
|
|
||||||
ttk.Checkbutton(
|
ttk.Checkbutton(
|
||||||
|
|
@ -296,7 +309,7 @@ class WImageEditTk(tk.Tk):
|
||||||
ttk.Label(scale_row, text="(1.1 ≈ 10% border)").pack(side=tk.LEFT)
|
ttk.Label(scale_row, text="(1.1 ≈ 10% border)").pack(side=tk.LEFT)
|
||||||
|
|
||||||
# Rotation controls
|
# Rotation controls
|
||||||
rot_frame = ttk.LabelFrame(right, text="Rotation", padding=5)
|
rot_frame = ttk.LabelFrame(right_controls, text="Rotation", padding=5)
|
||||||
rot_frame.pack(fill=tk.X, pady=5)
|
rot_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
def rot_rb(text, value):
|
def rot_rb(text, value):
|
||||||
|
|
@ -323,11 +336,11 @@ class WImageEditTk(tk.Tk):
|
||||||
).pack(side=tk.LEFT, padx=4)
|
).pack(side=tk.LEFT, padx=4)
|
||||||
|
|
||||||
# Annotation
|
# Annotation
|
||||||
annot_frame = ttk.LabelFrame(right, text="Text annotation", padding=5)
|
annot_frame = ttk.LabelFrame(right_controls, text="Text annotation", padding=5)
|
||||||
annot_frame.pack(fill=tk.X, pady=5)
|
annot_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
ttk.Label(annot_frame, text="Text:").pack(anchor=tk.W)
|
ttk.Label(annot_frame, text="Text:").pack(anchor=tk.W)
|
||||||
self.annot_text = tk.Text(annot_frame, width=30, height=4)
|
self.annot_text = tk.Text(annot_frame, width=30, height=3)
|
||||||
self.annot_text.pack(fill=tk.X)
|
self.annot_text.pack(fill=tk.X)
|
||||||
self.annot_text.bind("<<Modified>>", self.on_annot_modified)
|
self.annot_text.bind("<<Modified>>", self.on_annot_modified)
|
||||||
|
|
||||||
|
|
@ -356,24 +369,47 @@ class WImageEditTk(tk.Tk):
|
||||||
).pack(anchor=tk.W)
|
).pack(anchor=tk.W)
|
||||||
|
|
||||||
# Directory defaults
|
# Directory defaults
|
||||||
defaults_frame = ttk.LabelFrame(right, text="Directory defaults", padding=5)
|
defaults_frame = ttk.LabelFrame(right_controls, text="Directory defaults", padding=5)
|
||||||
defaults_frame.pack(fill=tk.X, pady=5)
|
defaults_frame.pack(fill=tk.X, pady=5)
|
||||||
ttk.Button(
|
ttk.Button(
|
||||||
defaults_frame, text="Set from current image",
|
defaults_frame, text="Set from current image",
|
||||||
command=self.set_directory_defaults_from_current
|
command=self.set_directory_defaults_from_current
|
||||||
).pack(fill=tk.X)
|
).pack(fill=tk.X)
|
||||||
|
|
||||||
# Actions buttons
|
# IM command display
|
||||||
action_frame = ttk.Frame(right)
|
cmd_frame = ttk.LabelFrame(right_controls, text="Current IM command", padding=5)
|
||||||
|
cmd_frame.pack(fill=tk.BOTH, expand=True, pady=5)
|
||||||
|
|
||||||
|
self.cmd_text = tk.Text(cmd_frame, width=40, height=6, wrap="word")
|
||||||
|
self.cmd_text.pack(fill=tk.BOTH, expand=True)
|
||||||
|
self.cmd_text.configure(state="disabled")
|
||||||
|
|
||||||
|
btn_row = ttk.Frame(cmd_frame)
|
||||||
|
btn_row.pack(fill=tk.X, pady=(4, 0))
|
||||||
|
ttk.Button(btn_row, text="Copy IM Cmd", command=self.copy_cmd_to_clipboard).pack(side=tk.LEFT, expand=True, fill=tk.X)
|
||||||
|
ttk.Button(btn_row, text="IM Preview", command=self.run_im_preview).pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(4, 0))
|
||||||
|
|
||||||
|
ttk.Label(
|
||||||
|
cmd_frame,
|
||||||
|
text="Note: if 'Granite background' is checked, two IM\n"
|
||||||
|
"commands are used (subject + background).",
|
||||||
|
justify=tk.LEFT,
|
||||||
|
wraplength=260,
|
||||||
|
).pack(fill=tk.X, pady=(4, 0))
|
||||||
|
|
||||||
|
|
||||||
|
# Actions buttons (bottom)
|
||||||
|
action_frame = ttk.Frame(right_controls)
|
||||||
action_frame.pack(fill=tk.X, pady=10)
|
action_frame.pack(fill=tk.X, pady=10)
|
||||||
ttk.Button(action_frame, text="Process", command=self.process_current).pack(fill=tk.X)
|
ttk.Button(action_frame, text="Process", command=self.process_current).pack(fill=tk.X)
|
||||||
ttk.Button(action_frame, text="Quit", command=self.destroy).pack(fill=tk.X, pady=(5, 0))
|
ttk.Button(action_frame, text="Quit", command=self.destroy).pack(fill=tk.X, pady=(5, 0))
|
||||||
|
|
||||||
ttk.Label(
|
# Initial action
|
||||||
right, textvariable=self.info_var,
|
# print(dir(self.dir_var))
|
||||||
wraplength=220, justify=tk.LEFT
|
if os.path.exists(self.dir_var.get()):
|
||||||
).pack(fill=tk.X, pady=5)
|
self.state.directory = Path(self.dir_var.get())
|
||||||
|
self.load_files()
|
||||||
|
|
||||||
# ------------------------------ directory & files ------------------------
|
# ------------------------------ directory & files ------------------------
|
||||||
|
|
||||||
def select_directory(self):
|
def select_directory(self):
|
||||||
|
|
@ -400,6 +436,7 @@ class WImageEditTk(tk.Tk):
|
||||||
self.canvas_orig.delete("all")
|
self.canvas_orig.delete("all")
|
||||||
self.canvas_prev.delete("all")
|
self.canvas_prev.delete("all")
|
||||||
self.info_var.set("No image files found in directory.")
|
self.info_var.set("No image files found in directory.")
|
||||||
|
self.update_cmd_display(clear=True)
|
||||||
|
|
||||||
def on_file_select(self, event=None):
|
def on_file_select(self, event=None):
|
||||||
sel = self.file_listbox.curselection()
|
sel = self.file_listbox.curselection()
|
||||||
|
|
@ -439,8 +476,8 @@ class WImageEditTk(tk.Tk):
|
||||||
|
|
||||||
def display_on_canvas(self, canvas: tk.Canvas, img: Image.Image, is_original: bool):
|
def display_on_canvas(self, canvas: tk.Canvas, img: Image.Image, is_original: bool):
|
||||||
"""Scale and display img on the given canvas, center it."""
|
"""Scale and display img on the given canvas, center it."""
|
||||||
canvas_w = canvas.winfo_width() or 400
|
canvas_w = canvas.winfo_width() or 600
|
||||||
canvas_h = canvas.winfo_height() or 600
|
canvas_h = canvas.winfo_height() or 300
|
||||||
scale = min(canvas_w / img.size[0], canvas_h / img.size[1])
|
scale = min(canvas_w / img.size[0], canvas_h / img.size[1])
|
||||||
new_size = (max(1, int(img.size[0] * scale)), max(1, int(img.size[1] * scale)))
|
new_size = (max(1, int(img.size[0] * scale)), max(1, int(img.size[1] * scale)))
|
||||||
disp_img = img.resize(new_size, Image.LANCZOS)
|
disp_img = img.resize(new_size, Image.LANCZOS)
|
||||||
|
|
@ -693,6 +730,7 @@ class WImageEditTk(tk.Tk):
|
||||||
Rebuild the rotated original and the preview from base_image + options.
|
Rebuild the rotated original and the preview from base_image + options.
|
||||||
"""
|
"""
|
||||||
if self.state.base_image is None:
|
if self.state.base_image is None:
|
||||||
|
self.update_cmd_display(clear=True)
|
||||||
return
|
return
|
||||||
opts = self.gather_options_from_ui(include_crop=False)
|
opts = self.gather_options_from_ui(include_crop=False)
|
||||||
# Rotate base image according to opts
|
# Rotate base image according to opts
|
||||||
|
|
@ -806,13 +844,16 @@ class WImageEditTk(tk.Tk):
|
||||||
|
|
||||||
def update_preview(self):
|
def update_preview(self):
|
||||||
"""
|
"""
|
||||||
Recompute and display the preview image (processed) on the right canvas.
|
Recompute and display the preview image (processed) on the bottom canvas.
|
||||||
|
Also updates the IM command text.
|
||||||
"""
|
"""
|
||||||
if not self.state.current_image:
|
if not self.state.current_image:
|
||||||
|
self.update_cmd_display(clear=True)
|
||||||
return
|
return
|
||||||
opts = self.gather_options_from_ui(include_crop=True)
|
opts = self.gather_options_from_ui(include_crop=True)
|
||||||
preview_img = self.apply_preview_effects(self.state.current_image.copy(), opts)
|
preview_img = self.apply_preview_effects(self.state.current_image.copy(), opts)
|
||||||
self.display_on_canvas(self.canvas_prev, preview_img, is_original=False)
|
self.display_on_canvas(self.canvas_prev, preview_img, is_original=False)
|
||||||
|
self.update_cmd_display(clear=False)
|
||||||
|
|
||||||
def options_changed(self):
|
def options_changed(self):
|
||||||
"""Callback used by various controls to trigger preview update."""
|
"""Callback used by various controls to trigger preview update."""
|
||||||
|
|
@ -823,6 +864,186 @@ class WImageEditTk(tk.Tk):
|
||||||
self.annot_text.edit_modified(False)
|
self.annot_text.edit_modified(False)
|
||||||
self.update_preview()
|
self.update_preview()
|
||||||
|
|
||||||
|
# ----------------------- IM command display / copy -----------------------
|
||||||
|
|
||||||
|
def update_cmd_display(self, clear: bool = False):
|
||||||
|
"""
|
||||||
|
Build and show the current main ImageMagick command.
|
||||||
|
|
||||||
|
For directories with no image selected, or clear=True, the display is emptied.
|
||||||
|
|
||||||
|
If 'Granite background' is checked, note that a second command
|
||||||
|
will be run on Process, but we don't synthesize that here.
|
||||||
|
"""
|
||||||
|
if clear or not self.state.files:
|
||||||
|
self.current_cmd_str = ""
|
||||||
|
self.cmd_text.configure(state="normal")
|
||||||
|
self.cmd_text.delete("1.0", tk.END)
|
||||||
|
self.cmd_text.configure(state="disabled")
|
||||||
|
return
|
||||||
|
|
||||||
|
path = self.state.files[self.state.current_index]
|
||||||
|
opts = self.gather_options_from_ui(include_crop=True)
|
||||||
|
|
||||||
|
# Destination name (same as Process)
|
||||||
|
dest = path.with_name(path.stem + "_edit.jpg")
|
||||||
|
|
||||||
|
# For background, we still show the main command (first pass).
|
||||||
|
if opts.use_background:
|
||||||
|
tmp_subject = path.with_name(path.stem + "_tmp_subject.jpg")
|
||||||
|
opts_main = EditOptions(
|
||||||
|
crop_box=opts.crop_box,
|
||||||
|
rotation_degrees=opts.rotation_degrees,
|
||||||
|
color_correction=opts.color_correction,
|
||||||
|
sigmoidal=opts.sigmoidal,
|
||||||
|
sigmoidal_contrast=opts.sigmoidal_contrast,
|
||||||
|
sigmoidal_midpoint=opts.sigmoidal_midpoint,
|
||||||
|
sharpen=opts.sharpen,
|
||||||
|
sharpen_radius=opts.sharpen_radius,
|
||||||
|
sharpen_amount=opts.sharpen_amount,
|
||||||
|
sharpen_threshold=opts.sharpen_threshold,
|
||||||
|
grayscale=opts.grayscale,
|
||||||
|
invert=opts.invert,
|
||||||
|
use_background=False,
|
||||||
|
background_scale=opts.background_scale,
|
||||||
|
annotation_text=opts.annotation_text,
|
||||||
|
annotation_pos=opts.annotation_pos,
|
||||||
|
annotation_size=opts.annotation_size,
|
||||||
|
extra_args=opts.extra_args,
|
||||||
|
)
|
||||||
|
cmd = build_imagemagick_cmd(path, tmp_subject, opts_main)
|
||||||
|
cmd_str = " ".join(cmd)
|
||||||
|
cmd_str += "\n# Note: Granite background compositing is run as a second IM command on Process."
|
||||||
|
else:
|
||||||
|
cmd = build_imagemagick_cmd(path, dest, opts)
|
||||||
|
cmd_str = " ".join(cmd)
|
||||||
|
|
||||||
|
self.current_cmd_str = cmd_str
|
||||||
|
self.cmd_text.configure(state="normal")
|
||||||
|
self.cmd_text.delete("1.0", tk.END)
|
||||||
|
self.cmd_text.insert("1.0", cmd_str)
|
||||||
|
self.cmd_text.configure(state="disabled")
|
||||||
|
|
||||||
|
def run_im_preview(self):
|
||||||
|
"""
|
||||||
|
Generate a preview using the *actual* ImageMagick pipeline,
|
||||||
|
but resized down to roughly the preview canvas size for speed.
|
||||||
|
|
||||||
|
It uses the same options as Process (including crop, rotation, etc.),
|
||||||
|
writes to a temporary JPEG, and displays that in the Preview canvas.
|
||||||
|
"""
|
||||||
|
if not self.state.files or self.state.base_image is None:
|
||||||
|
return
|
||||||
|
if not self.state.directory:
|
||||||
|
messagebox.showerror("Error", "No directory selected.")
|
||||||
|
return
|
||||||
|
|
||||||
|
path = self.state.files[self.state.current_index]
|
||||||
|
opts = self.gather_options_from_ui(include_crop=True)
|
||||||
|
|
||||||
|
# Determine preview size from preview canvas
|
||||||
|
canvas_w = self.canvas_prev.winfo_width() or 800
|
||||||
|
canvas_h = self.canvas_prev.winfo_height() or 450
|
||||||
|
resize_arg = f"{canvas_w}x{canvas_h}"
|
||||||
|
|
||||||
|
# We'll add a "-resize WxH" at the end of the main subject command.
|
||||||
|
# That way, all IM operations are done at preview resolution.
|
||||||
|
opts_preview = EditOptions(
|
||||||
|
crop_box=opts.crop_box,
|
||||||
|
rotation_degrees=opts.rotation_degrees,
|
||||||
|
color_correction=opts.color_correction,
|
||||||
|
sigmoidal=opts.sigmoidal,
|
||||||
|
sigmoidal_contrast=opts.sigmoidal_contrast,
|
||||||
|
sigmoidal_midpoint=opts.sigmoidal_midpoint,
|
||||||
|
sharpen=opts.sharpen,
|
||||||
|
sharpen_radius=opts.sharpen_radius,
|
||||||
|
sharpen_amount=opts.sharpen_amount,
|
||||||
|
sharpen_threshold=opts.sharpen_threshold,
|
||||||
|
grayscale=opts.grayscale,
|
||||||
|
invert=opts.invert,
|
||||||
|
use_background=False, # background handled separately below
|
||||||
|
background_scale=opts.background_scale,
|
||||||
|
annotation_text=opts.annotation_text,
|
||||||
|
annotation_pos=opts.annotation_pos,
|
||||||
|
annotation_size=opts.annotation_size,
|
||||||
|
extra_args=["-resize", resize_arg],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Temporary files for subject and final preview
|
||||||
|
tmp_subject = None
|
||||||
|
tmp_preview = None
|
||||||
|
try:
|
||||||
|
if not opts.use_background:
|
||||||
|
# Single-command preview
|
||||||
|
with tempfile.NamedTemporaryFile(suffix="_impreview.jpg", delete=False) as tf:
|
||||||
|
tmp_preview = Path(tf.name)
|
||||||
|
cmd = build_imagemagick_cmd(path, tmp_preview, opts_preview)
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, check=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
messagebox.showerror("IM Preview error", f"IM preview failed:\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Two-step: subject + granite background
|
||||||
|
with tempfile.NamedTemporaryFile(suffix="_subject_impreview.jpg", delete=False) as tf1:
|
||||||
|
tmp_subject = Path(tf1.name)
|
||||||
|
with tempfile.NamedTemporaryFile(suffix="_impreview.jpg", delete=False) as tf2:
|
||||||
|
tmp_preview = Path(tf2.name)
|
||||||
|
|
||||||
|
# 1) Subject at preview resolution
|
||||||
|
cmd1 = build_imagemagick_cmd(path, tmp_subject, opts_preview)
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd1, check=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
messagebox.showerror("IM Preview error", f"IM subject preview failed:\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2) Background + subject composite
|
||||||
|
try:
|
||||||
|
with Image.open(tmp_subject) as s_img:
|
||||||
|
subject_size = s_img.size
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("IM Preview error", f"Failed to open subject preview:\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd2 = build_granite_background_cmd(tmp_subject, tmp_preview, opts, subject_size)
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd2, check=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
messagebox.showerror("IM Preview error", f"IM background preview failed:\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Load and display the resulting preview JPEG
|
||||||
|
if tmp_preview and tmp_preview.exists():
|
||||||
|
try:
|
||||||
|
with Image.open(tmp_preview) as im_prev:
|
||||||
|
im_prev = im_prev.convert("RGB")
|
||||||
|
self.display_on_canvas(self.canvas_prev, im_prev, is_original=False)
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("IM Preview error", f"Failed to load preview image:\n{e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Clean up temp files
|
||||||
|
try:
|
||||||
|
if tmp_subject and tmp_subject.exists():
|
||||||
|
tmp_subject.unlink()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if tmp_preview and tmp_preview.exists():
|
||||||
|
tmp_preview.unlink()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def copy_cmd_to_clipboard(self):
|
||||||
|
if not self.current_cmd_str:
|
||||||
|
return
|
||||||
|
self.clipboard_clear()
|
||||||
|
self.clipboard_append(self.current_cmd_str)
|
||||||
|
self.update() # make sure clipboard is updated
|
||||||
|
|
||||||
# -------------------------- processing (IM) ------------------------------
|
# -------------------------- processing (IM) ------------------------------
|
||||||
|
|
||||||
def process_current(self):
|
def process_current(self):
|
||||||
|
|
@ -867,7 +1088,6 @@ class WImageEditTk(tk.Tk):
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd1 = build_imagemagick_cmd(path, tmp_subject, opts_main)
|
cmd1 = build_imagemagick_cmd(path, tmp_subject, opts_main)
|
||||||
print(cmd1)
|
|
||||||
if not self.confirm_and_run(cmd1):
|
if not self.confirm_and_run(cmd1):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -880,7 +1100,6 @@ class WImageEditTk(tk.Tk):
|
||||||
return
|
return
|
||||||
|
|
||||||
cmd2 = build_granite_background_cmd(tmp_subject, dest, opts, subject_size)
|
cmd2 = build_granite_background_cmd(tmp_subject, dest, opts, subject_size)
|
||||||
print(cmd2)
|
|
||||||
if not self.confirm_and_run(cmd2):
|
if not self.confirm_and_run(cmd2):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -916,11 +1135,10 @@ class WImageEditTk(tk.Tk):
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("start ...")
|
|
||||||
app = WImageEditTk()
|
app = WImageEditTk()
|
||||||
app.mainloop()
|
app.mainloop()
|
||||||
print("done.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,8 @@ def build_imagemagick_cmd(
|
||||||
|
|
||||||
# Tone & color operations
|
# Tone & color operations
|
||||||
if opts.color_correction:
|
if opts.color_correction:
|
||||||
cmd.extend(["-channel", "RGB", "-auto-level", "-auto-gamma"])
|
# cmd.extend(["-channel", "RGB", "-auto-level", "-auto-gamma"])
|
||||||
|
cmd.extend(["-channel", "RGB", "-auto-level", "-contrast-stretch"])
|
||||||
|
|
||||||
if opts.sigmoidal:
|
if opts.sigmoidal:
|
||||||
# Clamp / format midpoint to something IM likes: 0–1
|
# Clamp / format midpoint to something IM likes: 0–1
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue