From 3a75980d823e2e331d852cf2f00224379d5fddc4 Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Tue, 14 Mar 2017 21:45:11 +0100 Subject: [PATCH 01/58] fix typos --- cropgui_common.py | 4 ++-- debian/changelog | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cropgui_common.py b/cropgui_common.py index ba338a3..100522c 100644 --- a/cropgui_common.py +++ b/cropgui_common.py @@ -152,7 +152,7 @@ def describe_ratio(self): return describe_ratio(w, h) def set_stdsize(self, x, y): - # if frame doesn't fit in image, scale, preserving apect ratio + # if frame doesn't fit in image, scale, preserving aspect ratio if (x > self.w): y = y * self.w / x x = self.w @@ -166,7 +166,7 @@ def set_stdsize(self, x, y): top = (self.top + self.bottom - y) / 2 bottom = top + y - # move crop area into the image, if necessairy + # move crop area into the image, if necessary if (left < 0): left = 0 right = x diff --git a/debian/changelog b/debian/changelog index ad6598c..147abd4 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,6 +1,6 @@ cropgui (0.2+git27ee40aeee) hardy; urgency=low - * use absoloute paths + * use absolute paths * don't apply scaling to jpegtrans dimensions -- Reuben Thu, 30 Jul 2015 07:04:00 +1000 @@ -17,7 +17,7 @@ cropgui (0.2) hardy; urgency=low cropgui (0.1.1) hardy; urgency=low - * fix dash-compatability of build script + * fix dash-compatibility of build script -- Jeff Epler Sun, 26 Jul 2009 12:18:37 -0500 From a697dc4747993583176484df1599bbb758343b12 Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Wed, 15 Mar 2017 14:29:34 +0100 Subject: [PATCH 02/58] strip trailing whitespace --- cropgtk.py | 10 +++++----- cropgui_common.py | 2 +- filechooser.py | 2 +- log.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cropgtk.py b/cropgtk.py index f7723ee..6ba198a 100755 --- a/cropgtk.py +++ b/cropgtk.py @@ -140,7 +140,7 @@ def key(self, w, e): def image_set(self): self.render() - + def render(self): if self.idle is None: self.idle = gobject.idle_add(self.do_render) @@ -267,7 +267,7 @@ def run(self): if v == 0: self.log("Skipped %s" % os.path.basename(image_name)) continue # user hit "next" / escape - + t, l, r, b = drag.top, drag.left, drag.right, drag.bottom cropspec = "%dx%d+%d+%d" % (r-l, b-t, l, t) @@ -287,7 +287,7 @@ def run(self): if not rotation == "none": command.extend(['-rotate', rotation]) command.extend(['-copy', 'all', '-crop', cropspec,'-outfile', target, image_name]) # All other images use imagemagic convert. - else: + else: command = ['nice', 'convert'] if not rotation == "none": command.extend(['-rotate', rotation]) command.extend([image_name, '-crop', cropspec, target]) @@ -308,7 +308,7 @@ def output_name(self, image_name, image_type): image_name = os.path.abspath(image_name) d = os.path.dirname(image_name) i = os.path.basename(image_name) - j = os.path.splitext(i)[0].lower() + j = os.path.splitext(i)[0].lower() if j.endswith('-crop'): j += os.path.splitext(i)[1] else: j += "-crop" + os.path.splitext(i)[1] if os.access(d, os.W_OK): return os.path.join(d, j) @@ -323,7 +323,7 @@ def output_name(self, image_name, image_type): if not r: return '' r = r[0] e = os.path.splitext(r)[1] - if image_type == "jpeg": + if image_type == "jpeg": if e.lower() in ['.jpg', '.jpeg']: return r return e + ".jpg" elif e.lower() == image_type: return r diff --git a/cropgui_common.py b/cropgui_common.py index 100522c..0025b07 100644 --- a/cropgui_common.py +++ b/cropgui_common.py @@ -179,7 +179,7 @@ def set_stdsize(self, x, y): if (bottom > self.h): bottom = self.h top = bottom - y - + self.set_crop (top, left, right, bottom) def set_crop(self, top, left, right, bottom): diff --git a/filechooser.py b/filechooser.py index 09f55a0..eb36194 100644 --- a/filechooser.py +++ b/filechooser.py @@ -53,7 +53,7 @@ def update_preview_cb(file_chooser, preview): image_data = i.tostring() except: image_data = i.tobytes() - pixbuf = gtk.gdk.pixbuf_new_from_data(image_data, + pixbuf = gtk.gdk.pixbuf_new_from_data(image_data, gtk.gdk.COLORSPACE_RGB, 0, 8, i.size[0], i.size[1], i.size[0]*3) preview.set_from_pixbuf(pixbuf) diff --git a/log.py b/log.py index 3008aee..d010bea 100644 --- a/log.py +++ b/log.py @@ -54,7 +54,7 @@ def progress(message, *args): message += " " * (last_width - width) sys.stderr.write(message + "\r") sys.stderr.flush() - last_width = width + last_width = width @locked def log(message, *args): From fbcda379b2ff86a27f0a444b1b3e7d88fbc05daa Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Wed, 15 Mar 2017 14:33:57 +0100 Subject: [PATCH 03/58] remove unused code This code uses prompt_open(), which was removed in 45eaaedd7519305345a9e5b1945ec705f1970df8. --- filechooser.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/filechooser.py b/filechooser.py index 09f55a0..8c31975 100644 --- a/filechooser.py +++ b/filechooser.py @@ -141,6 +141,3 @@ def set_title(self, title): def set_current_folder(self, directory): self.dialog.set_current_folder(directory) - -if __name__ == '__main__': - print prompt_open() From 8e596e0657353139af8ed35893c5fcdc3e8e85f3 Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Wed, 15 Mar 2017 14:44:54 +0100 Subject: [PATCH 04/58] remove duplicated fcntl import --- log.py | 1 - 1 file changed, 1 deletion(-) diff --git a/log.py b/log.py index 3008aee..b30417a 100644 --- a/log.py +++ b/log.py @@ -27,7 +27,6 @@ screen_width = screen_height = None def screen_size(): if not os.isatty(2): return 0, 0 - import fcntl res = fcntl.ioctl(2, TIOCGWINSZ, "\0" * 4) return struct.unpack("hh", res) screen_width, screen_height = screen_size() From cdc489e182d558ffd52b2dd416326d4e97ea5efe Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Wed, 15 Mar 2017 15:39:06 +0100 Subject: [PATCH 05/58] don't hardcode TIOCGWINSZ value The TIOCGWINSZ constant is available in the termios module, so let's use it instead of hardcoding the value. --- log.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/log.py b/log.py index 706d506..30f041e 100644 --- a/log.py +++ b/log.py @@ -18,16 +18,15 @@ import os import struct import sys +import termios import threading lock = threading.RLock() -TIOCGWINSZ = 0x5413 - screen_width = screen_height = None def screen_size(): if not os.isatty(2): return 0, 0 - res = fcntl.ioctl(2, TIOCGWINSZ, "\0" * 4) + res = fcntl.ioctl(2, termios.TIOCGWINSZ, "\0" * 4) return struct.unpack("hh", res) screen_width, screen_height = screen_size() From bf5f8d5df07337cd19626836d2b244d1563a3be3 Mon Sep 17 00:00:00 2001 From: "c.magyar" Date: Sat, 29 Apr 2017 15:43:19 -0500 Subject: [PATCH 06/58] corrected lowercase naming issue in cropgtk.py --- cropgtk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cropgtk.py b/cropgtk.py index 6ba198a..e822b8c 100755 --- a/cropgtk.py +++ b/cropgtk.py @@ -308,7 +308,7 @@ def output_name(self, image_name, image_type): image_name = os.path.abspath(image_name) d = os.path.dirname(image_name) i = os.path.basename(image_name) - j = os.path.splitext(i)[0].lower() + j = os.path.splitext(i)[0] if j.endswith('-crop'): j += os.path.splitext(i)[1] else: j += "-crop" + os.path.splitext(i)[1] if os.access(d, os.W_OK): return os.path.join(d, j) From aff540a1a21df3c312acef4d6191169c2f0300d5 Mon Sep 17 00:00:00 2001 From: "c.magyar" Date: Sat, 6 May 2017 21:06:44 -0500 Subject: [PATCH 07/58] Copy file if no cropping or rotation is required. --- cropgtk.py | 5 ++++- cropgui.py | 16 +++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/cropgtk.py b/cropgtk.py index e822b8c..5dac591 100755 --- a/cropgtk.py +++ b/cropgtk.py @@ -281,8 +281,11 @@ def run(self): self.log("Skipped %s" % os.path.basename(image_name)) continue # user hit "cancel" on save dialog + # Copy file if no cropping or rotation. + if (r+b-l-t) == (drag.w+drag.h) and rotation =="none": + command = ['nice', 'cp' , image_name, target] # JPEG crop uses jpegtran - if image_type is "jpeg": + elif image_type is "jpeg": command = ['nice', 'jpegtran'] if not rotation == "none": command.extend(['-rotate', rotation]) command.extend(['-copy', 'all', '-crop', cropspec,'-outfile', target, image_name]) diff --git a/cropgui.py b/cropgui.py index a799d3c..6a6a38e 100755 --- a/cropgui.py +++ b/cropgui.py @@ -287,12 +287,18 @@ def set_busy(new_busy=True): if v == -1: break # user closed app if v == 0: continue # user hit "next" / escape + # Copy file if no cropping. + if (r+b-l-t) == (drag.w+drag.h): + command = ['nice', 'cp' , image_name, target] # call jpegtran - base, ext = os.path.splitext(image_name) - t, l, r, b = drag.get_corners() - cropspec = "%dx%d+%d+%d" % (r-l, b-t, l, t) - target = base + "-crop" + ext - task.add(['nice', 'jpegtran', '-copy', 'all', '-crop', cropspec, '-outfile', target, image_name], target) + else: + base, ext = os.path.splitext(image_name) + t, l, r, b = drag.get_corners() + cropspec = "%dx%d+%d+%d" % (r-l, b-t, l, t) + target = base + "-crop" + ext + command=['nice', 'jpegtran', '-copy', 'all', '-crop', cropspec, '-outfile', target, image_name] + print " ".join(command) + task.add(command, target) finally: task.done() From 60a94edb5f2bc63843e2ac080471ece27a08c952 Mon Sep 17 00:00:00 2001 From: xiota Date: Thu, 20 Jul 2017 18:55:01 -0700 Subject: [PATCH 08/58] update compat version to 9 update source format to 3.0 (quilt) add depend on dh-python --- debian/compat | 2 +- debian/control | 2 +- debian/source/format | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 debian/source/format diff --git a/debian/compat b/debian/compat index b8626c4..ec63514 100644 --- a/debian/compat +++ b/debian/compat @@ -1 +1 @@ -4 +9 diff --git a/debian/control b/debian/control index 11e8854..bf3b87f 100644 --- a/debian/control +++ b/debian/control @@ -2,7 +2,7 @@ Source: cropgui Section: graphics Priority: extra Maintainer: Jeff Epler -Build-Depends: python, python-support, debhelper +Build-Depends: python, debhelper, python-support | dh-python XS-Python-Version: current Standards-Version: 2.0.0-rc diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) From 0fed1720a4b7cccbc58b3bd3fda9aeebc18a2c80 Mon Sep 17 00:00:00 2001 From: David Emerson Date: Wed, 2 Aug 2017 12:20:55 -0700 Subject: [PATCH 09/58] Update cropgui.py assign t, l, r, b before checking for no cropping --- cropgui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cropgui.py b/cropgui.py index 6a6a38e..2cbf2c0 100755 --- a/cropgui.py +++ b/cropgui.py @@ -287,13 +287,13 @@ def set_busy(new_busy=True): if v == -1: break # user closed app if v == 0: continue # user hit "next" / escape + t, l, r, b = drag.get_corners() # Copy file if no cropping. if (r+b-l-t) == (drag.w+drag.h): command = ['nice', 'cp' , image_name, target] # call jpegtran else: base, ext = os.path.splitext(image_name) - t, l, r, b = drag.get_corners() cropspec = "%dx%d+%d+%d" % (r-l, b-t, l, t) target = base + "-crop" + ext command=['nice', 'jpegtran', '-copy', 'all', '-crop', cropspec, '-outfile', target, image_name] From 4756d976d7d442400e2a65c488d931cc86b67949 Mon Sep 17 00:00:00 2001 From: Yan Li Date: Thu, 7 Sep 2017 09:33:50 -0700 Subject: [PATCH 10/58] Added the list of packages needed for running on Fedora. --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 478a2fb..9d94264 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,13 @@ order to fit onscreen. After releasing the mouse button, the cropped image boundary may move a little bit; this represents the limitation that the upper-left corner must be at a multiple of 8x8 original image pixels. -cropgui is written in Python and requires python, python-tkinter, -python-imaging, python-imaging-tk, and libjpeg-progs. It is available under the -terms of the GNU GPL version 2 or later. +cropgui is written in Python and requires the following packages: + * Debian: python, python-tkinter, python-imaging, python-imaging-tk, + and libjpeg-progs. + * Fedora: python2-pillow, libjpeg-turbo-utils, pygtk2, + pygtk2-libglade, and ImageMagick. + +It is available under the terms of the GNU GPL version 2 or later. The specific external programs required are: * `jpegtran` to crop jpeg images (debian package: libjpeg-turbo-progs or libjpeg-progs) From 8a7aadeea9665a416c71050314cc3164debb9026 Mon Sep 17 00:00:00 2001 From: Yan Li Date: Thu, 7 Sep 2017 14:27:34 -0700 Subject: [PATCH 11/58] Switched from jpegexiforient to exiftool jpegexiforient is not available on Fedora. --- README.md | 6 +++--- cropgui_common.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) mode change 100644 => 100755 cropgui_common.py diff --git a/README.md b/README.md index 9d94264..22f408c 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,13 @@ upper-left corner must be at a multiple of 8x8 original image pixels. cropgui is written in Python and requires the following packages: * Debian: python, python-tkinter, python-imaging, python-imaging-tk, - and libjpeg-progs. + libjpeg-progs, and libimage-exiftool-perl. * Fedora: python2-pillow, libjpeg-turbo-utils, pygtk2, - pygtk2-libglade, and ImageMagick. + pygtk2-libglade, ImageMagick, and perl-Image-ExifTool. It is available under the terms of the GNU GPL version 2 or later. The specific external programs required are: * `jpegtran` to crop jpeg images (debian package: libjpeg-turbo-progs or libjpeg-progs) - * `jpegexiforient` to clear the EXIF rotation flag from jpeg output images (debian package: libjpeg-turbo-progs or libjpeg-progs) + * `exiftool` to clear the EXIF rotation flag from jpeg output images (debian package: libimage-exiftool-perl) * `convert` to rotate and crop other image types (debian package: imagemagick or graphicsmagick-imagemagick-compat) diff --git a/cropgui_common.py b/cropgui_common.py old mode 100644 new mode 100755 index 0025b07..270aa69 --- a/cropgui_common.py +++ b/cropgui_common.py @@ -84,7 +84,7 @@ def runner(self): shortname = os.path.basename(target) self.log.progress(_("Cropping to %s") % shortname) subprocess.call(command) - subprocess.call(["jpegexiforient", "-1", target]) + subprocess.call(["exiftool", "-overwrite_original", "-Orientation=1", "-n", target]) self.log.log(_("Cropped to %s") % shortname) class DragManagerBase(object): From 0deb9e9d1f9a73b1d18b6976722129a9958d0292 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 7 Sep 2017 16:47:57 -0500 Subject: [PATCH 12/58] add new dependency to debian packaging --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index bf3b87f..909a0e3 100644 --- a/debian/control +++ b/debian/control @@ -7,7 +7,7 @@ XS-Python-Version: current Standards-Version: 2.0.0-rc Package: cropgui -Depends: python-imaging, python-gtk2, libjpeg-turbo-progs | libjpeg-progs +Depends: python-imaging, python-gtk2, libjpeg-turbo-progs | libjpeg-progs, libimage-exiftool-perl Section: graphics Architecture: all Description: interactive tool to losslessly resize jpeg images From f9fcff15256c26c896fcb0bcc4f1b1a21b3cf4bf Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Sat, 30 Sep 2017 09:30:31 -0500 Subject: [PATCH 13/58] Prepare for 0.3 release --- debian/changelog | 18 ++++++++++++++++++ debian/control | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 147abd4..9def94b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,21 @@ +cropgui (0.3) UNRELEASED; urgency=medium + + * Tag a new release to help packagers + * Use jpegexiforient instead of exiftool + * Crop other types of images (may not be lossless) + * Rotate other types of images via imagemagick convert + * Just copy file to output if the cropped and uncropped dimensions match + * Don't forcibly convert filenames to lowercase + * Don't create output filenames like foo-crop-crop.jpg + * Don't hardcode TIOCGWINSZ value, it's in module termios + * Fix initial rotation of images with EXIF rotation + * Make cropgui.desktop follow desktop file specification better + * Work around gratuitious 'tobytes' breakage in pillow + * Other bugfixes and enhancements + * Update debian packaging + + -- Jeff Epler Sat, 30 Sep 2017 09:24:01 -0500 + cropgui (0.2+git27ee40aeee) hardy; urgency=low * use absolute paths diff --git a/debian/control b/debian/control index 909a0e3..b2522b2 100644 --- a/debian/control +++ b/debian/control @@ -7,7 +7,7 @@ XS-Python-Version: current Standards-Version: 2.0.0-rc Package: cropgui -Depends: python-imaging, python-gtk2, libjpeg-turbo-progs | libjpeg-progs, libimage-exiftool-perl +Depends: python-imaging, python-gtk2, libjpeg-turbo-progs | libjpeg-progs, libimage-exiftool-perl, imagemagick Section: graphics Architecture: all Description: interactive tool to losslessly resize jpeg images From 0ff9b2f45c3284b3e5cb155b56d7cf0739000c96 Mon Sep 17 00:00:00 2001 From: isync Date: Sat, 23 Dec 2017 15:50:13 +0100 Subject: [PATCH 14/58] Add install notes to README to tell people like me what to do with install.sh --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 22f408c..7bd7201 100644 --- a/README.md +++ b/README.md @@ -38,3 +38,13 @@ The specific external programs required are: * `jpegtran` to crop jpeg images (debian package: libjpeg-turbo-progs or libjpeg-progs) * `exiftool` to clear the EXIF rotation flag from jpeg output images (debian package: libimage-exiftool-perl) * `convert` to rotate and crop other image types (debian package: imagemagick or graphicsmagick-imagemagick-compat) + +## INSTALLATION + +Although there are packages in the making, for a system-wide install, do this on +command line after cloning this repo: + + $ sudo bash ./install.sh -p /usr -P /usr/bin/python + +Where the _-p_ flag tells install.sh to install to /usr instead of your home dir. And +flag _-P_ points to your python binary, which you can find via _$ type python_ From a77e1872bcc46406432cc12e0f809ca9b0ad52bb Mon Sep 17 00:00:00 2001 From: isync Date: Sat, 23 Dec 2017 16:06:33 +0100 Subject: [PATCH 15/58] --amend last commit --- README.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7bd7201..6def228 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,14 @@ order to fit onscreen. After releasing the mouse button, the cropped image boundary may move a little bit; this represents the limitation that the upper-left corner must be at a multiple of 8x8 original image pixels. +## PREREQUISITES + cropgui is written in Python and requires the following packages: * Debian: python, python-tkinter, python-imaging, python-imaging-tk, libjpeg-progs, and libimage-exiftool-perl. * Fedora: python2-pillow, libjpeg-turbo-utils, pygtk2, pygtk2-libglade, ImageMagick, and perl-Image-ExifTool. -It is available under the terms of the GNU GPL version 2 or later. - The specific external programs required are: * `jpegtran` to crop jpeg images (debian package: libjpeg-turbo-progs or libjpeg-progs) * `exiftool` to clear the EXIF rotation flag from jpeg output images (debian package: libimage-exiftool-perl) @@ -41,10 +41,19 @@ The specific external programs required are: ## INSTALLATION -Although there are packages in the making, for a system-wide install, do this on -command line after cloning this repo: +Although there are packages in the making, for a system-wide install, first make sure +prerequisites are met for your system and the "flavor" of cropgui you want to install. +For the GTK version, you may skip the TK dependencies. But make sure `jpegtran`, `exiftool` +and `convert` are installed. + +Then do this on command line after cloning this repo: $ sudo bash ./install.sh -p /usr -P /usr/bin/python Where the _-p_ flag tells install.sh to install to /usr instead of your home dir. And -flag _-P_ points to your python binary, which you can find via _$ type python_ +flag _-P_ points to your python binary, which you can find via _$ type python_. You may +set the optional -f flag to switch between _tk_ and _gtk_ (the default) flavor of the app. + +## LICENSE +cropgui is available under the terms of the GNU GPL version 2 or later. + From e761fe6009bc0796ae386c239089def6825d6565 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Mon, 25 Dec 2017 09:32:38 -0600 Subject: [PATCH 16/58] Note that I'm not actively developing this project --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6def228..5829b59 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,12 @@ Where the _-p_ flag tells install.sh to install to /usr instead of your home dir flag _-P_ points to your python binary, which you can find via _$ type python_. You may set the optional -f flag to switch between _tk_ and _gtk_ (the default) flavor of the app. +## Development status + +The author (@jepler) is not actively developing this project. +Issues and pull requests are not likely to be acted on. +I would be interested in passing this project to a new maintainer. + + ## LICENSE cropgui is available under the terms of the GNU GPL version 2 or later. - From 7b37e283f6bcc5cd9cdff73987b802aa399d1c16 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Sun, 14 Jan 2018 08:37:13 -0600 Subject: [PATCH 17/58] Fix dpkg-genchanges warning --- debian/changelog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 9def94b..1e00a4f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -21,7 +21,7 @@ cropgui (0.2+git27ee40aeee) hardy; urgency=low * use absolute paths * don't apply scaling to jpegtrans dimensions - -- Reuben Thu, 30 Jul 2015 07:04:00 +1000 + -- Reuben Thu, 30 Jul 2015 07:04:00 +1000 cropgui (0.2) hardy; urgency=low From a6e9388751920875a9b12a57fd3b3e92d77019c6 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Sun, 14 Jan 2018 08:37:18 -0600 Subject: [PATCH 18/58] Fix failure to build source archive --- debian/source/format | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/source/format b/debian/source/format index 163aaf8..89ae9db 100644 --- a/debian/source/format +++ b/debian/source/format @@ -1 +1 @@ -3.0 (quilt) +3.0 (native) From d28c6b35314a84a5ef952e24d19ccaf256fd158e Mon Sep 17 00:00:00 2001 From: Reuben Peterkin Date: Wed, 13 Jun 2018 23:44:20 +1000 Subject: [PATCH 19/58] Start GTK3 changes - glade file need fixing --- cropgtk.py | 62 +++++++++++++------------ cropgui.glade | 126 +++++++++++++++++++++++++------------------------- 2 files changed, 96 insertions(+), 92 deletions(-) diff --git a/cropgtk.py b/cropgtk.py index 5dac591..546984e 100755 --- a/cropgtk.py +++ b/cropgtk.py @@ -18,10 +18,12 @@ from cropgui_common import * from cropgui_common import _ -import gobject -import gtk -import gtk.glade - +import gi +from gi.repository import GObject as gbject +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk as gtk +#import gtk.glade +from gi.repository import Gdk as gdk import filechooser import sys @@ -39,8 +41,8 @@ def excepthook(exc_type, exc_obj, exc_tb): lines = traceback.format_exception(exc_type, exc_obj, exc_tb) print "".join(lines) m = gtk.MessageDialog(w, - gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, + gtk.DialogFlags.MODAL | gtk.DialogFlags.DESTROY_WITH_PARENT, + gtk.MessageType.ERROR, gtk.ButtonsType.OK, _("Stepconf encountered an error. The following " "information may be useful in troubleshooting:\n\n") + "".join(lines)) @@ -83,29 +85,29 @@ def coords(self, event): return event.x, event.y def press(self, w, event): - if event.type == gtk.gdk._2BUTTON_PRESS: + if event.type == gdk._2BUTTON_PRESS: return self.done() x, y = self.coords(event) - self.drag_start(x, y, event.state & gtk.gdk.SHIFT_MASK) + self.drag_start(x, y, event.state & gdk.SHIFT_MASK) def motion(self, w, event): x, y = self.coords(event) - if event.state & gtk.gdk.BUTTON1_MASK: + if event.state & gdk.BUTTON1_MASK: self.drag_continue(x, y) else: self.idle_motion(x, y) - idle_cursor = gtk.gdk.Cursor(gtk.gdk.WATCH) + idle_cursor = gdk.Cursor(gdk.CursorType.WATCH) cursor_map = { - DRAG_TL: gtk.gdk.Cursor(gtk.gdk.TOP_LEFT_CORNER), - DRAG_L: gtk.gdk.Cursor(gtk.gdk.LEFT_SIDE), - DRAG_BL: gtk.gdk.Cursor(gtk.gdk.BOTTOM_LEFT_CORNER), - DRAG_TR: gtk.gdk.Cursor(gtk.gdk.TOP_RIGHT_CORNER), - DRAG_R: gtk.gdk.Cursor(gtk.gdk.RIGHT_SIDE), - DRAG_BR: gtk.gdk.Cursor(gtk.gdk.BOTTOM_RIGHT_CORNER), - DRAG_T: gtk.gdk.Cursor(gtk.gdk.TOP_SIDE), - DRAG_B: gtk.gdk.Cursor(gtk.gdk.BOTTOM_SIDE), - DRAG_C: gtk.gdk.Cursor(gtk.gdk.FLEUR)} + DRAG_TL: gdk.Cursor(gdk.CursorType.TOP_LEFT_CORNER), + DRAG_L: gdk.Cursor(gdk.CursorType.LEFT_SIDE), + DRAG_BL: gdk.Cursor(gdk.CursorType.BOTTOM_LEFT_CORNER), + DRAG_TR: gdk.Cursor(gdk.CursorType.TOP_RIGHT_CORNER), + DRAG_R: gdk.Cursor(gdk.CursorType.RIGHT_SIDE), + DRAG_BR: gdk.Cursor(gdk.CursorType.BOTTOM_RIGHT_CORNER), + DRAG_T: gdk.Cursor(gdk.CursorType.TOP_SIDE), + DRAG_B: gdk.Cursor(gdk.CursorType.BOTTOM_SIDE), + DRAG_C: gdk.Cursor(gdk.CursorType.FLEUR)} def idle_motion(self, x, y): i = self.g['image1'] @@ -156,8 +158,8 @@ def do_render(self): 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) + pixbuf = gdk.pixbuf_new_from_data('\0\0\0', + gdk.COLORSPACE_RGB, 0, 8, 1, 1, 3) i.set_from_pixbuf(pixbuf) g['pos_left'].set_text('---') g['pos_right'].set_text('---') @@ -175,8 +177,8 @@ def do_render(self): image_data = rendered.tostring() except: image_data = rendered.tobytes() - pixbuf = gtk.gdk.pixbuf_new_from_data(image_data, - gtk.gdk.COLORSPACE_RGB, 0, 8, + pixbuf = gdk.pixbuf_new_from_data(image_data, + gdk.COLORSPACE_RGB, 0, 8, rendered.size[0], rendered.size[1], 3*rendered.size[0]) tt, ll, rr, bb = self.get_corners() @@ -201,19 +203,21 @@ def wait(self): self.loop.run() return self.result -max_h = gtk.gdk.screen_height() - 64*3 -max_w = gtk.gdk.screen_width() - 64 +max_h = gdk.Screen.height() - 64*3 +max_w = gdk.Screen.width() - 64 class App: def __init__(self): - self.glade = gtk.glade.XML(gladefile) + self.builder = gtk.Builder() + self.builder.add_from_file(gladefile) + #self.glade = gtk.glade.XML(gladefile) self.drag = DragManager(self) self.task = CropTask(self) self.dirchooser = None self['window1'].set_title(_("CropGTK")) def __getitem__(self, name): - return self.glade.get_widget(name) + return self.builder.get_object(name) def log(self, msg): s = self['statusbar1'] @@ -245,8 +249,8 @@ def run(self): i.thumbnail((drag.w/scale, drag.h/scale)) except (IOError,), detail: m = gtk.MessageDialog(self['window1'], - gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, + gtk.DialogFlags.MODAL | gtk.DialogFlags.DESTROY_WITH_PARENT, + gtk.MessageType.ERROR, gtk.ButtonsType.OK, "Could not open %s: %s" % (image_name, detail)) m.show() m.run() diff --git a/cropgui.glade b/cropgui.glade index a57385f..e198661 100644 --- a/cropgui.glade +++ b/cropgui.glade @@ -1,9 +1,9 @@ - + - + - + True %s - CropGUI GTK_WINDOW_TOPLEVEL @@ -20,21 +20,21 @@ False - + True False 0 - + True GTK_ORIENTATION_HORIZONTAL GTK_TOOLBAR_BOTH - True + True - + True Crop True @@ -42,7 +42,7 @@ True True False - + False True @@ -50,7 +50,7 @@ - + True Skip True @@ -58,7 +58,7 @@ True True False - + False True @@ -66,12 +66,12 @@ - + True True True True - + False False @@ -79,15 +79,15 @@ - + True Rotate left True - stock-rotate-270-16.png + True True False - + False True @@ -95,15 +95,15 @@ - + True Rotate right True - stock-rotate-90-16.png + True True False - + False True @@ -111,12 +111,12 @@ - + True True True True - + False False @@ -124,14 +124,14 @@ - + True True True False - + True 2 8 @@ -140,7 +140,7 @@ 0 - + True Left: False @@ -156,7 +156,7 @@ -1 False 0 - + 0 1 @@ -168,7 +168,7 @@ - + True Width: False @@ -184,7 +184,7 @@ -1 False 0 - + 0 1 @@ -196,7 +196,7 @@ - + True %d False @@ -212,7 +212,7 @@ 5 False 0 - + 1 2 @@ -224,7 +224,7 @@ - + True %d False @@ -240,7 +240,7 @@ 5 False 0 - + 1 2 @@ -252,7 +252,7 @@ - + True Top: False @@ -268,7 +268,7 @@ -1 False 0 - + 2 3 @@ -280,7 +280,7 @@ - + True Height: False @@ -296,7 +296,7 @@ -1 False 0 - + 2 3 @@ -308,7 +308,7 @@ - + True %d False @@ -324,7 +324,7 @@ 5 False 0 - + 3 4 @@ -336,7 +336,7 @@ - + True %d False @@ -352,7 +352,7 @@ 5 False 0 - + 3 4 @@ -364,7 +364,7 @@ - + True Right: False @@ -380,7 +380,7 @@ -1 False 0 - + 4 5 @@ -392,7 +392,7 @@ - + True Ratio: False @@ -408,7 +408,7 @@ -1 False 0 - + 4 5 @@ -420,7 +420,7 @@ - + True %d False @@ -436,7 +436,7 @@ 5 False 0 - + 5 6 @@ -448,7 +448,7 @@ - + True Bottom: False @@ -464,7 +464,7 @@ -1 False 0 - + 6 7 @@ -476,7 +476,7 @@ - + True %d False @@ -492,7 +492,7 @@ 5 False 0 - + 7 8 @@ -504,7 +504,7 @@ - + True %s False @@ -520,7 +520,7 @@ 5 False 0 - + 5 7 @@ -530,15 +530,15 @@ - + - + False False - + 0 False @@ -547,14 +547,14 @@ - + True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK True False - + 1000 650 True @@ -562,9 +562,9 @@ 0 0 0 - + - + 0 True @@ -573,18 +573,18 @@ - + True - True - + + 0 False False - + - + - + From 27ca72e6a76ff60db232e20e6b4a95a8e383643e Mon Sep 17 00:00:00 2001 From: Reuben Peterkin Date: Wed, 13 Jun 2018 23:52:39 +1000 Subject: [PATCH 20/58] filechooser --- filechooser.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/filechooser.py b/filechooser.py index 278ec2e..96af5a6 100644 --- a/filechooser.py +++ b/filechooser.py @@ -16,11 +16,12 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA PREVIEW_SIZE = 300 -import pygtk -pygtk.require('2.0') +import gi +from gi.repository import GObject as gbject +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk as gtk +#import gtk.glade -import gtk -import gobject import os from PIL import Image @@ -79,20 +80,21 @@ def run(self): self.dialog.show() response = self.dialog.run() self.dialog.hide() - if response == gtk.RESPONSE_OK: + if response == gtk.ResponseType.OK: return self.dialog.get_filenames() else: return [] class Chooser(BaseChooser): - mode = gtk.FILE_CHOOSER_ACTION_OPEN - buttons = (gtk.STOCK_QUIT, gtk.RESPONSE_CANCEL, - gtk.STOCK_OPEN, gtk.RESPONSE_OK) + mode = gtk.FileChooserAction.OPEN + + buttons = (gtk.STOCK_QUIT, gtk.ResponseType.CANCEL, + gtk.STOCK_OPEN, gtk.ResponseType.OK) def __init__(self, title, parent): BaseChooser.__init__(self, parent, title) - self.dialog.set_default_response(gtk.RESPONSE_OK) + self.dialog.set_default_response(gtk.ResponseType.OK) self.dialog.set_select_multiple(True) preview = gtk.Image() @@ -125,13 +127,13 @@ def __init__(self, title, parent): self.dialog.add_filter(filter) class DirChooser(BaseChooser): - mode = gtk.FILE_CHOOSER_ACTION_SAVE - buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, - gtk.STOCK_SAVE, gtk.RESPONSE_OK) + mode = gtk.FileChooserAction.SAVE + buttons = (gtk.STOCK_CANCEL, gtk.ResponseType.CANCEL, + gtk.STOCK_SAVE, gtk.ResponseType.OK) def __init__(self, title, parent): BaseChooser.__init__(self, parent, title) - self.dialog.set_default_response(gtk.RESPONSE_OK) + self.dialog.set_default_response(gtk.ResponseType.OK) def set_current_name(self, filename): self.dialog.set_current_name(filename) From 306735962919d682a3bd55293b6f0f4f1b88bba2 Mon Sep 17 00:00:00 2001 From: Reuben Peterkin Date: Sun, 1 Jul 2018 17:33:52 +1000 Subject: [PATCH 21/58] Minor glade updates --- cropgui.glade | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/cropgui.glade b/cropgui.glade index e198661..5710b7f 100644 --- a/cropgui.glade +++ b/cropgui.glade @@ -20,8 +20,9 @@ False - + True + GTK_ORIENTATION_VERTICAL False 0 @@ -38,7 +39,7 @@ True Crop True - gtk-cut + edit-cut True True False @@ -54,7 +55,7 @@ True Skip True - gtk-go-forward + go-next True True False @@ -83,7 +84,7 @@ True Rotate left True - + object-rotate-left True True False @@ -99,7 +100,7 @@ True Rotate right True - + object-rotate-right True True False @@ -133,9 +134,9 @@ True - 2 - 8 - False + + False--> 0 0 From 806dcbc3d5ecf1d1880eccec6834b08bb2c9ae4d Mon Sep 17 00:00:00 2001 From: Reuben Peterkin Date: Sun, 1 Jul 2018 17:37:42 +1000 Subject: [PATCH 22/58] Update cropgtk.py and filechoser.py to GTK3 --- cropgtk.py | 23 ++++++++++++----------- filechooser.py | 12 ++++++------ 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/cropgtk.py b/cropgtk.py index 546984e..e994d41 100755 --- a/cropgtk.py +++ b/cropgtk.py @@ -19,12 +19,13 @@ from cropgui_common import _ import gi -from gi.repository import GObject as gbject +from gi.repository import GObject as gobject gi.require_version('Gtk', '3.0') from gi.repository import Gtk as gtk #import gtk.glade from gi.repository import Gdk as gdk import filechooser +from gi.repository import GdkPixbuf as GdkPixbuf import sys import traceback @@ -85,14 +86,14 @@ def coords(self, event): return event.x, event.y def press(self, w, event): - if event.type == gdk._2BUTTON_PRESS: + if event.type == gdk.EventType._2BUTTON_PRESS: return self.done() x, y = self.coords(event) - self.drag_start(x, y, event.state & gdk.SHIFT_MASK) + self.drag_start(x, y, event.state & gdk.ModifierType.SHIFT_MASK) def motion(self, w, event): x, y = self.coords(event) - if event.state & gdk.BUTTON1_MASK: + if event.state & gdk.ModifierType.BUTTON1_MASK: self.drag_continue(x, y) else: self.idle_motion(x, y) @@ -116,7 +117,7 @@ def idle_motion(self, x, y): else: what = self.classify(x, y) cursor = self.cursor_map.get(what, None) - i.window.set_cursor(cursor) +# i.window.set_cursor(cursor) def release(self, w, event): x, y = self.coords(event) @@ -135,8 +136,8 @@ def close(self, *args): self.loop.quit() def key(self, w, e): - if e.keyval == gtk.keysyms.Escape: self.escape() - elif e.keyval == gtk.keysyms.Return: self.done() + if e.keyval == gdk.KEY_Escape: self.escape() + elif e.keyval == gdk.KEY_Return: self.done() elif e.string and e.string in ',<': self.rotate_ccw() elif e.string and e.string in '.>': self.rotate_cw() @@ -158,8 +159,8 @@ def do_render(self): return if self.image is None: - pixbuf = gdk.pixbuf_new_from_data('\0\0\0', - gdk.COLORSPACE_RGB, 0, 8, 1, 1, 3) + pixbuf = GdkPixbuf.Pixbuf.new_from_data('\0\0\0', + GdkPixbuf.Colorspace.RGB, 0, 8, 1, 1, 3) i.set_from_pixbuf(pixbuf) g['pos_left'].set_text('---') g['pos_right'].set_text('---') @@ -177,8 +178,8 @@ def do_render(self): image_data = rendered.tostring() except: image_data = rendered.tobytes() - pixbuf = gdk.pixbuf_new_from_data(image_data, - gdk.COLORSPACE_RGB, 0, 8, + pixbuf = GdkPixbuf.Pixbuf.new_from_data(image_data, + GdkPixbuf.Colorspace.RGB, 0, 8, rendered.size[0], rendered.size[1], 3*rendered.size[0]) tt, ll, rr, bb = self.get_corners() diff --git a/filechooser.py b/filechooser.py index 96af5a6..e8087d4 100644 --- a/filechooser.py +++ b/filechooser.py @@ -17,9 +17,9 @@ PREVIEW_SIZE = 300 import gi -from gi.repository import GObject as gbject gi.require_version('Gtk', '3.0') from gi.repository import Gtk as gtk +from gi.repository import GdkPixbuf as GdkPixbuf #import gtk.glade @@ -40,7 +40,7 @@ def update_preview_cb(file_chooser, preview): file_chooser.set_preview_widget_active(True) filename = file_chooser.get_preview_filename() if not filename or os.path.isdir(filename): - preview.set_from_stock(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_LARGE_TOOLBAR) + preview.set_from_stock(gtk.STOCK_DIRECTORY, gtk.IconSize.LARGE_TOOLBAR) elif filename in image_cache: preview.set_from_pixbuf(image_cache[filename]) else: @@ -54,8 +54,8 @@ def update_preview_cb(file_chooser, preview): image_data = i.tostring() except: image_data = i.tobytes() - pixbuf = gtk.gdk.pixbuf_new_from_data(image_data, - gtk.gdk.COLORSPACE_RGB, 0, 8, i.size[0], i.size[1], + pixbuf = GdkPixbuf.Pixbuf.new_from_data(image_data, + GdkPixbuf.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: @@ -65,10 +65,10 @@ def update_preview_cb(file_chooser, preview): except IOError, detail: print detail preview.set_from_stock(gtk.STOCK_MISSING_IMAGE, - gtk.ICON_SIZE_LARGE_TOOLBAR) + gtk.IconSize.LARGE_TOOLBAR) except: preview.set_from_stock(gtk.STOCK_MISSING_IMAGE, - gtk.ICON_SIZE_LARGE_TOOLBAR) + gtk.IconSize.LARGE_TOOLBAR) raise class BaseChooser: From 552ae7b472027507d30814cea4242316e65abd20 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Fri, 27 Jul 2018 17:30:47 -0500 Subject: [PATCH 23/58] packaging: update for gtk3 --- debian/control | 2 +- debian/rules | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/debian/control b/debian/control index b2522b2..41a4eca 100644 --- a/debian/control +++ b/debian/control @@ -7,7 +7,7 @@ XS-Python-Version: current Standards-Version: 2.0.0-rc Package: cropgui -Depends: python-imaging, python-gtk2, libjpeg-turbo-progs | libjpeg-progs, libimage-exiftool-perl, imagemagick +Depends: ${python:Depends}, ${misc:Depends}, python-gobject, python-gi, libgtk-3-0, libjpeg-turbo-progs | libjpeg-progs, libimage-exiftool-perl, imagemagick Section: graphics Architecture: all Description: interactive tool to losslessly resize jpeg images diff --git a/debian/rules b/debian/rules index 6768c7c..ec090cb 100755 --- a/debian/rules +++ b/debian/rules @@ -9,6 +9,7 @@ install: binary-indep: install (cd debian/tmp; find -not -type d) > debian/cropgui.files dh_movefiles + dh_python2 dh_installchangelogs dh_installdocs dh_installdeb From f264cfddf3519854cd6b8927c5d3eda310512626 Mon Sep 17 00:00:00 2001 From: Reuben Peterkin Date: Wed, 4 Sep 2019 19:09:20 +1000 Subject: [PATCH 24/58] update to python3 using 2to3-2.7 --- cropgtk.py | 12 ++++++------ cropgui.py | 50 +++++++++++++++++++++++------------------------ cropgui_common.py | 23 +++++++++++----------- filechooser.py | 6 +++--- 4 files changed, 46 insertions(+), 45 deletions(-) diff --git a/cropgtk.py b/cropgtk.py index e994d41..07a1957 100755 --- a/cropgtk.py +++ b/cropgtk.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2 +#!/usr/bin/python3 # 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 @@ -40,7 +40,7 @@ def excepthook(exc_type, exc_obj, exc_tb): except NameError: w = None lines = traceback.format_exception(exc_type, exc_obj, exc_tb) - print "".join(lines) + print("".join(lines)) m = gtk.MessageDialog(w, gtk.DialogFlags.MODAL | gtk.DialogFlags.DESTROY_WITH_PARENT, gtk.MessageType.ERROR, gtk.ButtonsType.OK, @@ -209,8 +209,8 @@ def wait(self): class App: def __init__(self): - self.builder = gtk.Builder() - self.builder.add_from_file(gladefile) + self.builder = gtk.Builder() + self.builder.add_from_file(gladefile) #self.glade = gtk.glade.XML(gladefile) self.drag = DragManager(self) self.task = CropTask(self) @@ -248,7 +248,7 @@ def run(self): scale = max (scale, (drag.w-1)/max_w+1) scale = max (scale, (drag.h-1)/max_h+1) i.thumbnail((drag.w/scale, drag.h/scale)) - except (IOError,), detail: + except (IOError,) as detail: m = gtk.MessageDialog(self['window1'], gtk.DialogFlags.MODAL | gtk.DialogFlags.DESTROY_WITH_PARENT, gtk.MessageType.ERROR, gtk.ButtonsType.OK, @@ -299,7 +299,7 @@ def run(self): command = ['nice', 'convert'] if not rotation == "none": command.extend(['-rotate', rotation]) command.extend([image_name, '-crop', cropspec, target]) - print " ".join(command) + print(" ".join(command)) task.add(command, target) def image_names(self): diff --git a/cropgui.py b/cropgui.py index 2cbf2c0..630115e 100755 --- a/cropgui.py +++ b/cropgui.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2 +#!/usr/bin/python3 # 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 @@ -18,60 +18,60 @@ from cropgui_common import * from cropgui_common import _ -import Tkinter -import ImageTk -import tkFileDialog +import tkinter +from PIL import ImageTk +import tkinter.filedialog import sys import os import signal import log -app = Tkinter.Tk() +app = tkinter.Tk() app.wm_title(_("CropGUI -- lossless cropping and rotation of jpeg files")) app.wm_iconname(_("CropGUI")) -preview = Tkinter.Label(app) +preview = tkinter.Label(app) preview.pack(side="bottom") -do_crop = Tkinter.Button(app, text="Crop") +do_crop = tkinter.Button(app, text="Crop") do_crop.pack(side="left") -crop169_button = Tkinter.Menubutton(app, text="16:9") +crop169_button = tkinter.Menubutton(app, text="16:9") crop169_button.pack(side="left") -crop169 = Tkinter.Menu(crop169_button) +crop169 = tkinter.Menu(crop169_button) crop169_button.config(menu=crop169) -crop85_button = Tkinter.Menubutton(app, text="8:5") +crop85_button = tkinter.Menubutton(app, text="8:5") crop85_button.pack(side="left") -crop85 = Tkinter.Menu(crop85_button) +crop85 = tkinter.Menu(crop85_button) crop85_button.config(menu=crop85) -crop32_button = Tkinter.Menubutton(app, text="3:2") +crop32_button = tkinter.Menubutton(app, text="3:2") crop32_button.pack(side="left") -crop32 = Tkinter.Menu(crop32_button) +crop32 = tkinter.Menu(crop32_button) crop32_button.config(menu=crop32) -crop43_button = Tkinter.Menubutton(app, text="4:3") +crop43_button = tkinter.Menubutton(app, text="4:3") crop43_button.pack(side="left") -crop43 = Tkinter.Menu(crop43_button) +crop43 = tkinter.Menu(crop43_button) crop43_button.config(menu=crop43) -crop11_button = Tkinter.Menubutton(app, text="1:1") +crop11_button = tkinter.Menubutton(app, text="1:1") crop11_button.pack(side="left") -crop11 = Tkinter.Menu(crop11_button) +crop11 = tkinter.Menu(crop11_button) crop11_button.config(menu=crop11) -crop34_button = Tkinter.Menubutton(app, text="3:4") +crop34_button = tkinter.Menubutton(app, text="3:4") crop34_button.pack(side="left") -crop34 = Tkinter.Menu(crop34_button) +crop34 = tkinter.Menu(crop34_button) crop34_button.config(menu=crop34) -crop23_button = Tkinter.Menubutton(app, text="2:3") +crop23_button = tkinter.Menubutton(app, text="2:3") crop23_button.pack(side="left") -crop23 = Tkinter.Menu(crop23_button) +crop23 = tkinter.Menu(crop23_button) crop23_button.config(menu=crop23) -info = Tkinter.Label(app) +info = tkinter.Label(app) info.pack(side="left") task = CropTask(log) @@ -95,7 +95,7 @@ def __init__(self, w, b, inf): w.bind("", "#nothing") dummy_image = Image.new('L', (max_w/2,max_h/2), 0xff) self.dummy_tkimage = ImageTk.PhotoImage(dummy_image) - self.v = Tkinter.IntVar(app) + self.v = tkinter.IntVar(app) DragManagerBase.__init__(self) def image_set(self): @@ -236,7 +236,7 @@ def image_names(): for i in sys.argv[1:]: yield i else: while 1: - names = tkFileDialog.askopenfilenames(master=app, + names = tkinter.filedialog.askopenfilenames(master=app, defaultextension=".jpg", multiple=1, parent=app, filetypes=( (_("JPEG Image Files"), ".jpg .JPG .jpeg .JPEG"), @@ -297,7 +297,7 @@ def set_busy(new_busy=True): cropspec = "%dx%d+%d+%d" % (r-l, b-t, l, t) target = base + "-crop" + ext command=['nice', 'jpegtran', '-copy', 'all', '-crop', cropspec, '-outfile', target, image_name] - print " ".join(command) + print(" ".join(command)) task.add(command, target) finally: task.done() diff --git a/cropgui_common.py b/cropgui_common.py index 270aa69..6ebc7ef 100755 --- a/cropgui_common.py +++ b/cropgui_common.py @@ -18,7 +18,7 @@ from PIL import ImageDraw import subprocess import threading -import Queue +import queue import os import math @@ -35,7 +35,7 @@ def _(s): return s # TODO: i18n DRAG_TL, DRAG_T, DRAG_TR, DRAG_L, DRAG_C, DRAG_R, DRAG_BL, DRAG_B, DRAG_BR -) = range(10) +) = list(range(10)) def describe_ratio(a, b): if a == 0 or b == 0: return "degenerate" @@ -56,7 +56,7 @@ def ncpus(): class CropTask(object): def __init__(self, log): self.log = log - self.tasks = Queue.Queue() + self.tasks = queue.Queue() self.threads = set(self.create_task() for i in range(ncpus)) for t in self.threads: t.start() @@ -144,7 +144,8 @@ def get_corners(self): def get_screencorners(self): t, l, r, b = self.get_corners() - return t/self.scale, l/self.scale, r/self.scale, b/self.scale + return(int(t/int(self.scale)), int(l/int(self.scale)), + int(r/int(self.scale)), int(b/int(self.scale))) def describe_ratio(self): w = self.right - self.left @@ -276,8 +277,8 @@ def drag_start(self, x, y, fixed=False): self.fixed_ratio = False def drag_continue(self, x, y): - dx = (x - self.x0) * self.scale - dy = (y - self.y0) * self.scale + dx = (x - self.x0) * int(self.scale) + dy = (y - self.y0) * int(self.scale) if self.fixed_ratio: ratio = (self.r0-self.l0) * 1. / (self.b0 - self.t0) if self.state in (DRAG_TR, DRAG_BL): ratio = -ratio @@ -339,9 +340,9 @@ def rotate_cw(self): def set_rotation(self, rotation): if rotation not in (1, 3, 6, 8): - raise ValueError, 'Unsupported rotation %r' % rotation + raise ValueError('Unsupported rotation %r' % rotation) - print "rotation", self.rotation, "->", rotation + print("rotation", self.rotation, "->", rotation) self._rotation = rotation self.image_or_rotation_changed() @@ -352,14 +353,14 @@ def get_rotation(self): def image_rotation(i): if not hasattr(i, '_getexif'): - print "no getexif?", type(i), getattr(i, '_getexif', None) + print("no getexif?", type(i), getattr(i, '_getexif', None)) return 1 exif = i._getexif() if not isinstance(exif, dict): - print "not dict?", repr(exif) + print("not dict?", repr(exif)) return 1 result = exif.get(0x112, None) - print "image_rotation", result + print("image_rotation", result) return result or 1 _desktop_name = None diff --git a/filechooser.py b/filechooser.py index e8087d4..5879c19 100644 --- a/filechooser.py +++ b/filechooser.py @@ -28,7 +28,7 @@ import cropgui_common def apply_rotation(rotation, image): - print "apply_rotation", rotation + print("apply_rotation", rotation) if rotation == 3: return image.transpose(Image.ROTATE_180) if rotation == 6: return image.transpose(Image.ROTATE_270) if rotation == 8: return image.transpose(Image.ROTATE_90) @@ -62,8 +62,8 @@ def update_preview_cb(file_chooser, preview): while len(image_cache) > LOW_WATER: image_cache.popitem() image_cache[filename] = pixbuf - except IOError, detail: - print detail + except IOError as detail: + print(detail) preview.set_from_stock(gtk.STOCK_MISSING_IMAGE, gtk.IconSize.LARGE_TOOLBAR) except: From ef24623d3763209f695153565314c1eb016e817e Mon Sep 17 00:00:00 2001 From: Reuben Peterkin Date: Thu, 5 Sep 2019 11:56:32 +1000 Subject: [PATCH 25/58] update cropgui.py --- cropgui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cropgui.py b/cropgui.py index 630115e..ea8a310 100755 --- a/cropgui.py +++ b/cropgui.py @@ -93,7 +93,7 @@ def __init__(self, w, b, inf): app.bind("", self.escape) w.bind("", "#nothing") w.bind("", "#nothing") - dummy_image = Image.new('L', (max_w/2,max_h/2), 0xff) + 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) From 3610f0bb935e736a0d5e6476dcd3a108ef49d7fa Mon Sep 17 00:00:00 2001 From: Reuben Peterkin Date: Thu, 5 Sep 2019 12:34:03 +1000 Subject: [PATCH 26/58] replace deprecated gobject functions --- cropgtk.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cropgtk.py b/cropgtk.py index 07a1957..bf34973 100755 --- a/cropgtk.py +++ b/cropgtk.py @@ -19,9 +19,10 @@ from cropgui_common import _ import gi -from gi.repository import GObject as gobject +#from gi.repository import GObject as gobject gi.require_version('Gtk', '3.0') from gi.repository import Gtk as gtk +from gi.repository import GLib #import gtk.glade from gi.repository import Gdk as gdk import filechooser @@ -146,7 +147,7 @@ def image_set(self): def render(self): if self.idle is None: - self.idle = gobject.idle_add(self.do_render) + self.idle = GLib.idle_add(self.do_render) def do_render(self): if not self.idle: @@ -199,7 +200,7 @@ def do_render(self): def wait(self): - self.loop = gobject.MainLoop() + self.loop = GLib.MainLoop() self.result = -1 self.loop.run() return self.result From 954d8593cc5428895573d05db0c990a1dc0d7303 Mon Sep 17 00:00:00 2001 From: Reuben Peterkin Date: Thu, 5 Sep 2019 12:35:02 +1000 Subject: [PATCH 27/58] Update Debian dependencies --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index b2522b2..38a9b0b 100644 --- a/debian/control +++ b/debian/control @@ -7,7 +7,7 @@ XS-Python-Version: current Standards-Version: 2.0.0-rc Package: cropgui -Depends: python-imaging, python-gtk2, libjpeg-turbo-progs | libjpeg-progs, libimage-exiftool-perl, imagemagick +Depends: python3-pil, python3-gi, libjpeg-turbo-progs | libjpeg-progs, libimage-exiftool-perl, imagemagick Section: graphics Architecture: all Description: interactive tool to losslessly resize jpeg images From 34c8fd6377b7b668c7203f3f8de7077d724cb3ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20B=C3=B6ck?= Date: Thu, 5 Mar 2020 08:50:01 +0100 Subject: [PATCH 28/58] Fix warning with Python 3.8 (is/==). --- cropgtk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cropgtk.py b/cropgtk.py index bf34973..9fbfeb3 100755 --- a/cropgtk.py +++ b/cropgtk.py @@ -291,7 +291,7 @@ def run(self): if (r+b-l-t) == (drag.w+drag.h) and rotation =="none": command = ['nice', 'cp' , image_name, target] # JPEG crop uses jpegtran - elif image_type is "jpeg": + elif image_type == "jpeg": command = ['nice', 'jpegtran'] if not rotation == "none": command.extend(['-rotate', rotation]) command.extend(['-copy', 'all', '-crop', cropspec,'-outfile', target, image_name]) From 2e2671b66f26be793fd4d90b6c55378cf6389b38 Mon Sep 17 00:00:00 2001 From: "Wayne A. Ptaff" Date: Sat, 7 Mar 2020 03:10:40 -0500 Subject: [PATCH 29/58] Fix image scaling miscalculation. The missing parenthesis mean that the scaling factor is always about off by one. --- cropgtk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cropgtk.py b/cropgtk.py index 9fbfeb3..861fa8d 100755 --- a/cropgtk.py +++ b/cropgtk.py @@ -246,8 +246,8 @@ def run(self): i = Image.open(image_name) drag.w, drag.h = i.size scale = 1 - scale = max (scale, (drag.w-1)/max_w+1) - scale = max (scale, (drag.h-1)/max_h+1) + scale = max (scale, (drag.w-1)/(max_w+1)) + scale = max (scale, (drag.h-1)/(max_h+1)) i.thumbnail((drag.w/scale, drag.h/scale)) except (IOError,) as detail: m = gtk.MessageDialog(self['window1'], From c8f569c086f0a79533f767a4d937f959b5ebf5f3 Mon Sep 17 00:00:00 2001 From: "Wayne A. Ptaff" Date: Sat, 7 Mar 2020 03:23:25 -0500 Subject: [PATCH 30/58] Make image scaling use integer powers of two. Fractional scaling sometimes creates a situation where the drag region does not match the image at all; jpegtran cropping then does not match what the GUI displays, by a large margin. Since the documentation states the GUI should scale by powers of two, implement exactly this. --- cropgtk.py | 4 ++-- cropgui_common.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/cropgtk.py b/cropgtk.py index 861fa8d..e81f4a9 100755 --- a/cropgtk.py +++ b/cropgtk.py @@ -246,8 +246,8 @@ def run(self): i = Image.open(image_name) drag.w, drag.h = i.size scale = 1 - scale = max (scale, (drag.w-1)/(max_w+1)) - scale = max (scale, (drag.h-1)/(max_h+1)) + scale = max (scale, nextPowerOf2((drag.w-1)/(max_w+1))) + scale = max (scale, nextPowerOf2((drag.h-1)/(max_h+1))) i.thumbnail((drag.w/scale, drag.h/scale)) except (IOError,) as detail: m = gtk.MessageDialog(self['window1'], diff --git a/cropgui_common.py b/cropgui_common.py index 6ebc7ef..5d2ba2a 100755 --- a/cropgui_common.py +++ b/cropgui_common.py @@ -47,6 +47,20 @@ def clamp(value, low, high): if high < value: return high return value +def nextPowerOf2(n): + count = 0; + ceiln = math.ceil(n) + # First ceiln in the below condition is for the + # case where ceiln is 0 + if (ceiln and not(ceiln & (ceiln - 1))): + return ceiln + + while (ceiln != 0): + ceiln >>= 1 + count += 1 + + return 1 << count; + def ncpus(): if os.path.exists("/proc/cpuinfo"): return open("/proc/cpuinfo").read().count("bogomips") or 1 From e5695d7889cfa508179772e212e1193f6d76f956 Mon Sep 17 00:00:00 2001 From: Pavel Date: Mon, 23 Mar 2020 13:32:11 +0500 Subject: [PATCH 31/58] install.sh: site_packages python3 support --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 05a916f..a103db0 100755 --- a/install.sh +++ b/install.sh @@ -12,7 +12,7 @@ default_flavor () { } site_packages () { - $PYTHON -c 'import distutils.sysconfig; print distutils.sysconfig.get_python_lib()' + $PYTHON -c 'import distutils.sysconfig; print(distutils.sysconfig.get_python_lib())' } usage () { From 2625f2c77a0e68c94412629514dfd64a02ce18df Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 26 Mar 2020 19:47:33 +0500 Subject: [PATCH 32/58] Use python3 in install.sh --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index a103db0..f8698e8 100755 --- a/install.sh +++ b/install.sh @@ -1,5 +1,5 @@ #!/bin/sh -PYTHON=python2 +PYTHON=python3 BINDIR=$HOME/bin; LIBDIR=$HOME/lib/python SHAREDIR=$HOME/share default_flavor () { From 3b9f9f250dd85125b29f995fbbad38baa3ebdc19 Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 26 Mar 2020 20:20:59 +0500 Subject: [PATCH 33/58] Update debian packaging files --- debian/control | 12 ++++++------ debian/rules | 31 +++++-------------------------- 2 files changed, 11 insertions(+), 32 deletions(-) diff --git a/debian/control b/debian/control index 38a9b0b..db88647 100644 --- a/debian/control +++ b/debian/control @@ -1,13 +1,13 @@ Source: cropgui Section: graphics -Priority: extra +Priority: optional Maintainer: Jeff Epler -Build-Depends: python, debhelper, python-support | dh-python -XS-Python-Version: current -Standards-Version: 2.0.0-rc +Build-Depends: debhelper (>=9), dh-python, python3 +Standards-Version: 3.9.8 +Homepage: https://github.com/jepler/cropgui Package: cropgui -Depends: python3-pil, python3-gi, libjpeg-turbo-progs | libjpeg-progs, libimage-exiftool-perl, imagemagick +Depends: ${python3:Depends}, python3-pil, python3-gi, libjpeg-turbo-progs | libjpeg-progs, libimage-exiftool-perl, imagemagick Section: graphics Architecture: all -Description: interactive tool to losslessly resize jpeg images +Description: A GTK GUI for lossless JPEG cropping diff --git a/debian/rules b/debian/rules index ec090cb..39af6f4 100755 --- a/debian/rules +++ b/debian/rules @@ -1,29 +1,8 @@ #!/usr/bin/make -f +#export DH_VERBOSE = 1 -PREFIX=debian/tmp/usr +%: + dh $@ --with python3 -build:; @true -install: - ./install.sh -f gtk -t debian/tmp -p /usr - -binary-indep: install - (cd debian/tmp; find -not -type d) > debian/cropgui.files - dh_movefiles - dh_python2 - dh_installchangelogs - dh_installdocs - dh_installdeb - dh_gencontrol - dh_md5sums - dh_builddeb - -binary: binary-indep - -clean: - dh_clean - rm -rf build dist - rm -f debian/postinst.debhelper debian/prerm.debhelper - -.PHONY: build install binary binary-indep clean - -# vim:noet:ts=8:sts=8:sw=8: +override_dh_auto_install: + ./install.sh -f gtk -p /usr -t $(CURDIR)/debian/cropgui From 7cfebd4eeaeb74b9b425774a97a0f442b1110ea1 Mon Sep 17 00:00:00 2001 From: Jing Wang <99jingw@gmail.com> Date: Mon, 13 Apr 2020 20:22:58 -0400 Subject: [PATCH 34/58] Use integer division in `fix` This makes it actually round. Also, it makes the crop boundary snap to the grid while dragging. --- README.md | 6 +++--- cropgui_common.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5829b59..284ddfc 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,9 @@ overwrite an earlier cropped version). For example, if the input is "moon.jpg" then the output is "moon-cropped.jpg". Images are automatically scaled by a power of 2 (e.g., 1/2, 1/4 or 1/8) in -order to fit onscreen. After releasing the mouse button, the cropped image -boundary may move a little bit; this represents the limitation that the -upper-left corner must be at a multiple of 8x8 original image pixels. +order to fit onscreen. While dragging, the cropped image boundary will snap +to a multiple of 8 or 16 pixels; this represents the limitation that the +upper-left corner must be at a multiple of the iMCU blocks. ## PREREQUISITES diff --git a/cropgui_common.py b/cropgui_common.py index 5d2ba2a..da06fdf 100755 --- a/cropgui_common.py +++ b/cropgui_common.py @@ -146,19 +146,19 @@ def image_or_rotation_changed(self): self.render() def fix(self, a, b, lim): - a, b = sorted((b,a)) + a, b = sorted((int(a), int(b))) a = clamp(a, 0, lim) b = clamp(b, 0, lim) - a = (a / self.round)*self.round - b = (b / self.round)*self.round - return int(a), int(b) + a = (a // self.round)*self.round + b = (b // self.round)*self.round + return a, b def get_corners(self): return self.top, self.left, self.right, self.bottom def get_screencorners(self): t, l, r, b = self.get_corners() - return(int(t/int(self.scale)), int(l/int(self.scale)), + return(int(t/int(self.scale)), int(l/int(self.scale)), int(r/int(self.scale)), int(b/int(self.scale))) def describe_ratio(self): From 166ce791fde91296aeaef7c48227c804386a5f02 Mon Sep 17 00:00:00 2001 From: Jing Wang <99jingw@gmail.com> Date: Mon, 13 Apr 2020 20:25:22 -0400 Subject: [PATCH 35/58] Round right/bottom up instead of rounding down This makes it possible to include the edges of an image that is not exactly a multiple of the block size. Also, this behavior feels more symmetrical/intuitive to me, since it means both sides are rounded outwards. --- cropgui_common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cropgui_common.py b/cropgui_common.py index da06fdf..6c8f1b8 100755 --- a/cropgui_common.py +++ b/cropgui_common.py @@ -147,10 +147,10 @@ def image_or_rotation_changed(self): def fix(self, a, b, lim): a, b = sorted((int(a), int(b))) + a = (a // self.round)*self.round + b = ((b + self.round - 1) // self.round)*self.round a = clamp(a, 0, lim) b = clamp(b, 0, lim) - a = (a // self.round)*self.round - b = (b // self.round)*self.round return a, b def get_corners(self): From ed8b91a3bf72219e3afa93973e64833e5113ce7e Mon Sep 17 00:00:00 2001 From: Jing Wang <99jingw@gmail.com> Date: Mon, 13 Apr 2020 20:35:45 -0400 Subject: [PATCH 36/58] Automatically determine iMCU size Not all JPEGs use 8x8 blocks. 16x16, 16x8, and 8x16 are also possible depending on chroma subsampling. --- README.md | 4 ++-- cropgtk.py | 1 + cropgui.py | 3 ++- cropgui_common.py | 37 +++++++++++++++++++++++++++++-------- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 284ddfc..b47b9ba 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ write the result. It turns out that debian's jpegtran has a "-crop" flag which performs lossless cropping of jpeg images as long as the crop is to a multiple of what the -manpage calls the "iMCU boundary", a (usually?) 8x8 block of pixels. This -feature may have been pioneered by Guido of jpegclub.org some years ago. +manpage calls the "iMCU boundary", usually an 8x8 or 16x16 block of pixels. +This feature may have been pioneered by Guido of jpegclub.org some years ago. There's apparently a nice Windows front-end to this program, but I didn't find a Linux one. So I wrote one! It's pretty basic, but it gets the job done. You diff --git a/cropgtk.py b/cropgtk.py index e81f4a9..8e9158e 100755 --- a/cropgtk.py +++ b/cropgtk.py @@ -244,6 +244,7 @@ def run(self): self.set_busy() try: i = Image.open(image_name) + drag.round_x, drag.round_y = image_round(i) drag.w, drag.h = i.size scale = 1 scale = max (scale, nextPowerOf2((drag.w-1)/(max_w+1))) diff --git a/cropgui.py b/cropgui.py index ea8a310..9bf679c 100755 --- a/cropgui.py +++ b/cropgui.py @@ -270,8 +270,9 @@ def set_busy(new_busy=True): set_busy() i = Image.open(image_name) - # compute scale to fit image on display + drag.round_x, drag.round_y = image_round(i) drag.w, drag.h = i.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) diff --git a/cropgui_common.py b/cropgui_common.py index 6c8f1b8..e9def3a 100755 --- a/cropgui_common.py +++ b/cropgui_common.py @@ -106,7 +106,8 @@ def __init__(self): self.render_flag = 0 self.show_handles = True self.state = DRAG_NONE - self.round = 8 + self.round_x = None + self.round_y = None self.image = None self.w = 0 self.h = 0 @@ -145,10 +146,15 @@ def image_or_rotation_changed(self): self.image_set() self.render() - def fix(self, a, b, lim): + def fix(self, a, b, lim, r): + """ + a, b: interval to fix + lim: upper bound + r: rounding size + """ a, b = sorted((int(a), int(b))) - a = (a // self.round)*self.round - b = ((b + self.round - 1) // self.round)*self.round + a = (a // r) * r + b = ((b + r - 1) // r) * r a = clamp(a, 0, lim) b = clamp(b, 0, lim) return a, b @@ -198,8 +204,8 @@ def set_stdsize(self, x, y): self.set_crop (top, left, right, bottom) def set_crop(self, top, left, right, bottom): - self.top, self.bottom = self.fix(top, bottom, self.h) - self.left, self.right = self.fix(left, right, self.w) + self.top, self.bottom = self.fix(top, bottom, self.h, self.round_y) + self.left, self.right = self.fix(left, right, self.w, self.round_x) self.render() def get_image(self): @@ -332,8 +338,12 @@ def drag_end(self, x, y): self.set_crop(self.top, self.left, self.right, self.bottom) self.state = DRAG_NONE - def rotate_ccw(self): + def _flip_dimensions(self): self.w, self.h = self.h, self.w + self.round_x, self.round_y = self.round_y, self.round_x + + def rotate_ccw(self): + self._flip_dimensions() r = self.rotation if r == 1: r = 8 elif r == 8: r = 3 @@ -342,7 +352,7 @@ def rotate_ccw(self): self.rotation = r def rotate_cw(self): - self.w, self.h = self.h, self.w + self._flip_dimensions() r = self.rotation if r == 1: r = 6 elif r == 6: r = 3 @@ -377,6 +387,17 @@ def image_rotation(i): print("image_rotation", result) return result or 1 + +def image_round(i): + """Return (horizontal block size, vertical block size)""" + if i.format == "JPEG": + x = max(xsamp for _id, xsamp, ysamp, _qtable in i.layer) + y = max(ysamp for _id, xsamp, ysamp, _qtable in i.layer) + return x * 8, y * 8 + else: + return 1, 1 + + _desktop_name = None def desktop_name(): global _desktop_name From f18adb41fd32b16fca105100e615f2d3b9e5b262 Mon Sep 17 00:00:00 2001 From: Jing Wang <99jingw@gmail.com> Date: Mon, 13 Apr 2020 21:05:49 -0400 Subject: [PATCH 37/58] Deduplicate code in cropgui/cropgtk by moving to cropgui_common --- cropgtk.py | 43 +++++++++++++------------------------- cropgui.py | 32 ++++++++++++++-------------- cropgui_common.py | 53 ++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 79 insertions(+), 49 deletions(-) diff --git a/cropgtk.py b/cropgtk.py index 8e9158e..aaa6294 100755 --- a/cropgtk.py +++ b/cropgtk.py @@ -243,13 +243,14 @@ def run(self): _("%s - CropGTK") % os.path.basename(image_name)) self.set_busy() try: - i = Image.open(image_name) - drag.round_x, drag.round_y = image_round(i) - drag.w, drag.h = i.size + image = Image.open(image_name) + drag.round_x, drag.round_y = image_round(image) + drag.w, drag.h = image.size scale = 1 scale = max (scale, nextPowerOf2((drag.w-1)/(max_w+1))) scale = max (scale, nextPowerOf2((drag.h-1)/(max_h+1))) - i.thumbnail((drag.w/scale, drag.h/scale)) + thumbnail = image.copy() + thumbnail.thumbnail((drag.w/scale, drag.h/scale)) except (IOError,) as detail: m = gtk.MessageDialog(self['window1'], gtk.DialogFlags.MODAL | gtk.DialogFlags.DESTROY_WITH_PARENT, @@ -260,9 +261,9 @@ def run(self): m.destroy() continue image_type = imghdr.what(image_name) - drag.image = i + drag.image = thumbnail drag.rotation = 1 - rotation = image_rotation(i) + rotation = image_rotation(image) if rotation in (3,6,8): while drag.rotation != rotation: drag.rotate_ccw() @@ -275,34 +276,18 @@ def run(self): self.log("Skipped %s" % os.path.basename(image_name)) continue # user hit "next" / escape - t, l, r, b = drag.top, drag.left, drag.right, drag.bottom - cropspec = "%dx%d+%d+%d" % (r-l, b-t, l, t) - - if drag.rotation == 3: rotation = '180' - elif drag.rotation == 6: rotation = '90' - elif drag.rotation == 8: rotation = '270' - else: rotation = "none" - target = self.output_name(image_name,image_type) if not target: self.log("Skipped %s" % os.path.basename(image_name)) continue # user hit "cancel" on save dialog - # Copy file if no cropping or rotation. - if (r+b-l-t) == (drag.w+drag.h) and rotation =="none": - command = ['nice', 'cp' , image_name, target] - # JPEG crop uses jpegtran - elif image_type == "jpeg": - command = ['nice', 'jpegtran'] - if not rotation == "none": command.extend(['-rotate', rotation]) - command.extend(['-copy', 'all', '-crop', cropspec,'-outfile', target, image_name]) - # All other images use imagemagic convert. - else: - command = ['nice', 'convert'] - if not rotation == "none": command.extend(['-rotate', rotation]) - command.extend([image_name, '-crop', cropspec, target]) - print(" ".join(command)) - task.add(command, target) + task.add(CropRequest( + image=image, + image_name=image_name, + corners=drag.get_corners(), + rotation=drag.rotation, + target=target, + )) def image_names(self): if len(sys.argv) > 1: diff --git a/cropgui.py b/cropgui.py index 9bf679c..29d349a 100755 --- a/cropgui.py +++ b/cropgui.py @@ -268,18 +268,19 @@ def set_busy(new_busy=True): for image_name in image_names(): # load new image set_busy() - i = Image.open(image_name) + image = Image.open(image_name) - drag.round_x, drag.round_y = image_round(i) - drag.w, drag.h = i.size + 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 - i.thumbnail((drag.w/drag.scale, drag.h/drag.scale)) - drag.image = i + thumbnail = image.copy() + thumbnail.thumbnail((drag.w/drag.scale, drag.h/drag.scale)) + drag.image = thumbnail # get user input set_busy(0) @@ -288,18 +289,15 @@ def set_busy(new_busy=True): if v == -1: break # user closed app if v == 0: continue # user hit "next" / escape - t, l, r, b = drag.get_corners() - # Copy file if no cropping. - if (r+b-l-t) == (drag.w+drag.h): - command = ['nice', 'cp' , image_name, target] - # call jpegtran - else: - base, ext = os.path.splitext(image_name) - cropspec = "%dx%d+%d+%d" % (r-l, b-t, l, t) - target = base + "-crop" + ext - command=['nice', 'jpegtran', '-copy', 'all', '-crop', cropspec, '-outfile', target, image_name] - print(" ".join(command)) - task.add(command, target) + 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, + )) finally: task.done() diff --git a/cropgui_common.py b/cropgui_common.py index e9def3a..9eaccac 100755 --- a/cropgui_common.py +++ b/cropgui_common.py @@ -13,6 +13,8 @@ # 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 collections import namedtuple + from PIL import Image from PIL import ImageFilter from PIL import ImageDraw @@ -67,6 +69,13 @@ def ncpus(): return 1 ncpus = ncpus() + +CropRequest = namedtuple( + "CropRequest", + ["image", "image_name", "corners", "rotation", "target"] +) + + class CropTask(object): def __init__(self, log): self.log = log @@ -86,17 +95,55 @@ def create_task(self): def count(self): return len(self.tasks) + len(self.threads) - def add(self, args, target): - self.tasks.put((args, target)) + def add(self, task): + self.tasks.put(task) def runner(self): while 1: task = self.tasks.get() if task is None: break - command, target = task + image = task.image + image_name = task.image_name + rotation_int = task.rotation + target = task.target shortname = os.path.basename(target) self.log.progress(_("Cropping to %s") % shortname) + + t, l, r, b = task.corners + cropspec = "%dx%d+%d+%d" % (r - l, b - t, l, t) + + if rotation_int == 3: + rotation = "180" + elif rotation_int == 6: + rotation = "90" + elif rotation_int == 8: + rotation = "270" + else: + rotation = "none" + + # Copy file if no cropping or rotation. + if (r + b - l - t) == (image.width + image.height) and rotation == "none": + command = ["nice", "cp", image_name, target] + # JPEG crop uses jpegtran + elif image.format == "JPEG": + command = ["nice", "jpegtran"] + if rotation != "none": + command += ["-rotate", rotation] + command += [ + "-copy", "all", + "-crop", cropspec, + "-outfile", target, + image_name, + ] + # All other images use ImageMagick convert. + else: + command = ["nice", "convert"] + if rotation != "none": + command += ["-rotate", rotation] + command += [image_name, "-crop", cropspec, target] + + print(" ".join(command)) subprocess.call(command) subprocess.call(["exiftool", "-overwrite_original", "-Orientation=1", "-n", target]) self.log.log(_("Cropped to %s") % shortname) From c8c7f79abf8b8c1787d2ff55a3686c8571cbc048 Mon Sep 17 00:00:00 2001 From: Jing Wang <99jingw@gmail.com> Date: Mon, 13 Apr 2020 23:21:49 -0400 Subject: [PATCH 38/58] Round coordinates correctly for rotated images For images that aren't a perfect multiple of the block size, the rounding needs to happen relative to the rotated origin. --- cropgui_common.py | 55 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/cropgui_common.py b/cropgui_common.py index 9eaccac..0520df2 100755 --- a/cropgui_common.py +++ b/cropgui_common.py @@ -63,6 +63,31 @@ def nextPowerOf2(n): return 1 << count; + +def get_cropspec(image, corners, rotation): + t, l, r, b = corners + w = r - l + h = b - t + + # The coordinates passed to jpegtran are interpreted post-rotation. + # Non-whole blocks are already imperfectly rotated by being left on the + # side, so we need to subtract them + if image.format == "JPEG": + round_x, round_y = image_round(image) + orig_w, orig_h = image.size + if rotation in (8, 6): + orig_w, orig_h = orig_h, orig_w + round_x, round_y = round_y, round_x + if rotation in (3, 8): + t -= orig_h % round_y + if rotation in (3, 6): + l -= orig_w % round_x + assert t >= 0, "t < 0 should be handled in fix(): {}".format(t) + assert l >= 0, "l < 0 should be handled in fix(): {}".format(l) + + return "%dx%d+%d+%d" % (w, h, l, t) + + def ncpus(): if os.path.exists("/proc/cpuinfo"): return open("/proc/cpuinfo").read().count("bogomips") or 1 @@ -111,7 +136,7 @@ def runner(self): self.log.progress(_("Cropping to %s") % shortname) t, l, r, b = task.corners - cropspec = "%dx%d+%d+%d" % (r - l, b - t, l, t) + cropspec = get_cropspec(image, task.corners, rotation_int) if rotation_int == 3: rotation = "180" @@ -181,10 +206,8 @@ def apply_rotation(self, image): def image_or_rotation_changed(self): self._image = image = self.apply_rotation(self._orig_image) self.apply_rotation(image) - self.top = 0 - self.left = 0 - self.right = self.w - self.bottom = self.h + self.top, self.bottom = self.fix(0, self.h, self.h, self.round_y, self.rotation in (3, 8)) + self.left, self.right = self.fix(0, self.w, self.w, self.round_x, self.rotation in (3, 6)) blurred = image.copy() mult = len(self.image.mode) # replicate filter for L, RGB, RGBA self.blurred = image.copy().filter( @@ -193,17 +216,25 @@ def image_or_rotation_changed(self): self.image_set() self.render() - def fix(self, a, b, lim, r): + def fix(self, a, b, lim, r, reverse): """ a, b: interval to fix lim: upper bound r: rounding size + reverse: True to treat the upper bound as the origin """ + if reverse: + offset = lim % r + else: + offset = 0 a, b = sorted((int(a), int(b))) - a = (a // r) * r - b = ((b + r - 1) // r) * r - a = clamp(a, 0, lim) - b = clamp(b, 0, lim) + a = ((a - offset) // r) * r + offset + b = ((b - offset + r - 1) // r) * r + offset + # jpegtran handles non-whole blocks by leaving them on the edge of the + # image, away from the rotated position of their old neighbors. + # Keeping them isn't useful, so clamp them off. + a = clamp(a, offset if reverse else 0, lim) + b = clamp(b, offset if reverse else 0, lim) return a, b def get_corners(self): @@ -251,8 +282,8 @@ def set_stdsize(self, x, y): self.set_crop (top, left, right, bottom) def set_crop(self, top, left, right, bottom): - self.top, self.bottom = self.fix(top, bottom, self.h, self.round_y) - self.left, self.right = self.fix(left, right, self.w, self.round_x) + self.top, self.bottom = self.fix(top, bottom, self.h, self.round_y, self.rotation in (3, 8)) + self.left, self.right = self.fix(left, right, self.w, self.round_x, self.rotation in (3, 6)) self.render() def get_image(self): From 8d26da5c16fdf7e758ffda7cf0069ed88c6cbc52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ball=C3=B3=20Gy=C3=B6rgy?= Date: Wed, 10 Jun 2020 07:57:24 +0200 Subject: [PATCH 39/58] Fix output name detection for non-jpeg files This fixes the problem, when the png files are saved as ".png.png". --- cropgtk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cropgtk.py b/cropgtk.py index aaa6294..2179cf5 100755 --- a/cropgtk.py +++ b/cropgtk.py @@ -321,7 +321,7 @@ def output_name(self, image_name, image_type): if image_type == "jpeg": if e.lower() in ['.jpg', '.jpeg']: return r return e + ".jpg" - elif e.lower() == image_type: return r + elif e.lower() == "." + image_type: return r else: return e + "." + image_type app = App() From eebcddad8359677a013650b86f185b26f4a29e74 Mon Sep 17 00:00:00 2001 From: abrooks Date: Fri, 3 Jul 2020 10:12:48 +0100 Subject: [PATCH 40/58] Fix cropping in python3 --- cropgtk.py | 4 ++-- cropgui.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cropgtk.py b/cropgtk.py index 2179cf5..778a8df 100755 --- a/cropgtk.py +++ b/cropgtk.py @@ -247,8 +247,8 @@ def run(self): drag.round_x, drag.round_y = image_round(image) drag.w, drag.h = image.size scale = 1 - scale = max (scale, nextPowerOf2((drag.w-1)/(max_w+1))) - scale = max (scale, nextPowerOf2((drag.h-1)/(max_h+1))) + scale = max (scale, nextPowerOf2((drag.w-1)//(max_w+1))) + scale = max (scale, nextPowerOf2((drag.h-1)//(max_h+1))) thumbnail = image.copy() thumbnail.thumbnail((drag.w/scale, drag.h/scale)) except (IOError,) as detail: diff --git a/cropgui.py b/cropgui.py index 29d349a..44332bf 100755 --- a/cropgui.py +++ b/cropgui.py @@ -274,8 +274,8 @@ def set_busy(new_busy=True): 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) + 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() From 034fd33924881186432de0b21de4511d3632de66 Mon Sep 17 00:00:00 2001 From: abrooks Date: Fri, 3 Jul 2020 10:32:52 +0100 Subject: [PATCH 41/58] Windows support --- README.md | 4 +++- cropgui_common.py | 4 ++++ log.py | 9 +++++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b47b9ba..9e7ac61 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,8 @@ upper-left corner must be at a multiple of the iMCU blocks. ## PREREQUISITES cropgui is written in Python and requires the following packages: - * Debian: python, python-tkinter, python-imaging, python-imaging-tk, + * Debian: python3, python3-pil, python3-pil.imagetk, + (python-tkinter, python-imaging, python-imaging-tk on older systems), libjpeg-progs, and libimage-exiftool-perl. * Fedora: python2-pillow, libjpeg-turbo-utils, pygtk2, pygtk2-libglade, ImageMagick, and perl-Image-ExifTool. @@ -38,6 +39,7 @@ The specific external programs required are: * `jpegtran` to crop jpeg images (debian package: libjpeg-turbo-progs or libjpeg-progs) * `exiftool` to clear the EXIF rotation flag from jpeg output images (debian package: libimage-exiftool-perl) * `convert` to rotate and crop other image types (debian package: imagemagick or graphicsmagick-imagemagick-compat) + * `magick` on Windows (download from ImageMagick and install) ## INSTALLATION diff --git a/cropgui_common.py b/cropgui_common.py index 0520df2..6b1fd93 100755 --- a/cropgui_common.py +++ b/cropgui_common.py @@ -18,6 +18,7 @@ from PIL import Image from PIL import ImageFilter from PIL import ImageDraw +import platform import subprocess import threading import queue @@ -150,9 +151,11 @@ def runner(self): # Copy file if no cropping or rotation. if (r + b - l - t) == (image.width + image.height) and rotation == "none": command = ["nice", "cp", image_name, target] + if platform.system() == 'Windows': command = ["copy", image_name, target] # JPEG crop uses jpegtran elif image.format == "JPEG": command = ["nice", "jpegtran"] + if platform.system() == 'Windows': command = ["jpegtran"] if rotation != "none": command += ["-rotate", rotation] command += [ @@ -164,6 +167,7 @@ def runner(self): # All other images use ImageMagick convert. else: command = ["nice", "convert"] + if platform.system() == 'Windows': command = ["magick"] if rotation != "none": command += ["-rotate", rotation] command += [image_name, "-crop", cropspec, target] diff --git a/log.py b/log.py index 30f041e..a158774 100644 --- a/log.py +++ b/log.py @@ -14,18 +14,23 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import atexit -import fcntl import os +import platform import struct import sys -import termios import threading +try: + import fcntl + import termios +except: + pass lock = threading.RLock() screen_width = screen_height = None def screen_size(): if not os.isatty(2): return 0, 0 + if platform.system() == 'Windows': return 80, 25 res = fcntl.ioctl(2, termios.TIOCGWINSZ, "\0" * 4) return struct.unpack("hh", res) screen_width, screen_height = screen_size() From 3df7abad68ff866e6c65c513b91ab2a4fd477228 Mon Sep 17 00:00:00 2001 From: Conrad Hughes Date: Mon, 2 Nov 2020 16:26:47 +0000 Subject: [PATCH 42/58] Consistent chooser parameter order. --- cropgtk.py | 4 ++-- filechooser.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cropgtk.py b/cropgtk.py index 778a8df..fd25927 100755 --- a/cropgtk.py +++ b/cropgtk.py @@ -293,7 +293,7 @@ def image_names(self): if len(sys.argv) > 1: for i in sys.argv[1:]: yield i else: - c = filechooser.Chooser(self['window1'], _("Select images to crop")) + c = filechooser.Chooser(_("Select images to crop"), self['window1']) while 1: files = c.run() if not files: break @@ -309,7 +309,7 @@ def output_name(self, image_name, image_type): if os.access(d, os.W_OK): return os.path.join(d, j) title = _('Save cropped version of %s') % i if self.dirchooser is None: - self.dirchooser = filechooser.DirChooser(self['window1'], title) + self.dirchooser = filechooser.DirChooser(title, self['window1']) self.dirchooser.set_current_folder(desktop_name()) else: self.dirchooser.set_title(title) diff --git a/filechooser.py b/filechooser.py index 5879c19..6b3be7c 100644 --- a/filechooser.py +++ b/filechooser.py @@ -92,7 +92,7 @@ class Chooser(BaseChooser): gtk.STOCK_OPEN, gtk.ResponseType.OK) def __init__(self, title, parent): - BaseChooser.__init__(self, parent, title) + BaseChooser.__init__(self, title, parent) self.dialog.set_default_response(gtk.ResponseType.OK) self.dialog.set_select_multiple(True) From 5cc15b6d6e8d51290af2845056a6d2b581d3e9ad Mon Sep 17 00:00:00 2001 From: Conrad Hughes Date: Mon, 2 Nov 2020 16:29:14 +0000 Subject: [PATCH 43/58] Fix broken install. 'cp' subject to umask problems: use 'install'. --- install.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/install.sh b/install.sh index f8698e8..c8e6165 100755 --- a/install.sh +++ b/install.sh @@ -47,21 +47,21 @@ if [ -z "$FLAVOR" ]; then FLAVOR=`default_flavor`; fi mkdir -p $TARGET$BINDIR $TARGET$LIBDIR $TARGET$SHAREDIR/applications \ $TARGET$SHAREDIR/pixmaps -cp cropgui.desktop $TARGET$SHAREDIR/applications -cp cropgui.png $TARGET$SHAREDIR/pixmaps +install --mode=644 cropgui.desktop $TARGET$SHAREDIR/applications +install --mode=644 cropgui.png $TARGET$SHAREDIR/pixmaps case $FLAVOR in gtk) echo "Installing gtk version of cropgui" - cp cropgtk.py $TARGET$BINDIR/cropgui && \ - cp cropgui_common.py filechooser.py cropgui.glade \ + install --mode=755 cropgtk.py $TARGET$BINDIR/cropgui && \ + install --mode=644 cropgui_common.py filechooser.py cropgui.glade \ stock-rotate-90-16.png stock-rotate-270-16.png \ $TARGET$LIBDIR ;; tk) echo "Installing tkinter version of cropgui" - cp cropgui.py $TARGET$BINDIR/cropgui && \ - cp log.py cropgui_common.py $TARGET$LIBDIR + install --mode=755 cropgui.py $TARGET$BINDIR/cropgui && \ + install --mode=644 log.py cropgui_common.py $TARGET$LIBDIR ;; *) echo "Unknown flavor $FLAVOR" From 395624b5e134604781294f2bed6b7a91b239c06f Mon Sep 17 00:00:00 2001 From: Conrad Hughes Date: Mon, 2 Nov 2020 17:45:11 +0000 Subject: [PATCH 44/58] Create some JPGs for testing with. --- generate_test_images.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100755 generate_test_images.sh diff --git a/generate_test_images.sh b/generate_test_images.sh new file mode 100755 index 0000000..69964c3 --- /dev/null +++ b/generate_test_images.sh @@ -0,0 +1,15 @@ +#!/bin/sh -e + +# Generate some chessboard JPGs for testing with. + +dir=test +[ -d $dir ] || mkdir $dir + +convert -size 100x100 pattern:gray50 -scale 1600% -sampling-factor 2x2 \ + -crop 796x396+0+0 $dir/chess-2x2.jpg +convert -size 100x100 pattern:gray50 -scale 800%x1600% -sampling-factor 1x2 \ + -crop 796x396+0+0 $dir/chess-1x2.jpg +convert -size 100x100 pattern:gray50 -scale 1600%x800% -sampling-factor 2x1 \ + -crop 796x396+0+0 $dir/chess-2x1.jpg +convert -size 100x100 pattern:gray50 -scale 800% -sampling-factor 1x1 \ + -crop 796x396+0+0 $dir/chess-1x1.jpg From 627faa476524a8774294ad2dc930597cebc70f78 Mon Sep 17 00:00:00 2001 From: Conrad Hughes Date: Tue, 3 Nov 2020 11:03:43 +0000 Subject: [PATCH 45/58] Revert "Consistent chooser parameter order." This reverts commit 3df7abad68ff866e6c65c513b91ab2a4fd477228. --- cropgtk.py | 4 ++-- filechooser.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cropgtk.py b/cropgtk.py index fd25927..778a8df 100755 --- a/cropgtk.py +++ b/cropgtk.py @@ -293,7 +293,7 @@ def image_names(self): if len(sys.argv) > 1: for i in sys.argv[1:]: yield i else: - c = filechooser.Chooser(_("Select images to crop"), self['window1']) + c = filechooser.Chooser(self['window1'], _("Select images to crop")) while 1: files = c.run() if not files: break @@ -309,7 +309,7 @@ def output_name(self, image_name, image_type): if os.access(d, os.W_OK): return os.path.join(d, j) title = _('Save cropped version of %s') % i if self.dirchooser is None: - self.dirchooser = filechooser.DirChooser(title, self['window1']) + self.dirchooser = filechooser.DirChooser(self['window1'], title) self.dirchooser.set_current_folder(desktop_name()) else: self.dirchooser.set_title(title) diff --git a/filechooser.py b/filechooser.py index 6b3be7c..5879c19 100644 --- a/filechooser.py +++ b/filechooser.py @@ -92,7 +92,7 @@ class Chooser(BaseChooser): gtk.STOCK_OPEN, gtk.ResponseType.OK) def __init__(self, title, parent): - BaseChooser.__init__(self, title, parent) + BaseChooser.__init__(self, parent, title) self.dialog.set_default_response(gtk.ResponseType.OK) self.dialog.set_select_multiple(True) From e051ebb83ea314525df219973b47879f426e036e Mon Sep 17 00:00:00 2001 From: Conrad Hughes Date: Tue, 3 Nov 2020 11:05:42 +0000 Subject: [PATCH 46/58] Consistent chooser parameter order. This time the fix is all here! --- cropgtk.py | 4 ++-- filechooser.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cropgtk.py b/cropgtk.py index 778a8df..fd25927 100755 --- a/cropgtk.py +++ b/cropgtk.py @@ -293,7 +293,7 @@ def image_names(self): if len(sys.argv) > 1: for i in sys.argv[1:]: yield i else: - c = filechooser.Chooser(self['window1'], _("Select images to crop")) + c = filechooser.Chooser(_("Select images to crop"), self['window1']) while 1: files = c.run() if not files: break @@ -309,7 +309,7 @@ def output_name(self, image_name, image_type): if os.access(d, os.W_OK): return os.path.join(d, j) title = _('Save cropped version of %s') % i if self.dirchooser is None: - self.dirchooser = filechooser.DirChooser(self['window1'], title) + self.dirchooser = filechooser.DirChooser(title, self['window1']) self.dirchooser.set_current_folder(desktop_name()) else: self.dirchooser.set_title(title) diff --git a/filechooser.py b/filechooser.py index 5879c19..918aa13 100644 --- a/filechooser.py +++ b/filechooser.py @@ -92,7 +92,7 @@ class Chooser(BaseChooser): gtk.STOCK_OPEN, gtk.ResponseType.OK) def __init__(self, title, parent): - BaseChooser.__init__(self, parent, title) + BaseChooser.__init__(self, title, parent) self.dialog.set_default_response(gtk.ResponseType.OK) self.dialog.set_select_multiple(True) @@ -132,7 +132,7 @@ class DirChooser(BaseChooser): gtk.STOCK_SAVE, gtk.ResponseType.OK) def __init__(self, title, parent): - BaseChooser.__init__(self, parent, title) + BaseChooser.__init__(self, title, parent) self.dialog.set_default_response(gtk.ResponseType.OK) def set_current_name(self, filename): From 4acc9e6399e74c261922a7b54e848d7b04cb987a Mon Sep 17 00:00:00 2001 From: Conrad Hughes Date: Tue, 3 Nov 2020 11:19:53 +0000 Subject: [PATCH 47/58] Add keyboard controls. Also fix event handler bug. Also slightly improved file chooser behaviour. Controls: 'n' for next image without saving 'q' to quit 's' to save a crop from this image without going to next --- cropgtk.py | 83 ++++++++++++++++++++++++++++++++------------------ filechooser.py | 4 +++ 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/cropgtk.py b/cropgtk.py index fd25927..b407fcc 100755 --- a/cropgtk.py +++ b/cropgtk.py @@ -132,6 +132,10 @@ def escape(self, *args): self.result = 0 self.loop.quit() + def save_and_stay(self, *args): + self.result = 2 + self.loop.quit() + def close(self, *args): self.result = -1 self.loop.quit() @@ -139,8 +143,18 @@ def close(self, *args): def key(self, w, e): if e.keyval == gdk.KEY_Escape: self.escape() elif e.keyval == gdk.KEY_Return: self.done() - elif e.string and e.string in ',<': self.rotate_ccw() - elif e.string and e.string in '.>': self.rotate_cw() + elif e.string: + if e.string == 'n': self.escape() + elif e.string == 'q': self.close() + elif e.string == 's': self.save_and_stay() + elif e.string in ',<': self.rotate_ccw() + elif e.string in '.>': self.rotate_cw() + # Don't know whether other event handlers need it too, but if + # this doesn't return True (True prevents further handlers from + # being invoked), a return somehow double-triggers self.done(), + # skipping the next image in a multi-file invocation. Not clear + # what's going on, but this stops it. + return True def image_set(self): self.render() @@ -238,6 +252,7 @@ def run(self): drag = self.drag task = self.task + prev_name = None for image_name in self.image_names(): self['window1'].set_title( _("%s - CropGTK") % os.path.basename(image_name)) @@ -268,26 +283,32 @@ def run(self): while drag.rotation != rotation: drag.rotate_ccw() drag.scale = scale - self.set_busy(0) - v = self.drag.wait() - self.set_busy() - if v == -1: break # user closed app - if v == 0: - self.log("Skipped %s" % os.path.basename(image_name)) - continue # user hit "next" / escape - - target = self.output_name(image_name,image_type) - if not target: - self.log("Skipped %s" % os.path.basename(image_name)) - continue # user hit "cancel" on save dialog - - task.add(CropRequest( - image=image, - image_name=image_name, - corners=drag.get_corners(), - rotation=drag.rotation, - target=target, - )) + + v = 2 + while v == 2: + self.set_busy(0) + v = self.drag.wait() + self.set_busy() + if v == -1: break # user closed app + if v == 0: + self.log("Skipped %s" % os.path.basename(image_name)) + continue # user hit "next" / escape + if v == 2: # save but stick with this image + target = self.output_name(image_name,image_type,True,prev_name) + prev_name = target + else: + target = self.output_name(image_name,image_type) + if not target: + self.log("Skipped %s" % os.path.basename(image_name)) + continue # user hit "cancel" on save dialog + task.add(CropRequest( + image=image, + image_name=image_name, + corners=drag.get_corners(), + rotation=drag.rotation, + target=target, + )) + if v == -1: break # user closed app def image_names(self): if len(sys.argv) > 1: @@ -299,20 +320,24 @@ def image_names(self): if not files: break for i in files: yield i - def output_name(self, image_name, image_type): + def output_name(self, image_name, image_type, chooser=False, prev_name=None): image_name = os.path.abspath(image_name) - d = os.path.dirname(image_name) i = os.path.basename(image_name) - j = os.path.splitext(i)[0] - if j.endswith('-crop'): j += os.path.splitext(i)[1] - else: j += "-crop" + os.path.splitext(i)[1] - if os.access(d, os.W_OK): return os.path.join(d, j) + if chooser and prev_name is not None: + d = os.path.dirname(prev_name) + j = os.path.basename(prev_name) + else: + d = os.path.dirname(image_name) + j = os.path.splitext(i)[0] + if j.endswith('-crop'): j += os.path.splitext(i)[1] + else: j += "-crop" + os.path.splitext(i)[1] + if os.access(d, os.W_OK) and not chooser: return os.path.join(d, j) title = _('Save cropped version of %s') % i if self.dirchooser is None: self.dirchooser = filechooser.DirChooser(title, self['window1']) - self.dirchooser.set_current_folder(desktop_name()) else: self.dirchooser.set_title(title) + self.dirchooser.set_current_folder(d if os.access(d, os.W_OK) else desktop_name()) self.dirchooser.set_current_name(j) r = self.dirchooser.run() if not r: return '' diff --git a/filechooser.py b/filechooser.py index 918aa13..baba74c 100644 --- a/filechooser.py +++ b/filechooser.py @@ -73,6 +73,9 @@ def update_preview_cb(file_chooser, preview): class BaseChooser: def __init__(self, title, parent): +# Gnome's "attach-modal-dialogs" can be set to false using +# gnome-tweak-tool in order to enable movable modal dialogs. No idea +# how to do that app-specifically though. self.dialog = dialog = \ gtk.FileChooserDialog(title, parent, self.mode, self.buttons) @@ -134,6 +137,7 @@ class DirChooser(BaseChooser): def __init__(self, title, parent): BaseChooser.__init__(self, title, parent) self.dialog.set_default_response(gtk.ResponseType.OK) + self.dialog.set_do_overwrite_confirmation(True) def set_current_name(self, filename): self.dialog.set_current_name(filename) From c7ff1046b8bc1512ead376d44c7ba538f042c096 Mon Sep 17 00:00:00 2001 From: Conrad Hughes Date: Tue, 3 Nov 2020 13:01:29 +0000 Subject: [PATCH 48/58] Perfect crops. Also eliminate redundant get_cropspec code. Also eliminate redundant apply_rotation call. --- cropgui_common.py | 33 +++++---------------------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/cropgui_common.py b/cropgui_common.py index 6b1fd93..b931620 100755 --- a/cropgui_common.py +++ b/cropgui_common.py @@ -69,23 +69,8 @@ def get_cropspec(image, corners, rotation): t, l, r, b = corners w = r - l h = b - t - - # The coordinates passed to jpegtran are interpreted post-rotation. - # Non-whole blocks are already imperfectly rotated by being left on the - # side, so we need to subtract them - if image.format == "JPEG": - round_x, round_y = image_round(image) - orig_w, orig_h = image.size - if rotation in (8, 6): - orig_w, orig_h = orig_h, orig_w - round_x, round_y = round_y, round_x - if rotation in (3, 8): - t -= orig_h % round_y - if rotation in (3, 6): - l -= orig_w % round_x - assert t >= 0, "t < 0 should be handled in fix(): {}".format(t) - assert l >= 0, "l < 0 should be handled in fix(): {}".format(l) - + # Technically this should produce perfect crops, but jpegtran is broken + # and so mistakenly rounds out fractional crops on flipped images. Sigh. return "%dx%d+%d+%d" % (w, h, l, t) @@ -209,7 +194,6 @@ def apply_rotation(self, image): def image_or_rotation_changed(self): self._image = image = self.apply_rotation(self._orig_image) - self.apply_rotation(image) self.top, self.bottom = self.fix(0, self.h, self.h, self.round_y, self.rotation in (3, 8)) self.left, self.right = self.fix(0, self.w, self.w, self.round_x, self.rotation in (3, 6)) blurred = image.copy() @@ -227,18 +211,11 @@ def fix(self, a, b, lim, r, reverse): r: rounding size reverse: True to treat the upper bound as the origin """ + a, b = sorted((int(a), int(b))) if reverse: - offset = lim % r + a = lim - ((((lim - a) + r - 1) // r) * r) else: - offset = 0 - a, b = sorted((int(a), int(b))) - a = ((a - offset) // r) * r + offset - b = ((b - offset + r - 1) // r) * r + offset - # jpegtran handles non-whole blocks by leaving them on the edge of the - # image, away from the rotated position of their old neighbors. - # Keeping them isn't useful, so clamp them off. - a = clamp(a, offset if reverse else 0, lim) - b = clamp(b, offset if reverse else 0, lim) + a = (a // r) * r return a, b def get_corners(self): From 4175b2c957503900d111ce577ff2fab626a98fd4 Mon Sep 17 00:00:00 2001 From: Conrad Hughes Date: Thu, 12 Nov 2020 15:59:00 +0000 Subject: [PATCH 49/58] Work around jpegtran to get correct rotated crops. --- cropgui_common.py | 13 ++++++++++--- generate_test_images.sh | 9 +++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/cropgui_common.py b/cropgui_common.py index b931620..e8d090e 100755 --- a/cropgui_common.py +++ b/cropgui_common.py @@ -69,9 +69,16 @@ def get_cropspec(image, corners, rotation): t, l, r, b = corners w = r - l h = b - t - # Technically this should produce perfect crops, but jpegtran is broken - # and so mistakenly rounds out fractional crops on flipped images. Sigh. - return "%dx%d+%d+%d" % (w, h, l, t) + # Technically these parameters should straightforwardly produce perfect + # crops, but jpegtran is broken here in two regards: (1) it doesn't + # recognise perfect rotated crops as such, so the '-perfect' switch + # erroneously rejects correctly ICMU-aligned rotate-and-crop commands, and + # (2) it mistakenly rounds out crops on rotated/flipped images so that even + # the bottom right corner is ICMU-aligned. Sigh. Adding the 'f' suffixes + # to the dimensions here at least solves (2), and seems to produce the same + # results as you get by manually constructing a pipeline of '-perfect' + # command lines. + return "%dfx%df+%d+%d" % (w, h, l, t) def ncpus(): diff --git a/generate_test_images.sh b/generate_test_images.sh index 69964c3..9405248 100755 --- a/generate_test_images.sh +++ b/generate_test_images.sh @@ -5,11 +5,12 @@ dir=test [ -d $dir ] || mkdir $dir +text='-density 72 -pointsize 32 -weight Bold -gravity NorthWest -fill rgba(255,0,0,0.5) -annotate +0+0 A -annotate +32+32 B -annotate +64+64 C -annotate +96+96 D -annotate +128+128 E -annotate +160+160 F -annotate +192+192 G -annotate +224+224 H -annotate +256+256 I -annotate +288+288 J -annotate +320+320 K -fill rgba(0,255,0,0.5) -annotate +352+352 L -annotate +384+320 M -annotate +416+288 N -annotate +448+256 O -annotate +480+224 P -annotate +512+192 Q -annotate +544+160 R -annotate +576+128 S -annotate +608+96 T -annotate +640+64 U -annotate +672+32 V -fill rgba(0,0,255,0.5) -annotate +704+0 W -annotate +736+32 X -annotate +768+64 Y' convert -size 100x100 pattern:gray50 -scale 1600% -sampling-factor 2x2 \ - -crop 796x396+0+0 $dir/chess-2x2.jpg + -crop 796x396+0+0 $text $dir/chess-2x2.jpg convert -size 100x100 pattern:gray50 -scale 800%x1600% -sampling-factor 1x2 \ - -crop 796x396+0+0 $dir/chess-1x2.jpg + -crop 796x396+0+0 $text $dir/chess-1x2.jpg convert -size 100x100 pattern:gray50 -scale 1600%x800% -sampling-factor 2x1 \ - -crop 796x396+0+0 $dir/chess-2x1.jpg + -crop 796x396+0+0 $text $dir/chess-2x1.jpg convert -size 100x100 pattern:gray50 -scale 800% -sampling-factor 1x1 \ - -crop 796x396+0+0 $dir/chess-1x1.jpg + -crop 796x396+0+0 $text $dir/chess-1x1.jpg From fb6116427d38b45949f4dac68e7c81279db68654 Mon Sep 17 00:00:00 2001 From: howff Date: Mon, 30 Nov 2020 12:04:23 +0000 Subject: [PATCH 50/58] Preserve previous crop --- cropgui.py | 3 ++- cropgui_common.py | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/cropgui.py b/cropgui.py index 44332bf..a6cab09 100755 --- a/cropgui.py +++ b/cropgui.py @@ -266,6 +266,7 @@ def set_busy(new_busy=True): try: for image_name in image_names(): + drag.save_prev_crop() # load new image set_busy() image = Image.open(image_name) @@ -279,7 +280,7 @@ def set_busy(new_busy=True): # put image into drag object thumbnail = image.copy() - thumbnail.thumbnail((drag.w/drag.scale, drag.h/drag.scale)) + thumbnail.thumbnail((drag.w//drag.scale, drag.h//drag.scale)) drag.image = thumbnail # get user input diff --git a/cropgui_common.py b/cropgui_common.py index e8d090e..6fa9552 100755 --- a/cropgui_common.py +++ b/cropgui_common.py @@ -179,9 +179,22 @@ def __init__(self): self.image = None self.w = 0 self.h = 0 + self.left = self.right = self.top = self.bottom = 0 + self.prev_w = self.prev_h = 0 + self.prev_top = self.prev_bottom = 0 + self.prev_left = self=prev_right = 0 + + def save_prev_crop(self): + self.prev_w = self.w + self.prev_h = self.h + self.prev_top = self.top + self.prev_bottom = self.bottom + self.prev_left = self.left + self.prev_right = self.right def set_image(self, image): if image is None: + # XXX Why are left,right,bottom deleted? if hasattr(self, 'left'): del self.left if hasattr(self, 'right'): del self.right if hasattr(self, 'bottom'): del self.bottom @@ -201,8 +214,13 @@ def apply_rotation(self, image): def image_or_rotation_changed(self): self._image = image = self.apply_rotation(self._orig_image) - self.top, self.bottom = self.fix(0, self.h, self.h, self.round_y, self.rotation in (3, 8)) - self.left, self.right = self.fix(0, self.w, self.w, self.round_x, self.rotation in (3, 6)) + # If new image size matches previous image size then preserve previous crop rect + if self.w == 0 or self.w != self.prev_w or self.h != self.prev_h: + self.top, self.bottom = self.fix(0, self.h, self.h, self.round_y, self.rotation in (3, 8)) + self.left, self.right = self.fix(0, self.w, self.w, self.round_x, self.rotation in (3, 6)) + else: + self.top, self.bottom = self.prev_top, self.prev_bottom + self.left, self.right = self.prev_left, self.prev_right blurred = image.copy() mult = len(self.image.mode) # replicate filter for L, RGB, RGBA self.blurred = image.copy().filter( From 4ef13cb0ba252e85345e4dbb93195422ff23ad2a Mon Sep 17 00:00:00 2001 From: howff Date: Mon, 30 Nov 2020 12:54:45 +0000 Subject: [PATCH 51/58] Only use 'f' suffix when cropping JPEG (jpegtran) --- cropgui_common.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cropgui_common.py b/cropgui_common.py index 6fa9552..0fe8cf0 100755 --- a/cropgui_common.py +++ b/cropgui_common.py @@ -78,7 +78,10 @@ def get_cropspec(image, corners, rotation): # to the dimensions here at least solves (2), and seems to produce the same # results as you get by manually constructing a pipeline of '-perfect' # command lines. - return "%dfx%df+%d+%d" % (w, h, l, t) + if image.format == "JPEG": + return "%dfx%df+%d+%d" % (w, h, l, t) + else: + return "%dx%d+%d+%d" % (w, h, l, t) def ncpus(): From a4489d9d9bbaa5626ae8d02d561b596a92d50e7b Mon Sep 17 00:00:00 2001 From: howff Date: Mon, 30 Nov 2020 21:52:43 +0000 Subject: [PATCH 52/58] GTK version also preserves previous crop --- cropgtk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cropgtk.py b/cropgtk.py index b407fcc..d3e4fd3 100755 --- a/cropgtk.py +++ b/cropgtk.py @@ -254,6 +254,7 @@ def run(self): prev_name = None for image_name in self.image_names(): + drag.save_prev_crop() self['window1'].set_title( _("%s - CropGTK") % os.path.basename(image_name)) self.set_busy() @@ -265,7 +266,7 @@ def run(self): scale = max (scale, nextPowerOf2((drag.w-1)//(max_w+1))) scale = max (scale, nextPowerOf2((drag.h-1)//(max_h+1))) thumbnail = image.copy() - thumbnail.thumbnail((drag.w/scale, drag.h/scale)) + thumbnail.thumbnail((drag.w//scale, drag.h//scale)) except (IOError,) as detail: m = gtk.MessageDialog(self['window1'], gtk.DialogFlags.MODAL | gtk.DialogFlags.DESTROY_WITH_PARENT, From 806f1742802330b13778530bf370a22159a350fe Mon Sep 17 00:00:00 2001 From: howff Date: Mon, 30 Nov 2020 21:56:52 +0000 Subject: [PATCH 53/58] Need +repage otherwise subsequent crops fail --- cropgui_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cropgui_common.py b/cropgui_common.py index 0fe8cf0..3b5e172 100755 --- a/cropgui_common.py +++ b/cropgui_common.py @@ -165,7 +165,7 @@ def runner(self): if platform.system() == 'Windows': command = ["magick"] if rotation != "none": command += ["-rotate", rotation] - command += [image_name, "-crop", cropspec, target] + command += [image_name, "-crop", cropspec, '+repage', target] print(" ".join(command)) subprocess.call(command) From 30f824379408c014eaf5797e9fa60e298f8625bc Mon Sep 17 00:00:00 2001 From: Conrad Hughes Date: Mon, 12 Apr 2021 12:10:06 +0100 Subject: [PATCH 54/58] Better default window sizes on Gnome. --- cropgtk.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cropgtk.py b/cropgtk.py index b407fcc..e86dec7 100755 --- a/cropgtk.py +++ b/cropgtk.py @@ -219,8 +219,10 @@ def wait(self): self.loop.run() return self.result -max_h = gdk.Screen.height() - 64*3 -max_w = gdk.Screen.width() - 64 +display = gdk.Display().get_default() +wa = display.get_primary_monitor().get_workarea() +max_h = wa.height - 192 +max_w = wa.width - 64 class App: def __init__(self): @@ -262,8 +264,8 @@ def run(self): drag.round_x, drag.round_y = image_round(image) drag.w, drag.h = image.size scale = 1 - scale = max (scale, nextPowerOf2((drag.w-1)//(max_w+1))) - scale = max (scale, nextPowerOf2((drag.h-1)//(max_h+1))) + scale = max (scale, nextPowerOf2((drag.w-1)/(max_w+1))) + scale = max (scale, nextPowerOf2((drag.h-1)/(max_h+1))) thumbnail = image.copy() thumbnail.thumbnail((drag.w/scale, drag.h/scale)) except (IOError,) as detail: From d745df1338d4c7b8353d1609ca5e8eff06f1cc03 Mon Sep 17 00:00:00 2001 From: Conrad Hughes Date: Mon, 12 Apr 2021 12:34:12 +0100 Subject: [PATCH 55/58] OOB on rotate & non-crop of non-ICU-aligned images. --- cropgui_common.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cropgui_common.py b/cropgui_common.py index 3b5e172..8614e76 100755 --- a/cropgui_common.py +++ b/cropgui_common.py @@ -242,6 +242,12 @@ def fix(self, a, b, lim, r, reverse): a, b = sorted((int(a), int(b))) if reverse: a = lim - ((((lim - a) + r - 1) // r) * r) + # Rotation of non-ICU-aligned images can push bounds outside + # the visible area, and while it might be nice to be able to + # expose this, I don't know how to; so, crop inwards + # instead. + if a < 0: + a += r else: a = (a // r) * r return a, b From e64e456071d4f4be1763e2603521f62b57a9f361 Mon Sep 17 00:00:00 2001 From: howff Date: Thu, 15 Apr 2021 11:44:44 +0100 Subject: [PATCH 56/58] Treat MPO files like JPEG files --- cropgui_common.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cropgui_common.py b/cropgui_common.py index 8614e76..55d527b 100755 --- a/cropgui_common.py +++ b/cropgui_common.py @@ -78,7 +78,7 @@ def get_cropspec(image, corners, rotation): # to the dimensions here at least solves (2), and seems to produce the same # results as you get by manually constructing a pipeline of '-perfect' # command lines. - if image.format == "JPEG": + if image.format == "JPEG" or image.format == "MPO": return "%dfx%df+%d+%d" % (w, h, l, t) else: return "%dx%d+%d+%d" % (w, h, l, t) @@ -148,7 +148,7 @@ def runner(self): command = ["nice", "cp", image_name, target] if platform.system() == 'Windows': command = ["copy", image_name, target] # JPEG crop uses jpegtran - elif image.format == "JPEG": + elif image.format == "JPEG" or image.format == "MPO": command = ["nice", "jpegtran"] if platform.system() == 'Windows': command = ["jpegtran"] if rotation != "none": @@ -483,7 +483,7 @@ def image_rotation(i): def image_round(i): """Return (horizontal block size, vertical block size)""" - if i.format == "JPEG": + if i.format == "JPEG" or i.format == "MPO": x = max(xsamp for _id, xsamp, ysamp, _qtable in i.layer) y = max(ysamp for _id, xsamp, ysamp, _qtable in i.layer) return x * 8, y * 8 From a1883d683b0b77eadb90af880b8eae00e710ffe3 Mon Sep 17 00:00:00 2001 From: Johannes Kalliauer Date: Mon, 19 Apr 2021 13:32:36 +0200 Subject: [PATCH 57/58] closes #89 update packages for Python3 on Fedora --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 9e7ac61..6017548 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,7 @@ cropgui is written in Python and requires the following packages: * Debian: python3, python3-pil, python3-pil.imagetk, (python-tkinter, python-imaging, python-imaging-tk on older systems), libjpeg-progs, and libimage-exiftool-perl. - * Fedora: python2-pillow, libjpeg-turbo-utils, pygtk2, - pygtk2-libglade, ImageMagick, and perl-Image-ExifTool. + * Fedora: `dnf install python2-pillow libjpeg-turbo-utils pygtk2 pygtk2-libglade ImageMagick and perl-Image-ExifTool python3-pillow-tk`. The specific external programs required are: * `jpegtran` to crop jpeg images (debian package: libjpeg-turbo-progs or libjpeg-progs) From 6bb7c01fd67f8697dd6c999135cf04a0443e3434 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Mon, 19 Apr 2021 07:48:34 -0500 Subject: [PATCH 58/58] debian/changelog: Update package version --- debian/changelog | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 1e00a4f..7092174 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,16 @@ -cropgui (0.3) UNRELEASED; urgency=medium +cropgui (0.6~pre0) UNRELEASED; urgency=medium + + * Pre-tag the next release + + -- Jeff Epler Mon, 19 Apr 2021 07:48:04 -0500 + +cropgui (0.5) buster; urgency=medium + + * Tag a new release to help packagers + + -- Jeff Epler Mon, 19 Apr 2021 07:44:06 -0500 + +cropgui (0.3) stretch; urgency=medium * Tag a new release to help packagers * Use jpegexiforient instead of exiftool