Edit Book: When embedding fonts fails to find a matching font, give a more informative message that explains why matching failed.

This commit is contained in:
Kovid Goyal 2016-11-14 09:54:38 +05:30
parent 94ca8a615b
commit 933f0f55d2
2 changed files with 112 additions and 72 deletions

View File

@ -37,6 +37,22 @@ def matching_rule(font, rules):
return rule return rule
def format_failed_match_report(fonts, font_family, font, report):
msg = _('Failed to find a font in the "%s" family matching the CSS font specification:') % font_family
msg += '\n\n* font-weight: %s' % font['font-weight']
msg += '\n* font-style: %s' % font['font-style']
msg += '\n* font-stretch: %s' % font['font-stretch']
msg += '\n\n' + _('Available fonts in the family are:') + '\n'
for f in fonts:
msg += '\n' + f['path']
msg += '\n\n* font-weight: %s' % f.get('font-weight', '400').strip()
msg += '\n* font-style: %s' % f.get('font-style', 'normal').strip()
msg += '\n* font-stretch: %s' % f.get('font-stretch', 'normal').strip()
msg += '\n\n'
report(msg)
report('')
def embed_font(container, font, all_font_rules, report, warned): def embed_font(container, font, all_font_rules, report, warned):
rule = matching_rule(font, all_font_rules) rule = matching_rule(font, all_font_rules)
ff = font['font-family'] ff = font['font-family']
@ -49,9 +65,12 @@ def embed_font(container, font, all_font_rules, report, warned):
try: try:
fonts = font_scanner.fonts_for_family(ff) fonts = font_scanner.fonts_for_family(ff)
except NoFonts: except NoFonts:
report(_('Failed to find fonts for family: %s, not embedding') % ff) try:
warned.add(ff) fonts = font_scanner.alt_fonts_for_family(ff)
return except NoFonts:
report(_('Failed to find fonts for family: %s, not embedding') % ff)
warned.add(ff)
return
wt = int(font.get('font-weight', '400')) wt = int(font.get('font-weight', '400'))
for f in fonts: for f in fonts:
if f['weight'] == wt and f['font-style'] == font.get('font-style', 'normal') and f['font-stretch'] == font.get('font-stretch', 'normal'): if f['weight'] == wt and f['font-style'] == font.get('font-style', 'normal') and f['font-stretch'] == font.get('font-stretch', 'normal'):
@ -69,11 +88,10 @@ def embed_font(container, font, all_font_rules, report, warned):
rule['src'] = 'url(%s)' % href rule['src'] = 'url(%s)' % href
rule['name'] = name rule['name'] = name
return rule return rule
msg = _('Failed to find font matching: family: %(family)s; weight: %(weight)s; style: %(style)s; stretch: %(stretch)s') % dict( wkey = ('spec-mismatch-', ff, wt, font['font-style'], font['font-stretch'])
family=ff, weight=font['font-weight'], style=font['font-style'], stretch=font['font-stretch']) if wkey not in warned:
if msg not in warned: warned.add(wkey)
warned.add(msg) format_failed_match_report(fonts, ff, font, report)
report(msg)
else: else:
name = rule['src'] name = rule['src']
href = container.name_to_href(name) href = container.name_to_href(name)
@ -159,4 +177,3 @@ if __name__ == '__main__':
prints(msg) prints(msg)
print() print()
prints('Output written to:', outbook) prints('Output written to:', outbook)

View File

