mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Edit Book: Add a new tool to manage fonts. Allows easily changing/removing/embedding fonts in the entire book. To use it go to Tools->Manage Fonts
This commit is contained in:
parent
621d67273f
commit
99e6287ec1
@ -13,6 +13,7 @@ from calibre.constants import plugins
|
||||
from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES
|
||||
from calibre.ebooks.oeb.polish.check.base import BaseError, WARN
|
||||
from calibre.ebooks.oeb.polish.container import OEB_FONTS
|
||||
from calibre.ebooks.oeb.polish.fonts import change_font_family_value
|
||||
from calibre.ebooks.oeb.polish.utils import guess_type
|
||||
from calibre.ebooks.oeb.polish.pretty import pretty_script_or_style
|
||||
from calibre.utils.fonts.utils import get_all_font_names
|
||||
@ -31,12 +32,7 @@ def fix_declaration(style, css_name, font_name):
|
||||
for i in xrange(ff.length):
|
||||
val = ff.item(i)
|
||||
if hasattr(val.value, 'lower') and val.value.lower() == css_name.lower():
|
||||
val.value = font_name
|
||||
# If val.type == 'IDENT' cssutils will not serialize the font
|
||||
# name properly (it will not enclose it in quotes). There we
|
||||
# use the following hack (setting an internal property of the
|
||||
# Value class)
|
||||
val._type = 'STRING'
|
||||
change_font_family_value(val, font_name)
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
|
@ -6,6 +6,8 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import re
|
||||
|
||||
from calibre.ebooks.oeb.polish.container import OEB_STYLES, OEB_DOCS
|
||||
from calibre.ebooks.oeb.normalize_css import normalize_font
|
||||
|
||||
@ -18,12 +20,12 @@ def font_family_data_from_declaration(style, families):
|
||||
font_families = []
|
||||
f = style.getProperty('font')
|
||||
if f is not None:
|
||||
f = normalize_font(f.cssValue, font_family_as_list=True).get('font-family', None)
|
||||
f = normalize_font(f.propertyValue, font_family_as_list=True).get('font-family', None)
|
||||
if f is not None:
|
||||
font_families = f
|
||||
f = style.getProperty('font-family')
|
||||
if f is not None:
|
||||
font_families = [x.cssText for x in f.cssValue]
|
||||
font_families = [x.cssText for x in f.propertyValue]
|
||||
|
||||
for f in font_families:
|
||||
f = unquote(f)
|
||||
@ -36,7 +38,7 @@ def font_family_data_from_sheet(sheet, families):
|
||||
elif rule.type == rule.FONT_FACE_RULE:
|
||||
ff = rule.style.getProperty('font-family')
|
||||
if ff is not None:
|
||||
for f in ff.cssValue:
|
||||
for f in ff.propertyValue:
|
||||
families[unquote(f.cssText)] = True
|
||||
|
||||
def font_family_data(container):
|
||||
@ -56,3 +58,101 @@ def font_family_data(container):
|
||||
style = container.parse_css(style, is_declaration=True)
|
||||
font_family_data_from_declaration(style, families)
|
||||
return families
|
||||
|
||||
def change_font_family_value(cssvalue, new_name):
|
||||
# If cssvalue.type == 'IDENT' cssutils will not serialize the font
|
||||
# name properly (it will not enclose it in quotes). So we
|
||||
# use the following hack (setting an internal property of the
|
||||
# Value class)
|
||||
cssvalue.value = new_name
|
||||
cssvalue._type = 'STRING'
|
||||
|
||||
def change_font_family_in_property(style, prop, old_name, new_name=None):
|
||||
changed = False
|
||||
families = {unquote(x.cssText) for x in prop.propertyValue}
|
||||
_dummy_family = 'd7d81cf1-1c8c-4993-b788-e1ab596c0f1f'
|
||||
if new_name and new_name in families:
|
||||
new_name = None # new name already exists in this property, so simply remove old_name
|
||||
for val in prop.propertyValue:
|
||||
if unquote(val.cssText) == old_name:
|
||||
change_font_family_value(val, new_name or _dummy_family)
|
||||
changed = True
|
||||
if changed and not new_name:
|
||||
# Remove dummy family, cssutils provides no clean way to do this, so we
|
||||
# roundtrip via cssText
|
||||
pat = re.compile(r'''['"]{0,1}%s['"]{0,1}\s*,{0,1}''' % _dummy_family)
|
||||
repl = pat.sub('', prop.propertyValue.cssText).strip().rstrip(',').strip()
|
||||
if repl:
|
||||
prop.propertyValue.cssText = repl
|
||||
if prop.name == 'font' and not prop.validate():
|
||||
style.removeProperty(prop.name) # no families left in font:
|
||||
else:
|
||||
style.removeProperty(prop.name)
|
||||
return changed
|
||||
|
||||
def change_font_in_declaration(style, old_name, new_name=None):
|
||||
changed = False
|
||||
for x in ('font', 'font-family'):
|
||||
prop = style.getProperty(x)
|
||||
if prop is not None:
|
||||
changed |= change_font_family_in_property(style, prop, old_name, new_name)
|
||||
return changed
|
||||
|
||||
def remove_embedded_font(container, sheet, rule, sheet_name):
|
||||
src = getattr(rule.style.getProperty('src'), 'value')
|
||||
if src is not None:
|
||||
if src.startswith('url('):
|
||||
src = src[4:-1]
|
||||
sheet.cssRules.remove(rule)
|
||||
if src:
|
||||
src = unquote(src)
|
||||
name = container.href_to_name(src, sheet_name)
|
||||
if container.has_name(name):
|
||||
container.remove_item(name)
|
||||
|
||||
def change_font_in_sheet(container, sheet, old_name, new_name, sheet_name):
|
||||
changed = False
|
||||
removals = []
|
||||
for rule in sheet.cssRules:
|
||||
if rule.type == rule.STYLE_RULE:
|
||||
changed |= change_font_in_declaration(rule.style, old_name, new_name)
|
||||
elif rule.type == rule.FONT_FACE_RULE:
|
||||
ff = rule.style.getProperty('font-family')
|
||||
if ff is not None:
|
||||
families = {unquote(x.cssText) for x in ff.propertyValue}
|
||||
if old_name in families:
|
||||
changed = True
|
||||
removals.append(rule)
|
||||
for rule in reversed(removals):
|
||||
remove_embedded_font(container, sheet, rule, sheet_name)
|
||||
return changed
|
||||
|
||||
def change_font(container, old_name, new_name=None):
|
||||
changed = False
|
||||
for name, mt in tuple(container.mime_map.iteritems()):
|
||||
if mt in OEB_STYLES:
|
||||
sheet = container.parsed(name)
|
||||
if change_font_in_sheet(container, sheet, old_name, new_name, name):
|
||||
container.dirty(name)
|
||||
changed = True
|
||||
elif mt in OEB_DOCS:
|
||||
root = container.parsed(name)
|
||||
for style in root.xpath('//*[local-name() = "style"]'):
|
||||
if style.text and style.get('type', 'text/css').lower() == 'text/css':
|
||||
sheet = container.parse_css(style.text)
|
||||
if change_font_in_sheet(container, sheet, old_name, new_name, name):
|
||||
container.dirty(name)
|
||||
changed = True
|
||||
for elem in root.xpath('//*[@style]'):
|
||||
style = elem.get('style', '')
|
||||
if style:
|
||||
style = container.parse_css(style, is_declaration=True)
|
||||
if change_font_in_declaration(style, old_name, new_name):
|
||||
style = style.cssText.strip().rstrip(';').strip()
|
||||
if style:
|
||||
elem.set('style', style)
|
||||
else:
|
||||
del elem.attrib['style']
|
||||
container.dirty(name)
|
||||
changed = True
|
||||
return changed
|
||||
|
@ -119,6 +119,9 @@ class Boss(QObject):
|
||||
self.gui.spell_check.word_replaced.connect(self.word_replaced)
|
||||
self.gui.spell_check.word_ignored.connect(self.word_ignored)
|
||||
self.gui.live_css.goto_declaration.connect(self.goto_style_declaration)
|
||||
self.gui.manage_fonts.container_changed.connect(self.apply_container_update_to_gui)
|
||||
self.gui.manage_fonts.embed_all_fonts.connect(self.manage_fonts_embed)
|
||||
self.gui.manage_fonts.subset_all_fonts.connect(self.manage_fonts_subset)
|
||||
|
||||
def preferences(self):
|
||||
p = Preferences(self.gui)
|
||||
@ -421,7 +424,7 @@ class Boss(QObject):
|
||||
self.apply_container_update_to_gui()
|
||||
self.edit_file(name, 'html')
|
||||
|
||||
def polish(self, action, name):
|
||||
def polish(self, action, name, parent=None):
|
||||
with BusyCursor():
|
||||
self.add_savepoint(_('Before: %s') % name)
|
||||
try:
|
||||
@ -435,7 +438,7 @@ class Boss(QObject):
|
||||
report = markdown('# %s\n\n'%self.current_metadata.title + '\n\n'.join(report), output_format='html4')
|
||||
if not changed:
|
||||
self.rewind_savepoint()
|
||||
d = QDialog(self.gui)
|
||||
d = QDialog(parent or self.gui)
|
||||
d.l = QVBoxLayout()
|
||||
d.setLayout(d.l)
|
||||
d.e = QTextBrowser(d)
|
||||
@ -453,6 +456,17 @@ class Boss(QObject):
|
||||
d.resize(600, 400)
|
||||
d.exec_()
|
||||
|
||||
def manage_fonts(self):
|
||||
self.commit_all_editors_to_container()
|
||||
self.gui.manage_fonts.display()
|
||||
|
||||
def manage_fonts_embed(self):
|
||||
self.polish('embed', _('Embed all fonts'), parent=self.gui.manage_fonts)
|
||||
self.gui.manage_fonts.refresh()
|
||||
|
||||
def manage_fonts_subset(self):
|
||||
self.polish('subset', _('Subset all fonts'), parent=self.gui.manage_fonts)
|
||||
|
||||
# Renaming {{{
|
||||
|
||||
def rationalize_folders(self):
|
||||
|
@ -10,13 +10,16 @@ import sys
|
||||
|
||||
from PyQt4.Qt import (
|
||||
QSplitter, QVBoxLayout, QTableView, QWidget, QLabel, QAbstractTableModel,
|
||||
Qt, QApplication, QTimer, QPushButton)
|
||||
Qt, QApplication, QTimer, QPushButton, pyqtSignal, QFormLayout, QLineEdit,
|
||||
QIcon)
|
||||
|
||||
from calibre.ebooks.oeb.polish.container import get_container
|
||||
from calibre.ebooks.oeb.polish.fonts import font_family_data
|
||||
from calibre.ebooks.oeb.polish.fonts import font_family_data, change_font
|
||||
from calibre.gui2 import error_dialog
|
||||
from calibre.gui2.tweak_book import current_container, set_current_container
|
||||
from calibre.gui2.tweak_book.widgets import Dialog, BusyCursor
|
||||
from calibre.utils.icu import primary_sort_key as sort_key
|
||||
from calibre.utils.fonts.scanner import font_scanner
|
||||
|
||||
class AllFonts(QAbstractTableModel):
|
||||
|
||||
@ -59,6 +62,10 @@ class AllFonts(QAbstractTableModel):
|
||||
except (IndexError, KeyError):
|
||||
return
|
||||
return name if col == 1 else embedded
|
||||
if role == Qt.TextAlignmentRole:
|
||||
col = index.column()
|
||||
if col == 0:
|
||||
return Qt.AlignHCenter
|
||||
|
||||
def sort(self, col, order=Qt.AscendingOrder):
|
||||
sorted_on = (('name' if col == 1 else 'embedded'), order == Qt.AscendingOrder)
|
||||
@ -68,12 +75,67 @@ class AllFonts(QAbstractTableModel):
|
||||
self.do_sort()
|
||||
self.endResetModel()
|
||||
|
||||
def data_for_indices(self, indices):
|
||||
ans = {}
|
||||
for idx in indices:
|
||||
try:
|
||||
name = self.items[idx.row()]
|
||||
ans[name] = self.font_data[name]
|
||||
except (IndexError, KeyError):
|
||||
pass
|
||||
return ans
|
||||
|
||||
class ChangeFontFamily(Dialog):
|
||||
|
||||
def __init__(self, old_family, embedded_families, parent=None):
|
||||
self.old_family = old_family
|
||||
self.local_families = {icu_lower(f) for f in font_scanner.find_font_families()} | {
|
||||
icu_lower(f) for f in embedded_families}
|
||||
Dialog.__init__(self, _('Change font'), 'change-font-family', parent=parent)
|
||||
self.setMinimumWidth(300)
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
def setup_ui(self):
|
||||
self.l = l = QFormLayout(self)
|
||||
self.setLayout(l)
|
||||
self.la = la = QLabel(ngettext(
|
||||
'Change the font %s to:', 'Change the fonts %s to:',
|
||||
self.old_family.count(',')+1) % self.old_family)
|
||||
la.setWordWrap(True)
|
||||
l.addRow(la)
|
||||
self._family = f = QLineEdit(self)
|
||||
l.addRow(_('&New font:'), f)
|
||||
f.textChanged.connect(self.updated_family)
|
||||
self.embed_status = e = QLabel('')
|
||||
e.setWordWrap(True)
|
||||
l.addRow(e)
|
||||
l.addRow(self.bb)
|
||||
|
||||
@property
|
||||
def family(self):
|
||||
return unicode(self._family.text())
|
||||
|
||||
def updated_family(self):
|
||||
family = self.family
|
||||
found = icu_lower(family) in self.local_families
|
||||
t = _('The font %s <b>exists</b> on your computer and can be embedded') if found else _(
|
||||
'The font %s <b>does not exist</b> on your computer and cannot be embedded')
|
||||
t = (t % family) if family else ''
|
||||
self.embed_status.setText(t)
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
|
||||
class ManageFonts(Dialog):
|
||||
|
||||
container_changed = pyqtSignal()
|
||||
embed_all_fonts = pyqtSignal()
|
||||
subset_all_fonts = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
Dialog.__init__(self, _('Manage Fonts'), 'manage-fonts', parent=parent)
|
||||
|
||||
def setup_ui(self):
|
||||
self.setAttribute(Qt.WA_DeleteOnClose, False)
|
||||
self.l = l = QVBoxLayout(self)
|
||||
self.setLayout(l)
|
||||
|
||||
@ -98,14 +160,25 @@ class ManageFonts(Dialog):
|
||||
s.addWidget(fv), s.addWidget(c)
|
||||
|
||||
self.cb = b = QPushButton(_('&Change selected fonts'))
|
||||
b.setIcon(QIcon(I('auto_author_sort.png')))
|
||||
b.clicked.connect(self.change_fonts)
|
||||
l.addWidget(b)
|
||||
self.rb = b = QPushButton(_('&Remove selected fonts'))
|
||||
b.clicked.connect(self.remove_fonts)
|
||||
b.setIcon(QIcon(I('trash.png')))
|
||||
l.addWidget(b)
|
||||
self.eb = b = QPushButton(_('&Embed all fonts'))
|
||||
b.setIcon(QIcon(I('embed-fonts.png')))
|
||||
b.clicked.connect(self.embed_fonts)
|
||||
l.addWidget(b)
|
||||
self.sb = b = QPushButton(_('&Subset all fonts'))
|
||||
b.setIcon(QIcon(I('subset-fonts.png')))
|
||||
b.clicked.connect(self.subset_fonts)
|
||||
l.addWidget(b)
|
||||
self.refresh_button = b = self.bb.addButton(_('&Refresh'), self.bb.ActionRole)
|
||||
b.setToolTip(_('Rescan the book for fonts in case you have made changes'))
|
||||
b.setIcon(QIcon(I('view-refresh.png')))
|
||||
b.clicked.connect(self.refresh)
|
||||
|
||||
self.la = la = QLabel('<p>' + _(
|
||||
''' All the fonts declared in this book are shown to the left, along with whether they are embedded or not.
|
||||
@ -114,16 +187,54 @@ class ManageFonts(Dialog):
|
||||
l.addWidget(la)
|
||||
|
||||
l.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
|
||||
QTimer.singleShot(0, m.build)
|
||||
|
||||
def display(self):
|
||||
if not self.isVisible():
|
||||
self.show()
|
||||
self.raise_()
|
||||
QTimer.singleShot(0, self.model.build)
|
||||
|
||||
def get_selected_data(self):
|
||||
ans = self.model.data_for_indices(list(self.fonts_view.selectedIndexes()))
|
||||
if not ans:
|
||||
error_dialog(self, _('No fonts selected'), _(
|
||||
'No fonts selected, you must first select some fonts in the left panel'), show=True)
|
||||
return ans
|
||||
|
||||
def change_fonts(self):
|
||||
pass
|
||||
fonts = self.get_selected_data()
|
||||
if not fonts:
|
||||
return
|
||||
d = ChangeFontFamily(', '.join(fonts), {f for f, embedded in self.model.font_data.iteritems() if embedded}, self)
|
||||
if d.exec_() != d.Accepted:
|
||||
return
|
||||
changed = False
|
||||
new_family = d.family
|
||||
for font in fonts:
|
||||
changed |= change_font(current_container(), font, new_family)
|
||||
if changed:
|
||||
self.model.build()
|
||||
self.container_changed.emit()
|
||||
|
||||
def remove_fonts(self):
|
||||
pass
|
||||
fonts = self.get_selected_data()
|
||||
if not fonts:
|
||||
return
|
||||
changed = False
|
||||
for font in fonts:
|
||||
changed |= change_font(current_container(), font)
|
||||
if changed:
|
||||
self.model.build()
|
||||
self.container_changed.emit()
|
||||
|
||||
def embed_fonts(self):
|
||||
pass
|
||||
self.embed_all_fonts.emit()
|
||||
|
||||
def subset_fonts(self):
|
||||
self.subset_all_fonts.emit()
|
||||
|
||||
def refresh(self):
|
||||
self.model.build()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QApplication([])
|
||||
|
@ -34,6 +34,7 @@ from calibre.gui2.tweak_book.search import SavedSearches
|
||||
from calibre.gui2.tweak_book.toc import TOCViewer
|
||||
from calibre.gui2.tweak_book.char_select import CharSelect
|
||||
from calibre.gui2.tweak_book.live_css import LiveCSS
|
||||
from calibre.gui2.tweak_book.manage_fonts import ManageFonts
|
||||
from calibre.gui2.tweak_book.editor.widget import register_text_editor_actions
|
||||
from calibre.gui2.tweak_book.editor.insert_resource import InsertImage
|
||||
from calibre.utils.icu import character_name
|
||||
@ -228,6 +229,7 @@ class Main(MainWindow):
|
||||
self.saved_searches = SavedSearches(self)
|
||||
self.image_browser = InsertImage(self, for_browsing=True)
|
||||
self.insert_char = CharSelect(self)
|
||||
self.manage_fonts = ManageFonts(self)
|
||||
|
||||
self.create_actions()
|
||||
self.create_toolbars()
|
||||
@ -353,6 +355,7 @@ class Main(MainWindow):
|
||||
_('Set Semantics'))
|
||||
self.action_filter_css = reg('filter.png', _('&Filter style information'), self.boss.filter_css, 'filter-css', (),
|
||||
_('Filter style information'))
|
||||
self.action_manage_fonts = reg('font.png', _('Manage &fonts'), self.boss.manage_fonts, 'manage-fonts', (), _('Manage fonts in the book'))
|
||||
|
||||
# Polish actions
|
||||
group = _('Polish Book')
|
||||
@ -480,6 +483,7 @@ class Main(MainWindow):
|
||||
tm = e.addMenu(_('Table of Contents'))
|
||||
tm.addAction(self.action_toc)
|
||||
tm.addAction(self.action_inline_toc)
|
||||
e.addAction(self.action_manage_fonts)
|
||||
e.addAction(self.action_embed_fonts)
|
||||
e.addAction(self.action_subset_fonts)
|
||||
e.addAction(self.action_smarten_punctuation)
|
||||
@ -568,7 +572,7 @@ class Main(MainWindow):
|
||||
a(self.action_help)
|
||||
|
||||
a = create(_('Polish book tool bar'), 'polish').addAction
|
||||
for x in ('embed_fonts', 'subset_fonts', 'smarten_punctuation', 'remove_unused_css'):
|
||||
for x in ('manage_fonts', 'embed_fonts', 'subset_fonts', 'smarten_punctuation', 'remove_unused_css'):
|
||||
a(getattr(self, 'action_' + x))
|
||||
|
||||
def create_docks(self):
|
||||
|
Loading…
x
Reference in New Issue
Block a user