gtk version of cropgui
This commit is contained in:
parent
5e1082b8f7
commit
c823b5f992
4 changed files with 321 additions and 4 deletions
230
cropgtk.py
Executable file
230
cropgtk.py
Executable file
|
|
@ -0,0 +1,230 @@
|
|||
#!/usr/bin/python
|
||||
# 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 gobject
|
||||
import gtk
|
||||
import gtk.glade
|
||||
|
||||
import filechooser
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
# otherwise, on hardy the user is shown spurious "[application] closed
|
||||
# unexpectedly" messages but denied the ability to actually "report [the]
|
||||
# problem"
|
||||
def excepthook(exc_type, exc_obj, exc_tb):
|
||||
try:
|
||||
w = app['window1']
|
||||
except NameError:
|
||||
w = None
|
||||
lines = traceback.format_exception(exc_type, exc_obj, exc_tb)
|
||||
m = gtk.MessageDialog(w,
|
||||
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
|
||||
gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
|
||||
_("Stepconf encountered an error. The following "
|
||||
"information may be useful in troubleshooting:\n\n")
|
||||
+ "".join(lines))
|
||||
m.show()
|
||||
m.run()
|
||||
m.destroy()
|
||||
sys.excepthook = excepthook
|
||||
|
||||
import cropgui_common
|
||||
gladefile = os.path.join(os.path.dirname(cropgui_common.__file__),
|
||||
"cropgui.glade")
|
||||
|
||||
class DragManager(DragManagerBase):
|
||||
def __init__(self, g):
|
||||
self.g = g
|
||||
self.idle = None
|
||||
DragManagerBase.__init__(self)
|
||||
|
||||
w = g['window1']
|
||||
i = g['eventbox1']
|
||||
|
||||
i.connect('button-press-event', self.press)
|
||||
i.connect('motion-notify-event', self.motion)
|
||||
i.connect('button-release-event', self.release)
|
||||
w.connect('delete-event', self.close)
|
||||
w.connect('key-press-event', self.key)
|
||||
g['toolbutton1'].connect('clicked', self.done)
|
||||
g['toolbutton2'].connect('clicked', self.escape)
|
||||
|
||||
def coords(self, event):
|
||||
return event.x, event.y
|
||||
|
||||
def press(self, w, event):
|
||||
if event.type == gtk.gdk._2BUTTON_PRESS:
|
||||
return self.done()
|
||||
x, y = self.coords(event)
|
||||
self.drag_start(x, y, event.state & gtk.gdk.SHIFT_MASK)
|
||||
|
||||
def motion(self, w, event):
|
||||
x, y = self.coords(event)
|
||||
self.drag_continue(x, y)
|
||||
|
||||
def release(self, w, event):
|
||||
x, y = self.coords(event)
|
||||
self.drag_end(x, y)
|
||||
|
||||
def done(self, *args):
|
||||
self.result = 1
|
||||
self.loop.quit()
|
||||
|
||||
def escape(self, *args):
|
||||
self.result = 0
|
||||
self.loop.quit()
|
||||
|
||||
def close(self, *args):
|
||||
self.result = -1
|
||||
self.loop.quit()
|
||||
|
||||
def key(self, w, e):
|
||||
if e.keyval == gtk.keysyms.Escape: self.escape()
|
||||
elif e.keyval == gtk.keysyms.Return: self.done()
|
||||
|
||||
def image_set(self):
|
||||
self.render()
|
||||
|
||||
def render(self):
|
||||
if self.idle is None:
|
||||
self.idle = gobject.idle_add(self.do_render)
|
||||
|
||||
def do_render(self):
|
||||
if not self.idle:
|
||||
return
|
||||
self.idle = None
|
||||
|
||||
g = self.g
|
||||
i = g['image1']
|
||||
if not i: # app shutting down
|
||||
return
|
||||
|
||||
if self.image is None:
|
||||
pixbuf = gtk.gdk.pixbuf_new_from_data('\0\0\0',
|
||||
gtk.gdk.COLORSPACE_RGB, 0, 8, 1, 1, 3)
|
||||
i.set_from_pixbuf(pixbuf)
|
||||
#i.set_size_request(max_w, max_h)
|
||||
g['pos_left'].set_text('---')
|
||||
g['pos_right'].set_text('---')
|
||||
g['pos_top'].set_text('---')
|
||||
g['pos_bottom'].set_text('---')
|
||||
g['pos_width'].set_text('---')
|
||||
g['pos_height'].set_text('---')
|
||||
g['pos_ratio'].set_text('---')
|
||||
|
||||
else:
|
||||
rendered = self.rendered()
|
||||
rendered = rendered.convert('RGB')
|
||||
i.set_size_request(*rendered.size)
|
||||
pixbuf = gtk.gdk.pixbuf_new_from_data(rendered.tostring(),
|
||||
gtk.gdk.COLORSPACE_RGB, 0, 8,
|
||||
rendered.size[0], rendered.size[1], 3*rendered.size[0])
|
||||
|
||||
ll, tt, rr, bb = self.get_corners()
|
||||
ratio = self.describe_ratio()
|
||||
|
||||
g['pos_left'].set_text('%d' % ll)
|
||||
g['pos_right'].set_text('%d' % rr)
|
||||
g['pos_top'].set_text('%d' % tt)
|
||||
g['pos_bottom'].set_text('%d' % bb)
|
||||
g['pos_width'].set_text('%d' % (rr-ll))
|
||||
g['pos_height'].set_text('%d' % (bb-tt))
|
||||
g['pos_ratio'].set_text(self.describe_ratio())
|
||||
|
||||
i.set_from_pixbuf(pixbuf)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def wait(self):
|
||||
self.loop = gobject.MainLoop()
|
||||
self.result = -1
|
||||
self.loop.run()
|
||||
|
||||
max_h = gtk.gdk.screen_height() - 64 - 64
|
||||
max_w = gtk.gdk.screen_width() - 64
|
||||
|
||||
class App:
|
||||
def __init__(self):
|
||||
self.glade = gtk.glade.XML(gladefile)
|
||||
self.drag = DragManager(self)
|
||||
self.task = CropTask(self)
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self.glade.get_widget(name)
|
||||
|
||||
def log(self, msg):
|
||||
s = self['statusbar1']
|
||||
if s:
|
||||
s.pop(0)
|
||||
s.push(0, msg)
|
||||
progress = log
|
||||
|
||||
def set_busy(self, is_busy=True):
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
drag = self.drag
|
||||
task = self.task
|
||||
|
||||
for image_name in self.image_names():
|
||||
self.set_busy()
|
||||
i = Image.open(image_name)
|
||||
iw, ih = i.size
|
||||
scale = 1
|
||||
while iw > max_w or ih > max_h:
|
||||
iw /= 2
|
||||
ih /= 2
|
||||
scale *= 2
|
||||
i.thumbnail((iw, ih))
|
||||
drag.image = i
|
||||
drag.round = max(1, 8/scale)
|
||||
drag.scale = scale
|
||||
self.set_busy(0)
|
||||
v = self.drag.wait()
|
||||
self.set_busy()
|
||||
if v == -1: break # user closed app
|
||||
if v == 0: continue # user hit "next" / escape
|
||||
|
||||
base, ext = os.path.splitext(image_name)
|
||||
t, l, r, b = drag.top, drag.left, drag.right, drag.bottom
|
||||
t *= scale
|
||||
l *= scale
|
||||
r *= scale
|
||||
b *= scale
|
||||
cropspec = "%dx%d+%d+%d" % (r-l, b-t, l, t)
|
||||
target = base + "-crop" + ext
|
||||
task.add(['nice', 'jpegtran', '-crop', cropspec, image_name], target)
|
||||
|
||||
def image_names(self):
|
||||
if len(sys.argv) > 1:
|
||||
return sys.argv[1:]
|
||||
else:
|
||||
return filechooser.prompt_open()
|
||||
|
||||
app = App()
|
||||
try:
|
||||
app.run()
|
||||
finally:
|
||||
app.task.done()
|
||||
del app.task
|
||||
del app.drag
|
||||
8
cropgui.gladep
Normal file
8
cropgui.gladep
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
|
||||
<!DOCTYPE glade-project SYSTEM "http://glade.gnome.org/glade-project-2.0.dtd">
|
||||
|
||||
<glade-project>
|
||||
<name>Cropgui</name>
|
||||
<program_name>cropgui</program_name>
|
||||
<gnome_support>FALSE</gnome_support>
|
||||
</glade-project>
|
||||
79
filechooser.py
Normal file
79
filechooser.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
#!/usr/bin/env python
|
||||
PREVIEW_SIZE = 300
|
||||
|
||||
import pygtk
|
||||
pygtk.require('2.0')
|
||||
|
||||
import gtk
|
||||
import gobject
|
||||
|
||||
import os
|
||||
import Image
|
||||
|
||||
HIGH_WATER, LOW_WATER = 25, 5
|
||||
image_cache = {}
|
||||
def update_preview_cb(file_chooser, preview):
|
||||
file_chooser.set_preview_widget_active(True)
|
||||
filename = file_chooser.get_preview_filename()
|
||||
if os.path.isdir(filename):
|
||||
preview.set_from_stock(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_LARGE_TOOLBAR)
|
||||
elif filename in image_cache:
|
||||
preview.set_from_pixbuf(image_cache[filename])
|
||||
else:
|
||||
try:
|
||||
i = Image.open(filename)
|
||||
w, h = i.size
|
||||
i.thumbnail((PREVIEW_SIZE, PREVIEW_SIZE), Image.ANTIALIAS)
|
||||
i = i.convert('RGB')
|
||||
pixbuf = gtk.gdk.pixbuf_new_from_data(i.tostring(),
|
||||
gtk.gdk.COLORSPACE_RGB, 0, 8, i.size[0], i.size[1],
|
||||
i.size[0]*3)
|
||||
preview.set_from_pixbuf(pixbuf)
|
||||
if len(image_cache) > HIGH_WATER:
|
||||
while len(image_cache) > LOW_WATER:
|
||||
image_cache.popitem()
|
||||
image_cache[filename] = pixbuf
|
||||
except:
|
||||
preview.set_from_stock(gtk.STOCK_MISSING_IMAGE,
|
||||
gtk.ICON_SIZE_LARGE_TOOLBAR)
|
||||
raise
|
||||
|
||||
def prompt_open():
|
||||
dialog = gtk.FileChooserDialog("Open..",
|
||||
None,
|
||||
gtk.FILE_CHOOSER_ACTION_OPEN,
|
||||
(gtk.STOCK_QUIT, gtk.RESPONSE_CANCEL,
|
||||
gtk.STOCK_OPEN, gtk.RESPONSE_OK))
|
||||
dialog.set_default_response(gtk.RESPONSE_OK)
|
||||
dialog.set_select_multiple(True)
|
||||
|
||||
preview = gtk.Image()
|
||||
preview.set_size_request(PREVIEW_SIZE, PREVIEW_SIZE)
|
||||
|
||||
dialog.set_preview_widget(preview)
|
||||
dialog.connect("update-preview", update_preview_cb, preview)
|
||||
|
||||
filter = gtk.FileFilter()
|
||||
filter.set_name("JPEG Images")
|
||||
filter.add_mime_type("image/jpeg")
|
||||
filter.add_pattern("*.jpg")
|
||||
filter.add_pattern("*.jpeg")
|
||||
filter.add_pattern("*.JPG")
|
||||
filter.add_pattern("*.JPEG")
|
||||
dialog.add_filter(filter)
|
||||
|
||||
filter = gtk.FileFilter()
|
||||
filter.set_name("All files")
|
||||
filter.add_pattern("*")
|
||||
dialog.add_filter(filter)
|
||||
|
||||
response = dialog.run()
|
||||
if response == gtk.RESPONSE_OK:
|
||||
result = dialog.get_filenames()
|
||||
else:
|
||||
result = []
|
||||
dialog.destroy()
|
||||
return result
|
||||
|
||||
if __name__ == '__main__':
|
||||
print prompt_open()
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
#!/bin/sh
|
||||
cp cropgui.py $HOME/bin/cropgui
|
||||
cp log.py cropgui_common.py $HOME/lib/python
|
||||
if ! python -c 'import log' 2>&1; then
|
||||
echo "Failed to import log.py: add $HOME/lib/python to PYTHONPATH"
|
||||
cp cropgtk.py $HOME/bin/cropgui
|
||||
cp cropgui_common.py filechooser.py $HOME/lib/python
|
||||
if ! python -c 'import filechooser' 2>&1; then
|
||||
echo "Failed to import filechooser.py: add $HOME/lib/python to PYTHONPATH"
|
||||
fi
|
||||
chmod +x $HOME/bin/cropgui
|
||||
# installation script for cropgui, a graphical front-end for lossless jpeg
|
||||
|
|
|
|||
Loading…
Reference in a new issue