From f30860ff1d95fb3c7d747cc169ab11950848a730 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 4 Oct 2010 13:15:21 -0600 Subject: [PATCH 1/5] Trim cover: Use a lossless intermediate format --- src/calibre/gui2/dialogs/metadata_single.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 6c57e30166..b39b752ac6 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -308,7 +308,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): im = Image() im.load(cdata) im.trim(10) - cdata = im.export('jpg') + cdata = im.export('png') pix = QPixmap() pix.loadFromData(cdata) self.cover.setPixmap(pix) From 8854982f14ea91d012783ff68fa1fa0bc66562c0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 4 Oct 2010 13:17:51 -0600 Subject: [PATCH 2/5] 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." }, From 588ce1adb86f9a0e421f10206ae2e5fff3a561ce Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 4 Oct 2010 14:04:25 -0600 Subject: [PATCH 3/5] Drag 'n drop: Dont start drag if any keyboard modifiers are pressed --- src/calibre/gui2/library/views.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 40f74425c8..051f871e73 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -500,11 +500,13 @@ class BooksView(QTableView): # {{{ return QTableView.mousePressEvent(self, event) def mouseMoveEvent(self, event): - if not (event.buttons() & Qt.LeftButton) or self.drag_start_pos is None: - return - if (event.pos() - self.drag_start_pos).manhattanLength() \ - < QApplication.startDragDistance(): - return + if not (event.buttons() & Qt.LeftButton) or \ + self.drag_start_pos is None or \ + QApplication.keyboardModifiers() != Qt.NoModifier or \ + (event.pos() - self.drag_start_pos).manhattanLength() \ + < QApplication.startDragDistance(): + return QTableView.mouseMoveEvent(self, event) + index = self.indexAt(event.pos()) if not index.isValid(): return From fbb87d9f8a02ca810a16e916b4b28d297ad8730e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 4 Oct 2010 14:07:44 -0600 Subject: [PATCH 4/5] Improved recipe for The Guardian --- resources/recipes/guardian.recipe | 35 ++++++++++++++----------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/resources/recipes/guardian.recipe b/resources/recipes/guardian.recipe index 344e061c26..17138fe909 100644 --- a/resources/recipes/guardian.recipe +++ b/resources/recipes/guardian.recipe @@ -8,10 +8,16 @@ www.guardian.co.uk ''' from calibre import strftime from calibre.web.feeds.news import BasicNewsRecipe +from datetime import date class Guardian(BasicNewsRecipe): - title = u'The Guardian' + title = u'The Guardian / The Observer' + if date.today().weekday() == 6: + base_url = "http://www.guardian.co.uk/theobserver" + else: + base_url = "http://www.guardian.co.uk/theguardian" + __author__ = 'Seabound and Sujata Raman' language = 'en_GB' @@ -19,6 +25,10 @@ class Guardian(BasicNewsRecipe): max_articles_per_feed = 100 remove_javascript = True + # List of section titles to ignore + # For example: ['Sport'] + ignore_sections = [] + timefmt = ' [%a, %d %b %Y]' keep_only_tags = [ dict(name='div', attrs={'id':["content","article_header","main-article-info",]}), @@ -28,6 +38,7 @@ class Guardian(BasicNewsRecipe): dict(name='div', attrs={'id':["article-toolbox","subscribe-feeds",]}), dict(name='ul', attrs={'class':["pagination"]}), dict(name='ul', attrs={'id':["content-actions"]}), + dict(name='img'), ] use_embedded_content = False @@ -43,18 +54,6 @@ class Guardian(BasicNewsRecipe): #match-stats-summary{font-size:small; font-family:Arial,Helvetica,sans-serif;font-weight:normal;} ''' - feeds = [ - ('Front Page', 'http://www.guardian.co.uk/rss'), - ('Business', 'http://www.guardian.co.uk/business/rss'), - ('Sport', 'http://www.guardian.co.uk/sport/rss'), - ('Culture', 'http://www.guardian.co.uk/culture/rss'), - ('Money', 'http://www.guardian.co.uk/money/rss'), - ('Life & Style', 'http://www.guardian.co.uk/lifeandstyle/rss'), - ('Travel', 'http://www.guardian.co.uk/travel/rss'), - ('Environment', 'http://www.guardian.co.uk/environment/rss'), - ('Comment','http://www.guardian.co.uk/commentisfree/rss'), - ] - def get_article_url(self, article): url = article.get('guid', None) if '/video/' in url or '/flyer/' in url or '/quiz/' in url or \ @@ -76,7 +75,8 @@ class Guardian(BasicNewsRecipe): return soup def find_sections(self): - soup = self.index_to_soup('http://www.guardian.co.uk/theguardian') + # soup = self.index_to_soup("http://www.guardian.co.uk/theobserver") + soup = self.index_to_soup(self.base_url) # find cover pic img = soup.find( 'img',attrs ={'alt':'Guardian digital edition'}) if img is not None: @@ -113,13 +113,10 @@ class Guardian(BasicNewsRecipe): try: feeds = [] for title, href in self.find_sections(): - feeds.append((title, list(self.find_articles(href)))) + if not title in self.ignore_sections: + feeds.append((title, list(self.find_articles(href)))) return feeds except: raise NotImplementedError - def postprocess_html(self,soup,first): - return soup.findAll('html')[0] - - From 7f6996dcf6b1501c2eb0f8ab2baa123277d88813 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 4 Oct 2010 14:16:46 -0600 Subject: [PATCH 5/5] Fix regression that broke setting of MOBI metadata --- src/calibre/ebooks/metadata/mobi.py | 12 +++++++----- src/calibre/ebooks/mobi/writer.py | 27 +++++++++++++++++++++++---- src/calibre/utils/magick/draw.py | 2 ++ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/calibre/ebooks/metadata/mobi.py b/src/calibre/ebooks/metadata/mobi.py index 408bab828d..30668d70f7 100644 --- a/src/calibre/ebooks/metadata/mobi.py +++ b/src/calibre/ebooks/metadata/mobi.py @@ -404,14 +404,16 @@ class MetadataUpdater(object): if self.cover_record is not None: size = len(self.cover_record) cover = rescale_image(data, size) - cover += '\0' * (size - len(cover)) - self.cover_record[:] = cover + if len(cover) <= size: + cover += '\0' * (size - len(cover)) + self.cover_record[:] = cover if self.thumbnail_record is not None: size = len(self.thumbnail_record) thumbnail = rescale_image(data, size, dimen=MAX_THUMB_DIMEN) - thumbnail += '\0' * (size - len(thumbnail)) - self.thumbnail_record[:] = thumbnail - return + if len(thumbnail) <= size: + thumbnail += '\0' * (size - len(thumbnail)) + self.thumbnail_record[:] = thumbnail + return def set_metadata(stream, mi): mu = MetadataUpdater(stream) diff --git a/src/calibre/ebooks/mobi/writer.py b/src/calibre/ebooks/mobi/writer.py index e8fc4557fd..5e4dca4a9e 100644 --- a/src/calibre/ebooks/mobi/writer.py +++ b/src/calibre/ebooks/mobi/writer.py @@ -112,15 +112,34 @@ def align_block(raw, multiple=4, pad='\0'): def rescale_image(data, maxsizeb, dimen=None): if dimen is not None: - return thumbnail(data, width=dimen, height=dimen)[-1] - # Replace transparent pixels with white pixels and convert to JPEG - data = save_cover_data_to(data, 'img.jpg', return_data=True) + data = thumbnail(data, width=dimen, height=dimen)[-1] + else: + # Replace transparent pixels with white pixels and convert to JPEG + data = save_cover_data_to(data, 'img.jpg', return_data=True) + if len(data) <= maxsizeb: + return data + orig_data = data + img = Image() + quality = 95 + + if hasattr(img, 'set_compression_quality'): + img.load(data) + while len(data) >= maxsizeb and quality >= 10: + quality -= 5 + img.set_compression_quality(quality) + data = img.export('jpg') + if len(data) <= maxsizeb: + return data + orig_data = data + scale = 0.9 while len(data) >= maxsizeb and scale >= 0.05: img = Image() - img.load(data) + img.load(orig_data) w, h = img.size img.size = (int(scale*w), int(scale*h)) + if hasattr(img, 'set_compression_quality'): + img.set_compression_quality(quality) data = img.export('jpg') scale -= 0.05 return data diff --git a/src/calibre/utils/magick/draw.py b/src/calibre/utils/magick/draw.py index 80fd683196..88f488cb23 100644 --- a/src/calibre/utils/magick/draw.py +++ b/src/calibre/utils/magick/draw.py @@ -72,6 +72,8 @@ def thumbnail(data, width=120, height=120, bgcolor='#ffffff', fmt='jpg'): img.size = (nwidth, nheight) canvas = create_canvas(img.size[0], img.size[1], bgcolor) canvas.compose(img) + if fmt == 'jpg' and hasattr(canvas, 'set_compression_quality'): + canvas.set_compression_quality(70) return (canvas.size[0], canvas.size[1], canvas.export(fmt)) def identify_data(data):