cropgui/cropgui.py

320 lines
12 KiB
Python
Executable file

#!/usr/bin/python3
# cropgui, a graphical front-end for lossless jpeg cropping
# Copyright (C) 2009 Jeff Epler <jepler@unpythonic.net>
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
from cropgui_common import *
from cropgui_common import _
import tkinter
from PIL import ImageTk
import tkinter.filedialog
import sys
import os
import signal
import log
app = tkinter.Tk()
app.wm_title(_("CropGUI -- lossless cropping and rotation of jpeg files"))
app.wm_iconname(_("CropGUI"))
preview = tkinter.Label(app)
preview.pack(side="bottom")
do_crop = tkinter.Button(app, text="Crop")
do_crop.pack(side="left")
crop169_button = tkinter.Menubutton(app, text="16:9")
crop169_button.pack(side="left")
crop169 = tkinter.Menu(crop169_button)
crop169_button.config(menu=crop169)
crop85_button = tkinter.Menubutton(app, text="8:5")
crop85_button.pack(side="left")
crop85 = tkinter.Menu(crop85_button)
crop85_button.config(menu=crop85)
crop32_button = tkinter.Menubutton(app, text="3:2")
crop32_button.pack(side="left")
crop32 = tkinter.Menu(crop32_button)
crop32_button.config(menu=crop32)
crop43_button = tkinter.Menubutton(app, text="4:3")
crop43_button.pack(side="left")
crop43 = tkinter.Menu(crop43_button)
crop43_button.config(menu=crop43)
crop11_button = tkinter.Menubutton(app, text="1:1")
crop11_button.pack(side="left")
crop11 = tkinter.Menu(crop11_button)
crop11_button.config(menu=crop11)
crop34_button = tkinter.Menubutton(app, text="3:4")
crop34_button.pack(side="left")
crop34 = tkinter.Menu(crop34_button)
crop34_button.config(menu=crop34)
crop23_button = tkinter.Menubutton(app, text="2:3")
crop23_button.pack(side="left")
crop23 = tkinter.Menu(crop23_button)
crop23_button.config(menu=crop23)
info = tkinter.Label(app)
info.pack(side="left")
task = CropTask(log)
class DragManager(DragManagerBase):
def __init__(self, w, b, inf):
self.l = w
b.configure(command=self.done)
self.inf = inf
w.bind("<Button-1>", self.press)
w.bind("<Shift-Button-1>", self.shift_press)
w.bind("<Double-Button-1>", self.double)
w.bind("<Button1-Motion>", self.motion)
w.bind("<Motion>", self.idle_motion)
w.bind("<ButtonRelease-1>", self.end)
w.bind("<Enter>", self.enter)
w.bind("<Leave>", self.leave)
app.bind("<Return>", self.double)
app.bind("<Escape>", self.escape)
w.bind("<Button1-Enter>", "#nothing")
w.bind("<Button1-Leave>", "#nothing")
dummy_image = Image.new('L', (int(max_w/2),int(max_h/2)), 0xff)
self.dummy_tkimage = ImageTk.PhotoImage(dummy_image)
self.v = tkinter.IntVar(app)
DragManagerBase.__init__(self)
def image_set(self):
if self.image is None:
self.l.configure(image=self.dummy_tkimage)
self.inf.configure(text="\n\n")
else:
self.tkimage = ImageTk.PhotoImage(self.image)
self.l.configure(image=self.tkimage)
self.render()
def render(self):
if not self.render_flag:
self.render_flag = True
app.after_idle(self.do_render)
def do_render(self):
if not self.render_flag:
return
self.render_flag = False
if self.image is None:
self.l.configure(image=self.dummy_tkimage)
self.inf.configure(text="\n\n")
return
tt, ll, rr, bb = self.get_corners()
ratio = self.describe_ratio()
self.inf.configure(text=
"Left: %4d Top: %4d Right: %4d Bottom: %4d\n"
"Width: %4d Height: %4d Ratio: %8s\n"
% (ll, tt, rr, bb, rr-ll, bb-tt, ratio),
font="fixed", justify="l", anchor="w")
self.tkimage.paste(self.rendered())
def enter(self, event):
self.show_handles = True
self.render()
def leave(self, event):
self.show_handles = False
self.render()
def press(self, event): self.drag_start(event.x, event.y, False)
def shift_press(self, event): self.drag_start(event.x, event.y, True)
def idle_motion(self, event):
what = self.classify(event.x, event.y)
if busy:
cursor = "watch"
else:
cursor = self.cursor_map.get(what, "")
self.l.configure(cursor=cursor)
def motion(self, event):
self.drag_continue(event.x, event.y)
def end(self, event):
self.drag_end(event.x, event.y)
def close(self):
self.v.set(-1)
def cancel(self):
self.v.set(0)
def escape(self, event):
self.cancel()
def done(self):
self.v.set(1)
def double(self, event):
self.done()
def wait(self):
app.wait_variable(self.v)
value = self.v.get()
return value
max_h = app.winfo_screenheight() - 64 - 32
max_w = app.winfo_screenwidth() - 64
drag = DragManager(preview, do_crop, info)
app.wm_protocol('WM_DELETE_WINDOW', drag.close)
crop169.add_command(label='1920 x 1080',command=lambda: drag.set_stdsize(1920,1080))
crop169.add_command(label='3840 x 2160',command=lambda: drag.set_stdsize(3840,2160))
crop169.add_command(label='4000 x 2248',command=lambda: drag.set_stdsize(4000,2248))
crop85.add_command (label='1920 x 1200',command=lambda: drag.set_stdsize(1920,1200))
crop85.add_command (label='3456 x 2160',command=lambda: drag.set_stdsize(3456,2160))
crop85.add_command (label='4000 x 2496',command=lambda: drag.set_stdsize(4000,2496))
crop32.add_command (label='1136 x 760',command=lambda: drag.set_stdsize(1136, 760))
crop32.add_command (label='1440 x 960',command=lambda: drag.set_stdsize(1440, 960))
crop32.add_command (label='1536 x 1024',command=lambda: drag.set_stdsize(1536,1024))
crop32.add_command (label='1752 x 1168',command=lambda: drag.set_stdsize(1752,1168))
crop32.add_command (label='2048 x 1360',command=lambda: drag.set_stdsize(2048,1360))
crop32.add_command (label='2592 x 1728',command=lambda: drag.set_stdsize(2592,1728))
crop32.add_command (label='3072 x 2048',command=lambda: drag.set_stdsize(3072,2048))
crop32.add_command (label='3240 x 2160',command=lambda: drag.set_stdsize(3240,2160))
crop32.add_command (label='4000 x 2664',command=lambda: drag.set_stdsize(4000,2664))
crop43.add_command (label='1280 x 960',command=lambda: drag.set_stdsize(1280, 960))
crop43.add_command (label='1600 x 1200',command=lambda: drag.set_stdsize(1600,1200))
crop43.add_command (label='1720 x 1280',command=lambda: drag.set_stdsize(1720,1280))
crop43.add_command (label='2048 x 1536',command=lambda: drag.set_stdsize(2048,1536))
crop43.add_command (label='2560 x 1920',command=lambda: drag.set_stdsize(2560,1920))
crop43.add_command (label='2880 x 2160',command=lambda: drag.set_stdsize(2880,2160))
crop43.add_command (label='4000 x 3000',command=lambda: drag.set_stdsize(4000,3000))
crop43.add_command (label='4320 x 3240',command=lambda: drag.set_stdsize(4320,3240))
crop11.add_command (label='3000 x 3000',command=lambda: drag.set_stdsize(3000,3000))
crop34.add_command (label=' 960 x 1280',command=lambda: drag.set_stdsize( 960,1280))
crop34.add_command (label='1200 x 1600',command=lambda: drag.set_stdsize(1200,1600))
crop34.add_command (label='1280 x 1720',command=lambda: drag.set_stdsize(1280,1720))
crop34.add_command (label='1536 x 2048',command=lambda: drag.set_stdsize(1536,2048))
crop34.add_command (label='1920 x 2560',command=lambda: drag.set_stdsize(1920,2560))
crop34.add_command (label='2160 x 2880',command=lambda: drag.set_stdsize(2160,2880))
crop34.add_command (label='3000 x 4000',command=lambda: drag.set_stdsize(3000,4000))
crop34.add_command (label='3240 x 4320',command=lambda: drag.set_stdsize(3240,4320))
crop23.add_command (label=' 760 x 1136',command=lambda: drag.set_stdsize( 760,1136))
crop23.add_command (label=' 960 x 1440',command=lambda: drag.set_stdsize( 960,1440))
crop23.add_command (label='1024 x 1536',command=lambda: drag.set_stdsize(1024,1536))
crop23.add_command (label='1168 x 1752',command=lambda: drag.set_stdsize(1168,1752))
crop23.add_command (label='1360 x 2048',command=lambda: drag.set_stdsize(1360,2048))
crop23.add_command (label='1728 x 2592',command=lambda: drag.set_stdsize(1728,2592))
crop23.add_command (label='2048 x 3072',command=lambda: drag.set_stdsize(2048,3072))
crop23.add_command (label='2664 x 4000',command=lambda: drag.set_stdsize(2664,4000))
def image_names():
if len(sys.argv) > 1:
for i in sys.argv[1:]: yield i
else:
while 1:
names = tkinter.filedialog.askopenfilenames(master=app,
defaultextension=".jpg", multiple=1, parent=app,
filetypes=(
(_("JPEG Image Files"), ".jpg .JPG .jpeg .JPEG"),
(_("All files"), "*"),
),
title=_("Select images to crop"))
if not names: break
for name in names: yield name
pids = set()
def reap():
global pids
pids = set(p for p in pids if p.poll() is None)
def set_busy(new_busy=True):
global busy
busy = new_busy
if busy:
drag.l.configure(cursor="watch")
app.configure(cursor="watch")
do_crop.configure(state="disabled")
else:
drag.l.configure(cursor="")
app.configure(cursor="")
do_crop.configure(state="normal")
app.update_idletasks()
cropped_any_image = False
try:
# Check *only* the first arg to see if it is -round-rb
round_right_and_bottom = False
if sys.argv[1] == "-round-rb":
round_right_and_bottom = True
args = [arg for arg in sys.argv[0:] if arg != "-round-rb"]
sys.argv = args
for image_name in image_names():
drag.save_prev_crop()
drag.round_right_and_bottom = round_right_and_bottom
# load new image
set_busy()
image = Image.open(image_name)
drag.round_x, drag.round_y = image_round(image)
drag.w, drag.h = image.size
# compute scale to fit image on display
drag.scale=1
drag.scale = max (drag.scale, (drag.w-1)//max_w+1)
drag.scale = max (drag.scale, (drag.h-1)//max_h+1)
# put image into drag object
thumbnail = image.copy()
thumbnail.thumbnail((drag.w//drag.scale, drag.h//drag.scale))
drag.image = thumbnail
# get user input
set_busy(0)
v = drag.wait()
set_busy()
if v == -1: break # user closed app
if v == 0: continue # user hit "next" / escape
base, ext = os.path.splitext(image_name)
target = base + "-crop" + ext
task.add(CropRequest(
image=image,
image_name=image_name,
corners=drag.get_corners(),
rotation=drag.rotation,
target=target,
))
cropped_any_image = True
finally:
task.done()
if not cropped_any_image:
raise SystemExit(1)
# 1. open image
# 2. choose 1/2, 1/4, 1/8 scaling so that resized image fits onscreen
# 3. load image at requested size
# 4. run GUI to get desired crop settings
# 5. write output file