diff --git a/resources/jacket/stylesheet.css b/resources/jacket/stylesheet.css index 204342ed22..8dee8edc3c 100644 --- a/resources/jacket/stylesheet.css +++ b/resources/jacket/stylesheet.css @@ -1,11 +1,11 @@ /* ** Book Jacket generation ** -** The template for Book Jackets is in ebooks.oeb.transforms.jacket:JACKET_TEMPLATE +** The template for Book Jackets is template.xhtml ** This CSS is inserted into the generated HTML at conversion time ** ** Users can control parts of the presentation of a generated book jacket by -** editing this file. +** editing this file and template.xhtml ** ** The general form of a generated Book Jacket: ** diff --git a/resources/jacket/template.xhtml b/resources/jacket/template.xhtml new file mode 100644 index 0000000000..93e12983e8 --- /dev/null +++ b/resources/jacket/template.xhtml @@ -0,0 +1,34 @@ + + + {title_str} + + + + +
+
{title}
+ + + + + + + + + + + + + + + + + +
{series_label}:{series}
{pubdate_label}:{pubdate}
{rating_label}:{rating}
{tags_label}:{tags}
+ +
+
+
{comments}
+ + + diff --git a/resources/recipes/slate.recipe b/resources/recipes/slate.recipe index c03255d2df..9da1c4da78 100644 --- a/resources/recipes/slate.recipe +++ b/resources/recipes/slate.recipe @@ -27,9 +27,6 @@ class PeriodicalNameHere(BasicNewsRecipe): encoding = None language = 'en' - - - # Method variables for customizing feed parsing summary_length = 250 use_embedded_content = None @@ -45,13 +42,26 @@ class PeriodicalNameHere(BasicNewsRecipe): match_regexps = [] # The second entry is for 'Big Money', which comes from a different site, uses different markup - keep_only_tags = [dict(attrs={ 'id':['article_top', 'article_body']}), + keep_only_tags = [dict(attrs={ 'id':['article_top', 'article_body', 'story']}), dict(attrs={ 'id':['content']}) ] # The second entry is for 'Big Money', which comes from a different site, uses different markup - remove_tags = [dict(attrs={ 'id':['toolbox','recommend_tab','insider_ad_wrapper', - 'article_bottom_tools_cntr','fray_article_discussion', 'fray_article_links','bottom_sponsored_links','author_bio', - 'bizbox_links_bottom','ris_links_wrapper','BOXXLE']}), + remove_tags = [dict(attrs={ 'id':[ + 'add_comments_button', + 'article_bottom_tools', + 'article_bottom_tools_cntr', + 'bizbox_links_bottom', + 'BOXXLE', + 'comments_button', + 'comments-to-fray', + 'fbog_article_bottom_cntr', + 'fray_article_discussion', 'fray_article_links','bottom_sponsored_links','author_bio', + 'insider_ad_wrapper', + 'js_kit_cntr', + 'recommend_tab', + 'ris_links_wrapper', + 'toolbox', + ]}), dict(attrs={ 'id':['content-top','service-links-bottom','hed']}) ] excludedDescriptionKeywords = ['Slate V','Twitter feed','podcast'] @@ -339,8 +349,8 @@ class PeriodicalNameHere(BasicNewsRecipe): # Change

to

headline = soup.find("h1") - tag = headline.find("span") - tag.name = 'div' + #tag = headline.find("span") + #tag.name = 'div' if headline is not None : h2tag = Tag(soup, "h2") diff --git a/src/calibre/ebooks/fb2/output.py b/src/calibre/ebooks/fb2/output.py index d0125afe89..d6c7a25a90 100644 --- a/src/calibre/ebooks/fb2/output.py +++ b/src/calibre/ebooks/fb2/output.py @@ -28,6 +28,9 @@ class FB2Output(OutputFormatPlugin): ]) def convert(self, oeb_book, output_path, input_plugin, opts, log): + from calibre.ebooks.oeb.transforms.jacket import linearize_jacket + linearize_jacket(oeb_book) + fb2mlizer = FB2MLizer(log) fb2_content = fb2mlizer.extract_content(oeb_book, opts) diff --git a/src/calibre/ebooks/oeb/transforms/cover.py b/src/calibre/ebooks/oeb/transforms/cover.py index 59b42df68a..532c9bbc03 100644 --- a/src/calibre/ebooks/oeb/transforms/cover.py +++ b/src/calibre/ebooks/oeb/transforms/cover.py @@ -99,7 +99,8 @@ class CoverManager(object): series_string = None if m.series and m.series_index: series_string = _('Book %s of %s')%( - fmt_sidx(m.series_index[0], use_roman=True), m.series[0]) + fmt_sidx(m.series_index[0], use_roman=True), + unicode(m.series[0])) try: from calibre.ebooks import calibre_cover diff --git a/src/calibre/ebooks/oeb/transforms/jacket.py b/src/calibre/ebooks/oeb/transforms/jacket.py index 6786d7cf9c..dc1d2fea41 100644 --- a/src/calibre/ebooks/oeb/transforms/jacket.py +++ b/src/calibre/ebooks/oeb/transforms/jacket.py @@ -6,7 +6,7 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import os, textwrap +import sys from xml.sax.saxutils import escape from itertools import repeat @@ -14,216 +14,202 @@ from lxml import etree from calibre import guess_type, strftime from calibre.constants import __appname__, __version__ -from calibre.utils.date import now from calibre.ebooks.BeautifulSoup import BeautifulSoup -from calibre.ebooks.oeb.base import XPath, XPNSMAP +from calibre.ebooks.oeb.base import XPath, XHTML_NS, XHTML from calibre.library.comments import comments_to_html +from calibre.utils.magick.draw import save_cover_data_to + +JACKET_XPATH = '//h:meta[@name="calibre-content" and @content="jacket"]' + class Jacket(object): ''' Book jacket manipulation. Remove first image and insert comments at start of book. ''' - JACKET_TEMPLATE = textwrap.dedent(u'''\ - - - %(title_str)s - - - - -
-
%(title)s
- - - - - - - - - - - - - - - - - -
Series:%(series)s
Published:%(pubdate)s
Rating:%(rating)s
Tags:%(tags)s
- -
-
-
%(comments)s
- - - ''') + def remove_images(self, item, limit=1): + path = XPath('//h:img[@src]') + removed = 0 + for img in path(item.data): + if removed >= limit: + break + href = item.abshref(img.get('src')) + image = self.oeb.manifest.hrefs.get(href, None) + if image is not None: + self.oeb.manifest.remove(image) + img.getparent().remove(img) + removed += 1 + return removed def remove_first_image(self): - path = XPath('//h:img[@src]') - for i, item in enumerate(self.oeb.spine): - if i > 2: break - for img in path(item.data): - href = item.abshref(img.get('src')) - image = self.oeb.manifest.hrefs.get(href, None) - if image is not None: - self.log('Removing first image', img.get('src')) - self.oeb.manifest.remove(image) - img.getparent().remove(img) - return - - def get_rating(self, rating): - ans = '' - if rating is None: - return ans - try: - num = float(rating)/2 - except: - return ans - num = max(0, num) - num = min(num, 5) - if num < 1: - return ans - if self.opts.output_profile.name == 'Kindle': - ans = '%s' % ''.join(repeat('★', num)) - else: - id, href = self.oeb.manifest.generate('star', 'star.png') - self.oeb.manifest.add(id, href, 'image/png', data=I('star.png', data=True)) - ans = '%s' % ''.join(repeat('star'%href, num)) - return ans + for item in self.oeb.spine: + removed = self.remove_images(item) + if removed > 0: + self.log('Removed first image') + break def insert_metadata(self, mi): self.log('Inserting metadata into book...') - jacket_resources = P("jacket") - css_data = '' - stylesheet = os.path.join(jacket_resources, 'stylesheet.css') - with open(stylesheet) as f: - css = f.read() + fname = 'star.png' + img = I(fname, data=True) + + if self.opts.output_profile.short_name == 'kindle': + fname = 'star.jpg' + img = save_cover_data_to(img, fname, + return_data=True) + + + id, href = self.oeb.manifest.generate('calibre_jacket_star', fname) + self.oeb.manifest.add(id, href, guess_type(fname)[0], data=img) try: - title_str = mi.title if mi.title else unicode(self.oeb.metadata.title[0]) + tags = map(unicode, self.oeb.metadata.subject) except: - title_str = _('Unknown') - title = '%s' % (escape(title_str)) - - series = escape(mi.series if mi.series else '') - if mi.series and mi.series_index is not None: - series += escape(' [%s]'%mi.format_series_index()) - if not mi.series: - series = '' - - try: - pubdate = strftime(u'%Y', mi.pubdate.timetuple()) - except: - #pubdate = strftime(u'%Y', now()) - pubdate = '' - - rating = self.get_rating(mi.rating) - - tags = mi.tags - if not tags: - try: - tags = map(unicode, self.oeb.metadata.subject) - except: - tags = [] - if tags: - #tags = self.opts.dest.tags_to_string(tags) - tags = ', '.join(tags) - else: - tags = '' - - comments = mi.comments - if not comments: - try: - comments = unicode(self.oeb.metadata.description[0]) - except: - comments = '' - if not comments.strip(): - comments = '' - orig_comments = comments - if comments: - comments = comments_to_html(comments) - - footer = 'BOOK JACKET GENERATED BY %s %s' % (__appname__.upper(),__version__) - - def generate_html(comments): - args = dict(xmlns=XPNSMAP['h'], - title_str=title_str, - css=css, - title=title, - pubdate=pubdate, - series=series, - rating=rating, - tags=tags, - comments=comments, - footer = footer) - - # Post-process the generated html to strip out empty header items - generated_html = self.JACKET_TEMPLATE % args - soup = BeautifulSoup(generated_html) - if not series: - series_tag = soup.find('tr', attrs={'class':'cbj_series'}) - series_tag.extract() - if not rating: - rating_tag = soup.find('tr', attrs={'class':'cbj_rating'}) - rating_tag.extract() - if not tags: - tags_tag = soup.find('tr', attrs={'class':'cbj_tags'}) - tags_tag.extract() - if not pubdate: - pubdate_tag = soup.find('tr', attrs={'class':'cbj_pubdate'}) - pubdate_tag.extract() - if self.opts.output_profile.name != 'Kindle': - hr_tag = soup.find('hr', attrs={'class':'cbj_kindle_banner_hr'}) - hr_tag.extract() - - return soup.renderContents() + tags = [] + root = render_jacket(mi, self.opts.output_profile, star_href=href, + alt_title=unicode(self.oeb.metadata.title[0]), alt_tags=tags, + alt_comments=unicode(self.oeb.metadata.description[0])) id, href = self.oeb.manifest.generate('calibre_jacket', 'jacket.xhtml') - from calibre.ebooks.oeb.base import RECOVER_PARSER, XPath - try: - root = etree.fromstring(generate_html(comments), parser=RECOVER_PARSER) - except: - root = etree.fromstring(generate_html(escape(orig_comments)), - parser=RECOVER_PARSER) - - jacket = XPath('//h:meta[@name="calibre-content" and @content="jacket"]') - found = None - for item in list(self.oeb.spine)[:4]: - try: - if jacket(item.data): - found = item - break - except: - continue - if found is None: - item = self.oeb.manifest.add(id, href, guess_type(href)[0], data=root) - self.oeb.spine.insert(0, item, True) - else: - self.log('Found existing book jacket, replacing...') - found.data = root + item = self.oeb.manifest.add(id, href, guess_type(href)[0], data=root) + self.oeb.spine.insert(0, item, True) + def remove_existing_jacket(self): + for x in self.oeb.spine[:4]: + if XPath(JACKET_XPATH)(x.data): + self.remove_images(x, limit=sys.maxint) + self.oeb.manifest.remove(x) + break def __call__(self, oeb, opts, metadata): ''' - Add metadata in jacket.xhtml if specifed in opts + Add metadata in jacket.xhtml if specified in opts If not specified, remove previous jacket instance ''' self.oeb, self.opts, self.log = oeb, opts, oeb.log + self.remove_existing_jacket() if opts.remove_first_image: self.remove_first_image() if opts.insert_metadata: self.insert_metadata(metadata) - else: - jacket = XPath('//h:meta[@name="calibre-content" and @content="jacket"]') - for item in list(self.oeb.spine)[:4]: - if jacket(item.data): - try: - self.log.info("Removing previous jacket instance") - self.oeb.manifest.remove(item) - break - except: - continue + +# Render Jacket {{{ + +def get_rating(rating, href): + ans = '' + try: + num = float(rating)/2 + except: + return ans + num = max(0, num) + num = min(num, 5) + if num < 1: + return ans + + if href is not None: + ans = ' '.join(repeat( + 'star'% + href, int(num))) + else: + ans = u' '.join(u'\u2605') + return ans + + +def render_jacket(mi, output_profile, star_href=None, + alt_title=_('Unknown'), alt_tags=[], alt_comments=''): + css = P('jacket/stylesheet.css', data=True).decode('utf-8') + + try: + title_str = mi.title if mi.title else alt_title + except: + title_str = _('Unknown') + title = '%s' % (escape(title_str)) + + series = escape(mi.series if mi.series else '') + if mi.series and mi.series_index is not None: + series += escape(' [%s]'%mi.format_series_index()) + if not mi.series: + series = '' + + try: + pubdate = strftime(u'%Y', mi.pubdate.timetuple()) + except: + pubdate = '' + + rating = get_rating(mi.rating, star_href) + + tags = mi.tags if mi.tags else alt_tags + if tags: + tags = output_profile.tags_to_string(tags) + else: + tags = '' + + comments = mi.comments if mi.comments else alt_comments + comments = comments.strip() + orig_comments = comments + if comments: + comments = comments_to_html(comments) + + footer = 'BOOK JACKET GENERATED BY %s %s' % (__appname__.upper(),__version__) + + def generate_html(comments): + args = dict(xmlns=XHTML_NS, + title_str=title_str, + css=css, + title=title, + pubdate_label=_('Published'), pubdate=pubdate, + series_label=_('Series'), series=series, + rating_label=_('Rating'), rating=rating, + tags_label=_('Tags'), tags=tags, + comments=comments, + footer = footer) + + generated_html = P('jacket/template.xhtml', + data=True).decode('utf-8').format(**args) + + # Post-process the generated html to strip out empty header items + soup = BeautifulSoup(generated_html) + if not series: + series_tag = soup.find('tr', attrs={'class':'cbj_series'}) + series_tag.extract() + if not rating: + rating_tag = soup.find('tr', attrs={'class':'cbj_rating'}) + rating_tag.extract() + if not tags: + tags_tag = soup.find('tr', attrs={'class':'cbj_tags'}) + tags_tag.extract() + if not pubdate: + pubdate_tag = soup.find('tr', attrs={'class':'cbj_pubdate'}) + pubdate_tag.extract() + if output_profile.short_name != 'kindle': + hr_tag = soup.find('hr', attrs={'class':'cbj_kindle_banner_hr'}) + hr_tag.extract() + + return soup.renderContents(None) + + from calibre.ebooks.oeb.base import RECOVER_PARSER + + try: + root = etree.fromstring(generate_html(comments), parser=RECOVER_PARSER) + except: + try: + root = etree.fromstring(generate_html(escape(orig_comments)), + parser=RECOVER_PARSER) + except: + root = etree.fromstring(generate_html(''), + parser=RECOVER_PARSER) + return root + +# }}} + +def linearize_jacket(oeb): + for x in oeb.spine[:4]: + if XPath(JACKET_XPATH)(x.data): + for e in XPath('//h:table|//h:tr|//h:th')(x.data): + e.tag = XHTML('div') + for e in XPath('//h:td')(x.data): + e.tag = XHTML('span') + break + diff --git a/src/calibre/utils/magick/draw.py b/src/calibre/utils/magick/draw.py index 82a0237b8d..301bf9912a 100644 --- a/src/calibre/utils/magick/draw.py +++ b/src/calibre/utils/magick/draw.py @@ -5,12 +5,14 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' +import os from calibre.utils.magick import Image, DrawingWand, create_canvas from calibre.constants import __appname__, __version__ from calibre import fit_image -def save_cover_data_to(data, path, bgcolor='white', resize_to=None): +def save_cover_data_to(data, path, bgcolor='white', resize_to=None, + return_data=False): ''' Saves image in data to path, in the format specified by the path extension. Composes the image onto a blank canvas so as to @@ -22,6 +24,8 @@ def save_cover_data_to(data, path, bgcolor='white', resize_to=None): img.size = (resize_to[0], resize_to[1]) canvas = create_canvas(img.size[0], img.size[1], bgcolor) canvas.compose(img) + if return_data: + return canvas.export(os.path.splitext(path)[1][1:]) canvas.save(path) def thumbnail(data, width=120, height=120, bgcolor='white', fmt='jpg'):