A dialog to trim images

This commit is contained in:
Kovid Goyal 2013-12-04 13:38:29 +05:30
parent b8733d9f41
commit a83476cea8
2 changed files with 180 additions and 8 deletions

View File

@ -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 <kovid at kovidgoyal.net>'
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_()

View File

@ -6,14 +6,15 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, string import sys, string, weakref
from functools import wraps from functools import wraps
from PyQt4.Qt import ( from PyQt4.Qt import (
QWidget, QImage, QPainter, QColor, QApplication, Qt, QPixmap, QRectF, QWidget, QImage, QPainter, QColor, QApplication, Qt, QPixmap, QRectF,
QPointF, QPen, pyqtSignal) QPointF, QPen, pyqtSignal, QUndoCommand, QUndoStack, QIcon)
from calibre import fit_image from calibre import fit_image
from calibre.gui2 import error_dialog
def painter(func): def painter(func):
@wraps(func) @wraps(func)
@ -46,6 +47,51 @@ class SelectionState(object):
self.dragging = None self.dragging = None
self.last_drag_pos = 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): class Canvas(QWidget):
BACKGROUND = QColor(60, 60, 60) BACKGROUND = QColor(60, 60, 60)
@ -53,6 +99,7 @@ class Canvas(QWidget):
SELECT_PEN = QPen(QColor(Qt.white)) SELECT_PEN = QPen(QColor(Qt.white))
selection_state_changed = pyqtSignal(object) selection_state_changed = pyqtSignal(object)
image_changed = pyqtSignal(object)
@property @property
def has_selection(self): def has_selection(self):
@ -62,21 +109,49 @@ class Canvas(QWidget):
QWidget.__init__(self, parent) QWidget.__init__(self, parent)
self.setMouseTracking(True) self.setMouseTracking(True)
self.selection_state = SelectionState() 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_image = None
self.current_scaled_pixmap = None self.current_scaled_pixmap = None
self.last_canvas_size = None self.last_canvas_size = None
self.target = QRectF(0, 0, 0, 0) 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.selection_state.reset()
self.current_image_data = data self.original_image_data = data
self.current_image = i = QImage() self.current_image = i = self.original_image = QImage()
i.loadFromData(data) i.loadFromData(data)
self.is_valid = not i.isNull() self.is_valid = not i.isNull()
self.update() 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 @property
def dc_size(self): def dc_size(self):
sr = self.selection_state.rect 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()): 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.setCursor(self.get_cursor())
self.update() self.update()
# }}}
# Painting {{{
@painter @painter
def draw_background(self, painter): def draw_background(self, painter):
painter.fillRect(self.rect(), self.BACKGROUND) painter.fillRect(self.rect(), self.BACKGROUND)
@ -300,7 +377,7 @@ class Canvas(QWidget):
p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform)
try: try:
self.draw_background(p) self.draw_background(p)
if self.current_image_data is None: if self.original_image_data is None:
return return
if not self.is_valid: if not self.is_valid:
return self.draw_image_error(p) return self.draw_image_error(p)
@ -310,12 +387,13 @@ class Canvas(QWidget):
self.draw_selection_rect(p) self.draw_selection_rect(p)
finally: finally:
p.end() p.end()
# }}}
if __name__ == '__main__': if __name__ == '__main__':
app = QApplication([]) app = QApplication([])
with open(sys.argv[-1], 'rb') as f: with open(sys.argv[-1], 'rb') as f:
data = f.read() data = f.read()
c = Canvas() c = Canvas()
c.show_image(data) c.load_image(data)
c.show() c.show()
app.exec_() app.exec_()