From 8854982f14ea91d012783ff68fa1fa0bc66562c0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 4 Oct 2010 13:17:51 -0600 Subject: [PATCH] When saving cover images don't re-encode the image data unless absolutely neccessary. This prevents information loss due to JPEG re-compression --- src/calibre/utils/magick/__init__.py | 2 +- src/calibre/utils/magick/draw.py | 49 ++++++++++++++++++++++---- src/calibre/utils/magick/magick.c | 51 ++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 8 deletions(-) diff --git a/src/calibre/utils/magick/__init__.py b/src/calibre/utils/magick/__init__.py index 2707430c67..3a4fca09c0 100644 --- a/src/calibre/utils/magick/__init__.py +++ b/src/calibre/utils/magick/__init__.py @@ -158,7 +158,7 @@ class Image(_magick.Image): # {{{ format = ext[1:] format = format.upper() - with open(path, 'wb') as f: + with lopen(path, 'wb') as f: f.write(self.export(format)) def compose(self, img, left=0, top=0, operation='OverCompositeOp'): diff --git a/src/calibre/utils/magick/draw.py b/src/calibre/utils/magick/draw.py index dcf9d7b671..80fd683196 100644 --- a/src/calibre/utils/magick/draw.py +++ b/src/calibre/utils/magick/draw.py @@ -11,22 +11,57 @@ from calibre.utils.magick import Image, DrawingWand, create_canvas from calibre.constants import __appname__, __version__ from calibre import fit_image +def normalize_format_name(fmt): + fmt = fmt.lower() + if fmt == 'jpeg': + fmt = 'jpg' + return fmt + def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None, - return_data=False): + return_data=False, compression_quality=90): ''' Saves image in data to path, in the format specified by the path - extension. Composes the image onto a blank canvas so as to - properly convert transparent images. + extension. Removes any transparency. If there is no transparency and no + resize and the input and output image formats are the same, no changes are + made. + + :param compression_quality: The quality of the image after compression. + Number between 1 and 100. 1 means highest compression, 100 means no + compression (lossless). + :param bgcolor: The color for transparent pixels. Must be specified in hex. + :param resize_to: A tuple (width, height) or None for no resizing + ''' + changed = False img = Image() img.load(data) + orig_fmt = normalize_format_name(img.format) + fmt = os.path.splitext(path)[1] + fmt = normalize_format_name(fmt[1:]) + if resize_to is not None: img.size = (resize_to[0], resize_to[1]) - canvas = create_canvas(img.size[0], img.size[1], bgcolor) - canvas.compose(img) + changed = True + if not hasattr(img, 'has_transparent_pixels') or img.has_transparent_pixels(): + canvas = create_canvas(img.size[0], img.size[1], bgcolor) + canvas.compose(img) + img = canvas + changed = True + if not changed: + changed = fmt != orig_fmt if return_data: - return canvas.export(os.path.splitext(path)[1][1:]) - canvas.save(path) + if changed: + if hasattr(img, 'set_compression_quality') and fmt == 'jpg': + img.set_compression_quality(compression_quality) + return img.export(fmt) + return data + if changed: + if hasattr(img, 'set_compression_quality') and fmt == 'jpg': + img.set_compression_quality(compression_quality) + img.save(path) + else: + with lopen(path, 'wb') as f: + f.write(data) def thumbnail(data, width=120, height=120, bgcolor='#ffffff', fmt='jpg'): img = Image() diff --git a/src/calibre/utils/magick/magick.c b/src/calibre/utils/magick/magick.c index 92d68d5afd..b1436a830b 100644 --- a/src/calibre/utils/magick/magick.c +++ b/src/calibre/utils/magick/magick.c @@ -725,6 +725,49 @@ magick_Image_set_page(magick_Image *self, PyObject *args, PyObject *kwargs) { } // }}} +// Image.set_compression_quality {{{ + +static PyObject * +magick_Image_set_compression_quality(magick_Image *self, PyObject *args, PyObject *kwargs) { + Py_ssize_t quality; + + if (!PyArg_ParseTuple(args, "n", &quality)) return NULL; + + if (!MagickSetImageCompressionQuality(self->wand, quality)) return magick_set_exception(self->wand); + + Py_RETURN_NONE; +} +// }}} + +// Image.has_transparent_pixels {{{ + +static PyObject * +magick_Image_has_transparent_pixels(magick_Image *self, PyObject *args, PyObject *kwargs) { + PixelIterator *pi = NULL; + PixelWand **pixels = NULL; + int found = 0; + size_t r, c, width, height; + double alpha; + + height = MagickGetImageHeight(self->wand); + pi = NewPixelIterator(self->wand); + + for (r = 0; r < height; r++) { + pixels = PixelGetNextIteratorRow(pi, &width); + for (c = 0; c < width; c++) { + alpha = PixelGetAlpha(pixels[c]); + if (alpha < 1.00) { + found = 1; + c = width; r = height; + } + } + } + pi = DestroyPixelIterator(pi); + if (found) Py_RETURN_TRUE; + Py_RETURN_FALSE; +} +// }}} + // Image.normalize {{{ static PyObject * @@ -872,6 +915,14 @@ static PyMethodDef magick_Image_methods[] = { "set_page(width, height, x, y) \n\n Sets the page geometry of the image." }, + {"set_compression_quality", (PyCFunction)magick_Image_set_compression_quality, METH_VARARGS, + "set_compression_quality(quality) \n\n Sets the compression quality when exporting the image." + }, + + {"has_transparent_pixels", (PyCFunction)magick_Image_has_transparent_pixels, METH_VARARGS, + "has_transparent_pixels() \n\n Returns True iff image has a (semi-) transparent pixel" + }, + {"thumbnail", (PyCFunction)magick_Image_thumbnail, METH_VARARGS, "thumbnail(width, height) \n\n Convert to a thumbnail of specified size." },