Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates of possible interest #81

Merged
merged 7 commits into from
Nov 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 56 additions & 31 deletions cropgtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,29 @@ 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()

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()
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -268,51 +283,61 @@ 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:
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
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(self['window1'], title)
self.dirchooser.set_current_folder(desktop_name())
self.dirchooser = filechooser.DirChooser(title, self['window1'])
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 ''
Expand Down
33 changes: 5 additions & 28 deletions cropgui_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down Expand Up @@ -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()
Expand All @@ -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):
Expand Down
8 changes: 6 additions & 2 deletions filechooser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -92,7 +95,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)
Expand Down Expand Up @@ -132,8 +135,9 @@ 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)
self.dialog.set_do_overwrite_confirmation(True)

def set_current_name(self, filename):
self.dialog.set_current_name(filename)
Expand Down
15 changes: 15 additions & 0 deletions generate_test_images.sh
Original file line number Diff line number Diff line change
@@ -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
12 changes: 6 additions & 6 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down