From d3dda1af39916abfa260b458abb1c400cf7ee8ab Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 22 Apr 2023 14:08:52 +0530 Subject: [PATCH] Edit book: Compress images: Support compression of images in the WEBP format as well. Fixes #2017195 [[feature] support webp in epub](https://bugs.launchpad.net/calibre/+bug/2017195) --- src/calibre/ebooks/oeb/polish/images.py | 24 ++++++--- src/calibre/gui2/tweak_book/boss.py | 2 +- src/calibre/gui2/tweak_book/polish.py | 72 ++++++++++++++++--------- 3 files changed, 64 insertions(+), 34 deletions(-) diff --git a/src/calibre/ebooks/oeb/polish/images.py b/src/calibre/ebooks/oeb/polish/images.py index 93b76ec80d..f927b9819c 100644 --- a/src/calibre/ebooks/oeb/polish/images.py +++ b/src/calibre/ebooks/oeb/polish/images.py @@ -4,22 +4,23 @@ import os from functools import partial -from threading import Thread, Event +from threading import Event, Thread -from calibre import detect_ncpus, human_readable, force_unicode, filesystem_encoding +from calibre import detect_ncpus, filesystem_encoding, force_unicode, human_readable from polyglot.builtins import iteritems -from polyglot.queue import Queue, Empty +from polyglot.queue import Empty, Queue class Worker(Thread): daemon = True - def __init__(self, abort, name, queue, results, jpeg_quality, progress_callback): + def __init__(self, abort, name, queue, results, jpeg_quality, webp_quality, progress_callback): Thread.__init__(self, name=name) self.queue, self.results = queue, results self.progress_callback = progress_callback self.jpeg_quality = jpeg_quality + self.webp_quality = webp_quality self.abort = abort self.start() @@ -43,9 +44,16 @@ class Worker(Thread): self.queue.task_done() def compress(self, name, path, mime_type): - from calibre.utils.img import optimize_png, optimize_jpeg, encode_jpeg + from calibre.utils.img import ( + encode_jpeg, encode_webp, optimize_jpeg, optimize_png, optimize_webp, + ) if 'png' in mime_type: func = optimize_png + elif 'webp' in mime_type: + if self.webp_quality is None: + func = optimize_webp + else: + func = partial(encode_webp, quality=self.jpeg_quality) elif self.jpeg_quality is None: func = optimize_jpeg else: @@ -65,12 +73,12 @@ class Worker(Thread): def get_compressible_images(container): mt_map = container.manifest_type_map images = set() - for mt in 'png jpg jpeg'.split(): + for mt in 'png jpg jpeg webp'.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): +def compress_images(container, report=None, names=None, jpeg_quality=None, webp_quality=None, progress_callback=lambda n, t, name:True): images = get_compressible_images(container) if names is not None: images &= set(names) @@ -92,7 +100,7 @@ def compress_images(container, report=None, names=None, jpeg_quality=None, progr if not keep_going: abort.set() progress_callback(0, num_to_process, '') - [Worker(abort, 'CompressImage%d' % i, queue, results, jpeg_quality, pc) for i in range(min(detect_ncpus(), num_to_process))] + [Worker(abort, 'CompressImage%d' % i, queue, results, jpeg_quality, webp_quality, pc) for i in range(min(detect_ncpus(), num_to_process))] queue.join() before_total = after_total = 0 processed_num = 0 diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index dbadd4fde1..bef6d0b2d8 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -1620,7 +1620,7 @@ class Boss(QObject): if d.exec() == QDialog.DialogCode.Accepted: with BusyCursor(): self.add_savepoint(_('Before: compress images')) - d = CompressImagesProgress(names=d.names, jpeg_quality=d.jpeg_quality, parent=self.gui) + d = CompressImagesProgress(names=d.names, jpeg_quality=d.jpeg_quality, webp_quality=d.webp_quality, parent=self.gui) if d.exec() != QDialog.DialogCode.Accepted: self.rewind_savepoint() return diff --git a/src/calibre/gui2/tweak_book/polish.py b/src/calibre/gui2/tweak_book/polish.py index 4ddfa0b2f2..c4f8e0f200 100644 --- a/src/calibre/gui2/tweak_book/polish.py +++ b/src/calibre/gui2/tweak_book/polish.py @@ -9,7 +9,7 @@ from qt.core import ( QAbstractItemView, QApplication, QCheckBox, QDialog, QDialogButtonBox, QHBoxLayout, QIcon, QLabel, QListWidget, QListWidgetItem, QPalette, QPen, QPixmap, QProgressBar, QSize, QSpinBox, QStyle, QStyledItemDelegate, Qt, QTextBrowser, QVBoxLayout, - pyqtSignal, + QWidget, pyqtSignal, ) from threading import Thread @@ -176,6 +176,35 @@ class ImageItemDelegate(QStyledItemDelegate): painter.restore() +class LossyCompression(QWidget): + + def __init__(self, image_type, default_compression=80, parent=None): + super().__init__(parent) + l = QVBoxLayout(self) + image_type = image_type.upper() + self.enable_lossy = el = QCheckBox(_('Enable &lossy compression of {} images').format(image_type)) + el.setToolTip(_('This allows you to change the quality factor used for {} images.\nBy lowering' + ' the quality you can greatly reduce file size, at the expense of the image looking blurred.'.format(image_type))) + l.addWidget(el) + self.h2 = h = QHBoxLayout() + l.addLayout(h) + self.jq = jq = QSpinBox(self) + image_type = image_type.lower() + self.image_type = image_type + self.quality_pref_name = f'{image_type}_compression_quality_for_lossless_compression' + jq.setMinimum(1), jq.setMaximum(100), jq.setValue(tprefs.get(self.quality_pref_name, default_compression)) + jq.setEnabled(False) + jq.setToolTip(_('The image quality, 1 is high compression with low image quality, 100 is low compression with high image quality')) + jq.valueChanged.connect(self.save_compression_quality) + el.toggled.connect(jq.setEnabled) + self.jql = la = QLabel(_('Image &quality:')) + la.setBuddy(jq) + h.addWidget(la), h.addWidget(jq) + + def save_compression_quality(self): + tprefs.set(self.quality_pref_name, self.jq.value()) + + class CompressImages(Dialog): def __init__(self, parent=None): @@ -203,38 +232,30 @@ class CompressImages(Dialog): ' 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(tprefs.get('jpeg_compression_quality_for_lossless_compression', 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.')) - jq.valueChanged.connect(self.save_compression_quality) - el.toggled.connect(jq.setEnabled) - self.jql = la = QLabel(_('Compression &quality:')) - la.setBuddy(jq) - h.addWidget(la), h.addWidget(jq) + l.addWidget(la) + self.jpeg = LossyCompression('jpeg', parent=self) + l.addSpacing(30), l.addWidget(self.jpeg) + self.webp = LossyCompression('webp', default_compression=75, parent=self) + l.addSpacing(30), l.addWidget(self.webp) l.addStretch(10) l.addWidget(self.bb) - def save_compression_quality(self): - tprefs.set('jpeg_compression_quality_for_lossless_compression', self.jq.value()) - @property def names(self): return {item.text() for item in self.images.selectedItems()} @property def jpeg_quality(self): - if not self.enable_lossy.isChecked(): + if not self.jpeg.enable_lossy.isChecked(): return None - return self.jq.value() + return self.jpeg.jq.value() + + @property + def webp_quality(self): + if not self.webp.enable_lossy.isChecked(): + return None + return self.webp.jq.value() + class CompressImagesProgress(Dialog): @@ -242,8 +263,9 @@ class CompressImagesProgress(Dialog): gui_loop = pyqtSignal(object, object, object) cidone = pyqtSignal() - def __init__(self, names=None, jpeg_quality=None, parent=None): + def __init__(self, names=None, jpeg_quality=None, webp_quality=None, parent=None): self.names, self.jpeg_quality = names, jpeg_quality + self.webp_quality = webp_quality self.keep_going = True self.result = (None, '') Dialog.__init__(self, _('Compressing images...'), 'compress-images-progress', parent=parent) @@ -259,7 +281,7 @@ class CompressImagesProgress(Dialog): report = [] try: self.result = (compress_images( - current_container(), report=report.append, names=self.names, jpeg_quality=self.jpeg_quality, + current_container(), report=report.append, names=self.names, jpeg_quality=self.jpeg_quality, webp_quality=self.webp_quality, progress_callback=self.progress_callback )[0], report) except Exception: