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.
# 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
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.
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'
description = 'Global news'
__author__ = 'Kovid Goyal'
__author__ = 'Kovid Goyal and Sujata Raman'
use_embedded_content = False
language = 'en'
no_stylesheets = True
max_articles_per_feed = 15
html2lrf_options = ['--force-page-break-before-tag="chapter"']
preprocess_regexps = [ (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in
[
(r'<HEAD>.*?</HEAD>' , lambda match : '<HEAD></HEAD>'),
(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>'),
(r'<span class="entry-content">', lambda match : '<div class="entry-content">'),
]
]
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 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'),
@ -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 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'),
]
]

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):
title = u'Nature News'
language = 'en'
__author__ = 'Krittika Goyal'
__author__ = 'Krittika Goyal, Starson17'
oldest_article = 31 #days
remove_empty_feeds = True
max_articles_per_feed = 50
#encoding = 'latin1'
no_stylesheets = True
remove_tags_before = dict(name='h1', attrs={'class':'heading entry-title'})
remove_tags_after = dict(name='h2', attrs={'id':'comments'})
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='ul', attrs={'class':'toolsmenu xoxo'}),
]
dict(attrs={'alt':'Advertisement'}),
dict(name='div', attrs={'class':'ad'}),
]
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')]
def get_article_url(self, article):
return article.get('id')

View File

@ -6,10 +6,9 @@ www.standardmedia.co.ke
import os
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.constants import preferred_encoding
from calibre.utils.magick import Image
class NationKeRecipe(BasicNewsRecipe):
@ -95,19 +94,9 @@ class NationKeRecipe(BasicNewsRecipe):
self.cover_img_path = None
def prepare_cover_image(self, path_to_image, out_path):
with pw.ImageMagick():
img = pw.NewMagickWand()
if img < 0:
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)
img = Image()
img.open(path_to_image)
img.save(out_path)
def default_cover(self, cover_file):
'''

View File

@ -1,4 +1,3 @@
__license__ = 'GPL v3'
__copyright__ = '2008-2010, AprilHare, Darko Miletic <darko.miletic at gmail.com>'
'''
@ -36,7 +35,7 @@ class NewScientist(BasicNewsRecipe):
remove_tags = [
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='meta' , attrs={'name' :'description' })
,dict(name='a' , attrs={'rel' :'tag' })

View File

