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 @@
+
+
\ 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 0000000000..2ccca44026
Binary files /dev/null and b/resources/images/compress-image.png differ
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)