mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
KG updates
This commit is contained in:
commit
47f7bacc78
@ -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:
|
||||
**
|
||||
|
34
resources/jacket/template.xhtml
Normal file
34
resources/jacket/template.xhtml
Normal file
@ -0,0 +1,34 @@
|
||||
<html xmlns="{xmlns}">
|
||||
<head>
|
||||
<title>{title_str}</title>
|
||||
<meta name="calibre-content" content="jacket"/>
|
||||
<style type="text/css" media="screen">{css}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="cbj_banner">
|
||||
<div class="cbj_title">{title}</div>
|
||||
<table class="cbj_header">
|
||||
<tr class="cbj_series">
|
||||
<td class="cbj_label">{series_label}:</td>
|
||||
<td class="cbj_content">{series}</td>
|
||||
</tr>
|
||||
<tr class="cbj_pubdate">
|
||||
<td class="cbj_label">{pubdate_label}:</td>
|
||||
<td class="cbj_content">{pubdate}</td>
|
||||
</tr>
|
||||
<tr class="cbj_rating">
|
||||
<td class="cbj_label">{rating_label}:</td>
|
||||
<td class="cbj_content">{rating}</td>
|
||||
</tr>
|
||||
<tr class="cbj_tags">
|
||||
<td class="cbj_label">{tags_label}:</td>
|
||||
<td class="cbj_content">{tags}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="cbj_footer">{footer}</div>
|
||||
</div>
|
||||
<hr class="cbj_kindle_banner_hr" />
|
||||
<div class="cbj_comments">{comments}</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -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 <h1> to <h2>
|
||||
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")
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -6,7 +6,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__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'''\
|
||||
<html xmlns="%(xmlns)s">
|
||||
<head>
|
||||
<title>%(title_str)s</title>
|
||||
<meta name="calibre-content" content="jacket"/>
|
||||
<style type="text/css" media="screen">%(css)s</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="cbj_banner">
|
||||
<div class="cbj_title">%(title)s</div>
|
||||
<table class="cbj_header">
|
||||
<tr class="cbj_series">
|
||||
<td class="cbj_label">Series:</td>
|
||||
<td class="cbj_content">%(series)s</td>
|
||||
</tr>
|
||||
<tr class="cbj_pubdate">
|
||||
<td class="cbj_label">Published:</td>
|
||||
<td class="cbj_content">%(pubdate)s</td>
|
||||
</tr>
|
||||
<tr class="cbj_rating">
|
||||
<td class="cbj_label">Rating:</td>
|
||||
<td class="cbj_content">%(rating)s</td>
|
||||
</tr>
|
||||
<tr class="cbj_tags">
|
||||
<td class="cbj_label">Tags:</td>
|
||||
<td class="cbj_content">%(tags)s</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="cbj_footer">%(footer)s</div>
|
||||
</div>
|
||||
<hr class="cbj_kindle_banner_hr" />
|
||||
<div class="cbj_comments">%(comments)s</div>
|
||||
</body>
|
||||
</html>
|
||||
''')
|
||||
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('<img style="vertical-align:text-bottom" alt="star" src="%s" />'%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 = '<span class="title">%s</span>' % (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 = 'B<span class="cbj_smallcaps">OOK JACKET GENERATED BY %s %s</span>' % (__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(
|
||||
'<img style="vertical-align:text-bottom" alt="star" src="%s" />'%
|
||||
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 = '<span class="title">%s</span>' % (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 = 'B<span class="cbj_smallcaps">OOK JACKET GENERATED BY %s %s</span>' % (__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
|
||||
|
||||
|
@ -5,12 +5,14 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__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'):
|
||||
|
Loading…
x
Reference in New Issue
Block a user