diff --git a/setup/extensions.py b/setup/extensions.py index 33bb378c1a..70a5aa187c 100644 --- a/setup/extensions.py +++ b/setup/extensions.py @@ -239,6 +239,13 @@ extensions = [ sip_files=['calibre/gui2/progress_indicator/QProgressIndicator.sip'] ), + Extension('imageops', + ['calibre/utils/imageops/imageops.cpp'], + inc_dirs=['calibre/utils/imageops'], + headers=['calibre/utils/imageops/imageops.h'], + sip_files=['calibre/utils/imageops/imageops.sip'] + ), + Extension('qt_hack', ['calibre/ebooks/pdf/render/qt_hack.cpp'], inc_dirs=['calibre/ebooks/pdf/render'], diff --git a/src/calibre/constants.py b/src/calibre/constants.py index 5cb1e8e2ea..d0ff576cca 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -143,6 +143,7 @@ class Plugins(collections.Mapping): 'html', 'freetype', 'unrar', + 'imageops', 'qt_hack', '_regex', 'hunspell', diff --git a/src/calibre/utils/imageops/imageops.cpp b/src/calibre/utils/imageops/imageops.cpp new file mode 100644 index 0000000000..1e575f5166 --- /dev/null +++ b/src/calibre/utils/imageops/imageops.cpp @@ -0,0 +1,76 @@ +/* + * imageops.cpp + * Copyright (C) 2016 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#include "imageops.h" +#define SQUARE(x) (x)*(x) +#define MAX(x, y) ((x) > (y)) ? (x) : (y) +#define DISTANCE(r, g, b) (SQUARE(r - red_average) + SQUARE(g - green_average) + SQUARE(b - blue_average)) + +unsigned int read_border_row(const QImage &img, const unsigned int width, const unsigned int height, int *reds, const double fuzz, const bool top) { + unsigned int r = 0, c = 0, start = 0, delta = top ? 1 : -1, ans = 0; + const QRgb *row = NULL, *pixel = NULL; + int *greens = NULL, *blues = NULL; + double red_average = 0, green_average = 0, blue_average = 0, distance = 0, first_red = 0, first_green = 0, first_blue = 0; + + greens = reds + width + 1; blues = greens + width + 1; + start = top ? 0 : height - 1; + + for (r = start; top ? height - r : r > 0; r += delta) { + row = reinterpret_cast(img.constScanLine(r)); + red_average = 0; green_average = 0; blue_average = 0; + for (c = 0, pixel = row; c < width; c++, pixel++) { + reds[c] = qRed(*pixel); greens[c] = qGreen(*pixel); blues[c] = qBlue(*pixel); + 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, DISTANCE(reds[c], greens[c], blues[c])); + if (distance > fuzz) break; // row is not homogeneous + if (r == start) { first_red = red_average; first_green = green_average; first_blue = blue_average; } + else if (DISTANCE(first_red, first_green, first_blue) > fuzz) break; // this row's average color is far from the previous row's average color + ans += 1; + } + return ans; +} + +QImage* remove_borders(const QImage &image, double fuzz) { + int *buf = NULL; + QImage* ans = NULL, img = image, timg; + QTransform transpose; + transpose.rotate(90); + unsigned int width = img.width(), height = img.height(); + unsigned int top_border = 0, bottom_border = 0, left_border = 0, right_border = 0; + + if (img.format() != QImage::Format_RGB32) { + img = img.convertToFormat(QImage::Format_RGB32); + if (img.isNull()) { PyErr_NoMemory(); return NULL; } + } + buf = new int[3*(MAX(width, height)+1)]; + fuzz /= 255; + + top_border = read_border_row(img, width, height, buf, fuzz, true); + if (top_border >= height - 1) goto end; + bottom_border = read_border_row(img, width, height, buf, fuzz, false); + if (bottom_border >= height - 1) goto end; + timg = img.transformed(transpose); + if (timg.isNull()) { PyErr_NoMemory(); goto end; } + left_border = read_border_row(timg, height, width, buf, fuzz, true); + if (left_border >= width - 1) goto end; + right_border = read_border_row(timg, height, width, buf, fuzz, false); + if (right_border >= width - 1) goto end; + if (left_border || right_border || top_border || bottom_border) { + // printf("111111 l=%d t=%d r=%d b=%d\n", left_border, top_border, right_border, bottom_border); + img = img.copy(left_border, top_border, width - left_border - right_border, height - top_border - bottom_border); + if (img.isNull()) { PyErr_NoMemory(); goto end; } + } + +end: + delete[] buf; + if (!PyErr_Occurred()) ans = new QImage(img); + return ans; +} diff --git a/src/calibre/utils/imageops/imageops.h b/src/calibre/utils/imageops/imageops.h new file mode 100644 index 0000000000..c7ca6dab10 --- /dev/null +++ b/src/calibre/utils/imageops/imageops.h @@ -0,0 +1,14 @@ +/* + * imageops.h + * Copyright (C) 2016 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#pragma once + +#include +#include + +QImage* remove_borders(const QImage &image, double fuzz); + diff --git a/src/calibre/utils/imageops/imageops.sip b/src/calibre/utils/imageops/imageops.sip new file mode 100644 index 0000000000..273fb0b8c7 --- /dev/null +++ b/src/calibre/utils/imageops/imageops.sip @@ -0,0 +1,18 @@ +//Define the SIP wrapper to the imageops code +//Author - Kovid Goyal + +%Module(name=imageops, version=1) + +%Import QtCore/QtCoremod.sip +%Import QtGui/QtGuimod.sip +%ModuleCode +#include +%End + +QImage* remove_borders(const QImage &image, double fuzz); +%MethodCode + try { + sipRes = remove_borders(*a0, a1); + if (sipRes == NULL) return NULL; + } catch (std::bad_alloc) { PyErr_NoMemory(); return NULL; } +%End diff --git a/src/calibre/utils/img.py b/src/calibre/utils/img.py index bd5c425026..142212cf0a 100644 --- a/src/calibre/utils/img.py +++ b/src/calibre/utils/img.py @@ -10,9 +10,10 @@ from threading import Thread from PyQt5.Qt import QImage, QByteArray, QBuffer, Qt, QPainter, QImageReader, QColor from calibre import fit_image, force_unicode -from calibre.constants import iswindows +from calibre.constants import iswindows, plugins from calibre.utils.config_base import tweaks from calibre.utils.filenames import atomic_rename +imageops, imageops_err = plugins['imageops'] def get_exe_path(name): from calibre.ebooks.pdf.pdftohtml import PDFTOHTML @@ -198,6 +199,12 @@ class Canvas(object): def flip_image(img, horizontal=False, vertical=False): return image_from_data(img).mirrored(horizontal, vertical) +def remove_borders(img, fuzz=None): + if imageops is None: + raise RuntimeError(imageops_err) + fuzz = tweaks['cover_trim_fuzz_value'] if fuzz is None else fuzz + return imageops.remove_borders(image_from_data(img), max(0, fuzz)) + def run_optimizer(file_path, cmd, as_filter=False, input_data=None): file_path = os.path.abspath(file_path) cwd = os.path.dirname(file_path)