@ -14,33 +14,39 @@ class Nspm(BasicNewsRecipe):
description = 'Casopis za politicku teoriju i drustvena istrazivanja'
publisher = 'NSPM'
category = 'news, politics, Serbia'
oldest_article = 2
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
INDEX = 'http://www.nspm.rs/?alphabet=l'
encoding = 'utf-8'
language = 'sr'
delay = 2
publication_type = 'magazine'
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 = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
, 'linearize_tables' : True
}
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
keep_only_tags = [dict(attrs={'id':'jsn-mainbody'})]
remove_tags = [
dict(name=['link','object','embed','script','meta'])
,dict(name='td', attrs={'class':'buttonheading'})
dict(name=['link','object','embed','script','meta','base','iframe'])
,dict(attrs={'class':'buttonheading'})
]
keep_only_tags = [
dict(attrs={'class':['contentpagetitle','author','createdate']})
,dict(name='p')
]
remove_tags_after = dict(attrs={'class':'article_separator'})
remove_attributes = ['width','height']
def get_browser(self):
@ -48,25 +54,18 @@ class Nspm(BasicNewsRecipe):
br.open(self.INDEX)
return br
feeds = [(u'Nova srpska politicka misao', u'http://www.nspm.rs/feed/rss.html')]
def print_version(self, url):
return url.replace('.html','/stampa.html')
feeds = [
(u'Rubrike' , u'http://www.nspm.rs/rubrike/feed/rss.html')
,(u'Debate' , u'http://www.nspm.rs/debate/feed/rss.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):
for item in soup.body.findAll(style=True):
del item['style']
att = soup.find('a',attrs={'class':'contentpagetitle'})
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
return self.adeify_images(soup)

View File

@ -1,7 +1,5 @@
#!/usr/bin/env python
__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
'''
@ -11,29 +9,44 @@ from calibre.web.feeds.news import BasicNewsRecipe
class Nspm_int(BasicNewsRecipe):
title = 'NSPM in English'
__author__ = 'Darko Miletic'
description = 'Magazine dedicated to political theory and sociological research'
oldest_article = 20
description = 'Magazine dedicated to political theory and sociological research'
publisher = 'NSPM'
category = 'news, politics, Serbia'
oldest_article = 7
max_articles_per_feed = 100
language = 'en'
no_stylesheets = True
use_embedded_content = False
INDEX = 'http://www.nspm.rs/?alphabet=l'
cover_url = 'http://nspm.rs/templates/jsn_epic_pro/images/logol.jpg'
html2lrf_options = [
'--comment', description
, '--base-font-size', '10'
, '--category', 'news, politics, Serbia, english'
, '--publisher', 'IIC NSPM'
]
encoding = 'utf-8'
language = 'en'
delay = 2
publication_type = 'magazine'
masthead_url = 'http://www.nspm.rs/templates/jsn_epic_pro/images/logol.jpg'
extra_css = """
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):
br = BasicNewsRecipe.get_browser()
br.open(self.INDEX)
return br
conversion_options = {
'comment' : description
, 'tags' : category
, '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'})]
remove_tags = [dict(name='div', attrs={'id':'yvComment' })]
feeds = [(u'Articles', u'http://www.nspm.rs/nspm-in-english/feed/rss.html')]
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.'
__author__ = 'Kovid Goyal and Sujata Raman'
language = 'en'
remove_javascript = True
oldest_article = 30
max_articles_per_feed = 100
no_stylesheets = True
@ -31,11 +31,13 @@ class ScientificAmerican(BasicNewsRecipe):
remove_tags_after = dict(id=['article'])
remove_tags = [
dict(id=['sharetools', 'reddit']),
dict(name='script'),
#dict(name='script'),
{'class':['float_left', 'atools']},
{"class": re.compile(r'also-in-this')},
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='div', attrs={'class':['commentbox']}),
dict(name='h2', attrs={'class':['discuss_h2']}),
]
html2lrf_options = ['--base-font-size', '8']
@ -110,3 +112,10 @@ class ScientificAmerican(BasicNewsRecipe):
div.extract()
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
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.constants import preferred_encoding
from calibre.utils.magick import Image
class StandardMediaKeRecipe(BasicNewsRecipe):
@ -88,19 +87,9 @@ class StandardMediaKeRecipe(BasicNewsRecipe):
self.cover_img_path = None
def prepare_cover_image(self, path_to_image, out_path):
with pw.ImageMagick():
img = pw.NewMagickWand()
if img < 0:
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)
img = Image()
img.open(path_to_image)
img.save(out_path)
def default_cover(self, cover_file):
'''

View File

@ -30,10 +30,12 @@ class Starbulletin(BasicNewsRecipe):
}
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 = [
dict(name=['object','link'])
dict(name=['object','link','script','span'])
,dict(attrs={'class':'insideStoryImage'})
,dict(attrs={'name':'fb_share'})
,dict(name='div',attrs={'class':'storytext'})
]
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,
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',
reflow_sources,

View File

@ -1,17 +1,85 @@
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
-------
Copy
jconfig.vc to jconfig.h, makejsln.vc9 to jpeg.sln,
makeasln.vc9 to apps.sln, makejvcp.vc9 to jpeg.vcproj,
makecvcp.vc9 to cjpeg.vcproj, makedvcp.vc9 to djpeg.vcproj,
maketvcp.vc9 to jpegtran.vcproj, makervcp.vc9 to rdjpgcom.vcproj, and
makewvcp.vc9 to wrjpgcom.vcproj. (Note that the renaming is critical!)
Copy::
jconfig.vc to jconfig.h, makejsln.vc9 to jpeg.sln,
makeasln.vc9 to apps.sln, makejvcp.vc9 to jpeg.vcproj,
makecvcp.vc9 to cjpeg.vcproj, makedvcp.vc9 to djpeg.vcproj,
maketvcp.vc9 to jpegtran.vcproj, makervcp.vc9 to rdjpgcom.vcproj, and
makewvcp.vc9 to wrjpgcom.vcproj. (Note that the renaming is critical!)
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 -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
===================================================================
@ -214,7 +282,7 @@ Edit VisualMagick/configure/configure.cpp to set
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
@ -222,3 +290,19 @@ Undefine ProvideDllMain and MAGICKCORE_X11_DELEGATE
Now open VisualMagick/VisualDynamicMT.sln set to Release
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'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
__version__ = '0.7.12'
__version__ = '0.7.13'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re
@ -60,6 +60,7 @@ if plugins is None:
'pictureflow',
'lzx',
'msdes',
'magick',
'podofo',
'cPalmdoc',
'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.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS
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.kobo.driver import KOBO
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \
LibraryThing
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.ebooks.epub.fix.unmanifested import Unmanifested
from calibre.ebooks.epub.fix.epubcheck import Epubcheck
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 += [
ComicInput,
EPUBInput,
@ -564,7 +567,6 @@ plugins += [
MENTOR,
SWEEX,
PDNOVEL,
PROMEDIA,
ITUNES,
]
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 '
'computer or on a range of devices.')
# The image size for comics
#: The image size for comics
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
# If True output should be optimized for a touchscreen interface
#: If True output should be optimized for a touchscreen interface
touchscreen = False
touchscreen_news_css = ''
# A list of extra (beyond CSS 2.1) modules supported by the device
# Format is a cssutils profile dictionary (see iPad for example)
#: A list of extra (beyond CSS 2.1) modules supported by the device
#: Format is a cssutils profile dictionary (see iPad for example)
extra_css_modules = []
#: If True, the date is appended to the title of downloaded news
periodical_date_in_title = True
@classmethod
def tags_to_string(cls, tags):
@ -550,6 +552,7 @@ class KindleOutput(OutputProfile):
fbase = 16
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
supports_mobi_indexing = True
periodical_date_in_title = False
@classmethod
def tags_to_string(cls, tags):
@ -567,6 +570,7 @@ class KindleDXOutput(OutputProfile):
dpi = 150.0
comic_screen_size = (741, 1022)
supports_mobi_indexing = True
periodical_date_in_title = False
@classmethod
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.devices.interface import DevicePlugin
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.covers import CoverDownload
from calibre.ebooks.metadata.fetch import MetadataSource
from calibre.utils.config import make_config_dir, Config, ConfigProxy, \
plugin_dir, OptionParser, prefs
@ -234,6 +235,15 @@ def migrate_isbndb_key():
if key:
prefs.set('isbndb_com_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 {{{

View File

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

View File

@ -52,6 +52,11 @@ class DevicePlugin(Plugin):
#: long time
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
def get_gui_name(cls):
if hasattr(cls, 'gui_name'):

View File

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

View File

@ -46,12 +46,13 @@ class AVANT(USBMS):
BCD = [0x0319]
VENDOR_NAME = 'E-BOOK'
WINDOWS_MAIN_MEM = 'READER'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'READER'
EBOOK_DIR_MAIN = ''
SUPPORTS_SUB_DIRS = True
class SWEEX(USBMS):
# Identical to the Promedia
name = 'Sweex Device Interface'
gui_name = 'Sweex'
description = _('Communicate with the Sweex MM300')
@ -89,6 +90,8 @@ class PDNOVEL(USBMS):
EBOOK_DIR_MAIN = 'eBooks'
SUPPORTS_SUB_DIRS = False
DELETE_EXTS = ['.jpg', '.jpeg', '.png']
def upload_cover(self, path, filename, metadata):
coverdata = getattr(metadata, 'thumbnail', None)
@ -96,20 +99,4 @@ class PDNOVEL(USBMS):
with open('%s.jpg' % os.path.join(path, filename), 'wb') as coverfile:
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):
order = 0
match = re.search(r'REV_.*?&(\d+)', pnp_id)
match = re.search(r'REV_.*?&(\d+)#', pnp_id)
if match is not None:
order = int(match.group(1))
return order

View File

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

View File

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

View File

@ -5,11 +5,253 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__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 import browser, prints
from calibre.ebooks.BeautifulSoup import BeautifulSoup
from calibre.constants import preferred_encoding, DEBUG
class CoverDownload(Plugin):
'''
These plugins are used to download covers for books.
'''
supported_platforms = ['windows', 'osx', 'linux']
author = 'Kovid Goyal'
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'''
import os, re, posixpath
import os, re, posixpath, shutil
from cStringIO import StringIO
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.metadata import MetaInformation
from calibre.ebooks.metadata.opf2 import OPF
from calibre.ptempfile import TemporaryDirectory
from calibre.ptempfile import TemporaryDirectory, PersistentTemporaryFile
from calibre import CurrentDir
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 \
os.path.splitext(cpath)[1].lower() in ('.png', '.jpg', '.jpeg')
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.close()
save_cover_data_to(new_cdata, new_cover.name)
resize_to = None
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')
except:
import traceback

View File

@ -10,7 +10,7 @@ from calibre import prints
from calibre.utils.config import OptionParser
from calibre.utils.logging import default_log
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
@ -289,11 +289,10 @@ def filter_metadata_results(item):
def do_cover_check(item):
item.has_cover = False
if item.isbn:
try:
item.has_cover = check_for_cover(item.isbn)
except:
pass # Cover not found
try:
item.has_cover = check_for_cover(item)
except:
pass # Cover not found
def check_for_covers(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']
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),
32)]
img_data = create_cover_page(lines, I('library.png'))

View File

@ -50,7 +50,7 @@ class RTFInput(InputFormatPlugin):
parser = ParseRtf(
in_file = stream,
out_file = ofile,
deb_dir = 'I:\\Calibre\\rtfdebug',
deb_dir = 'I:\\Calibre\\rtfdebug',
# Convert symbol fonts to unicode equivalents. Default
# is 1
convert_symbol = 1,
@ -121,27 +121,23 @@ class RTFInput(InputFormatPlugin):
return self.convert_images(imap)
def convert_images(self, imap):
from calibre.utils.PythonMagickWand import ImageMagick
with ImageMagick():
for count, val in imap.items():
try:
imap[count] = self.convert_image(val)
except:
self.log.exception('Failed to convert', val)
for count, val in imap.items():
try:
imap[count] = self.convert_image(val)
except:
self.log.exception('Failed to convert', val)
return imap
def convert_image(self, name):
import calibre.utils.PythonMagickWand as p
img = p.NewMagickWand()
if img < 0:
raise RuntimeError('Cannot create wand.')
if not p.MagickReadImage(img, name):
self.log.warn('Failed to read image:', name)
from calibre.utils.magick import Image
img = Image()
img.open(name)
name = name.replace('.wmf', '.jpg')
p.MagickWriteImage(img, name)
img.save(name)
return name
def write_inline_css(self, ic):
font_size_classes = ['span.fs%d { font-size: %spt }'%(i, x) for i, x in
enumerate(ic.font_sizes)]
@ -152,11 +148,17 @@ class RTFInput(InputFormatPlugin):
text-decoration: none; font-weight: normal;
font-style: normal; font-variant: normal
}
span.italics { font-style: italic }
span.bold { font-weight: bold }
span.small-caps { font-variant: small-caps }
span.underlined { text-decoration: underline }
span.strike-through { text-decoration: line-through }
''')
css += '\n'+'\n'.join(font_size_classes)
css += '\n' +'\n'.join(color_classes)
@ -194,11 +196,11 @@ class RTFInput(InputFormatPlugin):
except RtfInvalidCodeException, e:
raise ValueError(_('This RTF file has a feature calibre does not '
'support. Convert it to HTML first and then try it.\n%s')%e)
dataxml = open('dataxml.xml', 'w')
dataxml.write(xml)
dataxml.close
d = glob.glob(os.path.join('*_rtf_pict_dir', 'picts.rtf'))
if d:
imap = {}
@ -206,7 +208,7 @@ class RTFInput(InputFormatPlugin):
imap = self.extract_images(d[0])
except:
self.log.exception('Failed to extract images...')
self.log('Parsing XML...')
parser = etree.XMLParser(recover=True, no_network=True)
doc = etree.fromstring(xml, parser=parser)

