mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-07 10:14:46 -04:00
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:
parent
c6c8fbeb64
commit
d3dda1af39
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user