def change_font_in_declaration(style, old_name, new_name=None): changed = False ff = style.getProperty('font-family') if ff is not None: fams = parse_font_family(css_text(ff.propertyValue)) nfams = list( filter(None, [new_name if x == old_name else x for x in fams])) if fams != nfams: if nfams: ff.propertyValue.cssText = serialize_font_family(nfams) else: style.removeProperty(ff.name) changed = True ff = style.getProperty('font') if ff is not None: props = parse_font(css_text(ff.propertyValue)) fams = props.get('font-family') or [] nfams = list( filter(None, [new_name if x == old_name else x for x in fams])) if fams != nfams: props['font-family'] = nfams if nfams: ff.propertyValue.cssText = serialize_font(props) else: style.removeProperty(ff.name) changed = True return changed
def change_font_in_declaration(style, old_name, new_name=None): changed = False ff = style.getProperty('font-family') if ff is not None: fams = parse_font_family(css_text(ff.propertyValue)) nfams = list(filter(None, [new_name if x == old_name else x for x in fams])) if fams != nfams: if nfams: ff.propertyValue.cssText = serialize_font_family(nfams) else: style.removeProperty(ff.name) changed = True ff = style.getProperty('font') if ff is not None: props = parse_font(css_text(ff.propertyValue)) fams = props.get('font-family') or [] nfams = list(filter(None, [new_name if x == old_name else x for x in fams])) if fams != nfams: props['font-family'] = nfams if nfams: ff.propertyValue.cssText = serialize_font(props) else: style.removeProperty(ff.name) changed = True return changed
def normalize_edge(name, cssvalue): style = {} if isinstance(cssvalue, PropertyValue): primitives = [css_text(v) for v in cssvalue] else: primitives = [css_text(cssvalue)] if len(primitives) == 1: value, = primitives values = (value, value, value, value) elif len(primitives) == 2: vert, horiz = primitives values = (vert, horiz, vert, horiz) elif len(primitives) == 3: top, horiz, bottom = primitives values = (top, horiz, bottom, horiz) else: values = primitives[:4] if '-' in name: l, _, r = name.partition('-') for edge, value in zip(EDGES, values): style['%s-%s-%s' % (l, edge, r)] = value else: for edge, value in zip(EDGES, values): style['%s-%s' % (name, edge)] = value return style
def remove_links_to(container, predicate): ''' predicate must be a function that takes the arguments (name, href, fragment=None) and returns True iff the link should be removed ''' from calibre.ebooks.oeb.base import iterlinks, OEB_DOCS, OEB_STYLES, XPath, XHTML stylepath = XPath('//h:style') styleattrpath = XPath('//*[@style]') changed = set() for name, mt in iteritems(container.mime_map): removed = False if mt in OEB_DOCS: root = container.parsed(name) for el, attr, href, pos in iterlinks(root, find_links_in_css=False): hname = container.href_to_name(href, name) frag = href.partition('#')[-1] if predicate(hname, href, frag): if attr is None: el.text = None else: if el.tag == XHTML('link') or el.tag == XHTML('img'): extract(el) else: del el.attrib[attr] removed = True for tag in stylepath(root): if tag.text and (tag.get('type') or 'text/css').lower() == 'text/css': sheet = container.parse_css(tag.text) if remove_links_in_sheet( partial(container.href_to_name, base=name), sheet, predicate): tag.text = css_text(sheet) removed = True for tag in styleattrpath(root): style = tag.get('style') if style: style = container.parse_css(style, is_declaration=True) if remove_links_in_declaration( partial(container.href_to_name, base=name), style, predicate): removed = True tag.set('style', css_text(style)) elif mt in OEB_STYLES: removed = remove_links_in_sheet( partial(container.href_to_name, base=name), container.parsed(name), predicate) if removed: changed.add(name) for i in changed: container.dirty(i) return changed
def remove_property_value(prop, predicate): ''' Remove the Values that match the predicate from this property. If all values of the property would be removed, the property is removed from its parent instead. Note that this means the property must have a parent (a CSSStyleDeclaration). ''' removed_vals = list(filter(predicate, prop.propertyValue)) if len(removed_vals) == len(prop.propertyValue): prop.parent.removeProperty(prop.name) else: x = css_text(prop.propertyValue) for v in removed_vals: x = x.replace(css_text(v), '').strip() prop.propertyValue.cssText = x return bool(removed_vals)
def test_border_condensation(self): vals = 'red solid 5px' css = '; '.join('border-%s-%s: %s' % (edge, p, v) for edge in EDGES for p, v in zip(BORDER_PROPS, vals.split())) style = parseStyle(css) condense_rule(style) for e, p in product(EDGES, BORDER_PROPS): self.assertFalse(style.getProperty('border-%s-%s' % (e, p))) self.assertFalse(style.getProperty('border-%s' % e)) self.assertFalse(style.getProperty('border-%s' % p)) self.assertEqual(style.getProperty('border').value, vals) css = '; '.join('border-%s-%s: %s' % (edge, p, v) for edge in ('top', ) for p, v in zip(BORDER_PROPS, vals.split())) style = parseStyle(css) condense_rule(style) self.assertEqual(css_text(style), 'border-top: %s' % vals) css += ';' + '; '.join( 'border-%s-%s: %s' % (edge, p, v) for edge in ('right', 'left', 'bottom') for p, v in zip(BORDER_PROPS, vals.replace('red', 'green').split())) style = parseStyle(css) condense_rule(style) self.assertEqual(len(style.getProperties()), 4) self.assertEqual(style.getProperty('border-top').value, vals) self.assertEqual( style.getProperty('border-left').value, vals.replace('red', 'green'))
def change_font(container, old_name, new_name=None): ''' Change a font family from old_name to new_name. Changes all occurrences of the font family in stylesheets, style tags and style attributes. If the old_name refers to an embedded font, it is removed. You can set new_name to None to remove the font family instead of changing it. ''' changed = False for name, mt in tuple(iteritems(container.mime_map)): 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 = css_text(style).strip().rstrip(';').strip() if style: elem.set('style', style) else: del elem.attrib['style'] container.dirty(name) changed = True return changed
def normalize_simple_composition(name, cssvalue, composition, check_inherit=True): if check_inherit and css_text(cssvalue) == 'inherit': style = {k:'inherit' for k in composition} else: style = {k:DEFAULTS[k] for k in composition} try: primitives = [css_text(v) for v in cssvalue] except TypeError: primitives = [css_text(cssvalue)] while primitives: value = primitives.pop() for key in composition: if cssprofiles.validate(key, value): style[key] = value break return style
def normalize_style_declaration(decl, sheet_name): ans = {} for prop in iterdeclaration(decl): if prop.name == 'font-family': # Needed because of https://bitbucket.org/cthedot/cssutils/issues/66/incorrect-handling-of-spaces-in-font prop.propertyValue.cssText = serialize_font_family(parse_font_family(css_text(prop.propertyValue))) ans[prop.name] = Values(prop.propertyValue, sheet_name, prop.priority) return ans
def font_family_data_from_sheet(sheet, families): for rule in sheet.cssRules: if rule.type == rule.STYLE_RULE: font_family_data_from_declaration(rule.style, families) elif rule.type == rule.FONT_FACE_RULE: ff = rule.style.getProperty('font-family') if ff is not None: for f in parse_font_family(css_text(ff.propertyValue)): families[f] = True
def remove_links_to(container, predicate): ''' predicate must be a function that takes the arguments (name, href, fragment=None) and returns True iff the link should be removed ''' from calibre.ebooks.oeb.base import iterlinks, OEB_DOCS, OEB_STYLES, XPath, XHTML stylepath = XPath('//h:style') styleattrpath = XPath('//*[@style]') changed = set() for name, mt in iteritems(container.mime_map): removed = False if mt in OEB_DOCS: root = container.parsed(name) for el, attr, href, pos in iterlinks(root, find_links_in_css=False): hname = container.href_to_name(href, name) frag = href.partition('#')[-1] if predicate(hname, href, frag): if attr is None: el.text = None else: if el.tag == XHTML('link') or el.tag == XHTML('img'): extract(el) else: del el.attrib[attr] removed = True for tag in stylepath(root): if tag.text and (tag.get('type') or 'text/css').lower() == 'text/css': sheet = container.parse_css(tag.text) if remove_links_in_sheet(partial(container.href_to_name, base=name), sheet, predicate): tag.text = css_text(sheet) removed = True for tag in styleattrpath(root): style = tag.get('style') if style: style = container.parse_css(style, is_declaration=True) if remove_links_in_declaration(partial(container.href_to_name, base=name), style, predicate): removed = True tag.set('style', css_text(style)) elif mt in OEB_STYLES: removed = remove_links_in_sheet(partial(container.href_to_name, base=name), container.parsed(name), predicate) if removed: changed.add(name) tuple(map(container.dirty, changed)) return changed
def __call__(self, oeb): if not self.body_font_family: return None if not self.href: iid, href = oeb.manifest.generate(u'page_styles', u'page_styles.css') rules = [css_text(x) for x in self.rules] rules = u'\n\n'.join(rules) sheet = css_parser.parseString(rules, validate=False) self.href = oeb.manifest.add(iid, href, guess_type(href)[0], data=sheet).href return self.href
def collect_font_face_rules(self, container, processed, spine_name, sheet, sheet_name): if sheet_name in processed: sheet_rules = processed[sheet_name] else: sheet_rules = [] if sheet_name != spine_name: processed[sheet_name] = sheet_rules for rule, base_name, rule_index in iterrules( container, sheet_name, rules=sheet, rule_type='FONT_FACE_RULE'): cssdict = {} for prop in iterdeclaration(rule.style): if prop.name == 'font-family': cssdict['font-family'] = [ icu_lower(x) for x in parse_font_family( css_text(prop.propertyValue)) ] elif prop.name.startswith('font-'): cssdict[prop.name] = prop.propertyValue[0].value elif prop.name == 'src': for val in prop.propertyValue: x = val.value fname = container.href_to_name(x, sheet_name) if container.has_name(fname): cssdict['src'] = fname break else: container.log.warn( 'The @font-face rule refers to a font file that does not exist in the book: %s' % css_text(prop.propertyValue)) if 'src' not in cssdict: continue ff = cssdict.get('font-family') if not ff or ff[0] in bad_fonts: continue normalize_font_properties(cssdict) prepare_font_rule(cssdict) sheet_rules.append(cssdict) self.font_rule_map[spine_name].extend(sheet_rules)
def font_family_data_from_declaration(style, families): font_families = [] f = style.getProperty('font') if f is not None: f = normalize_font(f.propertyValue, font_family_as_list=True).get('font-family', None) if f is not None: font_families = [unquote(x) for x in f] f = style.getProperty('font-family') if f is not None: font_families = parse_font_family(css_text(f.propertyValue)) for f in font_families: families[f] = families.get(f, False)
def process_fonts(self): ''' Make sure all fonts are embeddable. Also remove some fonts that cause problems. ''' from calibre.ebooks.oeb.base import urlnormalize, css_text from calibre.utils.fonts.utils import remove_embed_restriction processed = set() for item in list(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: try: s = rule.style src = s.getProperty('src').propertyValue[0].uri except: continue path = item.abshref(src) ff = self.oeb.manifest.hrefs.get(urlnormalize(path), None) if ff is None: continue raw = nraw = ff.data if path not in processed: processed.add(path) try: nraw = remove_embed_restriction(raw) except: continue if nraw != raw: ff.data = nraw self.oeb.container.write(path, nraw) elif iswindows and rule.type == rule.STYLE_RULE: from tinycss.fonts3 import parse_font_family, serialize_font_family s = rule.style f = s.getProperty(u'font-family') if f is not None: font_families = parse_font_family( css_text(f.propertyValue)) ff = [ x for x in font_families if x.lower() != u'courier' ] if len(ff) != len(font_families): if 'courier' not in self.filtered_font_warnings: # See https://bugs.launchpad.net/bugs/1665835 self.filtered_font_warnings.add(u'courier') self.log.warn( u'Removing courier font family as it does not render on windows' ) f.propertyValue.cssText = serialize_font_family( ff or [u'monospace'])
def sort_css(self): from calibre.gui2.dialogs.confirm_delete import confirm if confirm(_('Sorting CSS rules can in rare cases change the effective styles applied to the book.' ' Are you sure you want to proceed?'), 'edit-book-confirm-sort-css', parent=self, config_set=tprefs): c = self.textCursor() c.beginEditBlock() c.movePosition(c.Start), c.movePosition(c.End, c.KeepAnchor) text = unicode_type(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') from calibre.ebooks.oeb.polish.css import sort_sheet text = css_text(sort_sheet(current_container(), text)) c.insertText(text) c.movePosition(c.Start) c.endEditBlock() self.setTextCursor(c)
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 = {x for x in parse_font_family(css_text(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 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 = parse_font_family( css_text(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_type(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 normalize_font(cssvalue, font_family_as_list=False): # See https://developer.mozilla.org/en-US/docs/Web/CSS/font composition = font_composition val = css_text(cssvalue) if val == 'inherit': ans = {k:'inherit' for k in composition} elif val in {'caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar'}: ans = {k:DEFAULTS[k] for k in composition} else: ans = {k:DEFAULTS[k] for k in composition} ans.update(parse_font(val)) if font_family_as_list: if isinstance(ans['font-family'], string_or_bytes): ans['font-family'] = [x.strip() for x in ans['font-family'].split(',')] else: if not isinstance(ans['font-family'], string_or_bytes): ans['font-family'] = serialize_font_family(ans['font-family']) return ans
def process_fonts(self): ''' Make sure all fonts are embeddable. Also remove some fonts that cause problems. ''' from calibre.ebooks.oeb.base import urlnormalize, css_text from calibre.utils.fonts.utils import remove_embed_restriction processed = set() for item in list(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: try: s = rule.style src = s.getProperty('src').propertyValue[0].uri except: continue path = item.abshref(src) ff = self.oeb.manifest.hrefs.get(urlnormalize(path), None) if ff is None: continue raw = nraw = ff.data if path not in processed: processed.add(path) try: nraw = remove_embed_restriction(raw) except: continue if nraw != raw: ff.data = nraw self.oeb.container.write(path, nraw) elif iswindows and rule.type == rule.STYLE_RULE: from tinycss.fonts3 import parse_font_family, serialize_font_family s = rule.style f = s.getProperty(u'font-family') if f is not None: font_families = parse_font_family(css_text(f.propertyValue)) ff = [x for x in font_families if x.lower() != u'courier'] if len(ff) != len(font_families): if 'courier' not in self.filtered_font_warnings: # See https://bugs.launchpad.net/bugs/1665835 self.filtered_font_warnings.add(u'courier') self.log.warn(u'Removing courier font family as it does not render on windows') f.propertyValue.cssText = serialize_font_family(ff or [u'monospace'])
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 = parse_font_family(css_text(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_type(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 test_border_condensation(self): vals = 'red solid 5px' css = '; '.join('border-%s-%s: %s' % (edge, p, v) for edge in EDGES for p, v in zip(BORDER_PROPS, vals.split())) style = parseStyle(css) condense_rule(style) for e, p in product(EDGES, BORDER_PROPS): self.assertFalse(style.getProperty('border-%s-%s' % (e, p))) self.assertFalse(style.getProperty('border-%s' % e)) self.assertFalse(style.getProperty('border-%s' % p)) self.assertEqual(style.getProperty('border').value, vals) css = '; '.join('border-%s-%s: %s' % (edge, p, v) for edge in ('top',) for p, v in zip(BORDER_PROPS, vals.split())) style = parseStyle(css) condense_rule(style) self.assertEqual(css_text(style), 'border-top: %s' % vals) css += ';' + '; '.join('border-%s-%s: %s' % (edge, p, v) for edge in ('right', 'left', 'bottom') for p, v in zip(BORDER_PROPS, vals.replace('red', 'green').split())) style = parseStyle(css) condense_rule(style) self.assertEqual(len(style.getProperties()), 4) self.assertEqual(style.getProperty('border-top').value, vals) self.assertEqual(style.getProperty('border-left').value, vals.replace('red', 'green'))
def collect_global_css(self): global_css = defaultdict(list) for item in self.items: stylizer = self.stylizers[item] if float(self.context.margin_top) >= 0: stylizer.page_rule['margin-top'] = '%gpt'%\ float(self.context.margin_top) if float(self.context.margin_bottom) >= 0: stylizer.page_rule['margin-bottom'] = '%gpt'%\ float(self.context.margin_bottom) items = sorted(stylizer.page_rule.items()) css = ';\n'.join("%s: %s" % (key, val) for key, val in items) css = ('@page {\n%s\n}\n' % css) if items else '' rules = [ css_text(r) for r in stylizer.font_face_rules + self.embed_font_rules ] raw = '\n\n'.join(rules) css += '\n\n' + raw global_css[css].append(item) gc_map = {} manifest = self.oeb.manifest for css in global_css: href = None if css.strip(): id_, href = manifest.generate('page_css', 'page_styles.css') sheet = css_parser.parseString(css, validate=False) if self.transform_css_rules: from calibre.ebooks.css_transform_rules import transform_sheet transform_sheet(self.transform_css_rules, sheet) manifest.add(id_, href, CSS_MIME, data=sheet) gc_map[css] = href ans = {} for css, items in iteritems(global_css): for item in items: ans[item] = gc_map[css] return ans
def collect_global_css(self): global_css = defaultdict(list) for item in self.items: stylizer = self.stylizers[item] if float(self.context.margin_top) >= 0: stylizer.page_rule['margin-top'] = '%gpt'%\ float(self.context.margin_top) if float(self.context.margin_bottom) >= 0: stylizer.page_rule['margin-bottom'] = '%gpt'%\ float(self.context.margin_bottom) items = sorted(stylizer.page_rule.items()) css = ';\n'.join("%s: %s" % (key, val) for key, val in items) css = ('@page {\n%s\n}\n'%css) if items else '' rules = [css_text(r) for r in stylizer.font_face_rules + self.embed_font_rules] raw = '\n\n'.join(rules) css += '\n\n' + raw global_css[css].append(item) gc_map = {} manifest = self.oeb.manifest for css in global_css: href = None if css.strip(): id_, href = manifest.generate('page_css', 'page_styles.css') sheet = css_parser.parseString(css, validate=False) if self.transform_css_rules: from calibre.ebooks.css_transform_rules import transform_sheet transform_sheet(self.transform_css_rules, sheet) manifest.add(id_, href, CSS_MIME, data=sheet) gc_map[css] = href ans = {} for css, items in iteritems(global_css): for item in items: ans[item] = gc_map[css] return ans
def __init__(self, oeb, opts, replace_previous_inline_toc=True, ignore_existing_toc=False): self.oeb, self.opts, self.log = oeb, opts, oeb.log self.title = opts.toc_title or DEFAULT_TITLE self.at_start = opts.mobi_toc_at_start self.generated_item = None self.added_toc_guide_entry = False self.has_toc = oeb.toc and oeb.toc.count() > 1 self.tocitem = tocitem = None if replace_previous_inline_toc: tocitem = self.tocitem = find_previous_calibre_inline_toc(oeb) if ignore_existing_toc and 'toc' in oeb.guide: oeb.guide.remove('toc') if 'toc' in oeb.guide: # Remove spurious toc entry from guide if it is not in spine or it # does not have any hyperlinks href = urlnormalize(oeb.guide['toc'].href.partition('#')[0]) if href in oeb.manifest.hrefs: item = oeb.manifest.hrefs[href] if (hasattr(item.data, 'xpath') and XPath('//h:a[@href]')(item.data)): if oeb.spine.index(item) < 0: oeb.spine.add(item, linear=False) return elif self.has_toc: oeb.guide.remove('toc') else: oeb.guide.remove('toc') if (not self.has_toc or 'toc' in oeb.guide or opts.no_inline_toc or getattr(opts, 'mobi_passthrough', False)): return self.log('\tGenerating in-line ToC') embed_css = '' s = getattr(oeb, 'store_embed_font_rules', None) if getattr(s, 'body_font_family', None): css = [css_text(x) for x in s.rules] + [ 'body { font-family: %s }'%s.body_font_family] embed_css = '\n\n'.join(css) root = safe_xml_fromstring(TEMPLATE.format(xhtmlns=XHTML_NS, title=self.title, embed_css=embed_css, extra_css=(opts.extra_css or ''))) parent = XPath('//h:ul')(root)[0] parent.text = '\n\t' for child in self.oeb.toc: self.process_toc_node(child, parent) if tocitem is not None: href = tocitem.href if oeb.spine.index(tocitem) > -1: oeb.spine.remove(tocitem) tocitem.data = root else: id, href = oeb.manifest.generate('contents', 'contents.xhtml') tocitem = self.generated_item = oeb.manifest.add(id, href, XHTML_MIME, data=root) if self.at_start: oeb.spine.insert(0, tocitem, linear=True) else: oeb.spine.add(tocitem, linear=False) oeb.guide.add('toc', 'Table of Contents', href)
def __init__(self, oeb, opts, replace_previous_inline_toc=True, ignore_existing_toc=False): self.oeb, self.opts, self.log = oeb, opts, oeb.log self.title = opts.toc_title or DEFAULT_TITLE self.at_start = opts.mobi_toc_at_start self.generated_item = None self.added_toc_guide_entry = False self.has_toc = oeb.toc and oeb.toc.count() > 1 self.tocitem = tocitem = None if replace_previous_inline_toc: tocitem = self.tocitem = find_previous_calibre_inline_toc(oeb) if ignore_existing_toc and 'toc' in oeb.guide: oeb.guide.remove('toc') if 'toc' in oeb.guide: # Remove spurious toc entry from guide if it is not in spine or it # does not have any hyperlinks href = urlnormalize(oeb.guide['toc'].href.partition('#')[0]) if href in oeb.manifest.hrefs: item = oeb.manifest.hrefs[href] if (hasattr(item.data, 'xpath') and XPath('//h:a[@href]')(item.data)): if oeb.spine.index(item) < 0: oeb.spine.add(item, linear=False) return elif self.has_toc: oeb.guide.remove('toc') else: oeb.guide.remove('toc') if (not self.has_toc or 'toc' in oeb.guide or opts.no_inline_toc or getattr(opts, 'mobi_passthrough', False)): return self.log('\tGenerating in-line ToC') embed_css = '' s = getattr(oeb, 'store_embed_font_rules', None) if getattr(s, 'body_font_family', None): css = [css_text(x) for x in s.rules] + [ 'body { font-family: %s }'%s.body_font_family] embed_css = '\n\n'.join(css) root = etree.fromstring(TEMPLATE.format(xhtmlns=XHTML_NS, title=self.title, embed_css=embed_css, extra_css=(opts.extra_css or ''))) parent = XPath('//h:ul')(root)[0] parent.text = '\n\t' for child in self.oeb.toc: self.process_toc_node(child, parent) if tocitem is not None: href = tocitem.href if oeb.spine.index(tocitem) > -1: oeb.spine.remove(tocitem) tocitem.data = root else: id, href = oeb.manifest.generate('contents', 'contents.xhtml') tocitem = self.generated_item = oeb.manifest.add(id, href, XHTML_MIME, data=root) if self.at_start: oeb.spine.insert(0, tocitem, linear=True) else: oeb.spine.add(tocitem, linear=False) oeb.guide.add('toc', 'Table of Contents', href)
def subset_all_fonts(container, font_stats, report): remove = set() total_old = total_new = 0 changed = False for name, mt in iter_subsettable_fonts(container): chars = font_stats.get(name, set()) with container.open(name, 'rb') as f: f.seek(0, os.SEEK_END) total_old += f.tell() if not chars: remove.add(name) report(_('Removed unused font: %s') % name) continue with container.open(name, 'r+b') as f: raw = f.read() try: font_name = get_font_names(raw)[-1] except Exception as e: container.log.warning( 'Corrupted font: %s, ignoring. Error: %s' % (name, as_unicode(e))) continue warnings = [] container.log('Subsetting font: %s' % (font_name or name)) try: nraw, old_sizes, new_sizes = subset(raw, chars, warnings=warnings) except UnsupportedFont as e: container.log.warning( 'Unsupported font: %s, ignoring. Error: %s' % (name, as_unicode(e))) continue for w in warnings: container.log.warn(w) olen = sum(itervalues(old_sizes)) nlen = sum(itervalues(new_sizes)) total_new += len(nraw) if nlen == olen: report(_('The font %s was already subset') % font_name) else: report( _('Decreased the font {0} to {1} of its original size'). format(font_name, ('%.1f%%' % (nlen / olen * 100)))) changed = True f.seek(0), f.truncate(), f.write(nraw) for name in remove: container.remove_item(name) changed = True if remove: for name, mt in iteritems(container.mime_map): if mt in OEB_STYLES: sheet = container.parsed(name) if remove_font_face_rules(container, sheet, remove, name): container.dirty(name) elif mt in OEB_DOCS: for style in XPath('//h:style')(container.parsed(name)): if style.get('type', 'text/css') == 'text/css' and style.text: sheet = container.parse_css(style.text, name) if remove_font_face_rules(container, sheet, remove, name): style.text = css_text(sheet) container.dirty(name) if total_old > 0: report( _('Reduced total font size to %.1f%% of original') % (total_new / total_old * 100)) else: report(_('No embedded fonts found')) return changed
def collect_font_face_rules(self, container, processed, spine_name, sheet, sheet_name): if sheet_name in processed: sheet_rules = processed[sheet_name] else: sheet_rules = [] if sheet_name != spine_name: processed[sheet_name] = sheet_rules for rule, base_name, rule_index in iterrules(container, sheet_name, rules=sheet, rule_type='FONT_FACE_RULE'): cssdict = {} for prop in iterdeclaration(rule.style): if prop.name == 'font-family': cssdict['font-family'] = [icu_lower(x) for x in parse_font_family(css_text(prop.propertyValue))] elif prop.name.startswith('font-'): cssdict[prop.name] = prop.propertyValue[0].value elif prop.name == 'src': for val in prop.propertyValue: x = val.value fname = container.href_to_name(x, sheet_name) if container.has_name(fname): cssdict['src'] = fname break else: container.log.warn('The @font-face rule refers to a font file that does not exist in the book: %s' % css_text(prop.propertyValue)) if 'src' not in cssdict: continue ff = cssdict.get('font-family') if not ff or ff[0] in bad_fonts: continue normalize_font_properties(cssdict) prepare_font_rule(cssdict) sheet_rules.append(cssdict) self.font_rule_map[spine_name].extend(sheet_rules)
def css(d): return css_text(d).replace('\n', ' ')
def subset_all_fonts(container, font_stats, report): remove = set() total_old = total_new = 0 changed = False for name, mt in iter_subsettable_fonts(container): chars = font_stats.get(name, set()) with container.open(name, 'rb') as f: f.seek(0, os.SEEK_END) total_old += f.tell() if not chars: remove.add(name) report(_('Removed unused font: %s')%name) continue with container.open(name, 'r+b') as f: raw = f.read() try: font_name = get_font_names(raw)[-1] except Exception as e: container.log.warning( 'Corrupted font: %s, ignoring. Error: %s'%( name, as_unicode(e))) continue warnings = [] container.log('Subsetting font: %s'%(font_name or name)) try: nraw, old_sizes, new_sizes = subset(raw, chars, warnings=warnings) except UnsupportedFont as e: container.log.warning( 'Unsupported font: %s, ignoring. Error: %s'%( name, as_unicode(e))) continue for w in warnings: container.log.warn(w) olen = sum(itervalues(old_sizes)) nlen = sum(itervalues(new_sizes)) total_new += len(nraw) if nlen == olen: report(_('The font %s was already subset')%font_name) else: report(_('Decreased the font {0} to {1} of its original size').format( font_name, ('%.1f%%' % (nlen/olen * 100)))) changed = True f.seek(0), f.truncate(), f.write(nraw) for name in remove: container.remove_item(name) changed = True if remove: for name, mt in iteritems(container.mime_map): if mt in OEB_STYLES: sheet = container.parsed(name) if remove_font_face_rules(container, sheet, remove, name): container.dirty(name) elif mt in OEB_DOCS: for style in XPath('//h:style')(container.parsed(name)): if style.get('type', 'text/css') == 'text/css' and style.text: sheet = container.parse_css(style.text, name) if remove_font_face_rules(container, sheet, remove, name): style.text = css_text(sheet) container.dirty(name) if total_old > 0: report(_('Reduced total font size to %.1f%% of original')%( total_new/total_old*100)) else: report(_('No embedded fonts found')) return changed
def cssText(self): ' This will return either a string or a tuple of strings ' if len(self) == 1: return css_text(self[0]) return tuple(css_text(x) for x in self)