Source code for calibre.ebooks.oeb.polish.fonts

#!/usr/bin/env python


__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'

from tinycss.fonts3 import parse_font, parse_font_family, serialize_font, serialize_font_family

from calibre.ebooks.oeb.base import css_text
from calibre.ebooks.oeb.normalize_css import normalize_font
from calibre.ebooks.oeb.polish.container import OEB_DOCS, OEB_STYLES
from polyglot.builtins import iteritems


def unquote(x):
    if x and len(x) > 1 and x[0] == x[-1] and x[0] in ('"', "'"):
        x = x[1:-1]
    return x


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 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 font_family_data(container):
    families = {}
    for name, mt in iteritems(container.mime_map):
        if mt in OEB_STYLES:
            sheet = container.parsed(name)
            font_family_data_from_sheet(sheet, families)
        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)
                    font_family_data_from_sheet(sheet, families)
            for style in root.xpath('//*/@style'):
                if style:
                    style = container.parse_css(style, is_declaration=True)
                    font_family_data_from_declaration(style, families)
    return families


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 remove_embedded_font(container, sheet, rule, sheet_name):
    src = getattr(rule.style.getProperty('src'), 'value', None)
    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 = {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


[docs] 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