From 30a8f1298b335c7d03f050699fd7f2a5e1f15f4a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 28 Nov 2015 13:08:35 +0530 Subject: [PATCH] Edit Book: Add a new tool to compress images in the book losslessly, accessed from the Tools menu --- imgsrc/compress-image.svg | 65 +++++++++ resources/images/compress-image.png | Bin 0 -> 6455 bytes src/calibre/ebooks/oeb/polish/images.py | 28 +++- src/calibre/gui2/tweak_book/boss.py | 23 ++++ src/calibre/gui2/tweak_book/polish.py | 174 +++++++++++++++++++++++- src/calibre/gui2/tweak_book/ui.py | 3 + 6 files changed, 285 insertions(+), 8 deletions(-) create mode 100644 imgsrc/compress-image.svg create mode 100644 resources/images/compress-image.png diff --git a/imgsrc/compress-image.svg b/imgsrc/compress-image.svg new file mode 100644 index 0000000000..932092bd2a --- /dev/null +++ b/imgsrc/compress-image.svg @@ -0,0 +1,65 @@ + +image/svg+xml \ No newline at end of file diff --git a/resources/images/compress-image.png b/resources/images/compress-image.png new file mode 100644 index 0000000000000000000000000000000000000000..2ccca4402691ce3c528acb77f9b0fda367dc35d3 GIT binary patch literal 6455 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 literal 0 HcmV?d00001 diff --git a/src/calibre/ebooks/oeb/polish/images.py b/src/calibre/ebooks/oeb/polish/images.py index 766a6b717b..31a203f2c0 100644 --- a/src/calibre/ebooks/oeb/polish/images.py +++ b/src/calibre/ebooks/oeb/polish/images.py @@ -6,7 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import, print_function) import os from functools import partial -from threading import Thread +from threading import Thread, Event from Queue import Queue, Empty from calibre import detect_ncpus, human_readable @@ -15,14 +15,16 @@ class Worker(Thread): daemon = True - def __init__(self, name, queue, results, container, jpeg_quality): + def __init__(self, abort, name, queue, results, container, jpeg_quality, progress_callback): Thread.__init__(self, name=name) self.queue, self.results, self.container = queue, results, container + self.progress_callback = progress_callback self.jpeg_quality = jpeg_quality + self.abort = abort self.start() def run(self): - while True: + while not self.abort.is_set(): try: name = self.queue.get_nowait() except Empty: @@ -33,6 +35,11 @@ class Worker(Thread): import traceback self.results[name] = (False, traceback.format_exc()) finally: + try: + self.progress_callback(name) + except Exception: + import traceback + traceback.print_exc() self.queue.task_done() def compress(self, name): @@ -50,19 +57,28 @@ class Worker(Thread): after = os.path.getsize(path) self.results[name] = (True, (before, after)) - -def compress_images(container, report=None, names=None, jpeg_quality=None): +def get_compressible_images(container): mt_map = container.manifest_type_map images = set() for mt in 'png jpg jpeg'.split(): images |= set(mt_map.get('image/' + mt, ())) + return images + +def compress_images(container, report=None, names=None, jpeg_quality=None, progress_callback=lambda n, t, name:True): + images = get_compressible_images(container) if names is not None: images &= set(names) results = {} queue = Queue() + abort = Event() for name in images: queue.put(name) - [Worker('CompressImage%d' % i, queue, results, container, jpeg_quality) for i in xrange(min(detect_ncpus(), len(images)))] + def pc(name): + keep_going = progress_callback(len(results), len(images), name) + if not keep_going: + abort.set() + progress_callback(0, len(images), '') + [Worker(abort, 'CompressImage%d' % i, queue, results, container, jpeg_quality, pc) for i in xrange(min(detect_ncpus(), len(images)))] queue.join() before_total = after_total = 0 for name, (ok, res) in results.iteritems(): diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index 2ff7417f5f..a6b53dbd24 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -1211,6 +1211,29 @@ class Boss(QObject): if self.ensure_book(_('You must first open a book in order to check links.')): self.gui.check_external_links.show() + def compress_images(self): + if not self.ensure_book(_('You must first open a book in order to compress images.')): + return + from calibre.gui2.tweak_book.polish import show_report, CompressImages, CompressImagesProgress + d = CompressImages(self.gui) + if d.exec_() == d.Accepted: + with BusyCursor(): + self.add_savepoint(_('Before: compress images')) + d = CompressImagesProgress(names=d.names, jpeg_quality=d.jpeg_quality, parent=self.gui) + if d.exec_() != d.Accepted: + self.rewind_savepoint() + return + changed, report = d.result + if changed is None and report: + self.rewind_savepoint() + return error_dialog(self.gui, _('Unexpected error'), _( + 'Failed to compress images, click "Show details" for more information'), det_msg=report, show=True) + if changed: + self.apply_container_update_to_gui() + else: + self.rewind_savepoint() + show_report(changed, self.current_metadata.title, report, self.gui, self.show_current_diff) + def sync_editor_to_preview(self, name, sourceline_address): editor = self.edit_file(name, 'html') self.ignore_preview_to_editor_sync = True diff --git a/src/calibre/gui2/tweak_book/polish.py b/src/calibre/gui2/tweak_book/polish.py index f6a65bda75..4e5f86a5e7 100644 --- a/src/calibre/gui2/tweak_book/polish.py +++ b/src/calibre/gui2/tweak_book/polish.py @@ -6,13 +6,20 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' +from threading import Thread from PyQt5.Qt import ( QTextBrowser, QVBoxLayout, QDialog, QDialogButtonBox, QIcon, QLabel, - QCheckBox, Qt) + QCheckBox, Qt, QListWidgetItem, QHBoxLayout, QListWidget, QPixmap, + QSpinBox, QStyledItemDelegate, QSize, QModelIndex, QStyle, QPen, + QProgressBar, pyqtSignal +) +from calibre import human_readable, fit_image from calibre.ebooks.oeb.polish.main import CUSTOMIZATION -from calibre.gui2.tweak_book import tprefs +from calibre.gui2.tweak_book import tprefs, current_container, set_current_container +from calibre.gui2.tweak_book.widgets import Dialog +from calibre.utils.icu import numeric_sort_key class Abort(Exception): pass @@ -70,3 +77,166 @@ def show_report(changed, title, report, parent, show_current_diff): d.bb.accepted.connect(d.accept) d.resize(600, 400) d.exec_() + +# CompressImages {{{ + +class ImageItemDelegate(QStyledItemDelegate): + + def sizeHint(self, option, index): + return QSize(300, 100) + + def paint(self, painter, option, index): + name = index.data(Qt.DisplayRole) + sz = human_readable(index.data(Qt.UserRole)) + pmap = index.data(Qt.UserRole+1) + irect = option.rect.adjusted(0, 5, 0, -5) + irect.setRight(irect.left() + 70) + if pmap is None: + pmap = QPixmap(current_container().get_file_path_for_processing(name)) + scaled, nwidth, nheight = fit_image(pmap.width(), pmap.height(), irect.width(), irect.height()) + if scaled: + pmap = pmap.scaled(nwidth, nheight, transformMode=Qt.SmoothTransformation) + index.model().setData(index, pmap, Qt.UserRole+1) + x, y = (irect.width() - pmap.width())//2, (irect.height() - pmap.height())//2 + r = irect.adjusted(x, y, -x, -y) + QStyledItemDelegate.paint(self, painter, option, QModelIndex()) + painter.drawPixmap(r, pmap) + trect = irect.adjusted(irect.width() + 10, 0, 0, 0) + trect.setRight(option.rect.right()) + painter.save() + if option.state & QStyle.State_Selected: + painter.setPen(QPen(option.palette.color(option.palette.HighlightedText))) + painter.drawText(trect, Qt.AlignVCenter | Qt.AlignLeft, name + '\n' + sz) + painter.restore() + +class CompressImages(Dialog): + + def __init__(self, parent=None): + Dialog.__init__(self, _('Compress Images'), 'compress-images', parent=parent) + + def setup_ui(self): + from calibre.ebooks.oeb.polish.images import get_compressible_images + self.setWindowIcon(QIcon(I('compress-image.png'))) + self.h = h = QHBoxLayout(self) + self.images = i = QListWidget(self) + h.addWidget(i) + self.l = l = QVBoxLayout() + h.addLayout(l) + c = current_container() + for name in sorted(get_compressible_images(c), key=numeric_sort_key): + x = QListWidgetItem(name, i) + x.setData(Qt.UserRole, c.filesize(name)) + i.setSelectionMode(i.ExtendedSelection) + i.setMinimumHeight(500), i.setMinimumWidth(350) + i.selectAll(), i.setSpacing(5) + self.delegate = ImageItemDelegate(self) + i.setItemDelegate(self.delegate) + self.la = la = QLabel(_( + 'You can compress the images in this book losslessly, reducing the file size of the book,' + ' without affecting image quality. Typically image size is reduced by 5 - 15%.')) + la.setWordWrap(True) + la.setMinimumWidth(250) + l.addWidget(la), l.addSpacing(30) + + self.enable_lossy = el = QCheckBox(_('Enable &lossy compression of JPEG images')) + el.setToolTip(_('This allows you to change the quality factor used for JPEG images.\nBy lowering' + ' the quality you can greatly reduce file size, at the expense of the image looking blurred.')) + l.addWidget(el) + self.h2 = h = QHBoxLayout() + l.addLayout(h) + self.jq = jq = QSpinBox(self) + jq.setMinimum(0), jq.setMaximum(100), jq.setValue(80), jq.setEnabled(False) + jq.setToolTip(_('The compression quality, 1 is high compression, 100 is low compression.\nImage' + ' quality is inversely correlated with compression quality.')) + el.toggled.connect(jq.setEnabled) + self.jql = la = QLabel(_('Compression &quality:')) + la.setBuddy(jq) + h.addWidget(la), h.addWidget(jq) + l.addStretch(10) + l.addWidget(self.bb) + + @property + def names(self): + return {item.text() for item in self.images.selectedItems()} + + @property + def jpeg_quality(self): + if not self.enable_lossy.isChecked(): + return None + return self.jq.value() + +class CompressImagesProgress(Dialog): + + gui_loop = pyqtSignal(object, object, object) + cidone = pyqtSignal() + + def __init__(self, names=None, jpeg_quality=None, parent=None): + self.names, self.jpeg_quality = names, jpeg_quality + self.keep_going = True + self.result = (None, '') + Dialog.__init__(self, _('Compressing Images...'), 'compress-images-progress', parent=parent) + self.gui_loop.connect(self.update_progress, type=Qt.QueuedConnection) + self.cidone.connect(self.accept, type=Qt.QueuedConnection) + t = Thread(name='RunCompressImages', target=self.run_compress) + t.daemon = True + t.start() + + def run_compress(self): + from calibre.gui2.tweak_book import current_container + from calibre.ebooks.oeb.polish.images import compress_images + report = [] + try: + self.result = (compress_images( + current_container(), report=report.append, names=self.names, jpeg_quality=self.jpeg_quality, + progress_callback=self.progress_callback + )[0], report) + except Exception: + import traceback + self.result = (None, traceback.format_exc()) + self.cidone.emit() + + def setup_ui(self): + self.setWindowIcon(QIcon(I('compress-image.png'))) + self.setCursor(Qt.BusyCursor) + self.setMinimumWidth(350) + self.l = l = QVBoxLayout(self) + self.la = la = QLabel(_('Compressing images, please wait...')) + la.setStyleSheet('QLabel { font-weight: bold }'), la.setAlignment(Qt.AlignCenter), la.setTextFormat(Qt.PlainText) + l.addWidget(la) + self.progress = p = QProgressBar(self) + p.setMinimum(0), p.setMaximum(0) + l.addWidget(p) + self.msg = la = QLabel('\xa0') + la.setAlignment(Qt.AlignCenter), la.setTextFormat(Qt.PlainText) + l.addWidget(la) + + self.bb.setStandardButtons(self.bb.Cancel) + l.addWidget(self.bb) + + def reject(self): + self.keep_going = False + self.bb.button(self.bb.Cancel).setEnabled(False) + Dialog.reject(self) + + def progress_callback(self, num, total, name): + self.gui_loop.emit(num, total, name) + return self.keep_going + + def update_progress(self, num, total, name): + self.progress.setMaximum(total), self.progress.setValue(num) + self.msg.setText(name) + +# }}} + +if __name__ == '__main__': + from calibre.gui2 import Application + app = Application([]) + import sys, sip + from calibre.ebooks.oeb.polish.container import get_container + c = get_container(sys.argv[-1], tweak_mode=True) + set_current_container(c) + d = CompressImages() + if d.exec_() == d.Accepted: + pass + sip.delete(app) + del app diff --git a/src/calibre/gui2/tweak_book/ui.py b/src/calibre/gui2/tweak_book/ui.py index d53dd9e7dd..15d2339351 100644 --- a/src/calibre/gui2/tweak_book/ui.py +++ b/src/calibre/gui2/tweak_book/ui.py @@ -390,6 +390,8 @@ class Main(MainWindow): 'reports.png', _('&Reports'), self.boss.show_reports, 'show-reports', ('Ctrl+Shift+R',), _('Show a report on various aspects of the book')) self.action_check_external_links = treg('insert-link.png', _('Check &external links'), self.boss.check_external_links, 'check-external-links', (), _( 'Check external links in the book')) + self.action_compress_images = treg('compress-image.png', _('Compress &images losslessly'), self.boss.compress_images, 'compress-images', (), _( + 'Compress images losslessly')) def ereg(icon, text, target, sid, keys, description): return reg(icon, text, partial(self.boss.editor_action, target), sid, keys, description) @@ -538,6 +540,7 @@ class Main(MainWindow): e.addAction(self.action_manage_fonts) e.addAction(self.action_embed_fonts) e.addAction(self.action_subset_fonts) + e.addAction(self.action_compress_images) e.addAction(self.action_smarten_punctuation) e.addAction(self.action_remove_unused_css) e.addAction(self.action_fix_html_all)