View File

@ -6,7 +6,7 @@ Read content from txt file.
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.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>'
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
# all line breaks with \n.
txt = '\n'.join([line.strip() for line in txt.splitlines()])
@ -30,19 +32,23 @@ def convert_basic(txt, title='', epub_split_size_kb=0):
# Remove excessive line breaks.
txt = re.sub('\n{3,}', '\n\n', txt)
#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| \
\x0B|\x0E|\x0F|\x10|\x11|\x12|\x13|\x14|\x15|\x16|\x17|\x18')
txt = illegal_char.sub('', txt)
chars = list(range(8)) + [0x0B, 0x0E, 0x0F] + list(range(0x10, 0x19))
illegal_chars = re.compile(u'|'.join(map(unichr, chars)))
txt = illegal_chars.sub('', txt)
#Takes care if there is no point to split
if epub_split_size_kb > 0:
if isinstance(txt, unicode):
txt = txt.encode('utf-8')
length_byte = len(txt)
#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 ))
#if there are chunks with a superior size then go and break
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')])
if isbytestring(txt):
txt = txt.decode('utf-8')
lines = []
# Split into paragraphs based on having a blank line between text.

View File

@ -430,6 +430,20 @@ class AddAction(object): # {{{
d.exec_()
return
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:
d = error_dialog(self, _('Add to library'), _('No book files found'))
d.exec_()
@ -830,7 +844,7 @@ class EditMetadataAction(object): # {{{
dest_mi.series = src_mi.series
dest_mi.series_index = src_mi.series_index
db.set_metadata(dest_id, dest_mi, ignore_errors=False)
for key in db.field_metadata: #loop thru all defined fields
if db.field_metadata[key]['is_custom']:
colnum = db.field_metadata[key]['colnum']
@ -841,12 +855,12 @@ class EditMetadataAction(object): # {{{
dest_value = db.get_custom(dest_id, num=colnum, index_is_id=True)
src_value = db.get_custom(src_id, num=colnum, index_is_id=True)
if db.field_metadata[key]['datatype'] == 'comments':
if src_value and src_value != orig_dest_value:
if not dest_value:
db.set_custom(dest_id, src_value, num=colnum)
else:
dest_value = unicode(dest_value) + u'\n\n' + unicode(src_value)
db.set_custom(dest_id, dest_value, num=colnum)
if src_value and src_value != orig_dest_value:
if not dest_value:
db.set_custom(dest_id, src_value, num=colnum)
else:
dest_value = unicode(dest_value) + u'\n\n' + unicode(src_value)
db.set_custom(dest_id, dest_value, num=colnum)
if db.field_metadata[key]['datatype'] in \
('bool', 'int', 'float', 'rating', 'datetime') \
and not dest_value:
@ -861,7 +875,7 @@ class EditMetadataAction(object): # {{{
and not dest_value:
db.set_custom(dest_id, src_value, num=colnum)
if db.field_metadata[key]['datatype'] == 'text' \
and db.field_metadata[key]['is_multiple']:
and db.field_metadata[key]['is_multiple']:
if src_value:
if not dest_value:
dest_value = src_value
@ -913,6 +927,14 @@ class SaveToDiskAction(object): # {{{
_('Choose destination directory'))
if not path:
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:
from calibre.gui2.add import Saver

