From a83476cea83cb71774e244b05f1d84ff62e24d2e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 4 Dec 2013 13:38:29 +0530 Subject: [PATCH] A dialog to trim images --- src/calibre/gui2/dialogs/trim_image.py | 94 +++++++++++++++++++++ src/calibre/gui2/tweak_book/editor/image.py | 94 +++++++++++++++++++-- 2 files changed, 180 insertions(+), 8 deletions(-) create mode 100644 src/calibre/gui2/dialogs/trim_image.py diff --git a/src/calibre/gui2/dialogs/trim_image.py b/src/calibre/gui2/dialogs/trim_image.py new file mode 100644 index 0000000000..64893d7a70 --- /dev/null +++ b/src/calibre/gui2/dialogs/trim_image.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2013, Kovid Goyal ' + +import sys + +from PyQt4.Qt import ( + QDialog, QGridLayout, QToolBar, Qt, QLabel, QIcon, QDialogButtonBox, QSize, + QApplication, QKeySequence) + +from calibre.gui2 import gprefs +from calibre.gui2.tweak_book.editor.image import Canvas + +class TrimImage(QDialog): + + def __init__(self, img_data, parent=None): + QDialog.__init__(self, parent) + self.l = l = QGridLayout(self) + self.setLayout(l) + self.setWindowTitle(_('Trim Image')) + + self.bar = b = QToolBar(self) + l.addWidget(b) + b.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + b.setIconSize(QSize(32, 32)) + + self.msg = la = QLabel('\xa0' + _( + 'Select a region by dragging with your mouse on the image, and then click trim')) + self.sz = QLabel('') + + self.canvas = c = Canvas(self) + c.image_changed.connect(self.image_changed) + c.load_image(img_data) + self.undo_action = u = c.undo_action + u.setShortcut(QKeySequence(QKeySequence.Undo)) + self.redo_action = r = c.redo_action + r.setShortcut(QKeySequence(QKeySequence.Redo)) + self.trim_action = ac = self.bar.addAction(QIcon(I('trim.png')), _('&Trim'), c.trim_image) + ac.setShortcut(QKeySequence('Ctrl+T')) + ac.setToolTip('%s [%s]' % (_('Trim image by removing borders outside the selected region'), ac.shortcut().toString(QKeySequence.NativeText))) + ac.setEnabled(False) + c.selection_state_changed.connect(self.selection_changed) + l.addWidget(c) + self.bar.addAction(self.trim_action) + self.bar.addSeparator() + self.bar.addAction(u) + self.bar.addAction(r) + self.bar.addSeparator() + self.bar.addWidget(la) + self.bar.addSeparator() + self.bar.addWidget(self.sz) + + self.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + bb.accepted.connect(self.accept) + bb.rejected.connect(self.reject) + l.addWidget(bb) + + self.resize(QSize(800, 600)) + geom = gprefs.get('image-trim-dialog-geometry', None) + if geom is not None: + self.restoreGeometry(geom) + self.setWindowIcon(self.trim_action.icon()) + + def selection_changed(self, has_selection): + self.trim_action.setEnabled(has_selection) + self.msg.setVisible(not has_selection) + + def image_changed(self, qimage): + self.sz.setText('\xa0' + _('Size:') + '%dx%d' % (qimage.width(), qimage.height())) + + def cleanup(self): + self.canvas.cleanup() + gprefs.set('image-trim-dialog-geometry', bytearray(self.saveGeometry())) + + def accept(self): + self.cleanup() + QDialog.accept(self) + + def reject(self): + self.cleanup() + QDialog.reject(self) + +if __name__ == '__main__': + app = QApplication([]) + with open(sys.argv[-1], 'rb') as f: + data = f.read() + d = TrimImage(data) + d.exec_() + + diff --git a/src/calibre/gui2/tweak_book/editor/image.py b/src/calibre/gui2/tweak_book/editor/image.py index 6e5232978f..20bf3f3ade 100644 --- a/src/calibre/gui2/tweak_book/editor/image.py +++ b/src/calibre/gui2/tweak_book/editor/image.py @@ -6,14 +6,15 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' -import sys, string +import sys, string, weakref from functools import wraps from PyQt4.Qt import ( QWidget, QImage, QPainter, QColor, QApplication, Qt, QPixmap, QRectF, - QPointF, QPen, pyqtSignal) + QPointF, QPen, pyqtSignal, QUndoCommand, QUndoStack, QIcon) from calibre import fit_image +from calibre.gui2 import error_dialog def painter(func): @wraps(func) @@ -46,6 +47,51 @@ class SelectionState(object): self.dragging = None self.last_drag_pos = None +class Command(QUndoCommand): + + def __init__(self, text, canvas): + QUndoCommand.__init__(self, text) + self.canvas_ref = weakref.ref(canvas) + self.before_image = i = canvas.current_image + if i is None: + raise ValueError('No image loaded') + if i.isNull(): + raise ValueError('Cannot perform operations on invalid images') + self.after_image = canvas.current_image = self(canvas) + + def undo(self): + canvas = self.canvas_ref() + canvas.set_image(self.before_image) + + def redo(self): + canvas = self.canvas_ref() + canvas.set_image(self.after_image) + +class Trim(Command): + + def __init__(self, canvas): + Command.__init__(self, _('Trim image'), canvas) + + def __call__(self, canvas): + img = canvas.current_image + target = canvas.target + sr = canvas.selection_state.rect + left_border = (abs(sr.left() - target.left())/target.width()) * img.width() + top_border = (abs(sr.top() - target.top())/target.height()) * img.height() + right_border = (abs(target.right() - sr.right())/target.width()) * img.width() + bottom_border = (abs(target.bottom() - sr.bottom())/target.height()) * img.height() + return img.copy(left_border, top_border, img.width() - left_border - right_border, img.height() - top_border - bottom_border) + +def imageop(func): + @wraps(func) + def ans(self, *args, **kwargs): + if self.original_image_data is None: + return error_dialog(self, _('No image'), _('No image loaded'), show=True) + if not self.is_valid: + return error_dialog(self, _('Invalid image'), _('The current image is not valid'), show=True) + return func(self, *args, **kwargs) + return ans + class Canvas(QWidget): BACKGROUND = QColor(60, 60, 60) @@ -53,6 +99,7 @@ class Canvas(QWidget): SELECT_PEN = QPen(QColor(Qt.white)) selection_state_changed = pyqtSignal(object) + image_changed = pyqtSignal(object) @property def has_selection(self): @@ -62,21 +109,49 @@ class Canvas(QWidget): QWidget.__init__(self, parent) self.setMouseTracking(True) self.selection_state = SelectionState() + self.undo_stack = QUndoStack() + self.undo_stack.setUndoLimit(10) - self.current_image_data = None + self.original_image_data = None self.current_image = None self.current_scaled_pixmap = None self.last_canvas_size = None self.target = QRectF(0, 0, 0, 0) - def show_image(self, data): + self.undo_action = a = self.undo_stack.createUndoAction(self, _('Undo') + ' ') + a.setIcon(QIcon(I('edit-undo.png'))) + self.redo_action = a = self.undo_stack.createRedoAction(self, _('Redo') + ' ') + a.setIcon(QIcon(I('edit-redo.png'))) + + def load_image(self, data): self.selection_state.reset() - self.current_image_data = data - self.current_image = i = QImage() + self.original_image_data = data + self.current_image = i = self.original_image = QImage() i.loadFromData(data) self.is_valid = not i.isNull() self.update() + self.image_changed.emit(self.current_image) + def set_image(self, qimage): + self.selection_state.reset() + self.current_scaled_pixmap = None + self.current_image = qimage + self.is_valid = not qimage.isNull() + self.update() + self.image_changed.emit(self.current_image) + + def cleanup(self): + self.undo_stack.clear() + self.original_image_data = self.current_image = self.current_scaled_pixmap = None + + @imageop + def trim_image(self): + if self.selection_state.rect is None: + return error_dialog(self, _('No selection'), _( + 'No active selection, first select a region in the image, by dragging with your mouse'), show=True) + self.undo_stack.push(Trim(self)) + + # The selection rectangle {{{ @property def dc_size(self): sr = self.selection_state.rect @@ -225,7 +300,9 @@ class Canvas(QWidget): elif self.selection_state.current_mode == 'selected' and self.selection_state.rect is not None and self.selection_state.rect.contains(ev.pos()): self.setCursor(self.get_cursor()) self.update() + # }}} + # Painting {{{ @painter def draw_background(self, painter): painter.fillRect(self.rect(), self.BACKGROUND) @@ -300,7 +377,7 @@ class Canvas(QWidget): p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) try: self.draw_background(p) - if self.current_image_data is None: + if self.original_image_data is None: return if not self.is_valid: return self.draw_image_error(p) @@ -310,12 +387,13 @@ class Canvas(QWidget): self.draw_selection_rect(p) finally: p.end() + # }}} if __name__ == '__main__': app = QApplication([]) with open(sys.argv[-1], 'rb') as f: data = f.read() c = Canvas() - c.show_image(data) + c.load_image(data) c.show() app.exec_()