From c823b5f9925ed32821082c297f775950bfd4796d Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 23 Jul 2009 10:52:05 -0500 Subject: [PATCH] gtk version of cropgui --- cropgtk.py | 230 +++++++++++++++++++++++++++++++++++++++++++++++++ cropgui.gladep | 8 ++ filechooser.py | 79 +++++++++++++++++ install.sh | 8 +- 4 files changed, 321 insertions(+), 4 deletions(-) create mode 100755 cropgtk.py create mode 100644 cropgui.gladep create mode 100644 filechooser.py diff --git a/cropgtk.py b/cropgtk.py new file mode 100755 index 0000000..2406e63 --- /dev/null +++ b/cropgtk.py @@ -0,0 +1,230 @@ +#!/usr/bin/python +# cropgui, a graphical front-end for lossless jpeg cropping +# Copyright (C) 2009 Jeff Epler +# 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 diff --git a/cropgui.gladep b/cropgui.gladep new file mode 100644 index 0000000..c033dd7 --- /dev/null +++ b/cropgui.gladep @@ -0,0 +1,8 @@ + + + + + Cropgui + cropgui + FALSE + diff --git a/filechooser.py b/filechooser.py new file mode 100644 index 0000000..739366b --- /dev/null +++ b/filechooser.py @@ -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() diff --git a/install.sh b/install.sh index ecfd3f2..0444335 100755 --- a/install.sh +++ b/install.sh @@ -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