%PDF- %PDF-
Direktori : /lib/calibre/calibre/gui2/ |
Current File : //lib/calibre/calibre/gui2/covers.py |
#!/usr/bin/env python3 # License: GPLv3 Copyright: 2014, Kovid Goyal <kovid at kovidgoyal.net> import os from collections import OrderedDict from contextlib import suppress from copy import deepcopy from qt.core import ( QApplication, QCheckBox, QColor, QColorDialog, QDialog, QDialogButtonBox, QFormLayout, QFrame, QGridLayout, QHBoxLayout, QIcon, QInputDialog, QLabel, QLineEdit, QListWidget, QListWidgetItem, QMenu, QPixmap, QPushButton, QSize, QSizePolicy, QSpinBox, Qt, QTabWidget, QTimer, QToolButton, QVBoxLayout, QWidget, pyqtSignal ) from calibre.constants import config_dir from calibre.ebooks.covers import ( all_styles, cprefs, default_color_themes, generate_cover, override_prefs ) from calibre.gui2 import error_dialog, gprefs from calibre.gui2.font_family_chooser import FontFamilyChooser from calibre.utils.date import now from calibre.utils.filenames import make_long_path_useable from calibre.utils.icu import primary_sort_key, sort_key from polyglot.builtins import iteritems, itervalues class Preview(QLabel): def __init__(self, parent=None): QLabel.__init__(self, parent) self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) def sizeHint(self): return QSize(300, 400) class ColorButton(QToolButton): def __init__(self, color, parent=None): QToolButton.__init__(self, parent) self.setIconSize(QSize(50, 25)) self.pix = QPixmap(self.iconSize()) self._color = QColor('#' + color) self.pix.fill(self._color) self.setIcon(QIcon(self.pix)) self.clicked.connect(self.choose_color) @property def color(self): return self._color.name(QColor.NameFormat.HexRgb)[1:] @color.setter def color(self, val): self._color = QColor('#' + val) def update_display(self): self.pix.fill(self._color) self.setIcon(QIcon(self.pix)) def choose_color(self): c = QColorDialog.getColor(self._color, self, _('Choose color')) if c.isValid(): self._color = c self.update_display() class CreateColorScheme(QDialog): def __init__(self, scheme_name, scheme, existing_names, edit_scheme=False, parent=None): QDialog.__init__(self, parent) self.existing_names, self.is_editing, self.scheme_name = existing_names, edit_scheme, scheme_name self.l = l = QFormLayout(self) self.setLayout(l) self.setWindowTitle(scheme_name) self.name = n = QLineEdit(self) n.setText(scheme_name if edit_scheme else '#' +('My Color Scheme')) l.addRow(_('&Name:'), self.name) for x in 'color1 color2 contrast_color1 contrast_color2'.split(): setattr(self, x, ColorButton(scheme[x], self)) l.addRow(_('Color &1:'), self.color1) l.addRow(_('Color &2:'), self.color2) l.addRow(_('Contrast color &1 (mainly for text):'), self.contrast_color1) l.addRow(_('Contrast color &2 (mainly for text):'), self.contrast_color2) self.bb = bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) l.addRow(bb) @property def data(self): return self.name.text(), {x:getattr(self, x).color for x in 'color1 color2 contrast_color1 contrast_color2'.split()} def accept(self): name = self.name.text() if not name or len(name) < 2 or not name.startswith('#'): return error_dialog(self, _('Invalid name'), _( 'The color scheme name "%s" is invalid. It must start with a # and be at least two characters long.') % name, show=True) if name in self.existing_names: if not self.is_editing or name != self.scheme_name: return error_dialog(self, _('Invalid name'), _( 'A color scheme with the name "%s" already exists.') % name, show=True) QDialog.accept(self) class CoverSettingsWidget(QWidget): changed = pyqtSignal() def __init__(self, mi=None, prefs=None, parent=None, for_global_prefs=False): QWidget.__init__(self, parent) self.ignore_changed = False self.for_global_prefs = for_global_prefs self.l = l = QHBoxLayout(self) l.setContentsMargins(0, 0, 0, 0) self.setLayout(l) self.settings_tabs = st = QTabWidget(self) l.addWidget(st) self.preview_label = la = Preview(self) l.addWidget(la) if prefs is None: prefs = cprefs self.original_prefs = prefs self.mi = mi or self.default_mi() self.colors_page = cp = QWidget(st) st.addTab(cp, _('&Colors')) cp.l = l = QGridLayout() cp.setLayout(l) if for_global_prefs: msg = _('When generating covers, a color scheme for the cover is chosen at random from the' ' color schemes below. You can prevent an individual scheme from being selected by' ' unchecking it. The preview on the right shows the currently selected color scheme.') else: msg = _('Choose a color scheme to be used for this generated cover.') + '<p>' + _( 'In normal cover generation, the color scheme is chosen at random from the list of color schemes below. You' ' can prevent an individual color scheme from being chosen by unchecking it here.') cp.la = la = QLabel('<p>' + msg) la.setWordWrap(True) l.addWidget(la, 0, 0, 1, -1) self.colors_list = cl = QListWidget(cp) l.addWidget(cl, 1, 0, 1, -1) self.colors_map = OrderedDict() self.ncs = ncs = QPushButton(QIcon(I('plus.png')), _('&New color scheme'), cp) ncs.clicked.connect(self.create_color_scheme) l.addWidget(ncs) self.ecs = ecs = QPushButton(QIcon(I('format-fill-color.png')), _('&Edit color scheme'), cp) ecs.clicked.connect(self.edit_color_scheme) l.addWidget(ecs, l.rowCount()-1, 1) self.rcs = rcs = QPushButton(QIcon(I('minus.png')), _('&Remove color scheme'), cp) rcs.clicked.connect(self.remove_color_scheme) l.addWidget(rcs, l.rowCount()-1, 2) self.styles_page = sp = QWidget(st) st.addTab(sp, _('&Styles')) sp.l = l = QVBoxLayout() sp.setLayout(l) if for_global_prefs: msg = _('When generating covers, a style for the cover is chosen at random from the' ' styles below. You can prevent an individual style from being selected by' ' unchecking it. The preview on the right shows the currently selected style.') else: msg = _('Choose a style to be used for this generated cover.') + '<p>' + _( 'In normal cover generation, the style is chosen at random from the list of styles below. You' ' can prevent an individual style from being chosen by unchecking it here.') sp.la = la = QLabel('<p>' + msg) la.setWordWrap(True) l.addWidget(la) self.styles_list = sl = QListWidget(sp) l.addWidget(sl) self.style_map = OrderedDict() self.font_page = fp = QWidget(st) st.addTab(fp, _('&Fonts and sizes')) fp.l = l = QFormLayout() fp.setLayout(l) fp.f = [] def add_hline(): f = QFrame() fp.f.append(f) f.setFrameShape(QFrame.Shape.HLine) l.addRow(f) for x, label, size_label in ( ('title', _('&Title font family:'), _('&Title font size:')), ('subtitle', _('&Subtitle font family:'), _('&Subtitle font size:')), ('footer', _('&Footer font family:'), _('&Footer font size:')), ): attr = '%s_font_family' % x ff = FontFamilyChooser(fp) setattr(self, attr, ff) l.addRow(label, ff) ff.family_changed.connect(self.emit_changed) attr = '%s_font_size' % x fs = QSpinBox(fp) setattr(self, attr, fs) fs.setMinimum(8), fs.setMaximum(200), fs.setSuffix(' px') fs.setValue(prefs[attr]) fs.valueChanged.connect(self.emit_changed) l.addRow(size_label, fs) add_hline() self.changed_timer = t = QTimer(self) t.setSingleShot(True), t.setInterval(500), t.timeout.connect(self.emit_changed) def create_sz(label): ans = QSpinBox(self) ans.setSuffix(' px'), ans.setMinimum(100), ans.setMaximum(10000) l.addRow(label, ans) ans.valueChanged.connect(self.changed_timer.start) return ans self.cover_width = create_sz(_('Cover &width:')) self.cover_height = create_sz(_('Cover &height:')) fp.cla = la = QLabel(_( 'Note that the preview to the side is of fixed aspect ratio, so changing the cover' ' width above will not have any effect. If you change the height, you should also change the width nevertheless' ' as it will be used in actual cover generation.')) la.setWordWrap(True) l.addRow(la) self.templates_page = tp = QWidget(st) st.addTab(tp, _('&Text')) tp.l = l = QVBoxLayout() tp.setLayout(l) tp.la = la = QLabel(_( 'The text on the generated cover is taken from the metadata of the book.' ' This is controlled via templates. You can use the <b>, <i> and <br> tags' ' in the templates for bold, italic and line breaks, respectively. The' ' default templates use the title, series and authors. You can change them to use' ' whatever metadata you like.')) la.setWordWrap(True), la.setTextFormat(Qt.TextFormat.PlainText) l.addWidget(la) def create_template_widget(title, which, button): attr = which + '_template' heading = QLabel('<h2>' + title) setattr(tp, attr + '_heading', heading) l.addWidget(heading) la = QLabel() setattr(self, attr, la) l.addWidget(la), la.setTextFormat(Qt.TextFormat.PlainText), la.setStyleSheet('QLabel {font-family: monospace}') la.setWordWrap(True) b = QPushButton(button) b.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) connect_lambda(b.clicked, self, lambda self: self.change_template(which)) setattr(self, attr + '_button', b) l.addWidget(b) if which != 'footer': f = QFrame(tp) setattr(tp, attr + '_sep', f), f.setFrameShape(QFrame.Shape.HLine) l.addWidget(f) l.addSpacing(10) create_template_widget(_('The title template'), 'title', _('Change the &title template')) create_template_widget(_('The sub-title template'), 'subtitle', _('Change the &sub-title template')) create_template_widget(_('The footer template'), 'footer', _('Change the &footer template')) l.addStretch(2) self.apply_prefs(prefs) self.changed.connect(self.update_preview) self.styles_list.itemSelectionChanged.connect(self.update_preview) self.colors_list.itemSelectionChanged.connect(self.update_preview) self.update_preview() def __enter__(self): self.ignore_changed = True def __exit__(self, *args): self.ignore_changed = False def emit_changed(self): if not self.ignore_changed: self.changed.emit() def apply_prefs(self, prefs): with self: self._apply_prefs(prefs) def _apply_prefs(self, prefs): for x in ('title', 'subtitle', 'footer'): attr = '%s_font_family' % x getattr(self, attr).font_family = prefs[attr] attr = '%s_font_size' % x getattr(self, attr).setValue(prefs[attr]) for x in ('title', 'subtitle', 'footer'): x += '_template' getattr(self, x).setText(prefs[x]) for x in ('width', 'height'): x = 'cover_' + x getattr(self, x).setValue(prefs[x]) color_themes = prefs['color_themes'].copy() color_themes.update(default_color_themes) disabled = set(prefs['disabled_color_themes']) self.colors_list.clear() self.colors_map = {} for name in sorted(color_themes, key=sort_key): self.colors_map[name] = li = QListWidgetItem(name, self.colors_list) li.setFlags(li.flags() | Qt.ItemFlag.ItemIsUserCheckable) li.setCheckState(Qt.CheckState.Unchecked if name in disabled else Qt.CheckState.Checked) li.setData(Qt.ItemDataRole.UserRole, color_themes[name]) lu = prefs.get('last_used_colors') if not self.for_global_prefs and lu in self.colors_map and self.colors_map[lu].checkState() == Qt.CheckState.Checked: self.colors_map[lu].setSelected(True) else: for name, li in iteritems(self.colors_map): if li.checkState() == Qt.CheckState.Checked: li.setSelected(True) break else: next(itervalues(self.colors_map)).setSelected(True) disabled = set(prefs['disabled_styles']) self.styles_list.clear() self.style_map.clear() for name in sorted(all_styles(), key=sort_key): self.style_map[name] = li = QListWidgetItem(name, self.styles_list) li.setFlags(li.flags() | Qt.ItemFlag.ItemIsUserCheckable) li.setCheckState(Qt.CheckState.Unchecked if name in disabled else Qt.CheckState.Checked) lu = prefs.get('last_used_style') if not self.for_global_prefs and lu in self.style_map and self.style_map[lu].checkState() == Qt.CheckState.Checked: self.style_map[lu].setSelected(True) else: for name, li in iteritems(self.style_map): if li.checkState() == Qt.CheckState.Checked: li.setSelected(True) break else: next(itervalues(self.style_map)).setSelected(True) @property def current_colors(self): for name, li in iteritems(self.colors_map): if li.isSelected(): return name @property def disabled_colors(self): for name, li in iteritems(self.colors_map): if li.checkState() == Qt.CheckState.Unchecked: yield name @property def custom_colors(self): ans = {} for name, li in iteritems(self.colors_map): if name.startswith('#'): ans[name] = li.data(Qt.ItemDataRole.UserRole) return ans @property def current_style(self): for name, li in iteritems(self.style_map): if li.isSelected(): return name @property def disabled_styles(self): for name, li in iteritems(self.style_map): if li.checkState() == Qt.CheckState.Unchecked: yield name @property def current_prefs(self): prefs = {k:self.original_prefs[k] for k in self.original_prefs.defaults} for x in ('title', 'subtitle', 'footer'): attr = '%s_font_family' % x prefs[attr] = getattr(self, attr).font_family attr = '%s_font_size' % x prefs[attr] = getattr(self, attr).value() prefs['color_themes'] = self.custom_colors prefs['disabled_styles'] = list(self.disabled_styles) prefs['disabled_color_themes'] = list(self.disabled_colors) for x in ('title', 'subtitle', 'footer'): x += '_template' prefs[x] = getattr(self, x).text() for x in ('width', 'height'): x = 'cover_' + x prefs[x] = getattr(self, x).value() return prefs def insert_scheme(self, name, li): with self: self.colors_list.insertItem(0, li) cm = OrderedDict() cm[name] = li for k, v in iteritems(self.colors_map): cm[k] = v self.colors_map = cm li.setSelected(True) for i in range(1, self.colors_list.count()): self.colors_list.item(i).setSelected(False) def create_color_scheme(self): scheme = self.colors_map[self.current_colors].data(Qt.ItemDataRole.UserRole) d = CreateColorScheme('#' + _('My Color Scheme'), scheme, set(self.colors_map), parent=self) if d.exec() == QDialog.DialogCode.Accepted: name, scheme = d.data li = QListWidgetItem(name) li.setData(Qt.ItemDataRole.UserRole, scheme), li.setFlags(li.flags() | Qt.ItemFlag.ItemIsUserCheckable), li.setCheckState(Qt.CheckState.Checked) self.insert_scheme(name, li) self.emit_changed() self.original_prefs['color_themes'] = self.current_prefs['color_themes'] def edit_color_scheme(self): cs = self.current_colors if cs is None or not cs.startswith('#'): return error_dialog(self, _('Cannot edit'), _( 'Cannot edit a builtin color scheme. Create a new' ' color scheme instead.'), show=True) li = self.colors_map[cs] d = CreateColorScheme(cs, li.data(Qt.ItemDataRole.UserRole), set(self.colors_map), edit_scheme=True, parent=self) if d.exec() == QDialog.DialogCode.Accepted: name, scheme = d.data li.setText(name) li.setData(Qt.ItemDataRole.UserRole, scheme) if name != cs: self.colors_map.pop(cs, None) self.insert_scheme(name, li) self.emit_changed() self.original_prefs['color_themes'] = self.current_prefs['color_themes'] def remove_color_scheme(self): cs = self.current_colors if cs is None or not cs.startswith('#'): return error_dialog(self, _('Cannot remove'), _( 'Cannot remove a builtin color scheme.'), show=True) for i in range(self.colors_list.count()): item = self.colors_list.item(i) if item.isSelected(): with self: del self.colors_map[item.text()] self.colors_list.takeItem(i) i = i % self.colors_list.count() self.colors_list.item(i).setSelected(True) self.emit_changed() self.original_prefs['color_themes'] = self.current_prefs['color_themes'] return def change_template(self, which): from calibre.gui2.dialogs.template_dialog import TemplateDialog from calibre.gui2.ui import get_gui gui = get_gui() if gui is None: from calibre.ebooks.metadata.book.base import field_metadata else: field_metadata = gui.current_db.new_api.field_metadata attr = which + '_template' templ = getattr(self, attr).text() d = TemplateDialog(self, templ, mi=self.mi, fm=field_metadata) if d.exec() == QDialog.DialogCode.Accepted: templ = d.rule[1] getattr(self, attr).setText(templ) self.emit_changed() @property def prefs_for_rendering(self): prefs = self.current_prefs prefs = override_prefs(prefs, override_style=self.current_style, override_color_theme=self.current_colors) return prefs def update_preview(self): if self.ignore_changed: return dpr = getattr(self, 'devicePixelRatioF', self.devicePixelRatio)() w, h = int(dpr * self.preview_label.sizeHint().width()), int(dpr * self.preview_label.sizeHint().height()) prefs = self.prefs_for_rendering hr = h / prefs['cover_height'] for x in ('title', 'subtitle', 'footer'): attr = '%s_font_size' % x prefs[attr] = int(prefs[attr] * hr) prefs['cover_width'], prefs['cover_height'] = w, h img = generate_cover(self.mi, prefs=prefs, as_qimage=True) img.setDevicePixelRatio(dpr) self.preview_label.setPixmap(QPixmap.fromImage(img)) def default_mi(self): from calibre.ebooks.metadata.book.base import Metadata mi = Metadata(_('A sample book'), [_('Author One'), _('Author Two')]) mi.series = _('A series of samples') mi.series_index = 4 mi.tags = [_('Tag One'), _('Tag Two')] mi.publisher = _('Some publisher') mi.rating = 4 mi.identifiers = {'isbn':'123456789', 'url': 'https://calibre-ebook.com'} mi.languages = ['eng', 'fra'] mi.pubdate = mi.timestamp = now() return mi def restore_defaults(self): defaults = self.original_prefs.defaults.copy() # Dont delete custom color themes when restoring defaults defaults['color_themes'] = self.custom_colors self.apply_prefs(defaults) self.update_preview() def save_state(self): self.original_prefs.set('last_used_colors', self.current_colors) self.original_prefs.set('last_used_style', self.current_style) def save_as_prefs(self): with self.original_prefs: for k, v in iteritems(self.current_prefs): self.original_prefs[k] = v @property def serialized_prefs(self) -> bytes: from calibre.utils.serialize import json_dumps c = dict(deepcopy(self.original_prefs)) c.update(self.current_prefs) return json_dumps(c, indent=2) @serialized_prefs.setter def serialized_prefs(self, val: bytes) -> None: from calibre.utils.serialize import json_loads prefs = json_loads(val) self.apply_prefs(prefs) self.update_preview() class CoverSettingsDialog(QDialog): def __init__(self, mi=None, prefs=None, parent=None): QDialog.__init__(self, parent) self.setWindowTitle(_('Cover generation settings')) self.l = l = QVBoxLayout(self) self.setLayout(l) self.settings = CoverSettingsWidget(mi=mi, prefs=prefs, parent=self) l.addWidget(self.settings) self.save_settings = ss = QCheckBox(_('Save these settings as the &defaults for future use')) ss.setChecked(gprefs.get('cover_generation_save_settings_for_future', True)) l.addWidget(ss) self.bb = bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) l.addWidget(bb) bb.accepted.connect(self.accept), bb.rejected.connect(self.reject) bb.b = b = bb.addButton(_('Restore &defaults'), QDialogButtonBox.ButtonRole.ActionRole) b.clicked.connect(self.restore_defaults) bb.ld = b = bb.addButton(_('&Save'), QDialogButtonBox.ButtonRole.ActionRole) b.clicked.connect(self.export_settings) b.setToolTip(_('Save the current cover generation settings for later re-use')) bb.sd = b = bb.addButton(_('&Load'), QDialogButtonBox.ButtonRole.ActionRole) self.load_menu = QMenu(b) self.load_menu.aboutToShow.connect(self.populate_load_menu) b.setMenu(self.load_menu) b.setToolTip(_('Load previously saved cover generation settings')) ss.setToolTip('<p>' + _( 'Save the current settings as the settings to use always instead of just this time. Remember that' ' for styles and colors the actual style or color used is chosen at random from' ' the list of checked styles/colors.')) self.resize(self.sizeHint()) geom = gprefs.get('cover_settings_dialog_geom', None) if geom is not None: QApplication.instance().safe_restore_geometry(self, geom) self.prefs_for_rendering = None def restore_defaults(self): self.settings.restore_defaults() self.settings.save_as_prefs() def export_settings(self): name, ok = QInputDialog.getText(self, _('Name for these settings'), _('Theme name:'), text=_('My cover style')) if ok: base = os.path.join(config_dir, 'cover-generation-themes') os.makedirs(base, exist_ok=True) path = make_long_path_useable(os.path.join(base, name + '.json')) raw = self.settings.serialized_prefs with open(path, 'wb') as f: f.write(raw) def populate_load_menu(self): m = self.load_menu m.clear() base = os.path.join(config_dir, 'cover-generation-themes') entries = () with suppress(FileNotFoundError): entries = sorted((x.rpartition('.')[0] for x in os.listdir(base) if x.endswith('.json')), key=primary_sort_key) for name in entries: m.addAction(name, self.import_settings) def import_settings(self): fname = self.sender().text() + '.json' base = os.path.join(config_dir, 'cover-generation-themes') with open(os.path.join(base, fname), 'rb') as f: raw = f.read() self.settings.serialized_prefs = raw def _save_settings(self): gprefs.set('cover_generation_save_settings_for_future', self.save_settings.isChecked()) gprefs.set('cover_settings_dialog_geom', bytearray(self.saveGeometry())) self.settings.save_state() def accept(self): self._save_settings() self.prefs_for_rendering = self.settings.prefs_for_rendering if self.save_settings.isChecked(): self.settings.save_as_prefs() QDialog.accept(self) def reject(self): self._save_settings() QDialog.reject(self) if __name__ == '__main__': from calibre.gui2 import Application app = Application([]) d = CoverSettingsDialog() d.show() app.exec() del d del app