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)

This commit is contained in:
Kovid Goyal 2023-04-22 14:08:52 +05:30
parent c6c8fbeb64
commit d3dda1af39
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 64 additions and 34 deletions

View File

@ -4,22 +4,23 @@
import os import os
from functools import partial 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.builtins import iteritems
from polyglot.queue import Queue, Empty from polyglot.queue import Empty, Queue
class Worker(Thread): class Worker(Thread):
daemon = True 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) Thread.__init__(self, name=name)
self.queue, self.results = queue, results self.queue, self.results = queue, results
self.progress_callback = progress_callback self.progress_callback = progress_callback
self.jpeg_quality = jpeg_quality self.jpeg_quality = jpeg_quality
self.webp_quality = webp_quality
self.abort = abort self.abort = abort
self.start() self.start()
@ -43,9 +44,16 @@ class Worker(Thread):
self.queue.task_done() self.queue.task_done()
def compress(self, name, path, mime_type): 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: if 'png' in mime_type:
func = optimize_png 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: elif self.jpeg_quality is None:
func = optimize_jpeg func = optimize_jpeg
else: else:
@ -65,12 +73,12 @@ class Worker(Thread):
def get_compressible_images(container): def get_compressible_images(container):
mt_map = container.manifest_type_map mt_map = container.manifest_type_map
images = set() images = set()
for mt in 'png jpg jpeg'.split(): for mt in 'png jpg jpeg webp'.split():
images |= set(mt_map.get('image/' + mt, ())) images |= set(mt_map.get('image/' + mt, ()))
return images 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) images = get_compressible_images(container)
if names is not None: if names is not None:
images &= set(names) images &= set(names)
@ -92,7 +100,7 @@ def compress_images(container, report=None, names=None, jpeg_quality=None, progr
if not keep_going: if not keep_going:
abort.set() abort.set()
progress_callback(0, num_to_process, '') 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() queue.join()
before_total = after_total = 0 before_total = after_total = 0
processed_num = 0 processed_num = 0

View File

@ -1620,7 +1620,7 @@ class Boss(QObject):
if d.exec() == QDialog.DialogCode.Accepted: if d.exec() == QDialog.DialogCode.Accepted:
with BusyCursor(): with BusyCursor():
self.add_savepoint(_('Before: compress images')) 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: if d.exec() != QDialog.DialogCode.Accepted:
self.rewind_savepoint() self.rewind_savepoint()
return return

View File

@ -9,7 +9,7 @@ from qt.core import (
QAbstractItemView, QApplication, QCheckBox, QDialog, QDialogButtonBox, QHBoxLayout, QAbstractItemView, QApplication, QCheckBox, QDialog, QDialogButtonBox, QHBoxLayout,
QIcon, QLabel, QListWidget, QListWidgetItem, QPalette, QPen, QPixmap, QProgressBar, QIcon, QLabel, QListWidget, QListWidgetItem, QPalette, QPen, QPixmap, QProgressBar,
QSize, QSpinBox, QStyle, QStyledItemDelegate, Qt, QTextBrowser, QVBoxLayout, QSize, QSpinBox, QStyle, QStyledItemDelegate, Qt, QTextBrowser, QVBoxLayout,
pyqtSignal, QWidget, pyqtSignal,
) )
from threading import Thread from threading import Thread
@ -176,6 +176,35 @@ class ImageItemDelegate(QStyledItemDelegate):
painter.restore() 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): class CompressImages(Dialog):
def __init__(self, parent=None): def __init__(self, parent=None):
@ -203,38 +232,30 @@ class CompressImages(Dialog):
' without affecting image quality. Typically image size is reduced by 5 - 15%.')) ' without affecting image quality. Typically image size is reduced by 5 - 15%.'))
la.setWordWrap(True) la.setWordWrap(True)
la.setMinimumWidth(250) la.setMinimumWidth(250)
l.addWidget(la), l.addSpacing(30) l.addWidget(la)
self.jpeg = LossyCompression('jpeg', parent=self)
self.enable_lossy = el = QCheckBox(_('Enable &lossy compression of JPEG images')) l.addSpacing(30), l.addWidget(self.jpeg)
el.setToolTip(_('This allows you to change the quality factor used for JPEG images.\nBy lowering' self.webp = LossyCompression('webp', default_compression=75, parent=self)
' the quality you can greatly reduce file size, at the expense of the image looking blurred.')) l.addSpacing(30), l.addWidget(self.webp)
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.addStretch(10) l.addStretch(10)
l.addWidget(self.bb) l.addWidget(self.bb)
def save_compression_quality(self):
tprefs.set('jpeg_compression_quality_for_lossless_compression', self.jq.value())
@property @property
def names(self): def names(self):
return {item.text() for item in self.images.selectedItems()} return {item.text() for item in self.images.selectedItems()}
@property @property
def jpeg_quality(self): def jpeg_quality(self):
if not self.enable_lossy.isChecked(): if not self.jpeg.enable_lossy.isChecked():
return None 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): class CompressImagesProgress(Dialog):
@ -242,8 +263,9 @@ class CompressImagesProgress(Dialog):
gui_loop = pyqtSignal(object, object, object) gui_loop = pyqtSignal(object, object, object)
cidone = pyqtSignal() 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.names, self.jpeg_quality = names, jpeg_quality
self.webp_quality = webp_quality
self.keep_going = True self.keep_going = True
self.result = (None, '') self.result = (None, '')
Dialog.__init__(self, _('Compressing images...'), 'compress-images-progress', parent=parent) Dialog.__init__(self, _('Compressing images...'), 'compress-images-progress', parent=parent)
@ -259,7 +281,7 @@ class CompressImagesProgress(Dialog):
report = [] report = []
try: try:
self.result = (compress_images( 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 progress_callback=self.progress_callback
)[0], report) )[0], report)
except Exception: except Exception: