Start work on icon themes

This commit is contained in:
Kovid Goyal 2015-08-18 14:55:07 +05:30
parent d86179c938
commit fee64691ea

View File

@ -0,0 +1,276 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import os, errno, json, importlib, math
from io import BytesIO
from PyQt5.Qt import (
QImageReader, QFormLayout, QVBoxLayout, QSplitter, QGroupBox, QListWidget,
QLineEdit, QSpinBox, QTextEdit, QSize, QListWidgetItem, QIcon
)
from calibre import walk, fit_image
from calibre.customize.ui import interface_actions
from calibre.gui2 import must_use_qt, gprefs, choose_dir, error_dialog, choose_save_file
from calibre.gui2.widgets2 import Dialog
from calibre.utils.filenames import ascii_filename
from calibre.utils.magick import create_canvas, Image
from calibre.utils.zipfile import ZipFile, ZIP_STORED
from lzma.xz import compress
IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg'}
THEME_COVER = 'icon-theme-cover.jpg'
THEME_METADATA = 'metadata.json'
def read_images_from_folder(path):
name_map = {}
path = os.path.abspath(path)
for filepath in walk(path):
name = os.path.relpath(filepath, path).replace(os.sep, '/').lower()
ext = name.rpartition('.')[-1]
if ext in IMAGE_EXTENSIONS:
name_map[name] = filepath
return name_map
class Theme(object):
def __init__(self, title='', author='', version=-1, description='', cover=None):
self.title, self.author, self.version, self.description = title, author, version, description
self.cover = cover
class Report(object):
def __init__(self, path, name_map, extra, missing, theme):
self.path, self.name_map, self.extra, self.missing, self.theme = path, name_map, extra, missing, theme
self.bad = {}
def read_theme_from_folder(path):
path = os.path.abspath(path)
current_image_map = read_images_from_folder(P('images', allow_user_override=False))
name_map = read_images_from_folder(path)
name_map.pop(THEME_COVER, None)
current_names = frozenset(current_image_map)
names = frozenset(name_map)
extra = names - current_names
missing = current_names - names
try:
with open(os.path.join(path, THEME_METADATA), 'rb') as f:
metadata = json.load(f)
except EnvironmentError as e:
if e.errno != errno.ENOENT:
raise
metadata = {}
except ValueError:
# Corrupted metadata file
metadata = {}
def safe_int(x):
try:
return int(x)
except Exception:
return -1
theme = Theme(metadata.get('title', ''), metadata.get('author', ''), safe_int(metadata.get('version', -1)), metadata.get('description', ''))
ans = Report(path, name_map, extra, missing, theme)
try:
with open(os.path.join(path, THEME_COVER), 'rb') as f:
theme.cover = f.read()
except EnvironmentError as e:
if e.errno != errno.ENOENT:
raise
theme.cover = create_cover(ans)
return ans
def icon_for_action(name):
for plugin in interface_actions():
if plugin.name == name:
module, class_name = plugin.actual_plugin.partition(':')[::2]
mod = importlib.import_module(module)
cls = getattr(mod, class_name)
icon = cls.action_spec[1]
if icon:
return icon
def default_cover_icons(cols=5):
count = 0
for ac in gprefs.defaults['action-layout-toolbar']:
if ac:
icon = icon_for_action(ac)
if icon:
count += 1
yield icon
for x in 'user_profile plus minus series sync tags default_cover'.split():
yield x + '.png'
count += 1
extra = 'search donate cover_flow reader publisher back forward'.split()
while count < 15 or count % cols != 0:
yield extra[0] + '.png'
del extra[0]
count += 1
def create_cover(report, icons=(), cols=5, size=60, padding=8):
icons = icons or tuple(default_cover_icons(cols))
rows = int(math.ceil(len(icons) / cols))
canvas = create_canvas(cols * (size + padding), rows * (size + padding), '#eeeeee')
y = -size - padding // 2
x = 0
for i, icon in enumerate(icons):
if i % cols == 0:
y += padding + size
x = padding // 2
else:
x += size + padding
if report and icon in report.name_map:
ipath = os.path.join(report.path, report.name_map[icon])
else:
ipath = I(icon, allow_user_override=False)
img = Image()
with open(ipath, 'rb') as f:
img.load(f.read())
scaled, nwidth, nheight = fit_image(img.size[0], img.size[1], size, size)
img.size = nwidth, nheight
dx = (size - nwidth) // 2
canvas.compose(img, x + dx, y)
return canvas.export('JPEG')
def verify_theme(report):
must_use_qt()
report.bad = bad = {}
for name, path in report.name_map.iteritems():
reader = QImageReader(os.path.join(report.path, path))
img = reader.read()
if img.isNull():
bad[name] = reader.errorString()
return bool(bad)
class ThemeCreateDialog(Dialog):
def __init__(self, parent, report):
self.report = report
Dialog.__init__(self, _('Create an icon theme'), 'create-icon-theme', parent)
def setup_ui(self):
self.splitter = QSplitter(self)
self.l = l = QVBoxLayout(self)
l.addWidget(self.splitter)
l.addWidget(self.bb)
self.w = w = QGroupBox(_('Theme Metadata'), self)
self.splitter.addWidget(w)
l = w.l = QFormLayout(w)
self.missing_icons_group = mg = QGroupBox(self)
self.mising_icons = mi = QListWidget(mg)
mi.setSelectionMode(mi.NoSelection)
mg.l = QVBoxLayout(mg)
mg.l.addWidget(mi)
self.splitter.addWidget(mg)
self.title = QLineEdit(self)
l.addRow(_('&Title:'), self.title)
self.author = QLineEdit(self)
l.addRow(_('&Author:'), self.author)
self.version = v = QSpinBox(self)
v.setMinimum(1), v.setMaximum(1000000)
l.addRow(_('&Version:'), v)
self.description = QTextEdit(self)
l.addRow(self.description)
self.refresh_button = rb = self.bb.addButton(_('&Refresh'), self.bb.ActionRole)
rb.setIcon(QIcon(I('view-refresh.png')))
rb.clicked.connect(self.refresh)
self.apply_report()
def sizeHint(self):
return QSize(900, 600)
@property
def metadata(self):
return {
'title': self.title.text().strip(),
'author': self.author.text().strip(),
'version': self.version.value(),
'description': self.description.toPlainText().strip(),
}
def save_metadata(self):
with open(os.path.join(self.report.path, THEME_METADATA), 'wb') as f:
json.dump(self.metadata, f, indent=2)
def refresh(self):
self.save_metadata()
self.report = read_theme_from_folder(self.report.path)
self.apply_report()
def apply_report(self):
theme = self.report.theme
self.title.setText((theme.title or '').strip())
self.author.setText((theme.author or '').strip())
self.version.setValue(theme.version or 1)
self.description.setText((theme.description or '').strip())
if self.report.missing:
title = _('%d icons missing in this theme') % len(self.report.missing)
else:
title = _('No missing icons')
self.missing_icons_group.setTitle(title)
mi = self.mising_icons
mi.clear()
for name in sorted(self.report.missing):
QListWidgetItem(QIcon(I(name, allow_user_override=False)), name, mi)
def accept(self):
mi = self.metadata
if not mi.get('title'):
return error_dialog(self, _('No title specified'), _(
'You must specify a title for this icon theme'), show=True)
if not mi.get('author'):
return error_dialog(self, _('No author specified'), _(
'You must specify an author for this icon theme'), show=True)
return Dialog.accept(self)
def create_themeball(report):
buf = BytesIO()
with ZipFile(buf, 'w') as zf:
for name, path in report.name_map.iteritems():
if name not in report.extra:
with open(os.path.join(report.path, name), 'rb') as f:
zf.writestr(name, f.read(), compression=ZIP_STORED)
buf.seek(0)
out = BytesIO()
compress(buf, out, level=9)
buf = BytesIO()
prefix = ascii_filename(report.theme.title).replace(' ', '_').lower()
with ZipFile(buf, 'w') as zf:
with open(os.path.join(report.path, THEME_METADATA), 'rb') as f:
zf.writestr(prefix + '/' + THEME_METADATA, f.read())
zf.writestr(prefix + '/' + THEME_COVER, create_cover(report))
zf.writestr(prefix + '/' + 'icons.zip.xz', out.getvalue(), compression=ZIP_STORED)
return buf.getvalue()
def create_theme(folder=None, parent=None):
if folder is None:
folder = choose_dir(parent, 'create-icon-theme-folder', _(
'Choose a folder from which to read the icons'))
if not folder:
return
report = read_theme_from_folder(folder)
d = ThemeCreateDialog(parent, report)
if d.exec_() != d.Accepted:
return
d.save_metadata()
raw = create_themeball(d.report)
dest = choose_save_file(parent, 'create-icon-theme-dest', _(
'Choose destination for icon theme'),
[(_('ZIP files'), ['zip'])], initial_filename='my-calibre-icon-theme.zip')
if dest:
with open(dest, 'wb') as f:
f.write(raw)
if __name__ == '__main__':
from calibre.gui2 import Application
app = Application([])
create_theme('.')
del app