diff --git a/src/calibre/ebooks/oeb/polish/check/fonts.py b/src/calibre/ebooks/oeb/polish/check/fonts.py new file mode 100644 index 0000000000..972052c451 --- /dev/null +++ b/src/calibre/ebooks/oeb/polish/check/fonts.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2013, Kovid Goyal ' + +from cssutils.css import CSSRule + +from calibre import force_unicode +from calibre.constants import plugins +from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES +from calibre.ebooks.oeb.polish.check.base import BaseError, WARN +from calibre.ebooks.oeb.polish.container import guess_type, OEB_FONTS +from calibre.ebooks.oeb.polish.pretty import pretty_script_or_style +from calibre.utils.fonts.utils import get_all_font_names + +woff = plugins['woff'][0] + +class InvalidFont(BaseError): + + HELP = _('This font could not be processed. It most likely will' + ' not work in an ebook reader, either') + +def fix_declaration(style, css_name, font_name): + ff = style.getPropertyCSSValue('font-family') + changed = False + if ff is not None: + for i in xrange(ff.length): + val = ff.item(i) + if val.value and val.value.lower() == css_name.lower(): + val.value = font_name + changed = True + return changed + +def fix_sheet(sheet, css_name, font_name): + changed = False + for rule in sheet.cssRules: + if rule.type in (CSSRule.FONT_FACE_RULE, CSSRule.STYLE_RULE): + if fix_declaration(rule.style, css_name, font_name): + changed = True + return changed + +class FontAliasing(BaseError): + + level = WARN + + def __init__(self, font_name, css_name, name, line): + BaseError.__init__(self, _('The CSS font-family name %s does not match the actual font name %s') % (css_name, font_name), name, line) + self.HELP = _('The font family name specified in the CSS @font-face rule: "{0}" does' + ' not match the font name inside the actual font file: "{1}". This can' + ' cause problems in some viewers. You should change the CSS font name' + ' to match the actual font name.').format(css_name, font_name) + self.INDIVIDUAL_FIX = _('Change the font name {0} to {1} everywhere').format(css_name, font_name) + self.font_name, self.css_name = font_name, css_name + + def __call__(self, container): + changed = False + for name, mt in container.mime_map.iteritems(): + if mt in OEB_STYLES: + sheet = container.parsed(name) + if fix_sheet(sheet, self.css_name, self.font_name): + container.dirty(name) + changed = True + elif mt in OEB_DOCS: + for style in container.parsed(name).xpath('//*[local-name()="style"]'): + if style.get('type', 'text/css') == 'text/css': + sheet = container.parse_css(style.text) + if fix_sheet(sheet, self.css_name, self.font_name): + style.text = force_unicode(sheet.cssText, 'utf-8') + pretty_script_or_style(container, style) + container.dirty(name) + changed = True + for elem in container.parsed(name).xpath('//*[@style and contains(@style, "font-family")]'): + style = container.parse_css(elem.get('style'), is_declaration=True) + if fix_declaration(style, self.css_name, self.font_name): + elem.set('style', force_unicode(style.cssText, 'utf-8').replace('\n', ' ')) + container.dirty(name) + changed = True + return changed + +def check_fonts(container): + font_map = {} + errors = [] + for name, mt in container.mime_map.iteritems(): + if mt in OEB_FONTS: + raw = container.raw_data(name) + if mt == guess_type('a.woff'): + try: + raw = woff.from_woff(raw) + except Exception as e: + errors.append(InvalidFont(_('Not a valid WOFF font: %s') % e, name)) + continue + try: + name_map = get_all_font_names(raw) + except Exception as e: + errors.append(InvalidFont(_('Not a valid font: %s') % e, name)) + continue + font_map[name] = name_map.get('family_name', None) or name_map.get('preferred_family_name', None) or name_map.get('wws_family_name', None) + + sheets = [] + for name, mt in container.mime_map.iteritems(): + if mt in OEB_STYLES: + sheets.append((name, container.parsed(name), None)) + elif mt in OEB_DOCS: + for style in container.parsed(name).xpath('//*[local-name()="style"]'): + if style.get('type', 'text/css') == 'text/css': + sheets.append((name, container.parse_css(style.text), style.sourceline)) + + for name, sheet, line_offset in sheets: + for rule in sheet.cssRules.rulesOfType(CSSRule.FONT_FACE_RULE): + src = rule.style.getPropertyCSSValue('src') + if src is not None and src.length > 0: + href = getattr(src.item(0), 'uri', None) + if href is not None: + fname = container.href_to_name(href, name) + font_name = font_map.get(fname, None) + if font_name is None: + continue + ff = rule.style.getPropertyCSSValue('font-family') + if ff is not None and ff.length > 0: + ff = getattr(ff.item(0), 'value', None) + if ff is not None and ff != font_name: + errors.append(FontAliasing(font_name, ff, name, line_offset)) + + return errors diff --git a/src/calibre/ebooks/oeb/polish/check/main.py b/src/calibre/ebooks/oeb/polish/check/main.py index 16c1a7dafb..4bc61e4deb 100644 --- a/src/calibre/ebooks/oeb/polish/check/main.py +++ b/src/calibre/ebooks/oeb/polish/check/main.py @@ -15,6 +15,7 @@ from calibre.ebooks.oeb.polish.check.base import run_checkers from calibre.ebooks.oeb.polish.check.parsing import check_xml_parsing, check_css_parsing, fix_style_tag from calibre.ebooks.oeb.polish.check.images import check_raster_images from calibre.ebooks.oeb.polish.check.links import check_links +from calibre.ebooks.oeb.polish.check.fonts import check_fonts XML_TYPES = frozenset(map(guess_type, ('a.xml', 'a.svg', 'a.opf', 'a.ncx'))) @@ -50,6 +51,7 @@ def run_checks(container): errors.extend(check_css_parsing(name, style.text, line_offset=style.sourceline - 1)) errors += check_links(container) + errors += check_fonts(container) return errors diff --git a/src/calibre/ebooks/oeb/polish/container.py b/src/calibre/ebooks/oeb/polish/container.py index 8105f318fa..ee41cb4e72 100644 --- a/src/calibre/ebooks/oeb/polish/container.py +++ b/src/calibre/ebooks/oeb/polish/container.py @@ -430,7 +430,7 @@ class Container(object): # {{{ ans = self.decode(ans) return ans - def parse_css(self, data, fname=''): + def parse_css(self, data, fname='', is_declaration=False): from cssutils import CSSParser, log log.setLevel(logging.WARN) log.raiseExceptions = False @@ -441,7 +441,10 @@ class Container(object): # {{{ parser = CSSParser(loglevel=logging.WARNING, # We dont care about @import rules fetcher=lambda x: (None, None), log=_css_logger) - data = parser.parseString(data, href=fname, validate=False) + if is_declaration: + data = parser.parseStyle(data, validate=False) + else: + data = parser.parseString(data, href=fname, validate=False) return data def parsed(self, name):