@ -21,6 +21,8 @@ from calibre.utils.icu import sort_key
class NoFonts(ValueError): class NoFonts(ValueError):
pass pass
# Font dirs {{{
def default_font_dirs(): def default_font_dirs():
return [ return [
@ -115,6 +117,77 @@ def font_dirs():
os.path.expanduser('~/Library/Fonts'), os.path.expanduser('~/Library/Fonts'),
] ]
return fc_list() return fc_list()
# }}}
# Build font family maps {{{
def font_priority(font):
'''
Try to ensure that the "Regular" face is the first font for a given
family.
'''
style_normal = font['font-style'] == 'normal'
width_normal = font['font-stretch'] == 'normal'
weight_normal = font['font-weight'] == 'normal'
num_normal = sum(filter(None, (style_normal, width_normal,
weight_normal)))
subfamily_name = (font['wws_subfamily_name'] or
font['preferred_subfamily_name'] or font['subfamily_name'])
if num_normal == 3 and subfamily_name == 'Regular':
return 0
if num_normal == 3:
return 1
if subfamily_name == 'Regular':
return 2
return 3 + (3 - num_normal)
def path_significance(path, folders):
path = os.path.normcase(os.path.abspath(path))
for i, q in enumerate(folders):
if path.startswith(q):
return i
return -1
def build_families(cached_fonts, folders, family_attr='font-family'):
families = defaultdict(list)
for f in cached_fonts.itervalues():
if not f:
continue
lf = icu_lower(f.get(family_attr) or '')
if lf:
families[lf].append(f)
for fonts in families.itervalues():
# Look for duplicate font files and choose the copy that is from a
# more significant font directory (prefer user directories over
# system directories).
fmap = {}
remove = []
for f in fonts:
fingerprint = (icu_lower(f['font-family']), f['font-weight'],
f['font-stretch'], f['font-style'])
if fingerprint in fmap:
opath = fmap[fingerprint]['path']
npath = f['path']
if path_significance(npath, folders) >= path_significance(opath, folders):
remove.append(fmap[fingerprint])
fmap[fingerprint] = f
else:
remove.append(f)
else:
fmap[fingerprint] = f
for font in remove:
fonts.remove(font)
fonts.sort(key=font_priority)
font_family_map = dict.copy(families)
font_families = tuple(sorted((f[0]['font-family'] for f in
font_family_map.itervalues()), key=sort_key))
return font_family_map, font_families
# }}}
class FontScanner(Thread): class FontScanner(Thread):
@ -149,6 +222,17 @@ class FontScanner(Thread):
except KeyError: except KeyError:
raise NoFonts('No fonts found for the family: %r'%family) raise NoFonts('No fonts found for the family: %r'%family)
def alt_fonts_for_family(self, family):
''' Same as fonts_for_family() except that it uses the family name key
instead of the preferred_family_name key. This is needed because some
software, like Word uses the family name to refer to fonts instead of
the preferred family name. '''
self.join()
try:
return self.alt_font_family_map[icu_lower(family)]
except KeyError:
raise NoFonts('No fonts found for the family: %r'%family)
def legacy_fonts_for_family(self, family): def legacy_fonts_for_family(self, family):
''' '''
Return a simple set of regular, bold, italic and bold-italic faces for Return a simple set of regular, bold, italic and bold-italic faces for
@ -285,68 +369,9 @@ class FontScanner(Thread):
self.build_families() self.build_families()
def font_priority(self, font):
'''
Try to ensure that the "Regular" face is the first font for a given
family.
'''
style_normal = font['font-style'] == 'normal'
width_normal = font['font-stretch'] == 'normal'
weight_normal = font['font-weight'] == 'normal'
num_normal = sum(filter(None, (style_normal, width_normal,
weight_normal)))
subfamily_name = (font['wws_subfamily_name'] or
font['preferred_subfamily_name'] or font['subfamily_name'])
if num_normal == 3 and subfamily_name == 'Regular':
return 0
if num_normal == 3:
return 1
if subfamily_name == 'Regular':
return 2
return 3 + (3 - num_normal)
def build_families(self): def build_families(self):
families = defaultdict(list) self.font_family_map, self.font_families = build_families(self.cached_fonts, self.folders)
for f in self.cached_fonts.itervalues(): self.alt_font_family_map = build_families(self.cached_fonts, self.folders, 'family_name')[0]
if not f:
continue
lf = icu_lower(f['font-family'] or '')
if lf:
families[lf].append(f)
for fonts in families.itervalues():
# Look for duplicate font files and choose the copy that is from a
# more significant font directory (prefer user directories over
# system directories).
fmap = {}
remove = []
for f in fonts:
fingerprint = (icu_lower(f['font-family']), f['font-weight'],
f['font-stretch'], f['font-style'])
if fingerprint in fmap:
opath = fmap[fingerprint]['path']
npath = f['path']
if self.path_significance(npath) >= self.path_significance(opath):
remove.append(fmap[fingerprint])
fmap[fingerprint] = f
else:
remove.append(f)
else:
fmap[fingerprint] = f
for font in remove:
fonts.remove(font)
fonts.sort(key=self.font_priority)
self.font_family_map = dict.copy(families)
self.font_families = tuple(sorted((f[0]['font-family'] for f in
self.font_family_map.itervalues()), key=sort_key))
def path_significance(self, path):
path = os.path.normcase(os.path.abspath(path))
for i, q in enumerate(self.folders):
if path.startswith(q):
return i
return -1
def write_cache(self): def write_cache(self):
with self.cache: with self.cache:
@ -395,5 +420,3 @@ def force_rescan():
if __name__ == '__main__': if __name__ == '__main__':
font_scanner.dump_fonts() font_scanner.dump_fonts()