View File

@ -31,7 +31,14 @@
</widget>
</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>
<widget class="QCheckBox" name="opt_individual_saved_settings">
@ -64,7 +71,14 @@
</widget>
</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>
</layout>
</item>
@ -115,8 +129,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>810</width>
<height>489</height>
<width>805</width>
<height>484</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">

View File

@ -118,6 +118,7 @@ class DeviceManager(Thread): # {{{
self.jobs = Queue.Queue(0)
self.keep_going = True
self.job_manager = job_manager
self.reported_errors = set([])
self.current_job = None
self.scanner = DeviceScanner()
self.connected_device = None
@ -141,13 +142,16 @@ class DeviceManager(Thread): # {{{
for dev, detected_device in connected_devices:
if dev.OPEN_FEEDBACK_MESSAGE is not None:
self.open_feedback_slot(dev.OPEN_FEEDBACK_MESSAGE)
dev.reset(detected_device=detected_device,
report_progress=self.report_progress)
try:
dev.reset(detected_device=detected_device,
report_progress=self.report_progress)
dev.open()
except:
prints('Unable to open device', str(dev))
traceback.print_exc()
tb = traceback.format_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
self.connected_device = dev
self.connected_device_kind = device_kind
@ -192,11 +196,13 @@ class DeviceManager(Thread): # {{{
if possibly_connected_devices:
if not self.do_connect(possibly_connected_devices,
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)
if not self.do_connect(possibly_connected_devices,
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
# 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'
elif self.empty_library.isChecked():
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,
loc):
return

