From a7b7577fd1b4d037276dd31e10072e43a28ba27c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 5 Dec 2013 17:45:53 +0530 Subject: [PATCH] Framework for image filters, alongwith an auto-trim filter --- imgsrc/filter.svg | 702 +++++++++++++++++++ resources/images/filter.png | Bin 0 -> 8481 bytes src/calibre/gui2/tweak_book/editor/canvas.py | 80 ++- src/calibre/gui2/tweak_book/editor/image.py | 14 +- src/calibre/utils/magick/magick.c | 33 + 5 files changed, 817 insertions(+), 12 deletions(-) create mode 100644 imgsrc/filter.svg create mode 100644 resources/images/filter.png 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 0000000000000000000000000000000000000000..d7900ffa76d41c59c2998dde48b25189267b1f48 GIT binary patch literal 8481 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 literal 0 HcmV?d00001 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)" },