320 lines
12 KiB
Python
Executable file
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
|