View File

@ -50,15 +50,35 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
# set up the signal after the table is filled
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.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:
self.table.setCurrentItem(select_item)
self.table.editItem(select_item)
else:
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):
self.result = []
for row in range(0,self.table.rowCount()):
@ -79,8 +99,4 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
else:
item = self.table.item(row, 1)
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)

View File

@ -34,17 +34,54 @@
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="sort_by_author">
<property name="text">
<string>Sort by author</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="sort_by_author_sort">
<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>
</layout>
</widget>

View File

@ -24,8 +24,9 @@ from calibre.gui2.widgets import ProgressIndicator
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.metadata import string_to_authors, \
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 import MetaInformation
from calibre.utils.config import prefs, tweaks
from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp
from calibre.customize.ui import run_plugins_on_import, get_isbndb_key
@ -48,12 +49,13 @@ class CoverFetcher(QThread):
def run(self):
try:
au = self.author if self.author else None
mi = MetaInformation(self.title, [au])
if not self.isbn:
from calibre.ebooks.metadata.fetch import search
if not self.title:
self.needs_isbn = True
return
au = self.author if self.author else None
key = get_isbndb_key()
if not key:
key = None
@ -66,8 +68,10 @@ class CoverFetcher(QThread):
return
self.isbn = results[0]
self.cover_data = cover_from_isbn(self.isbn, timeout=self.timeout,
username=self.username, password=self.password)[0]
mi.isbn = self.isbn
self.cover_data, self.errors = download_cover(mi,
timeout=self.timeout)
except Exception, e:
self.exception = e
self.traceback = traceback.format_exc()
@ -138,6 +142,21 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.cpixmap = pix
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):
files = choose_files(self, 'add formats dialog',
@ -421,6 +440,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.central_widget.tabBar().setVisible(False)
else:
self.create_custom_column_editors()
self.generate_cover_button.clicked.connect(self.generate_cover)
def create_custom_column_editors(self):
w = self.central_widget.widget(1)
@ -576,6 +596,13 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>')+unicode(err)).exec_()
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.loadFromData(self.cover_fetcher.cover_data)

View File

@ -653,6 +653,16 @@
</property>
</widget>
</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>
</item>
</layout>
@ -736,15 +746,17 @@
<tabstop>fetch_metadata_button</tabstop>
<tabstop>add_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_button</tabstop>
<tabstop>reset_cover</tabstop>
<tabstop>fetch_cover_button</tabstop>
<tabstop>button_set_cover</tabstop>
<tabstop>formats</tabstop>
<tabstop>button_set_metadata</tabstop>
<tabstop>button_box</tabstop>
<tabstop>generate_cover_button</tabstop>
<tabstop>scrollArea</tabstop>
<tabstop>central_widget</tabstop>
<tabstop>button_box</tabstop>
</tabstops>
<resources>
<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.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 import prints
from calibre.constants import DEBUG
class Worker(Thread):
@ -26,13 +28,15 @@ class Worker(Thread):
def run(self):
while True:
isbn = self.jobs.get()
if not isbn:
mi = self.jobs.get()
if not getattr(mi, 'isbn', False):
break
try:
cdata, _ = cover_from_isbn(isbn)
cdata, errors = download_cover(mi)
if cdata:
self.results.put((isbn, cdata))
self.results.put((mi.isbn, cdata))
elif DEBUG:
prints('Cover download failed:', errors)
except:
traceback.print_exc()
@ -98,7 +102,7 @@ class DownloadMetadata(Thread):
fmi = results[0]
self.fetched_metadata[id] = fmi
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']):
fmi.authors = mi.authors
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.dialogs.progress import ProgressDialog
# Devices {{{
class Device(object):
output_profile = 'default'
@ -166,9 +168,9 @@ class iPhone(Device):
class Android(Device):
name = 'Adroid phone + WordPlayer'
name = 'Adroid phone + WordPlayer/Aldiko'
output_format = 'EPUB'
manufacturer = 'Google/HTC'
manufacturer = 'Android'
id = 'android'
class HanlinV3(Device):
@ -209,6 +211,7 @@ class EZReaderPP(HanlinV5):
manufacturer = 'Astak'
id = 'ezreader_pp'
# }}}
def get_devices():
for x in globals().values():

View File

@ -37,8 +37,8 @@
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>56</height>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
@ -46,7 +46,7 @@
<item>
<widget class="QLabel" name="label_2">
<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 name="wordWrap">
<bool>true</bool>
@ -59,19 +59,6 @@
</property>
</widget>
</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>
<widget class="QLabel" name="label_3">
<property name="text">
@ -88,19 +75,6 @@
</property>
</widget>
</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>
</widget>
<resources/>

View File

@ -542,6 +542,8 @@ class ResultCache(SearchQueryParser):
if field is not None:
self.sort(field, ascending)
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):
try:

