Losslessly optimize images automatically when creating a custom icon theme

This commit is contained in:
Kovid Goyal 2016-06-07 10:21:34 +05:30
parent 150d6762f7
commit 2f9f3503e3

View File

@ -6,21 +6,23 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import os, errno, json, importlib, math, httplib, bz2, shutil import os, errno, json, importlib, math, httplib, bz2, shutil, sys
from itertools import count
from io import BytesIO from io import BytesIO
from future_builtins import map from future_builtins import map
from Queue import Queue, Empty from Queue import Queue, Empty
from threading import Thread from threading import Thread, Event
from multiprocessing.pool import ThreadPool
from PyQt5.Qt import ( from PyQt5.Qt import (
QImageReader, QFormLayout, QVBoxLayout, QSplitter, QGroupBox, QListWidget, QImageReader, QFormLayout, QVBoxLayout, QSplitter, QGroupBox, QListWidget,
QLineEdit, QSpinBox, QTextEdit, QSize, QListWidgetItem, QIcon, QImage, QLineEdit, QSpinBox, QTextEdit, QSize, QListWidgetItem, QIcon, QImage,
pyqtSignal, QStackedLayout, QWidget, QLabel, Qt, QComboBox, QPixmap, pyqtSignal, QStackedLayout, QWidget, QLabel, Qt, QComboBox, QPixmap,
QGridLayout, QStyledItemDelegate, QModelIndex, QApplication, QStaticText, QGridLayout, QStyledItemDelegate, QModelIndex, QApplication, QStaticText,
QStyle, QPen QStyle, QPen, QProgressDialog
) )
from calibre import walk, fit_image, human_readable from calibre import walk, fit_image, human_readable, detect_ncpus as cpu_count
from calibre.constants import cache_dir, config_dir from calibre.constants import cache_dir, config_dir
from calibre.customize.ui import interface_actions from calibre.customize.ui import interface_actions
from calibre.gui2 import must_use_qt, gprefs, choose_dir, error_dialog, choose_save_file, question_dialog from calibre.gui2 import must_use_qt, gprefs, choose_dir, error_dialog, choose_save_file, question_dialog
@ -31,7 +33,7 @@ from calibre.utils.date import utcnow
from calibre.utils.filenames import ascii_filename from calibre.utils.filenames import ascii_filename
from calibre.utils.https import get_https_resource_securely, HTTPError from calibre.utils.https import get_https_resource_securely, HTTPError
from calibre.utils.icu import numeric_sort_key as sort_key from calibre.utils.icu import numeric_sort_key as sort_key
from calibre.utils.img import image_from_data, Canvas from calibre.utils.img import image_from_data, Canvas, optimize_png, optimize_jpeg
from calibre.utils.zipfile import ZipFile, ZIP_STORED from calibre.utils.zipfile import ZipFile, ZIP_STORED
from calibre.utils.filenames import atomic_rename from calibre.utils.filenames import atomic_rename
from lzma.xz import compress, decompress from lzma.xz import compress, decompress
@ -282,22 +284,104 @@ class ThemeCreateDialog(Dialog):
'You must specify an author for this icon theme'), show=True) 'You must specify an author for this icon theme'), show=True)
return Dialog.accept(self) return Dialog.accept(self)
def create_themeball(report): class Compress(QProgressDialog):
update_signal = pyqtSignal(object, object)
def __init__(self, report, parent=None):
total = 2 + len(report.name_map)
QProgressDialog.__init__(self, _('Losslessly optimizing images, please wait...'), _('&Abort'), 0, total, parent)
self.setWindowTitle(self.labelText())
self.setWindowIcon(QIcon(I('lt.png')))
self.setMinimumDuration(0)
self.update_signal.connect(self.do_update, type=Qt.QueuedConnection)
self.raw = self.prefix = None
self.abort = Event()
self.canceled.connect(self.abort.set)
self.t = Thread(name='CompressIcons', target=self.run_compress, args=(report,))
self.t.daemon = False
self.t.start()
def do_update(self, num, message):
if num < 0:
return self.onerror(_('Optimizing images failed, click "Show details" for more information'), message)
self.setValue(num)
self.setLabelText(message)
def onerror(self, msg, details):
error_dialog(self, _('Compression failed'), msg, det_msg=details, show=True)
self.close()
def onprogress(self, num, msg):
self.update_signal.emit(num, msg)
return not self.wasCanceled()
def run_compress(self, report):
try:
self.raw, self.prefix = create_themeball(report, self.onprogress, self.abort)
except Exception:
import traceback
self.update_signal.emit(-1, traceback.format_exc())
else:
self.update_signal.emit(self.maximum(), '')
def create_themeball(report, progress=None, abort=None):
pool = ThreadPool(processes=cpu_count())
buf = BytesIO() buf = BytesIO()
num = count()
error_occurred = Event()
def optimize(name):
if abort is not None and abort.is_set():
return
if error_occurred.is_set():
return
try:
i = next(num)
if progress is not None:
progress(i, _('Optimizing %s') % name)
srcpath = os.path.join(report.path, name)
ext = srcpath.rpartition('.')[-1].lower()
if ext == 'png':
optimize_png(srcpath)
elif ext in ('jpg', 'jpeg'):
optimize_jpeg(srcpath)
except Exception:
return sys.exc_info()
errors = tuple(filter(None, pool.map(optimize, tuple(report.name_map.iterkeys()))))
pool.close(), pool.join()
if abort is not None and abort.is_set():
return
if errors:
e = errors[0]
raise e[0], e[1], e[2]
if progress is not None:
progress(next(num), _('Creating theme file'))
with ZipFile(buf, 'w') as zf: with ZipFile(buf, 'w') as zf:
for name, path in report.name_map.iteritems(): for name in report.name_map:
with open(os.path.join(report.path, name), 'rb') as f: srcpath = os.path.join(report.path, name)
with lopen(srcpath, 'rb') as f:
zf.writestr(name, f.read(), compression=ZIP_STORED) zf.writestr(name, f.read(), compression=ZIP_STORED)
buf.seek(0) buf.seek(0)
out = BytesIO() out = BytesIO()
if abort is not None and abort.is_set():
return None, None
if progress is not None:
progress(next(num), _('Compressing theme file'))
compress(buf, out, level=9) compress(buf, out, level=9)
buf = BytesIO() buf = BytesIO()
prefix = report.name prefix = report.name
if abort is not None and abort.is_set():
return None, None
with ZipFile(buf, 'w') as zf: with ZipFile(buf, 'w') as zf:
with open(os.path.join(report.path, THEME_METADATA), 'rb') as f: with lopen(os.path.join(report.path, THEME_METADATA), 'rb') as f:
zf.writestr(prefix + '/' + THEME_METADATA, f.read()) zf.writestr(prefix + '/' + THEME_METADATA, f.read())
zf.writestr(prefix + '/' + THEME_COVER, create_cover(report)) zf.writestr(prefix + '/' + THEME_COVER, create_cover(report))
zf.writestr(prefix + '/' + 'icons.zip.xz', out.getvalue(), compression=ZIP_STORED) zf.writestr(prefix + '/' + 'icons.zip.xz', out.getvalue(), compression=ZIP_STORED)
if progress is not None:
progress(next(num), _('Finished'))
return buf.getvalue(), prefix return buf.getvalue(), prefix
@ -312,12 +396,16 @@ def create_theme(folder=None, parent=None):
if d.exec_() != d.Accepted: if d.exec_() != d.Accepted:
return return
d.save_metadata() d.save_metadata()
raw, prefix = create_themeball(d.report) d = Compress(d.report, parent=parent)
d.exec_()
if d.wasCanceled() or d.raw is None:
return
raw, prefix = d.raw, d.prefix
dest = choose_save_file(parent, 'create-icon-theme-dest', _( dest = choose_save_file(parent, 'create-icon-theme-dest', _(
'Choose destination for icon theme'), 'Choose destination for icon theme'),
[(_('ZIP files'), ['zip'])], initial_filename=prefix + '.zip') [(_('ZIP files'), ['zip'])], initial_filename=prefix + '.zip')
if dest: if dest:
with open(dest, 'wb') as f: with lopen(dest, 'wb') as f:
f.write(raw) f.write(raw)
# }}} # }}}