mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge 7.13
This commit is contained in:
commit
12668be260
@ -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
|
||||
|
||||
|
83
resources/content_server/mobile.css
Normal file
83
resources/content_server/mobile.css
Normal 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;
|
||||
}
|
||||
|
||||
|
@ -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'
|
||||
|
||||
|
BIN
resources/images/news/balkaninsight.png
Normal file
BIN
resources/images/news/balkaninsight.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 573 B |
BIN
resources/images/news/discover_magazine.png
Normal file
BIN
resources/images/news/discover_magazine.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1014 B |
BIN
resources/images/news/dr_dk.png
Normal file
BIN
resources/images/news/dr_dk.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 391 B |
@ -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'),
|
||||
]
|
||||
]
|
||||
|
||||
|
62
resources/recipes/balkaninsight.recipe
Normal file
62
resources/recipes/balkaninsight.recipe
Normal 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)
|
42
resources/recipes/dr_dk.recipe
Normal file
42
resources/recipes/dr_dk.recipe
Normal 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
|
74
resources/recipes/folhadesaopaulo.recipe
Normal file
74
resources/recipes/folhadesaopaulo.recipe
Normal 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'
|
@ -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')
|
||||
|
||||
|
@ -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):
|
||||
'''
|
||||
|
@ -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' })
|
||||
|
@ -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)
|
@ -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)
|
@ -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: ''),
|
||||
]
|
||||
|
49
resources/recipes/skeptic.recipe
Normal file
49
resources/recipes/skeptic.recipe
Normal 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;}
|
||||
'''
|
||||
|
50
resources/recipes/skeptical_enquirer.recipe
Normal file
50
resources/recipes/skeptical_enquirer.recipe
Normal 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;}
|
||||
'''
|
||||
|
46
resources/recipes/snopes.recipe
Normal file
46
resources/recipes/snopes.recipe
Normal 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;}
|
||||
'''
|
@ -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):
|
||||
'''
|
||||
|
@ -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 = [
|
||||
|
@ -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
|
@ -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,
|
||||
|
@ -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.
|
||||
|
@ -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',
|
||||
|
@ -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 \
|
||||
|
@ -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):
|
||||
|
@ -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 {{{
|
||||
|
@ -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],
|
||||
|
@ -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'):
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 = []
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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'))
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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>&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"/>
|
||||
|
@ -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
|
||||
|
@ -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():
|
||||
|
@ -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><h2>Demo videos</h2>Videos demonstrating the various features of calibre are available <a href="http://calibre-ebook.com/demo">online</a>.</string>
|
||||
<string><h2>Demo videos</h2>Videos demonstrating the various features of calibre are available <a href="http://calibre-ebook.com/demo">online</a>.</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/>
|
||||
|
@ -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:
|
||||
|
@ -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 '''
|
||||
|
@ -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(',')]
|
||||
|
@ -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:
|
||||
|
@ -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'):
|
||||
|
@ -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
|
||||
|
@ -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
200
src/calibre/utils/magick/__init__.py
Normal file
200
src/calibre/utils/magick/__init__.py
Normal 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
|
164
src/calibre/utils/magick/draw.py
Normal file
164
src/calibre/utils/magick/draw.py
Normal 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)
|
||||
|
71
src/calibre/utils/magick/generate.py
Normal file
71
src/calibre/utils/magick/generate.py
Normal 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()
|
1082
src/calibre/utils/magick/magick.c
Normal file
1082
src/calibre/utils/magick/magick.c
Normal file
File diff suppressed because it is too large
Load Diff
289
src/calibre/utils/magick/magick_constants.h
Normal file
289
src/calibre/utils/magick/magick_constants.h
Normal 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);
|
||||
}
|
@ -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()
|
@ -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);
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user