From 57898355502c551a0a1fed9d03056f62b72fade1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 15 Sep 2013 17:04:34 +0530 Subject: [PATCH] Use shorthand properties for margin, border and padding --- .../ebooks/conversion/plugins/oeb_output.py | 5 +- src/calibre/ebooks/mobi/writer8/main.py | 3 + src/calibre/ebooks/oeb/normalize_css.py | 121 +++++++++++++++++- 3 files changed, 127 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/conversion/plugins/oeb_output.py b/src/calibre/ebooks/conversion/plugins/oeb_output.py index 19468425b1..d63be32fd9 100644 --- a/src/calibre/ebooks/conversion/plugins/oeb_output.py +++ b/src/calibre/ebooks/conversion/plugins/oeb_output.py @@ -25,7 +25,8 @@ class OEBOutput(OutputFormatPlugin): self.log, self.opts = log, opts if not os.path.exists(output_path): os.makedirs(output_path) - from calibre.ebooks.oeb.base import OPF_MIME, NCX_MIME, PAGE_MAP_MIME + from calibre.ebooks.oeb.base import OPF_MIME, NCX_MIME, PAGE_MAP_MIME, OEB_STYLES + from calibre.ebooks.oeb.normalize_css import condense_sheet with CurrentDir(output_path): results = oeb_book.to_opf2(page_map=True) for key in (OPF_MIME, NCX_MIME, PAGE_MAP_MIME): @@ -53,6 +54,8 @@ class OEBOutput(OutputFormatPlugin): f.write(raw) for item in oeb_book.manifest: + if item.media_type in OEB_STYLES and hasattr(item.data, 'cssText'): + condense_sheet(item.data) path = os.path.abspath(unquote(item.href)) dir = os.path.dirname(path) if not os.path.exists(dir): diff --git a/src/calibre/ebooks/mobi/writer8/main.py b/src/calibre/ebooks/mobi/writer8/main.py index efdfb00ba8..e1e0981af1 100644 --- a/src/calibre/ebooks/mobi/writer8/main.py +++ b/src/calibre/ebooks/mobi/writer8/main.py @@ -22,6 +22,7 @@ from calibre.ebooks.mobi.utils import (create_text_record, to_base, from calibre.ebooks.compression.palmdoc import compress_doc from calibre.ebooks.oeb.base import (OEB_DOCS, OEB_STYLES, SVG_MIME, XPath, extract, XHTML, urlnormalize) +from calibre.ebooks.oeb.normalize_css import condense_sheet from calibre.ebooks.oeb.parse_utils import barename from calibre.ebooks.mobi.writer8.skeleton import Chunker, aid_able_tags, to_href from calibre.ebooks.mobi.writer8.index import (NCXIndex, SkelIndex, @@ -150,6 +151,8 @@ class KF8Writer(object): for item in self.oeb.manifest: if item.media_type in OEB_STYLES: + if hasattr(item.data, 'cssText'): + condense_sheet(self.data(item)) data = self.data(item).cssText sheets[item.href] = len(self.flows) self.flows.append(force_unicode(data, 'utf-8')) diff --git a/src/calibre/ebooks/oeb/normalize_css.py b/src/calibre/ebooks/oeb/normalize_css.py index dea3826c87..d1f7eb0783 100644 --- a/src/calibre/ebooks/oeb/normalize_css.py +++ b/src/calibre/ebooks/oeb/normalize_css.py @@ -57,6 +57,7 @@ DEFAULTS = {'azimuth': 'center', 'background-attachment': 'scroll', # {{{ # }}} EDGES = ('top', 'right', 'bottom', 'left') +BORDER_PROPS = ('color', 'style', 'width') def normalize_edge(name, cssvalue): style = {} @@ -162,11 +163,85 @@ for x in ('margin', 'padding', 'border-style', 'border-width', 'border-color'): for x in EDGES: name = 'border-' + x - normalizers[name] = simple_normalizer(name, ('color', 'style', 'width'), check_inherit=False) + normalizers[name] = simple_normalizer(name, BORDER_PROPS, check_inherit=False) + +def condense_edge(vals): + edges = {x.name.rpartition('-')[-1]:x.value for x in vals} + if len(edges) != 4: + return + ce = {} + for (x, y) in [('left', 'right'), ('top', 'bottom')]: + if edges[x] == edges[y]: + ce[x] = edges[x] + else: + ce[x], ce[y] = edges[x], edges[y] + if len(ce) == 4: + return ' '.join(ce[x] for x in ('top', 'right', 'bottom', 'left')) + if len(ce) == 3: + if 'right' in ce: + return ' '.join(ce[x] for x in ('top', 'right', 'top', 'left')) + return ' '.join(ce[x] for x in ('top', 'left', 'bottom')) + if len(ce) == 2: + if ce['top'] == ce['left']: + return ce['top'] + return ' '.join(ce[x] for x in ('top', 'left')) + +def simple_condenser(prefix, func): + @wraps(func) + def condense_simple(style, props): + cp = func(props) + if cp is not None: + for prop in props: + style.removeProperty(prop.name) + style.setProperty(prefix, cp) + return condense_simple + +def condense_border(style, props): + prop_map = {p.name:p for p in props} + edge_vals = [] + for edge in EDGES: + name = 'border-%s' % edge + vals = [] + for prop in BORDER_PROPS: + x = prop_map.get('%s-%s' % (name, prop), None) + if x is not None: + vals.append(x) + if len(vals) == 3: + for prop in vals: + style.removeProperty(prop.name) + style.setProperty(name, ' '.join(x.value for x in vals)) + prop_map[name] = style.getProperty(name) + x = prop_map.get(name, None) + if x is not None: + edge_vals.append(x) + if len(edge_vals) == 4 and len({x.value for x in edge_vals}) == 1: + for prop in edge_vals: + style.removeProperty(prop.name) + style.setProperty('border', edge_vals[0].value) + +condensers = {'margin': simple_condenser('margin', condense_edge), 'padding': simple_condenser('padding', condense_edge), 'border': condense_border} + + +def condense_rule(style): + expanded = {'margin-':[], 'padding-':[], 'border-':[]} + for prop in style.getProperties(): + for x in expanded: + if prop.name and prop.name.startswith(x): + expanded[x].append(prop) + break + for prefix, vals in expanded.iteritems(): + if len(vals) > 1 and {x.priority for x in vals} == {''}: + condensers[prefix[:-1]](style, vals) + +def condense_sheet(sheet): + for rule in sheet.cssRules: + if rule.type == rule.STYLE_RULE: + condense_rule(rule.style) def test_normalization(): # {{{ import unittest from cssutils import parseStyle + from itertools import product class TestNormalization(unittest.TestCase): longMessage = True @@ -262,6 +337,50 @@ def test_normalization(): # {{{ cval = tuple(parseStyle('list-style: %s' % raw, validate=False))[0].cssValue self.assertDictEqual(ls_dict(expected), normalizers['list-style']('list-style', cval)) + def test_edge_condensation(self): + for s, v in { + (1, 1, 3) : None, + (1, 2, 3, 4) : '2pt 3pt 4pt 1pt', + (1, 2, 3, 2) : '2pt 3pt 2pt 1pt', + (1, 2, 1, 3) : '2pt 1pt 3pt', + (1, 2, 1, 2) : '2pt 1pt', + (1, 1, 1, 1) : '1pt', + ('2%', '2%', '2%', '2%') : '2%', + tuple('0 0 0 0'.split()) : '0', + }.iteritems(): + for prefix in ('margin', 'padding'): + css = {'%s-%s' % (prefix, x) : str(y)+'pt' if isinstance(y, (int, float)) else y for x, y in zip(('left', 'top', 'right', 'bottom'), s)} + css = '; '.join(('%s:%s' % (k, v) for k, v in css.iteritems())) + style = parseStyle(css) + condense_rule(style) + val = getattr(style.getProperty(prefix), 'value', None) + self.assertEqual(v, val) + if val is not None: + for edge in EDGES: + self.assertFalse(getattr(style.getProperty('%s-%s' % (prefix, edge)), 'value', None)) + + 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(style.cssText, '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')) + tests = unittest.defaultTestLoader.loadTestsFromTestCase(TestNormalization) unittest.TextTestRunner(verbosity=4).run(tests) # }}}