Merge 7.13

This commit is contained in:
Sengian 2010-08-09 00:36:07 +02:00
commit 12668be260
96 changed files with 21323 additions and 19696 deletions

View File

@ -4,6 +4,57 @@
# for important features/bug fixes. # for important features/bug fixes.
# Also, each release can have new and improved recipes. # Also, each release can have new and improved recipes.
- version: 0.7.13
date: 2010-08-06
new features:
- title: "Add a button to the edit metadata dialog to generate a cover based on the book metadata"
tickets: [5959]
- title: "When using series or title in a save template to generate a file path, remove leading prepositions. This behavior can be controlled via a tweak."
- title: "News download: When downloading news for the Kindle, do not add date to the title, to allow the Kindle's periodical archiving to work."
tickets: [6411]
- title: "Content Server OPDS feeds: Grouping of items by first alphabet is now case-insensitive."
- title: "Do not allow the user to use save to disk to save files into the calibre library"
tickets: [6392]
- title: "Switch to a new C based API for using ImageMagick. More robust and a minor speedup when doing image manipulations"
- title: "Move cover downloading to a plugin based API. You can now add new cover sources to calibre using plugins."
bug fixes:
- title: "Content server OPDS feeds: Handle the case when the author field is blank"
tickets: [6371]
- title: "TXT Input: Strip out illegal chars from txt files."
tickets: [6335]
- title: "Save to disk/send to device templates: Always render {series_index} as an empty string when the book has no series."
tickets: [6409]
- title: "PD Novel driver: Remove covers when deleting books"
new recipes:
- title: "Snopes"
author: Startson17
- title: "dr.dk and Balkan Insight"
author: Darko Miletic
- title: Folha de Sao Paulo
author: Saverio Palmieri Neto
improved recipes:
- Honolulu Star Advertiser
- Nature News
- Associated Press
- Scientific American
- New Scientist
- version: 0.7.12 - version: 0.7.12
date: 2010-07-30 date: 2010-07-30

View File

@ -0,0 +1,83 @@
/* CSS for the mobile version of the content server webpage */
.navigation table.buttons {
width: 100%;
}
.navigation .button {
width: 50%;
}
.button a, .button:visited a {
padding: 0.5em;
font-size: 1.25em;
border: 1px solid black;
text-color: black;
background-color: #ddd;
border-top: 1px solid ThreeDLightShadow;
border-right: 1px solid ButtonShadow;
border-bottom: 1px solid ButtonShadow;
border-left: 1 px solid ThreeDLightShadow;
-moz-border-radius: 0.25em;
-webkit-border-radius: 0.25em;
}
.button:hover a {
border-top: 1px solid #666;
border-right: 1px solid #CCC;
border-bottom: 1 px solid #CCC;
border-left: 1 px solid #666;
}
div.navigation {
padding-bottom: 1em;
clear: both;
}
#search_box {
border: 1px solid #393;
-moz-border-radius: 0.5em;
-webkit-border-radius: 0.5em;
padding: 1em;
margin-bottom: 0.5em;
float: right;
}
#listing {
width: 100%;
border-collapse: collapse;
}
#listing td {
padding: 0.25em;
}
#listing td.thumbnail {
height: 60px;
width: 60px;
}
#listing tr:nth-child(even) {
background: #eee;
}
#listing .button a{
display: inline-block;
width: 2.5em;
padding-left: 0em;
padding-right: 0em;
overflow: hidden;
text-align: center;
}
#logo {
float: left;
}
#spacer {
clear: both;
}

View File

@ -72,4 +72,11 @@ gui_pubdate_display_format = 'MMM yyyy'
# without changing anything is sufficient to change the sort. # without changing anything is sufficient to change the sort.
title_series_sorting = 'library_order' title_series_sorting = 'library_order'
# Control how title and series names are formatted when saving to disk/sending
# to device. If set to library_order, leading articles such as The and A will
# be put at the end
# If set to 'strictly_alphabetic', the titles will be sorted without processing
# For example, with library_order, "The Client" will become "Client, The". With
# strictly_alphabetic, it would remain "The Client".
save_template_title_series_sorting = 'library_order'

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

View File

