%PDF- %PDF-
Direktori : /usr/lib/calibre/calibre/ebooks/oeb/polish/ |
Current File : //usr/lib/calibre/calibre/ebooks/oeb/polish/embed.py |
#!/usr/bin/env python3 __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' __docformat__ = 'restructuredtext en' import sys from lxml import etree from calibre import prints from calibre.ebooks.oeb.base import XHTML from calibre.utils.filenames import ascii_filename from polyglot.builtins import iteritems, itervalues, string_or_bytes props = {'font-family':None, 'font-weight':'normal', 'font-style':'normal', 'font-stretch':'normal'} def matching_rule(font, rules): ff = font['font-family'] if not isinstance(ff, string_or_bytes): ff = tuple(ff)[0] family = icu_lower(ff) wt = font['font-weight'] style = font['font-style'] stretch = font['font-stretch'] for rule in rules: if rule['font-style'] == style and rule['font-stretch'] == stretch and rule['font-weight'] == wt: ff = rule['font-family'] if not isinstance(ff, string_or_bytes): ff = tuple(ff)[0] if icu_lower(ff) == family: return rule def format_fallback_match_report(matched_font, font_family, css_font, report): msg = _('Could not find a font in the "%s" family exactly matching the CSS font specification,' ' will embed a fallback font instead. CSS font specification:') % font_family msg += '\n\n* font-weight: %s' % css_font.get('font-weight', 'normal') msg += '\n* font-style: %s' % css_font.get('font-style', 'normal') msg += '\n* font-stretch: %s' % css_font.get('font-stretch', 'normal') msg += '\n\n' + _('Matched font specification:') msg += '\n' + matched_font['path'] msg += '\n\n* font-weight: %s' % matched_font.get('font-weight', 'normal').strip() msg += '\n* font-style: %s' % matched_font.get('font-style', 'normal').strip() msg += '\n* font-stretch: %s' % matched_font.get('font-stretch', 'normal').strip() report(msg) report('') def stretch_as_number(val): try: return int(val) except Exception: pass try: return ('ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed', 'normal', 'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded').index(val) except Exception: return 4 # normal def filter_by_stretch(fonts, val): val = stretch_as_number(val) stretch_map = [stretch_as_number(f['font-stretch']) for f in fonts] equal = [f for i, f in enumerate(fonts) if stretch_map[i] == val] if equal: return equal condensed = [i for i in range(len(fonts)) if stretch_map[i] <= 4] expanded = [i for i in range(len(fonts)) if stretch_map[i] > 4] if val <= 4: candidates = condensed or expanded else: candidates = expanded or condensed distance_map = {i:abs(stretch_map[i] - val) for i in candidates} min_dist = min(itervalues(distance_map)) return [fonts[i] for i in candidates if distance_map[i] == min_dist] def filter_by_style(fonts, val): order = { 'normal':('normal', 'oblique', 'italic'), 'italic':('italic', 'oblique', 'normal'), 'oblique':('oblique', 'italic', 'normal'), } if val not in order: val = 'normal' for q in order[val]: ans = [f for f in fonts if f['font-style'] == q] if ans: return ans return fonts def weight_as_number(wt): try: return int(wt) except Exception: return {'normal':400, 'bold':700}.get(wt, 400) def filter_by_weight(fonts, val): val = weight_as_number(val) weight_map = [weight_as_number(f['font-weight']) for f in fonts] equal = [f for i, f in enumerate(fonts) if weight_map[i] == val] if equal: return equal rmap = {w:i for i, w in enumerate(weight_map)} below = [i for i in range(len(fonts)) if weight_map[i] < val] above = [i for i in range(len(fonts)) if weight_map[i] > val] if val < 400: candidates = below or above elif val > 500: candidates = above or below elif val == 400: if 500 in rmap: return [fonts[rmap[500]]] candidates = below or above else: if 400 in rmap: return [fonts[rmap[400]]] candidates = below or above distance_map = {i:abs(weight_map[i] - val) for i in candidates} min_dist = min(itervalues(distance_map)) return [fonts[i] for i in candidates if distance_map[i] == min_dist] def find_matching_font(fonts, weight='normal', style='normal', stretch='normal'): # See https://www.w3.org/TR/css-fonts-3/#font-style-matching # We dont implement the unicode character range testing # We also dont implement bolder, lighter for f, q in ((filter_by_stretch, stretch), (filter_by_style, style), (filter_by_weight, weight)): fonts = f(fonts, q) if len(fonts) == 1: return fonts[0] return fonts[0] def do_embed(container, font, report): from calibre.utils.fonts.scanner import font_scanner report('Embedding font {} from {}'.format(font['full_name'], font['path'])) data = font_scanner.get_font_data(font) fname = font['full_name'] ext = 'otf' if font['is_otf'] else 'ttf' fname = ascii_filename(fname).replace(' ', '-').replace('(', '').replace(')', '') item = container.generate_item('fonts/%s.%s'%(fname, ext), id_prefix='font') name = container.href_to_name(item.get('href'), container.opf_name) with container.open(name, 'wb') as out: out.write(data) href = container.name_to_href(name) rule = {k:font.get(k, v) for k, v in iteritems(props)} rule['src'] = 'url(%s)' % href rule['name'] = name return rule def embed_font(container, font, all_font_rules, report, warned): rule = matching_rule(font, all_font_rules) ff = font['font-family'] if not isinstance(ff, string_or_bytes): ff = ff[0] if rule is None: from calibre.utils.fonts.scanner import font_scanner, NoFonts if ff in warned: return try: fonts = font_scanner.fonts_for_family(ff) except NoFonts: report(_('Failed to find fonts for family: %s, not embedding') % ff) warned.add(ff) return wt = weight_as_number(font.get('font-weight')) 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'): return do_embed(container, f, report) f = find_matching_font(fonts, font.get('font-weight', '400'), font.get('font-style', 'normal'), font.get('font-stretch', 'normal')) wkey = ('fallback-font', ff, wt, font.get('font-style'), font.get('font-stretch')) if wkey not in warned: warned.add(wkey) format_fallback_match_report(f, ff, font, report) return do_embed(container, f, report) else: name = rule['src'] href = container.name_to_href(name) rule = {k:ff if k == 'font-family' else rule.get(k, v) for k, v in iteritems(props)} rule['src'] = 'url(%s)' % href rule['name'] = name return rule def font_key(font): return tuple(map(font.get, 'font-family font-weight font-style font-stretch'.split())) def embed_all_fonts(container, stats, report): all_font_rules = tuple(itervalues(stats.all_font_rules)) warned = set() rules, nrules = [], {} modified = set() for path in container.spine_items: name = container.abspath_to_name(path) fu = stats.font_usage_map.get(name, None) fs = stats.font_spec_map.get(name, None) fr = stats.font_rule_map.get(name, None) if None in (fs, fu, fr): continue fs = {icu_lower(x) for x in fs} for font in itervalues(fu): if icu_lower(font['font-family']) not in fs: continue rule = matching_rule(font, fr) if rule is None: # This font was not already embedded in this HTML file, before # processing started key = font_key(font) rule = nrules.get(key) if rule is None: rule = embed_font(container, font, all_font_rules, report, warned) if rule is not None: rules.append(rule) nrules[key] = rule modified.add(name) stats.font_stats[rule['name']] = font['text'] else: # This font was previously embedded by this code, update its stats stats.font_stats[rule['name']] |= font['text'] modified.add(name) if not rules: report(_('No embeddable fonts found')) return False # Write out CSS rules = [';\n\t'.join('{}: {}'.format( k, '"%s"' % v if k == 'font-family' else v) for k, v in iteritems(rulel) if (k in props and props[k] != v and v != '400') or k == 'src') for rulel in rules] css = '\n\n'.join(['@font-face {\n\t%s\n}' % r for r in rules]) item = container.generate_item('fonts.css', id_prefix='font_embed') name = container.href_to_name(item.get('href'), container.opf_name) with container.open(name, 'wb') as out: out.write(css.encode('utf-8')) # Add link to CSS in all files that need it for spine_name in modified: root = container.parsed(spine_name) try: head = root.xpath('//*[local-name()="head"][1]')[0] except IndexError: head = root.makeelement(XHTML('head')) root.insert(0, head) head.tail = '\n' head.text = '\n ' href = container.name_to_href(name, spine_name) etree.SubElement(head, XHTML('link'), rel='stylesheet', type='text/css', href=href).tail = '\n' container.dirty(spine_name) return True if __name__ == '__main__': from calibre.ebooks.oeb.polish.container import get_container from calibre.ebooks.oeb.polish.stats import StatsCollector from calibre.utils.logging import default_log default_log.filter_level = default_log.DEBUG inbook = sys.argv[-1] ebook = get_container(inbook, default_log) report = [] stats = StatsCollector(ebook, do_embed=True) embed_all_fonts(ebook, stats, report.append) outbook, ext = inbook.rpartition('.')[0::2] outbook += '_subset.'+ext ebook.commit(outbook) prints('\nReport:') for msg in report: prints(msg) print() prints('Output written to:', outbook)