'
+
+import logging
+from collections import defaultdict
+
+import cssutils
+from lxml import etree
+
+from calibre import guess_type
+from calibre.ebooks.oeb.base import XPath, CSS_MIME, XHTML
+from calibre.ebooks.oeb.transforms.subset import get_font_properties, find_font_face_rules, elem_style
+from calibre.utils.filenames import ascii_filename
+from calibre.utils.fonts.scanner import font_scanner, NoFonts
+
+def used_font(style, embedded_fonts):
+ ff = [unicode(f) for f in style.get('font-family', []) if unicode(f).lower() not in {
+ 'serif', 'sansserif', 'sans-serif', 'fantasy', 'cursive', 'monospace'}]
+ if not ff:
+ return False, None
+ lnames = {unicode(x).lower() for x in ff}
+
+ matching_set = []
+
+ # Filter on font-family
+ for ef in embedded_fonts:
+ flnames = {x.lower() for x in ef.get('font-family', [])}
+ if not lnames.intersection(flnames):
+ continue
+ matching_set.append(ef)
+ if not matching_set:
+ return True, None
+
+ # Filter on font-stretch
+ widths = {x:i for i, x in enumerate(('ultra-condensed',
+ 'extra-condensed', 'condensed', 'semi-condensed', 'normal',
+ 'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded'
+ ))}
+
+ width = widths[style.get('font-stretch', 'normal')]
+ for f in matching_set:
+ f['width'] = widths[style.get('font-stretch', 'normal')]
+
+ min_dist = min(abs(width-f['width']) for f in matching_set)
+ if min_dist > 0:
+ return True, None
+ nearest = [f for f in matching_set if abs(width-f['width']) ==
+ min_dist]
+ if width <= 4:
+ lmatches = [f for f in nearest if f['width'] <= width]
+ else:
+ lmatches = [f for f in nearest if f['width'] >= width]
+ matching_set = (lmatches or nearest)
+
+ # Filter on font-style
+ fs = style.get('font-style', 'normal')
+ matching_set = [f for f in matching_set if f.get('font-style', 'normal') == fs]
+
+ # Filter on font weight
+ fw = int(style.get('font-weight', '400'))
+ matching_set = [f for f in matching_set if f.get('weight', 400) == fw]
+
+ if not matching_set:
+ return True, None
+ return True, matching_set[0]
+
+
+class EmbedFonts(object):
+
+ '''
+ Embed all referenced fonts, if found on system. Must be called after CSS flattening.
+ '''
+
+ def __call__(self, oeb, log, opts):
+ self.oeb, self.log, self.opts = oeb, log, opts
+ self.sheet_cache = {}
+ self.find_style_rules()
+ self.find_embedded_fonts()
+ self.parser = cssutils.CSSParser(loglevel=logging.CRITICAL, log=logging.getLogger('calibre.css'))
+ self.warned = set()
+ self.warned2 = set()
+
+ for item in oeb.spine:
+ if not hasattr(item.data, 'xpath'):
+ continue
+ sheets = []
+ for href in XPath('//h:link[@href and @type="text/css"]/@href')(item.data):
+ sheet = self.oeb.manifest.hrefs.get(item.abshref(href), None)
+ if sheet is not None:
+ sheets.append(sheet)
+ if sheets:
+ self.process_item(item, sheets)
+
+ def find_embedded_fonts(self):
+ '''
+ Find all @font-face rules and extract the relevant info from them.
+ '''
+ self.embedded_fonts = []
+ for item in self.oeb.manifest:
+ if not hasattr(item.data, 'cssRules'):
+ continue
+ self.embedded_fonts.extend(find_font_face_rules(item, self.oeb))
+
+ def find_style_rules(self):
+ '''
+ Extract all font related style information from all stylesheets into a
+ dict mapping classes to font properties specified by that class. All
+ the heavy lifting has already been done by the CSS flattening code.
+ '''
+ rules = defaultdict(dict)
+ for item in self.oeb.manifest:
+ if not hasattr(item.data, 'cssRules'):
+ continue
+ for i, rule in enumerate(item.data.cssRules):
+ if rule.type != rule.STYLE_RULE:
+ continue
+ props = {k:v for k,v in
+ get_font_properties(rule).iteritems() if v}
+ if not props:
+ continue
+ for sel in rule.selectorList:
+ sel = sel.selectorText
+ if sel and sel.startswith('.'):
+ # We dont care about pseudo-selectors as the worst that
+ # can happen is some extra characters will remain in
+ # the font
+ sel = sel.partition(':')[0]
+ rules[sel[1:]].update(props)
+
+ self.style_rules = dict(rules)
+
+ def get_page_sheet(self):
+ if self.page_sheet is None:
+ manifest = self.oeb.manifest
+ id_, href = manifest.generate('page_css', 'page_styles.css')
+ self.page_sheet = manifest.add(id_, href, CSS_MIME, data=self.parser.parseString('', validate=False))
+ head = self.current_item.xpath('//*[local-name()="head"][1]')
+ if head:
+ href = self.current_item.relhref(href)
+ l = etree.SubElement(head[0], XHTML('link'),
+ rel='stylesheet', type=CSS_MIME, href=href)
+ l.tail = '\n'
+ else:
+ self.log.warn('No cannot embed font rules')
+ return self.page_sheet
+
+ def process_item(self, item, sheets):
+ ff_rules = []
+ self.current_item = item
+ self.page_sheet = None
+ for sheet in sheets:
+ if 'page_css' in sheet.id:
+ ff_rules.extend(find_font_face_rules(sheet, self.oeb))
+ self.page_sheet = sheet
+
+ base = {'font-family':['serif'], 'font-weight': '400',
+ 'font-style':'normal', 'font-stretch':'normal'}
+
+ for body in item.data.xpath('//*[local-name()="body"]'):
+ self.find_usage_in(body, base, ff_rules)
+
+ def find_usage_in(self, elem, inherited_style, ff_rules):
+ style = elem_style(self.style_rules, elem.get('class', '') or '', inherited_style)
+ for child in elem:
+ self.find_usage_in(child, style, ff_rules)
+ has_font, existing = used_font(style, ff_rules)
+ if not has_font:
+ return
+ if existing is None:
+ in_book = used_font(style, self.embedded_fonts)[1]
+ if in_book is None:
+ # Try to find the font in the system
+ added = self.embed_font(style)
+ if added is not None:
+ ff_rules.append(added)
+ self.embedded_fonts.append(added)
+ else:
+ # TODO: Create a page rule from the book rule (cannot use it
+ # directly as paths might be different)
+ item = in_book['item']
+ sheet = self.parser.parseString(in_book['rule'].cssText, validate=False)
+ rule = sheet.cssRules[0]
+ page_sheet = self.get_page_sheet()
+ href = page_sheet.abshref(item.href)
+ rule.style.setProperty('src', 'url(%s)' % href)
+ ff_rules.append(find_font_face_rules(sheet, self.oeb)[0])
+ page_sheet.data.insertRule(rule, len(page_sheet.data.cssRules))
+
+ def embed_font(self, style):
+ ff = [unicode(f) for f in style.get('font-family', []) if unicode(f).lower() not in {
+ 'serif', 'sansserif', 'sans-serif', 'fantasy', 'cursive', 'monospace'}]
+ if not ff:
+ return
+ ff = ff[0]
+ if ff in self.warned:
+ return
+ try:
+ fonts = font_scanner.fonts_for_family(ff)
+ except NoFonts:
+ self.log.warn('Failed to find fonts for family:', ff, 'not embedding')
+ self.warned.add(ff)
+ return
+ try:
+ weight = int(style.get('font-weight', '400'))
+ except (ValueError, TypeError, AttributeError):
+ w = style['font-weight']
+ if w not in self.warned2:
+ self.log.warn('Invalid weight in font style: %r' % w)
+ self.warned2.add(w)
+ return
+ for f in fonts:
+ if f['weight'] == weight and f['font-style'] == style.get('font-style', 'normal') and f['font-stretch'] == style.get('font-stretch', 'normal'):
+ self.log('Embedding font %s from %s' % (f['full_name'], f['path']))
+ data = font_scanner.get_font_data(f)
+ name = f['full_name']
+ ext = 'otf' if f['is_otf'] else 'ttf'
+ name = ascii_filename(name).replace(' ', '-').replace('(', '').replace(')', '')
+ fid, href = self.oeb.manifest.generate(id=u'font', href=u'fonts/%s.%s'%(name, ext))
+ item = self.oeb.manifest.add(fid, href, guess_type('dummy.'+ext)[0], data=data)
+ item.unload_data_from_memory()
+ page_sheet = self.get_page_sheet()
+ href = page_sheet.relhref(item.href)
+ css = '''@font-face { font-family: "%s"; font-weight: %s; font-style: %s; font-stretch: %s; src: url(%s) }''' % (
+ f['font-family'], f['font-weight'], f['font-style'], f['font-stretch'], href)
+ sheet = self.parser.parseString(css, validate=False)
+ page_sheet.data.insertRule(sheet.cssRules[0], len(page_sheet.data.cssRules))
+ return find_font_face_rules(sheet, self.oeb)[0]
+
diff --git a/src/calibre/ebooks/oeb/transforms/flatcss.py b/src/calibre/ebooks/oeb/transforms/flatcss.py
index dd2d20333d..9c08934938 100644
--- a/src/calibre/ebooks/oeb/transforms/flatcss.py
+++ b/src/calibre/ebooks/oeb/transforms/flatcss.py
@@ -194,7 +194,7 @@ class CSSFlattener(object):
for i, font in enumerate(faces):
ext = 'otf' if font['is_otf'] else 'ttf'
fid, href = self.oeb.manifest.generate(id=u'font',
- href=u'%s.%s'%(ascii_filename(font['full_name']).replace(u' ', u'-'), ext))
+ href=u'fonts/%s.%s'%(ascii_filename(font['full_name']).replace(u' ', u'-'), ext))
item = self.oeb.manifest.add(fid, href,
guess_type('dummy.'+ext)[0],
data=font_scanner.get_font_data(font))
diff --git a/src/calibre/ebooks/oeb/transforms/subset.py b/src/calibre/ebooks/oeb/transforms/subset.py
index 744e37b193..96170bd49c 100644
--- a/src/calibre/ebooks/oeb/transforms/subset.py
+++ b/src/calibre/ebooks/oeb/transforms/subset.py
@@ -12,6 +12,111 @@ from collections import defaultdict
from calibre.ebooks.oeb.base import urlnormalize
from calibre.utils.fonts.sfnt.subset import subset, NoGlyphs, UnsupportedFont
+def get_font_properties(rule, default=None):
+ '''
+ Given a CSS rule, extract normalized font properties from
+ it. Note that shorthand font property should already have been expanded
+ by the CSS flattening code.
+ '''
+ props = {}
+ s = rule.style
+ for q in ('font-family', 'src', 'font-weight', 'font-stretch',
+ 'font-style'):
+ g = 'uri' if q == 'src' else 'value'
+ try:
+ val = s.getProperty(q).propertyValue[0]
+ val = getattr(val, g)
+ if q == 'font-family':
+ val = [x.value for x in s.getProperty(q).propertyValue]
+ if val and val[0] == 'inherit':
+ val = None
+ except (IndexError, KeyError, AttributeError, TypeError, ValueError):
+ val = None if q in {'src', 'font-family'} else default
+ if q in {'font-weight', 'font-stretch', 'font-style'}:
+ val = unicode(val).lower() if (val or val == 0) else val
+ if val == 'inherit':
+ val = default
+ if q == 'font-weight':
+ val = {'normal':'400', 'bold':'700'}.get(val, val)
+ if val not in {'100', '200', '300', '400', '500', '600', '700',
+ '800', '900', 'bolder', 'lighter'}:
+ val = default
+ if val == 'normal':
+ val = '400'
+ elif q == 'font-style':
+ if val not in {'normal', 'italic', 'oblique'}:
+ val = default
+ elif q == 'font-stretch':
+ if val not in {'normal', 'ultra-condensed', 'extra-condensed',
+ 'condensed', 'semi-condensed', 'semi-expanded',
+ 'expanded', 'extra-expanded', 'ultra-expanded'}:
+ val = default
+ props[q] = val
+ return props
+
+
+def find_font_face_rules(sheet, oeb):
+ '''
+ Find all @font-face rules in the given sheet and extract the relevant info from them.
+ sheet can be either a ManifestItem or a CSSStyleSheet.
+ '''
+ ans = []
+ try:
+ rules = sheet.data.cssRules
+ except AttributeError:
+ rules = sheet.cssRules
+
+ for i, rule in enumerate(rules):
+ if rule.type != rule.FONT_FACE_RULE:
+ continue
+ props = get_font_properties(rule, default='normal')
+ if not props['font-family'] or not props['src']:
+ continue
+
+ try:
+ path = sheet.abshref(props['src'])
+ except AttributeError:
+ path = props['src']
+ ff = oeb.manifest.hrefs.get(urlnormalize(path), None)
+ if not ff:
+ continue
+ props['item'] = ff
+ if props['font-weight'] in {'bolder', 'lighter'}:
+ props['font-weight'] = '400'
+ props['weight'] = int(props['font-weight'])
+ props['rule'] = rule
+ props['chars'] = set()
+ ans.append(props)
+
+ return ans
+
+
+def elem_style(style_rules, cls, inherited_style):
+ '''
+ Find the effective style for the given element.
+ '''
+ classes = cls.split()
+ style = inherited_style.copy()
+ for cls in classes:
+ style.update(style_rules.get(cls, {}))
+ wt = style.get('font-weight', None)
+ pwt = inherited_style.get('font-weight', '400')
+ if wt == 'bolder':
+ style['font-weight'] = {
+ '100':'400',
+ '200':'400',
+ '300':'400',
+ '400':'700',
+ '500':'700',
+ }.get(pwt, '900')
+ elif wt == 'lighter':
+ style['font-weight'] = {
+ '600':'400', '700':'400',
+ '800':'700', '900':'700'}.get(pwt, '100')
+
+ return style
+
+
class SubsetFonts(object):
'''
@@ -76,72 +181,15 @@ class SubsetFonts(object):
self.log('Reduced total font size to %.1f%% of original'%
(totals[0]/totals[1] * 100))
- def get_font_properties(self, rule, default=None):
- '''
- Given a CSS rule, extract normalized font properties from
- it. Note that shorthand font property should already have been expanded
- by the CSS flattening code.
- '''
- props = {}
- s = rule.style
- for q in ('font-family', 'src', 'font-weight', 'font-stretch',
- 'font-style'):
- g = 'uri' if q == 'src' else 'value'
- try:
- val = s.getProperty(q).propertyValue[0]
- val = getattr(val, g)
- if q == 'font-family':
- val = [x.value for x in s.getProperty(q).propertyValue]
- if val and val[0] == 'inherit':
- val = None
- except (IndexError, KeyError, AttributeError, TypeError, ValueError):
- val = None if q in {'src', 'font-family'} else default
- if q in {'font-weight', 'font-stretch', 'font-style'}:
- val = unicode(val).lower() if (val or val == 0) else val
- if val == 'inherit':
- val = default
- if q == 'font-weight':
- val = {'normal':'400', 'bold':'700'}.get(val, val)
- if val not in {'100', '200', '300', '400', '500', '600', '700',
- '800', '900', 'bolder', 'lighter'}:
- val = default
- if val == 'normal': val = '400'
- elif q == 'font-style':
- if val not in {'normal', 'italic', 'oblique'}:
- val = default
- elif q == 'font-stretch':
- if val not in { 'normal', 'ultra-condensed', 'extra-condensed',
- 'condensed', 'semi-condensed', 'semi-expanded',
- 'expanded', 'extra-expanded', 'ultra-expanded'}:
- val = default
- props[q] = val
- return props
-
def find_embedded_fonts(self):
'''
Find all @font-face rules and extract the relevant info from them.
'''
self.embedded_fonts = []
for item in self.oeb.manifest:
- if not hasattr(item.data, 'cssRules'): continue
- for i, rule in enumerate(item.data.cssRules):
- if rule.type != rule.FONT_FACE_RULE:
- continue
- props = self.get_font_properties(rule, default='normal')
- if not props['font-family'] or not props['src']:
- continue
-
- path = item.abshref(props['src'])
- ff = self.oeb.manifest.hrefs.get(urlnormalize(path), None)
- if not ff:
- continue
- props['item'] = ff
- if props['font-weight'] in {'bolder', 'lighter'}:
- props['font-weight'] = '400'
- props['weight'] = int(props['font-weight'])
- props['chars'] = set()
- props['rule'] = rule
- self.embedded_fonts.append(props)
+ if not hasattr(item.data, 'cssRules'):
+ continue
+ self.embedded_fonts.extend(find_font_face_rules(item, self.oeb))
def find_style_rules(self):
'''
@@ -151,12 +199,13 @@ class SubsetFonts(object):
'''
rules = defaultdict(dict)
for item in self.oeb.manifest:
- if not hasattr(item.data, 'cssRules'): continue
+ if not hasattr(item.data, 'cssRules'):
+ continue
for i, rule in enumerate(item.data.cssRules):
if rule.type != rule.STYLE_RULE:
continue
props = {k:v for k,v in
- self.get_font_properties(rule).iteritems() if v}
+ get_font_properties(rule).iteritems() if v}
if not props:
continue
for sel in rule.selectorList:
@@ -172,41 +221,17 @@ class SubsetFonts(object):
def find_font_usage(self):
for item in self.oeb.manifest:
- if not hasattr(item.data, 'xpath'): continue
+ if not hasattr(item.data, 'xpath'):
+ continue
for body in item.data.xpath('//*[local-name()="body"]'):
base = {'font-family':['serif'], 'font-weight': '400',
'font-style':'normal', 'font-stretch':'normal'}
self.find_usage_in(body, base)
- def elem_style(self, cls, inherited_style):
- '''
- Find the effective style for the given element.
- '''
- classes = cls.split()
- style = inherited_style.copy()
- for cls in classes:
- style.update(self.style_rules.get(cls, {}))
- wt = style.get('font-weight', None)
- pwt = inherited_style.get('font-weight', '400')
- if wt == 'bolder':
- style['font-weight'] = {
- '100':'400',
- '200':'400',
- '300':'400',
- '400':'700',
- '500':'700',
- }.get(pwt, '900')
- elif wt == 'lighter':
- style['font-weight'] = {
- '600':'400', '700':'400',
- '800':'700', '900':'700'}.get(pwt, '100')
-
- return style
-
def used_font(self, style):
'''
Given a style find the embedded font that matches it. Returns None if
- no match is found ( can happen if not family matches).
+ no match is found (can happen if no family matches).
'''
ff = style.get('font-family', [])
lnames = {unicode(x).lower() for x in ff}
@@ -222,7 +247,7 @@ class SubsetFonts(object):
return None
# Filter on font-stretch
- widths = {x:i for i, x in enumerate(( 'ultra-condensed',
+ widths = {x:i for i, x in enumerate(('ultra-condensed',
'extra-condensed', 'condensed', 'semi-condensed', 'normal',
'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded'
))}
@@ -280,7 +305,7 @@ class SubsetFonts(object):
return ans
def find_usage_in(self, elem, inherited_style):
- style = self.elem_style(elem.get('class', '') or '', inherited_style)
+ style = elem_style(self.style_rules, elem.get('class', '') or '', inherited_style)
for child in elem:
self.find_usage_in(child, style)
font = self.used_font(style)
@@ -290,3 +315,4 @@ class SubsetFonts(object):
font['chars'] |= chars
+
diff --git a/src/calibre/ebooks/pdf/render/from_html.py b/src/calibre/ebooks/pdf/render/from_html.py
index 5b9f58e326..8ea1d8203e 100644
--- a/src/calibre/ebooks/pdf/render/from_html.py
+++ b/src/calibre/ebooks/pdf/render/from_html.py
@@ -253,7 +253,7 @@ class PDFWriter(QObject):
return self.loop.exit(1)
try:
if not self.render_queue:
- if self.toc is not None and len(self.toc) > 0 and not hasattr(self, 'rendered_inline_toc'):
+ if self.opts.pdf_add_toc and self.toc is not None and len(self.toc) > 0 and not hasattr(self, 'rendered_inline_toc'):
return self.render_inline_toc()
self.loop.exit()
else:
diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py
index e5a9bfbc7d..729de33c7f 100644
--- a/src/calibre/gui2/actions/edit_metadata.py
+++ b/src/calibre/gui2/actions/edit_metadata.py
@@ -399,8 +399,7 @@ class EditMetadataAction(InterfaceAction):
if safe_merge:
if not confirm(''+_(
'Book formats and metadata from the selected books '
- 'will be added to the first selected book (%s). '
- 'ISBN will not be merged.
'
+ 'will be added to the first selected book (%s).
'
'The second and subsequently selected books will not '
'be deleted or changed.
'
'Please confirm you want to proceed.')%title
@@ -413,7 +412,7 @@ class EditMetadataAction(InterfaceAction):
'Book formats from the selected books will be merged '
'into the first selected book (%s). '
'Metadata in the first selected book will not be changed. '
- 'Author, Title, ISBN and all other metadata will not be merged.
'
+ 'Author, Title and all other metadata will not be merged.
'
'After merger the second and subsequently '
'selected books, with any metadata they have will be deleted.
'
'All book formats of the first selected book will be kept '
@@ -427,8 +426,7 @@ class EditMetadataAction(InterfaceAction):
else:
if not confirm('
'+_(
'Book formats and metadata from the selected books will be merged '
- 'into the first selected book (%s). '
- 'ISBN will not be merged.
'
+ 'into the first selected book (%s).
'
'After merger the second and '
'subsequently selected books will be deleted.
'
'All book formats of the first selected book will be kept '
@@ -490,11 +488,13 @@ class EditMetadataAction(InterfaceAction):
def merge_metadata(self, dest_id, src_ids):
db = self.gui.library_view.model().db
dest_mi = db.get_metadata(dest_id, index_is_id=True)
+ merged_identifiers = db.get_identifiers(dest_id, index_is_id=True)
orig_dest_comments = dest_mi.comments
dest_cover = db.cover(dest_id, index_is_id=True)
had_orig_cover = bool(dest_cover)
for src_id in src_ids:
src_mi = db.get_metadata(src_id, index_is_id=True)
+
if src_mi.comments and orig_dest_comments != src_mi.comments:
if not dest_mi.comments:
dest_mi.comments = src_mi.comments
@@ -523,7 +523,15 @@ class EditMetadataAction(InterfaceAction):
if not dest_mi.series:
dest_mi.series = src_mi.series
dest_mi.series_index = src_mi.series_index
+
+ src_identifiers = db.get_identifiers(src_id, index_is_id=True)
+ src_identifiers.update(merged_identifiers)
+ merged_identifiers = src_identifiers.copy()
+
+ if merged_identifiers:
+ dest_mi.set_identifiers(merged_identifiers)
db.set_metadata(dest_id, dest_mi, ignore_errors=False)
+
if not had_orig_cover and dest_cover:
db.set_cover(dest_id, dest_cover)
diff --git a/src/calibre/gui2/convert/look_and_feel.py b/src/calibre/gui2/convert/look_and_feel.py
index 24ee288cc6..a3e364b9ca 100644
--- a/src/calibre/gui2/convert/look_and_feel.py
+++ b/src/calibre/gui2/convert/look_and_feel.py
@@ -32,7 +32,7 @@ class LookAndFeelWidget(Widget, Ui_Form):
Widget.__init__(self, parent,
['change_justification', 'extra_css', 'base_font_size',
'font_size_mapping', 'line_height', 'minimum_line_height',
- 'embed_font_family', 'subset_embedded_fonts',
+ 'embed_font_family', 'embed_all_fonts', 'subset_embedded_fonts',
'smarten_punctuation', 'unsmarten_punctuation',
'disable_font_rescaling', 'insert_blank_line',
'remove_paragraph_spacing',
diff --git a/src/calibre/gui2/convert/look_and_feel.ui b/src/calibre/gui2/convert/look_and_feel.ui
index 43736fb1f2..e9d9caeed7 100644
--- a/src/calibre/gui2/convert/look_and_feel.ui
+++ b/src/calibre/gui2/convert/look_and_feel.ui
@@ -14,6 +14,70 @@
Form
+ -
+
+
+ Keep &ligatures
+
+
+
+ -
+
+
+ &Linearize tables
+
+
+
+ -
+
+
+ Base &font size:
+
+
+ opt_base_font_size
+
+
+
+ -
+
+
+ &Line size:
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ opt_insert_blank_line_size
+
+
+
+ -
+
+
+ true
+
+
+
+ -
+
+
+ Remove &spacing between paragraphs
+
+
+
+ -
+
+
+ &Indent size:
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ opt_remove_paragraph_spacing_indent_size
+
+
+
-
@@ -24,6 +88,57 @@
+ -
+
+
+ Insert &blank line between paragraphs
+
+
+
+ -
+
+
+ em
+
+
+ 1
+
+
+
+ -
+
+
+ Text &justification:
+
+
+ opt_change_justification
+
+
+
+ -
+
+
+ -
+
+
+ Smarten &punctuation
+
+
+
+ -
+
+
+ &Transliterate unicode characters to ASCII
+
+
+
+ -
+
+
+ &UnSmarten punctuation
+
+
+
-
@@ -44,51 +159,6 @@
- -
-
-
- %
-
-
- 1
-
-
- 900.000000000000000
-
-
-
- -
-
-
- pt
-
-
- 1
-
-
- 0.000000000000000
-
-
- 50.000000000000000
-
-
- 1.000000000000000
-
-
- 15.000000000000000
-
-
-
- -
-
-
- Font size &key:
-
-
- opt_font_size_mapping
-
-
-
-
-
@@ -133,56 +203,72 @@
- -
-
-
- true
-
-
-
- -
-
-
- Remove &spacing between paragraphs
-
-
-
- -
-
-
- &Indent size:
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- opt_remove_paragraph_spacing_indent_size
-
-
-
- -
-
-
- <p>When calibre removes inter paragraph spacing, it automatically sets a paragraph indent, to ensure that paragraphs can be easily distinguished. This option controls the width of that indent.
-
-
- No change
-
+
-
+
- em
+ %
+
+
+ 1
+
+
+ 900.000000000000000
+
+
+
+ -
+
+
+ pt
1
- -0.100000000000000
+ 0.000000000000000
+
+
+ 50.000000000000000
- 0.100000000000000
+ 1.000000000000000
+
+
+ 15.000000000000000
- -
+
-
+
+
+ &Disable font size rescaling
+
+
+
+ -
+
+
+ -
+
+
+ Font size &key:
+
+
+ opt_font_size_mapping
+
+
+
+ -
+
+
+ &Embed font family:
+
+
+ opt_embed_font_family
+
+
+
+ -
0
@@ -300,121 +386,42 @@
- -
-
-
- Insert &blank line between paragraphs
-
-
-
-
-
+
+
+ <p>When calibre removes inter paragraph spacing, it automatically sets a paragraph indent, to ensure that paragraphs can be easily distinguished. This option controls the width of that indent.
+
+
+ No change
+
em
1
-
-
- -
-
-
- Text &justification:
+
+ -0.100000000000000
-
- opt_change_justification
+
+ 0.100000000000000
- -
-
-
- -
-
-
- Smarten &punctuation
-
-
-
- -
-
-
- &Transliterate unicode characters to ASCII
-
-
-
- -
-
-
- &UnSmarten punctuation
-
-
-
- -
-
-
- Keep &ligatures
-
-
-
- -
-
-
- &Linearize tables
-
-
-
- -
-
-
- Base &font size:
-
-
- opt_base_font_size
-
-
-
- -
-
-
- &Line size:
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- opt_insert_blank_line_size
-
-
-
- -
-
-
- &Embed font family:
-
-
- opt_embed_font_family
-
-
-
- -
-
-
- &Disable font size rescaling
-
-
-
- -
-
-
- -
+
-
&Subset all embedded fonts
+ -
+
+
+ &Embed referenced fonts
+
+
+
diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py
index 2bafc2812a..3db6e37eb0 100644
--- a/src/calibre/gui2/dialogs/template_dialog.py
+++ b/src/calibre/gui2/dialogs/template_dialog.py
@@ -262,6 +262,8 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
self.mi.rating = 4.0
self.mi.tags = [_('Tag 1'), _('Tag 2')]
self.mi.languages = ['eng']
+ if fm is not None:
+ self.mi.set_all_user_metadata(fm.custom_field_metadata())
# Remove help icon on title bar
icon = self.windowIcon()
diff --git a/src/calibre/gui2/email.py b/src/calibre/gui2/email.py
index 9ebb94b00a..f8c7552437 100644
--- a/src/calibre/gui2/email.py
+++ b/src/calibre/gui2/email.py
@@ -32,7 +32,7 @@ class Worker(Thread):
self.func, self.args = func, args
def run(self):
- #time.sleep(1000)
+ # time.sleep(1000)
try:
self.func(*self.args)
except Exception as e:
@@ -46,7 +46,7 @@ class Worker(Thread):
class Sendmail(object):
MAX_RETRIES = 1
- TIMEOUT = 15 * 60 # seconds
+ TIMEOUT = 15 * 60 # seconds
def __init__(self):
self.calculate_rate_limit()
@@ -92,7 +92,11 @@ class Sendmail(object):
raise worker.exception
def sendmail(self, attachment, aname, to, subject, text, log):
+ logged = False
while time.time() - self.last_send_time <= self.rate_limit:
+ if not logged and self.rate_limit > 0:
+ log('Waiting %s seconds before sending, to avoid being marked as spam.\nYou can control this delay via Preferences->Tweaks' % self.rate_limit)
+ logged = True
time.sleep(1)
try:
opts = email_config().parse()
@@ -162,7 +166,7 @@ def email_news(mi, remove, get_fmts, done, job_manager):
plugboard_email_value = 'email'
plugboard_email_formats = ['epub', 'mobi', 'azw3']
-class EmailMixin(object): # {{{
+class EmailMixin(object): # {{{
def send_by_mail(self, to, fmts, delete_from_library, subject='', send_ids=None,
do_auto_convert=True, specific_format=None):
@@ -204,10 +208,10 @@ class EmailMixin(object): # {{{
if not components:
components = [mi.title]
subjects.append(os.path.join(*components))
- a = authors_to_string(mi.authors if mi.authors else \
+ a = authors_to_string(mi.authors if mi.authors else
[_('Unknown')])
- texts.append(_('Attached, you will find the e-book') + \
- '\n\n' + t + '\n\t' + _('by') + ' ' + a + '\n\n' + \
+ texts.append(_('Attached, you will find the e-book') +
+ '\n\n' + t + '\n\t' + _('by') + ' ' + a + '\n\n' +
_('in the %s format.') %
os.path.splitext(f)[1][1:].upper())
prefix = ascii_filename(t+' - '+a)
@@ -227,7 +231,7 @@ class EmailMixin(object): # {{{
auto = []
if _auto_ids != []:
for id in _auto_ids:
- if specific_format == None:
+ if specific_format is None:
dbfmts = self.library_view.model().db.formats(id, index_is_id=True)
formats = [f.lower() for f in (dbfmts.split(',') if dbfmts else
[])]
@@ -298,8 +302,9 @@ class EmailMixin(object): # {{{
sent_mails = email_news(mi, remove,
get_fmts, self.email_sent, self.job_manager)
if sent_mails:
- self.status_bar.show_message(_('Sent news to')+' '+\
+ self.status_bar.show_message(_('Sent news to')+' '+
', '.join(sent_mails), 3000)
# }}}
+
diff --git a/src/calibre/gui2/preferences/save_template.py b/src/calibre/gui2/preferences/save_template.py
index 627c4c7fa9..145e014800 100644
--- a/src/calibre/gui2/preferences/save_template.py
+++ b/src/calibre/gui2/preferences/save_template.py
@@ -24,7 +24,7 @@ class SaveTemplate(QWidget, Ui_Form):
Ui_Form.__init__(self)
self.setupUi(self)
- def initialize(self, name, default, help):
+ def initialize(self, name, default, help, field_metadata):
variables = sorted(FORMAT_ARG_DESCS.keys())
rows = []
for var in variables:
@@ -36,6 +36,7 @@ class SaveTemplate(QWidget, Ui_Form):
table = u''%(u'\n'.join(rows))
self.template_variables.setText(table)
+ self.field_metadata = field_metadata
self.opt_template.initialize(name+'_template_history',
default, help)
self.opt_template.editTextChanged.connect(self.changed)
@@ -44,7 +45,7 @@ class SaveTemplate(QWidget, Ui_Form):
self.open_editor.clicked.connect(self.do_open_editor)
def do_open_editor(self):
- t = TemplateDialog(self, self.opt_template.text())
+ t = TemplateDialog(self, self.opt_template.text(), fm=self.field_metadata)
t.setWindowTitle(_('Edit template'))
if t.exec_():
self.opt_template.set_value(t.rule[1])
diff --git a/src/calibre/gui2/preferences/saving.py b/src/calibre/gui2/preferences/saving.py
index bd5fcbb078..e1a235803d 100644
--- a/src/calibre/gui2/preferences/saving.py
+++ b/src/calibre/gui2/preferences/saving.py
@@ -34,7 +34,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
ConfigWidgetBase.initialize(self)
self.save_template.blockSignals(True)
self.save_template.initialize('save_to_disk', self.proxy['template'],
- self.proxy.help('template'))
+ self.proxy.help('template'),
+ self.gui.library_view.model().db.field_metadata)
self.save_template.blockSignals(False)
def restore_defaults(self):
diff --git a/src/calibre/gui2/preferences/sending.py b/src/calibre/gui2/preferences/sending.py
index 3fce5cb072..bc46ac500b 100644
--- a/src/calibre/gui2/preferences/sending.py
+++ b/src/calibre/gui2/preferences/sending.py
@@ -44,7 +44,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
ConfigWidgetBase.initialize(self)
self.send_template.blockSignals(True)
self.send_template.initialize('send_to_device', self.proxy['send_template'],
- self.proxy.help('send_template'))
+ self.proxy.help('send_template'),
+ self.gui.library_view.model().db.field_metadata)
self.send_template.blockSignals(False)
def restore_defaults(self):
diff --git a/src/calibre/gui2/tools.py b/src/calibre/gui2/tools.py
index eda60a4fec..32e3174c8d 100644
--- a/src/calibre/gui2/tools.py
+++ b/src/calibre/gui2/tools.py
@@ -24,7 +24,7 @@ from calibre.ebooks.conversion.config import GuiRecommendations, \
load_defaults, load_specifics, save_specifics
from calibre.gui2.convert import bulk_defaults_for_input_format
-def convert_single_ebook(parent, db, book_ids, auto_conversion=False, # {{{
+def convert_single_ebook(parent, db, book_ids, auto_conversion=False, # {{{
out_format=None, show_no_format_warning=True):
changed = False
jobs = []
@@ -47,7 +47,7 @@ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, # {{{
result = d.exec_()
if result == QDialog.Accepted:
- #if not convert_existing(parent, db, [book_id], d.output_format):
+ # if not convert_existing(parent, db, [book_id], d.output_format):
# continue
mi = db.get_metadata(book_id, True)
@@ -116,7 +116,6 @@ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, # {{{
msg = _('This book has no actual ebook files')
res.append('%s - %s'%(title, msg))
-
msg = '%s' % '\n'.join(res)
warning_dialog(parent, _('Could not convert some books'),
_('Could not convert %(num)d of %(tot)d books, because no supported source'
@@ -254,7 +253,7 @@ class QueueBulk(QProgressDialog):
# }}}
-def fetch_scheduled_recipe(arg): # {{{
+def fetch_scheduled_recipe(arg): # {{{
fmt = prefs['output_format'].lower()
# Never use AZW3 for periodicals...
if fmt == 'azw3':
@@ -266,6 +265,10 @@ def fetch_scheduled_recipe(arg): # {{{
if 'output_profile' in ps:
recs.append(('output_profile', ps['output_profile'],
OptionRecommendation.HIGH))
+ for edge in ('left', 'top', 'bottom', 'right'):
+ edge = 'margin_' + edge
+ if edge in ps:
+ recs.append((edge, ps[edge], OptionRecommendation.HIGH))
lf = load_defaults('look_and_feel')
if lf.get('base_font_size', 0.0) != 0.0:
@@ -283,18 +286,24 @@ def fetch_scheduled_recipe(arg): # {{{
if epub.get('epub_flatten', False):
recs.append(('epub_flatten', True, OptionRecommendation.HIGH))
+ if fmt == 'pdf':
+ pdf = load_defaults('pdf_output')
+ from calibre.customize.ui import plugin_for_output_format
+ p = plugin_for_output_format('pdf')
+ for opt in p.options:
+ recs.append(opt.name, pdf.get(opt.name, opt.recommended_value), OptionRecommendation.HIGH)
+
args = [arg['recipe'], pt.name, recs]
if arg['username'] is not None:
recs.append(('username', arg['username'], OptionRecommendation.HIGH))
if arg['password'] is not None:
recs.append(('password', arg['password'], OptionRecommendation.HIGH))
-
return 'gui_convert', args, _('Fetch news from ')+arg['title'], fmt.upper(), [pt]
# }}}
-def generate_catalog(parent, dbspec, ids, device_manager, db): # {{{
+def generate_catalog(parent, dbspec, ids, device_manager, db): # {{{
from calibre.gui2.dialogs.catalog import Catalog
# Build the Catalog dialog in gui2.dialogs.catalog
@@ -354,7 +363,7 @@ def generate_catalog(parent, dbspec, ids, device_manager, db): # {{{
d.catalog_title
# }}}
-def convert_existing(parent, db, book_ids, output_format): # {{{
+def convert_existing(parent, db, book_ids, output_format): # {{{
already_converted_ids = []
already_converted_titles = []
for book_id in book_ids:
@@ -372,3 +381,4 @@ def convert_existing(parent, db, book_ids, output_format): # {{{
return book_ids
# }}}
+
diff --git a/src/calibre/web/fetch/javascript.py b/src/calibre/web/fetch/javascript.py
index 56460c18bf..6e9ef86ff1 100644
--- a/src/calibre/web/fetch/javascript.py
+++ b/src/calibre/web/fetch/javascript.py
@@ -128,6 +128,8 @@ def download_resources(browser, resource_cache, output_dir):
else:
img_counter += 1
ext = what(None, raw) or 'jpg'
+ if ext == 'jpeg':
+ ext = 'jpg' # Apparently Moon+ cannot handle .jpeg
href = 'img_%d.%s' % (img_counter, ext)
dest = os.path.join(output_dir, href)
resource_cache[h] = dest