@ -6,31 +6,38 @@ class AssociatedPress(BasicNewsRecipe):
title = u'Associated Press' title = u'Associated Press'
description = 'Global news' description = 'Global news'
__author__ = 'Kovid Goyal' __author__ = 'Kovid Goyal and Sujata Raman'
use_embedded_content = False use_embedded_content = False
language = 'en' language = 'en'
no_stylesheets = True
max_articles_per_feed = 15 max_articles_per_feed = 15
html2lrf_options = ['--force-page-break-before-tag="chapter"'] html2lrf_options = ['--force-page-break-before-tag="chapter"']
preprocess_regexps = [ (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in preprocess_regexps = [ (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in
[ [
(r'<HEAD>.*?</HEAD>' , lambda match : '<HEAD></HEAD>'), (r'<span class="entry-content">', lambda match : '<div class="entry-content">'),
(r'<body class="apple-rss-no-unread-mode" onLoad="setup(null)">.*?<!-- start Entries -->', lambda match : '<body>'),
(r'<!-- end apple-rss-content-area -->.*?</body>', lambda match : '</body>'),
(r'<script.*?>.*?</script>', lambda match : ''),
(r'<body.*?>.*?<span class="headline">', lambda match : '<body><span class="headline"><chapter>'),
(r'<tr><td><div class="body">.*?<p class="ap-story-p">', lambda match : '<p class="ap-story-p">'),
(r'<p class="ap-story-p">', lambda match : '<p>'),
(r'Learn more about our <a href="http://apdigitalnews.com/privacy.html">Privacy Policy</a>.*?</body>', lambda match : '</body>'),
] ]
] ]
keep_only_tags = [ dict(name='div', attrs={'class':['body']}),
dict(name='div', attrs={'class':['entry-content']}),
]
remove_tags = [dict(name='table', attrs={'class':['ap-video-table','ap-htmlfragment-table','ap-htmltable-table']}),
dict(name='span', attrs={'class':['apCaption','tabletitle']}),
dict(name='td', attrs={'bgcolor':['#333333']}),
]
extra_css = '''
.headline{font-family:Verdana,Arial,Helvetica,sans-serif;font-weight:bold;}
.bline{color:#003366;}
body{font-family:Arial,Helvetica,sans-serif;}
'''
feeds = [ ('AP Headlines', 'http://hosted.ap.org/lineups/TOPHEADS-rss_2.0.xml?SITE=ORAST&SECTION=HOME'),
('AP US News', 'http://hosted.ap.org/lineups/USHEADS-rss_2.0.xml?SITE=CAVIC&SECTION=HOME'), feeds = [
('AP Headlines', 'http://hosted.ap.org/lineups/TOPHEADS-rss_2.0.xml?SITE=ORAST&SECTION=HOME'),
('AP US News', 'http://hosted.ap.org/lineups/USHEADS-rss_2.0.xml?SITE=CAVIC&SECTION=HOME'),
('AP World News', 'http://hosted.ap.org/lineups/WORLDHEADS-rss_2.0.xml?SITE=SCAND&SECTION=HOME'), ('AP World News', 'http://hosted.ap.org/lineups/WORLDHEADS-rss_2.0.xml?SITE=SCAND&SECTION=HOME'),
('AP Political News', 'http://hosted.ap.org/lineups/POLITICSHEADS-rss_2.0.xml?SITE=ORMED&SECTION=HOME'), ('AP Political News', 'http://hosted.ap.org/lineups/POLITICSHEADS-rss_2.0.xml?SITE=ORMED&SECTION=HOME'),
('AP Washington State News', 'http://hosted.ap.org/lineups/WASHINGTONHEADS-rss_2.0.xml?SITE=NYPLA&SECTION=HOME'), ('AP Washington State News', 'http://hosted.ap.org/lineups/WASHINGTONHEADS-rss_2.0.xml?SITE=NYPLA&SECTION=HOME'),
@ -38,4 +45,5 @@ class AssociatedPress(BasicNewsRecipe):
('AP Health News', 'http://hosted.ap.org/lineups/HEALTHHEADS-rss_2.0.xml?SITE=FLDAY&SECTION=HOME'), ('AP Health News', 'http://hosted.ap.org/lineups/HEALTHHEADS-rss_2.0.xml?SITE=FLDAY&SECTION=HOME'),
('AP Science News', 'http://hosted.ap.org/lineups/SCIENCEHEADS-rss_2.0.xml?SITE=OHCIN&SECTION=HOME'), ('AP Science News', 'http://hosted.ap.org/lineups/SCIENCEHEADS-rss_2.0.xml?SITE=OHCIN&SECTION=HOME'),
('AP Strange News', 'http://hosted.ap.org/lineups/STRANGEHEADS-rss_2.0.xml?SITE=WCNC&SECTION=HOME'), ('AP Strange News', 'http://hosted.ap.org/lineups/STRANGEHEADS-rss_2.0.xml?SITE=WCNC&SECTION=HOME'),
] ]

View File

@ -0,0 +1,62 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
balkaninsight.com
'''
import re
from calibre.web.feeds.news import BasicNewsRecipe
class BalkanInsight(BasicNewsRecipe):
title = 'Balkan Insight'
__author__ = 'Darko Miletic'
description = 'Get exclusive news and in depth information on business, politics, events and lifestyle in the Balkans. Free and exclusive premium content.'
publisher = 'BalkanInsight.com'
category = 'news, politics, Balcans'
oldest_article = 2
max_articles_per_feed = 100
no_stylesheets = False
use_embedded_content = False
encoding = 'utf-8'
masthead_url = 'http://www.balkaninsight.com/templates/balkaninsight/images/aindex_02.jpg'
language = 'en'
publication_type = 'newsportal'
remove_empty_feeds = True
extra_css = """ @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)}
@font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
.article_description,body{font-family: Arial,Verdana,Helvetica,sans1,sans-serif}
img{margin-bottom: 0.8em}
h1,h2,h3,h4{font-family: Times,Georgia,serif1,serif; color: #24569E}
.article-deck {color:#777777; font-size: small;}
.main_news_img{font-size: small} """
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
keep_only_tags = [dict(name='div', attrs={'id':'article'})]
remove_tags = [
dict(name=['object','link','iframe'])
]
feeds = [
(u'Albania' , u'http://www.balkaninsight.com/?tpl=653&tpid=144' )
,(u'Bosnia' , u'http://www.balkaninsight.com/?tpl=653&tpid=145' )
,(u'Bulgaria' , u'http://www.balkaninsight.com/?tpl=653&tpid=146' )
,(u'Croatia' , u'http://www.balkaninsight.com/?tpl=653&tpid=147' )
,(u'Kosovo' , u'http://www.balkaninsight.com/?tpl=653&tpid=148' )
,(u'Macedonia' , u'http://www.balkaninsight.com/?tpl=653&tpid=149' )
,(u'Montenegro' , u'http://www.balkaninsight.com/?tpl=653&tpid=150' )
,(u'Romania' , u'http://www.balkaninsight.com/?tpl=653&tpid=151' )
,(u'Serbia' , u'http://www.balkaninsight.com/?tpl=653&tpid=152' )
]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return self.adeify_images(soup)

View File

@ -0,0 +1,42 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
dr.dk
'''
from calibre.web.feeds.news import BasicNewsRecipe
class dr_dk(BasicNewsRecipe):
title = 'DR Nyheder'
__author__ = 'Darko Miletic'
description = 'Myndighederne indfører nu eskorte af brandbiler og ambulancer i Ishøj af frygt for hærværk.'
publisher = 'Nyhedsbureauet DR Nyheder'
category = 'news, politics, Denmark'
oldest_article = 2
max_articles_per_feed = 200
no_stylesheets = True
delay = 1
encoding = 'utf8'
use_embedded_content = False
language = 'da'
extra_css = """ body{font-family: Verdana,Arial,sans-serif }
img{margin-bottom: 0.4em}
.txtContent,.stamp{font-size: small}
"""
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
keep_only_tags = [dict(name='div', attrs={'class':'articleContent'})]
remove_attributes=['xmlns:msxsl','width','height']
feeds = [(u'All news', u'http://www.dr.dk/Nyheder/Service/feeds/Allenyheder.htm')]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return soup

View File

@ -0,0 +1,74 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2010, Saverio Palmieri Neto <saverio.palmieri at gmail.com>'
'''
folha.uol.com.br
'''
from calibre.web.feeds.news import BasicNewsRecipe
class FolhaOnline(BasicNewsRecipe):
title = 'Folha de Sao Paulo'
__author__ = 'Saverio Palmieri Neto'
description = 'Brazilian news from Folha de Sao Paulo Online'
publisher = 'Folha de Sao Paulo'
category = 'Brasil, news'
oldest_article = 2
max_articles_per_feed = 1000
summary_length = 2048
no_stylesheets = True
use_embedded_content = False
timefmt = ' [%d %b %Y (%a)]'
encoding = 'cp1252'
cover_url = 'http://lh5.ggpht.com/_hEb7sFmuBvk/TFoiKLRS5dI/AAAAAAAAADM/kcVKggZwKnw/capa_folha.jpg'
cover_margins = (5,5,'white')
remove_javascript = True
keep_only_tags = [dict(name='div', attrs={'id':'articleNew'})]
remove_tags = [
dict(name='script')
,dict(name='div',
attrs={'id':[
'articleButton'
,'bookmarklets'
,'ad-180x150-1'
,'contextualAdsArticle'
,'articleEnd'
,'articleComments'
]})
,dict(name='div',
attrs={'class':[
'openBox adslibraryArticle'
]})
,dict(name='a')
,dict(name='iframe')
,dict(name='link')
]
feeds = [
(u'Em cima da hora', u'http://feeds.folha.uol.com.br/emcimadahora/rss091.xml')
,(u'Ambiente', u'http://feeds.folha.uol.com.br/ambiente/rss091.xml')
,(u'Bichos', u'http://feeds.folha.uol.com.br/bichos/rss091.xml')
,(u'Poder', u'http://feeds.folha.uol.com.br/poder/rss091.xml')
,(u'Ciencia', u'http://feeds.folha.uol.com.br/ciencia/rss091.xml')
,(u'Cotidiano', u'http://feeds.folha.uol.com.br/cotidiado/rss091.xml')
,(u'Saber', u'http://feeds.folha.uol.com.br/saber/rss091.xml')
,(u'Equilíbrio e Saúde', u'http://feeds.folha.uol.com.br/equilibrioesaude/rss091.xml')
,(u'Esporte', u'http://feeds.folha.uol.com.br/esporte/rss091.xml')
,(u'Ilustrada', u'http://feeds.folha.uol.com.br/ilustrada/rss091.xml')
,(u'Ilustríssima', u'http://feeds.folha.uol.com.br/ilustrissima/rss091.xml')
,(u'Mercado', u'http://feeds.folha.uol.com.br/mercado/rss091.xml')
,(u'Mundo', u'http://feeds.folha.uol.com.br/mundo/rss091.xml')
,(u'Tec', u'http://feeds.folha.uol.com.br/tec/rss091.xml')
,(u'Turismo', u'http://feeds.folha.uol.com.br/turismo/rss091.xml')
]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return soup
language = 'pt'

View File

@ -4,28 +4,23 @@ import re
class NatureNews(BasicNewsRecipe): class NatureNews(BasicNewsRecipe):
title = u'Nature News' title = u'Nature News'
language = 'en' language = 'en'
__author__ = 'Krittika Goyal' __author__ = 'Krittika Goyal, Starson17'
oldest_article = 31 #days oldest_article = 31 #days
remove_empty_feeds = True
max_articles_per_feed = 50 max_articles_per_feed = 50
#encoding = 'latin1'
no_stylesheets = True no_stylesheets = True
remove_tags_before = dict(name='h1', attrs={'class':'heading entry-title'}) remove_tags_before = dict(name='h1', attrs={'class':'heading entry-title'})
remove_tags_after = dict(name='h2', attrs={'id':'comments'}) remove_tags_after = dict(name='h2', attrs={'id':'comments'})
remove_tags = [ remove_tags = [
#dict(name='iframe'),
#dict(name='div', attrs={'class':['pt-box-title', 'pt-box-content']}),
#dict(name='div', attrs={'id':['block-td_search_160', 'block-cam_search_160']}),
dict(name='h2', attrs={'id':'comments'}), dict(name='h2', attrs={'id':'comments'}),
dict(name='ul', attrs={'class':'toolsmenu xoxo'}), dict(attrs={'alt':'Advertisement'}),
dict(name='div', attrs={'class':'ad'}),
] ]
preprocess_regexps = [ preprocess_regexps = [
(re.compile(r'<script.*?</script>', re.DOTALL), lambda m: '') (re.compile(r'<p>ADVERTISEMENT</p>', re.DOTALL|re.IGNORECASE), lambda match: ''),
] ]
feeds = [('Nature News', 'http://feeds.nature.com/news/rss/most_recent')] feeds = [('Nature News', 'http://feeds.nature.com/news/rss/most_recent')]
def get_article_url(self, article):
return article.get('id')

View File

@ -6,10 +6,9 @@ www.standardmedia.co.ke
import os import os
from calibre import strftime, __appname__, __version__ from calibre import strftime, __appname__, __version__
import calibre.utils.PythonMagickWand as pw
from ctypes import byref
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from calibre.constants import preferred_encoding from calibre.constants import preferred_encoding
from calibre.utils.magick import Image
class NationKeRecipe(BasicNewsRecipe): class NationKeRecipe(BasicNewsRecipe):
@ -95,19 +94,9 @@ class NationKeRecipe(BasicNewsRecipe):
self.cover_img_path = None self.cover_img_path = None
def prepare_cover_image(self, path_to_image, out_path): def prepare_cover_image(self, path_to_image, out_path):
with pw.ImageMagick(): img = Image()
img = pw.NewMagickWand() img.open(path_to_image)
if img < 0: img.save(out_path)
raise RuntimeError('Out of memory')
if not pw.MagickReadImage(img, path_to_image):
severity = pw.ExceptionType(0)
msg = pw.MagickGetException(img, byref(severity))
raise IOError('Failed to read image from: %s: %s'
%(path_to_image, msg))
if not pw.MagickWriteImage(img, out_path):
raise RuntimeError('Failed to save image to %s'%out_path)
pw.DestroyMagickWand(img)
def default_cover(self, cover_file): def default_cover(self, cover_file):
''' '''

View File

@ -1,4 +1,3 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008-2010, AprilHare, Darko Miletic <darko.miletic at gmail.com>' __copyright__ = '2008-2010, AprilHare, Darko Miletic <darko.miletic at gmail.com>'
''' '''
@ -36,7 +35,7 @@ class NewScientist(BasicNewsRecipe):
remove_tags = [ remove_tags = [
dict(name='div' , attrs={'class':['hldBd','adline','pnl','infotext' ]}) dict(name='div' , attrs={'class':['hldBd','adline','pnl','infotext' ]})
,dict(name='div' , attrs={'id' :['compnl','artIssueInfo','artTools','comments','blgsocial']}) ,dict(name='div' , attrs={'id' :['compnl','artIssueInfo','artTools','comments','blgsocial','sharebtns']})
,dict(name='p' , attrs={'class':['marker','infotext' ]}) ,dict(name='p' , attrs={'class':['marker','infotext' ]})
,dict(name='meta' , attrs={'name' :'description' }) ,dict(name='meta' , attrs={'name' :'description' })
,dict(name='a' , attrs={'rel' :'tag' }) ,dict(name='a' , attrs={'rel' :'tag' })

View File

@ -14,33 +14,39 @@ class Nspm(BasicNewsRecipe):
description = 'Casopis za politicku teoriju i drustvena istrazivanja' description = 'Casopis za politicku teoriju i drustvena istrazivanja'
publisher = 'NSPM' publisher = 'NSPM'
category = 'news, politics, Serbia' category = 'news, politics, Serbia'
oldest_article = 2 oldest_article = 7
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True
use_embedded_content = False use_embedded_content = False
INDEX = 'http://www.nspm.rs/?alphabet=l' INDEX = 'http://www.nspm.rs/?alphabet=l'
encoding = 'utf-8' encoding = 'utf-8'
language = 'sr' language = 'sr'
delay = 2
publication_type = 'magazine' publication_type = 'magazine'
masthead_url = 'http://www.nspm.rs/templates/jsn_epic_pro/images/logol.jpg' masthead_url = 'http://www.nspm.rs/templates/jsn_epic_pro/images/logol.jpg'
extra_css = ' @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} body{font-family: "Times New Roman", serif1, serif} .article_description{font-family: Arial, sans1, sans-serif} img{margin-top:0.5em; margin-bottom: 0.7em} .author{color: #990000; font-weight: bold} .author,.createdate{font-size: 0.9em} img{margin-top:0.5em; margin-bottom: 0.7em} ' extra_css = """ @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)}
@font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
body{font-family: "Times New Roman", serif1, serif}
.article_description{font-family: Arial, sans1, sans-serif}
img{margin-top:0.5em; margin-bottom: 0.7em}
.author{color: #990000; font-weight: bold}
.author,.createdate{font-size: 0.9em} """
conversion_options = { conversion_options = {
'comment' : description 'comment' : description
, 'tags' : category , 'tags' : category
, 'publisher' : publisher , 'publisher' : publisher
, 'language' : language , 'language' : language
, 'linearize_tables' : True
} }
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')] preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
keep_only_tags = [dict(attrs={'id':'jsn-mainbody'})]
remove_tags = [ remove_tags = [
dict(name=['link','object','embed','script','meta']) dict(name=['link','object','embed','script','meta','base','iframe'])
,dict(name='td', attrs={'class':'buttonheading'}) ,dict(attrs={'class':'buttonheading'})
] ]
keep_only_tags = [ remove_tags_after = dict(attrs={'class':'article_separator'})
dict(attrs={'class':['contentpagetitle','author','createdate']})
,dict(name='p')
]
remove_attributes = ['width','height'] remove_attributes = ['width','height']
def get_browser(self): def get_browser(self):
@ -48,25 +54,18 @@ class Nspm(BasicNewsRecipe):
br.open(self.INDEX) br.open(self.INDEX)
return br return br
feeds = [(u'Nova srpska politicka misao', u'http://www.nspm.rs/feed/rss.html')] feeds = [
(u'Rubrike' , u'http://www.nspm.rs/rubrike/feed/rss.html')
def print_version(self, url): ,(u'Debate' , u'http://www.nspm.rs/debate/feed/rss.html')
return url.replace('.html','/stampa.html') ,(u'Reci i misli' , u'http://www.nspm.rs/reci-i-misli/feed/rss.html')
,(u'Samo smeh srbina spasava', u'http://www.nspm.rs/samo-smeh-srbina-spasava/feed/rss.html')
,(u'Polemike' , u'http://www.nspm.rs/polemike/feed/rss.html')
,(u'Prikazi' , u'http://www.nspm.rs/prikazi/feed/rss.html')
,(u'Prenosimo' , u'http://www.nspm.rs/prenosimo/feed/rss.html')
,(u'Hronika' , u'http://www.nspm.rs/tabela/hronika/feed/rss.html')
]
def preprocess_html(self, soup): def preprocess_html(self, soup):
for item in soup.body.findAll(style=True): for item in soup.body.findAll(style=True):
del item['style'] del item['style']
att = soup.find('a',attrs={'class':'contentpagetitle'}) return self.adeify_images(soup)
if att:
att.name = 'h1';
del att['href']
att2 = soup.find('td')
if att2:
att2.name = 'p';
del att['valign']
for pt in soup.findAll('img'):
brtag = Tag(soup,'br')
brtag2 = Tag(soup,'br')
pt.append(brtag)
pt.append(brtag2)
return soup

View File

@ -1,7 +1,5 @@
#!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Darko Miletic <darko.miletic at gmail.com>' __copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>'
''' '''
nspm.rs/nspm-in-english nspm.rs/nspm-in-english
''' '''
@ -12,28 +10,43 @@ class Nspm_int(BasicNewsRecipe):
title = 'NSPM in English' title = 'NSPM in English'
__author__ = 'Darko Miletic' __author__ = 'Darko Miletic'
description = 'Magazine dedicated to political theory and sociological research' description = 'Magazine dedicated to political theory and sociological research'
oldest_article = 20 publisher = 'NSPM'
category = 'news, politics, Serbia'
oldest_article = 7
max_articles_per_feed = 100 max_articles_per_feed = 100
language = 'en'
no_stylesheets = True no_stylesheets = True
use_embedded_content = False use_embedded_content = False
INDEX = 'http://www.nspm.rs/?alphabet=l' encoding = 'utf-8'
cover_url = 'http://nspm.rs/templates/jsn_epic_pro/images/logol.jpg' language = 'en'
html2lrf_options = [ delay = 2
'--comment', description publication_type = 'magazine'
, '--base-font-size', '10' masthead_url = 'http://www.nspm.rs/templates/jsn_epic_pro/images/logol.jpg'
, '--category', 'news, politics, Serbia, english' extra_css = """
, '--publisher', 'IIC NSPM' body{font-family: "Times New Roman", serif}
] .article_description{font-family: Arial, sans-serif}
img{margin-top:0.5em; margin-bottom: 0.7em}
.author{color: #990000; font-weight: bold}
.author,.createdate{font-size: 0.9em} """
def get_browser(self): conversion_options = {
br = BasicNewsRecipe.get_browser() 'comment' : description
br.open(self.INDEX) , 'tags' : category
return br , 'publisher' : publisher
, 'language' : language
, 'linearize_tables' : True
}
keep_only_tags = [dict(attrs={'id':'jsn-mainbody'})]
remove_tags = [
dict(name=['link','object','embed','script','meta','base','iframe'])
,dict(attrs={'class':'buttonheading'})
]
remove_tags_after = dict(attrs={'class':'article_separator'})
remove_attributes = ['width','height']
keep_only_tags = [dict(name='div', attrs={'id':'jsn-mainbody'})] feeds = [(u'Articles', u'http://www.nspm.rs/nspm-in-english/feed/rss.html')]
remove_tags = [dict(name='div', attrs={'id':'yvComment' })]
feeds = [ (u'NSPM in English', u'http://nspm.rs/nspm-in-english/feed/rss.html')] def preprocess_html(self, soup):
for item in soup.body.findAll(style=True):
del item['style']
return self.adeify_images(soup)

View File

@ -14,7 +14,7 @@ class ScientificAmerican(BasicNewsRecipe):
description = u'Popular science. Monthly magazine.' description = u'Popular science. Monthly magazine.'
__author__ = 'Kovid Goyal and Sujata Raman' __author__ = 'Kovid Goyal and Sujata Raman'
language = 'en' language = 'en'
remove_javascript = True
oldest_article = 30 oldest_article = 30
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True
@ -31,11 +31,13 @@ class ScientificAmerican(BasicNewsRecipe):
remove_tags_after = dict(id=['article']) remove_tags_after = dict(id=['article'])
remove_tags = [ remove_tags = [
dict(id=['sharetools', 'reddit']), dict(id=['sharetools', 'reddit']),
dict(name='script'), #dict(name='script'),
{'class':['float_left', 'atools']}, {'class':['float_left', 'atools']},
{"class": re.compile(r'also-in-this')}, {"class": re.compile(r'also-in-this')},
dict(name='a',title = ["Get the Rest of the Article","Subscribe","Buy this Issue"]), dict(name='a',title = ["Get the Rest of the Article","Subscribe","Buy this Issue"]),
dict(name = 'img',alt = ["Graphic - Get the Rest of the Article"]), dict(name = 'img',alt = ["Graphic - Get the Rest of the Article"]),
dict(name='div', attrs={'class':['commentbox']}),
dict(name='h2', attrs={'class':['discuss_h2']}),
] ]
html2lrf_options = ['--base-font-size', '8'] html2lrf_options = ['--base-font-size', '8']
@ -110,3 +112,10 @@ class ScientificAmerican(BasicNewsRecipe):
div.extract() div.extract()
return soup return soup
preprocess_regexps = [
(re.compile(r'Already a Digital subscriber.*Now</a>', re.DOTALL|re.IGNORECASE), lambda match: ''),
(re.compile(r'If your institution has site license access, enter.*here</a>.', re.DOTALL|re.IGNORECASE), lambda match: ''),
(re.compile(r'to subscribe to our.*;.*\}', re.DOTALL|re.IGNORECASE), lambda match: ''),
(re.compile(r'\)\(jQuery\);.*-->', re.DOTALL|re.IGNORECASE), lambda match: ''),
]

View File

@ -0,0 +1,49 @@
from calibre.web.feeds.news import BasicNewsRecipe
import re
class Skeptic(BasicNewsRecipe):
title = u'The Skeptic'
description = 'Discussions with leading experts and investigation of fringe science and paranormal claims.'
language = 'en'
__author__ = 'Starson17'
oldest_article = 31
cover_url = 'http://www.skeptricks.com/images/Skeptic_Magazine.jpg'
remove_empty_feeds = True
remove_javascript = True
max_articles_per_feed = 50
no_stylesheets = True
remove_tags = [dict(name='div', attrs={'class':['Introduction','divider']}),
dict(name='div', attrs={'id':['feature', 'podcast']}),
dict(name='div', attrs={'id':re.compile(r'follow.*', re.DOTALL|re.IGNORECASE)}),
dict(name='hr'),
]
feeds = [
('The Skeptic', 'http://www.skeptic.com/feed'),
('E-Skeptic', 'http://www.skeptic.com/eskeptic'),
('All-SkepticBlog', 'http://skepticblog.org/feed'),
('Brian Dunning', 'http://skepticblog.org/author/dunning/feed/'),
('Daniel Loxton', 'http://skepticblog.org/author/loxton/feed/'),
('Kirsten Sanford', 'http://skepticblog.org/author/sanford/feed/'),
('Mark Edward', 'http://skepticblog.org/author/edward/feed/'),
('Michael Shermer', 'http://skepticblog.org/author/shermer/feed/'),
('Phil Plait', 'http://skepticblog.org/author/plait/feed/'),
('Ryan Johnson', 'http://skepticblog.org/author/johnson/feed/'),
('Steven Novella', 'http://skepticblog.org/author/novella/feed/'),
('Yau-Man Chan', 'http://skepticblog.org/author/chan/feed/'),
]
def get_browser(self):
br = BasicNewsRecipe.get_browser(self)
br.addheaders = [('Accept', 'text/html')]
return br
extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
'''

View File

@ -0,0 +1,50 @@
from calibre.web.feeds.news import BasicNewsRecipe
import re
class TheSkepticalInquirer(BasicNewsRecipe):
title = u'The Skeptical Inquirer'
description = 'Investigation of fringe science and paranormal claims.'
language = 'en'
__author__ = 'Starson17'
oldest_article = 31
cover_url = 'http://www.skeptricks.com/images/Skeptical_Inquirer_Magazine.jpg'
remove_empty_feeds = True
remove_javascript = True
max_articles_per_feed = 50
no_stylesheets = True
keep_only_tags = [dict(name='div', attrs={'id':['content', 'bio']})]
remove_tags = [
dict(name='div', attrs={'id':['socialMedia']}),
]
preprocess_regexps = [
(re.compile(r'\.\(JavaScript must be enabled to view this email address\)', re.DOTALL|re.IGNORECASE), lambda match: ''),
]
def parse_index(self):
feeds = []
for title, url in [("The Skeptical Inquirer", "http://www.csicop.org")]:
articles = self.make_links(url)
if articles:
feeds.append((title, articles))
return feeds
def make_links(self, url):
soup = self.index_to_soup(url)
title = ''
current_articles = []
for item in soup.findAll(attrs={'class':['article-single bigger']}):
page_url = url + str(item.a["href"])
title = str(item.a.string)
current_articles.append({'title': title, 'url': page_url, 'description':'', 'date':''})
return current_articles
extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
'''

View File

@ -0,0 +1,46 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Starson17'
'''
snopes.com
'''
from calibre.web.feeds.recipes import BasicNewsRecipe
class Snopes(BasicNewsRecipe):
title = 'Snopes'
__author__ = 'Starson17'
description = 'Urban Legends'
oldest_article = 21
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
encoding = 'utf8'
publisher = 'Snopes'
category = 'news, '
language = 'en'
publication_type = 'newsportal'
remove_javascript = True
no_stylesheets = True
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
,'linearize_tables': True
}
keep_only_tags = [
dict(name='h1'),
dict(name='div', attrs={'class':['article_text']}),
]
feeds = [
('Snopes', 'http://www.snopes.com/info/whatsnew.xml'),
]
extra_css = '''
h1{font-family:Trebuchet MS,Bookman Old Style,Arial;color:#75b570}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:medium;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Arial,Helvetica,sans-serif;font-size:small;}
'''

View File

@ -6,11 +6,10 @@ www.standardmedia.co.ke
import os import os
from calibre import strftime, __appname__, __version__ from calibre import strftime, __appname__, __version__
import calibre.utils.PythonMagickWand as pw
from ctypes import byref
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from calibre.constants import preferred_encoding from calibre.constants import preferred_encoding
from calibre.utils.magick import Image
class StandardMediaKeRecipe(BasicNewsRecipe): class StandardMediaKeRecipe(BasicNewsRecipe):
@ -88,19 +87,9 @@ class StandardMediaKeRecipe(BasicNewsRecipe):
self.cover_img_path = None self.cover_img_path = None
def prepare_cover_image(self, path_to_image, out_path): def prepare_cover_image(self, path_to_image, out_path):
with pw.ImageMagick(): img = Image()
img = pw.NewMagickWand() img.open(path_to_image)
if img < 0: img.save(out_path)
raise RuntimeError('Out of memory')
if not pw.MagickReadImage(img, path_to_image):
severity = pw.ExceptionType(0)
msg = pw.MagickGetException(img, byref(severity))
raise IOError('Failed to read image from: %s: %s'
%(path_to_image, msg))
if not pw.MagickWriteImage(img, out_path):
raise RuntimeError('Failed to save image to %s'%out_path)
pw.DestroyMagickWand(img)
def default_cover(self, cover_file): def default_cover(self, cover_file):
''' '''

View File

@ -30,10 +30,12 @@ class Starbulletin(BasicNewsRecipe):
} }
remove_tags_before = dict(attrs={'id':'storyTitle'}) remove_tags_before = dict(attrs={'id':'storyTitle'})
remove_tags_after = dict(name='div', attrs={'class':'storytext'}) remove_tags_after = dict(name='div',attrs={'class':'storytext'})
remove_tags = [ remove_tags = [
dict(name=['object','link']) dict(name=['object','link','script','span'])
,dict(attrs={'class':'insideStoryImage'}) ,dict(attrs={'class':'insideStoryImage'})
,dict(attrs={'name':'fb_share'})
,dict(name='div',attrs={'class':'storytext'})
] ]
feeds = [ feeds = [

View File

@ -1,314 +0,0 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
'''
online.wsj.com
'''
import re
from calibre.web.feeds.recipes import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import Tag, NavigableString
from datetime import timedelta, date
class WSJ(BasicNewsRecipe):
# formatting adapted from original recipe by Kovid Goyal and Sujata Raman
title = u'Wall Street Journal (free)'
__author__ = 'Nick Redding'
language = 'en'
description = ('All the free content from the Wall Street Journal (business, financial and political news)')
no_stylesheets = True
timefmt = ' [%b %d]'
# customization notes: delete sections you are not interested in
# set omit_paid_content to False if you want the paid content article snippets
# set oldest_article to the maximum number of days back from today to include articles
sectionlist = [
['/home-page','Front Page'],
['/public/page/news-opinion-commentary.html','Commentary'],
['/public/page/news-global-world.html','World News'],
['/public/page/news-world-business.html','US News'],
['/public/page/news-business-us.html','Business'],
['/public/page/news-financial-markets-stock.html','Markets'],
['/public/page/news-tech-technology.html','Technology'],
['/public/page/news-personal-finance.html','Personal Finnce'],
['/public/page/news-lifestyle-arts-entertainment.html','Life & Style'],
['/public/page/news-real-estate-homes.html','Real Estate'],
['/public/page/news-career-jobs.html','Careers'],
['/public/page/news-small-business-marketing.html','Small Business']
]
oldest_article = 2
omit_paid_content = True
extra_css = '''h1{font-size:large; font-family:Times,serif;}
h2{font-family:Times,serif; font-size:small; font-style:italic;}
.subhead{font-family:Times,serif; font-size:small; font-style:italic;}
.insettipUnit {font-family:Times,serif;font-size:xx-small;}
.targetCaption{font-size:x-small; font-family:Times,serif; font-style:italic; margin-top: 0.25em;}
.article{font-family:Times,serif; font-size:x-small;}
.tagline { font-size:xx-small;}
.dateStamp {font-family:Times,serif;}
h3{font-family:Times,serif; font-size:xx-small;}
.byline {font-family:Times,serif; font-size:xx-small; list-style-type: none;}
.metadataType-articleCredits {list-style-type: none;}
h6{font-family:Times,serif; font-size:small; font-style:italic;}
.paperLocation{font-size:xx-small;}'''
remove_tags_before = dict({'class':re.compile('^articleHeadlineBox')})
remove_tags = [ dict({'id':re.compile('^articleTabs_tab_')}),
#dict(id=["articleTabs_tab_article", "articleTabs_tab_comments",
# "articleTabs_tab_interactive","articleTabs_tab_video",
# "articleTabs_tab_map","articleTabs_tab_slideshow"]),
{'class': ['footer_columns','network','insetCol3wide','interactive','video','slideshow','map',
'insettip','insetClose','more_in', "insetContent",
# 'articleTools_bottom','articleTools_bottom mjArticleTools',
'aTools', 'tooltip',
'adSummary', 'nav-inline','insetFullBracket']},
dict({'class':re.compile('^articleTools_bottom')}),
dict(rel='shortcut icon')
]
remove_tags_after = [dict(id="article_story_body"), {'class':"article story"}]
def get_browser(self):
br = BasicNewsRecipe.get_browser()
return br
def preprocess_html(self,soup):
def decode_us_date(datestr):
udate = datestr.strip().lower().split()
m = ['january','february','march','april','may','june','july','august','september','october','november','december'].index(udate[0])+1
d = int(udate[1])
y = int(udate[2])
return date(y,m,d)
# check if article is paid content
if self.omit_paid_content:
divtags = soup.findAll('div','tooltip')
if divtags:
for divtag in divtags:
if divtag.find(text="Subscriber Content"):
return None
# check if article is too old
datetag = soup.find('li',attrs={'class' : re.compile("^dateStamp")})
if datetag:
dateline_string = self.tag_to_string(datetag,False)
date_items = dateline_string.split(',')
datestring = date_items[0]+date_items[1]
article_date = decode_us_date(datestring)
earliest_date = date.today() - timedelta(days=self.oldest_article)
if article_date < earliest_date:
self.log("Skipping article dated %s" % datestring)
return None
datetag.parent.extract()
# place dateline in article heading
bylinetag = soup.find('h3','byline')
if bylinetag:
h3bylinetag = bylinetag
else:
bylinetag = soup.find('li','byline')
if bylinetag:
h3bylinetag = bylinetag.h3
if not h3bylinetag:
h3bylinetag = bylinetag
bylinetag = bylinetag.parent
if bylinetag:
if h3bylinetag.a:
bylinetext = 'By '+self.tag_to_string(h3bylinetag.a,False)
else:
bylinetext = self.tag_to_string(h3bylinetag,False)
h3byline = Tag(soup,'h3',[('class','byline')])
if bylinetext.isspace() or (bylinetext == ''):
h3byline.insert(0,NavigableString(date_items[0]+','+date_items[1]))
else:
h3byline.insert(0,NavigableString(bylinetext+u'\u2014'+date_items[0]+','+date_items[1]))
bylinetag.replaceWith(h3byline)
else:
headlinetag = soup.find('div',attrs={'class' : re.compile("^articleHeadlineBox")})
if headlinetag:
dateline = Tag(soup,'h3', [('class','byline')])
dateline.insert(0,NavigableString(date_items[0]+','+date_items[1]))
headlinetag.insert(len(headlinetag),dateline)
else: # if no date tag, don't process this page--it's not a news item
return None
# This gets rid of the annoying superfluous bullet symbol preceding columnist bylines
ultag = soup.find('ul',attrs={'class' : 'cMetadata metadataType-articleCredits'})
if ultag:
a = ultag.h3
if a:
ultag.replaceWith(a)
return soup
def parse_index(self):
articles = {}
key = None
ans = []
def parse_index_page(page_name,page_title):
def article_title(tag):
atag = tag.find('h2') # title is usually in an h2 tag
if not atag: # if not, get text from the a tag
atag = tag.find('a',href=True)
if not atag:
return ''
t = self.tag_to_string(atag,False)
if t == '':
# sometimes the title is in the second a tag
atag.extract()
atag = tag.find('a',href=True)
if not atag:
return ''
return self.tag_to_string(atag,False)
return t
return self.tag_to_string(atag,False)
def article_author(tag):
atag = tag.find('strong') # author is usually in a strong tag
if not atag:
atag = tag.find('h4') # if not, look for an h4 tag
if not atag:
return ''
return self.tag_to_string(atag,False)
def article_summary(tag):
atag = tag.find('p')
if not atag:
return ''
subtag = atag.strong
if subtag:
subtag.extract()
return self.tag_to_string(atag,False)
def article_url(tag):
atag = tag.find('a',href=True)
if not atag:
return ''
url = re.sub(r'\?.*', '', atag['href'])
return url
def handle_section_name(tag):
# turns a tag into a section name with special processing
# for Wat's News, U.S., World & U.S. and World
s = self.tag_to_string(tag,False)
if ("What" in s) and ("News" in s):
s = "What's News"
elif (s == "U.S.") or (s == "World & U.S.") or (s == "World"):
s = s + " News"
return s
mainurl = 'http://online.wsj.com'
pageurl = mainurl+page_name
#self.log("Page url %s" % pageurl)
soup = self.index_to_soup(pageurl)
# Find each instance of div with class including "headlineSummary"
for divtag in soup.findAll('div',attrs={'class' : re.compile("^headlineSummary")}):
# divtag contains all article data as ul's and li's
# first, check if there is an h3 tag which provides a section name
stag = divtag.find('h3')
if stag:
if stag.parent.get('class', '') == 'dynamic':
# a carousel of articles is too complex to extract a section name
# for each article, so we'll just call the section "Carousel"
section_name = 'Carousel'
else:
section_name = handle_section_name(stag)
else:
section_name = "What's News"
#self.log("div Section %s" % section_name)
# find each top-level ul in the div
# we don't restrict to class = newsItem because the section_name
# sometimes changes via a ul tag inside the div
for ultag in divtag.findAll('ul',recursive=False):
stag = ultag.find('h3')
if stag:
if stag.parent.name == 'ul':
# section name has changed
section_name = handle_section_name(stag)
#self.log("ul Section %s" % section_name)
# delete the h3 tag so it doesn't get in the way
stag.extract()
# find each top level li in the ul
for litag in ultag.findAll('li',recursive=False):
stag = litag.find('h3')
if stag:
# section name has changed
section_name = handle_section_name(stag)
#self.log("li Section %s" % section_name)
# delete the h3 tag so it doesn't get in the way
stag.extract()
# if there is a ul tag inside the li it is superfluous;
# it is probably a list of related articles
utag = litag.find('ul')
if utag:
utag.extract()
# now skip paid subscriber articles if desired
subscriber_tag = litag.find(text="Subscriber Content")
if subscriber_tag:
if self.omit_paid_content:
continue
# delete the tip div so it doesn't get in the way
tiptag = litag.find("div", { "class" : "tipTargetBox" })
if tiptag:
tiptag.extract()
h1tag = litag.h1
# if there's an h1 tag, it's parent is a div which should replace
# the li tag for the analysis
if h1tag:
litag = h1tag.parent
h5tag = litag.h5
if h5tag:
# section mame has changed
section_name = self.tag_to_string(h5tag,False)
#self.log("h5 Section %s" % section_name)
# delete the h5 tag so it doesn't get in the way
h5tag.extract()
url = article_url(litag)
if url == '':
continue
if url.startswith("/article"):
url = mainurl+url
if not url.startswith("http://online.wsj.com"):
continue
if not url.endswith(".html"):
continue
if 'video' in url:
continue
title = article_title(litag)
if title == '':
continue
#self.log("URL %s" % url)
#self.log("Title %s" % title)
pubdate = ''
#self.log("Date %s" % pubdate)
author = article_author(litag)
if author == '':
author = section_name
elif author == section_name:
author = ''
else:
author = section_name+': '+author
#if not author == '':
# self.log("Author %s" % author)
description = article_summary(litag)
#if not description == '':
# self.log("Description %s" % description)
if not articles.has_key(page_title):
articles[page_title] = []
articles[page_title].append(dict(title=title,url=url,date=pubdate,description=description,author=author,content=''))
for page_name,page_title in self.sectionlist:
parse_index_page(page_name,page_title)
ans.append(page_title)
ans = [(key, articles[key]) for key in ans if articles.has_key(key)]
return ans

View File

@ -72,6 +72,13 @@ extensions = [
lib_dirs=chmlib_lib_dirs, lib_dirs=chmlib_lib_dirs,
cflags=["-D__PYTHON__"]), cflags=["-D__PYTHON__"]),
Extension('magick',
['calibre/utils/magick/magick.c'],
headers=['calibre/utils/magick/magick_constants.h'],
libraries=magick_libs,
lib_dirs=magick_lib_dirs,
inc_dirs=magick_inc_dirs
),
Extension('pdfreflow', Extension('pdfreflow',
reflow_sources, reflow_sources,

View File

@ -1,17 +1,85 @@
Notes on setting up the windows development environment Notes on setting up the windows development environment
======================================================== ========================================================
Set CMAKE_PREFIX_PATH to C:\cygwin\home\kovid\sw Overview
----------
calibre and all its dependencies are compiled using Visual Studio 2008 express edition (free from MS). All the following instructions must be run in a visual studio command prompt unless otherwise noted.
calibre contains build script to automate the building of the calibre installer. These scripts make certain assumptions about where dependencies are installed. Your best best is to setup a VM and replicate the paths mentioned below exactly.
Basic dependencies
--------------------
Install cygwin and setup sshd (optional). Used to enable automation of the calibre build VM from linux, not needed if you are building manually.
Install MS Visual Studio 2008, cmake, python and WiX.
Set CMAKE_PREFIX_PATH environment variable to C:\cygwin\home\kovid\sw
This is where all dependencies will be installed.
Add C:\Python26\Scripts and C:\Python26 to PATH
Install setuptools from http://pypi.python.org/pypi/setuptools
If there are no windows binaries already compiled for the version of python you are using then download the source and run the following command in the folder where the source has been unpacked::
python setup.py install
Run the following command to install python dependencies::
easy_install --always-unzip -U ipython mechanize BeautifulSoup pyreadline python-dateutil dnspython
Qt
--------
Extract Qt sourcecode to C:\Qt\4.x.x. Run configure and make::
configure -opensource -qt-zlib -qt-gif -qt-libmng -qt-libpng -qt-libtiff -qt-libjpeg -release -platform win32-msvc -no-qt3support -webkit -xmlpatterns -no-phonon
nmake
SIP
-----
Available from: http://www.riverbankcomputing.co.uk/software/sip/download ::
python configure.py -p win32-msvc2008
nmake
nmake install
PyQt4
----------
Compiling instructions::
python configure.py -c -j5 -e QtCore -e QtGui -e QtSvg -e QtNetwork -e QtWebKit -e QtXmlPatterns --verbose
nmake
nmake install
Python Imaging Library
------------------------
Install as normal using provided installer.
Libunrar
----------
http://www.rarlab.com/rar/UnRARDLL.exe install and add C:\Program Files\UnrarDLL to PATH
lxml
------
http://pypi.python.org/pypi/lxml
jpeg-7 jpeg-7
------- -------
Copy Copy::
jconfig.vc to jconfig.h, makejsln.vc9 to jpeg.sln, jconfig.vc to jconfig.h, makejsln.vc9 to jpeg.sln,
makeasln.vc9 to apps.sln, makejvcp.vc9 to jpeg.vcproj, makeasln.vc9 to apps.sln, makejvcp.vc9 to jpeg.vcproj,
makecvcp.vc9 to cjpeg.vcproj, makedvcp.vc9 to djpeg.vcproj, makecvcp.vc9 to cjpeg.vcproj, makedvcp.vc9 to djpeg.vcproj,
maketvcp.vc9 to jpegtran.vcproj, makervcp.vc9 to rdjpgcom.vcproj, and maketvcp.vc9 to jpegtran.vcproj, makervcp.vc9 to rdjpgcom.vcproj, and
makewvcp.vc9 to wrjpgcom.vcproj. (Note that the renaming is critical!) makewvcp.vc9 to wrjpgcom.vcproj. (Note that the renaming is critical!)
Load jpeg.sln in Visual Studio Load jpeg.sln in Visual Studio
@ -169,7 +237,7 @@ cp build/podofo/build/src/Release/podofo.exp lib/
cp build/podofo/build/podofo_config.h include/podofo/ cp build/podofo/build/podofo_config.h include/podofo/
cp -r build/podofo/src/* include/podofo/ cp -r build/podofo/src/* include/podofo/
The following patch was required to get it to compile: The following patch (against 0.8.1) was required to get it to compile:
Index: src/PdfImage.cpp Index: src/PdfImage.cpp
=================================================================== ===================================================================
@ -214,7 +282,7 @@ Edit VisualMagick/configure/configure.cpp to set
int projectType = MULTITHREADEDDLL; int projectType = MULTITHREADEDDLL;
Run configure.bat ina visual studio command prompt Run configure.bat in a visual studio command prompt
Edit magick/magick-config.h Edit magick/magick-config.h
@ -222,3 +290,19 @@ Undefine ProvideDllMain and MAGICKCORE_X11_DELEGATE
Now open VisualMagick/VisualDynamicMT.sln set to Release Now open VisualMagick/VisualDynamicMT.sln set to Release
Remove the CORE_xlib project Remove the CORE_xlib project
calibre
---------
Take a linux calibre tree on which you have run the following command::
python setup.py stage1
and copy it to windows.
Run::
python setup.py build
python setup.py win32_freeze
This will create the .msi in the dist directory.

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
__appname__ = 'calibre' __appname__ = 'calibre'
__version__ = '0.7.12' __version__ = '0.7.13'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re import re
@ -60,6 +60,7 @@ if plugins is None:
'pictureflow', 'pictureflow',
'lzx', 'lzx',
'msdes', 'msdes',
'magick',
'podofo', 'podofo',
'cPalmdoc', 'cPalmdoc',
'fontconfig', 'fontconfig',

View File

@ -460,19 +460,22 @@ from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK
from calibre.devices.edge.driver import EDGE from calibre.devices.edge.driver import EDGE
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS
from calibre.devices.sne.driver import SNE from calibre.devices.sne.driver import SNE
from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, PROMEDIA from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
from calibre.devices.kobo.driver import KOBO from calibre.devices.kobo.driver import KOBO
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \ from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \
LibraryThing LibraryThing
from calibre.ebooks.metadata.douban import DoubanBooks from calibre.ebooks.metadata.douban import DoubanBooks
from calibre.ebooks.metadata.covers import OpenLibraryCovers, \
LibraryThingCovers
from calibre.library.catalog import CSV_XML, EPUB_MOBI, BIBTEX from calibre.library.catalog import CSV_XML, EPUB_MOBI, BIBTEX
from calibre.ebooks.epub.fix.unmanifested import Unmanifested from calibre.ebooks.epub.fix.unmanifested import Unmanifested
from calibre.ebooks.epub.fix.epubcheck import Epubcheck from calibre.ebooks.epub.fix.epubcheck import Epubcheck
plugins = [HTML2ZIP, PML2PMLZ, ArchiveExtract, GoogleBooks, ISBNDB, Amazon, plugins = [HTML2ZIP, PML2PMLZ, ArchiveExtract, GoogleBooks, ISBNDB, Amazon,
LibraryThing, DoubanBooks, CSV_XML, EPUB_MOBI, BIBTEX, Unmanifested, Epubcheck] LibraryThing, DoubanBooks, CSV_XML, EPUB_MOBI, BIBTEX, Unmanifested,
Epubcheck, OpenLibraryCovers, LibraryThingCovers]
plugins += [ plugins += [
ComicInput, ComicInput,
EPUBInput, EPUBInput,
@ -564,7 +567,6 @@ plugins += [
MENTOR, MENTOR,
SWEEX, SWEEX,
PDNOVEL, PDNOVEL,
PROMEDIA,
ITUNES, ITUNES,
] ]
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ plugins += [x for x in list(locals().values()) if isinstance(x, type) and \

View File

@ -233,18 +233,20 @@ class OutputProfile(Plugin):
'if you want to produce a document intended to be read at a ' 'if you want to produce a document intended to be read at a '
'computer or on a range of devices.') 'computer or on a range of devices.')
# The image size for comics #: The image size for comics
comic_screen_size = (584, 754) comic_screen_size = (584, 754)
# If True the MOBI renderer on the device supports MOBI indexing #: If True the MOBI renderer on the device supports MOBI indexing
supports_mobi_indexing = False supports_mobi_indexing = False
# If True output should be optimized for a touchscreen interface #: If True output should be optimized for a touchscreen interface
touchscreen = False touchscreen = False
touchscreen_news_css = '' touchscreen_news_css = ''
# A list of extra (beyond CSS 2.1) modules supported by the device #: A list of extra (beyond CSS 2.1) modules supported by the device
# Format is a cssutils profile dictionary (see iPad for example) #: Format is a cssutils profile dictionary (see iPad for example)
extra_css_modules = [] extra_css_modules = []
#: If True, the date is appended to the title of downloaded news
periodical_date_in_title = True
@classmethod @classmethod
def tags_to_string(cls, tags): def tags_to_string(cls, tags):
@ -550,6 +552,7 @@ class KindleOutput(OutputProfile):
fbase = 16 fbase = 16
fsizes = [12, 12, 14, 16, 18, 20, 22, 24] fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
supports_mobi_indexing = True supports_mobi_indexing = True
periodical_date_in_title = False
@classmethod @classmethod
def tags_to_string(cls, tags): def tags_to_string(cls, tags):
@ -567,6 +570,7 @@ class KindleDXOutput(OutputProfile):
dpi = 150.0 dpi = 150.0
comic_screen_size = (741, 1022) comic_screen_size = (741, 1022)
supports_mobi_indexing = True supports_mobi_indexing = True
periodical_date_in_title = False
@classmethod @classmethod
def tags_to_string(cls, tags): def tags_to_string(cls, tags):

View File

@ -13,6 +13,7 @@ from calibre.customize.builtins import plugins as builtin_plugins
from calibre.constants import numeric_version as version, iswindows, isosx from calibre.constants import numeric_version as version, iswindows, isosx
from calibre.devices.interface import DevicePlugin from calibre.devices.interface import DevicePlugin
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.covers import CoverDownload
from calibre.ebooks.metadata.fetch import MetadataSource from calibre.ebooks.metadata.fetch import MetadataSource
from calibre.utils.config import make_config_dir, Config, ConfigProxy, \ from calibre.utils.config import make_config_dir, Config, ConfigProxy, \
plugin_dir, OptionParser, prefs plugin_dir, OptionParser, prefs
@ -234,6 +235,15 @@ def migrate_isbndb_key():
if key: if key:
prefs.set('isbndb_com_key', '') prefs.set('isbndb_com_key', '')
set_isbndb_key(key) set_isbndb_key(key)
def cover_sources():
customization = config['plugin_customization']
for plugin in _initialized_plugins:
if isinstance(plugin, CoverDownload):
if not is_disabled(plugin):
plugin.site_customization = customization.get(plugin.name, '')
yield plugin
# }}} # }}}
# Metadata read/write {{{ # Metadata read/write {{{

View File

@ -19,7 +19,8 @@ class ANDROID(USBMS):
VENDOR_ID = { VENDOR_ID = {
# HTC # HTC
0x0bb4 : { 0x0c02 : [0x100], 0x0c01 : [0x100], 0x0ff9 : [0x0100]}, 0x0bb4 : { 0x0c02 : [0x100, 0x227], 0x0c01 : [0x100, 0x227], 0x0ff9
: [0x0100, 0x227]},
# Motorola # Motorola
0x22b8 : { 0x41d9 : [0x216], 0x2d67 : [0x100], 0x41db : [0x216], 0x22b8 : { 0x41d9 : [0x216], 0x2d67 : [0x100], 0x41db : [0x216],

View File

@ -52,6 +52,11 @@ class DevicePlugin(Plugin):
#: long time #: long time
OPEN_FEEDBACK_MESSAGE = None OPEN_FEEDBACK_MESSAGE = None
#: Set of extensions that are "virtual books" on the device
#: and therefore cannot be viewed/saved/added to library
#: For example: ``frozenset(['kobo'])``
VIRTUAL_BOOK_EXTENSIONS = frozenset([])
@classmethod @classmethod
def get_gui_name(cls): def get_gui_name(cls):
if hasattr(cls, 'gui_name'): if hasattr(cls, 'gui_name'):

View File

@ -38,6 +38,8 @@ class KOBO(USBMS):
EBOOK_DIR_MAIN = '' EBOOK_DIR_MAIN = ''
SUPPORTS_SUB_DIRS = True SUPPORTS_SUB_DIRS = True
VIRTUAL_BOOK_EXTENSIONS = frozenset(['kobo'])
def initialize(self): def initialize(self):
USBMS.initialize(self) USBMS.initialize(self)
self.book_class = Book self.book_class = Book

View File

@ -46,12 +46,13 @@ class AVANT(USBMS):
BCD = [0x0319] BCD = [0x0319]
VENDOR_NAME = 'E-BOOK' VENDOR_NAME = 'E-BOOK'
WINDOWS_MAIN_MEM = 'READER' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'READER'
EBOOK_DIR_MAIN = '' EBOOK_DIR_MAIN = ''
SUPPORTS_SUB_DIRS = True SUPPORTS_SUB_DIRS = True
class SWEEX(USBMS): class SWEEX(USBMS):
# Identical to the Promedia
name = 'Sweex Device Interface' name = 'Sweex Device Interface'
gui_name = 'Sweex' gui_name = 'Sweex'
description = _('Communicate with the Sweex MM300') description = _('Communicate with the Sweex MM300')
@ -89,6 +90,8 @@ class PDNOVEL(USBMS):
EBOOK_DIR_MAIN = 'eBooks' EBOOK_DIR_MAIN = 'eBooks'
SUPPORTS_SUB_DIRS = False SUPPORTS_SUB_DIRS = False
DELETE_EXTS = ['.jpg', '.jpeg', '.png']
def upload_cover(self, path, filename, metadata): def upload_cover(self, path, filename, metadata):
coverdata = getattr(metadata, 'thumbnail', None) coverdata = getattr(metadata, 'thumbnail', None)
@ -96,20 +99,4 @@ class PDNOVEL(USBMS):
with open('%s.jpg' % os.path.join(path, filename), 'wb') as coverfile: with open('%s.jpg' % os.path.join(path, filename), 'wb') as coverfile:
coverfile.write(coverdata[2]) coverfile.write(coverdata[2])
class PROMEDIA(USBMS):
name = 'Promedia eBook Reader'
gui_name = 'Promedia'
description = _('Communicate with the Promedia eBook reader')
author = 'Kovid Goyal'
supported_platforms = ['windows', 'linux', 'osx']
FORMATS = ['epub', 'rtf', 'pdf']
VENDOR_ID = [0x525]
PRODUCT_ID = [0xa4a5]
BCD = [0x319]
EBOOK_DIR_MAIN = 'calibre'
SUPPORTS_SUB_DIRS = True

View File

@ -55,7 +55,7 @@ class WinPNPScanner(object):
def drive_order(self, pnp_id): def drive_order(self, pnp_id):
order = 0 order = 0
match = re.search(r'REV_.*?&(\d+)', pnp_id) match = re.search(r'REV_.*?&(\d+)#', pnp_id)
if match is not None: if match is not None:
order = int(match.group(1)) order = int(match.group(1))
return order return order

View File

@ -8,7 +8,6 @@ Based on ideas from comiclrf created by FangornUK.
''' '''
import os, shutil, traceback, textwrap, time, codecs import os, shutil, traceback, textwrap, time, codecs
from ctypes import byref
from Queue import Empty from Queue import Empty
from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation
@ -71,141 +70,119 @@ class PageProcessor(list):
def render(self): def render(self):
import calibre.utils.PythonMagickWand as pw from calibre.utils.magick import Image
img = pw.NewMagickWand() img = Image()
if img < 0: img.open(self.path_to_page)
raise RuntimeError('Cannot create wand.') width, height = img.size
if not pw.MagickReadImage(img, self.path_to_page):
severity = pw.ExceptionType(0)
msg = pw.MagickGetException(img, byref(severity))
raise IOError('Failed to read image from: %s: %s'
%(self.path_to_page, msg))
width = pw.MagickGetImageWidth(img)
height = pw.MagickGetImageHeight(img)
if self.num == 0: # First image so create a thumbnail from it if self.num == 0: # First image so create a thumbnail from it
thumb = pw.CloneMagickWand(img) thumb = img.clone
if thumb < 0: thumb.thumbnail(60, 80)
raise RuntimeError('Cannot create wand.') thumb.save(os.path.join(self.dest, 'thumbnail.png'))
pw.MagickThumbnailImage(thumb, 60, 80)
pw.MagickWriteImage(thumb, os.path.join(self.dest, 'thumbnail.png'))
pw.DestroyMagickWand(thumb)
self.pages = [img] self.pages = [img]
if width > height: if width > height:
if self.opts.landscape: if self.opts.landscape:
self.rotate = True self.rotate = True
else: else:
split1, split2 = map(pw.CloneMagickWand, (img, img)) split1, split2 = img.clone, img.clone
pw.DestroyMagickWand(img) half = int(width/2)
if split1 < 0 or split2 < 0: split1.crop(half-1, height, 0, 0)
raise RuntimeError('Cannot create wand.') split2.crop(half-1, height, half, 0)
pw.MagickCropImage(split1, (width/2)-1, height, 0, 0)
pw.MagickCropImage(split2, (width/2)-1, height, width/2, 0 )
self.pages = [split2, split1] if self.opts.right2left else [split1, split2] self.pages = [split2, split1] if self.opts.right2left else [split1, split2]
self.process_pages() self.process_pages()
def process_pages(self): def process_pages(self):
import calibre.utils.PythonMagickWand as p from calibre.utils.magick import PixelWand
for i, wand in enumerate(self.pages): for i, wand in enumerate(self.pages):
pw = p.NewPixelWand() pw = PixelWand()
try: pw.color = 'white'
if pw < 0:
raise RuntimeError('Cannot create wand.')
p.PixelSetColor(pw, 'white')
p.MagickSetImageBorderColor(wand, pw) wand.set_border_color(pw)
if self.rotate: if self.rotate:
p.MagickRotateImage(wand, pw, -90) wand.rotate(pw, -90)
# 25 percent fuzzy trim? # 25 percent fuzzy trim?
if not self.opts.disable_trim: if not self.opts.disable_trim:
p.MagickTrimImage(wand, 25*65535/100) wand.trim(25*65535/100)
p.MagickSetImagePage(wand, 0,0,0,0) #Clear page after trim, like a "+repage" wand.set_page(0, 0, 0, 0) #Clear page after trim, like a "+repage"
# Do the Photoshop "Auto Levels" equivalent # Do the Photoshop "Auto Levels" equivalent
if not self.opts.dont_normalize: if not self.opts.dont_normalize:
p.MagickNormalizeImage(wand) wand.normalize()
sizex = p.MagickGetImageWidth(wand) sizex, sizey = wand.size
sizey = p.MagickGetImageHeight(wand)
SCRWIDTH, SCRHEIGHT = self.opts.output_profile.comic_screen_size SCRWIDTH, SCRHEIGHT = self.opts.output_profile.comic_screen_size
if self.opts.keep_aspect_ratio: if self.opts.keep_aspect_ratio:
# Preserve the aspect ratio by adding border # Preserve the aspect ratio by adding border
aspect = float(sizex) / float(sizey) aspect = float(sizex) / float(sizey)
if aspect <= (float(SCRWIDTH) / float(SCRHEIGHT)): if aspect <= (float(SCRWIDTH) / float(SCRHEIGHT)):
newsizey = SCRHEIGHT newsizey = SCRHEIGHT
newsizex = int(newsizey * aspect) newsizex = int(newsizey * aspect)
deltax = (SCRWIDTH - newsizex) / 2 deltax = (SCRWIDTH - newsizex) / 2
deltay = 0 deltay = 0
else:
newsizex = SCRWIDTH
newsizey = int(newsizex / aspect)
deltax = 0
deltay = (SCRHEIGHT - newsizey) / 2
p.MagickResizeImage(wand, newsizex, newsizey, p.CatromFilter, 1.0)
p.MagickSetImageBorderColor(wand, pw)
p.MagickBorderImage(wand, pw, deltax, deltay)
elif self.opts.wide:
# Keep aspect and Use device height as scaled image width so landscape mode is clean
aspect = float(sizex) / float(sizey)
screen_aspect = float(SCRWIDTH) / float(SCRHEIGHT)
# Get dimensions of the landscape mode screen
# Add 25px back to height for the battery bar.
wscreenx = SCRHEIGHT + 25
wscreeny = int(wscreenx / screen_aspect)
if aspect <= screen_aspect:
newsizey = wscreeny
newsizex = int(newsizey * aspect)
deltax = (wscreenx - newsizex) / 2
deltay = 0
else:
newsizex = wscreenx
newsizey = int(newsizex / aspect)
deltax = 0
deltay = (wscreeny - newsizey) / 2
p.MagickResizeImage(wand, newsizex, newsizey, p.CatromFilter, 1.0)
p.MagickSetImageBorderColor(wand, pw)
p.MagickBorderImage(wand, pw, deltax, deltay)
else: else:
p.MagickResizeImage(wand, SCRWIDTH, SCRHEIGHT, p.CatromFilter, 1.0) newsizex = SCRWIDTH
newsizey = int(newsizex / aspect)
deltax = 0
deltay = (SCRHEIGHT - newsizey) / 2
wand.size = (newsizex, newsizey)
wand.set_border_color(pw)
wand.add_border(pw, deltax, deltay)
elif self.opts.wide:
# Keep aspect and Use device height as scaled image width so landscape mode is clean
aspect = float(sizex) / float(sizey)
screen_aspect = float(SCRWIDTH) / float(SCRHEIGHT)
# Get dimensions of the landscape mode screen
# Add 25px back to height for the battery bar.
wscreenx = SCRHEIGHT + 25
wscreeny = int(wscreenx / screen_aspect)
if aspect <= screen_aspect:
newsizey = wscreeny
newsizex = int(newsizey * aspect)
deltax = (wscreenx - newsizex) / 2
deltay = 0
else:
newsizex = wscreenx
newsizey = int(newsizex / aspect)
deltax = 0
deltay = (wscreeny - newsizey) / 2
wand.size = (newsizex, newsizey)
wand.set_border_color(pw)
wand.add_border(pw, deltax, deltay)
else:
wand.size = (SCRWIDTH, SCRHEIGHT)
if not self.opts.dont_sharpen: if not self.opts.dont_sharpen:
p.MagickSharpenImage(wand, 0.0, 1.0) wand.sharpen(0.0, 1.0)
if not self.opts.dont_grayscale: if not self.opts.dont_grayscale:
p.MagickSetImageType(wand, p.GrayscaleType) wand.type = 'GrayscaleType'
if self.opts.despeckle: if self.opts.despeckle:
p.MagickDespeckleImage(wand) wand.despeckle()
p.MagickQuantizeImage(wand, self.opts.colors, p.RGBColorspace, 0, 1, 0) wand.quantize(self.opts.colors)
dest = '%d_%d.%s'%(self.num, i, self.opts.output_format) dest = '%d_%d.%s'%(self.num, i, self.opts.output_format)
dest = os.path.join(self.dest, dest) dest = os.path.join(self.dest, dest)
p.MagickWriteImage(wand, dest+'8') wand.save(dest+'8')
os.rename(dest+'8', dest) os.rename(dest+'8', dest)
self.append(dest) self.append(dest)
finally:
if pw > 0:
p.DestroyPixelWand(pw)
p.DestroyMagickWand(wand)
def render_pages(tasks, dest, opts, notification=lambda x, y: x): def render_pages(tasks, dest, opts, notification=lambda x, y: x):
''' '''
Entry point for the job server. Entry point for the job server.
''' '''
failures, pages = [], [] failures, pages = [], []
from calibre.utils.PythonMagickWand import ImageMagick for num, path in tasks:
with ImageMagick(): try:
for num, path in tasks: pages.extend(PageProcessor(path, dest, opts, num))
try: msg = _('Rendered %s')%path
pages.extend(PageProcessor(path, dest, opts, num)) except:
msg = _('Rendered %s')%path failures.append(path)
except: msg = _('Failed %s')%path
failures.append(path) if opts.verbose:
msg = _('Failed %s')%path msg += '\n' + traceback.format_exc()
if opts.verbose: prints(msg)
msg += '\n' + traceback.format_exc() notification(0.5, msg)
prints(msg)
notification(0.5, msg)
return pages, failures return pages, failures
@ -226,9 +203,6 @@ def process_pages(pages, opts, update, tdir):
''' '''
Render all identified comic pages. Render all identified comic pages.
''' '''
from calibre.utils.PythonMagickWand import ImageMagick
ImageMagick
progress = Progress(len(pages), update) progress = Progress(len(pages), update)
server = Server() server = Server()
jobs = [] jobs = []

View File

@ -46,6 +46,7 @@ def authors_to_sort_string(authors):
return ' & '.join(map(author_to_author_sort, authors)) return ' & '.join(map(author_to_author_sort, authors))
_title_pat = re.compile('^(A|The|An)\s+', re.IGNORECASE) _title_pat = re.compile('^(A|The|An)\s+', re.IGNORECASE)
def title_sort(title): def title_sort(title):
match = _title_pat.search(title) match = _title_pat.search(title)
if match: if match:

View File

@ -5,11 +5,253 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import traceback, socket, re, sys
from functools import partial
from threading import Thread, Event
from Queue import Queue, Empty
import mechanize
from calibre.customize import Plugin from calibre.customize import Plugin
from calibre import browser, prints
from calibre.ebooks.BeautifulSoup import BeautifulSoup
from calibre.constants import preferred_encoding, DEBUG
class CoverDownload(Plugin): class CoverDownload(Plugin):
'''
These plugins are used to download covers for books.
'''
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
author = 'Kovid Goyal' author = 'Kovid Goyal'
type = _('Cover download') type = _('Cover download')
def has_cover(self, mi, ans, timeout=5.):
'''
Check if the book described by mi has a cover. Call ans.set() if it
does. Do nothing if it doesn't.
:param mi: MetaInformation object
:param timeout: timeout in seconds
:param ans: A threading.Event object
'''
raise NotImplementedError()
def get_covers(self, mi, result_queue, abort, timeout=5.):
'''
Download covers for books described by the mi object. Downloaded covers
must be put into the result_queue. If more than one cover is available,
the plugin should continue downloading them and putting them into
result_queue until abort.is_set() returns True.
:param mi: MetaInformation object
:param result_queue: A multithreaded Queue
:param abort: A threading.Event object
:param timeout: timeout in seconds
'''
raise NotImplementedError()
def exception_to_string(self, ex):
try:
return unicode(ex)
except:
try:
return str(ex).decode(preferred_encoding, 'replace')
except:
return repr(ex)
def debug(self, *args, **kwargs):
if DEBUG:
prints('\t'+self.name+':', *args, **kwargs)
class HeadRequest(mechanize.Request):
def get_method(self):
return 'HEAD'
class OpenLibraryCovers(CoverDownload): # {{{
'Download covers from openlibrary.org'
OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false'
name = 'openlibrary.org covers'
description = _('Download covers from openlibrary.org')
author = 'Kovid Goyal'
def has_cover(self, mi, ans, timeout=5.):
if not mi.isbn:
return False
br = browser()
br.set_handle_redirect(False)
try:
br.open_novisit(HeadRequest(self.OPENLIBRARY%mi.isbn), timeout=timeout)
self.debug('cover for', mi.isbn, 'found')
ans.set()
except Exception, e:
if callable(getattr(e, 'getcode', None)) and e.getcode() == 302:
self.debug('cover for', mi.isbn, 'found')
ans.set()
else:
self.debug(e)
def get_covers(self, mi, result_queue, abort, timeout=5.):
if not mi.isbn:
return
br = browser()
try:
ans = br.open(self.OPENLIBRARY%mi.isbn, timeout=timeout).read()
result_queue.put((True, ans, 'jpg', self.name))
except Exception, e:
if callable(getattr(e, 'getcode', None)) and e.getcode() == 404:
result_queue.put((False, _('ISBN: %s not found')%mi.isbn, '', self.name))
else:
result_queue.put((False, self.exception_to_string(e),
traceback.format_exc(), self.name))
# }}}
class LibraryThingCovers(CoverDownload): # {{{
name = 'librarything.com covers'
description = _('Download covers from librarything.com')
author = 'Kovid Goyal'
LIBRARYTHING = 'http://www.librarything.com/isbn/'
def get_cover_url(self, isbn, br, timeout=5.):
try:
src = br.open_novisit('http://www.librarything.com/isbn/'+isbn,
timeout=timeout).read().decode('utf-8', 'replace')
except Exception, err:
if isinstance(getattr(err, 'args', [None])[0], socket.timeout):
err = Exception(_('LibraryThing.com timed out. Try again later.'))
raise err
else:
s = BeautifulSoup(src)
url = s.find('td', attrs={'class':'left'})
if url is None:
if s.find('div', attrs={'class':'highloadwarning'}) is not None:
raise Exception(_('Could not fetch cover as server is experiencing high load. Please try again later.'))
raise Exception(_('ISBN: %s not found')%isbn)
url = url.find('img')
if url is None:
raise Exception(_('LibraryThing.com server error. Try again later.'))
url = re.sub(r'_S[XY]\d+', '', url['src'])
return url
def has_cover(self, mi, ans, timeout=5.):
if not mi.isbn:
return False
br = browser()
try:
self.get_cover_url(mi.isbn, br, timeout=timeout)
self.debug('cover for', mi.isbn, 'found')
ans.set()
except Exception, e:
self.debug(e)
def get_covers(self, mi, result_queue, abort, timeout=5.):
if not mi.isbn:
return
br = browser()
try:
url = self.get_cover_url(mi.isbn, br, timeout=timeout)
cover_data = br.open_novisit(url).read()
result_queue.put((True, cover_data, 'jpg', self.name))
except Exception, e:
result_queue.put((False, self.exception_to_string(e),
traceback.format_exc(), self.name))
# }}}
def check_for_cover(mi, timeout=5.): # {{{
from calibre.customize.ui import cover_sources
ans = Event()
checkers = [partial(p.has_cover, mi, ans, timeout=timeout) for p in
cover_sources()]
workers = [Thread(target=c) for c in checkers]
for w in workers:
w.daemon = True
w.start()
while not ans.is_set():
ans.wait(0.1)
if sum([int(w.is_alive()) for w in workers]) == 0:
break
return ans.is_set()
# }}}
def download_covers(mi, result_queue, max_covers=50, timeout=5.): # {{{
from calibre.customize.ui import cover_sources
abort = Event()
temp = Queue()
getters = [partial(p.get_covers, mi, temp, abort, timeout=timeout) for p in
cover_sources()]
workers = [Thread(target=c) for c in getters]
for w in workers:
w.daemon = True
w.start()
count = 0
while count < max_covers:
try:
result = temp.get_nowait()
if result[0]:
count += 1
result_queue.put(result)
except Empty:
pass
if sum([int(w.is_alive()) for w in workers]) == 0:
break
abort.set()
while True:
try:
result = temp.get_nowait()
count += 1
result_queue.put(result)
except Empty:
break
# }}}
def download_cover(mi, timeout=5.): # {{{
results = Queue()
download_covers(mi, results, max_covers=1, timeout=timeout)
errors, ans = [], None
while True:
try:
x = results.get_nowait()
if x[0]:
ans = x[1]
else:
errors.append(x)
except Empty:
break
return ans, errors
# }}}
def test(isbns): # {{{
from calibre.ebooks.metadata import MetaInformation
mi = MetaInformation('test', ['test'])
for isbn in isbns:
prints('Testing ISBN:', isbn)
mi.isbn = isbn
found = check_for_cover(mi)
prints('Has cover:', found)
ans, errors = download_cover(mi)
if ans is not None:
prints('Cover downloaded')
else:
prints('Download failed:')
for err in errors:
prints('\t', err[-1]+':', err[1])
print '\n'
# }}}
if __name__ == '__main__':
isbns = sys.argv[1:] + ['9781591025412', '9780307272119']
test(isbns)

View File

@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''Read meta information from epub files''' '''Read meta information from epub files'''
import os, re, posixpath import os, re, posixpath, shutil
from cStringIO import StringIO from cStringIO import StringIO
from contextlib import closing from contextlib import closing
@ -13,7 +13,7 @@ from calibre.utils.zipfile import ZipFile, BadZipfile, safe_replace
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.opf2 import OPF from calibre.ebooks.metadata.opf2 import OPF
from calibre.ptempfile import TemporaryDirectory from calibre.ptempfile import TemporaryDirectory, PersistentTemporaryFile
from calibre import CurrentDir from calibre import CurrentDir
class EPubException(Exception): class EPubException(Exception):
@ -205,11 +205,19 @@ def set_metadata(stream, mi, apply_null=False, update_timestamp=False):
cover_replacable = not reader.encryption_meta.is_encrypted(cpath) and \ cover_replacable = not reader.encryption_meta.is_encrypted(cpath) and \
os.path.splitext(cpath)[1].lower() in ('.png', '.jpg', '.jpeg') os.path.splitext(cpath)[1].lower() in ('.png', '.jpg', '.jpeg')
if cover_replacable: if cover_replacable:
from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.magick.draw import save_cover_data_to, \
from calibre.utils.magick_draw import save_cover_data_to identify
new_cover = PersistentTemporaryFile(suffix=os.path.splitext(cpath)[1]) new_cover = PersistentTemporaryFile(suffix=os.path.splitext(cpath)[1])
new_cover.close() resize_to = None
save_cover_data_to(new_cdata, new_cover.name) if False: # Resize new cover to same size as old cover
shutil.copyfileobj(reader.open(cpath), new_cover)
new_cover.close()
width, height, fmt = identify(new_cover.name)
resize_to = (width, height)
else:
new_cover.close()
save_cover_data_to(new_cdata, new_cover.name,
resize_to=resize_to)
replacements[cpath] = open(new_cover.name, 'rb') replacements[cpath] = open(new_cover.name, 'rb')
except: except:
import traceback import traceback

View File

@ -10,7 +10,7 @@ from calibre import prints
from calibre.utils.config import OptionParser from calibre.utils.config import OptionParser
from calibre.utils.logging import default_log from calibre.utils.logging import default_log
from calibre.customize import Plugin from calibre.customize import Plugin
from calibre.ebooks.metadata.library_thing import check_for_cover from calibre.ebooks.metadata.covers import check_for_cover
metadata_config = None metadata_config = None
@ -289,11 +289,10 @@ def filter_metadata_results(item):
def do_cover_check(item): def do_cover_check(item):
item.has_cover = False item.has_cover = False
if item.isbn: try:
try: item.has_cover = check_for_cover(item)
item.has_cover = check_for_cover(item.isbn) except:
except: pass # Cover not found
pass # Cover not found
def check_for_covers(items): def check_for_covers(items):
threads = [Thread(target=do_cover_check, args=(item,)) for item in items] threads = [Thread(target=do_cover_check, args=(item,)) for item in items]

View File

@ -98,7 +98,7 @@ class CoverManager(object):
authors = [unicode(x) for x in m.creator if x.role == 'aut'] authors = [unicode(x) for x in m.creator if x.role == 'aut']
try: try:
from calibre.utils.magick_draw import create_cover_page, TextLine from calibre.utils.magick.draw import create_cover_page, TextLine
lines = [TextLine(title, 44), TextLine(authors_to_string(authors), lines = [TextLine(title, 44), TextLine(authors_to_string(authors),
32)] 32)]
img_data = create_cover_page(lines, I('library.png')) img_data = create_cover_page(lines, I('library.png'))

View File

@ -50,7 +50,7 @@ class RTFInput(InputFormatPlugin):
parser = ParseRtf( parser = ParseRtf(
in_file = stream, in_file = stream,
out_file = ofile, out_file = ofile,
deb_dir = 'I:\\Calibre\\rtfdebug', deb_dir = 'I:\\Calibre\\rtfdebug',
# Convert symbol fonts to unicode equivalents. Default # Convert symbol fonts to unicode equivalents. Default
# is 1 # is 1
convert_symbol = 1, convert_symbol = 1,
@ -121,27 +121,23 @@ class RTFInput(InputFormatPlugin):
return self.convert_images(imap) return self.convert_images(imap)
def convert_images(self, imap): def convert_images(self, imap):
from calibre.utils.PythonMagickWand import ImageMagick for count, val in imap.items():
with ImageMagick(): try:
for count, val in imap.items(): imap[count] = self.convert_image(val)
try: except:
imap[count] = self.convert_image(val) self.log.exception('Failed to convert', val)
except:
self.log.exception('Failed to convert', val)
return imap return imap
def convert_image(self, name): def convert_image(self, name):
import calibre.utils.PythonMagickWand as p from calibre.utils.magick import Image
img = p.NewMagickWand() img = Image()
if img < 0: img.open(name)
raise RuntimeError('Cannot create wand.')
if not p.MagickReadImage(img, name):
self.log.warn('Failed to read image:', name)
name = name.replace('.wmf', '.jpg') name = name.replace('.wmf', '.jpg')
p.MagickWriteImage(img, name) img.save(name)
return name return name
def write_inline_css(self, ic): def write_inline_css(self, ic):
font_size_classes = ['span.fs%d { font-size: %spt }'%(i, x) for i, x in font_size_classes = ['span.fs%d { font-size: %spt }'%(i, x) for i, x in
enumerate(ic.font_sizes)] enumerate(ic.font_sizes)]
@ -152,11 +148,17 @@ class RTFInput(InputFormatPlugin):
text-decoration: none; font-weight: normal; text-decoration: none; font-weight: normal;
font-style: normal; font-variant: normal font-style: normal; font-variant: normal
} }
span.italics { font-style: italic } span.italics { font-style: italic }
span.bold { font-weight: bold } span.bold { font-weight: bold }
span.small-caps { font-variant: small-caps } span.small-caps { font-variant: small-caps }
span.underlined { text-decoration: underline } span.underlined { text-decoration: underline }
span.strike-through { text-decoration: line-through } span.strike-through { text-decoration: line-through }
''') ''')
css += '\n'+'\n'.join(font_size_classes) css += '\n'+'\n'.join(font_size_classes)
css += '\n' +'\n'.join(color_classes) css += '\n' +'\n'.join(color_classes)

View File

@ -6,7 +6,7 @@ Read content from txt file.
import os, re import os, re
from calibre import prepare_string_for_xml from calibre import prepare_string_for_xml, isbytestring
from calibre.ebooks.markdown import markdown from calibre.ebooks.markdown import markdown
from calibre.ebooks.metadata.opf2 import OPFCreator from calibre.ebooks.metadata.opf2 import OPFCreator
@ -17,6 +17,8 @@ __docformat__ = 'restructuredtext en'
HTML_TEMPLATE = u'<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><title>%s</title></head><body>\n%s\n</body></html>' HTML_TEMPLATE = u'<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><title>%s</title></head><body>\n%s\n</body></html>'
def convert_basic(txt, title='', epub_split_size_kb=0): def convert_basic(txt, title='', epub_split_size_kb=0):
if isbytestring(txt):
txt = txt.decode('utf-8', 'replace')
# Strip whitespace from the beginning and end of the line. Also replace # Strip whitespace from the beginning and end of the line. Also replace
# all line breaks with \n. # all line breaks with \n.
txt = '\n'.join([line.strip() for line in txt.splitlines()]) txt = '\n'.join([line.strip() for line in txt.splitlines()])
@ -30,12 +32,13 @@ def convert_basic(txt, title='', epub_split_size_kb=0):
# Remove excessive line breaks. # Remove excessive line breaks.
txt = re.sub('\n{3,}', '\n\n', txt) txt = re.sub('\n{3,}', '\n\n', txt)
#remove ASCII invalid chars : 0 to 8 and 11-14 to 24 #remove ASCII invalid chars : 0 to 8 and 11-14 to 24
illegal_char = re.compile('\x00|\x01|\x02|\x03|\x04|\x05|\x06|\x07|\x08| \ chars = list(range(8)) + [0x0B, 0x0E, 0x0F] + list(range(0x10, 0x19))
\x0B|\x0E|\x0F|\x10|\x11|\x12|\x13|\x14|\x15|\x16|\x17|\x18') illegal_chars = re.compile(u'|'.join(map(unichr, chars)))
txt = illegal_char.sub('', txt) txt = illegal_chars.sub('', txt)
#Takes care if there is no point to split #Takes care if there is no point to split
if epub_split_size_kb > 0: if epub_split_size_kb > 0:
if isinstance(txt, unicode):
txt = txt.encode('utf-8')
length_byte = len(txt) length_byte = len(txt)
#Calculating the average chunk value for easy splitting as EPUB (+2 as a safe margin) #Calculating the average chunk value for easy splitting as EPUB (+2 as a safe margin)
chunk_size = long(length_byte / (int(length_byte / (epub_split_size_kb * 1024) ) + 2 )) chunk_size = long(length_byte / (int(length_byte / (epub_split_size_kb * 1024) ) + 2 ))
@ -43,6 +46,9 @@ def convert_basic(txt, title='', epub_split_size_kb=0):
if (len(filter(lambda x: len(x) > chunk_size, txt.split('\n\n')))) : if (len(filter(lambda x: len(x) > chunk_size, txt.split('\n\n')))) :
txt = '\n\n'.join([split_string_separator(line, chunk_size) txt = '\n\n'.join([split_string_separator(line, chunk_size)
for line in txt.split('\n\n')]) for line in txt.split('\n\n')])
if isbytestring(txt):
txt = txt.decode('utf-8')
lines = [] lines = []
# Split into paragraphs based on having a blank line between text. # Split into paragraphs based on having a blank line between text.

View File

@ -430,6 +430,20 @@ class AddAction(object): # {{{
d.exec_() d.exec_()
return return
paths = [p for p in view._model.paths(rows) if p is not None] paths = [p for p in view._model.paths(rows) if p is not None]
ve = self.device_manager.device.VIRTUAL_BOOK_EXTENSIONS
def ext(x):
ans = os.path.splitext(x)[1]
ans = ans[1:] if len(ans) > 1 else ans
return ans.lower()
remove = set([p for p in paths if ext(p) in ve])
if remove:
paths = [p for p in paths if p not in remove]
info_dialog(self, _('Not Implemented'),
_('The following books are virtual and cannot be added'
' to the calibre library:'), '\n'.join(remove),
show=True)
if not paths:
return
if not paths or len(paths) == 0: if not paths or len(paths) == 0:
d = error_dialog(self, _('Add to library'), _('No book files found')) d = error_dialog(self, _('Add to library'), _('No book files found'))
d.exec_() d.exec_()
@ -913,6 +927,14 @@ class SaveToDiskAction(object): # {{{
_('Choose destination directory')) _('Choose destination directory'))
if not path: if not path:
return return
dpath = os.path.abspath(path).replace('/', os.sep)
lpath = self.library_view.model().db.library_path.replace('/', os.sep)
if dpath.startswith(lpath):
return error_dialog(self, _('Not allowed'),
_('You are tying to save files into the calibre '
'library. This can cause corruption of your '
'library. Save to disk is meant to export '
'files from your calibre library elsewhere.'), show=True)
if self.current_view() is self.library_view: if self.current_view() is self.library_view:
from calibre.gui2.add import Saver from calibre.gui2.add import Saver

View File

@ -31,7 +31,14 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QComboBox" name="input_formats"/> <widget class="QComboBox" name="input_formats">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
<property name="minimumContentsLength">
<number>5</number>
</property>
</widget>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="opt_individual_saved_settings"> <widget class="QCheckBox" name="opt_individual_saved_settings">
@ -64,7 +71,14 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QComboBox" name="output_formats"/> <widget class="QComboBox" name="output_formats">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
<property name="minimumContentsLength">
<number>5</number>
</property>
</widget>
</item> </item>
</layout> </layout>
</item> </item>
@ -115,8 +129,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>810</width> <width>805</width>
<height>489</height> <height>484</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_3"> <layout class="QVBoxLayout" name="verticalLayout_3">

View File

@ -118,6 +118,7 @@ class DeviceManager(Thread): # {{{
self.jobs = Queue.Queue(0) self.jobs = Queue.Queue(0)
self.keep_going = True self.keep_going = True
self.job_manager = job_manager self.job_manager = job_manager
self.reported_errors = set([])
self.current_job = None self.current_job = None
self.scanner = DeviceScanner() self.scanner = DeviceScanner()
self.connected_device = None self.connected_device = None
@ -141,13 +142,16 @@ class DeviceManager(Thread): # {{{
for dev, detected_device in connected_devices: for dev, detected_device in connected_devices:
if dev.OPEN_FEEDBACK_MESSAGE is not None: if dev.OPEN_FEEDBACK_MESSAGE is not None:
self.open_feedback_slot(dev.OPEN_FEEDBACK_MESSAGE) self.open_feedback_slot(dev.OPEN_FEEDBACK_MESSAGE)
dev.reset(detected_device=detected_device,
report_progress=self.report_progress)
try: try:
dev.reset(detected_device=detected_device,
report_progress=self.report_progress)
dev.open() dev.open()
except: except:
prints('Unable to open device', str(dev)) tb = traceback.format_exc()
traceback.print_exc() if DEBUG or tb not in self.reported_errors:
self.reported_errors.add(tb)
prints('Unable to open device', str(dev))
prints(tb)
continue continue
self.connected_device = dev self.connected_device = dev
self.connected_device_kind = device_kind self.connected_device_kind = device_kind
@ -192,11 +196,13 @@ class DeviceManager(Thread): # {{{
if possibly_connected_devices: if possibly_connected_devices:
if not self.do_connect(possibly_connected_devices, if not self.do_connect(possibly_connected_devices,
device_kind='device'): device_kind='device'):
prints('Connect to device failed, retrying in 5 seconds...') if DEBUG:
prints('Connect to device failed, retrying in 5 seconds...')
time.sleep(5) time.sleep(5)
if not self.do_connect(possibly_connected_devices, if not self.do_connect(possibly_connected_devices,
device_kind='usb'): device_kind='usb'):
prints('Device connect failed again, giving up') if DEBUG:
prints('Device connect failed again, giving up')
# Mount devices that don't use USB, such as the folder device and iTunes # Mount devices that don't use USB, such as the folder device and iTunes
# This will be called on the GUI thread. Because of this, we must store # This will be called on the GUI thread. Because of this, we must store

View File

@ -75,7 +75,11 @@ class ChooseLibrary(QDialog, Ui_Dialog):
action = 'existing' action = 'existing'
elif self.empty_library.isChecked(): elif self.empty_library.isChecked():
action = 'new' action = 'new'
loc = os.path.abspath(unicode(self.location.text()).strip()) text = unicode(self.location.text()).strip()
if not text:
return error_dialog(self, _('No location'), _('No location selected'),
show=True)
loc = os.path.abspath(text)
if not loc or not os.path.exists(loc) or not self.check_action(action, if not loc or not os.path.exists(loc) or not self.check_action(action,
loc): loc):
return return

View File

@ -50,15 +50,35 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
# set up the signal after the table is filled # set up the signal after the table is filled
self.table.cellChanged.connect(self.cell_changed) self.table.cellChanged.connect(self.cell_changed)
self.sort_by_author.setCheckable(True)
self.sort_by_author.setChecked(False)
self.sort_by_author.clicked.connect(self.do_sort_by_author)
self.author_order = 1
self.table.setSortingEnabled(True)
self.table.sortByColumn(1, Qt.AscendingOrder) self.table.sortByColumn(1, Qt.AscendingOrder)
self.sort_by_author_sort.clicked.connect(self.do_sort_by_author_sort)
self.sort_by_author_sort.setCheckable(True)
self.sort_by_author_sort.setChecked(True)
self.author_sort_order = 1
if select_item is not None: if select_item is not None:
self.table.setCurrentItem(select_item) self.table.setCurrentItem(select_item)
self.table.editItem(select_item) self.table.editItem(select_item)
else: else:
self.table.setCurrentCell(0, 0) self.table.setCurrentCell(0, 0)
def do_sort_by_author(self):
self.author_order = 1 if self.author_order == 0 else 0
self.table.sortByColumn(0, self.author_order)
self.sort_by_author.setChecked(True)
self.sort_by_author_sort.setChecked(False)
def do_sort_by_author_sort(self):
self.author_sort_order = 1 if self.author_sort_order == 0 else 0
self.table.sortByColumn(1, self.author_sort_order)
self.sort_by_author.setChecked(False)
self.sort_by_author_sort.setChecked(True)
def accepted(self): def accepted(self):
self.result = [] self.result = []
for row in range(0,self.table.rowCount()): for row in range(0,self.table.rowCount()):
@ -79,8 +99,4 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
else: else:
item = self.table.item(row, 1) item = self.table.item(row, 1)
self.table.setCurrentItem(item) self.table.setCurrentItem(item)
# disable and reenable sorting to force the sort now, so we can scroll
# to the item after it moves
self.table.setSortingEnabled(False)
self.table.setSortingEnabled(True)
self.table.scrollToItem(item) self.table.scrollToItem(item)

View File

@ -34,17 +34,54 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QDialogButtonBox" name="buttonBox"> <layout class="QHBoxLayout" name="horizontalLayout">
<property name="orientation"> <item>
<enum>Qt::Horizontal</enum> <widget class="QPushButton" name="sort_by_author">
</property> <property name="text">
<property name="standardButtons"> <string>Sort by author</string>
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> </property>
</property> </widget>
<property name="centerButtons"> </item>
<bool>true</bool> <item>
</property> <widget class="QPushButton" name="sort_by_author_sort">
</widget> <property name="text">
<string>Sort by author sort</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item> </item>
</layout> </layout>
</widget> </widget>

View File

@ -24,8 +24,9 @@ from calibre.gui2.widgets import ProgressIndicator
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.metadata import string_to_authors, \ from calibre.ebooks.metadata import string_to_authors, \
authors_to_string, check_isbn authors_to_string, check_isbn
from calibre.ebooks.metadata.library_thing import cover_from_isbn from calibre.ebooks.metadata.covers import download_cover
from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.meta import get_metadata
from calibre.ebooks.metadata import MetaInformation
from calibre.utils.config import prefs, tweaks from calibre.utils.config import prefs, tweaks
from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp
from calibre.customize.ui import run_plugins_on_import, get_isbndb_key from calibre.customize.ui import run_plugins_on_import, get_isbndb_key
@ -48,12 +49,13 @@ class CoverFetcher(QThread):
def run(self): def run(self):
try: try:
au = self.author if self.author else None
mi = MetaInformation(self.title, [au])
if not self.isbn: if not self.isbn:
from calibre.ebooks.metadata.fetch import search from calibre.ebooks.metadata.fetch import search
if not self.title: if not self.title:
self.needs_isbn = True self.needs_isbn = True
return return
au = self.author if self.author else None
key = get_isbndb_key() key = get_isbndb_key()
if not key: if not key:
key = None key = None
@ -66,8 +68,10 @@ class CoverFetcher(QThread):
return return
self.isbn = results[0] self.isbn = results[0]
self.cover_data = cover_from_isbn(self.isbn, timeout=self.timeout, mi.isbn = self.isbn
username=self.username, password=self.password)[0]
self.cover_data, self.errors = download_cover(mi,
timeout=self.timeout)
except Exception, e: except Exception, e:
self.exception = e self.exception = e
self.traceback = traceback.format_exc() self.traceback = traceback.format_exc()
@ -138,6 +142,21 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.cpixmap = pix self.cpixmap = pix
self.cover_data = cover self.cover_data = cover
def generate_cover(self, *args):
from calibre.utils.magick.draw import create_cover_page, TextLine
title = unicode(self.title.text()).strip()
author = unicode(self.authors.text()).strip()
if not title or not author:
return error_dialog(self, _('Specify title and author'),
_('You must specify a title and author before generating '
'a cover'), show=True)
lines = [TextLine(title, 44), TextLine(author, 32)]
self.cover_data = create_cover_page(lines, I('library.png'))
pix = QPixmap()
pix.loadFromData(self.cover_data)
self.cover.setPixmap(pix)
self.cover_changed = True
self.cpixmap = pix
def add_format(self, x): def add_format(self, x):
files = choose_files(self, 'add formats dialog', files = choose_files(self, 'add formats dialog',
@ -421,6 +440,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.central_widget.tabBar().setVisible(False) self.central_widget.tabBar().setVisible(False)
else: else:
self.create_custom_column_editors() self.create_custom_column_editors()
self.generate_cover_button.clicked.connect(self.generate_cover)
def create_custom_column_editors(self): def create_custom_column_editors(self):
w = self.central_widget.widget(1) w = self.central_widget.widget(1)
@ -576,6 +596,13 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
error_dialog(self, _('Cannot fetch cover'), error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>')+unicode(err)).exec_() _('<b>Could not fetch cover.</b><br/>')+unicode(err)).exec_()
return return
if self.cover_fetcher.errors and self.cover_fetcher.cover_data is None:
details = u'\n\n'.join([e[-1] + ': ' + e[1] for e in self.cover_fetcher.errors])
error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>') +
_('For the error message from each cover source, '
'click Show details below.'), det_msg=details, show=True)
return
pix = QPixmap() pix = QPixmap()
pix.loadFromData(self.cover_fetcher.cover_data) pix.loadFromData(self.cover_fetcher.cover_data)

View File

@ -653,6 +653,16 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QPushButton" name="generate_cover_button">
<property name="toolTip">
<string>Generate a default cover based on the title and author</string>
</property>
<property name="text">
<string>&amp;Generate cover</string>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
</layout> </layout>
@ -736,15 +746,17 @@
<tabstop>fetch_metadata_button</tabstop> <tabstop>fetch_metadata_button</tabstop>
<tabstop>add_format_button</tabstop> <tabstop>add_format_button</tabstop>
<tabstop>remove_format_button</tabstop> <tabstop>remove_format_button</tabstop>
<tabstop>button_set_cover</tabstop>
<tabstop>button_set_metadata</tabstop>
<tabstop>formats</tabstop>
<tabstop>cover_path</tabstop> <tabstop>cover_path</tabstop>
<tabstop>cover_button</tabstop> <tabstop>cover_button</tabstop>
<tabstop>reset_cover</tabstop> <tabstop>reset_cover</tabstop>
<tabstop>fetch_cover_button</tabstop> <tabstop>fetch_cover_button</tabstop>
<tabstop>button_set_cover</tabstop> <tabstop>generate_cover_button</tabstop>
<tabstop>formats</tabstop>
<tabstop>button_set_metadata</tabstop>
<tabstop>button_box</tabstop>
<tabstop>scrollArea</tabstop> <tabstop>scrollArea</tabstop>
<tabstop>central_widget</tabstop>
<tabstop>button_box</tabstop>
</tabstops> </tabstops>
<resources> <resources>
<include location="../../../../resources/images.qrc"/> <include location="../../../../resources/images.qrc"/>

View File

@ -13,8 +13,10 @@ from Queue import Queue, Empty
from calibre.ebooks.metadata.fetch import search, get_social_metadata from calibre.ebooks.metadata.fetch import search, get_social_metadata
from calibre.gui2 import config from calibre.gui2 import config
from calibre.ebooks.metadata.library_thing import cover_from_isbn from calibre.ebooks.metadata.covers import download_cover
from calibre.customize.ui import get_isbndb_key from calibre.customize.ui import get_isbndb_key
from calibre import prints
from calibre.constants import DEBUG
class Worker(Thread): class Worker(Thread):
@ -26,13 +28,15 @@ class Worker(Thread):
def run(self): def run(self):
while True: while True:
isbn = self.jobs.get() mi = self.jobs.get()
if not isbn: if not getattr(mi, 'isbn', False):
break break
try: try:
cdata, _ = cover_from_isbn(isbn) cdata, errors = download_cover(mi)
if cdata: if cdata:
self.results.put((isbn, cdata)) self.results.put((mi.isbn, cdata))
elif DEBUG:
prints('Cover download failed:', errors)
except: except:
traceback.print_exc() traceback.print_exc()
@ -98,7 +102,7 @@ class DownloadMetadata(Thread):
fmi = results[0] fmi = results[0]
self.fetched_metadata[id] = fmi self.fetched_metadata[id] = fmi
if fmi.isbn and self.get_covers: if fmi.isbn and self.get_covers:
self.worker.jobs.put(fmi.isbn) self.worker.jobs.put(fmi)
if (not config['overwrite_author_title_metadata']): if (not config['overwrite_author_title_metadata']):
fmi.authors = mi.authors fmi.authors = mi.authors
fmi.author_sort = mi.author_sort fmi.author_sort = mi.author_sort

View File

@ -29,6 +29,8 @@ from calibre.utils.config import dynamic, prefs
from calibre.gui2 import NONE, choose_dir, error_dialog from calibre.gui2 import NONE, choose_dir, error_dialog
from calibre.gui2.dialogs.progress import ProgressDialog from calibre.gui2.dialogs.progress import ProgressDialog
# Devices {{{
class Device(object): class Device(object):
output_profile = 'default' output_profile = 'default'
@ -166,9 +168,9 @@ class iPhone(Device):
class Android(Device): class Android(Device):
name = 'Adroid phone + WordPlayer' name = 'Adroid phone + WordPlayer/Aldiko'
output_format = 'EPUB' output_format = 'EPUB'
manufacturer = 'Google/HTC' manufacturer = 'Android'
id = 'android' id = 'android'
class HanlinV3(Device): class HanlinV3(Device):
@ -209,6 +211,7 @@ class EZReaderPP(HanlinV5):
manufacturer = 'Astak' manufacturer = 'Astak'
id = 'ezreader_pp' id = 'ezreader_pp'
# }}}
def get_devices(): def get_devices():
for x in globals().values(): for x in globals().values():

View File

@ -37,8 +37,8 @@
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>20</width> <width>0</width>
<height>56</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -46,7 +46,7 @@
<item> <item>
<widget class="QLabel" name="label_2"> <widget class="QLabel" name="label_2">
<property name="text"> <property name="text">
<string>&lt;h2&gt;Demo videos&lt;/h2&gt;Videos demonstrating the various features of calibre are available &lt;a href=&quot;http://calibre-ebook.com/demo&quot;&gt;online&lt;/a&gt;.</string> <string>&lt;h2&gt;Demo videos&lt;/h2&gt;Videos demonstrating the various features of calibre are available &lt;a href=&quot;http://calibre-ebook.com/demo&quot;&gt;online&lt;/a&gt;.</string>
</property> </property>
<property name="wordWrap"> <property name="wordWrap">
<bool>true</bool> <bool>true</bool>
@ -59,19 +59,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>56</height>
</size>
</property>
</spacer>
</item>
<item> <item>
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">
<property name="text"> <property name="text">
@ -88,19 +75,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>

View File

@ -542,6 +542,8 @@ class ResultCache(SearchQueryParser):
if field is not None: if field is not None:
self.sort(field, ascending) self.sort(field, ascending)
self._map_filtered = list(self._map) self._map_filtered = list(self._map)
if self.search_restriction:
self.search('', return_matches=False, ignore_search_restriction=False)
def seriescmp(self, sidx, siidx, x, y, library_order=None): def seriescmp(self, sidx, siidx, x, y, library_order=None):
try: try:

View File

@ -10,7 +10,7 @@ from copy import deepcopy
from xml.sax.saxutils import escape from xml.sax.saxutils import escape
from calibre import filesystem_encoding, prints, prepare_string_for_xml, strftime from calibre import prints, prepare_string_for_xml, strftime
from calibre.constants import preferred_encoding from calibre.constants import preferred_encoding
from calibre.customize import CatalogPlugin from calibre.customize import CatalogPlugin
from calibre.customize.conversion import OptionRecommendation, DummyReporter from calibre.customize.conversion import OptionRecommendation, DummyReporter
@ -1207,9 +1207,7 @@ class EPUB_MOBI(CatalogPlugin):
self.generateHTMLByDateRead() self.generateHTMLByDateRead()
self.generateHTMLByTags() self.generateHTMLByTags()
from calibre.utils.PythonMagickWand import ImageMagick self.generateThumbnails()
with ImageMagick():
self.generateThumbnails()
self.generateOPF() self.generateOPF()
self.generateNCXHeader() self.generateNCXHeader()
@ -4062,29 +4060,15 @@ class EPUB_MOBI(CatalogPlugin):
return ' '.join(translated) return ' '.join(translated)
def generateThumbnail(self, title, image_dir, thumb_file): def generateThumbnail(self, title, image_dir, thumb_file):
import calibre.utils.PythonMagickWand as pw from calibre.utils.magick import Image
try: try:
img = pw.NewMagickWand() img = Image()
if img < 0: img.open(title['cover'])
raise RuntimeError('generateThumbnail(): Cannot create wand')
# Read the cover
if not pw.MagickReadImage(img,
title['cover'].encode(filesystem_encoding)):
self.opts.log.error('generateThumbnail(): Failed to read cover image from: %s' % title['cover'])
raise IOError
thumb = pw.CloneMagickWand(img)
if thumb < 0:
self.opts.log.error('generateThumbnail(): Cannot clone cover')
raise RuntimeError
# img, width, height # img, width, height
pw.MagickThumbnailImage(thumb, self.thumbWidth, self.thumbHeight) img.thumbnail(self.thumbWidth, self.thumbHeight)
pw.MagickWriteImage(thumb, os.path.join(image_dir, thumb_file)) img.save(os.path.join(image_dir, thumb_file))
pw.DestroyMagickWand(thumb) except:
pw.DestroyMagickWand(img) self.opts.log.error("generateThumbnail(): Error with %s" % title['title'])
except IOError:
self.opts.log.error("generateThumbnail(): IOError with %s" % title['title'])
except RuntimeError:
self.opts.log.error("generateThumbnail(): RuntimeError with %s" % title['title'])
def getMarkerTags(self): def getMarkerTags(self):
''' Return a list of special marker tags to be excluded from genre list ''' ''' Return a list of special marker tags to be excluded from genre list '''

View File

@ -32,7 +32,7 @@ from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp
from calibre.utils.config import prefs, tweaks from calibre.utils.config import prefs, tweaks
from calibre.utils.search_query_parser import saved_searches, set_saved_searches from calibre.utils.search_query_parser import saved_searches, set_saved_searches
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
from calibre.utils.magick_draw import save_cover_data_to from calibre.utils.magick.draw import save_cover_data_to
if iswindows: if iswindows:
import calibre.utils.winshell as winshell import calibre.utils.winshell as winshell
@ -317,6 +317,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
'title', 'timestamp', 'uuid', 'pubdate'): 'title', 'timestamp', 'uuid', 'pubdate'):
setattr(self, prop, functools.partial(get_property, setattr(self, prop, functools.partial(get_property,
loc=self.FIELD_MAP['comments' if prop == 'comment' else prop])) loc=self.FIELD_MAP['comments' if prop == 'comment' else prop]))
setattr(self, 'title_sort', functools.partial(get_property,
loc=self.FIELD_MAP['sort']))
def initialize_database(self): def initialize_database(self):
metadata_sqlite = open(P('metadata_sqlite.sql'), 'rb').read() metadata_sqlite = open(P('metadata_sqlite.sql'), 'rb').read()
@ -494,6 +496,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
mi.timestamp = self.timestamp(idx, index_is_id=index_is_id) mi.timestamp = self.timestamp(idx, index_is_id=index_is_id)
mi.pubdate = self.pubdate(idx, index_is_id=index_is_id) mi.pubdate = self.pubdate(idx, index_is_id=index_is_id)
mi.uuid = self.uuid(idx, index_is_id=index_is_id) mi.uuid = self.uuid(idx, index_is_id=index_is_id)
mi.title_sort = self.title_sort(idx, index_is_id=index_is_id)
tags = self.tags(idx, index_is_id=index_is_id) tags = self.tags(idx, index_is_id=index_is_id)
if tags: if tags:
mi.tags = [i.strip() for i in tags.split(',')] mi.tags = [i.strip() for i in tags.split(',')]

View File

@ -8,13 +8,13 @@ __docformat__ = 'restructuredtext en'
import os, traceback, cStringIO, re import os, traceback, cStringIO, re
from calibre.utils.config import Config, StringConfig from calibre.utils.config import Config, StringConfig, tweaks
from calibre.utils.filenames import shorten_components_to, supports_long_names, \ from calibre.utils.filenames import shorten_components_to, supports_long_names, \
ascii_filename, sanitize_file_name ascii_filename, sanitize_file_name
from calibre.ebooks.metadata.opf2 import metadata_to_opf from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.ebooks.metadata.meta import set_metadata from calibre.ebooks.metadata.meta import set_metadata
from calibre.constants import preferred_encoding, filesystem_encoding from calibre.constants import preferred_encoding, filesystem_encoding
from calibre.ebooks.metadata import title_sort
from calibre import strftime from calibre import strftime
DEFAULT_TEMPLATE = '{author_sort}/{title}/{title} - {authors}' DEFAULT_TEMPLATE = '{author_sort}/{title}/{title} - {authors}'
@ -99,7 +99,8 @@ def preprocess_template(template):
def safe_format(x, format_args): def safe_format(x, format_args):
try: try:
return x.format(**format_args).strip() ans = x.format(**format_args).strip()
return re.sub(r'\s+', ' ', ans)
except IndexError: # Thrown if user used [] and index is out of bounds except IndexError: # Thrown if user used [] and index is out of bounds
pass pass
except AttributeError: # Thrown if user used a non existing attribute except AttributeError: # Thrown if user used a non existing attribute
@ -109,9 +110,11 @@ def safe_format(x, format_args):
def get_components(template, mi, id, timefmt='%b %Y', length=250, def get_components(template, mi, id, timefmt='%b %Y', length=250,
sanitize_func=ascii_filename, replace_whitespace=False, sanitize_func=ascii_filename, replace_whitespace=False,
to_lowercase=False): to_lowercase=False):
library_order = tweaks['save_template_title_series_sorting'] == 'library_order'
tsfmt = title_sort if library_order else lambda x: x
format_args = dict(**FORMAT_ARGS) format_args = dict(**FORMAT_ARGS)
if mi.title: if mi.title:
format_args['title'] = mi.title format_args['title'] = tsfmt(mi.title)
if mi.authors: if mi.authors:
format_args['authors'] = mi.format_authors() format_args['authors'] = mi.format_authors()
format_args['author'] = format_args['authors'] format_args['author'] = format_args['authors']
@ -122,9 +125,11 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
if format_args['tags'].startswith('/'): if format_args['tags'].startswith('/'):
format_args['tags'] = format_args['tags'][1:] format_args['tags'] = format_args['tags'][1:]
if mi.series: if mi.series:
format_args['series'] = mi.series format_args['series'] = tsfmt(mi.series)
if mi.series_index is not None: if mi.series_index is not None:
format_args['series_index'] = mi.format_series_index() format_args['series_index'] = mi.format_series_index()
else:
template = re.sub(r'\{series_index[^}]*?\}', '', template)
if mi.rating is not None: if mi.rating is not None:
format_args['rating'] = mi.format_rating() format_args['rating'] = mi.format_rating()
if mi.isbn: if mi.isbn:

View File

@ -5,18 +5,19 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import re import re, os
import __builtin__ import __builtin__
import cherrypy import cherrypy
from lxml import html from lxml import html
from lxml.html.builder import HTML, HEAD, TITLE, STYLE, LINK, DIV, IMG, BODY, \ from lxml.html.builder import HTML, HEAD, TITLE, LINK, DIV, IMG, BODY, \
OPTION, SELECT, INPUT, FORM, SPAN, TABLE, TR, TD, A, HR OPTION, SELECT, INPUT, FORM, SPAN, TABLE, TR, TD, A, HR
from calibre.library.server.utils import strftime from calibre.library.server.utils import strftime
from calibre.ebooks.metadata import fmt_sidx from calibre.ebooks.metadata import fmt_sidx
from calibre.constants import __appname__ from calibre.constants import __appname__
from calibre import human_readable from calibre import human_readable
from calibre.utils.date import utcfromtimestamp
def CLASS(*args, **kwargs): # class is a reserved word in Python def CLASS(*args, **kwargs): # class is a reserved word in Python
kwargs['class'] = ' '.join(args) kwargs['class'] = ' '.join(args)
@ -140,85 +141,7 @@ def build_index(books, num, search, sort, order, start, total, url_base):
TITLE(__appname__ + ' Library'), TITLE(__appname__ + ' Library'),
LINK(rel='icon', href='http://calibre-ebook.com/favicon.ico', LINK(rel='icon', href='http://calibre-ebook.com/favicon.ico',
type='image/x-icon'), type='image/x-icon'),
STYLE( # {{{ LINK(rel='stylesheet', type='text/css', href='/mobile/style.css')
'''
.navigation table.buttons {
width: 100%;
}
.navigation .button {
width: 50%;
}
.button a, .button:visited a {
padding: 0.5em;
font-size: 1.25em;
border: 1px solid black;
text-color: black;
background-color: #ddd;
border-top: 1px solid ThreeDLightShadow;
border-right: 1px solid ButtonShadow;
border-bottom: 1px solid ButtonShadow;
border-left: 1 px solid ThreeDLightShadow;
-moz-border-radius: 0.25em;
-webkit-border-radius: 0.25em;
}
.button:hover a {
border-top: 1px solid #666;
border-right: 1px solid #CCC;
border-bottom: 1 px solid #CCC;
border-left: 1 px solid #666;
}
div.navigation {
padding-bottom: 1em;
clear: both;
}
#search_box {
border: 1px solid #393;
-moz-border-radius: 0.5em;
-webkit-border-radius: 0.5em;
padding: 1em;
margin-bottom: 0.5em;
float: right;
}
#listing {
width: 100%;
border-collapse: collapse;
}
#listing td {
padding: 0.25em;
}
#listing td.thumbnail {
height: 60px;
width: 60px;
}
#listing tr:nth-child(even) {
background: #eee;
}
#listing .button a{
display: inline-block;
width: 2.5em;
padding-left: 0em;
padding-right: 0em;
overflow: hidden;
text-align: center;
}
#logo {
float: left;
}
#spacer {
clear: both;
}
''', type='text/css') # }}}
), # End head ), # End head
body body
) # End html ) # End html
@ -231,6 +154,14 @@ class MobileServer(object):
def add_routes(self, connect): def add_routes(self, connect):
connect('mobile', '/mobile', self.mobile) connect('mobile', '/mobile', self.mobile)
connect('mobile_css', '/mobile/style.css', self.mobile_css)
def mobile_css(self, *args, **kwargs):
path = P('content_server/mobile.css')
cherrypy.response.headers['Content-Type'] = 'text/css; charset=utf-8'
updated = utcfromtimestamp(os.stat(path).st_mtime)
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
return open(path, 'rb').read()
def mobile(self, start='1', num='25', sort='date', search='', def mobile(self, start='1', num='25', sort='date', search='',
_=None, order='descending'): _=None, order='descending'):

View File

@ -400,7 +400,9 @@ class OPDSServer(object):
owhich = hexlify('N'+which) owhich = hexlify('N'+which)
up_url = url_for('opdsnavcatalog', version, which=owhich) up_url = url_for('opdsnavcatalog', version, which=owhich)
items = categories[category] items = categories[category]
items = [x for x in items if getattr(x, 'sort', x.name).startswith(which)] def belongs(x, which):
return getattr(x, 'sort', x.name).lower().startswith(which.lower())
items = [x for x in items if belongs(x, which)]
if not items: if not items:
raise cherrypy.HTTPError(404, 'No items in group %r:%r'%(category, raise cherrypy.HTTPError(404, 'No items in group %r:%r'%(category,
which)) which))
@ -465,7 +467,12 @@ class OPDSServer(object):
def __init__(self, text, count): def __init__(self, text, count):
self.text, self.count = text, count self.text, self.count = text, count
starts = set([getattr(x, 'sort', x.name)[0] for x in items]) starts = set([])
for x in items:
val = getattr(x, 'sort', x.name)
if not val:
val = 'A'
starts.add(val[0].upper())
category_groups = OrderedDict() category_groups = OrderedDict()
for x in sorted(starts, cmp=lambda x,y:cmp(x.lower(), y.lower())): for x in sorted(starts, cmp=lambda x,y:cmp(x.lower(), y.lower())):
category_groups[x] = len([y for y in items if category_groups[x] = len([y for y in items if

View File

@ -71,6 +71,11 @@ Metadata download plugins
:members: :members:
:member-order: bysource :member-order: bysource
.. autoclass:: calibre.ebooks.metadata.covers.CoverDownload
:show-inheritance:
:members:
:member-order: bysource
Conversion plugins Conversion plugins
-------------------- --------------------

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,200 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os
from calibre.constants import plugins, filesystem_encoding
_magick, _merr = plugins['magick']
if _magick is None:
raise RuntimeError('Failed to load ImageMagick: '+_merr)
_gravity_map = dict([(getattr(_magick, x), x) for x in dir(_magick) if
x.endswith('Gravity')])
_type_map = dict([(getattr(_magick, x), x) for x in dir(_magick) if
x.endswith('Type')])
# Font metrics {{{
class Rect(object):
def __init__(self, left, top, right, bottom):
self.left, self.top, self.right, self.bottom = left, top, right, bottom
def __str__(self):
return '(%s, %s) -- (%s, %s)'%(self.left, self.top, self.right,
self.bottom)
class FontMetrics(object):
def __init__(self, ret):
self._attrs = []
for i, x in enumerate(('char_width', 'char_height', 'ascender',
'descender', 'text_width', 'text_height',
'max_horizontal_advance')):
setattr(self, x, ret[i])
self._attrs.append(x)
self.bounding_box = Rect(ret[7], ret[8], ret[9], ret[10])
self.x, self.y = ret[11], ret[12]
self._attrs.extend(['bounding_box', 'x', 'y'])
self._attrs = tuple(self._attrs)
def __str__(self):
return '''FontMetrics:
char_width: %s
char_height: %s
ascender: %s
descender: %s
text_width: %s
text_height: %s
max_horizontal_advance: %s
bounding_box: %s
x: %s
y: %s
'''%tuple([getattr(self, x) for x in self._attrs])
# }}}
class PixelWand(_magick.PixelWand): # {{{
pass
# }}}
class DrawingWand(_magick.DrawingWand): # {{{
@dynamic_property
def font(self):
def fget(self):
return self.font_.decode(filesystem_encoding, 'replace').lower()
def fset(self, val):
if isinstance(val, unicode):
val = val.encode(filesystem_encoding)
self.font_ = str(val)
return property(fget=fget, fset=fset, doc=_magick.DrawingWand.font_.__doc__)
@dynamic_property
def gravity(self):
def fget(self):
val = self.gravity_
return _gravity_map[val]
def fset(self, val):
val = getattr(_magick, str(val))
self.gravity_ = val
return property(fget=fget, fset=fset, doc=_magick.DrawingWand.gravity_.__doc__)
@dynamic_property
def font_size(self):
def fget(self):
return self.font_size_
def fset(self, val):
self.font_size_ = float(val)
return property(fget=fget, fset=fset, doc=_magick.DrawingWand.font_size_.__doc__)
# }}}
class Image(_magick.Image): # {{{
@property
def clone(self):
ans = Image()
ans.copy(self)
return ans
def load(self, data):
return _magick.Image.load(self, bytes(data))
def open(self, path_or_file):
data = path_or_file
if hasattr(data, 'read'):
data = data.read()
else:
data = open(data, 'rb').read()
self.load(data)
@dynamic_property
def format(self):
def fget(self):
return self.format_.decode('utf-8', 'ignore').lower()
def fset(self, val):
self.format_ = str(val)
return property(fget=fget, fset=fset, doc=_magick.Image.format_.__doc__)
@dynamic_property
def type(self):
def fget(self):
return _type_map[self.type_]
def fset(self, val):
val = getattr(_magick, str(val))
self.type_ = val
return property(fget=fget, fset=fset, doc=_magick.Image.type_.__doc__)
@dynamic_property
def size(self):
def fget(self):
return self.size_
def fset(self, val):
filter = 'CatromFilter'
if len(val) > 2:
filter = val[2]
filter = int(getattr(_magick, filter))
blur = 1.0
if len(val) > 3:
blur = float(val[3])
self.size_ = (int(val[0]), int(val[1]), filter, blur)
return property(fget=fget, fset=fset, doc=_magick.Image.size_.__doc__)
def save(self, path, format=None):
if format is None:
ext = os.path.splitext(path)[1]
if len(ext) < 2:
raise ValueError('No format specified')
format = ext[1:]
format = format.upper()
with open(path, 'wb') as f:
f.write(self.export(format))
def compose(self, img, left=0, top=0, operation='OverCompositeOp'):
op = getattr(_magick, operation)
bounds = self.size
if left < 0 or top < 0 or left >= bounds[0] or top >= bounds[1]:
raise ValueError('left and/or top out of bounds')
_magick.Image.compose(self, img, int(left), int(top), op)
def font_metrics(self, drawing_wand, text):
if isinstance(text, unicode):
text = text.encode('UTF-8')
return FontMetrics(_magick.Image.font_metrics(self, drawing_wand, text))
def annotate(self, drawing_wand, x, y, angle, text):
if isinstance(text, unicode):
text = text.encode('UTF-8')
return _magick.Image.annotate(self, drawing_wand, x, y, angle, text)
def distort(self, method, arguments, bestfit):
method = getattr(_magick, method)
arguments = [float(x) for x in arguments]
_magick.Image.distort(self, method, arguments, bestfit)
def rotate(self, background_pixel_wand, degrees):
_magick.Image.rotate(self, background_pixel_wand, float(degrees))
def quantize(self, number_colors, colorspace='RGBColorspace', treedepth=0, dither=True,
measure_error=False):
colorspace = getattr(_magick, colorspace)
_magick.Image.quantize(self, number_colors, colorspace, treedepth, dither,
measure_error)
# }}}
def create_canvas(width, height, bgcolor):
canvas = Image()
canvas.create_canvas(int(width), int(height), str(bgcolor))
return canvas

View File

@ -0,0 +1,164 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from calibre.utils.magick import Image, DrawingWand, create_canvas
from calibre.constants import __appname__, __version__
def save_cover_data_to(data, path, bgcolor='white', resize_to=None):
'''
Saves image in data to path, in the format specified by the path
extension. Composes the image onto a blank canvas so as to
properly convert transparent images.
'''
img = Image()
img.load(data)
if resize_to is not None:
img.size = (resize_to[0], resize_to[1])
canvas = create_canvas(img.size[0], img.size[1], bgcolor)
canvas.compose(img)
canvas.save(path)
def identify_data(data):
'''
Identify the image in data. Returns a 3-tuple
(width, height, format)
or raises an Exception if data is not an image.
'''
img = Image()
img.load(data)
width, height = img.size
fmt = img.format
return (width, height, fmt)
def identify(path):
'''
Identify the image at path. Returns a 3-tuple
(width, height, format)
or raises an Exception.
'''
data = open(path, 'rb').read()
return identify_data(data)
def add_borders_to_image(path_to_image, left=0, top=0, right=0, bottom=0,
border_color='white'):
img = Image()
img.open(path_to_image)
lwidth, lheight = img.size
canvas = create_canvas(lwidth+left+right, lheight+top+bottom,
border_color)
canvas.compose(img, left, top)
canvas.save(path_to_image)
def create_text_wand(font_size, font_path=None):
if font_path is None:
font_path = P('fonts/liberation/LiberationSerif-Bold.ttf')
ans = DrawingWand()
ans.font = font_path
ans.font_size = font_size
ans.gravity = 'CenterGravity'
ans.text_alias = True
return ans
def create_text_arc(text, font_size, font=None, bgcolor='white'):
if isinstance(text, unicode):
text = text.encode('utf-8')
canvas = create_canvas(300, 300, bgcolor)
tw = create_text_wand(font_size, font_path=font)
m = canvas.font_metrics(tw, text)
canvas = create_canvas(int(m.text_width)+20, int(m.text_height*3.5), bgcolor)
canvas.annotate(tw, 0, 0, 0, text)
canvas.distort("ArcDistortion", [120], True)
canvas.trim(0)
return canvas
def _get_line(img, dw, tokens, line_width):
line, rest = tokens, []
while True:
m = img.font_metrics(dw, ' '.join(line))
width, height = m.text_width, m.text_height
if width < line_width:
return line, rest
rest = line[-1:] + rest
line = line[:-1]
def annotate_img(img, dw, left, top, rotate, text,
translate_from_top_left=True):
if isinstance(text, unicode):
text = text.encode('utf-8')
if translate_from_top_left:
m = img.font_metrics(dw, text)
img_width, img_height = img.size
left = left - img_width/2. + m.text_width/2.
top = top - img_height/2. + m.text_height/2.
img.annotate(dw, left, top, rotate, text)
def draw_centered_line(img, dw, line, top):
m = img.font_metrics(dw, line)
width, height = m.text_width, m.text_height
img_width = img.size[0]
left = max(int((img_width - width)/2.), 0)
annotate_img(img, dw, left, top, 0, line)
return top + height
def draw_centered_text(img, dw, text, top, margin=10):
img_width = img.size[0]
tokens = text.split(' ')
while tokens:
line, tokens = _get_line(img, dw, tokens, img_width-2*margin)
if not line:
# Could not fit the first token on the line
line = tokens[:1]
tokens = tokens[1:]
bottom = draw_centered_line(img, dw, ' '.join(line), top)
top = bottom
return top
class TextLine(object):
def __init__(self, text, font_size, bottom_margin=30, font_path=None):
self.text, self.font_size, = text, font_size
self.bottom_margin = bottom_margin
self.font_path = font_path
def __repr__(self):
return u'TextLine:%r:%f'%(self.text, self.font_size)
def create_cover_page(top_lines, logo_path, width=590, height=750,
bgcolor='white', output_format='jpg'):
'''
Create the standard calibre cover page and return it as a byte string in
the specified output_format.
'''
canvas = create_canvas(width, height, bgcolor)
bottom = 10
for line in top_lines:
twand = create_text_wand(line.font_size, font_path=line.font_path)
bottom = draw_centered_text(canvas, twand, line.text, bottom)
bottom += line.bottom_margin
bottom -= top_lines[-1].bottom_margin
vanity = create_text_arc(__appname__ + ' ' + __version__, 24,
font=P('fonts/liberation/LiberationMono-Regular.ttf'))
lwidth, lheight = vanity.size
left = int(max(0, (width - lwidth)/2.))
top = height - lheight - 10
canvas.compose(vanity, left, top)
logo = Image()
logo.open(logo_path)
lwidth, lheight = logo.size
left = int(max(0, (width - lwidth)/2.))
top = max(int((height - lheight)/2.), bottom+20)
canvas.compose(logo, left, top)
return canvas.export(output_format)

View File

@ -0,0 +1,71 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os, textwrap, re, subprocess
INC = '/usr/include/ImageMagick'
'''
Various constants defined in the ImageMagick header files. Note that
they are defined as actual numeric constants rather than symbolic names to
ensure that the extension can be compiled against older versions of ImageMagick
than the one this script is run against.
'''
def parse_enums(f):
print '\nParsing:', f
raw = open(os.path.join(INC, f)).read()
raw = re.sub(r'(?s)/\*.*?\*/', '', raw)
raw = re.sub('#.*', '', raw)
for enum in re.findall(r'typedef\s+enum\s+\{([^}]+)', raw):
enum = re.sub(r'(?s)/\*.*?\*/', '', enum)
for x in enum.splitlines():
e = x.split(',')[0].strip().split(' ')[0]
if e:
val = get_value(e)
print e, val
yield e, val
def get_value(const):
t = '''
#include <wand/MagickWand.h>
#include <stdio.h>
int main(int argc, char **argv) {
printf("%%d", %s);
return 0;
}
'''%const
with open('/tmp/ig.c','wb') as f:
f.write(t)
subprocess.check_call(['gcc', '-I/usr/include/ImageMagick', '/tmp/ig.c', '-o', '/tmp/ig', '-lMagickWand'])
return int(subprocess.Popen(["/tmp/ig"],
stdout=subprocess.PIPE).communicate()[0].strip())
def main():
constants = []
for x in ('resample', 'image', 'draw', 'distort', 'composite', 'geometry',
'colorspace'):
constants += list(parse_enums('magick/%s.h'%x))
base = os.path.dirname(__file__)
constants = [
'PyModule_AddIntConstant(m, "{0}", {1});'.format(c, v) for c, v in
constants]
raw = textwrap.dedent('''\
// Generated by generate.py
static void magick_add_module_constants(PyObject *m) {
%s
}
''')%'\n '.join(constants)
with open(os.path.join(base, 'magick_constants.h'), 'wb') as f:
f.write(raw)
if __name__ == '__main__':
main()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,289 @@
// Generated by generate.py
static void magick_add_module_constants(PyObject *m) {
PyModule_AddIntConstant(m, "UndefinedFilter", 0);
PyModule_AddIntConstant(m, "PointFilter", 1);
PyModule_AddIntConstant(m, "BoxFilter", 2);
PyModule_AddIntConstant(m, "TriangleFilter", 3);
PyModule_AddIntConstant(m, "HermiteFilter", 4);
PyModule_AddIntConstant(m, "HanningFilter", 5);
PyModule_AddIntConstant(m, "HammingFilter", 6);
PyModule_AddIntConstant(m, "BlackmanFilter", 7);
PyModule_AddIntConstant(m, "GaussianFilter", 8);
PyModule_AddIntConstant(m, "QuadraticFilter", 9);
PyModule_AddIntConstant(m, "CubicFilter", 10);
PyModule_AddIntConstant(m, "CatromFilter", 11);
PyModule_AddIntConstant(m, "MitchellFilter", 12);
PyModule_AddIntConstant(m, "LanczosFilter", 13);
PyModule_AddIntConstant(m, "BesselFilter", 14);
PyModule_AddIntConstant(m, "SincFilter", 15);
PyModule_AddIntConstant(m, "KaiserFilter", 16);
PyModule_AddIntConstant(m, "WelshFilter", 17);
PyModule_AddIntConstant(m, "ParzenFilter", 18);
PyModule_AddIntConstant(m, "LagrangeFilter", 19);
PyModule_AddIntConstant(m, "BohmanFilter", 20);
PyModule_AddIntConstant(m, "BartlettFilter", 21);
PyModule_AddIntConstant(m, "SentinelFilter", 22);
PyModule_AddIntConstant(m, "UndefinedInterpolatePixel", 0);
PyModule_AddIntConstant(m, "AverageInterpolatePixel", 1);
PyModule_AddIntConstant(m, "BicubicInterpolatePixel", 2);
PyModule_AddIntConstant(m, "BilinearInterpolatePixel", 3);
PyModule_AddIntConstant(m, "FilterInterpolatePixel", 4);
PyModule_AddIntConstant(m, "IntegerInterpolatePixel", 5);
PyModule_AddIntConstant(m, "MeshInterpolatePixel", 6);
PyModule_AddIntConstant(m, "NearestNeighborInterpolatePixel", 7);
PyModule_AddIntConstant(m, "SplineInterpolatePixel", 8);
PyModule_AddIntConstant(m, "UndefinedAlphaChannel", 0);
PyModule_AddIntConstant(m, "ActivateAlphaChannel", 1);
PyModule_AddIntConstant(m, "BackgroundAlphaChannel", 2);
PyModule_AddIntConstant(m, "CopyAlphaChannel", 3);
PyModule_AddIntConstant(m, "DeactivateAlphaChannel", 4);
PyModule_AddIntConstant(m, "ExtractAlphaChannel", 5);
PyModule_AddIntConstant(m, "OpaqueAlphaChannel", 6);
PyModule_AddIntConstant(m, "ResetAlphaChannel", 7);
PyModule_AddIntConstant(m, "SetAlphaChannel", 8);
PyModule_AddIntConstant(m, "ShapeAlphaChannel", 9);
PyModule_AddIntConstant(m, "TransparentAlphaChannel", 10);
PyModule_AddIntConstant(m, "UndefinedType", 0);
PyModule_AddIntConstant(m, "BilevelType", 1);
PyModule_AddIntConstant(m, "GrayscaleType", 2);
PyModule_AddIntConstant(m, "GrayscaleMatteType", 3);
PyModule_AddIntConstant(m, "PaletteType", 4);
PyModule_AddIntConstant(m, "PaletteMatteType", 5);
PyModule_AddIntConstant(m, "TrueColorType", 6);
PyModule_AddIntConstant(m, "TrueColorMatteType", 7);
PyModule_AddIntConstant(m, "ColorSeparationType", 8);
PyModule_AddIntConstant(m, "ColorSeparationMatteType", 9);
PyModule_AddIntConstant(m, "OptimizeType", 10);
PyModule_AddIntConstant(m, "PaletteBilevelMatteType", 11);
PyModule_AddIntConstant(m, "UndefinedInterlace", 0);
PyModule_AddIntConstant(m, "NoInterlace", 1);
PyModule_AddIntConstant(m, "LineInterlace", 2);
PyModule_AddIntConstant(m, "PlaneInterlace", 3);
PyModule_AddIntConstant(m, "PartitionInterlace", 4);
PyModule_AddIntConstant(m, "GIFInterlace", 5);
PyModule_AddIntConstant(m, "JPEGInterlace", 6);
PyModule_AddIntConstant(m, "PNGInterlace", 7);
PyModule_AddIntConstant(m, "UndefinedOrientation", 0);
PyModule_AddIntConstant(m, "TopLeftOrientation", 1);
PyModule_AddIntConstant(m, "TopRightOrientation", 2);
PyModule_AddIntConstant(m, "BottomRightOrientation", 3);
PyModule_AddIntConstant(m, "BottomLeftOrientation", 4);
PyModule_AddIntConstant(m, "LeftTopOrientation", 5);
PyModule_AddIntConstant(m, "RightTopOrientation", 6);
PyModule_AddIntConstant(m, "RightBottomOrientation", 7);
PyModule_AddIntConstant(m, "LeftBottomOrientation", 8);
PyModule_AddIntConstant(m, "UndefinedResolution", 0);
PyModule_AddIntConstant(m, "PixelsPerInchResolution", 1);
PyModule_AddIntConstant(m, "PixelsPerCentimeterResolution", 2);
PyModule_AddIntConstant(m, "UndefinedTransmitType", 0);
PyModule_AddIntConstant(m, "FileTransmitType", 1);
PyModule_AddIntConstant(m, "BlobTransmitType", 2);
PyModule_AddIntConstant(m, "StreamTransmitType", 3);
PyModule_AddIntConstant(m, "ImageTransmitType", 4);
PyModule_AddIntConstant(m, "UndefinedAlign", 0);
PyModule_AddIntConstant(m, "LeftAlign", 1);
PyModule_AddIntConstant(m, "CenterAlign", 2);
PyModule_AddIntConstant(m, "RightAlign", 3);
PyModule_AddIntConstant(m, "UndefinedPathUnits", 0);
PyModule_AddIntConstant(m, "UserSpace", 1);
PyModule_AddIntConstant(m, "UserSpaceOnUse", 2);
PyModule_AddIntConstant(m, "ObjectBoundingBox", 3);
PyModule_AddIntConstant(m, "UndefinedDecoration", 0);
PyModule_AddIntConstant(m, "NoDecoration", 1);
PyModule_AddIntConstant(m, "UnderlineDecoration", 2);
PyModule_AddIntConstant(m, "OverlineDecoration", 3);
PyModule_AddIntConstant(m, "LineThroughDecoration", 4);
PyModule_AddIntConstant(m, "UndefinedDirection", 0);
PyModule_AddIntConstant(m, "RightToLeftDirection", 1);
PyModule_AddIntConstant(m, "LeftToRightDirection", 2);
PyModule_AddIntConstant(m, "UndefinedRule", 0);
PyModule_AddIntConstant(m, "EvenOddRule", 1);
PyModule_AddIntConstant(m, "NonZeroRule", 2);
PyModule_AddIntConstant(m, "UndefinedGradient", 0);
PyModule_AddIntConstant(m, "LinearGradient", 1);
PyModule_AddIntConstant(m, "RadialGradient", 2);
PyModule_AddIntConstant(m, "UndefinedCap", 0);
PyModule_AddIntConstant(m, "ButtCap", 1);
PyModule_AddIntConstant(m, "RoundCap", 2);
PyModule_AddIntConstant(m, "SquareCap", 3);
PyModule_AddIntConstant(m, "UndefinedJoin", 0);
PyModule_AddIntConstant(m, "MiterJoin", 1);
PyModule_AddIntConstant(m, "RoundJoin", 2);
PyModule_AddIntConstant(m, "BevelJoin", 3);
PyModule_AddIntConstant(m, "UndefinedMethod", 0);
PyModule_AddIntConstant(m, "PointMethod", 1);
PyModule_AddIntConstant(m, "ReplaceMethod", 2);
PyModule_AddIntConstant(m, "FloodfillMethod", 3);
PyModule_AddIntConstant(m, "FillToBorderMethod", 4);
PyModule_AddIntConstant(m, "ResetMethod", 5);
PyModule_AddIntConstant(m, "UndefinedPrimitive", 0);
PyModule_AddIntConstant(m, "PointPrimitive", 1);
PyModule_AddIntConstant(m, "LinePrimitive", 2);
PyModule_AddIntConstant(m, "RectanglePrimitive", 3);
PyModule_AddIntConstant(m, "RoundRectanglePrimitive", 4);
PyModule_AddIntConstant(m, "ArcPrimitive", 5);
PyModule_AddIntConstant(m, "EllipsePrimitive", 6);
PyModule_AddIntConstant(m, "CirclePrimitive", 7);
PyModule_AddIntConstant(m, "PolylinePrimitive", 8);
PyModule_AddIntConstant(m, "PolygonPrimitive", 9);
PyModule_AddIntConstant(m, "BezierPrimitive", 10);
PyModule_AddIntConstant(m, "ColorPrimitive", 11);
PyModule_AddIntConstant(m, "MattePrimitive", 12);
PyModule_AddIntConstant(m, "TextPrimitive", 13);
PyModule_AddIntConstant(m, "ImagePrimitive", 14);
PyModule_AddIntConstant(m, "PathPrimitive", 15);
PyModule_AddIntConstant(m, "UndefinedReference", 0);
PyModule_AddIntConstant(m, "GradientReference", 1);
PyModule_AddIntConstant(m, "UndefinedSpread", 0);
PyModule_AddIntConstant(m, "PadSpread", 1);
PyModule_AddIntConstant(m, "ReflectSpread", 2);
PyModule_AddIntConstant(m, "RepeatSpread", 3);
PyModule_AddIntConstant(m, "UndefinedDistortion", 0);
PyModule_AddIntConstant(m, "AffineDistortion", 1);
PyModule_AddIntConstant(m, "AffineProjectionDistortion", 2);
PyModule_AddIntConstant(m, "ScaleRotateTranslateDistortion", 3);
PyModule_AddIntConstant(m, "PerspectiveDistortion", 4);
PyModule_AddIntConstant(m, "PerspectiveProjectionDistortion", 5);
PyModule_AddIntConstant(m, "BilinearForwardDistortion", 6);
PyModule_AddIntConstant(m, "BilinearDistortion", 6);
PyModule_AddIntConstant(m, "BilinearReverseDistortion", 7);
PyModule_AddIntConstant(m, "PolynomialDistortion", 8);
PyModule_AddIntConstant(m, "ArcDistortion", 9);
PyModule_AddIntConstant(m, "PolarDistortion", 10);
PyModule_AddIntConstant(m, "DePolarDistortion", 11);
PyModule_AddIntConstant(m, "BarrelDistortion", 12);
PyModule_AddIntConstant(m, "BarrelInverseDistortion", 13);
PyModule_AddIntConstant(m, "ShepardsDistortion", 14);
PyModule_AddIntConstant(m, "SentinelDistortion", 15);
PyModule_AddIntConstant(m, "UndefinedColorInterpolate", 0);
PyModule_AddIntConstant(m, "BarycentricColorInterpolate", 1);
PyModule_AddIntConstant(m, "BilinearColorInterpolate", 7);
PyModule_AddIntConstant(m, "PolynomialColorInterpolate", 8);
PyModule_AddIntConstant(m, "ShepardsColorInterpolate", 14);
PyModule_AddIntConstant(m, "VoronoiColorInterpolate", 15);
PyModule_AddIntConstant(m, "UndefinedCompositeOp", 0);
PyModule_AddIntConstant(m, "NoCompositeOp", 1);
PyModule_AddIntConstant(m, "ModulusAddCompositeOp", 2);
PyModule_AddIntConstant(m, "AtopCompositeOp", 3);
PyModule_AddIntConstant(m, "BlendCompositeOp", 4);
PyModule_AddIntConstant(m, "BumpmapCompositeOp", 5);
PyModule_AddIntConstant(m, "ChangeMaskCompositeOp", 6);
PyModule_AddIntConstant(m, "ClearCompositeOp", 7);
PyModule_AddIntConstant(m, "ColorBurnCompositeOp", 8);
PyModule_AddIntConstant(m, "ColorDodgeCompositeOp", 9);
PyModule_AddIntConstant(m, "ColorizeCompositeOp", 10);
PyModule_AddIntConstant(m, "CopyBlackCompositeOp", 11);
PyModule_AddIntConstant(m, "CopyBlueCompositeOp", 12);
PyModule_AddIntConstant(m, "CopyCompositeOp", 13);
PyModule_AddIntConstant(m, "CopyCyanCompositeOp", 14);
PyModule_AddIntConstant(m, "CopyGreenCompositeOp", 15);
PyModule_AddIntConstant(m, "CopyMagentaCompositeOp", 16);
PyModule_AddIntConstant(m, "CopyOpacityCompositeOp", 17);
PyModule_AddIntConstant(m, "CopyRedCompositeOp", 18);
PyModule_AddIntConstant(m, "CopyYellowCompositeOp", 19);
PyModule_AddIntConstant(m, "DarkenCompositeOp", 20);
PyModule_AddIntConstant(m, "DstAtopCompositeOp", 21);
PyModule_AddIntConstant(m, "DstCompositeOp", 22);
PyModule_AddIntConstant(m, "DstInCompositeOp", 23);
PyModule_AddIntConstant(m, "DstOutCompositeOp", 24);
PyModule_AddIntConstant(m, "DstOverCompositeOp", 25);
PyModule_AddIntConstant(m, "DifferenceCompositeOp", 26);
PyModule_AddIntConstant(m, "DisplaceCompositeOp", 27);
PyModule_AddIntConstant(m, "DissolveCompositeOp", 28);
PyModule_AddIntConstant(m, "ExclusionCompositeOp", 29);
PyModule_AddIntConstant(m, "HardLightCompositeOp", 30);
PyModule_AddIntConstant(m, "HueCompositeOp", 31);
PyModule_AddIntConstant(m, "InCompositeOp", 32);
PyModule_AddIntConstant(m, "LightenCompositeOp", 33);
PyModule_AddIntConstant(m, "LinearLightCompositeOp", 34);
PyModule_AddIntConstant(m, "LuminizeCompositeOp", 35);
PyModule_AddIntConstant(m, "MinusCompositeOp", 36);
PyModule_AddIntConstant(m, "ModulateCompositeOp", 37);
PyModule_AddIntConstant(m, "MultiplyCompositeOp", 38);
PyModule_AddIntConstant(m, "OutCompositeOp", 39);
PyModule_AddIntConstant(m, "OverCompositeOp", 40);
PyModule_AddIntConstant(m, "OverlayCompositeOp", 41);
PyModule_AddIntConstant(m, "PlusCompositeOp", 42);
PyModule_AddIntConstant(m, "ReplaceCompositeOp", 43);
PyModule_AddIntConstant(m, "SaturateCompositeOp", 44);
PyModule_AddIntConstant(m, "ScreenCompositeOp", 45);
PyModule_AddIntConstant(m, "SoftLightCompositeOp", 46);
PyModule_AddIntConstant(m, "SrcAtopCompositeOp", 47);
PyModule_AddIntConstant(m, "SrcCompositeOp", 48);
PyModule_AddIntConstant(m, "SrcInCompositeOp", 49);
PyModule_AddIntConstant(m, "SrcOutCompositeOp", 50);
PyModule_AddIntConstant(m, "SrcOverCompositeOp", 51);
PyModule_AddIntConstant(m, "ModulusSubtractCompositeOp", 52);
PyModule_AddIntConstant(m, "ThresholdCompositeOp", 53);
PyModule_AddIntConstant(m, "XorCompositeOp", 54);
PyModule_AddIntConstant(m, "DivideCompositeOp", 55);
PyModule_AddIntConstant(m, "DistortCompositeOp", 56);
PyModule_AddIntConstant(m, "BlurCompositeOp", 57);
PyModule_AddIntConstant(m, "PegtopLightCompositeOp", 58);
PyModule_AddIntConstant(m, "VividLightCompositeOp", 59);
PyModule_AddIntConstant(m, "PinLightCompositeOp", 60);
PyModule_AddIntConstant(m, "LinearDodgeCompositeOp", 61);
PyModule_AddIntConstant(m, "LinearBurnCompositeOp", 62);
PyModule_AddIntConstant(m, "MathematicsCompositeOp", 63);
PyModule_AddIntConstant(m, "NoValue", 0);
PyModule_AddIntConstant(m, "XValue", 1);
PyModule_AddIntConstant(m, "XiValue", 1);
PyModule_AddIntConstant(m, "YValue", 2);
PyModule_AddIntConstant(m, "PsiValue", 2);
PyModule_AddIntConstant(m, "WidthValue", 4);
PyModule_AddIntConstant(m, "RhoValue", 4);
PyModule_AddIntConstant(m, "HeightValue", 8);
PyModule_AddIntConstant(m, "SigmaValue", 8);
PyModule_AddIntConstant(m, "ChiValue", 16);
PyModule_AddIntConstant(m, "XiNegative", 32);
PyModule_AddIntConstant(m, "XNegative", 32);
PyModule_AddIntConstant(m, "PsiNegative", 64);
PyModule_AddIntConstant(m, "YNegative", 64);
PyModule_AddIntConstant(m, "ChiNegative", 128);
PyModule_AddIntConstant(m, "PercentValue", 4096);
PyModule_AddIntConstant(m, "AspectValue", 8192);
PyModule_AddIntConstant(m, "NormalizeValue", 8192);
PyModule_AddIntConstant(m, "LessValue", 16384);
PyModule_AddIntConstant(m, "GreaterValue", 32768);
PyModule_AddIntConstant(m, "MinimumValue", 65536);
PyModule_AddIntConstant(m, "CorrelateNormalizeValue", 65536);
PyModule_AddIntConstant(m, "AreaValue", 131072);
PyModule_AddIntConstant(m, "DecimalValue", 262144);
PyModule_AddIntConstant(m, "AllValues", 2147483647);
PyModule_AddIntConstant(m, "UndefinedGravity", 0);
PyModule_AddIntConstant(m, "ForgetGravity", 0);
PyModule_AddIntConstant(m, "NorthWestGravity", 1);
PyModule_AddIntConstant(m, "NorthGravity", 2);
PyModule_AddIntConstant(m, "NorthEastGravity", 3);
PyModule_AddIntConstant(m, "WestGravity", 4);
PyModule_AddIntConstant(m, "CenterGravity", 5);
PyModule_AddIntConstant(m, "EastGravity", 6);
PyModule_AddIntConstant(m, "SouthWestGravity", 7);
PyModule_AddIntConstant(m, "SouthGravity", 8);
PyModule_AddIntConstant(m, "SouthEastGravity", 9);
PyModule_AddIntConstant(m, "StaticGravity", 10);
PyModule_AddIntConstant(m, "UndefinedColorspace", 0);
PyModule_AddIntConstant(m, "RGBColorspace", 1);
PyModule_AddIntConstant(m, "GRAYColorspace", 2);
PyModule_AddIntConstant(m, "TransparentColorspace", 3);
PyModule_AddIntConstant(m, "OHTAColorspace", 4);
PyModule_AddIntConstant(m, "LabColorspace", 5);
PyModule_AddIntConstant(m, "XYZColorspace", 6);
PyModule_AddIntConstant(m, "YCbCrColorspace", 7);
PyModule_AddIntConstant(m, "YCCColorspace", 8);
PyModule_AddIntConstant(m, "YIQColorspace", 9);
PyModule_AddIntConstant(m, "YPbPrColorspace", 10);
PyModule_AddIntConstant(m, "YUVColorspace", 11);
PyModule_AddIntConstant(m, "CMYKColorspace", 12);
PyModule_AddIntConstant(m, "sRGBColorspace", 13);
PyModule_AddIntConstant(m, "HSBColorspace", 14);
PyModule_AddIntConstant(m, "HSLColorspace", 15);
PyModule_AddIntConstant(m, "HWBColorspace", 16);
PyModule_AddIntConstant(m, "Rec601LumaColorspace", 17);
PyModule_AddIntConstant(m, "Rec601YCbCrColorspace", 18);
PyModule_AddIntConstant(m, "Rec709LumaColorspace", 19);
PyModule_AddIntConstant(m, "Rec709YCbCrColorspace", 20);
PyModule_AddIntConstant(m, "LogColorspace", 21);
PyModule_AddIntConstant(m, "CMYColorspace", 22);
}

View File

@ -1,251 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from ctypes import byref, c_double
import calibre.utils.PythonMagickWand as p
from calibre.ptempfile import TemporaryFile
from calibre.constants import filesystem_encoding, __appname__, __version__
# Font metrics {{{
class Rect(object):
def __init__(self, left, top, right, bottom):
self.left, self.top, self.right, self.bottom = left, top, right, bottom
def __str__(self):
return '(%s, %s) -- (%s, %s)'%(self.left, self.top, self.right,
self.bottom)
class FontMetrics(object):
def __init__(self, ret):
self._attrs = []
for i, x in enumerate(('char_width', 'char_height', 'ascender',
'descender', 'text_width', 'text_height',
'max_horizontal_advance')):
setattr(self, x, ret[i])
self._attrs.append(x)
self.bounding_box = Rect(ret[7], ret[8], ret[9], ret[10])
self.x, self.y = ret[11], ret[12]
self._attrs.extend(['bounding_box', 'x', 'y'])
self._attrs = tuple(self._attrs)
def __str__(self):
return '''FontMetrics:
char_width: %s
char_height: %s
ascender: %s
descender: %s
text_width: %s
text_height: %s
max_horizontal_advance: %s
bounding_box: %s
x: %s
y: %s
'''%tuple([getattr(self, x) for x in self._attrs])
def get_font_metrics(image, d_wand, text):
if isinstance(text, unicode):
text = text.encode('utf-8')
ret = p.MagickQueryFontMetrics(image, d_wand, text)
return FontMetrics(ret)
# }}}
class TextLine(object):
def __init__(self, text, font_size, bottom_margin=30, font_path=None):
self.text, self.font_size, = text, font_size
self.bottom_margin = bottom_margin
self.font_path = font_path
def __repr__(self):
return u'TextLine:%r:%f'%(self.text, self.font_size)
def alloc_wand(name):
ans = getattr(p, name)()
if ans < 0:
raise RuntimeError('Cannot create wand')
return ans
def create_text_wand(font_size, font_path=None):
if font_path is None:
font_path = P('fonts/liberation/LiberationSerif-Bold.ttf')
if isinstance(font_path, unicode):
font_path = font_path.encode(filesystem_encoding)
ans = alloc_wand('NewDrawingWand')
if not p.DrawSetFont(ans, font_path):
raise ValueError('Failed to set font to: '+font_path)
p.DrawSetFontSize(ans, font_size)
p.DrawSetGravity(ans, p.CenterGravity)
p.DrawSetTextAntialias(ans, p.MagickTrue)
return ans
def _get_line(img, dw, tokens, line_width):
line, rest = tokens, []
while True:
m = get_font_metrics(img, dw, ' '.join(line))
width, height = m.text_width, m.text_height
if width < line_width:
return line, rest
rest = line[-1:] + rest
line = line[:-1]
def annotate_img(img, dw, left, top, rotate, text,
translate_from_top_left=True):
if isinstance(text, unicode):
text = text.encode('utf-8')
if translate_from_top_left:
m = get_font_metrics(img, dw, text)
img_width = p.MagickGetImageWidth(img)
img_height = p.MagickGetImageHeight(img)
left = left - img_width/2. + m.text_width/2.
top = top - img_height/2. + m.text_height/2.
p.MagickAnnotateImage(img, dw, left, top, rotate, text)
def draw_centered_line(img, dw, line, top):
m = get_font_metrics(img, dw, line)
width, height = m.text_width, m.text_height
img_width = p.MagickGetImageWidth(img)
left = max(int((img_width - width)/2.), 0)
annotate_img(img, dw, left, top, 0, line)
return top + height
def draw_centered_text(img, dw, text, top, margin=10):
img_width = p.MagickGetImageWidth(img)
tokens = text.split(' ')
while tokens:
line, tokens = _get_line(img, dw, tokens, img_width-2*margin)
if not line:
# Could not fit the first token on the line
line = tokens[:1]
tokens = tokens[1:]
bottom = draw_centered_line(img, dw, ' '.join(line), top)
top = bottom
return top
def create_canvas(width, height, bgcolor):
canvas = alloc_wand('NewMagickWand')
p_wand = alloc_wand('NewPixelWand')
p.PixelSetColor(p_wand, bgcolor)
p.MagickNewImage(canvas, width, height, p_wand)
p.DestroyPixelWand(p_wand)
return canvas
def compose_image(canvas, image, left, top):
p.MagickCompositeImage(canvas, image, p.OverCompositeOp, int(left),
int(top))
def load_image(path):
if isinstance(path, unicode):
path = path.encode(filesystem_encoding)
img = alloc_wand('NewMagickWand')
if not p.MagickReadImage(img, path):
severity = p.ExceptionType(0)
msg = p.MagickGetException(img, byref(severity))
raise IOError('Failed to read image from: %s: %s'
%(path, msg))
return img
def create_text_arc(text, font_size, font=None, bgcolor='white'):
if isinstance(text, unicode):
text = text.encode('utf-8')
canvas = create_canvas(300, 300, bgcolor)
tw = create_text_wand(font_size, font_path=font)
m = get_font_metrics(canvas, tw, text)
p.DestroyMagickWand(canvas)
canvas = create_canvas(int(m.text_width)+20, int(m.text_height*3.5), bgcolor)
p.MagickAnnotateImage(canvas, tw, 0, 0, 0, text)
angle = c_double(120.)
p.MagickDistortImage(canvas, 9, 1, byref(angle),
p.MagickTrue)
p.MagickTrimImage(canvas, 0)
return canvas
def add_borders_to_image(path_to_image, left=0, top=0, right=0, bottom=0,
border_color='white'):
with p.ImageMagick():
img = load_image(path_to_image)
lwidth = p.MagickGetImageWidth(img)
lheight = p.MagickGetImageHeight(img)
canvas = create_canvas(lwidth+left+right, lheight+top+bottom,
border_color)
compose_image(canvas, img, left, top)
p.DestroyMagickWand(img)
p.MagickWriteImage(canvas,path_to_image)
p.DestroyMagickWand(canvas)
def create_cover_page(top_lines, logo_path, width=590, height=750,
bgcolor='white', output_format='jpg'):
ans = None
with p.ImageMagick():
canvas = create_canvas(width, height, bgcolor)
bottom = 10
for line in top_lines:
twand = create_text_wand(line.font_size, font_path=line.font_path)
bottom = draw_centered_text(canvas, twand, line.text, bottom)
bottom += line.bottom_margin
p.DestroyDrawingWand(twand)
bottom -= top_lines[-1].bottom_margin
vanity = create_text_arc(__appname__ + ' ' + __version__, 24,
font=P('fonts/liberation/LiberationMono-Regular.ttf'))
lwidth = p.MagickGetImageWidth(vanity)
lheight = p.MagickGetImageHeight(vanity)
left = int(max(0, (width - lwidth)/2.))
top = height - lheight - 10
compose_image(canvas, vanity, left, top)
logo = load_image(logo_path)
lwidth = p.MagickGetImageWidth(logo)
lheight = p.MagickGetImageHeight(logo)
left = int(max(0, (width - lwidth)/2.))
top = max(int((height - lheight)/2.), bottom+20)
compose_image(canvas, logo, left, top)
p.DestroyMagickWand(logo)
with TemporaryFile('.'+output_format) as f:
p.MagickWriteImage(canvas, f)
with open(f, 'rb') as f:
ans = f.read()
p.DestroyMagickWand(canvas)
return ans
def save_cover_data_to(data, path, bgcolor='white'):
'''
Saves image in data to path, in the format specified by the path
extension. Composes the image onto a blank canvas so as to
properly convert transparent images.
'''
with open(path, 'wb') as f:
f.write(data)
with p.ImageMagick():
img = load_image(path)
canvas = create_canvas(p.MagickGetImageWidth(img),
p.MagickGetImageHeight(img), bgcolor)
compose_image(canvas, img, 0, 0)
p.MagickWriteImage(canvas, path)
p.DestroyMagickWand(img)
p.DestroyMagickWand(canvas)
def test():
import subprocess
with TemporaryFile('.png') as f:
data = create_cover_page(
[TextLine('A very long title indeed, don\'t you agree?', 42),
TextLine('Mad Max & Mixy poo', 32)], I('library.png'))
with open(f, 'wb') as g:
g.write(data)
subprocess.check_call(['gwenview', f])
if __name__ == '__main__':
test()

View File

@ -421,7 +421,7 @@ initpodofo(void)
return; return;
m = Py_InitModule3("podofo", podofo_methods, m = Py_InitModule3("podofo", podofo_methods,
"Wrapper for the PoDoFo pDF library"); "Wrapper for the PoDoFo PDF library");
Py_INCREF(&podofo_PDFDocType); Py_INCREF(&podofo_PDFDocType);
PyModule_AddObject(m, "PDFDoc", (PyObject *)&podofo_PDFDocType); PyModule_AddObject(m, "PDFDoc", (PyObject *)&podofo_PDFDocType);

View File

@ -24,7 +24,6 @@ from calibre.ebooks.metadata import MetaInformation
from calibre.web.feeds import feed_from_xml, templates, feeds_from_index, Feed from calibre.web.feeds import feed_from_xml, templates, feeds_from_index, Feed
from calibre.web.fetch.simple import option_parser as web2disk_option_parser from calibre.web.fetch.simple import option_parser as web2disk_option_parser
from calibre.web.fetch.simple import RecursiveFetcher from calibre.web.fetch.simple import RecursiveFetcher
from calibre.utils.magick_draw import add_borders_to_image
from calibre.utils.threadpool import WorkRequest, ThreadPool, NoResultsPending from calibre.utils.threadpool import WorkRequest, ThreadPool, NoResultsPending
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.date import now as nowf from calibre.utils.date import now as nowf
@ -964,6 +963,7 @@ class BasicNewsRecipe(Recipe):
with nested(open(cpath, 'wb'), closing(self.browser.open(cu))) as (cfile, r): with nested(open(cpath, 'wb'), closing(self.browser.open(cu))) as (cfile, r):
cfile.write(r.read()) cfile.write(r.read())
if self.cover_margins[0] or self.cover_margins[1]: if self.cover_margins[0] or self.cover_margins[1]:
from calibre.utils.magick.draw import add_borders_to_image
add_borders_to_image(cpath, add_borders_to_image(cpath,
left=self.cover_margins[0],right=self.cover_margins[0], left=self.cover_margins[0],right=self.cover_margins[0],
top=self.cover_margins[1],bottom=self.cover_margins[1], top=self.cover_margins[1],bottom=self.cover_margins[1],
@ -1018,7 +1018,7 @@ class BasicNewsRecipe(Recipe):
Create a generic cover for recipes that dont have a cover Create a generic cover for recipes that dont have a cover
''' '''
try: try:
from calibre.utils.magick_draw import create_cover_page, TextLine from calibre.utils.magick.draw import create_cover_page, TextLine
title = self.title if isinstance(self.title, unicode) else \ title = self.title if isinstance(self.title, unicode) else \
self.title.decode(preferred_encoding, 'replace') self.title.decode(preferred_encoding, 'replace')
date = strftime(self.timefmt) date = strftime(self.timefmt)
@ -1075,51 +1075,30 @@ class BasicNewsRecipe(Recipe):
img.save(open(out_path, 'wb'), 'JPEG') img.save(open(out_path, 'wb'), 'JPEG')
def prepare_masthead_image(self, path_to_image, out_path): def prepare_masthead_image(self, path_to_image, out_path):
import calibre.utils.PythonMagickWand as pw
from ctypes import byref
from calibre import fit_image from calibre import fit_image
from calibre.utils.magick import Image, create_canvas
with pw.ImageMagick(): img = Image()
img = pw.NewMagickWand() img.open(path_to_image)
img2 = pw.NewMagickWand() width, height = img.size
frame = pw.NewMagickWand() scaled, nwidth, nheight = fit_image(width, height, self.MI_WIDTH, self.MI_HEIGHT)
p = pw.NewPixelWand() img2 = create_canvas(width, height)
if img < 0 or img2 < 0 or p < 0 or frame < 0: frame = create_canvas(self.MI_WIDTH, self.MI_HEIGHT)
raise RuntimeError('Out of memory') img2.compose(img)
if not pw.MagickReadImage(img, path_to_image): if scaled:
severity = pw.ExceptionType(0) img2.size = (nwidth, nheight, 'LanczosFilter', 0.5)
msg = pw.MagickGetException(img, byref(severity)) left = int((self.MI_WIDTH - nwidth)/2.0)
raise IOError('Failed to read image from: %s: %s' top = int((self.MI_HEIGHT - nheight)/2.0)
%(path_to_image, msg)) frame.compose(img2, left, top)
pw.PixelSetColor(p, 'white') frame.save(out_path)
width, height = pw.MagickGetImageWidth(img),pw.MagickGetImageHeight(img)
scaled, nwidth, nheight = fit_image(width, height, self.MI_WIDTH, self.MI_HEIGHT)
if not pw.MagickNewImage(img2, width, height, p):
raise RuntimeError('Out of memory')
if not pw.MagickNewImage(frame, self.MI_WIDTH, self.MI_HEIGHT, p):
raise RuntimeError('Out of memory')
if not pw.MagickCompositeImage(img2, img, pw.OverCompositeOp, 0, 0):
raise RuntimeError('Out of memory')
if scaled:
if not pw.MagickResizeImage(img2, nwidth, nheight, pw.LanczosFilter,
0.5):
raise RuntimeError('Out of memory')
left = int((self.MI_WIDTH - nwidth)/2.0)
top = int((self.MI_HEIGHT - nheight)/2.0)
if not pw.MagickCompositeImage(frame, img2, pw.OverCompositeOp,
left, top):
raise RuntimeError('Out of memory')
if not pw.MagickWriteImage(frame, out_path):
raise RuntimeError('Failed to save image to %s'%out_path)
pw.DestroyPixelWand(p)
for x in (img, img2, frame):
pw.DestroyMagickWand(x)
def create_opf(self, feeds, dir=None): def create_opf(self, feeds, dir=None):
if dir is None: if dir is None:
dir = self.output_dir dir = self.output_dir
mi = MetaInformation(self.short_title() + strftime(self.timefmt), [__appname__]) title = self.short_title()
if self.output_profile.periodical_date_in_title:
title += strftime(self.timefmt)
mi = MetaInformation(title, [__appname__])
mi.publisher = __appname__ mi.publisher = __appname__
mi.author_sort = __appname__ mi.author_sort = __appname__
mi.publication_type = 'periodical:'+self.publication_type mi.publication_type = 'periodical:'+self.publication_type