mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
7b09e65c5d
@ -69,9 +69,12 @@ categories_use_field_for_author_name = 'author'
|
||||
# avg_rating: the averate rating of all the books referencing this item
|
||||
# sort: the sort value. For authors, this is the author_sort for that author
|
||||
# category: the category (e.g., authors, series) that the item is in.
|
||||
categories_collapsed_name_template = '{first.sort:shorten(4,'',0)} - {last.sort:shorten(4,'',0)}'
|
||||
categories_collapsed_rating_template = '{first.avg_rating:4.2f:ifempty(0)} - {last.avg_rating:4.2f:ifempty(0)}'
|
||||
categories_collapsed_popularity_template = '{first.count:d} - {last.count:d}'
|
||||
# Note that the "r'" in front of the { is necessary if there are backslashes
|
||||
# (\ characters) in the template. It doesn't hurt anything to leave it there
|
||||
# even if there aren't any backslashes.
|
||||
categories_collapsed_name_template = r'{first.sort:shorten(4,'',0)} - {last.sort:shorten(4,'',0)}'
|
||||
categories_collapsed_rating_template = r'{first.avg_rating:4.2f:ifempty(0)} - {last.avg_rating:4.2f:ifempty(0)}'
|
||||
categories_collapsed_popularity_template = r'{first.count:d} - {last.count:d}'
|
||||
|
||||
|
||||
# Set whether boolean custom columns are two- or three-valued.
|
||||
|
BIN
resources/images/news/arabian_business.png
Normal file
BIN
resources/images/news/arabian_business.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
86
resources/recipes/arabian_business.recipe
Normal file
86
resources/recipes/arabian_business.recipe
Normal file
@ -0,0 +1,86 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
www.arabianbusiness.com
|
||||
'''
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class Arabian_Business(BasicNewsRecipe):
|
||||
title = 'Arabian Business'
|
||||
__author__ = 'Darko Miletic'
|
||||
description = 'Comprehensive Guide to Middle East Business & Gulf Industry News including,Banking & Finance,Construction,Energy,Media & Marketing,Real Estate,Transportation,Travel,Technology,Politics,Healthcare,Lifestyle,Jobs & UAE guide.Top Gulf & Dubai Business News.'
|
||||
publisher = 'Arabian Business Publishing Ltd.'
|
||||
category = 'ArabianBusiness.com,Arab Business News,Middle East Business News,Middle East Business,Arab Media News,Industry Events,Middle East Industry News,Arab Business Industry,Dubai Business News,Financial News,UAE Business News,Middle East Press Releases,Gulf News,Arab News,GCC Business News,Banking Finance,Media Marketing,Construction,Oil Gas,Retail,Transportation,Travel Hospitality,Photos,Videos,Life Style,Fashion,United Arab Emirates,UAE,Dubai,Sharjah,Abu Dhabi,Qatar,KSA,Saudi Arabia,Bahrain,Kuwait,Oman,Europe,South Asia,America,Asia,news'
|
||||
oldest_article = 2
|
||||
max_articles_per_feed = 200
|
||||
no_stylesheets = True
|
||||
encoding = 'utf8'
|
||||
use_embedded_content = False
|
||||
language = 'en'
|
||||
remove_empty_feeds = True
|
||||
publication_type = 'newsportal'
|
||||
masthead_url = 'http://www.arabianbusiness.com/skins/ab.main/gfx/arabianbusiness_logo_sm.gif'
|
||||
extra_css = """
|
||||
body{font-family: Georgia,serif }
|
||||
img{margin-bottom: 0.4em; margin-top: 0.4em; display:block}
|
||||
.byline,.dateline{font-size: small; display: inline; font-weight: bold}
|
||||
ul{list-style: none outside none;}
|
||||
"""
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
remove_tags_before=dict(attrs={'id':'article-title'})
|
||||
remove_tags = [
|
||||
dict(name=['meta','link','base','iframe','embed','object'])
|
||||
,dict(attrs={'class':'printfooter'})
|
||||
]
|
||||
remove_attributes=['lang']
|
||||
|
||||
|
||||
feeds = [
|
||||
(u'Africa' , u'http://www.arabianbusiness.com/world/Africa/?service=rss' )
|
||||
,(u'Americas' , u'http://www.arabianbusiness.com/world/americas/?service=rss' )
|
||||
,(u'Asia Pacific' , u'http://www.arabianbusiness.com/world/asia-pacific/?service=rss' )
|
||||
,(u'Europe' , u'http://www.arabianbusiness.com/world/europe/?service=rss' )
|
||||
,(u'Middle East' , u'http://www.arabianbusiness.com/world/middle-east/?service=rss' )
|
||||
,(u'South Asia' , u'http://www.arabianbusiness.com/world/south-asia/?service=rss' )
|
||||
,(u'Banking & Finance', u'http://www.arabianbusiness.com/industries/banking-finance/?service=rss' )
|
||||
,(u'Construction' , u'http://www.arabianbusiness.com/industries/construction/?service=rss' )
|
||||
,(u'Education' , u'http://www.arabianbusiness.com/industries/education/?service=rss' )
|
||||
,(u'Energy' , u'http://www.arabianbusiness.com/industries/energy/?service=rss' )
|
||||
,(u'Healthcare' , u'http://www.arabianbusiness.com/industries/healthcare/?service=rss' )
|
||||
,(u'Media' , u'http://www.arabianbusiness.com/industries/media/?service=rss' )
|
||||
,(u'Real Estate' , u'http://www.arabianbusiness.com/industries/real-estate/?service=rss' )
|
||||
,(u'Retail' , u'http://www.arabianbusiness.com/industries/retail/?service=rss' )
|
||||
,(u'Technology' , u'http://www.arabianbusiness.com/industries/technology/?service=rss' )
|
||||
,(u'Transport' , u'http://www.arabianbusiness.com/industries/transport/?service=rss' )
|
||||
,(u'Travel' , u'http://www.arabianbusiness.com/industries/travel-hospitality/?service=rss')
|
||||
,(u'Equities' , u'http://www.arabianbusiness.com/markets/equities/?service=rss' )
|
||||
,(u'Commodities' , u'http://www.arabianbusiness.com/markets/commodities/?service=rss' )
|
||||
,(u'Currencies' , u'http://www.arabianbusiness.com/markets/currencies/?service=rss' )
|
||||
,(u'Market Data' , u'http://www.arabianbusiness.com/markets/market-data/?service=rss' )
|
||||
,(u'Comment' , u'http://www.arabianbusiness.com/opinion/comment/?service=rss' )
|
||||
,(u'Think Tank' , u'http://www.arabianbusiness.com/opinion/think-tank/?service=rss' )
|
||||
,(u'Arts' , u'http://www.arabianbusiness.com/lifestyle/arts/?service=rss' )
|
||||
,(u'Cars' , u'http://www.arabianbusiness.com/lifestyle/cars/?service=rss' )
|
||||
,(u'Food' , u'http://www.arabianbusiness.com/lifestyle/food/?service=rss' )
|
||||
,(u'Sport' , u'http://www.arabianbusiness.com/lifestyle/sport/?service=rss' )
|
||||
]
|
||||
|
||||
def print_version(self, url):
|
||||
return url + '?service=printer&page='
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
for alink in soup.findAll('a'):
|
||||
if alink.string is not None:
|
||||
tstr = alink.string
|
||||
alink.replaceWith(tstr)
|
||||
return soup
|
70
resources/recipes/deia.recipe
Normal file
70
resources/recipes/deia.recipe
Normal file
@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python
|
||||
__license__ = 'GPL v3'
|
||||
__author__ = 'Gerardo Diez'
|
||||
__copyright__ = 'Gerardo Diez<gerardo.diez.garcia@gmail.com>'
|
||||
description = 'Main daily newspaper from Spain - v1.00 (05, Enero 2011)'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
'''
|
||||
deia.com
|
||||
'''
|
||||
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
|
||||
class Deia(BasicNewsRecipe):
|
||||
title ='Deia'
|
||||
__author__ ='Gerardo Diez'
|
||||
publisher ='Editorial Iparraguirre, S.A'
|
||||
category ='news, politics, finances, world, spain, euskadi'
|
||||
publication_type ='newspaper'
|
||||
oldest_article =1
|
||||
max_articles_per_feed =100
|
||||
simultaneous_downloads =10
|
||||
cover_url ='http://2.bp.blogspot.com/_RjrWzC6tI14/TM6jrPLaBZI/AAAAAAAAFaI/ayffwxidFEY/s1600/2009-10-13-logo-deia.jpg'
|
||||
timefmt ='[%a, %d %b, %Y]'
|
||||
encoding ='utf8'
|
||||
language ='es_ES'
|
||||
remove_javascript =True
|
||||
remove_tags_after =dict(id='Texto')
|
||||
remove_tags_before =dict(id='Texto')
|
||||
remove_tags =[dict(name='div', attrs={'class':['Herramientas ', 'Multimedia']})]
|
||||
no_stylesheets =True
|
||||
extra_css ='h1 {margin-bottom: .15em;font-size: 2.7em; font-family: Georgia, "Times New Roman", Times, serif;} .Antetitulo {margin: 1em 0;text-transform: uppercase;color: #999;} .PieFoto {margin: .1em 0;padding: .5em .5em .5em .5em;background: #F0F0F0;} .PieFoto p {margin-bottom: 0;font-family: Georgia,"Times New Roman",Times,serif;font-weight: bold; font-style: italic; color: #666;}'
|
||||
keep_only_tags =[dict(name='div', attrs={'class':['Texto ', 'NoticiaFicha ']})]
|
||||
feeds = [
|
||||
(u'Bizkaia' ,u'http://www.deia.com/index.php/services/rss?seccion=bizkaia'),
|
||||
(u'Bilbao' ,u'http://www.deia.com/index.php/services/rss?seccion=bilbao'),
|
||||
(u'Hemendik eta Handik' ,u'http://www.deia.com/index.php/services/rss?seccion=hemendik-eta-handik'),
|
||||
(u'Margen Derecha' ,u'http://www.deia.com/index.php/services/rss?seccion=margen-derecha'),
|
||||
(u'Encartaciones y Margen Izquierda' ,u'http://www.deia.com/index.php/services/rss?seccion=margen-izquierda-encartaciones'),
|
||||
(u'Costa' ,u'http://www.deia.com/index.php/services/rss?seccion=costa'),
|
||||
(u'Duranguesado' ,u'http://www.deia.com/index.php/services/rss?seccion=duranguesado'),
|
||||
(u'Llodio-Nervión' ,u'http://www.deia.com/index.php/services/rss?seccion=llodio-nervion'),
|
||||
(u'Arratia-Nervión' ,u'http://www.deia.com/index.php/services/rss?seccion=arratia-nervion'),
|
||||
(u'Uribe-Txorierri' ,u'http://www.deia.com/index.php/services/rss?seccion=uribe-txorierri'),
|
||||
(u'Ecos de sociedad' ,u'http://www.deia.com/index.php/services/rss?seccion=ecos-de-sociedad'),
|
||||
(u'Sucesos' ,u'http://www.deia.com/index.php/services/rss?seccion=sucesos'),
|
||||
(u'Política' ,u'http://www.deia.com/index.php/services/rss?seccion=politica'),
|
||||
(u'Euskadi' ,u'http://www.deia.com/index.php/services/rss?seccion=politica/euskadi'),
|
||||
(u'España' ,u'http://www.deia.com/index.php/services/rss?seccion=politica/espana'),
|
||||
(u'Sociedad',u'http://www.deia.com/index.php/services/rss?seccion=sociedad'),
|
||||
(u'Euskadi' ,u'http://www.deia.com/index.php/services/rss?seccion=socidad/euskadi'),
|
||||
(u'Sociedad.España' ,u'http://www.deia.com/index.php/services/rss?seccion=sociedad/espana'),
|
||||
(u'Ocio y Cultura' ,u'http://www.deia.com/index.php/services/rss?seccion=ocio-y-cultura'),
|
||||
#(u'Cultura' ,u'http://www.deia.com/index.php/services/rss?seccion=cultura'),
|
||||
#(u'Ocio' ,u'http://www.deia.com/index.php/services/rss?seccion=ocio'),
|
||||
(u'On' ,u'http://www.deia.com/index.php/services/rss?seccion=on'),
|
||||
(u'Agenda' ,u'http://www.deia.com/index.php/services/rss?seccion=agenda'),
|
||||
(u'Comunicación' ,u'http://www.deia.com/index.php/services/rss?seccion=comunicacion'),
|
||||
(u'Viajes' ,u'http://www.deia.com/index.php/services/rss?seccion=viajes'),
|
||||
(u'¡Mundo!' ,u'http://www.deia.com/index.php/services/rss?seccion=que-mundo'),
|
||||
(u'Humor' ,u'http://www.deia.com/index.php/services/rss?seccion=humor'),
|
||||
(u'Opinión' ,u'http://www.deia.com/index.php/services/rss?seccion=opinion'),
|
||||
(u'Editorial' ,u'http://www.deia.com/index.php/services/rss?seccion=editorial'),
|
||||
(u'Tribuna abierta' ,u'http://www.deia.com/index.php/services/rss?seccion=tribuna-abierta'),
|
||||
(u'Colaboración' ,u'http://www.deia.com/index.php/services/rss?seccion=colaboracion'),
|
||||
(u'Columnistas' ,u'http://www.deia.com/index.php/services/rss?seccion=columnistas'),
|
||||
(u'Deportes' ,u'http://www.deia.com/index.php/services/rss?seccion=deportes'),
|
||||
(u'Athletic' ,u'http://www.deia.com/index.php/services/rss?seccion=athletic'),
|
||||
(u'Economía' ,'http://www.deia.com/index.php/services/rss?seccion=economia'),
|
||||
(u'Mundo' ,u'http://www.deia.com/index.php/services/rss?seccion=mundo')]
|
||||
|
23
resources/recipes/ibm_smarter_planet.recipe
Normal file
23
resources/recipes/ibm_smarter_planet.recipe
Normal file
@ -0,0 +1,23 @@
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class AdvancedUserRecipe1293122276(BasicNewsRecipe):
|
||||
title = u'Smarter Planet | Tumblr for eReaders'
|
||||
__author__ = 'Jack Mason'
|
||||
author = 'IBM Global Business Services'
|
||||
publisher = 'IBM'
|
||||
category = 'news, technology, IT, internet of things, analytics'
|
||||
oldest_article = 7
|
||||
max_articles_per_feed = 30
|
||||
no_stylesheets = True
|
||||
use_embedded_content = False
|
||||
masthead_url = 'http://30.media.tumblr.com/tumblr_l70dow9UmU1qzs4rbo1_r3_250.jpg'
|
||||
remove_tags_before = dict(id='item')
|
||||
remove_tags_after = dict(id='item')
|
||||
remove_tags = [dict(attrs={'class':['sidebar', 'about', 'footer', 'description,' 'disqus', 'nav', 'notes', 'disqus_thread']}),
|
||||
dict(id=['sidebar', 'footer', 'disqus', 'nav', 'notes', 'likes_container', 'description', 'disqus_thread', 'about']),
|
||||
dict(name=['script', 'noscript', 'style'])]
|
||||
|
||||
|
||||
|
||||
feeds = [(u'Smarter Planet Tumblr', u'http://smarterplanet.tumblr.com/mobile/rss')]
|
||||
|
115
resources/recipes/sunday_times.recipe
Normal file
115
resources/recipes/sunday_times.recipe
Normal file
@ -0,0 +1,115 @@
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
www.thesundaytimes.co.uk
|
||||
'''
|
||||
import urllib
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class TimesOnline(BasicNewsRecipe):
|
||||
title = 'The Sunday Times UK'
|
||||
__author__ = 'Darko Miletic'
|
||||
description = 'news from United Kingdom and World'
|
||||
language = 'en_GB'
|
||||
publisher = 'Times Newspapers Ltd'
|
||||
category = 'news, politics, UK'
|
||||
oldest_article = 3
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
use_embedded_content = False
|
||||
encoding = 'utf-8'
|
||||
delay = 1
|
||||
needs_subscription = True
|
||||
publication_type = 'newspaper'
|
||||
masthead_url = 'http://www.thesundaytimes.co.uk/sto/public/images/logos/logo-home.gif'
|
||||
INDEX = 'http://www.thesundaytimes.co.uk'
|
||||
PREFIX = u'http://www.thesundaytimes.co.uk/sto/'
|
||||
extra_css = """
|
||||
.author-name,.authorName{font-style: italic}
|
||||
.published-date,.multi-position-photo-text{font-family: Arial,Helvetica,sans-serif;
|
||||
font-size: small; color: gray;
|
||||
display:block; margin-bottom: 0.5em}
|
||||
body{font-family: Georgia,"Times New Roman",Times,serif}
|
||||
"""
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
|
||||
def get_browser(self):
|
||||
br = BasicNewsRecipe.get_browser()
|
||||
br.open('http://www.timesplus.co.uk/tto/news/?login=false&url=http://www.thesundaytimes.co.uk/sto/')
|
||||
if self.username is not None and self.password is not None:
|
||||
data = urllib.urlencode({ 'userName':self.username
|
||||
,'password':self.password
|
||||
,'keepMeLoggedIn':'false'
|
||||
})
|
||||
br.open('https://www.timesplus.co.uk/iam/app/authenticate',data)
|
||||
return br
|
||||
|
||||
remove_tags = [
|
||||
dict(name=['object','link','iframe','base','meta'])
|
||||
,dict(attrs={'class':'tools comments-parent' })
|
||||
]
|
||||
remove_attributes=['lang']
|
||||
keep_only_tags = [
|
||||
dict(attrs={'class':'standard-content'})
|
||||
,dict(attrs={'class':'f-author'})
|
||||
,dict(attrs={'id':'bodycopy'})
|
||||
]
|
||||
remove_tags_after=dict(attrs={'class':'tools_border'})
|
||||
|
||||
feeds = [
|
||||
(u'UK News' , PREFIX + u'news/uk_news/' )
|
||||
,(u'World' , PREFIX + u'news/world_news/' )
|
||||
,(u'Politics' , PREFIX + u'news/Politics/' )
|
||||
,(u'Focus' , PREFIX + u'news/focus/' )
|
||||
,(u'Insight' , PREFIX + u'news/insight/' )
|
||||
,(u'Ireland' , PREFIX + u'news/ireland/' )
|
||||
,(u'Columns' , PREFIX + u'comment/columns/' )
|
||||
,(u'Arts' , PREFIX + u'culture/arts/' )
|
||||
,(u'Books' , PREFIX + u'culture/books/' )
|
||||
,(u'Film and TV' , PREFIX + u'culture/film_and_tv/' )
|
||||
,(u'Sport' , PREFIX + u'sport/' )
|
||||
,(u'Business' , PREFIX + u'business' )
|
||||
,(u'Money' , PREFIX + u'business/money/' )
|
||||
,(u'Style' , PREFIX + u'style/' )
|
||||
,(u'Travel' , PREFIX + u'travel/' )
|
||||
,(u'Clarkson' , PREFIX + u'ingear/clarkson/' )
|
||||
,(u'Cars' , PREFIX + u'ingear/cars/' )
|
||||
,(u'Bikes' , PREFIX + u'ingear/2_Wheels/' )
|
||||
,(u'Tech' , PREFIX + u'ingear/Tech___Games/' )
|
||||
,(u'Magazine' , PREFIX + u'Magazine/' )
|
||||
]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
return self.adeify_images(soup)
|
||||
|
||||
def parse_index(self):
|
||||
totalfeeds = []
|
||||
lfeeds = self.get_feeds()
|
||||
for feedobj in lfeeds:
|
||||
feedtitle, feedurl = feedobj
|
||||
self.report_progress(0, _('Fetching feed')+' %s...'%(feedtitle if feedtitle else feedurl))
|
||||
articles = []
|
||||
soup = self.index_to_soup(feedurl)
|
||||
for atag in soup.findAll('a',href=True):
|
||||
parentName = atag.parent.name
|
||||
title = self.tag_to_string(atag).strip()
|
||||
if (parentName == 'h2' or parentName == 'h3') and title is not None and title != '':
|
||||
url = self.INDEX + atag['href']
|
||||
articles.append({
|
||||
'title' :title
|
||||
,'date' :''
|
||||
,'url' :url
|
||||
,'description':''
|
||||
})
|
||||
totalfeeds.append((feedtitle, articles))
|
||||
return totalfeeds
|
@ -121,7 +121,7 @@ if iswindows:
|
||||
poppler_lib_dirs = consolidate('POPPLER_LIB_DIR', sw_lib_dir)
|
||||
popplerqt4_lib_dirs = poppler_lib_dirs
|
||||
poppler_libs = ['poppler']
|
||||
magick_inc_dirs = [os.path.join(prefix, 'build', 'ImageMagick-6.5.6')]
|
||||
magick_inc_dirs = [os.path.join(prefix, 'build', 'ImageMagick-6.6.6')]
|
||||
magick_lib_dirs = [os.path.join(magick_inc_dirs[0], 'VisualMagick', 'lib')]
|
||||
magick_libs = ['CORE_RL_wand_', 'CORE_RL_magick_']
|
||||
podofo_inc = os.path.join(sw_inc_dir, 'podofo')
|
||||
|
@ -18,7 +18,7 @@ QT_DLLS = ['Core', 'Gui', 'Network', 'Svg', 'WebKit', 'Xml', 'XmlPatterns']
|
||||
LIBUSB_DIR = 'C:\\libusb'
|
||||
LIBUNRAR = 'C:\\Program Files\\UnrarDLL\\unrar.dll'
|
||||
SW = r'C:\cygwin\home\kovid\sw'
|
||||
IMAGEMAGICK = os.path.join(SW, 'build', 'ImageMagick-6.5.6',
|
||||
IMAGEMAGICK = os.path.join(SW, 'build', 'ImageMagick-6.6.6',
|
||||
'VisualMagick', 'bin')
|
||||
|
||||
VERSION = re.sub('[a-z]\d+', '', __version__)
|
||||
|
@ -301,12 +301,14 @@ int projectType = MULTITHREADEDDLL;
|
||||
|
||||
Run configure.bat in a visual studio command prompt
|
||||
|
||||
Run configure.exe generated by configure.bat
|
||||
|
||||
Edit magick/magick-config.h
|
||||
|
||||
Undefine ProvideDllMain and MAGICKCORE_X11_DELEGATE
|
||||
|
||||
Now open VisualMagick/VisualDynamicMT.sln set to Release
|
||||
Remove the CORE_xlib project
|
||||
Remove the CORE_xlib and UTIL_Imdisplay project CORE_Magick++
|
||||
|
||||
calibre
|
||||
---------
|
||||
|
@ -43,8 +43,8 @@ class Stage3(Command):
|
||||
|
||||
description = 'Stage 3 of the publish process'
|
||||
sub_commands = ['upload_user_manual', 'upload_demo', 'sdist',
|
||||
'upload_to_mobileread', 'upload_to_google_code',
|
||||
'tag_release', 'upload_to_server', 'upload_to_sourceforge',
|
||||
'upload_to_google_code', 'tag_release', 'upload_to_server',
|
||||
'upload_to_sourceforge', 'upload_to_mobileread',
|
||||
]
|
||||
|
||||
class Stage4(Command):
|
||||
|
@ -80,6 +80,100 @@ class Plugin(object): # {{{
|
||||
'''
|
||||
pass
|
||||
|
||||
def config_widget(self):
|
||||
'''
|
||||
Implement this method and :meth:`save_settings` in your plugin to
|
||||
use a custom configuration dialog, rather then relying on the simple
|
||||
string based default customization.
|
||||
|
||||
This method, if implemented, must return a QWidget. The widget can have
|
||||
an optional method validate() that takes no arguments and is called
|
||||
immediately after the user clicks OK. Changes are applied if and only
|
||||
if the method returns True.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def save_settings(self, config_widget):
|
||||
'''
|
||||
Save the settings specified by the user with config_widget.
|
||||
|
||||
:param config_widget: The widget returned by :meth:`config_widget`.
|
||||
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def do_user_config(self, parent=None):
|
||||
'''
|
||||
This method shows a configuration dialog for this plugin. It returns
|
||||
True if the user clicks OK, False otherwise. The changes are
|
||||
automatically applied.
|
||||
'''
|
||||
from PyQt4.Qt import QDialog, QDialogButtonBox, QVBoxLayout, \
|
||||
QLabel, Qt, QLineEdit
|
||||
from calibre.gui2 import gprefs
|
||||
|
||||
prefname = 'plugin config dialog:'+self.type + ':' + self.name
|
||||
geom = gprefs.get(prefname, None)
|
||||
|
||||
config_dialog = QDialog(parent)
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
v = QVBoxLayout(config_dialog)
|
||||
|
||||
def size_dialog():
|
||||
if geom is None:
|
||||
config_dialog.resize(config_dialog.sizeHint())
|
||||
else:
|
||||
config_dialog.restoreGeometry(geom)
|
||||
|
||||
button_box.accepted.connect(config_dialog.accept)
|
||||
button_box.rejected.connect(config_dialog.reject)
|
||||
config_dialog.setWindowTitle(_('Customize') + ' ' + self.name)
|
||||
try:
|
||||
config_widget = self.config_widget()
|
||||
except NotImplementedError:
|
||||
config_widget = None
|
||||
|
||||
if config_widget is not None:
|
||||
v.addWidget(config_widget)
|
||||
v.addWidget(button_box)
|
||||
size_dialog()
|
||||
config_dialog.exec_()
|
||||
|
||||
if config_dialog.result() == QDialog.Accepted:
|
||||
if hasattr(config_widget, 'validate'):
|
||||
if config_widget.validate():
|
||||
self.save_settings(config_widget)
|
||||
else:
|
||||
self.save_settings(config_widget)
|
||||
else:
|
||||
from calibre.customize.ui import plugin_customization, \
|
||||
customize_plugin
|
||||
help_text = self.customization_help(gui=True)
|
||||
help_text = QLabel(help_text, config_dialog)
|
||||
help_text.setWordWrap(True)
|
||||
help_text.setTextInteractionFlags(Qt.LinksAccessibleByMouse
|
||||
| Qt.LinksAccessibleByKeyboard)
|
||||
help_text.setOpenExternalLinks(True)
|
||||
v.addWidget(help_text)
|
||||
sc = plugin_customization(self)
|
||||
if not sc:
|
||||
sc = ''
|
||||
sc = sc.strip()
|
||||
sc = QLineEdit(sc, config_dialog)
|
||||
v.addWidget(sc)
|
||||
v.addWidget(button_box)
|
||||
size_dialog()
|
||||
config_dialog.exec_()
|
||||
|
||||
if config_dialog.result() == QDialog.Accepted:
|
||||
sc = unicode(sc.text()).strip()
|
||||
customize_plugin(self, sc)
|
||||
|
||||
geom = bytearray(config_dialog.saveGeometry())
|
||||
gprefs[prefname] = geom
|
||||
|
||||
return config_dialog.result()
|
||||
|
||||
def load_resources(self, names):
|
||||
'''
|
||||
If this plugin comes in a ZIP file (user added plugin), this method
|
||||
|
@ -259,7 +259,7 @@ class EEEREADER(USBMS):
|
||||
PRODUCT_ID = [0x178f]
|
||||
BCD = [0x0319]
|
||||
|
||||
EBOOK_DIR_MAIN = 'Books'
|
||||
EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'Book'
|
||||
|
||||
VENDOR_NAME = 'LINUX'
|
||||
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'FILE-STOR_GADGET'
|
||||
|
@ -61,14 +61,26 @@ class PRS505(USBMS):
|
||||
ALL_BY_TITLE = _('All by title')
|
||||
ALL_BY_AUTHOR = _('All by author')
|
||||
|
||||
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of metadata fields '
|
||||
EXTRA_CUSTOMIZATION_MESSAGE = [
|
||||
_('Comma separated list of metadata fields '
|
||||
'to turn into collections on the device. Possibilities include: ')+\
|
||||
'series, tags, authors' +\
|
||||
_('. Two special collections are available: %s:%s and %s:%s. Add '
|
||||
'these values to the list to enable them. The collections will be '
|
||||
'given the name provided after the ":" character.')%(
|
||||
'abt', ALL_BY_TITLE, 'aba', ALL_BY_AUTHOR)
|
||||
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(['series', 'tags'])
|
||||
'abt', ALL_BY_TITLE, 'aba', ALL_BY_AUTHOR),
|
||||
_('Upload separate cover thumbnails for books (newer readers)') +
|
||||
':::'+_('Normally, the SONY readers get the cover image from the'
|
||||
' ebook file itself. With this option, calibre will send a '
|
||||
'separate cover image to the reader, useful if you are '
|
||||
'sending DRMed books in which you cannot change the cover.'
|
||||
' WARNING: This option should only be used with newer '
|
||||
'SONY readers: 350, 650, 950 and newer.'),
|
||||
]
|
||||
EXTRA_CUSTOMIZATION_DEFAULT = [
|
||||
', '.join(['series', 'tags']),
|
||||
False
|
||||
]
|
||||
|
||||
plugboard = None
|
||||
plugboard_func = None
|
||||
@ -159,7 +171,7 @@ class PRS505(USBMS):
|
||||
opts = self.settings()
|
||||
if opts.extra_customization:
|
||||
collections = [x.strip() for x in
|
||||
opts.extra_customization.split(',')]
|
||||
opts.extra_customization[0].split(',')]
|
||||
else:
|
||||
collections = []
|
||||
debug_print('PRS505: collection fields:', collections)
|
||||
@ -186,8 +198,12 @@ class PRS505(USBMS):
|
||||
self.plugboard_func = pb_func
|
||||
|
||||
def upload_cover(self, path, filename, metadata, filepath):
|
||||
return # Disabled as the SONY's don't need this thumbnail anyway and
|
||||
# older models don't auto delete it
|
||||
opts = self.settings()
|
||||
if not opts.extra_customization[1]:
|
||||
# Building thumbnails disabled
|
||||
debug_print('PRS505: not uploading covers')
|
||||
return
|
||||
debug_print('PRS505: uploading covers')
|
||||
if metadata.thumbnail and metadata.thumbnail[-1]:
|
||||
path = path.replace('/', os.sep)
|
||||
is_main = path.startswith(self._main_prefix)
|
||||
|
@ -10,7 +10,21 @@ from calibre.utils.config import Config, ConfigProxy
|
||||
class DeviceConfig(object):
|
||||
|
||||
HELP_MESSAGE = _('Configure Device')
|
||||
|
||||
#: Can be None, a string or a list of strings. When it is a string
|
||||
#: that string is used for the help text and the actual customization value
|
||||
#: can be read from ``dev.settings().extra_customization``.
|
||||
#: If it a list of strings, then dev.settings().extra_customization will
|
||||
#: also be a list. In this case, you *must* ensure that
|
||||
#: EXTRA_CUSTOMIZATION_DEFAULT is also a list. The list can contain either
|
||||
#: boolean values or strings, in which case a checkbox or line edit will be
|
||||
#: used for them in the config widget, automatically.
|
||||
#: If a string contains ::: then the text after it is interpreted as the
|
||||
#: tooltip
|
||||
EXTRA_CUSTOMIZATION_MESSAGE = None
|
||||
|
||||
#: The default value for extra customization. If you set
|
||||
#: EXTRA_CUSTOMIZATION_MESSAGE you *must* set this as well.
|
||||
EXTRA_CUSTOMIZATION_DEFAULT = None
|
||||
|
||||
SUPPORTS_SUB_DIRS = False
|
||||
@ -73,16 +87,33 @@ class DeviceConfig(object):
|
||||
if cls.SUPPORTS_USE_AUTHOR_SORT:
|
||||
proxy['use_author_sort'] = config_widget.use_author_sort()
|
||||
if cls.EXTRA_CUSTOMIZATION_MESSAGE:
|
||||
ec = unicode(config_widget.opt_extra_customization.text()).strip()
|
||||
if not ec:
|
||||
ec = None
|
||||
if isinstance(cls.EXTRA_CUSTOMIZATION_MESSAGE, list):
|
||||
ec = []
|
||||
for i in range(0, len(cls.EXTRA_CUSTOMIZATION_MESSAGE)):
|
||||
if hasattr(config_widget.opt_extra_customization[i], 'isChecked'):
|
||||
ec.append(config_widget.opt_extra_customization[i].isChecked())
|
||||
else:
|
||||
ec.append(unicode(config_widget.opt_extra_customization[i].text()).strip())
|
||||
else:
|
||||
ec = unicode(config_widget.opt_extra_customization.text()).strip()
|
||||
if not ec:
|
||||
ec = None
|
||||
proxy['extra_customization'] = ec
|
||||
st = unicode(config_widget.opt_save_template.text())
|
||||
proxy['save_template'] = st
|
||||
|
||||
@classmethod
|
||||
def settings(cls):
|
||||
return cls._config().parse()
|
||||
opts = cls._config().parse()
|
||||
if isinstance(cls.EXTRA_CUSTOMIZATION_DEFAULT, list):
|
||||
if opts.extra_customization is None:
|
||||
opts.extra_customization = []
|
||||
if not isinstance(opts.extra_customization, list):
|
||||
opts.extra_customization = [opts.extra_customization]
|
||||
for i,d in enumerate(cls.EXTRA_CUSTOMIZATION_DEFAULT):
|
||||
if i >= len(opts.extra_customization):
|
||||
opts.extra_customization.append(d)
|
||||
return opts
|
||||
|
||||
@classmethod
|
||||
def save_template(cls):
|
||||
|
@ -16,6 +16,7 @@ from calibre.ebooks.metadata.book import TOP_LEVEL_CLASSIFIERS
|
||||
from calibre.ebooks.metadata.book import ALL_METADATA_FIELDS
|
||||
from calibre.library.field_metadata import FieldMetadata
|
||||
from calibre.utils.date import isoformat, format_date
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.utils.formatter import TemplateFormatter
|
||||
|
||||
|
||||
@ -38,15 +39,16 @@ class SafeFormat(TemplateFormatter):
|
||||
|
||||
def get_value(self, key, args, kwargs):
|
||||
try:
|
||||
key = key.lower()
|
||||
if key != 'title_sort':
|
||||
key = field_metadata.search_term_to_field_key(key.lower())
|
||||
key = field_metadata.search_term_to_field_key(key)
|
||||
b = self.book.get_user_metadata(key, False)
|
||||
if b and b['datatype'] == 'int' and self.book.get(key, 0) == 0:
|
||||
v = ''
|
||||
elif b and b['datatype'] == 'float' and self.book.get(key, 0.0) == 0.0:
|
||||
v = ''
|
||||
else:
|
||||
ign, v = self.book.format_field(key.lower(), series_with_index=False)
|
||||
ign, v = self.book.format_field(key, series_with_index=False)
|
||||
if v is None:
|
||||
return ''
|
||||
if v == '':
|
||||
@ -489,7 +491,7 @@ class Metadata(object):
|
||||
return authors_to_string(self.authors)
|
||||
|
||||
def format_tags(self):
|
||||
return u', '.join([unicode(t) for t in self.tags])
|
||||
return u', '.join([unicode(t) for t in sorted(self.tags, key=sort_key)])
|
||||
|
||||
def format_rating(self):
|
||||
return unicode(self.rating)
|
||||
@ -529,7 +531,7 @@ class Metadata(object):
|
||||
orig_res = res
|
||||
datatype = cmeta['datatype']
|
||||
if datatype == 'text' and cmeta['is_multiple']:
|
||||
res = u', '.join(res)
|
||||
res = u', '.join(sorted(res, key=sort_key))
|
||||
elif datatype == 'series' and series_with_index:
|
||||
if self.get_extra(key) is not None:
|
||||
res = res + \
|
||||
@ -559,7 +561,7 @@ class Metadata(object):
|
||||
elif key == 'series_index':
|
||||
res = self.format_series_index(res)
|
||||
elif datatype == 'text' and fmeta['is_multiple']:
|
||||
res = u', '.join(res)
|
||||
res = u', '.join(sorted(res, key=sort_key))
|
||||
elif datatype == 'series' and series_with_index:
|
||||
res = res + ' [%s]'%self.format_series_index()
|
||||
elif datatype == 'datetime':
|
||||
|
@ -55,10 +55,10 @@ class Query(object):
|
||||
BASE_URL = 'http://isbndb.com/api/books.xml?'
|
||||
|
||||
def __init__(self, key, title=None, author=None, publisher=None, isbn=None,
|
||||
keywords=None, max_results=40):
|
||||
keywords=None, max_results=30):
|
||||
assert not(title is None and author is None and publisher is None and \
|
||||
isbn is None and keywords is None)
|
||||
assert (max_results < 41)
|
||||
assert (max_results < 31)
|
||||
|
||||
if title == _('Unknown'):
|
||||
title=None
|
||||
|
@ -18,7 +18,6 @@ class xISBN(object):
|
||||
self._data = []
|
||||
self._map = {}
|
||||
|
||||
self.br = browser()
|
||||
self.isbn_pat = re.compile(r'[^0-9X]', re.IGNORECASE)
|
||||
|
||||
def purify(self, isbn):
|
||||
@ -26,7 +25,7 @@ class xISBN(object):
|
||||
|
||||
def fetch_data(self, isbn):
|
||||
url = self.QUERY%isbn
|
||||
data = self.br.open_novisit(url).read()
|
||||
data = browser().open_novisit(url).read()
|
||||
data = json.loads(data)
|
||||
if data.get('stat', None) != 'ok':
|
||||
return []
|
||||
|
@ -103,7 +103,7 @@ class CoverManager(object):
|
||||
from calibre.ebooks import calibre_cover
|
||||
img_data = calibre_cover(title, authors_to_string(authors),
|
||||
series_string=series_string)
|
||||
id, href = self.oeb.manifest.generate('cover_image',
|
||||
id, href = self.oeb.manifest.generate('cover',
|
||||
'cover_image.jpg')
|
||||
item = self.oeb.manifest.add(id, href, guess_type('t.jpg')[0],
|
||||
data=img_data)
|
||||
|
@ -127,9 +127,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1" colspan="3">
|
||||
<widget class="QLineEdit" name="opt_input_encoding"/>
|
||||
</item>
|
||||
<item row="6" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="opt_remove_paragraph_spacing">
|
||||
<property name="text">
|
||||
@ -244,8 +241,22 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1" colspan="3">
|
||||
<widget class="EncodingComboBox" name="opt_input_encoding">
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>EncodingComboBox</class>
|
||||
<extends>QComboBox</extends>
|
||||
<header>widgets.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources>
|
||||
<include location="../../../../resources/images.qrc"/>
|
||||
<include location="../../../../resources/images.qrc"/>
|
||||
|
@ -4,7 +4,10 @@ __license__ = 'GPL 3'
|
||||
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from PyQt4.Qt import QWidget, QListWidgetItem, Qt, QVariant, SIGNAL
|
||||
import textwrap
|
||||
|
||||
from PyQt4.Qt import QWidget, QListWidgetItem, Qt, QVariant, SIGNAL, \
|
||||
QLabel, QLineEdit, QCheckBox
|
||||
|
||||
from calibre.gui2 import error_dialog
|
||||
from calibre.gui2.device_drivers.configwidget_ui import Ui_ConfigWidget
|
||||
@ -46,12 +49,38 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
|
||||
else:
|
||||
self.opt_use_author_sort.hide()
|
||||
if extra_customization_message:
|
||||
self.extra_customization_label.setText(extra_customization_message)
|
||||
if settings.extra_customization:
|
||||
self.opt_extra_customization.setText(settings.extra_customization)
|
||||
else:
|
||||
self.extra_customization_label.setVisible(False)
|
||||
self.opt_extra_customization.setVisible(False)
|
||||
def parse_msg(m):
|
||||
msg, _, tt = m.partition(':::') if m else ('', '', '')
|
||||
return msg.strip(), textwrap.fill(tt.strip(), 100)
|
||||
|
||||
if isinstance(extra_customization_message, list):
|
||||
self.opt_extra_customization = []
|
||||
for i, m in enumerate(extra_customization_message):
|
||||
label_text, tt = parse_msg(m)
|
||||
if isinstance(settings.extra_customization[i], bool):
|
||||
self.opt_extra_customization.append(QCheckBox(label_text))
|
||||
self.opt_extra_customization[-1].setToolTip(tt)
|
||||
self.opt_extra_customization[i].setChecked(bool(settings.extra_customization[i]))
|
||||
else:
|
||||
self.opt_extra_customization.append(QLineEdit(self))
|
||||
l = QLabel(label_text)
|
||||
l.setToolTip(tt)
|
||||
l.setBuddy(self.opt_extra_customization[i])
|
||||
l.setWordWrap(True)
|
||||
self.opt_extra_customization[i].setText(settings.extra_customization[i])
|
||||
self.extra_layout.addWidget(l)
|
||||
self.extra_layout.addWidget(self.opt_extra_customization[i])
|
||||
else:
|
||||
self.opt_extra_customization = QLineEdit()
|
||||
label_text, tt = parse_msg(extra_customization_message)
|
||||
l = QLabel(label_text)
|
||||
l.setToolTip(tt)
|
||||
l.setBuddy(self.opt_extra_customization)
|
||||
l.setWordWrap(True)
|
||||
if settings.extra_customization:
|
||||
self.opt_extra_customization.setText(settings.extra_customization)
|
||||
self.extra_layout.addWidget(l)
|
||||
self.extra_layout.addWidget(self.opt_extra_customization)
|
||||
self.opt_save_template.setText(settings.save_template)
|
||||
|
||||
|
||||
|
@ -98,20 +98,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="extra_customization_label">
|
||||
<property name="text">
|
||||
<string>Extra customization</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_extra_customization</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<widget class="QLineEdit" name="opt_extra_customization"/>
|
||||
<layout class="QVBoxLayout" name="extra_layout"/>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
|
@ -321,7 +321,8 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
if (f in ['author_sort'] or
|
||||
(fm[f]['datatype'] in ['text', 'series', 'enumeration']
|
||||
and fm[f].get('search_terms', None)
|
||||
and f not in ['formats', 'ondevice', 'sort'])):
|
||||
and f not in ['formats', 'ondevice', 'sort']) or
|
||||
fm[f]['datatype'] in ['int', 'float', 'bool'] ):
|
||||
self.all_fields.append(f)
|
||||
self.writable_fields.append(f)
|
||||
if f in ['sort'] or fm[f]['datatype'] == 'composite':
|
||||
@ -431,12 +432,14 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
val = mi.get('title_sort', None)
|
||||
else:
|
||||
val = mi.get(field, None)
|
||||
if isinstance(val, (int, float, bool)):
|
||||
val = str(val)
|
||||
if val is None:
|
||||
val = [] if fm['is_multiple'] else ['']
|
||||
elif not fm['is_multiple']:
|
||||
val = [val]
|
||||
elif field == 'authors':
|
||||
val = [v.replace(',', '|') for v in val]
|
||||
val = [v.replace('|', ',') for v in val]
|
||||
else:
|
||||
val = []
|
||||
return val
|
||||
@ -566,17 +569,11 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
dest_val = mi.get(dest, '')
|
||||
if dest_val is None:
|
||||
dest_val = []
|
||||
elif isinstance(dest_val, list):
|
||||
if dest == 'authors':
|
||||
dest_val = [v.replace(',', '|') for v in dest_val]
|
||||
else:
|
||||
elif not isinstance(dest_val, list):
|
||||
dest_val = [dest_val]
|
||||
else:
|
||||
dest_val = []
|
||||
|
||||
if len(val) > 0:
|
||||
if src == 'authors':
|
||||
val = [v.replace(',', '|') for v in val]
|
||||
if dest_mode == 1:
|
||||
val.extend(dest_val)
|
||||
elif dest_mode == 2:
|
||||
|
@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import re, copy
|
||||
|
||||
from PyQt4.QtGui import QDialog, QDialogButtonBox
|
||||
from PyQt4.Qt import QDialog, QDialogButtonBox, QCompleter, Qt
|
||||
|
||||
from calibre.gui2.dialogs.search_ui import Ui_Dialog
|
||||
from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH
|
||||
@ -22,6 +22,28 @@ class SearchDialog(QDialog, Ui_Dialog):
|
||||
key=lambda x: sort_key(x if x[0] != '#' else x[1:]))
|
||||
self.general_combo.addItems(searchables)
|
||||
|
||||
all_authors = db.all_authors()
|
||||
all_authors.sort(key=lambda x : sort_key(x[1]))
|
||||
for i in all_authors:
|
||||
id, name = i
|
||||
name = name.strip().replace('|', ',')
|
||||
self.authors_box.addItem(name)
|
||||
self.authors_box.setEditText('')
|
||||
self.authors_box.completer().setCompletionMode(QCompleter.PopupCompletion)
|
||||
self.authors_box.setAutoCompletionCaseSensitivity(Qt.CaseInsensitive)
|
||||
|
||||
all_series = db.all_series()
|
||||
all_series.sort(key=lambda x : sort_key(x[1]))
|
||||
for i in all_series:
|
||||
id, name = i
|
||||
self.series_box.addItem(name)
|
||||
self.series_box.setEditText('')
|
||||
self.series_box.completer().setCompletionMode(QCompleter.PopupCompletion)
|
||||
self.series_box.setAutoCompletionCaseSensitivity(Qt.CaseInsensitive)
|
||||
|
||||
all_tags = db.all_tags()
|
||||
self.tags_box.update_tags_cache(all_tags)
|
||||
|
||||
self.box_last_values = copy.deepcopy(box_values)
|
||||
if self.box_last_values:
|
||||
for k,v in self.box_last_values.items():
|
||||
@ -121,26 +143,34 @@ class SearchDialog(QDialog, Ui_Dialog):
|
||||
return tok
|
||||
|
||||
def box_search_string(self):
|
||||
mk = self.matchkind.currentIndex()
|
||||
if mk == CONTAINS_MATCH:
|
||||
self.mc = ''
|
||||
elif mk == EQUALS_MATCH:
|
||||
self.mc = '='
|
||||
else:
|
||||
self.mc = '~'
|
||||
|
||||
ans = []
|
||||
self.box_last_values = {}
|
||||
title = unicode(self.title_box.text()).strip()
|
||||
self.box_last_values['title_box'] = title
|
||||
if title:
|
||||
ans.append('title:"' + title + '"')
|
||||
ans.append('title:"' + self.mc + title + '"')
|
||||
author = unicode(self.authors_box.text()).strip()
|
||||
self.box_last_values['authors_box'] = author
|
||||
if author:
|
||||
ans.append('author:"' + author + '"')
|
||||
ans.append('author:"' + self.mc + author + '"')
|
||||
series = unicode(self.series_box.text()).strip()
|
||||
self.box_last_values['series_box'] = series
|
||||
if series:
|
||||
ans.append('series:"' + series + '"')
|
||||
self.mc = '='
|
||||
ans.append('series:"' + self.mc + series + '"')
|
||||
|
||||
tags = unicode(self.tags_box.text())
|
||||
self.box_last_values['tags_box'] = tags
|
||||
tags = self.tokens(tags)
|
||||
tags = [t.strip() for t in tags.split(',') if t.strip()]
|
||||
if tags:
|
||||
tags = ['tags:' + t for t in tags]
|
||||
tags = ['tags:"=' + t + '"' for t in tags]
|
||||
ans.append('(' + ' or '.join(tags) + ')')
|
||||
general = unicode(self.general_box.text())
|
||||
self.box_last_values['general_box'] = general
|
||||
|
@ -21,7 +21,7 @@
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>What kind of match to use:</string>
|
||||
<string>&What kind of match to use:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>matchkind</cstring>
|
||||
@ -228,7 +228,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="title_box">
|
||||
<widget class="EnLineEdit" name="title_box">
|
||||
<property name="toolTip">
|
||||
<string>Enter the title.</string>
|
||||
</property>
|
||||
@ -265,21 +265,21 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="authors_box">
|
||||
<widget class="EnComboBox" name="authors_box">
|
||||
<property name="toolTip">
|
||||
<string>Enter an author's name. Only one author can be used.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QLineEdit" name="series_box">
|
||||
<widget class="EnComboBox" name="series_box">
|
||||
<property name="toolTip">
|
||||
<string>Enter a series name, without an index. Only one series name can be used.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QLineEdit" name="tags_box">
|
||||
<widget class="TagsLineEdit" name="tags_box">
|
||||
<property name="toolTip">
|
||||
<string>Enter tags separated by spaces</string>
|
||||
</property>
|
||||
@ -348,6 +348,23 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>EnLineEdit</class>
|
||||
<extends>QLineEdit</extends>
|
||||
<header>widgets.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>EnComboBox</class>
|
||||
<extends>QComboBox</extends>
|
||||
<header>widgets.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>TagsLineEdit</class>
|
||||
<extends>QLineEdit</extends>
|
||||
<header>widgets.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>all</tabstop>
|
||||
<tabstop>phrase</tabstop>
|
||||
|
@ -57,7 +57,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
(_('Never'), 'never')]
|
||||
r('toolbar_text', gprefs, choices=choices)
|
||||
|
||||
choices = [(_('Disabled'), 'disabled'), (_('By first letter'), 'first letter'),
|
||||
choices = [(_('Disabled'), 'disable'), (_('By first letter'), 'first letter'),
|
||||
(_('Partitioned'), 'partition')]
|
||||
r('tags_browser_partition_method', gprefs, choices=choices)
|
||||
r('tags_browser_collapse_at', gprefs)
|
||||
|
@ -8,13 +8,12 @@ __docformat__ = 'restructuredtext en'
|
||||
import textwrap, os
|
||||
|
||||
from PyQt4.Qt import Qt, QModelIndex, QAbstractItemModel, QVariant, QIcon, \
|
||||
QBrush, QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QLineEdit
|
||||
QBrush
|
||||
|
||||
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
|
||||
from calibre.gui2.preferences.plugins_ui import Ui_Form
|
||||
from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \
|
||||
disable_plugin, customize_plugin, \
|
||||
plugin_customization, add_plugin, \
|
||||
disable_plugin, plugin_customization, add_plugin, \
|
||||
remove_plugin
|
||||
from calibre.gui2 import NONE, error_dialog, info_dialog, choose_files
|
||||
|
||||
@ -129,6 +128,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
self.plugin_view.setModel(self._plugin_model)
|
||||
self.plugin_view.setStyleSheet(
|
||||
"QTreeView::item { padding-bottom: 10px;}")
|
||||
self.plugin_view.doubleClicked.connect(self.double_clicked)
|
||||
self.toggle_plugin_button.clicked.connect(self.toggle_plugin)
|
||||
self.customize_plugin_button.clicked.connect(self.customize_plugin)
|
||||
self.remove_plugin_button.clicked.connect(self.remove_plugin)
|
||||
@ -138,6 +138,10 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
def toggle_plugin(self, *args):
|
||||
self.modify_plugin(op='toggle')
|
||||
|
||||
def double_clicked(self, index):
|
||||
if index.parent().isValid():
|
||||
self.modify_plugin(op='customize')
|
||||
|
||||
def customize_plugin(self, *args):
|
||||
self.modify_plugin(op='customize')
|
||||
|
||||
@ -184,49 +188,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
_('Plugin: %s does not need customization')%plugin.name).exec_()
|
||||
return
|
||||
self.changed_signal.emit()
|
||||
|
||||
config_dialog = QDialog(self)
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
v = QVBoxLayout(config_dialog)
|
||||
|
||||
button_box.accepted.connect(config_dialog.accept)
|
||||
button_box.rejected.connect(config_dialog.reject)
|
||||
config_dialog.setWindowTitle(_('Customize') + ' ' + plugin.name)
|
||||
|
||||
if hasattr(plugin, 'config_widget'):
|
||||
config_widget = plugin.config_widget()
|
||||
v.addWidget(config_widget)
|
||||
v.addWidget(button_box)
|
||||
config_dialog.exec_()
|
||||
|
||||
if config_dialog.result() == QDialog.Accepted:
|
||||
if hasattr(config_widget, 'validate'):
|
||||
if config_widget.validate():
|
||||
plugin.save_settings(config_widget)
|
||||
else:
|
||||
plugin.save_settings(config_widget)
|
||||
self._plugin_model.refresh_plugin(plugin)
|
||||
else:
|
||||
help_text = plugin.customization_help(gui=True)
|
||||
help_text = QLabel(help_text, config_dialog)
|
||||
help_text.setWordWrap(True)
|
||||
help_text.setTextInteractionFlags(Qt.LinksAccessibleByMouse
|
||||
| Qt.LinksAccessibleByKeyboard)
|
||||
help_text.setOpenExternalLinks(True)
|
||||
v.addWidget(help_text)
|
||||
sc = plugin_customization(plugin)
|
||||
if not sc:
|
||||
sc = ''
|
||||
sc = sc.strip()
|
||||
sc = QLineEdit(sc, config_dialog)
|
||||
v.addWidget(sc)
|
||||
v.addWidget(button_box)
|
||||
config_dialog.exec_()
|
||||
|
||||
if config_dialog.result() == QDialog.Accepted:
|
||||
sc = unicode(sc.text()).strip()
|
||||
customize_plugin(plugin, sc)
|
||||
|
||||
if plugin.do_user_config():
|
||||
self._plugin_model.refresh_plugin(plugin)
|
||||
elif op == 'remove':
|
||||
if remove_plugin(plugin):
|
||||
|
@ -616,6 +616,31 @@ class ComboBoxWithHelp(QComboBox):
|
||||
QComboBox.hidePopup(self)
|
||||
self.set_state()
|
||||
|
||||
|
||||
class EncodingComboBox(QComboBox):
|
||||
'''
|
||||
A combobox that holds text encodings support
|
||||
by Python. This is only populated with the most
|
||||
common and standard encodings. There is no good
|
||||
way to programatically list all supported encodings
|
||||
using encodings.aliases.aliases.keys(). It
|
||||
will not work.
|
||||
'''
|
||||
|
||||
ENCODINGS = ['', 'cp1252', 'latin1', 'utf-8', '', 'ascii', 'big5', 'cp1250', 'cp1251', 'cp1253',
|
||||
'cp1254', 'cp1255', 'cp1256', 'euc_jp', 'euc_kr', 'gb2312', 'gb18030',
|
||||
'hz', 'iso2022_jp', 'iso2022_kr', 'iso8859_5', 'shift_jis',
|
||||
]
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QComboBox.__init__(self, parent)
|
||||
self.setEditable(True)
|
||||
self.setLineEdit(EnLineEdit(self))
|
||||
|
||||
for item in self.ENCODINGS:
|
||||
self.addItem(item)
|
||||
|
||||
|
||||
class PythonHighlighter(QSyntaxHighlighter):
|
||||
|
||||
Rules = []
|
||||
|
@ -132,6 +132,38 @@ def _match(query, value, matchkind):
|
||||
pass
|
||||
return False
|
||||
|
||||
class CacheRow(list):
|
||||
|
||||
def __init__(self, db, composites, val):
|
||||
self.db = db
|
||||
self._composites = composites
|
||||
list.__init__(self, val)
|
||||
self._must_do = len(composites) > 0
|
||||
|
||||
def __getitem__(self, col):
|
||||
if self._must_do:
|
||||
is_comp = False
|
||||
if isinstance(col, slice):
|
||||
start = 0 if col.start is None else col.start
|
||||
step = 1 if col.stop is None else col.stop
|
||||
for c in range(start, col.stop, step):
|
||||
if c in self._composites:
|
||||
is_comp = True
|
||||
break
|
||||
elif col in self._composites:
|
||||
is_comp = True
|
||||
if is_comp:
|
||||
id = list.__getitem__(self, 0)
|
||||
self._must_do = False
|
||||
mi = self.db.get_metadata(id, index_is_id=True)
|
||||
for c in self._composites:
|
||||
self[c] = mi.get(self._composites[c])
|
||||
return list.__getitem__(self, col)
|
||||
|
||||
def __getslice__(self, i, j):
|
||||
return self.__getitem__(slice(i, j))
|
||||
|
||||
|
||||
class ResultCache(SearchQueryParser): # {{{
|
||||
|
||||
'''
|
||||
@ -139,7 +171,12 @@ class ResultCache(SearchQueryParser): # {{{
|
||||
'''
|
||||
def __init__(self, FIELD_MAP, field_metadata):
|
||||
self.FIELD_MAP = FIELD_MAP
|
||||
self._map = self._data = self._map_filtered = []
|
||||
self.composites = {}
|
||||
for key in field_metadata:
|
||||
if field_metadata[key]['datatype'] == 'composite':
|
||||
self.composites[field_metadata[key]['rec_index']] = key
|
||||
self._data = []
|
||||
self._map = self._map_filtered = []
|
||||
self.first_sort = True
|
||||
self.search_restriction = ''
|
||||
self.field_metadata = field_metadata
|
||||
@ -148,10 +185,6 @@ class ResultCache(SearchQueryParser): # {{{
|
||||
self.build_date_relop_dict()
|
||||
self.build_numeric_relop_dict()
|
||||
|
||||
self.composites = []
|
||||
for key in field_metadata:
|
||||
if field_metadata[key]['datatype'] == 'composite':
|
||||
self.composites.append((key, field_metadata[key]['rec_index']))
|
||||
|
||||
def __getitem__(self, row):
|
||||
return self._data[self._map_filtered[row]]
|
||||
@ -583,13 +616,10 @@ class ResultCache(SearchQueryParser): # {{{
|
||||
'''
|
||||
for id in ids:
|
||||
try:
|
||||
self._data[id] = db.conn.get('SELECT * from meta2 WHERE id=?', (id,))[0]
|
||||
self._data[id] = CacheRow(db, self.composites,
|
||||
db.conn.get('SELECT * from meta2 WHERE id=?', (id,))[0])
|
||||
self._data[id].append(db.book_on_device_string(id))
|
||||
self._data[id].append(None)
|
||||
if len(self.composites) > 0:
|
||||
mi = db.get_metadata(id, index_is_id=True)
|
||||
for k,c in self.composites:
|
||||
self._data[id][c] = mi.get(k, None)
|
||||
except IndexError:
|
||||
return None
|
||||
try:
|
||||
@ -603,13 +633,10 @@ class ResultCache(SearchQueryParser): # {{{
|
||||
return
|
||||
self._data.extend(repeat(None, max(ids)-len(self._data)+2))
|
||||
for id in ids:
|
||||
self._data[id] = db.conn.get('SELECT * from meta2 WHERE id=?', (id,))[0]
|
||||
self._data[id] = CacheRow(db, self.composites,
|
||||
db.conn.get('SELECT * from meta2 WHERE id=?', (id,))[0])
|
||||
self._data[id].append(db.book_on_device_string(id))
|
||||
self._data[id].append(None)
|
||||
if len(self.composites) > 0:
|
||||
mi = db.get_metadata(id, index_is_id=True)
|
||||
for k,c in self.composites:
|
||||
self._data[id][c] = mi.get(k)
|
||||
self._map[0:0] = ids
|
||||
self._map_filtered[0:0] = ids
|
||||
|
||||
@ -630,16 +657,11 @@ class ResultCache(SearchQueryParser): # {{{
|
||||
temp = db.conn.get('SELECT * FROM meta2')
|
||||
self._data = list(itertools.repeat(None, temp[-1][0]+2)) if temp else []
|
||||
for r in temp:
|
||||
self._data[r[0]] = r
|
||||
self._data[r[0]] = CacheRow(db, self.composites, r)
|
||||
for item in self._data:
|
||||
if item is not None:
|
||||
item.append(db.book_on_device_string(item[0]))
|
||||
item.append(None)
|
||||
if len(self.composites) > 0:
|
||||
mi = db.get_metadata(item[0], index_is_id=True)
|
||||
for k,c in self.composites:
|
||||
item[c] = mi.get(k)
|
||||
|
||||
self._map = [i[0] for i in self._data if i is not None]
|
||||
if field is not None:
|
||||
self.sort(field, ascending)
|
||||
@ -669,13 +691,7 @@ class ResultCache(SearchQueryParser): # {{{
|
||||
fields = [('timestamp', False)]
|
||||
|
||||
keyg = SortKeyGenerator(fields, self.field_metadata, self._data)
|
||||
# For efficiency, the key generator returns a plain value if only one
|
||||
# field is in the sort field list. Because the normal cmp function will
|
||||
# always assume asc, we must deal with asc/desc here.
|
||||
if len(fields) == 1:
|
||||
self._map.sort(key=keyg, reverse=not fields[0][1])
|
||||
else:
|
||||
self._map.sort(key=keyg)
|
||||
self._map.sort(key=keyg)
|
||||
|
||||
tmap = list(itertools.repeat(False, len(self._data)))
|
||||
for x in self._map_filtered:
|
||||
@ -708,8 +724,6 @@ class SortKeyGenerator(object):
|
||||
|
||||
def __call__(self, record):
|
||||
values = tuple(self.itervals(self.data[record]))
|
||||
if len(values) == 1:
|
||||
return values[0]
|
||||
return SortKey(self.orders, values)
|
||||
|
||||
def itervals(self, record):
|
||||
@ -732,6 +746,11 @@ class SortKeyGenerator(object):
|
||||
val = (self.string_sort_key(val), sidx)
|
||||
|
||||
elif dt in ('text', 'comments', 'composite', 'enumeration'):
|
||||
if val:
|
||||
sep = fm['is_multiple']
|
||||
if sep:
|
||||
val = sep.join(sorted(val.split(sep),
|
||||
key=self.string_sort_key))
|
||||
val = self.string_sort_key(val)
|
||||
|
||||
elif dt == 'bool':
|
||||
|
@ -15,7 +15,7 @@ from calibre.customize import CatalogPlugin
|
||||
from calibre.customize.conversion import OptionRecommendation, DummyReporter
|
||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString
|
||||
from calibre.ebooks.chardet import substitute_entites
|
||||
from calibre.ebooks.oeb.base import RECOVER_PARSER, XHTML_NS
|
||||
from calibre.ebooks.oeb.base import XHTML_NS
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
from calibre.utils.config import config_dir
|
||||
from calibre.utils.date import format_date, isoformat, now as nowf
|
||||
|
@ -133,7 +133,15 @@ class CustomColumns(object):
|
||||
|
||||
def adapt_bool(x, d):
|
||||
if isinstance(x, (str, unicode, bytes)):
|
||||
x = bool(int(x))
|
||||
x = x.lower()
|
||||
if x == 'true':
|
||||
x = True
|
||||
elif x == 'false':
|
||||
x = False
|
||||
elif x == 'none':
|
||||
x = None
|
||||
else:
|
||||
x = bool(int(x))
|
||||
return x
|
||||
|
||||
def adapt_enum(x, d):
|
||||
@ -142,9 +150,17 @@ class CustomColumns(object):
|
||||
v = None
|
||||
return v
|
||||
|
||||
def adapt_number(x, d):
|
||||
if isinstance(x, (str, unicode, bytes)):
|
||||
if x.lower() == 'none':
|
||||
return None
|
||||
if d['datatype'] == 'int':
|
||||
return int(x)
|
||||
return float(x)
|
||||
|
||||
self.custom_data_adapters = {
|
||||
'float': lambda x,d : x if x is None else float(x),
|
||||
'int': lambda x,d : x if x is None else int(x),
|
||||
'float': adapt_number,
|
||||
'int': adapt_number,
|
||||
'rating':lambda x,d : x if x is None else min(10., max(0., float(x))),
|
||||
'bool': adapt_bool,
|
||||
'comments': lambda x,d: adapt_text(x, {'is_multiple':False}),
|
||||
|
@ -131,6 +131,8 @@ class SafeFormat(TemplateFormatter):
|
||||
return self.composite_values[key]
|
||||
if key in kwargs:
|
||||
val = kwargs[key]
|
||||
if isinstance(val, list):
|
||||
val = ','.join(val)
|
||||
return val.replace('/', '_').replace('\\', '_')
|
||||
return ''
|
||||
except:
|
||||
|
@ -101,7 +101,19 @@ def html_to_lxml(raw):
|
||||
root = html.fragment_fromstring(raw)
|
||||
root.set('xmlns', "http://www.w3.org/1999/xhtml")
|
||||
raw = etree.tostring(root, encoding=None)
|
||||
return etree.fromstring(raw)
|
||||
try:
|
||||
return etree.fromstring(raw)
|
||||
except:
|
||||
for x in root.iterdescendants():
|
||||
remove = []
|
||||
for attr in x.attrib:
|
||||
if ':' in attr:
|
||||
remove.append(attr)
|
||||
for a in remove:
|
||||
del x.attrib[a]
|
||||
raw = etree.tostring(root, encoding=None)
|
||||
return etree.fromstring(raw)
|
||||
|
||||
|
||||
def CATALOG_ENTRY(item, item_kind, base_href, version, updated,
|
||||
ignore_count=False, add_kind=False):
|
||||
|
@ -150,7 +150,7 @@ The example shows several things:
|
||||
|
||||
* program mode is used if the expression begins with ``:'`` and ends with ``'``. Anything else is assumed to be single-function.
|
||||
* the variable ``$`` stands for the field the expression is operating upon, ``#series`` in this case.
|
||||
* functions must be given all their arguments. There is no default value. This is true for the standard builtin functions, and is a significant difference from single-function mode.
|
||||
* functions must be given all their arguments. There is no default value. For example, the standard builtin functions must be given an additional initial parameter indicating the source field, which is a significant difference from single-function mode.
|
||||
* white space is ignored and can be used anywhere within the expression.
|
||||
* constant strings are enclosed in matching quotes, either ``'`` or ``"``.
|
||||
|
||||
@ -204,7 +204,7 @@ For various values of series_index, the program returns:
|
||||
|
||||
All the functions listed under single-function mode can be used in program mode, noting that unlike the functions described below you must supply a first parameter providing the value the function is to act upon.
|
||||
|
||||
The following functions are available in addition to those described in single-function mode. With the exception of the ``id`` parameter of assign, all parameters can be statements (sequences of expressions):
|
||||
The following functions are available in addition to those described in single-function mode. Remember from the example above that the single-function mode functions require an additional first parameter specifying the field to operate on. With the exception of the ``id`` parameter of assign, all parameters can be statements (sequences of expressions):
|
||||
|
||||
* ``add(x, y)`` -- returns x + y. Throws an exception if either x or y are not numbers.
|
||||
* ``assign(id, val)`` -- assigns val to id, then returns val. id must be an identifier, not an expression
|
||||
|
@ -431,7 +431,10 @@ class TemplateFormatter(string.Formatter):
|
||||
return prefix + val + suffix
|
||||
|
||||
def vformat(self, fmt, args, kwargs):
|
||||
ans = string.Formatter.vformat(self, fmt, args, kwargs)
|
||||
if fmt.startswith('program:'):
|
||||
ans = self._eval_program(None, fmt[8:])
|
||||
else:
|
||||
ans = string.Formatter.vformat(self, fmt, args, kwargs)
|
||||
return self.compress_spaces.sub(' ', ans).strip()
|
||||
|
||||
########## a formatter guaranteed not to throw and exception ############
|
||||
@ -441,10 +444,7 @@ class TemplateFormatter(string.Formatter):
|
||||
self.book = book
|
||||
self.composite_values = {}
|
||||
try:
|
||||
if fmt.startswith('program:'):
|
||||
ans = self._eval_program(None, fmt[8:])
|
||||
else:
|
||||
ans = self.vformat(fmt, [], kwargs).strip()
|
||||
ans = self.vformat(fmt, [], kwargs).strip()
|
||||
except Exception, e:
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
@ -468,6 +468,7 @@ class EvalFormatter(TemplateFormatter):
|
||||
A template formatter that uses a simple dict instead of an mi instance
|
||||
'''
|
||||
def get_value(self, key, args, kwargs):
|
||||
key = key.lower()
|
||||
return kwargs.get(key, _('No such variable ') + key)
|
||||
|
||||
eval_formatter = EvalFormatter()
|
||||
|
@ -106,13 +106,16 @@ class Image(_magick.Image): # {{{
|
||||
return ans
|
||||
|
||||
def load(self, data):
|
||||
return _magick.Image.load(self, bytes(data))
|
||||
data = bytes(data)
|
||||
if not data:
|
||||
raise ValueError('Cannot open image from empty data string')
|
||||
return _magick.Image.load(self, data)
|
||||
|
||||
def open(self, path_or_file):
|
||||
if not hasattr(path_or_file, 'read') and \
|
||||
path_or_file.lower().endswith('.wmf'):
|
||||
# Special handling for WMF files as ImageMagick seems
|
||||
# to hand while reading them from a blob on linux
|
||||
# to hang while reading them from a blob on linux
|
||||
if isinstance(path_or_file, unicode):
|
||||
path_or_file = path_or_file.encode(filesystem_encoding)
|
||||
return _magick.Image.read(self, path_or_file)
|
||||
@ -121,6 +124,8 @@ class Image(_magick.Image): # {{{
|
||||
data = data.read()
|
||||
else:
|
||||
data = open(data, 'rb').read()
|
||||
if not data:
|
||||
raise ValueError('%r is an empty file'%path_or_file)
|
||||
self.load(data)
|
||||
|
||||
@dynamic_property
|
||||
|
9
src/calibre/utils/wmf/__init__.py
Normal file
9
src/calibre/utils/wmf/__init__.py
Normal file
@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
|
||||
|
200
src/calibre/utils/wmf/wmf.c
Normal file
200
src/calibre/utils/wmf/wmf.c
Normal file
@ -0,0 +1,200 @@
|
||||
#define UNICODE
|
||||
#define PY_SSIZE_T_CLEAN
|
||||
#include <Python.h>
|
||||
|
||||
#include <libwmf/api.h>
|
||||
#include <libwmf/svg.h>
|
||||
|
||||
typedef struct {
|
||||
char *data;
|
||||
size_t len;
|
||||
size_t pos;
|
||||
} buf;
|
||||
|
||||
//This code is taken mostly from the Abiword wmf plugin
|
||||
|
||||
|
||||
// returns unsigned char cast to int, or EOF
|
||||
static int wmf_WMF_read(void * context) {
|
||||
char c;
|
||||
buf *info = (buf*)context;
|
||||
|
||||
if (info->pos == info->len)
|
||||
return EOF;
|
||||
|
||||
c = info->data[pos];
|
||||
|
||||
info->pos++;
|
||||
|
||||
return (int)c;
|
||||
}
|
||||
|
||||
// returns (-1) on error, else 0
|
||||
static int wmf_WMF_seek(void * context, long pos) {
|
||||
buf* info = (buf*) context;
|
||||
|
||||
if (pos < 0 || (size_t)pos > info->len) return -1;
|
||||
info->pos = (size_t)pos;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// returns (-1) on error, else pos
|
||||
static long wmf_WMF_tell(void * context) {
|
||||
buf* info = (buf*) context;
|
||||
|
||||
return (long) info->pos;
|
||||
}
|
||||
|
||||
|
||||
#define CLEANUP if(API) { if (stream) wmf_free(API, stream); wmf_api_destroy(API); };
|
||||
|
||||
static PyObject *
|
||||
wmf_render(PyObject *self, PyObject *args) {
|
||||
char *data;
|
||||
Py_ssize_t sz;
|
||||
PyObject *ans;
|
||||
|
||||
unsigned int disp_width = 0;
|
||||
unsigned int disp_height = 0;
|
||||
|
||||
float wmf_width;
|
||||
float wmf_height;
|
||||
float ratio_wmf;
|
||||
float ratio_bounds;
|
||||
|
||||
unsigned long flags;
|
||||
|
||||
unsigned int max_width = 1600;
|
||||
unsigned int max_height = 1200;
|
||||
unsigned long max_flags = 0;
|
||||
|
||||
static const char* Default_Description = "wmf2svg";
|
||||
|
||||
wmf_error_t err;
|
||||
|
||||
wmf_svg_t* ddata = 0;
|
||||
|
||||
wmfAPI* API = 0;
|
||||
wmfD_Rect bbox;
|
||||
|
||||
wmfAPI_Options api_options;
|
||||
|
||||
buf read_info;
|
||||
|
||||
char *stream = NULL;
|
||||
unsigned long stream_len = 0;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "s#", &data, &sz))
|
||||
return NULL;
|
||||
|
||||
flags = WMF_OPT_IGNORE_NONFATAL | WMF_OPT_FUNCTION;
|
||||
api_options.function = wmf_svg_function;
|
||||
|
||||
err = wmf_api_create(&API, flags, &api_options);
|
||||
|
||||
if (err != wmf_E_None) {
|
||||
CLEANUP;
|
||||
return PyErr_NoMemory();
|
||||
}
|
||||
|
||||
read_info.data = data;
|
||||
read_info.len = sz;
|
||||
read_info.pos = 0;
|
||||
|
||||
err = wmf_bbuf_input(API, wmf_WMF_read, wmf_WMF_seek, wmf_WMF_tell, (void *) &read_info);
|
||||
if (err != wmf_E_None) {
|
||||
CLEANUP;
|
||||
PyErr_SetString(PyExc_Exception, "Failed to initialize WMF input");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
err = wmf_scan(API, 0, &(bbox));
|
||||
if (err != wmf_E_None)
|
||||
{
|
||||
CLEANUP;
|
||||
PyErr_SetString(PyExc_ValueError, "Failed to scan the WMF");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Okay, got this far, everything seems cool.
|
||||
*/
|
||||
ddata = WMF_SVG_GetData (API);
|
||||
|
||||
ddata->out = wmf_stream_create(API, NULL);
|
||||
|
||||
ddata->Description = (char *)Default_Description;
|
||||
|
||||
ddata->bbox = bbox;
|
||||
|
||||
wmf_display_size(API, &disp_width, &disp_height, 96, 96);
|
||||
|
||||
wmf_width = (float) disp_width;
|
||||
wmf_height = (float) disp_height;
|
||||
|
||||
if ((wmf_width <= 0) || (wmf_height <= 0)) {
|
||||
CLEANUP;
|
||||
PyErr_SetString(PyExc_ValueError, "Bad WMF image size");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if ((wmf_width > (float) max_width )
|
||||
|| (wmf_height > (float) max_height)) {
|
||||
ratio_wmf = wmf_height / wmf_width;
|
||||
ratio_bounds = (float) max_height / (float) max_width;
|
||||
|
||||
if (ratio_wmf > ratio_bounds) {
|
||||
ddata->height = max_height;
|
||||
ddata->width = (unsigned int) ((float) ddata->height / ratio_wmf);
|
||||
}
|
||||
else {
|
||||
ddata->width = max_width;
|
||||
ddata->height = (unsigned int) ((float) ddata->width * ratio_wmf);
|
||||
}
|
||||
}
|
||||
else {
|
||||
ddata->width = (unsigned int) ceil ((double) wmf_width );
|
||||
ddata->height = (unsigned int) ceil ((double) wmf_height);
|
||||
}
|
||||
|
||||
ddata->flags |= WMF_SVG_INLINE_IMAGES;
|
||||
|
||||
ddata->flags |= WMF_GD_OUTPUT_MEMORY | WMF_GD_OWN_BUFFER;
|
||||
|
||||
err = wmf_play(API, 0, &(bbox));
|
||||
|
||||
if (err != wmf_E_None) {
|
||||
CLEANUP;
|
||||
PyErr_SetString(PyExc_ValueError, "Playing of the WMF file failed");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
wmf_stream_destroy(API, ddata->out, &stream, &stream_len);
|
||||
|
||||
ans = Py_BuildValue("s#", stream, stream_len);
|
||||
|
||||
wmf_free(API, stream);
|
||||
wmf_api_destroy (API);
|
||||
|
||||
return ans;
|
||||
}
|
||||
|
||||
|
||||
static PyMethodDef wmf_methods[] = {
|
||||
{"render", wmf_render, METH_VARARGS,
|
||||
"render(path) -> Render wmf as svg."
|
||||
},
|
||||
|
||||
{NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
|
||||
PyMODINIT_FUNC
|
||||
initwmf(void)
|
||||
{
|
||||
PyObject* m;
|
||||
m = Py_InitModule3("wmf", wmf_methods,
|
||||
"Wrapper for the libwmf library");
|
||||
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user