View File

@ -10,7 +10,7 @@ from copy import deepcopy
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.customize import CatalogPlugin
from calibre.customize.conversion import OptionRecommendation, DummyReporter
@ -1207,9 +1207,7 @@ class EPUB_MOBI(CatalogPlugin):
self.generateHTMLByDateRead()
self.generateHTMLByTags()
from calibre.utils.PythonMagickWand import ImageMagick
with ImageMagick():
self.generateThumbnails()
self.generateThumbnails()
self.generateOPF()
self.generateNCXHeader()
@ -4062,29 +4060,15 @@ class EPUB_MOBI(CatalogPlugin):
return ' '.join(translated)
def generateThumbnail(self, title, image_dir, thumb_file):
import calibre.utils.PythonMagickWand as pw
from calibre.utils.magick import Image
try:
img = pw.NewMagickWand()
if img < 0:
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 = Image()
img.open(title['cover'])
# img, width, height
pw.MagickThumbnailImage(thumb, self.thumbWidth, self.thumbHeight)
pw.MagickWriteImage(thumb, os.path.join(image_dir, thumb_file))
pw.DestroyMagickWand(thumb)
pw.DestroyMagickWand(img)
except IOError:
self.opts.log.error("generateThumbnail(): IOError with %s" % title['title'])
except RuntimeError:
self.opts.log.error("generateThumbnail(): RuntimeError with %s" % title['title'])
img.thumbnail(self.thumbWidth, self.thumbHeight)
img.save(os.path.join(image_dir, thumb_file))
except:
self.opts.log.error("generateThumbnail(): Error with %s" % title['title'])
def getMarkerTags(self):
''' 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.search_query_parser import saved_searches, set_saved_searches
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:
import calibre.utils.winshell as winshell
@ -317,6 +317,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
'title', 'timestamp', 'uuid', 'pubdate'):
setattr(self, prop, functools.partial(get_property,
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):
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.pubdate = self.pubdate(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)
if tags:
mi.tags = [i.strip() for i in tags.split(',')]

View File

@ -8,13 +8,13 @@ __docformat__ = 'restructuredtext en'
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, \
ascii_filename, sanitize_file_name
from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.ebooks.metadata.meta import set_metadata
from calibre.constants import preferred_encoding, filesystem_encoding
from calibre.ebooks.metadata import title_sort
from calibre import strftime
DEFAULT_TEMPLATE = '{author_sort}/{title}/{title} - {authors}'
@ -99,7 +99,8 @@ def preprocess_template(template):
def safe_format(x, format_args):
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
pass
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,
sanitize_func=ascii_filename, replace_whitespace=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)
if mi.title:
format_args['title'] = mi.title
format_args['title'] = tsfmt(mi.title)
if mi.authors:
format_args['authors'] = mi.format_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('/'):
format_args['tags'] = format_args['tags'][1:]
if mi.series:
format_args['series'] = mi.series
format_args['series'] = tsfmt(mi.series)
if mi.series_index is not None:
format_args['series_index'] = mi.format_series_index()
else:
template = re.sub(r'\{series_index[^}]*?\}', '', template)
if mi.rating is not None:
format_args['rating'] = mi.format_rating()
if mi.isbn:

View File

@ -5,18 +5,19 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import re
import re, os
import __builtin__
import cherrypy
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
from calibre.library.server.utils import strftime
from calibre.ebooks.metadata import fmt_sidx
from calibre.constants import __appname__
from calibre import human_readable
from calibre.utils.date import utcfromtimestamp
def CLASS(*args, **kwargs): # class is a reserved word in Python
kwargs['class'] = ' '.join(args)
@ -140,85 +141,7 @@ def build_index(books, num, search, sort, order, start, total, url_base):
TITLE(__appname__ + ' Library'),
LINK(rel='icon', href='http://calibre-ebook.com/favicon.ico',
type='image/x-icon'),
STYLE( # {{{
'''
.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') # }}}
LINK(rel='stylesheet', type='text/css', href='/mobile/style.css')
), # End head
body
) # End html
@ -231,6 +154,14 @@ class MobileServer(object):
def add_routes(self, connect):
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='',
_=None, order='descending'):

