mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
Merge branch 'normalize'
EPUB/AZX3 Output: Use shorthand forms for margin, padding and border CSS properties Conversion: Fix a bug that could cause incorrect border values to be used when cascading, shorthand border CSS is present.
This commit is contained in:
commit
53b752fb4a
@ -148,7 +148,6 @@ class MOBIOutput(OutputFormatPlugin):
|
|||||||
self.oeb.manifest.remove(self.oeb.manifest.hrefs[x.href])
|
self.oeb.manifest.remove(self.oeb.manifest.hrefs[x.href])
|
||||||
x.href = articles_[0].href
|
x.href = articles_[0].href
|
||||||
|
|
||||||
|
|
||||||
for sec in sections:
|
for sec in sections:
|
||||||
articles[id(sec)] = []
|
articles[id(sec)] = []
|
||||||
for a in list(sec):
|
for a in list(sec):
|
||||||
@ -179,7 +178,7 @@ class MOBIOutput(OutputFormatPlugin):
|
|||||||
|
|
||||||
mobi_type = opts.mobi_file_type
|
mobi_type = opts.mobi_file_type
|
||||||
if self.is_periodical:
|
if self.is_periodical:
|
||||||
mobi_type = 'old' # Amazon does not support KF8 periodicals
|
mobi_type = 'old' # Amazon does not support KF8 periodicals
|
||||||
create_kf8 = mobi_type in ('new', 'both')
|
create_kf8 = mobi_type in ('new', 'both')
|
||||||
|
|
||||||
remove_html_cover(self.oeb, self.log)
|
remove_html_cover(self.oeb, self.log)
|
||||||
@ -188,12 +187,10 @@ class MOBIOutput(OutputFormatPlugin):
|
|||||||
self.check_for_periodical()
|
self.check_for_periodical()
|
||||||
|
|
||||||
if create_kf8:
|
if create_kf8:
|
||||||
# Split on pagebreaks so that the resulting KF8 works better with
|
# Split on pagebreaks so that the resulting KF8 is faster to load
|
||||||
# calibre's viewer, which does not support CSS page breaks
|
|
||||||
from calibre.ebooks.oeb.transforms.split import Split
|
from calibre.ebooks.oeb.transforms.split import Split
|
||||||
Split()(self.oeb, self.opts)
|
Split()(self.oeb, self.opts)
|
||||||
|
|
||||||
|
|
||||||
kf8 = self.create_kf8(resources, for_joint=mobi_type=='both'
|
kf8 = self.create_kf8(resources, for_joint=mobi_type=='both'
|
||||||
) if create_kf8 else None
|
) if create_kf8 else None
|
||||||
if mobi_type == 'new':
|
if mobi_type == 'new':
|
||||||
|
@ -18,7 +18,6 @@ class OEBOutput(OutputFormatPlugin):
|
|||||||
|
|
||||||
recommendations = set([('pretty_print', True, OptionRecommendation.HIGH)])
|
recommendations = set([('pretty_print', True, OptionRecommendation.HIGH)])
|
||||||
|
|
||||||
|
|
||||||
def convert(self, oeb_book, output_path, input_plugin, opts, log):
|
def convert(self, oeb_book, output_path, input_plugin, opts, log):
|
||||||
from urllib import unquote
|
from urllib import unquote
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
@ -26,7 +25,8 @@ class OEBOutput(OutputFormatPlugin):
|
|||||||
self.log, self.opts = log, opts
|
self.log, self.opts = log, opts
|
||||||
if not os.path.exists(output_path):
|
if not os.path.exists(output_path):
|
||||||
os.makedirs(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):
|
with CurrentDir(output_path):
|
||||||
results = oeb_book.to_opf2(page_map=True)
|
results = oeb_book.to_opf2(page_map=True)
|
||||||
for key in (OPF_MIME, NCX_MIME, PAGE_MAP_MIME):
|
for key in (OPF_MIME, NCX_MIME, PAGE_MAP_MIME):
|
||||||
@ -54,6 +54,8 @@ class OEBOutput(OutputFormatPlugin):
|
|||||||
f.write(raw)
|
f.write(raw)
|
||||||
|
|
||||||
for item in oeb_book.manifest:
|
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))
|
path = os.path.abspath(unquote(item.href))
|
||||||
dir = os.path.dirname(path)
|
dir = os.path.dirname(path)
|
||||||
if not os.path.exists(dir):
|
if not os.path.exists(dir):
|
||||||
@ -62,7 +64,7 @@ class OEBOutput(OutputFormatPlugin):
|
|||||||
f.write(str(item))
|
f.write(str(item))
|
||||||
item.unload_data_from_memory(memory=path)
|
item.unload_data_from_memory(memory=path)
|
||||||
|
|
||||||
def workaround_nook_cover_bug(self, root): # {{{
|
def workaround_nook_cover_bug(self, root): # {{{
|
||||||
cov = root.xpath('//*[local-name() = "meta" and @name="cover" and'
|
cov = root.xpath('//*[local-name() = "meta" and @name="cover" and'
|
||||||
' @content != "cover"]')
|
' @content != "cover"]')
|
||||||
|
|
||||||
@ -96,7 +98,7 @@ class OEBOutput(OutputFormatPlugin):
|
|||||||
cov.set('content', 'cover')
|
cov.set('content', 'cover')
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def workaround_pocketbook_cover_bug(self, root): # {{{
|
def workaround_pocketbook_cover_bug(self, root): # {{{
|
||||||
m = root.xpath('//*[local-name() = "manifest"]/*[local-name() = "item" '
|
m = root.xpath('//*[local-name() = "manifest"]/*[local-name() = "item" '
|
||||||
' and @id="cover"]')
|
' and @id="cover"]')
|
||||||
if len(m) == 1:
|
if len(m) == 1:
|
||||||
@ -106,7 +108,7 @@ class OEBOutput(OutputFormatPlugin):
|
|||||||
p.insert(0, m)
|
p.insert(0, m)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def migrate_lang_code(self, root): # {{{
|
def migrate_lang_code(self, root): # {{{
|
||||||
from calibre.utils.localization import lang_as_iso639_1
|
from calibre.utils.localization import lang_as_iso639_1
|
||||||
for lang in root.xpath('//*[local-name() = "language"]'):
|
for lang in root.xpath('//*[local-name() = "language"]'):
|
||||||
clc = lang_as_iso639_1(lang.text)
|
clc = lang_as_iso639_1(lang.text)
|
||||||
|
@ -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.compression.palmdoc import compress_doc
|
||||||
from calibre.ebooks.oeb.base import (OEB_DOCS, OEB_STYLES, SVG_MIME, XPath,
|
from calibre.ebooks.oeb.base import (OEB_DOCS, OEB_STYLES, SVG_MIME, XPath,
|
||||||
extract, XHTML, urlnormalize)
|
extract, XHTML, urlnormalize)
|
||||||
|
from calibre.ebooks.oeb.normalize_css import condense_sheet
|
||||||
from calibre.ebooks.oeb.parse_utils import barename
|
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.skeleton import Chunker, aid_able_tags, to_href
|
||||||
from calibre.ebooks.mobi.writer8.index import (NCXIndex, SkelIndex,
|
from calibre.ebooks.mobi.writer8.index import (NCXIndex, SkelIndex,
|
||||||
@ -48,8 +49,8 @@ class KF8Writer(object):
|
|||||||
self.toc_adder = TOCAdder(oeb, opts)
|
self.toc_adder = TOCAdder(oeb, opts)
|
||||||
self.used_images = set()
|
self.used_images = set()
|
||||||
self.resources = resources
|
self.resources = resources
|
||||||
self.flows = [None] # First flow item is reserved for the text
|
self.flows = [None] # First flow item is reserved for the text
|
||||||
self.records = [None] # Placeholder for zeroth record
|
self.records = [None] # Placeholder for zeroth record
|
||||||
|
|
||||||
self.log('\tGenerating KF8 markup...')
|
self.log('\tGenerating KF8 markup...')
|
||||||
self.dup_data()
|
self.dup_data()
|
||||||
@ -145,11 +146,13 @@ class KF8Writer(object):
|
|||||||
cssutils.replaceUrls(sheet, replacer, ignoreImportRules=True)
|
cssutils.replaceUrls(sheet, replacer, ignoreImportRules=True)
|
||||||
|
|
||||||
def extract_css_into_flows(self):
|
def extract_css_into_flows(self):
|
||||||
inlines = defaultdict(list) # Ensure identical <style>s not repeated
|
inlines = defaultdict(list) # Ensure identical <style>s not repeated
|
||||||
sheets = {}
|
sheets = {}
|
||||||
|
|
||||||
for item in self.oeb.manifest:
|
for item in self.oeb.manifest:
|
||||||
if item.media_type in OEB_STYLES:
|
if item.media_type in OEB_STYLES:
|
||||||
|
if hasattr(item.data, 'cssText'):
|
||||||
|
condense_sheet(self.data(item))
|
||||||
data = self.data(item).cssText
|
data = self.data(item).cssText
|
||||||
sheets[item.href] = len(self.flows)
|
sheets[item.href] = len(self.flows)
|
||||||
self.flows.append(force_unicode(data, 'utf-8'))
|
self.flows.append(force_unicode(data, 'utf-8'))
|
||||||
@ -375,7 +378,7 @@ class KF8Writer(object):
|
|||||||
key=lambda entry: (entry['depth'], entry['offset']))
|
key=lambda entry: (entry['depth'], entry['offset']))
|
||||||
is_non_linear = original != linearized
|
is_non_linear = original != linearized
|
||||||
entries = linearized
|
entries = linearized
|
||||||
is_non_linear = False # False as we are using the linearized entries
|
is_non_linear = False # False as we are using the linearized entries
|
||||||
|
|
||||||
if is_non_linear:
|
if is_non_linear:
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
@ -428,7 +431,7 @@ class KF8Writer(object):
|
|||||||
_('Unknown'), ref.type, (pos, fid)))
|
_('Unknown'), ref.type, (pos, fid)))
|
||||||
|
|
||||||
if self.guide_table:
|
if self.guide_table:
|
||||||
self.guide_table.sort(key=lambda x:x.type) # Needed by the Kindle
|
self.guide_table.sort(key=lambda x:x.type) # Needed by the Kindle
|
||||||
self.guide_records = GuideIndex(self.guide_table)()
|
self.guide_records = GuideIndex(self.guide_table)()
|
||||||
|
|
||||||
def create_kf8_book(oeb, opts, resources, for_joint=False):
|
def create_kf8_book(oeb, opts, resources, for_joint=False):
|
||||||
|
389
src/calibre/ebooks/oeb/normalize_css.py
Normal file
389
src/calibre/ebooks/oeb/normalize_css.py
Normal file
@ -0,0 +1,389 @@
|
|||||||
|
#!/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 <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
|
from future_builtins import zip
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cssutils.css import PropertyValue
|
||||||
|
except ImportError:
|
||||||
|
raise RuntimeError('You need cssutils >= 0.9.9 for calibre')
|
||||||
|
from cssutils import profile as cssprofiles
|
||||||
|
|
||||||
|
DEFAULTS = {'azimuth': 'center', 'background-attachment': 'scroll', # {{{
|
||||||
|
'background-color': 'transparent', 'background-image': 'none',
|
||||||
|
'background-position': '0% 0%', 'background-repeat': 'repeat',
|
||||||
|
'border-bottom-color': 'currentColor', 'border-bottom-style': 'none',
|
||||||
|
'border-bottom-width': 'medium', 'border-collapse': 'separate',
|
||||||
|
'border-left-color': 'currentColor', 'border-left-style': 'none',
|
||||||
|
'border-left-width': 'medium', 'border-right-color': 'currentColor',
|
||||||
|
'border-right-style': 'none', 'border-right-width': 'medium',
|
||||||
|
'border-spacing': 0, 'border-top-color': 'currentColor',
|
||||||
|
'border-top-style': 'none', 'border-top-width': 'medium', 'bottom':
|
||||||
|
'auto', 'caption-side': 'top', 'clear': 'none', 'clip': 'auto',
|
||||||
|
'color': 'black', 'content': 'normal', 'counter-increment': 'none',
|
||||||
|
'counter-reset': 'none', 'cue-after': 'none', 'cue-before': 'none',
|
||||||
|
'cursor': 'auto', 'direction': 'ltr', 'display': 'inline',
|
||||||
|
'elevation': 'level', 'empty-cells': 'show', 'float': 'none',
|
||||||
|
'font-family': 'serif', 'font-size': 'medium', 'font-style':
|
||||||
|
'normal', 'font-variant': 'normal', 'font-weight': 'normal',
|
||||||
|
'height': 'auto', 'left': 'auto', 'letter-spacing': 'normal',
|
||||||
|
'line-height': 'normal', 'list-style-image': 'none',
|
||||||
|
'list-style-position': 'outside', 'list-style-type': 'disc',
|
||||||
|
'margin-bottom': 0, 'margin-left': 0, 'margin-right': 0,
|
||||||
|
'margin-top': 0, 'max-height': 'none', 'max-width': 'none',
|
||||||
|
'min-height': 0, 'min-width': 0, 'orphans': '2',
|
||||||
|
'outline-color': 'invert', 'outline-style': 'none',
|
||||||
|
'outline-width': 'medium', 'overflow': 'visible', 'padding-bottom':
|
||||||
|
0, 'padding-left': 0, 'padding-right': 0, 'padding-top': 0,
|
||||||
|
'page-break-after': 'auto', 'page-break-before': 'auto',
|
||||||
|
'page-break-inside': 'auto', 'pause-after': 0, 'pause-before':
|
||||||
|
0, 'pitch': 'medium', 'pitch-range': '50', 'play-during': 'auto',
|
||||||
|
'position': 'static', 'quotes': u"'“' '”' '‘' '’'", 'richness':
|
||||||
|
'50', 'right': 'auto', 'speak': 'normal', 'speak-header': 'once',
|
||||||
|
'speak-numeral': 'continuous', 'speak-punctuation': 'none',
|
||||||
|
'speech-rate': 'medium', 'stress': '50', 'table-layout': 'auto',
|
||||||
|
'text-align': 'auto', 'text-decoration': 'none', 'text-indent':
|
||||||
|
0, 'text-transform': 'none', 'top': 'auto', 'unicode-bidi':
|
||||||
|
'normal', 'vertical-align': 'baseline', 'visibility': 'visible',
|
||||||
|
'voice-family': 'default', 'volume': 'medium', 'white-space':
|
||||||
|
'normal', 'widows': '2', 'width': 'auto', 'word-spacing': 'normal',
|
||||||
|
'z-index': 'auto'}
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
EDGES = ('top', 'right', 'bottom', 'left')
|
||||||
|
BORDER_PROPS = ('color', 'style', 'width')
|
||||||
|
|
||||||
|
def normalize_edge(name, cssvalue):
|
||||||
|
style = {}
|
||||||
|
if isinstance(cssvalue, PropertyValue):
|
||||||
|
primitives = [v.cssText for v in cssvalue]
|
||||||
|
else:
|
||||||
|
primitives = [cssvalue.cssText]
|
||||||
|
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 simple_normalizer(prefix, names, check_inherit=True):
|
||||||
|
composition = tuple('%s-%s' %(prefix, n) for n in names)
|
||||||
|
@wraps(normalize_simple_composition)
|
||||||
|
def wrapper(name, cssvalue):
|
||||||
|
return normalize_simple_composition(name, cssvalue, composition, check_inherit=check_inherit)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_simple_composition(name, cssvalue, composition, check_inherit=True):
|
||||||
|
if check_inherit and cssvalue.cssText == 'inherit':
|
||||||
|
style = {k:'inherit' for k in composition}
|
||||||
|
else:
|
||||||
|
style = {k:DEFAULTS[k] for k in composition}
|
||||||
|
try:
|
||||||
|
primitives = [v.cssText for v in cssvalue]
|
||||||
|
except TypeError:
|
||||||
|
primitives = [cssvalue.cssText]
|
||||||
|
while primitives:
|
||||||
|
value = primitives.pop()
|
||||||
|
for key in composition:
|
||||||
|
if cssprofiles.validate(key, value):
|
||||||
|
style[key] = value
|
||||||
|
break
|
||||||
|
return style
|
||||||
|
|
||||||
|
font_composition = ('font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family')
|
||||||
|
|
||||||
|
def normalize_font(name, cssvalue):
|
||||||
|
# See https://developer.mozilla.org/en-US/docs/Web/CSS/font
|
||||||
|
composition = font_composition
|
||||||
|
val = cssvalue.cssText
|
||||||
|
if val == 'inherit':
|
||||||
|
return {k:'inherit' for k in composition}
|
||||||
|
if val in {'caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar'}:
|
||||||
|
return {k:DEFAULTS[k] for k in composition}
|
||||||
|
try:
|
||||||
|
primitives = list(v.cssText for v in cssvalue)
|
||||||
|
except TypeError:
|
||||||
|
primitives = [cssvalue.cssText]
|
||||||
|
if len(primitives) < 2:
|
||||||
|
return {} # Mandatory to define both font size and font family
|
||||||
|
style = {k:DEFAULTS[k] for k in composition}
|
||||||
|
style['font-family'] = primitives.pop()
|
||||||
|
if len(primitives) > 1 and cssprofiles.validate('line-height', primitives[-1]) and cssprofiles.validate('font-size', primitives[-2]):
|
||||||
|
style['line-height'], style['font-size'] = primitives.pop(), primitives.pop()
|
||||||
|
else:
|
||||||
|
val = primitives.pop()
|
||||||
|
if not cssprofiles.validate('font-size', val):
|
||||||
|
return {}
|
||||||
|
style['font-size'] = val
|
||||||
|
composition = composition[:3]
|
||||||
|
while primitives:
|
||||||
|
value = primitives.pop()
|
||||||
|
for key in composition:
|
||||||
|
if cssprofiles.validate(key, value):
|
||||||
|
style[key] = value
|
||||||
|
break
|
||||||
|
return style
|
||||||
|
|
||||||
|
def normalize_border(name, cssvalue):
|
||||||
|
style = normalizers['border-' + EDGES[0]]('border-' + EDGES[0], cssvalue)
|
||||||
|
vals = style.copy()
|
||||||
|
for edge in EDGES[1:]:
|
||||||
|
style.update({k.replace(EDGES[0], edge):v for k, v in vals.iteritems()})
|
||||||
|
return style
|
||||||
|
|
||||||
|
normalizers = {
|
||||||
|
'list-style': simple_normalizer('list-style', ('type', 'position', 'image')),
|
||||||
|
'font': normalize_font,
|
||||||
|
'border': normalize_border,
|
||||||
|
}
|
||||||
|
|
||||||
|
for x in ('margin', 'padding', 'border-style', 'border-width', 'border-color'):
|
||||||
|
normalizers[x] = normalize_edge
|
||||||
|
|
||||||
|
for x in EDGES:
|
||||||
|
name = 'border-' + x
|
||||||
|
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
|
||||||
|
maxDiff = None
|
||||||
|
|
||||||
|
def test_font_normalization(self):
|
||||||
|
def font_dict(expected):
|
||||||
|
ans = {k:DEFAULTS[k] for k in font_composition}
|
||||||
|
ans.update(expected)
|
||||||
|
return ans
|
||||||
|
|
||||||
|
for raw, expected in {
|
||||||
|
'some_font': {}, 'none': {}, 'inherit':{k:'inherit' for k in font_composition},
|
||||||
|
'1.2pt/1.4 A_Font': {'font-family':'A_Font', 'font-size':'1.2pt', 'line-height':'1.4'},
|
||||||
|
'bad font': {}, '10% serif': {'font-family':'serif', 'font-size':'10%'},
|
||||||
|
'bold italic large serif': {'font-family':'serif', 'font-weight':'bold', 'font-style':'italic', 'font-size':'large'},
|
||||||
|
'bold italic small-caps larger/normal serif':
|
||||||
|
{'font-family':'serif', 'font-weight':'bold', 'font-style':'italic', 'font-size':'larger',
|
||||||
|
'line-height':'normal', 'font-variant':'small-caps'},
|
||||||
|
}.iteritems():
|
||||||
|
val = tuple(parseStyle('font: %s' % raw, validate=False))[0].cssValue
|
||||||
|
style = normalizers['font']('font', val)
|
||||||
|
if expected:
|
||||||
|
self.assertDictEqual(font_dict(expected), style)
|
||||||
|
else:
|
||||||
|
self.assertDictEqual(expected, style)
|
||||||
|
|
||||||
|
def test_border_normalization(self):
|
||||||
|
def border_edge_dict(expected, edge='right'):
|
||||||
|
ans = {'border-%s-%s' % (edge, x): DEFAULTS['border-%s-%s' % (edge, x)] for x in ('style', 'width', 'color')}
|
||||||
|
for x, v in expected.iteritems():
|
||||||
|
ans['border-%s-%s' % (edge, x)] = v
|
||||||
|
return ans
|
||||||
|
def border_dict(expected):
|
||||||
|
ans = {}
|
||||||
|
for edge in EDGES:
|
||||||
|
ans.update(border_edge_dict(expected, edge))
|
||||||
|
return ans
|
||||||
|
def border_val_dict(expected, val='color'):
|
||||||
|
ans = {'border-%s-%s' % (edge, val): DEFAULTS['border-%s-%s' % (edge, val)] for edge in EDGES}
|
||||||
|
for edge in EDGES:
|
||||||
|
ans['border-%s-%s' % (edge, val)] = expected
|
||||||
|
return ans
|
||||||
|
|
||||||
|
for raw, expected in {
|
||||||
|
'solid 1px red': {'color':'red', 'width':'1px', 'style':'solid'},
|
||||||
|
'1px': {'width': '1px'}, '#aaa': {'color': '#aaa'},
|
||||||
|
'2em groove': {'width':'2em', 'style':'groove'},
|
||||||
|
}.iteritems():
|
||||||
|
for edge in EDGES:
|
||||||
|
br = 'border-%s' % edge
|
||||||
|
val = tuple(parseStyle('%s: %s' % (br, raw), validate=False))[0].cssValue
|
||||||
|
self.assertDictEqual(border_edge_dict(expected, edge), normalizers[br](br, val))
|
||||||
|
|
||||||
|
for raw, expected in {
|
||||||
|
'solid 1px red': {'color':'red', 'width':'1px', 'style':'solid'},
|
||||||
|
'1px': {'width': '1px'}, '#aaa': {'color': '#aaa'},
|
||||||
|
'thin groove': {'width':'thin', 'style':'groove'},
|
||||||
|
}.iteritems():
|
||||||
|
val = tuple(parseStyle('%s: %s' % ('border', raw), validate=False))[0].cssValue
|
||||||
|
self.assertDictEqual(border_dict(expected), normalizers['border']('border', val))
|
||||||
|
|
||||||
|
for name, val in {
|
||||||
|
'width': '10%', 'color': 'rgb(0, 1, 1)', 'style': 'double',
|
||||||
|
}.iteritems():
|
||||||
|
cval = tuple(parseStyle('border-%s: %s' % (name, val), validate=False))[0].cssValue
|
||||||
|
self.assertDictEqual(border_val_dict(val, name), normalizers['border-'+name]('border-'+name, cval))
|
||||||
|
|
||||||
|
def test_edge_normalization(self):
|
||||||
|
def edge_dict(prefix, expected):
|
||||||
|
return {'%s-%s' % (prefix, edge) : x for edge, x in zip(EDGES, expected)}
|
||||||
|
for raw, expected in {
|
||||||
|
'2px': ('2px', '2px', '2px', '2px'),
|
||||||
|
'1em 2em': ('1em', '2em', '1em', '2em'),
|
||||||
|
'1em 2em 3em': ('1em', '2em', '3em', '2em'),
|
||||||
|
'1 2 3 4': ('1', '2', '3', '4'),
|
||||||
|
}.iteritems():
|
||||||
|
for prefix in ('margin', 'padding'):
|
||||||
|
cval = tuple(parseStyle('%s: %s' % (prefix, raw), validate=False))[0].cssValue
|
||||||
|
self.assertDictEqual(edge_dict(prefix, expected), normalizers[prefix](prefix, cval))
|
||||||
|
|
||||||
|
def test_list_style_normalization(self):
|
||||||
|
def ls_dict(expected):
|
||||||
|
ans = {'list-style-%s' % x : DEFAULTS['list-style-%s' % x] for x in ('type', 'image', 'position')}
|
||||||
|
for k, v in expected.iteritems():
|
||||||
|
ans['list-style-%s' % k] = v
|
||||||
|
return ans
|
||||||
|
for raw, expected in {
|
||||||
|
'url(http://www.example.com/images/list.png)': {'image': 'url(http://www.example.com/images/list.png)'},
|
||||||
|
'inside square': {'position':'inside', 'type':'square'},
|
||||||
|
'upper-roman url(img) outside': {'position':'outside', 'type':'upper-roman', 'image':'url(img)'},
|
||||||
|
}.iteritems():
|
||||||
|
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)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test_normalization()
|
@ -8,15 +8,11 @@ from __future__ import with_statement
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
||||||
|
|
||||||
import os, itertools, re, logging, copy, unicodedata
|
import os, re, logging, copy, unicodedata
|
||||||
from weakref import WeakKeyDictionary
|
from weakref import WeakKeyDictionary
|
||||||
from xml.dom import SyntaxErr as CSSSyntaxError
|
from xml.dom import SyntaxErr as CSSSyntaxError
|
||||||
from cssutils.css import (CSSStyleRule, CSSPageRule, CSSFontFaceRule,
|
from cssutils.css import (CSSStyleRule, CSSPageRule, CSSFontFaceRule,
|
||||||
cssproperties)
|
cssproperties)
|
||||||
try:
|
|
||||||
from cssutils.css import PropertyValue
|
|
||||||
except ImportError:
|
|
||||||
raise RuntimeError('You need cssutils >= 0.9.9 for calibre')
|
|
||||||
from cssutils import (profile as cssprofiles, parseString, parseStyle, log as
|
from cssutils import (profile as cssprofiles, parseString, parseStyle, log as
|
||||||
cssutils_log, CSSParser, profiles, replaceUrls)
|
cssutils_log, CSSParser, profiles, replaceUrls)
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
@ -24,8 +20,8 @@ from cssselect import HTMLTranslator
|
|||||||
|
|
||||||
from calibre import force_unicode
|
from calibre import force_unicode
|
||||||
from calibre.ebooks import unit_convert
|
from calibre.ebooks import unit_convert
|
||||||
from calibre.ebooks.oeb.base import XHTML, XHTML_NS, CSS_MIME, OEB_STYLES
|
from calibre.ebooks.oeb.base import XHTML, XHTML_NS, CSS_MIME, OEB_STYLES, XPNSMAP, xpath, urlnormalize
|
||||||
from calibre.ebooks.oeb.base import XPNSMAP, xpath, urlnormalize
|
from calibre.ebooks.oeb.normalize_css import DEFAULTS, normalizers
|
||||||
|
|
||||||
cssutils_log.setLevel(logging.WARN)
|
cssutils_log.setLevel(logging.WARN)
|
||||||
|
|
||||||
@ -54,46 +50,6 @@ INHERITED = set(['azimuth', 'border-collapse', 'border-spacing',
|
|||||||
'visibility', 'voice-family', 'volume', 'white-space',
|
'visibility', 'voice-family', 'volume', 'white-space',
|
||||||
'widows', 'word-spacing'])
|
'widows', 'word-spacing'])
|
||||||
|
|
||||||
DEFAULTS = {'azimuth': 'center', 'background-attachment': 'scroll',
|
|
||||||
'background-color': 'transparent', 'background-image': 'none',
|
|
||||||
'background-position': '0% 0%', 'background-repeat': 'repeat',
|
|
||||||
'border-bottom-color': ':color', 'border-bottom-style': 'none',
|
|
||||||
'border-bottom-width': 'medium', 'border-collapse': 'separate',
|
|
||||||
'border-left-color': ':color', 'border-left-style': 'none',
|
|
||||||
'border-left-width': 'medium', 'border-right-color': ':color',
|
|
||||||
'border-right-style': 'none', 'border-right-width': 'medium',
|
|
||||||
'border-spacing': 0, 'border-top-color': ':color',
|
|
||||||
'border-top-style': 'none', 'border-top-width': 'medium', 'bottom':
|
|
||||||
'auto', 'caption-side': 'top', 'clear': 'none', 'clip': 'auto',
|
|
||||||
'color': 'black', 'content': 'normal', 'counter-increment': 'none',
|
|
||||||
'counter-reset': 'none', 'cue-after': 'none', 'cue-before': 'none',
|
|
||||||
'cursor': 'auto', 'direction': 'ltr', 'display': 'inline',
|
|
||||||
'elevation': 'level', 'empty-cells': 'show', 'float': 'none',
|
|
||||||
'font-family': 'serif', 'font-size': 'medium', 'font-style':
|
|
||||||
'normal', 'font-variant': 'normal', 'font-weight': 'normal',
|
|
||||||
'height': 'auto', 'left': 'auto', 'letter-spacing': 'normal',
|
|
||||||
'line-height': 'normal', 'list-style-image': 'none',
|
|
||||||
'list-style-position': 'outside', 'list-style-type': 'disc',
|
|
||||||
'margin-bottom': 0, 'margin-left': 0, 'margin-right': 0,
|
|
||||||
'margin-top': 0, 'max-height': 'none', 'max-width': 'none',
|
|
||||||
'min-height': 0, 'min-width': 0, 'orphans': '2',
|
|
||||||
'outline-color': 'invert', 'outline-style': 'none',
|
|
||||||
'outline-width': 'medium', 'overflow': 'visible', 'padding-bottom':
|
|
||||||
0, 'padding-left': 0, 'padding-right': 0, 'padding-top': 0,
|
|
||||||
'page-break-after': 'auto', 'page-break-before': 'auto',
|
|
||||||
'page-break-inside': 'auto', 'pause-after': 0, 'pause-before':
|
|
||||||
0, 'pitch': 'medium', 'pitch-range': '50', 'play-during': 'auto',
|
|
||||||
'position': 'static', 'quotes': u"'“' '”' '‘' '’'", 'richness':
|
|
||||||
'50', 'right': 'auto', 'speak': 'normal', 'speak-header': 'once',
|
|
||||||
'speak-numeral': 'continuous', 'speak-punctuation': 'none',
|
|
||||||
'speech-rate': 'medium', 'stress': '50', 'table-layout': 'auto',
|
|
||||||
'text-align': 'auto', 'text-decoration': 'none', 'text-indent':
|
|
||||||
0, 'text-transform': 'none', 'top': 'auto', 'unicode-bidi':
|
|
||||||
'normal', 'vertical-align': 'baseline', 'visibility': 'visible',
|
|
||||||
'voice-family': 'default', 'volume': 'medium', 'white-space':
|
|
||||||
'normal', 'widows': '2', 'width': 'auto', 'word-spacing': 'normal',
|
|
||||||
'z-index': 'auto'}
|
|
||||||
|
|
||||||
FONT_SIZE_NAMES = set(['xx-small', 'x-small', 'small', 'medium', 'large',
|
FONT_SIZE_NAMES = set(['xx-small', 'x-small', 'small', 'medium', 'large',
|
||||||
'x-large', 'xx-large'])
|
'x-large', 'xx-large'])
|
||||||
|
|
||||||
@ -421,14 +377,11 @@ class Stylizer(object):
|
|||||||
style = {}
|
style = {}
|
||||||
for prop in cssstyle:
|
for prop in cssstyle:
|
||||||
name = prop.name
|
name = prop.name
|
||||||
if name in ('margin', 'padding'):
|
normalizer = normalizers.get(name, None)
|
||||||
style.update(self._normalize_edge(prop.cssValue, name))
|
if normalizer is not None:
|
||||||
elif name == 'font':
|
style.update(normalizer(name, prop.cssValue))
|
||||||
style.update(self._normalize_font(prop.cssValue))
|
|
||||||
elif name == 'list-style':
|
|
||||||
style.update(self._normalize_list_style(prop.cssValue))
|
|
||||||
elif name == 'text-align':
|
elif name == 'text-align':
|
||||||
style.update(self._normalize_text_align(prop.cssValue))
|
style['text-align'] = self._apply_text_align(prop.value)
|
||||||
else:
|
else:
|
||||||
style[name] = prop.value
|
style[name] = prop.value
|
||||||
if 'font-size' in style:
|
if 'font-size' in style:
|
||||||
@ -441,93 +394,10 @@ class Stylizer(object):
|
|||||||
style['font-size'] = "%dpt" % self.profile.fnames[size]
|
style['font-size'] = "%dpt" % self.profile.fnames[size]
|
||||||
return style
|
return style
|
||||||
|
|
||||||
def _normalize_edge(self, cssvalue, name):
|
def _apply_text_align(self, text):
|
||||||
style = {}
|
if text in ('left', 'justify') and self.opts.change_justification in ('left', 'justify'):
|
||||||
if isinstance(cssvalue, PropertyValue):
|
text = self.opts.change_justification
|
||||||
primitives = [v.cssText for v in cssvalue]
|
return text
|
||||||
else:
|
|
||||||
primitives = [cssvalue.cssText]
|
|
||||||
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]
|
|
||||||
edges = ('top', 'right', 'bottom', 'left')
|
|
||||||
for edge, value in itertools.izip(edges, values):
|
|
||||||
style["%s-%s" % (name, edge)] = value
|
|
||||||
return style
|
|
||||||
|
|
||||||
def _normalize_list_style(self, cssvalue):
|
|
||||||
composition = ('list-style-type', 'list-style-position',
|
|
||||||
'list-style-image')
|
|
||||||
style = {}
|
|
||||||
if cssvalue.cssText == 'inherit':
|
|
||||||
for key in composition:
|
|
||||||
style[key] = 'inherit'
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
primitives = [v.cssText for v in cssvalue]
|
|
||||||
except TypeError:
|
|
||||||
primitives = [cssvalue.cssText]
|
|
||||||
primitives.reverse()
|
|
||||||
value = primitives.pop()
|
|
||||||
for key in composition:
|
|
||||||
if cssprofiles.validate(key, value):
|
|
||||||
style[key] = value
|
|
||||||
if not primitives:
|
|
||||||
break
|
|
||||||
value = primitives.pop()
|
|
||||||
for key in composition:
|
|
||||||
if key not in style:
|
|
||||||
style[key] = DEFAULTS[key]
|
|
||||||
|
|
||||||
return style
|
|
||||||
|
|
||||||
def _normalize_text_align(self, cssvalue):
|
|
||||||
style = {}
|
|
||||||
text = cssvalue.cssText
|
|
||||||
if text == 'inherit':
|
|
||||||
style['text-align'] = 'inherit'
|
|
||||||
else:
|
|
||||||
if text in ('left', 'justify') and self.opts.change_justification in ('left', 'justify'):
|
|
||||||
val = self.opts.change_justification
|
|
||||||
style['text-align'] = val
|
|
||||||
else:
|
|
||||||
style['text-align'] = text
|
|
||||||
return style
|
|
||||||
|
|
||||||
def _normalize_font(self, cssvalue):
|
|
||||||
composition = ('font-style', 'font-variant', 'font-weight',
|
|
||||||
'font-size', 'line-height', 'font-family')
|
|
||||||
style = {}
|
|
||||||
if cssvalue.cssText == 'inherit':
|
|
||||||
for key in composition:
|
|
||||||
style[key] = 'inherit'
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
primitives = [v.cssText for v in cssvalue]
|
|
||||||
except TypeError:
|
|
||||||
primitives = [cssvalue.cssText]
|
|
||||||
primitives.reverse()
|
|
||||||
value = primitives.pop()
|
|
||||||
for key in composition:
|
|
||||||
if cssprofiles.validate(key, value):
|
|
||||||
style[key] = value
|
|
||||||
if not primitives:
|
|
||||||
break
|
|
||||||
value = primitives.pop()
|
|
||||||
for key in composition:
|
|
||||||
if key not in style:
|
|
||||||
val = ('inherit' if key in {'font-family', 'font-size'}
|
|
||||||
else 'normal')
|
|
||||||
style[key] = val
|
|
||||||
return style
|
|
||||||
|
|
||||||
def style(self, element):
|
def style(self, element):
|
||||||
try:
|
try:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user