diff --git a/imgsrc/filter.svg b/imgsrc/filter.svg new file mode 100644 index 0000000000..fd299fccbb --- /dev/null +++ b/imgsrc/filter.svg @@ -0,0 +1,702 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Oxygen team + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/images/filter.png b/resources/images/filter.png new file mode 100644 index 0000000000..d7900ffa76 Binary files /dev/null and b/resources/images/filter.png differ diff --git a/src/calibre/gui2/tweak_book/editor/canvas.py b/src/calibre/gui2/tweak_book/editor/canvas.py index 435920782f..694dd06194 100644 --- a/src/calibre/gui2/tweak_book/editor/canvas.py +++ b/src/calibre/gui2/tweak_book/editor/canvas.py @@ -11,10 +11,12 @@ from functools import wraps from PyQt4.Qt import ( QWidget, QPainter, QColor, QApplication, Qt, QPixmap, QRectF, QMatrix, - QPointF, QPen, pyqtSignal, QUndoCommand, QUndoStack, QIcon, QImage) + QPointF, QPen, pyqtSignal, QUndoCommand, QUndoStack, QIcon, QImage, QByteArray) from calibre import fit_image from calibre.gui2 import error_dialog, pixmap_to_data +from calibre.utils.config_base import tweaks +from calibre.utils.magick import Image from calibre.utils.magick.draw import identify_data def painter(func): @@ -50,8 +52,10 @@ class SelectionState(object): class Command(QUndoCommand): - def __init__(self, text, canvas): - QUndoCommand.__init__(self, text) + TEXT = '' + + def __init__(self, canvas): + QUndoCommand.__init__(self, self.TEXT) self.canvas_ref = weakref.ref(canvas) self.before_image = i = canvas.current_image if i is None: @@ -76,12 +80,55 @@ def get_selection_rect(img, sr, target): bottom_border = (abs(target.bottom() - sr.bottom())/target.height()) * img.height() return left_border, top_border, img.width() - left_border - right_border, img.height() - top_border - bottom_border +_qimage_pixel_map = None +def get_pixel_map(): + ' Get the order of pixels in QImage (RGBA or BGRA usually) ' + global _qimage_pixel_map + if _qimage_pixel_map is None: + i = QImage(1, 1, QImage.Format_ARGB32) + i.fill(QColor(0, 1, 2, 3)) + raw = bytearray(i.constBits().asstring(4)) + _qimage_pixel_map = {c:raw.index(x) for c, x in zip('RGBA', b'\x00\x01\x02\x03')} + _qimage_pixel_map = ''.join(sorted(_qimage_pixel_map, key=_qimage_pixel_map.get)) + return _qimage_pixel_map + +def qimage_to_magick(img): + fmt = get_pixel_map() + if not img.hasAlphaChannel(): + if img.format() != img.Format_RGB32: + img = QImage(img) + img.setFormat(QImage.Format_RGB32) + fmt = fmt.replace('A', 'P') + else: + if img.format() != img.Format_ARGB32: + img = QImage(img) + img.setFormat(img.Format_ARGB32) + raw = img.constBits().ascapsule() + ans = Image() + ans.constitute(img.width(), img.height(), fmt, raw) + return ans + +def magick_to_qimage(img): + fmt = get_pixel_map() + # ImageMagick can output only output raw data in some formats that can be + # read into QImage directly, if the QImage format is not one of those, use + # PNG + if fmt in {'RGBA', 'BGRA'}: + w, h = img.size + img.depth = 8 # QImage expects 8bpp + raw = img.export(fmt) + i = QImage(raw, w, h, QImage.Format_ARGB32) + del raw # According to the documentation, raw is supposed to not be deleted, but it works, so make it explicit + return i + else: + raw = img.export('PNG') + return QImage.fromData(QByteArray(raw), 'PNG') + class Trim(Command): ''' Remove the areas of the image outside the current selection. ''' - def __init__(self, canvas): - Command.__init__(self, _('Trim image'), canvas) + TEXT = _('Trim image') def __call__(self, canvas): img = canvas.current_image @@ -89,10 +136,20 @@ class Trim(Command): sr = canvas.selection_state.rect return img.copy(*get_selection_rect(img, sr, target)) +class AutoTrim(Trim): + + ''' Auto trim borders from the image ''' + TEXT = _('Auto-trim image') + + def __call__(self, canvas): + img = canvas.current_image + i = qimage_to_magick(img) + i.trim(tweaks['cover_trim_fuzz_value']) + return magick_to_qimage(i) + class Rotate(Command): - def __init__(self, canvas): - Command.__init__(self, _('Rotate image'), canvas) + TEXT = _('Rotate image') def __call__(self, canvas): img = canvas.current_image @@ -102,9 +159,11 @@ class Rotate(Command): class Scale(Command): + TEXT = _('Resize image') + def __init__(self, width, height, canvas): self.width, self.height = width, height - Command.__init__(self, _('Resize image'), canvas) + Command.__init__(self, canvas) def __call__(self, canvas): img = canvas.current_image @@ -247,6 +306,11 @@ class Canvas(QWidget): self.undo_stack.push(Trim(self)) return True + @imageop + def autotrim_image(self): + self.undo_stack.push(AutoTrim(self)) + return True + @imageop def rotate_image(self): self.undo_stack.push(Rotate(self)) diff --git a/src/calibre/gui2/tweak_book/editor/image.py b/src/calibre/gui2/tweak_book/editor/image.py index c35143851b..9f3eee1146 100644 --- a/src/calibre/gui2/tweak_book/editor/image.py +++ b/src/calibre/gui2/tweak_book/editor/image.py @@ -11,7 +11,7 @@ from functools import partial from PyQt4.Qt import ( QMainWindow, Qt, QApplication, pyqtSignal, QLabel, QIcon, QFormLayout, - QDialog, QSpinBox, QCheckBox, QDialogButtonBox) + QDialog, QSpinBox, QCheckBox, QDialogButtonBox, QToolButton, QMenu) from calibre.gui2 import error_dialog from calibre.gui2.tweak_book import actions @@ -200,9 +200,9 @@ class Editor(QMainWindow): self.copy_available_state_changed.emit(self.copy_available) self.data_changed.emit(self) self.modification_state_changed.emit(True) - self.fmt_label.setText((self.canvas.original_image_format or '').upper()) + self.fmt_label.setText(' ' + (self.canvas.original_image_format or '').upper()) im = self.canvas.current_image - self.size_label.setText('{0} x {1}{2}'.format(im.width(), im.height(), 'px')) + self.size_label.setText('{0} x {1}{2}'.format(im.width(), im.height(), ' px')) def break_cycles(self): self.canvas.break_cycles() @@ -229,7 +229,7 @@ class Editor(QMainWindow): try: ac = actions['editor-%s' % x] except KeyError: - b.addAction(x, getattr(self.canvas, x)) + setattr(self, 'action_' + x, b.addAction(x, getattr(self.canvas, x))) else: setattr(self, 'action_' + x, b.addAction(ac.icon(), x, getattr(self, x))) self.update_clipboard_actions() @@ -238,6 +238,12 @@ class Editor(QMainWindow): self.action_trim = ac = b.addAction(QIcon(I('trim.png')), _('Trim image'), self.canvas.trim_image) self.action_rotate = ac = b.addAction(QIcon(I('rotate-right.png')), _('Rotate image'), self.canvas.rotate_image) self.action_resize = ac = b.addAction(QIcon(I('resize.png')), _('Resize image'), self.resize_image) + b.addSeparator() + self.action_filters = ac = b.addAction(QIcon(I('filter.png')), _('Image filters')) + b.widgetForAction(ac).setPopupMode(QToolButton.InstantPopup) + self.filters_menu = m = QMenu() + ac.setMenu(m) + m.addAction(_('Auto-trim image'), self.canvas.autotrim_image) self.info_bar = b = self.addToolBar(_('Image information bar')) self.fmt_label = QLabel('') diff --git a/src/calibre/utils/magick/magick.c b/src/calibre/utils/magick/magick.c index 2c7d47a3b7..7dd46d42ea 100644 --- a/src/calibre/utils/magick/magick.c +++ b/src/calibre/utils/magick/magick.c @@ -537,6 +537,35 @@ magick_Image_new(PyTypeObject *type, PyObject *args, PyObject *kwds) return (PyObject *)self; } +// Image.constitute {{{ +static PyObject * +magick_Image_constitute(magick_Image *self, PyObject *args) { + const char *map; + Py_ssize_t width, height; + PyObject *capsule; + MagickBooleanType res; + void *data; + + NULL_CHECK(NULL) + if (!PyArg_ParseTuple(args, "iisO", &width, &height, &map, &capsule)) return NULL; + + if (!PyCapsule_CheckExact(capsule)) { + PyErr_SetString(PyExc_TypeError, "data is not a capsule object"); + return NULL; + } + + data = PyCapsule_GetPointer(capsule, PyCapsule_GetName(capsule)); + if (data == NULL) return NULL; + + res = MagickConstituteImage(self->wand, width, height, map, CharPixel, data); + + if (!res) + return magick_set_exception(self->wand); + + Py_RETURN_NONE; +} + +// }}} // Image.load {{{ static PyObject * @@ -1296,6 +1325,10 @@ static PyMethodDef magick_Image_methods[] = { "Identify an image from a byte buffer (string)" }, + {"constitute", (PyCFunction)magick_Image_constitute, METH_VARARGS, + "constitute(width, height, map, data) -> Create an image from raw (A)RGB data. map should be 'ARGB' or 'PRGB' or whatever is needed for data. data must be a PyCapsule object." + }, + {"load", (PyCFunction)magick_Image_load, METH_VARARGS, "Load an image from a byte buffer (string)" },