diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui
index afc5e55449..741f8cae54 100644
--- a/src/calibre/gui2/dialogs/metadata_bulk.ui
+++ b/src/calibre/gui2/dialogs/metadata_bulk.ui
@@ -605,8 +605,13 @@ Future conversion of these books will use the default settings.
-
+
+ Try to automatically detect and remove borders and extra space
+ from the edges of cover images. This can sometimes remove too
+ much, so use with care.
+
- &Trim cover
+ &Trim cover (DANGEROUS)
@@ -1119,8 +1124,8 @@ not multiple and the destination field is multiple
0
0
- 934
- 256
+ 203
+ 58
diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py
index a596e43f35..6238d0c08a 100644
--- a/src/calibre/gui2/metadata/basic_widgets.py
+++ b/src/calibre/gui2/metadata/basic_widgets.py
@@ -902,6 +902,9 @@ class Cover(ImageView): # {{{
_('&Browse'), parent)
self.trim_cover_button = QPushButton(QIcon(I('trim.png')),
_('T&rim'), parent)
+ self.trim_cover_button.setToolTip(_(
+ 'Automatically detect and remove extra space at the cover\'s edges.\n'
+ 'Pressing it repeatedly can sometimes remove stubborn borders.'))
self.remove_cover_button = QPushButton(QIcon(I('trash.png')),
_('&Remove'), parent)
diff --git a/src/calibre/utils/magick/__init__.py b/src/calibre/utils/magick/__init__.py
index 99ed2d98be..0c1315eb71 100644
--- a/src/calibre/utils/magick/__init__.py
+++ b/src/calibre/utils/magick/__init__.py
@@ -225,6 +225,13 @@ class Image(_magick.Image): # {{{
_magick.Image.quantize(self, number_colors, colorspace, treedepth, dither,
measure_error)
+ def trim(self, fuzz):
+ try:
+ _magick.Image.remove_border(self, fuzz)
+ except AttributeError:
+ _magick.Image.trim(self, fuzz)
+
+
# }}}
def create_canvas(width, height, bgcolor='#ffffff'):
diff --git a/src/calibre/utils/magick/magick.c b/src/calibre/utils/magick/magick.c
index 0a4c3bd866..e306ad9065 100644
--- a/src/calibre/utils/magick/magick.c
+++ b/src/calibre/utils/magick/magick.c
@@ -7,6 +7,9 @@
// Ensure that the underlying MagickWand has not been deleted
#define NULL_CHECK(x) if(self->wand == NULL) {PyErr_SetString(PyExc_ValueError, "Underlying ImageMagick Wand has been destroyed"); return x; }
+#define MAX(x, y) ((x > y) ? x: y)
+#define ABS(x) ((x < 0) ? -x : x)
+#define SQUARE(x) x*x
// magick_set_exception {{{
PyObject* magick_set_exception(MagickWand *wand) {
@@ -17,6 +20,15 @@ PyObject* magick_set_exception(MagickWand *wand) {
desc = MagickRelinquishMemory(desc);
return NULL;
}
+
+PyObject* pw_iterator_set_exception(PixelIterator *pi) {
+ ExceptionType ext;
+ char *desc = PixelGetIteratorException(pi, &ext);
+ PyErr_SetString(PyExc_Exception, desc);
+ PixelClearIteratorException(pi);
+ desc = MagickRelinquishMemory(desc);
+ return NULL;
+}
// }}}
// PixelWand object definition {{{
@@ -840,6 +852,80 @@ magick_Image_trim(magick_Image *self, PyObject *args) {
}
// }}}
+// Image.remove_border {{{
+
+static size_t
+magick_find_border(PixelIterator *pi, double fuzz, size_t img_width, double *reds, PixelWand** (*next)(PixelIterator*, size_t*)) {
+ size_t band = 0, width = 0, c = 0;
+ double *greens = NULL, *blues = NULL, red_average, green_average, blue_average, distance, first_row[3] = {0.0, 0.0, 0.0};
+ PixelWand **pixels = NULL;
+
+ greens = reds + img_width + 1; blues = greens + img_width + 1;
+
+ while ( (pixels = next(pi, &width)) != NULL ) {
+ red_average = 0; green_average = 0; blue_average = 0;
+ for (c = 0; c < width; c++) {
+ reds[c] = PixelGetRed(pixels[c]); greens[c] = PixelGetGreen(pixels[c]); blues[c] = PixelGetBlue(pixels[c]);
+ /* PixelGetHSL(pixels[c], reds + c, greens + c, blues + c); */
+ red_average += reds[c]; green_average += greens[c]; blue_average += blues[c];
+ }
+ red_average /= MAX(1, width); green_average /= MAX(1, width); blue_average /= MAX(1, width);
+ distance = 0;
+ for (c = 0; c < width && distance < fuzz; c++)
+ distance = MAX(distance, SQUARE((reds[c] - red_average)) + SQUARE((greens[c] - green_average)) + SQUARE((blues[c] - blue_average)));
+ if (distance > fuzz) break; // row is not homogeneous
+ if (band == 0) {first_row[0] = red_average; first_row[1] = blue_average; first_row[2] = green_average; }
+ else {
+ distance = SQUARE((first_row[0] - red_average)) + SQUARE((first_row[1] - green_average)) + SQUARE((first_row[2] - blue_average));
+ if (distance > fuzz) break; // this row's average color is far from the previous rows average color
+ }
+ band += 1;
+ }
+ return band;
+}
+
+static PyObject *
+magick_Image_remove_border(magick_Image *self, PyObject *args) {
+ double fuzz, *buf = NULL;
+ PixelIterator *pi = NULL;
+ size_t width, height, iwidth, iheight;
+ size_t top_band = 0, bottom_band = 0, left_band = 0, right_band = 0;
+
+ NULL_CHECK(NULL)
+
+ if (!PyArg_ParseTuple(args, "d", &fuzz)) return NULL;
+ fuzz /= 255;
+
+ height = iwidth = MagickGetImageHeight(self->wand);
+ width = iheight = MagickGetImageWidth(self->wand);
+ buf = PyMem_New(double, 3*(MAX(width, height)+1));
+ pi = NewPixelIterator(self->wand);
+ if (buf == NULL || pi == NULL) { PyErr_NoMemory(); goto end; }
+ top_band = magick_find_border(pi, fuzz, width, buf, &PixelGetNextIteratorRow);
+ if (top_band >= height) goto end;
+ PixelSetLastIteratorRow(pi);
+ bottom_band = magick_find_border(pi, fuzz, width, buf, &PixelGetPreviousIteratorRow);
+ if (bottom_band >= height) goto end;
+ if (!MagickTransposeImage(self->wand)) { magick_set_exception(self->wand); goto end; }
+ pi = DestroyPixelIterator(pi);
+ pi = NewPixelIterator(self->wand);
+ if (pi == NULL) { PyErr_NoMemory(); goto end; }
+ left_band = magick_find_border(pi, fuzz, iwidth, buf, &PixelGetNextIteratorRow);
+ if (left_band >= iheight) goto end;
+ PixelSetLastIteratorRow(pi);
+ right_band = magick_find_border(pi, fuzz, iwidth, buf, &PixelGetPreviousIteratorRow);
+ if (right_band >= iheight) goto end;
+ if (!MagickTransposeImage(self->wand)) { magick_set_exception(self->wand); goto end; }
+ if (!MagickCropImage(self->wand, width - left_band - right_band, height - top_band - bottom_band, left_band, top_band)) { magick_set_exception(self->wand); goto end; }
+end:
+ if (pi != NULL) pi = DestroyPixelIterator(pi);
+ if (buf != NULL) PyMem_Free(buf);
+ if (PyErr_Occurred() != NULL) return NULL;
+
+ return Py_BuildValue("kkkk", left_band, top_band, right_band, bottom_band);
+}
+// }}}
+
// Image.thumbnail {{{
static PyObject *
@@ -1229,6 +1315,10 @@ static PyMethodDef magick_Image_methods[] = {
"trim(fuzz) \n\n Trim image."
},
+ {"remove_border", (PyCFunction)magick_Image_remove_border, METH_VARARGS,
+ "remove_border(fuzz) \n\n Try to detect and remove borders from the image, better than the ImageMagick trim() method. Detects rows of the same color at each image edge. Where color similarity testing is based on the fuzz factor (a number between 0 and 255). Returns the number of columns/rows removed from the left, top, right and bottom edges of the image."
+ },
+
{"crop", (PyCFunction)magick_Image_crop, METH_VARARGS,
"crop(width, height, x, y) \n\n Crop image."
},