View File

@ -400,7 +400,9 @@ class OPDSServer(object):
owhich = hexlify('N'+which)
up_url = url_for('opdsnavcatalog', version, which=owhich)
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:
raise cherrypy.HTTPError(404, 'No items in group %r:%r'%(category,
which))
@ -465,7 +467,12 @@ class OPDSServer(object):
def __init__(self, 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()
for x in sorted(starts, cmp=lambda x,y:cmp(x.lower(), y.lower())):
category_groups[x] = len([y for y in items if

View File

@ -71,6 +71,11 @@ Metadata download plugins
:members:
:member-order: bysource
.. autoclass:: calibre.ebooks.metadata.covers.CoverDownload
:show-inheritance:
:members:
:member-order: bysource
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;
m = Py_InitModule3("podofo", podofo_methods,
"Wrapper for the PoDoFo pDF library");
"Wrapper for the PoDoFo PDF library");
Py_INCREF(&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.fetch.simple import option_parser as web2disk_option_parser
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.ptempfile import PersistentTemporaryFile
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):
cfile.write(r.read())
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,
left=self.cover_margins[0],right=self.cover_margins[0],
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
'''
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 \
self.title.decode(preferred_encoding, 'replace')
date = strftime(self.timefmt)
@ -1075,51 +1075,30 @@ class BasicNewsRecipe(Recipe):
img.save(open(out_path, 'wb'), 'JPEG')
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.utils.magick import Image, create_canvas
with pw.ImageMagick():
img = pw.NewMagickWand()
img2 = pw.NewMagickWand()
frame = pw.NewMagickWand()
p = pw.NewPixelWand()
if img < 0 or img2 < 0 or p < 0 or frame < 0:
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))
pw.PixelSetColor(p, 'white')
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)
img = Image()
img.open(path_to_image)
width, height = img.size
scaled, nwidth, nheight = fit_image(width, height, self.MI_WIDTH, self.MI_HEIGHT)
img2 = create_canvas(width, height)
frame = create_canvas(self.MI_WIDTH, self.MI_HEIGHT)
img2.compose(img)
if scaled:
img2.size = (nwidth, nheight, 'LanczosFilter', 0.5)
left = int((self.MI_WIDTH - nwidth)/2.0)
top = int((self.MI_HEIGHT - nheight)/2.0)
frame.compose(img2, left, top)
frame.save(out_path)
def create_opf(self, feeds, dir=None):
if dir is None:
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.author_sort = __appname__
mi.publication_type = 'periodical:'+self.publication_type