This commit is contained in:
GRiker 2012-12-28 11:35:53 -07:00
commit 94aaddf772
161 changed files with 33952 additions and 24765 deletions

View File

@ -19,6 +19,46 @@
# new recipes: # new recipes:
# - title: # - title:
- version: 0.9.12
date: 2012-12-28
new features:
- title: "Drivers for Kibano e-reader and Slick ER-700-2"
tickets: [1093570, 1093732]
- title: "Add support for downloading metadata from Amazon Brazil."
tickets: [1092594]
- title: "Copy to library: Allow specifying the destination library by path."
tickets: [1093231]
- title: "When adding empty books, allow setting of the series for the new books. Also select the newly added book records after adding."
- title: "PDF Output: Add a checkbox to override the page size defined by the output profile. This allows you to specify a custom page size even if the output profile is not set to default."
- title: "Add usb ids for newer kindle fire to the linux mtp driver"
bug fixes:
- title: "Linux: Temporarily redirect stdout to get rid of the annoying and pointless message about mtpz during libmtp initialization"
- title: "Fix multiple 'All column' coloring rules not being applied"
tickets: [1093574]
- title: "Use custom icons in the content server as well."
tickets: [1092098]
improved recipes:
- La Voce
- Harpers Magazine (printed edition)
- Pajamas Media
- NSFW corp
- The Hindu
- Nikkei News
new recipes:
- title: Various Ukranian news sources
author: rpalyvoda
- version: 0.9.11 - version: 0.9.11
date: 2012-12-21 date: 2012-12-21
@ -34,6 +74,7 @@
- title: "Allow setting the color of all columns with a single rule in Preferences->Look & Feel->Column Coloring" - title: "Allow setting the color of all columns with a single rule in Preferences->Look & Feel->Column Coloring"
- title: "MOBI: When reading metadata from mobi files, put the contents of the ASIN field into an identifier named mobi-asin. Note that this value is not used when downloading metadata as it is not possible to know which (country specific) amazon website the ASIN comes from." - title: "MOBI: When reading metadata from mobi files, put the contents of the ASIN field into an identifier named mobi-asin. Note that this value is not used when downloading metadata as it is not possible to know which (country specific) amazon website the ASIN comes from."
tickets: [1090394]
bug fixes: bug fixes:
- title: "Windows build: Fix a regression in 0.9.9 that caused calibre to not start on some windows system that were missing the VC.90 dlls (some older XP systems)" - title: "Windows build: Fix a regression in 0.9.9 that caused calibre to not start on some windows system that were missing the VC.90 dlls (some older XP systems)"

View File

@ -49,7 +49,7 @@ All the |app| python code is in the ``calibre`` package. This package contains t
* Metadata reading, writing, and downloading is all in ebooks.metadata * Metadata reading, writing, and downloading is all in ebooks.metadata
* Conversion happens in a pipeline, for the structure of the pipeline, * Conversion happens in a pipeline, for the structure of the pipeline,
see :ref:`conversion-introduction`. The pipeline consists of an input see :ref:`conversion-introduction`. The pipeline consists of an input
plugin, various transforms and an output plugin. The code constructs plugin, various transforms and an output plugin. The that code constructs
and drives the pipeline is in plumber.py. The pipeline works on a and drives the pipeline is in plumber.py. The pipeline works on a
representation of an ebook that is like an unzipped epub, with representation of an ebook that is like an unzipped epub, with
manifest, spine, toc, guide, html content, etc. The manifest, spine, toc, guide, html content, etc. The
@ -74,10 +74,6 @@ After installing Bazaar, you can get the |app| source code with the command::
On Windows you will need the complete path name, that will be something like :file:`C:\\Program Files\\Bazaar\\bzr.exe`. On Windows you will need the complete path name, that will be something like :file:`C:\\Program Files\\Bazaar\\bzr.exe`.
To update a branch to the latest code, use the command::
bzr merge
|app| is a very large project with a very long source control history, so the |app| is a very large project with a very long source control history, so the
above can take a while (10mins to an hour depending on your internet speed). above can take a while (10mins to an hour depending on your internet speed).
@ -88,6 +84,11 @@ using::
bzr branch --stacked lp:calibre bzr branch --stacked lp:calibre
To update a branch to the latest code, use the command::
bzr merge
Submitting your changes to be included Submitting your changes to be included
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -1,18 +1,22 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>' __copyright__ = '2008-2012, Darko Miletic <darko.miletic at gmail.com>'
''' '''
harpers.org - paid subscription/ printed issue articles harpers.org - paid subscription/ printed issue articles
This recipe only get's article's published in text format This recipe only get's article's published in text format
images and pdf's are ignored images and pdf's are ignored
If you have institutional subscription based on access IP you do not need to enter
anything in username/password fields
''' '''
import time
import urllib
from calibre import strftime from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class Harpers_full(BasicNewsRecipe): class Harpers_full(BasicNewsRecipe):
title = "Harper's Magazine - articles from printed edition" title = "Harper's Magazine - articles from printed edition"
__author__ = 'Darko Miletic' __author__ = 'Darko Miletic'
description = "Harper's Magazine: Founded June 1850." description = "Harper's Magazine, the oldest general-interest monthly in America, explores the issues that drive our national conversation, through long-form narrative journalism and essays, and such celebrated features as the iconic Harper's Index."
publisher = "Harpers's" publisher = "Harpers's"
category = 'news, politics, USA' category = 'news, politics, USA'
oldest_article = 30 oldest_article = 30
@ -21,52 +25,69 @@ class Harpers_full(BasicNewsRecipe):
use_embedded_content = False use_embedded_content = False
delay = 1 delay = 1
language = 'en' language = 'en'
needs_subscription = True encoding = 'utf8'
masthead_url = 'http://www.harpers.org/media/image/Harpers_305x100.gif' needs_subscription = 'optional'
publication_type = 'magazine' masthead_url = 'http://harpers.org/wp-content/themes/harpers/images/pheader.gif'
INDEX = strftime('http://www.harpers.org/archive/%Y/%m') publication_type = 'magazine'
LOGIN = 'http://www.harpers.org' INDEX = strftime('http://harpers.org/archive/%Y/%m')
cover_url = strftime('http://www.harpers.org/media/pages/%Y/%m/gif/0001.gif') LOGIN = 'http://harpers.org/wp-content/themes/harpers/ajax_login.php'
extra_css = ' body{font-family: "Georgia",serif} ' extra_css = """
body{font-family: adobe-caslon-pro,serif}
.category{font-size: small}
.articlePost p:first-letter{display: inline; font-size: xx-large; font-weight: bold}
"""
conversion_options = { conversion_options = {
'comment' : description 'comment' : description
, 'tags' : category , 'tags' : category
, 'publisher' : publisher , 'publisher' : publisher
, 'language' : language , 'language' : language
} }
keep_only_tags = [ dict(name='div', attrs={'id':'cached'}) ] keep_only_tags = [ dict(name='div', attrs={'class':['postdetailFull','articlePost']}) ]
remove_tags = [ remove_tags = [
dict(name='table', attrs={'class':['rcnt','rcnt topline']}) dict(name='div', attrs={'class':'fRight rightDivPad'})
,dict(name='link') ,dict(name=['link','meta','object','embed','iframe'])
] ]
remove_attributes=['xmlns'] remove_attributes=['xmlns']
def get_browser(self): def get_browser(self):
br = BasicNewsRecipe.get_browser() br = BasicNewsRecipe.get_browser()
br.open('http://harpers.org/')
if self.username is not None and self.password is not None: if self.username is not None and self.password is not None:
br.open(self.LOGIN) tt = time.localtime()*1000
br.select_form(nr=1) data = urllib.urlencode({ 'm':self.username
br['handle' ] = self.username ,'p':self.password
br['password'] = self.password ,'rt':'http://harpers.org/'
br.submit() ,'tt':tt
})
br.open(self.LOGIN, data)
return br return br
def parse_index(self): def parse_index(self):
articles = [] articles = []
print 'Processing ' + self.INDEX print 'Processing ' + self.INDEX
soup = self.index_to_soup(self.INDEX) soup = self.index_to_soup(self.INDEX)
for item in soup.findAll('div', attrs={'class':'title'}): count = 0
text_link = item.parent.find('img',attrs={'alt':'Text'}) for item in soup.findAll('div', attrs={'class':'articleData'}):
if text_link: text_links = item.findAll('h2')
url = self.LOGIN + item.a['href'] for text_link in text_links:
title = item.a.contents[0] if count == 0:
date = strftime(' %B %Y') lcover_url = item.find(attrs={'class':'dwpdf'})
articles.append({ if lcover_url:
'title' :title self.cover_url = lcover_url.a['href']
,'date' :date count = 1
,'url' :url else:
,'description':'' url = text_link.a['href']
}) title = text_link.a.contents[0]
date = strftime(' %B %Y')
articles.append({
'title' :title
,'date' :date
,'url' :url
,'description':''
})
return [(soup.head.title.string, articles)] return [(soup.head.title.string, articles)]
def print_version(self, url):
return url + '?single=1'

View File

@ -16,10 +16,14 @@ class TheHindu(BasicNewsRecipe):
keep_only_tags = [dict(id='content')] keep_only_tags = [dict(id='content')]
remove_tags = [dict(attrs={'class':['article-links', 'breadcr']}), remove_tags = [dict(attrs={'class':['article-links', 'breadcr']}),
dict(id=['email-section', 'right-column', 'printfooter'])] dict(id=['email-section', 'right-column', 'printfooter', 'topover',
'slidebox', 'th_footer'])]
extra_css = '.photo-caption { font-size: smaller }' extra_css = '.photo-caption { font-size: smaller }'
def preprocess_raw_html(self, raw, url):
return raw.replace('<body><p>', '<p>').replace('</p></body>', '</p>')
def postprocess_html(self, soup, first_fetch): def postprocess_html(self, soup, first_fetch):
for t in soup.findAll(['table', 'tr', 'td','center']): for t in soup.findAll(['table', 'tr', 'td','center']):
t.name = 'div' t.name = 'div'

View File

@ -2,7 +2,7 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__author__ = 'Gabriele Marini, based on Darko Miletic' __author__ = 'Gabriele Marini, based on Darko Miletic'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>' __copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
__description__ = 'La Stampa 05/05/2010' __description__ = 'La Stampa 28/12/2012'
''' '''
http://www.lastampa.it/ http://www.lastampa.it/
@ -14,10 +14,11 @@ class LaStampa(BasicNewsRecipe):
title = u'La Stampa' title = u'La Stampa'
language = 'it' language = 'it'
__author__ = 'Gabriele Marini' __author__ = 'Gabriele Marini'
oldest_article = 15 #oldest_article = 15
oldest_articlce = 7 #for daily schedule
max_articles_per_feed = 50 max_articles_per_feed = 50
recursion = 100 recursion = 100
cover_url = 'http://www.lastampa.it/edicola/PDF/1.pdf' cover_url = 'http://www1.lastampa.it/edicola/PDF/1.pdf'
use_embedded_content = False use_embedded_content = False
remove_javascript = True remove_javascript = True
no_stylesheets = True no_stylesheets = True
@ -33,35 +34,41 @@ class LaStampa(BasicNewsRecipe):
if link: if link:
return link[0]['href'] return link[0]['href']
keep_only_tags = [dict(attrs={'class':['boxocchiello2','titoloRub','titologir','catenaccio','sezione','articologirata']}), keep_only_tags = [dict(attrs={'class':['boxocchiello2','titoloRub','titologir','autore-girata','luogo-girata','catenaccio','sezione','articologirata','bodytext','news-single-img','ls-articoloCorpo','ls-blog-list-1col']}),
dict(name='div', attrs={'id':'corpoarticolo'}) dict(name='div', attrs={'id':'corpoarticolo'})
] ]
remove_tags = [dict(name='div', attrs={'id':'menutop'}),
dict(name='div', attrs={'id':'fwnetblocco'}), remove_tags = [dict(name='div', attrs={'id':['menutop','fwnetblocco']}),
dict(name='table', attrs={'id':'strumenti'}), dict(attrs={'class':['ls-toolbarCommenti','ls-boxCommentsBlog']}),
dict(name='table', attrs={'id':'imgesterna'}), dict(name='table', attrs={'id':['strumenti','imgesterna']}),
dict(name='a', attrs={'class':'linkblu'}), dict(name='a', attrs={'class':['linkblu','link']}),
dict(name='a', attrs={'class':'link'}),
dict(name='span', attrs={'class':['boxocchiello','boxocchiello2','sezione']}) dict(name='span', attrs={'class':['boxocchiello','boxocchiello2','sezione']})
] ]
feeds = [(u'BuonGiorno',u'http://www.lastampa.it/cultura/opinioni/buongiorno/rss.xml'),
feeds = [ (u'Jena', u'http://www.lastampa.it/cultura/opinioni/jena/rss.xml'),
(u'Home', u'http://www.lastampa.it/redazione/rss_home.xml'), (u'Editoriali', u'http://www.lastampa.it/cultura/opinioni/editoriali'),
(u'Editoriali', u'http://www.lastampa.it/cmstp/rubriche/oggetti/rss.asp?ID_blog=25'), (u'Finestra sull America', u'http://lastampa.feedsportal.com/c/32418/f/625713/index.rss'),
(u'Politica', u'http://www.lastampa.it/redazione/cmssezioni/politica/rss_politica.xml'), (u'HomePage', u'http://www.lastampa.it/rss.xml'),
(u'ArciItaliana', u'http://www.lastampa.it/cmstp/rubriche/oggetti/rss.asp?ID_blog=14'), (u'Politica Italia', u'http://www.lastampa.it/italia/politica/rss.xml'),
(u'Cronache', u'http://www.lastampa.it/redazione/cmssezioni/cronache/rss_cronache.xml'), (u'ArciItaliana', u'http://www.lastampa.it/rss/blog/arcitaliana'),
(u'Esteri', u'http://www.lastampa.it/redazione/cmssezioni/esteri/rss_esteri.xml'), (u'Cronache', u'http://www.lastampa.it/italia/cronache/rss.xml'),
(u'Danni Collaterali', u'http://www.lastampa.it/cmstp/rubriche/oggetti/rss.asp?ID_blog=90'), (u'Esteri', u'http://www.lastampa.it/esteri/rss.xml'),
(u'Economia', u'http://www.lastampa.it/redazione/cmssezioni/economia/rss_economia.xml'), (u'Danni Collaterali', u'http://www.lastampa.it/rss/blog/danni-collaterali'),
(u'Tecnologia ', u'http://www.lastampa.it/cmstp/rubriche/oggetti/rss.asp?ID_blog=30'), (u'Economia', u'http://www.lastampa.it/economia/rss.xml'),
(u'Spettacoli', u'http://www.lastampa.it/redazione/cmssezioni/spettacoli/rss_spettacoli.xml'), (u'Tecnologia ', u'http://www.lastampa.it/tecnologia/rss.xml'),
(u'Sport', u'http://www.lastampa.it/sport/rss_home.xml'), (u'Spettacoli', u'http://www.lastampa.it/spettacoli/rss.xml'),
(u'Torino', u'http://rss.feedsportal.com/c/32418/f/466938/index.rss'), (u'Sport', u'http://www.lastampa.it/sport/rss.xml'),
(u'Motori', u'http://www.lastampa.it/cmstp/rubriche/oggetti/rss.asp?ID_blog=57'), (u'Torino', u'http://www.lastampa.it/cronaca/rss.xml'),
(u'Scienza', u'http://www.lastampa.it/cmstp/rubriche/oggetti/rss.asp?ID_blog=38'), (u'Motori', u'http://www.lastampa.it/motori/rss.xml'),
(u'Fotografia', u'http://rss.feedsportal.com/c/32418/f/478449/index.rss'), (u'Scienza', u'http://www.lastampa.it/scienza/rss.xml'),
(u'Scuola', u'http://www.lastampa.it/cmstp/rubriche/oggetti/rss.asp?ID_blog=60'), (u'Cultura', u'http://www.lastampa.it/cultura/rss.xml'),
(u'Tempo Libero', u'http://www.lastampa.it/tempolibero/rss_home.xml') (u'Scuola', u'http://www.lastampa.it/cultura/scuola/rss.xml'),
(u'Benessere', u'http://www.lastampa.it/scienza/benessere/rss.xml'),
(u'Cucina', u'http://www.lastampa.it/societa/cucina/rss.xml'),
(u'Casa', u'http://www.lastampa.it/societa/casa/rss.xml'),
(u'Moda',u'http://www.lastampa.it/societa/moda/rss.xml'),
(u'Giochi',u'http://www.lastampa.it/tecnologia/giochi/rss.xml'),
(u'Viaggi',u'http://www.lastampa.it/societa/viaggi/rss.xml'),
(u'Ambiente', u'http://www.lastampa.it/scienza/ambiente/rss.xml')
] ]

View File

@ -7,9 +7,9 @@ class AdvancedUserRecipe1324114228(BasicNewsRecipe):
max_articles_per_feed = 100 max_articles_per_feed = 100
auto_cleanup = True auto_cleanup = True
masthead_url = 'http://www.lavoce.info/binary/la_voce/testata/lavoce.1184661635.gif' masthead_url = 'http://www.lavoce.info/binary/la_voce/testata/lavoce.1184661635.gif'
feeds = [(u'La Voce', u'http://www.lavoce.info/feed_rss.php?id_feed=1')] feeds = [(u'La Voce', u'http://www.lavoce.info/feed/')]
__author__ = 'faber1971' __author__ = 'faber1971'
description = 'Italian website on Economy - v1.01 (17, December 2011)' description = 'Italian website on Economy - v1.02 (27, December 2012)'
language = 'it' language = 'it'

View File

@ -0,0 +1,12 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1356270446(BasicNewsRecipe):
title = u'\u041b\u044c\u0432\u0456\u0432\u0441\u044c\u043a\u0430 \u0433\u0430\u0437\u0435\u0442\u0430'
__author__ = 'rpalyvoda'
oldest_article = 7
max_articles_per_feed = 100
language = 'uk'
cover_url = 'http://lvivska.com/sites/all/themes/biblos/images/logo.png'
masthead_url = 'http://lvivska.com/sites/all/themes/biblos/images/logo.png'
auto_cleanup = True
feeds = [(u'\u041d\u043e\u0432\u0438\u043d\u0438', u'http://lvivska.com/rss/news.xml'), (u'\u041f\u043e\u043b\u0456\u0442\u0438\u043a\u0430', u'http://lvivska.com/rss/politic.xml'), (u'\u0415\u043a\u043e\u043d\u043e\u043c\u0456\u043a\u0430', u'http://lvivska.com/rss/economic.xml'), (u'\u041f\u0440\u0430\u0432\u043e', u'http://lvivska.com/rss/law.xml'), (u'\u0421\u0432\u0456\u0442', u'http://lvivska.com/rss/world.xml'), (u'\u0416\u0438\u0442\u0442\u044f', u'http://lvivska.com/rss/life.xml'), (u'\u041a\u0443\u043b\u044c\u0442\u0443\u0440\u0430', u'http://lvivska.com/rss/culture.xml'), (u'\u041b\u0430\u0441\u0443\u043d', u'http://lvivska.com/rss/cooking.xml'), (u'\u0421\u0442\u0438\u043b\u044c', u'http://lvivska.com/rss/style.xml'), (u'Galicia Incognita', u'http://lvivska.com/rss/galiciaincognita.xml'), (u'\u0421\u043f\u043e\u0440\u0442', u'http://lvivska.com/rss/sport.xml'), (u'\u0415\u043a\u043e\u043b\u043e\u0433\u0456\u044f', u'http://lvivska.com/rss/ecology.xml'), (u"\u0417\u0434\u043e\u0440\u043e\u0432'\u044f", u'http://lvivska.com/rss/health.xml'), (u'\u0410\u0432\u0442\u043e', u'http://lvivska.com/rss/auto.xml'), (u'\u0411\u043b\u043e\u0433\u0438', u'http://lvivska.com/rss/blog.xml')]

View File

@ -13,8 +13,11 @@ class NikkeiNet_paper_subscription(BasicNewsRecipe):
max_articles_per_feed = 30 max_articles_per_feed = 30
language = 'ja' language = 'ja'
no_stylesheets = True no_stylesheets = True
cover_url = 'http://parts.nikkei.com/parts/ds/images/common/logo_r1.svg' #cover_url = 'http://parts.nikkei.com/parts/ds/images/common/logo_r1.svg'
masthead_url = 'http://parts.nikkei.com/parts/ds/images/common/logo_r1.svg' cover_url = 'http://cdn.nikkei.co.jp/parts/ds/images/common/st_nikkei_r1_20101003_1.gif'
#masthead_url = 'http://parts.nikkei.com/parts/ds/images/common/logo_r1.svg'
masthead_url = 'http://cdn.nikkei.co.jp/parts/ds/images/common/st_nikkei_r1_20101003_1.gif'
cover_margins = (10, 188, '#ffffff')
remove_tags_before = {'class':"cmn-indent"} remove_tags_before = {'class':"cmn-indent"}
remove_tags = [ remove_tags = [
@ -40,8 +43,11 @@ class NikkeiNet_paper_subscription(BasicNewsRecipe):
print "-------------------------open top page-------------------------------------" print "-------------------------open top page-------------------------------------"
br.open('http://www.nikkei.com/') br.open('http://www.nikkei.com/')
print "-------------------------open first login form-----------------------------" print "-------------------------open first login form-----------------------------"
link = br.links(url_regex="www.nikkei.com/etc/accounts/login").next() try:
br.follow_link(link) url = br.links(url_regex="www.nikkei.com/etc/accounts/login").next().url
except StopIteration:
url = 'http://www.nikkei.com/etc/accounts/login?dps=3&pageflag=top&url=http%3A%2F%2Fwww.nikkei.com%2F'
br.open(url) #br.follow_link(link)
#response = br.response() #response = br.response()
#print response.get_data() #print response.get_data()
print "-------------------------JS redirect(send autoPostForm)--------------------" print "-------------------------JS redirect(send autoPostForm)--------------------"

View File

@ -6,7 +6,6 @@ www.nsfwcorp.com
''' '''
import urllib import urllib
from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class NotSafeForWork(BasicNewsRecipe): class NotSafeForWork(BasicNewsRecipe):
@ -21,8 +20,9 @@ class NotSafeForWork(BasicNewsRecipe):
needs_subscription = True needs_subscription = True
auto_cleanup = False auto_cleanup = False
INDEX = 'https://www.nsfwcorp.com' INDEX = 'https://www.nsfwcorp.com'
LOGIN = INDEX + '/login' LOGIN = INDEX + '/login/target/'
use_embedded_content = False SETTINGS = INDEX + '/settings/'
use_embedded_content = True
language = 'en' language = 'en'
publication_type = 'magazine' publication_type = 'magazine'
masthead_url = 'http://assets.nsfwcorp.com/media/headers/nsfw_banner.jpg' masthead_url = 'http://assets.nsfwcorp.com/media/headers/nsfw_banner.jpg'
@ -46,15 +46,6 @@ class NotSafeForWork(BasicNewsRecipe):
, 'language' : language , 'language' : language
} }
remove_tags_before = dict(attrs={'id':'fromToLine'})
remove_tags_after = dict(attrs={'id':'unlockButtonDiv'})
remove_tags=[
dict(name=['meta', 'link', 'iframe', 'embed', 'object'])
,dict(name='a', attrs={'class':'switchToDeskNotes'})
,dict(attrs={'id':'unlockButtonDiv'})
]
remove_attributes = ['lang']
def get_browser(self): def get_browser(self):
br = BasicNewsRecipe.get_browser() br = BasicNewsRecipe.get_browser()
br.open(self.LOGIN) br.open(self.LOGIN)
@ -65,30 +56,12 @@ class NotSafeForWork(BasicNewsRecipe):
br.open(self.LOGIN, data) br.open(self.LOGIN, data)
return br return br
def parse_index(self): def get_feeds(self):
articles = [] self.feeds = []
soup = self.index_to_soup(self.INDEX) soup = self.index_to_soup(self.SETTINGS)
dispatches = soup.find(attrs={'id':'dispatches'}) for item in soup.findAll('input', attrs={'type':'text'}):
if dispatches: if item.has_key('value') and item['value'].startswith('http://www.nsfwcorp.com/feed/'):
for item in dispatches.findAll('h3'): self.feeds.append(item['value'])
description = u'' return self.feeds
title_link = item.find('span', attrs={'class':'dispatchTitle'}) return self.feeds
description_link = item.find('span', attrs={'class':'dispatchSubtitle'})
feed_link = item.find('a', href=True)
if feed_link:
url = self.INDEX + feed_link['href']
title = self.tag_to_string(title_link)
description = self.tag_to_string(description_link)
date = strftime(self.timefmt)
articles.append({
'title' :title
,'date' :date
,'url' :url
,'description':description
})
return [('Dispatches', articles)]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return soup

View File

@ -1,27 +1,27 @@
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import BeautifulSoup
class PajamasMedia(BasicNewsRecipe): class PajamasMedia(BasicNewsRecipe):
title = u'Pajamas Media' title = u'Pajamas Media'
description = u'Provides exclusive news and opinion for forty countries.' description = u'Provides exclusive news and opinion for forty countries.'
language = 'en' language = 'en'
__author__ = 'Krittika Goyal' __author__ = 'Krittika Goyal'
oldest_article = 1 #days oldest_article = 2 #days
max_articles_per_feed = 25 max_articles_per_feed = 25
recursions = 1 recursions = 1
match_regexps = [r'http://pajamasmedia.com/blog/.*/2/$'] match_regexps = [r'http://pajamasmedia.com/blog/.*/2/$']
#encoding = 'latin1' #encoding = 'latin1'
remove_stylesheets = True remove_stylesheets = True
#remove_tags_before = dict(name='h1', attrs={'class':'heading'}) auto_cleanup = True
remove_tags_after = dict(name='div', attrs={'class':'paged-nav'}) ##remove_tags_before = dict(name='h1', attrs={'class':'heading'})
remove_tags = [ #remove_tags_after = dict(name='div', attrs={'class':'paged-nav'})
dict(name='iframe'), #remove_tags = [
dict(name='div', attrs={'class':['pages']}), #dict(name='iframe'),
#dict(name='div', attrs={'id':['bookmark']}), #dict(name='div', attrs={'class':['pages']}),
#dict(name='span', attrs={'class':['related_link', 'slideshowcontrols']}), ##dict(name='div', attrs={'id':['bookmark']}),
#dict(name='ul', attrs={'class':'articleTools'}), ##dict(name='span', attrs={'class':['related_link', 'slideshowcontrols']}),
] ##dict(name='ul', attrs={'class':'articleTools'}),
#]
feeds = [ feeds = [
('pajamas Media', ('pajamas Media',
@ -29,20 +29,20 @@ class PajamasMedia(BasicNewsRecipe):
] ]
def preprocess_html(self, soup): #def preprocess_html(self, soup):
story = soup.find(name='div', attrs={'id':'innerpage-content'}) #story = soup.find(name='div', attrs={'id':'innerpage-content'})
#td = heading.findParent(name='td') ##td = heading.findParent(name='td')
#td.extract() ##td.extract()
soup = BeautifulSoup('<html><head><title>t</title></head><body></body></html>') #soup = BeautifulSoup('<html><head><title>t</title></head><body></body></html>')
body = soup.find(name='body') #body = soup.find(name='body')
body.insert(0, story) #body.insert(0, story)
return soup #return soup
def postprocess_html(self, soup, first): #def postprocess_html(self, soup, first):
if not first: #if not first:
h = soup.find(attrs={'class':'innerpage-header'}) #h = soup.find(attrs={'class':'innerpage-header'})
if h: h.extract() #if h: h.extract()
auth = soup.find(attrs={'class':'author'}) #auth = soup.find(attrs={'class':'author'})
if auth: auth.extract() #if auth: auth.extract()
return soup #return soup

View File

@ -0,0 +1,13 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1356283265(BasicNewsRecipe):
title = u'\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0438\u0439 \u0422\u0438\u0436\u0434\u0435\u043d\u044c'
__author__ = 'rpalyvoda'
oldest_article = 7
max_articles_per_feed = 100
language = 'uk'
cover_url = 'http://tyzhden.ua/Images/Style1/tyzhden.ua-logo2.gif'
masthead_url = 'http://tyzhden.ua/Images/Style1/tyzhden.ua-logo2.gif'
auto_cleanup = True
feeds = [(u'\u041d\u043e\u0432\u0438\u043d\u0438', u'http://tyzhden.ua/RSS/News/'), (u'\u041e\u0440\u0438\u0433\u0456\u043d\u0430\u043b\u044c\u043d\u0456 \u043d\u043e\u0432\u0438\u043d\u0438', u'http://tyzhden.ua/RSS/News.Original/'), (u'\u041f\u0443\u0431\u043b\u0456\u043a\u0430\u0446\u0456\u0457', u'http://tyzhden.ua/RSS/Publications/')]

13
recipes/zaxid_net.recipe Normal file
View File

@ -0,0 +1,13 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1356281741(BasicNewsRecipe):
title = u'Zaxid.net'
__author__ = 'rpalyvoda'
oldest_article = 7
max_articles_per_feed = 100
language = 'uk'
cover_url = 'http://upload.wikimedia.org/wikipedia/uk/b/bc/Zaxid-net.jpg'
masthead_url = 'http://upload.wikimedia.org/wikipedia/uk/b/bc/Zaxid-net.jpg'
auto_cleanup = True
feeds = [(u'\u0422\u043e\u043f \u043d\u043e\u0432\u0438\u043d\u0438', u'http://feeds.feedburner.com/zaxid/topNews'), (u'\u0421\u0442\u0440\u0456\u0447\u043a\u0430 \u043d\u043e\u0432\u0438\u043d', u'http://feeds.feedburner.com/zaxid/AllNews'), (u'\u041d\u043e\u0432\u0438\u043d\u0438 \u041b\u044c\u0432\u043e\u0432\u0430', u'http://feeds.feedburner.com/zaxid/Lviv'), (u'\u041d\u043e\u0432\u0438\u043d\u0438 \u0423\u043a\u0440\u0430\u0457\u043d\u0438', u'http://feeds.feedburner.com/zaxid/Ukraine'), (u'\u041d\u043e\u0432\u0438\u043d\u0438 \u0441\u0432\u0456\u0442\u0443', u'http://feeds.feedburner.com/zaxid/World'), (u'\u041d\u043e\u0432\u0438\u043d\u0438 - \u0420\u0430\u0434\u0456\u043e 24', u'\u0420\u0430\u0434\u0456\u043e 24'), (u'\u0411\u043b\u043e\u0433\u0438', u'http://feeds.feedburner.com/zaxid/Blogs'), (u"\u041f\u0443\u0431\u043b\u0456\u043a\u0430\u0446\u0456\u0457 - \u0406\u043d\u0442\u0435\u0440\u0432'\u044e", u'http://feeds.feedburner.com/zaxid/Interview'), (u'\u041f\u0443\u0431\u043b\u0456\u043a\u0430\u0446\u0456\u0457 - \u0421\u0442\u0430\u0442\u0442\u0456', u'http://feeds.feedburner.com/zaxid/Articles'), (u'\u0410\u0444\u0456\u0448\u0430', u'http://zaxid.net/rss/subcategory/140.xml'), (u'\u0413\u0430\u043b\u0438\u0447\u0438\u043d\u0430', u'http://feeds.feedburner.com/zaxid/Galicia'), (u'\u041a\u0443\u043b\u044c\u0442\u0443\u0440\u0430.NET', u'http://feeds.feedburner.com/zaxid/KulturaNET'), (u"\u043d\u0435\u0412\u0456\u0434\u043e\u043c\u0456 \u043b\u044c\u0432\u0456\u0432'\u044f\u043d\u0438", u'http://feeds.feedburner.com/zaxid/UnknownLviv'), (u'\u041b\u0435\u043e\u043f\u043e\u043b\u0456\u0441 MULTIPLEX', u'http://feeds.feedburner.com/zaxid/LeopolisMULTIPLEX'), (u'\u0411\u0438\u0442\u0432\u0430 \u0437\u0430 \u043c\u043e\u0432\u0443', u'http://zaxid.net/rss/subcategory/138.xml'), (u'\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u0430 \u0441\u0445\u0435\u043c\u0430 \u041b\u044c\u0432\u043e\u0432\u0430', u'http://zaxid.net/rss/subcategory/132.xml'), (u'\u0414\u0435\u043c\u0456\u0444\u043e\u043b\u043e\u0433\u0456\u0437\u0430\u0446\u0456\u044f', u'http://zaxid.net/rss/subcategory/130.xml'), (u"\u041c\u0438 \u043f\u0430\u043c'\u044f\u0442\u0430\u0454\u043c\u043e", u'http://feeds.feedburner.com/zaxid/WeRemember'), (u'20 \u0440\u043e\u043a\u0456\u0432 \u041d\u0435\u0437\u0430\u043b\u0435\u0436\u043d\u043e\u0441\u0442\u0456', u'http://zaxid.net/rss/subcategory/129.xml'), (u'\u041f\u0440\u0430\u0432\u043e \u043d\u0430 \u0434\u0438\u0442\u0438\u043d\u0441\u0442\u0432\u043e', u'http://feeds.feedburner.com/zaxid/Childhood'), (u'\u0410\u043d\u043e\u043d\u0441\u0438', u'http://feeds.feedburner.com/zaxid/Announcements')]

Binary file not shown.

View File

@ -81,6 +81,7 @@ body {
background-color: #39a9cf; background-color: #39a9cf;
-moz-border-radius: 5px; -moz-border-radius: 5px;
-webkit-border-radius: 5px; -webkit-border-radius: 5px;
border-radius: 5px;
text-shadow: #27211b 1px 1px 1px; text-shadow: #27211b 1px 1px 1px;
-moz-box-shadow: 5px 5px 5px #222; -moz-box-shadow: 5px 5px 5px #222;
-webkit-box-shadow: 5px 5px 5px #222; -webkit-box-shadow: 5px 5px 5px #222;

View File

@ -12,6 +12,7 @@ let g:syntastic_cpp_include_dirs = [
\'/usr/include/fontconfig', \'/usr/include/fontconfig',
\'src/qtcurve/common', 'src/qtcurve', \'src/qtcurve/common', 'src/qtcurve',
\'src/unrar', \'src/unrar',
\'src/qt-harfbuzz/src',
\'/usr/include/ImageMagick', \'/usr/include/ImageMagick',
\] \]
let g:syntastic_c_include_dirs = g:syntastic_cpp_include_dirs let g:syntastic_c_include_dirs = g:syntastic_cpp_include_dirs

View File

@ -6,12 +6,13 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, socket, struct, subprocess, sys, glob import os, socket, struct, subprocess, glob
from distutils.spawn import find_executable from distutils.spawn import find_executable
from PyQt4 import pyqtconfig from PyQt4 import pyqtconfig
from setup import isosx, iswindows, islinux, is64bit from setup import isosx, iswindows, islinux, is64bit
is64bit
OSX_SDK = '/Developer/SDKs/MacOSX10.5.sdk' OSX_SDK = '/Developer/SDKs/MacOSX10.5.sdk'
@ -81,6 +82,7 @@ def consolidate(envvar, default):
pyqt = pyqtconfig.Configuration() pyqt = pyqtconfig.Configuration()
qt_inc = pyqt.qt_inc_dir qt_inc = pyqt.qt_inc_dir
qt_private_inc = []
qt_lib = pyqt.qt_lib_dir qt_lib = pyqt.qt_lib_dir
ft_lib_dirs = [] ft_lib_dirs = []
ft_libs = [] ft_libs = []
@ -140,6 +142,8 @@ elif isosx:
png_libs = ['png12'] png_libs = ['png12']
ft_libs = ['freetype'] ft_libs = ['freetype']
ft_inc_dirs = ['/sw/include/freetype2'] ft_inc_dirs = ['/sw/include/freetype2']
bq = glob.glob('/sw/build/qt-*/include')[-1]
qt_private_inc = ['%s/%s'%(bq, m) for m in ('QtGui', 'QtCore')]
else: else:
# Include directories # Include directories
png_inc_dirs = pkgconfig_include_dirs('libpng', 'PNG_INC_DIR', png_inc_dirs = pkgconfig_include_dirs('libpng', 'PNG_INC_DIR',

View File

@ -102,7 +102,8 @@ class Check(Command):
errors = True errors = True
if errors: if errors:
cPickle.dump(cache, open(self.CACHE, 'wb'), -1) cPickle.dump(cache, open(self.CACHE, 'wb'), -1)
subprocess.call(['gvim', '-f', f]) subprocess.call(['gvim', '-S',
self.j(self.SRC, '../session.vim'), '-f', f])
raise SystemExit(1) raise SystemExit(1)
cache[f] = mtime cache[f] = mtime
for x in builtins: for x in builtins:

View File

@ -18,7 +18,7 @@ from setup.build_environment import (chmlib_inc_dirs,
msvc, MT, win_inc, win_lib, win_ddk, magick_inc_dirs, magick_lib_dirs, msvc, MT, win_inc, win_lib, win_ddk, magick_inc_dirs, magick_lib_dirs,
magick_libs, chmlib_lib_dirs, sqlite_inc_dirs, icu_inc_dirs, magick_libs, chmlib_lib_dirs, sqlite_inc_dirs, icu_inc_dirs,
icu_lib_dirs, win_ddk_lib_dirs, ft_libs, ft_lib_dirs, ft_inc_dirs, icu_lib_dirs, win_ddk_lib_dirs, ft_libs, ft_lib_dirs, ft_inc_dirs,
zlib_libs, zlib_lib_dirs, zlib_inc_dirs, is64bit) zlib_libs, zlib_lib_dirs, zlib_inc_dirs, is64bit, qt_private_inc)
MT MT
isunix = islinux or isosx or isbsd isunix = islinux or isosx or isbsd
@ -183,6 +183,13 @@ extensions = [
sip_files = ['calibre/gui2/progress_indicator/QProgressIndicator.sip'] sip_files = ['calibre/gui2/progress_indicator/QProgressIndicator.sip']
), ),
Extension('qt_hack',
['calibre/ebooks/pdf/render/qt_hack.cpp'],
inc_dirs = qt_private_inc + ['calibre/ebooks/pdf/render', 'qt-harfbuzz/src'],
headers = ['calibre/ebooks/pdf/render/qt_hack.h'],
sip_files = ['calibre/ebooks/pdf/render/qt_hack.sip']
),
Extension('unrar', Extension('unrar',
['unrar/%s.cpp'%(x.partition('.')[0]) for x in ''' ['unrar/%s.cpp'%(x.partition('.')[0]) for x in '''
rar.o strlist.o strfn.o pathfn.o savepos.o smallfn.o global.o file.o rar.o strlist.o strfn.o pathfn.o savepos.o smallfn.o global.o file.o
@ -545,6 +552,9 @@ class Build(Command):
VERSION = 1.0.0 VERSION = 1.0.0
CONFIG += %s CONFIG += %s
''')%(ext.name, ' '.join(ext.headers), ' '.join(ext.sources), archs) ''')%(ext.name, ' '.join(ext.headers), ' '.join(ext.sources), archs)
if ext.inc_dirs:
idir = ' '.join(ext.inc_dirs)
pro += 'INCLUDEPATH = %s\n'%idir
pro = pro.replace('\\', '\\\\') pro = pro.replace('\\', '\\\\')
open(ext.name+'.pro', 'wb').write(pro) open(ext.name+'.pro', 'wb').write(pro)
qmc = [QMAKE, '-o', 'Makefile'] qmc = [QMAKE, '-o', 'Makefile']

View File

@ -12,14 +12,14 @@ msgstr ""
"Report-Msgid-Bugs-To: Debian iso-codes team <pkg-isocodes-" "Report-Msgid-Bugs-To: Debian iso-codes team <pkg-isocodes-"
"devel@lists.alioth.debian.org>\n" "devel@lists.alioth.debian.org>\n"
"POT-Creation-Date: 2011-11-25 14:01+0000\n" "POT-Creation-Date: 2011-11-25 14:01+0000\n"
"PO-Revision-Date: 2012-12-16 13:27+0000\n" "PO-Revision-Date: 2012-12-22 17:18+0000\n"
"Last-Translator: Ferran Rius <frius64@hotmail.com>\n" "Last-Translator: Ferran Rius <frius64@hotmail.com>\n"
"Language-Team: Catalan <linux@softcatala.org>\n" "Language-Team: Catalan <linux@softcatala.org>\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-12-17 04:42+0000\n" "X-Launchpad-Export-Date: 2012-12-23 04:38+0000\n"
"X-Generator: Launchpad (build 16372)\n" "X-Generator: Launchpad (build 16378)\n"
"Language: ca\n" "Language: ca\n"
#. name for aaa #. name for aaa
@ -19044,19 +19044,19 @@ msgstr "Nshi"
#. name for nsd #. name for nsd
msgid "Nisu; Southern" msgid "Nisu; Southern"
msgstr "" msgstr "Nisu; Meridional"
#. name for nse #. name for nse
msgid "Nsenga" msgid "Nsenga"
msgstr "" msgstr "Nsenga"
#. name for nsg #. name for nsg
msgid "Ngasa" msgid "Ngasa"
msgstr "" msgstr "Ngasa"
#. name for nsh #. name for nsh
msgid "Ngoshie" msgid "Ngoshie"
msgstr "" msgstr "Ngishe"
#. name for nsi #. name for nsi
msgid "Nigerian Sign Language" msgid "Nigerian Sign Language"
@ -19064,7 +19064,7 @@ msgstr "Llenguatge de signes nigerià"
#. name for nsk #. name for nsk
msgid "Naskapi" msgid "Naskapi"
msgstr "" msgstr "Naskapi"
#. name for nsl #. name for nsl
msgid "Norwegian Sign Language" msgid "Norwegian Sign Language"
@ -19076,7 +19076,7 @@ msgstr "Naga; Sumi"
#. name for nsn #. name for nsn
msgid "Nehan" msgid "Nehan"
msgstr "" msgstr "Nehan"
#. name for nso #. name for nso
msgid "Sotho; Northern" msgid "Sotho; Northern"
@ -19096,7 +19096,7 @@ msgstr "Llenguatge de signes marítim"
#. name for nss #. name for nss
msgid "Nali" msgid "Nali"
msgstr "" msgstr "Nali"
#. name for nst #. name for nst
msgid "Naga; Tase" msgid "Naga; Tase"
@ -19108,15 +19108,15 @@ msgstr "Nàhuatl; Sierra Negra"
#. name for nsv #. name for nsv
msgid "Nisu; Southwestern" msgid "Nisu; Southwestern"
msgstr "" msgstr "Nisu; Sudoccidental"
#. name for nsw #. name for nsw
msgid "Navut" msgid "Navut"
msgstr "" msgstr "Navut"
#. name for nsx #. name for nsx
msgid "Nsongo" msgid "Nsongo"
msgstr "" msgstr "Nsongo"
#. name for nsy #. name for nsy
msgid "Nasal" msgid "Nasal"
@ -19124,19 +19124,19 @@ msgstr ""
#. name for nsz #. name for nsz
msgid "Nisenan" msgid "Nisenan"
msgstr "" msgstr "Nisenan"
#. name for nte #. name for nte
msgid "Nathembo" msgid "Nathembo"
msgstr "" msgstr "Nathembo"
#. name for nti #. name for nti
msgid "Natioro" msgid "Natioro"
msgstr "" msgstr "Natioro"
#. name for ntj #. name for ntj
msgid "Ngaanyatjarra" msgid "Ngaanyatjarra"
msgstr "" msgstr "Ngaanyatjarra"
#. name for ntk #. name for ntk
msgid "Ikoma-Nata-Isenye" msgid "Ikoma-Nata-Isenye"
@ -19144,11 +19144,11 @@ msgstr ""
#. name for ntm #. name for ntm
msgid "Nateni" msgid "Nateni"
msgstr "" msgstr "Nateni"
#. name for nto #. name for nto
msgid "Ntomba" msgid "Ntomba"
msgstr "" msgstr "Ntomba"
#. name for ntp #. name for ntp
msgid "Tepehuan; Northern" msgid "Tepehuan; Northern"
@ -19160,15 +19160,15 @@ msgstr ""
#. name for nts #. name for nts
msgid "Natagaimas" msgid "Natagaimas"
msgstr "" msgstr "Natagaimas"
#. name for ntu #. name for ntu
msgid "Natügu" msgid "Natügu"
msgstr "" msgstr "Santa Cruz: Septentrional"
#. name for ntw #. name for ntw
msgid "Nottoway" msgid "Nottoway"
msgstr "" msgstr "Nottoway"
#. name for nty #. name for nty
msgid "Mantsi" msgid "Mantsi"
@ -19176,7 +19176,7 @@ msgstr ""
#. name for ntz #. name for ntz
msgid "Natanzi" msgid "Natanzi"
msgstr "" msgstr "Natanzi"
#. name for nua #. name for nua
msgid "Yuaga" msgid "Yuaga"
@ -19184,35 +19184,35 @@ msgstr ""
#. name for nuc #. name for nuc
msgid "Nukuini" msgid "Nukuini"
msgstr "" msgstr "Nukini"
#. name for nud #. name for nud
msgid "Ngala" msgid "Ngala"
msgstr "" msgstr "Ngala"
#. name for nue #. name for nue
msgid "Ngundu" msgid "Ngundu"
msgstr "" msgstr "Ngundu"
#. name for nuf #. name for nuf
msgid "Nusu" msgid "Nusu"
msgstr "" msgstr "Nusu"
#. name for nug #. name for nug
msgid "Nungali" msgid "Nungali"
msgstr "" msgstr "Nungali"
#. name for nuh #. name for nuh
msgid "Ndunda" msgid "Ndunda"
msgstr "" msgstr "Ndunda"
#. name for nui #. name for nui
msgid "Ngumbi" msgid "Ngumbi"
msgstr "" msgstr "Ngumbi"
#. name for nuj #. name for nuj
msgid "Nyole" msgid "Nyole"
msgstr "" msgstr "Nyole"
#. name for nuk #. name for nuk
msgid "Nuu-chah-nulth" msgid "Nuu-chah-nulth"
@ -19220,11 +19220,11 @@ msgstr ""
#. name for nul #. name for nul
msgid "Nusa Laut" msgid "Nusa Laut"
msgstr "" msgstr "Nusa Laut"
#. name for num #. name for num
msgid "Niuafo'ou" msgid "Niuafo'ou"
msgstr "" msgstr "Niuafo'ou"
#. name for nun #. name for nun
msgid "Anong" msgid "Anong"
@ -19232,31 +19232,31 @@ msgstr ""
#. name for nuo #. name for nuo
msgid "Nguôn" msgid "Nguôn"
msgstr "" msgstr "Nguon"
#. name for nup #. name for nup
msgid "Nupe-Nupe-Tako" msgid "Nupe-Nupe-Tako"
msgstr "" msgstr "Nupe"
#. name for nuq #. name for nuq
msgid "Nukumanu" msgid "Nukumanu"
msgstr "" msgstr "Nukumanu"
#. name for nur #. name for nur
msgid "Nukuria" msgid "Nukuria"
msgstr "" msgstr "Nuguria"
#. name for nus #. name for nus
msgid "Nuer" msgid "Nuer"
msgstr "" msgstr "Nuer"
#. name for nut #. name for nut
msgid "Nung (Viet Nam)" msgid "Nung (Viet Nam)"
msgstr "" msgstr "Nung (VietNam)"
#. name for nuu #. name for nuu
msgid "Ngbundu" msgid "Ngbundu"
msgstr "" msgstr "Ngbundu"
#. name for nuv #. name for nuv
msgid "Nuni; Northern" msgid "Nuni; Northern"
@ -19264,7 +19264,7 @@ msgstr "Nuni; Septentrional"
#. name for nuw #. name for nuw
msgid "Nguluwan" msgid "Nguluwan"
msgstr "" msgstr "Nguluwà"
#. name for nux #. name for nux
msgid "Mehek" msgid "Mehek"
@ -19272,7 +19272,7 @@ msgstr ""
#. name for nuy #. name for nuy
msgid "Nunggubuyu" msgid "Nunggubuyu"
msgstr "" msgstr "Nunggubuyu"
#. name for nuz #. name for nuz
msgid "Nahuatl; Tlamacazapa" msgid "Nahuatl; Tlamacazapa"
@ -19280,27 +19280,27 @@ msgstr "Nàhuatl; Tlamacazapa"
#. name for nvh #. name for nvh
msgid "Nasarian" msgid "Nasarian"
msgstr "" msgstr "Nasarià"
#. name for nvm #. name for nvm
msgid "Namiae" msgid "Namiae"
msgstr "" msgstr "Namiae"
#. name for nwa #. name for nwa
msgid "Nawathinehena" msgid "Nawathinehena"
msgstr "" msgstr "Nawathinahana"
#. name for nwb #. name for nwb
msgid "Nyabwa" msgid "Nyabwa"
msgstr "" msgstr "Nyabwa-Nyédébwa"
#. name for nwc #. name for nwc
msgid "Newari; Old" msgid "Newari; Old"
msgstr "" msgstr "Newar; Antic"
#. name for nwe #. name for nwe
msgid "Ngwe" msgid "Ngwe"
msgstr "" msgstr "Ngwe"
#. name for nwi #. name for nwi
msgid "Tanna; Southwest" msgid "Tanna; Southwest"
@ -19308,23 +19308,23 @@ msgstr ""
#. name for nwm #. name for nwm
msgid "Nyamusa-Molo" msgid "Nyamusa-Molo"
msgstr "" msgstr "Nyamusa-Molo"
#. name for nwr #. name for nwr
msgid "Nawaru" msgid "Nawaru"
msgstr "" msgstr "Nawaru"
#. name for nwx #. name for nwx
msgid "Newar; Middle" msgid "Newar; Middle"
msgstr "" msgstr "Newar; Mitjà"
#. name for nwy #. name for nwy
msgid "Nottoway-Meherrin" msgid "Nottoway-Meherrin"
msgstr "" msgstr "Nottoway"
#. name for nxa #. name for nxa
msgid "Nauete" msgid "Nauete"
msgstr "" msgstr "Naueti"
#. name for nxd #. name for nxd
msgid "Ngando (Democratic Republic of Congo)" msgid "Ngando (Democratic Republic of Congo)"
@ -19332,7 +19332,7 @@ msgstr "Ngando (República Democràtica del Congo)"
#. name for nxe #. name for nxe
msgid "Nage" msgid "Nage"
msgstr "" msgstr "Nage"
#. name for nxg #. name for nxg
msgid "Ngad'a" msgid "Ngad'a"
@ -19340,7 +19340,7 @@ msgstr "Ngada; Central"
#. name for nxi #. name for nxi
msgid "Nindi" msgid "Nindi"
msgstr "" msgstr "Nindi"
#. name for nxl #. name for nxl
msgid "Nuaulu; South" msgid "Nuaulu; South"
@ -19348,39 +19348,39 @@ msgstr "Nuaulu; Meridional"
#. name for nxm #. name for nxm
msgid "Numidian" msgid "Numidian"
msgstr "" msgstr "Líbic"
#. name for nxn #. name for nxn
msgid "Ngawun" msgid "Ngawun"
msgstr "" msgstr "Ngawun"
#. name for nxq #. name for nxq
msgid "Naxi" msgid "Naxi"
msgstr "" msgstr "Naxi"
#. name for nxr #. name for nxr
msgid "Ninggerum" msgid "Ninggerum"
msgstr "" msgstr "Ninggirum"
#. name for nxu #. name for nxu
msgid "Narau" msgid "Narau"
msgstr "" msgstr "Narau"
#. name for nxx #. name for nxx
msgid "Nafri" msgid "Nafri"
msgstr "" msgstr "Nafri"
#. name for nya #. name for nya
msgid "Nyanja" msgid "Nyanja"
msgstr "" msgstr "Nyanja"
#. name for nyb #. name for nyb
msgid "Nyangbo" msgid "Nyangbo"
msgstr "" msgstr "Nyangbo"
#. name for nyc #. name for nyc
msgid "Nyanga-li" msgid "Nyanga-li"
msgstr "" msgstr "Nyanga-li"
#. name for nyd #. name for nyd
msgid "Nyore" msgid "Nyore"
@ -19388,7 +19388,7 @@ msgstr ""
#. name for nye #. name for nye
msgid "Nyengo" msgid "Nyengo"
msgstr "" msgstr "Nyengo"
#. name for nyf #. name for nyf
msgid "Giryama" msgid "Giryama"
@ -19396,11 +19396,11 @@ msgstr ""
#. name for nyg #. name for nyg
msgid "Nyindu" msgid "Nyindu"
msgstr "" msgstr "Nyindu"
#. name for nyh #. name for nyh
msgid "Nyigina" msgid "Nyigina"
msgstr "" msgstr "Nyigina"
#. name for nyi #. name for nyi
msgid "Ama (Sudan)" msgid "Ama (Sudan)"
@ -19408,35 +19408,35 @@ msgstr ""
#. name for nyj #. name for nyj
msgid "Nyanga" msgid "Nyanga"
msgstr "" msgstr "Nyanga"
#. name for nyk #. name for nyk
msgid "Nyaneka" msgid "Nyaneka"
msgstr "" msgstr "Nyaneka"
#. name for nyl #. name for nyl
msgid "Nyeu" msgid "Nyeu"
msgstr "" msgstr "Nyeu"
#. name for nym #. name for nym
msgid "Nyamwezi" msgid "Nyamwezi"
msgstr "" msgstr "Nyamwesi"
#. name for nyn #. name for nyn
msgid "Nyankole" msgid "Nyankole"
msgstr "" msgstr "Nyankore"
#. name for nyo #. name for nyo
msgid "Nyoro" msgid "Nyoro"
msgstr "" msgstr "Nyoro"
#. name for nyp #. name for nyp
msgid "Nyang'i" msgid "Nyang'i"
msgstr "" msgstr "Nyangi"
#. name for nyq #. name for nyq
msgid "Nayini" msgid "Nayini"
msgstr "" msgstr "Nayini"
#. name for nyr #. name for nyr
msgid "Nyiha (Malawi)" msgid "Nyiha (Malawi)"
@ -19444,31 +19444,31 @@ msgstr ""
#. name for nys #. name for nys
msgid "Nyunga" msgid "Nyunga"
msgstr "" msgstr "Nyunga"
#. name for nyt #. name for nyt
msgid "Nyawaygi" msgid "Nyawaygi"
msgstr "" msgstr "Nyawaygi"
#. name for nyu #. name for nyu
msgid "Nyungwe" msgid "Nyungwe"
msgstr "" msgstr "Nyungwe"
#. name for nyv #. name for nyv
msgid "Nyulnyul" msgid "Nyulnyul"
msgstr "" msgstr "Nyulnyui"
#. name for nyw #. name for nyw
msgid "Nyaw" msgid "Nyaw"
msgstr "" msgstr "Nyaw"
#. name for nyx #. name for nyx
msgid "Nganyaywana" msgid "Nganyaywana"
msgstr "" msgstr "Nganyaywana"
#. name for nyy #. name for nyy
msgid "Nyakyusa-Ngonde" msgid "Nyakyusa-Ngonde"
msgstr "" msgstr "Nyakyusa-Ngonde"
#. name for nza #. name for nza
msgid "Mbembe; Tigon" msgid "Mbembe; Tigon"
@ -19476,15 +19476,15 @@ msgstr "Mbembe Tigon"
#. name for nzb #. name for nzb
msgid "Njebi" msgid "Njebi"
msgstr "" msgstr "Njebi"
#. name for nzi #. name for nzi
msgid "Nzima" msgid "Nzima"
msgstr "" msgstr "Nzema"
#. name for nzk #. name for nzk
msgid "Nzakara" msgid "Nzakara"
msgstr "" msgstr "Nzakara"
#. name for nzm #. name for nzm
msgid "Naga; Zeme" msgid "Naga; Zeme"
@ -19500,7 +19500,7 @@ msgstr "Teke; Nzikou"
#. name for nzy #. name for nzy
msgid "Nzakambay" msgid "Nzakambay"
msgstr "" msgstr "Nzakambay"
#. name for nzz #. name for nzz
msgid "Dogon; Nanga Dama" msgid "Dogon; Nanga Dama"
@ -19508,11 +19508,11 @@ msgstr "Dogon; Nanga Dama"
#. name for oaa #. name for oaa
msgid "Orok" msgid "Orok"
msgstr "" msgstr "Orok"
#. name for oac #. name for oac
msgid "Oroch" msgid "Oroch"
msgstr "" msgstr "Orotx"
#. name for oar #. name for oar
msgid "Aramaic; Old (up to 700 BCE)" msgid "Aramaic; Old (up to 700 BCE)"
@ -29600,7 +29600,7 @@ msgstr ""
#. name for yiv #. name for yiv
msgid "Nisu; Northern" msgid "Nisu; Northern"
msgstr "" msgstr "Yi; Eshan-Xinping"
#. name for yix #. name for yix
msgid "Yi; Axi" msgid "Yi; Axi"

View File

@ -9,14 +9,14 @@ msgstr ""
"Project-Id-Version: calibre\n" "Project-Id-Version: calibre\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" "Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2011-11-25 14:01+0000\n" "POT-Creation-Date: 2011-11-25 14:01+0000\n"
"PO-Revision-Date: 2012-08-15 10:30+0000\n" "PO-Revision-Date: 2012-12-24 08:05+0000\n"
"Last-Translator: Jellby <Unknown>\n" "Last-Translator: Adolfo Jayme Barrientos <fitoschido@gmail.com>\n"
"Language-Team: Español; Castellano <>\n" "Language-Team: Español; Castellano <>\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-08-16 04:40+0000\n" "X-Launchpad-Export-Date: 2012-12-25 04:46+0000\n"
"X-Generator: Launchpad (build 15810)\n" "X-Generator: Launchpad (build 16378)\n"
#. name for aaa #. name for aaa
msgid "Ghotuo" msgid "Ghotuo"
@ -9584,27 +9584,27 @@ msgstr "Holikachuk"
#. name for hoj #. name for hoj
msgid "Hadothi" msgid "Hadothi"
msgstr "" msgstr "Hadothi"
#. name for hol #. name for hol
msgid "Holu" msgid "Holu"
msgstr "" msgstr "Holu"
#. name for hom #. name for hom
msgid "Homa" msgid "Homa"
msgstr "" msgstr "Homa"
#. name for hoo #. name for hoo
msgid "Holoholo" msgid "Holoholo"
msgstr "" msgstr "Holoholo"
#. name for hop #. name for hop
msgid "Hopi" msgid "Hopi"
msgstr "" msgstr "Hopi"
#. name for hor #. name for hor
msgid "Horo" msgid "Horo"
msgstr "" msgstr "Horo"
#. name for hos #. name for hos
msgid "Ho Chi Minh City Sign Language" msgid "Ho Chi Minh City Sign Language"
@ -9612,27 +9612,27 @@ msgstr "Lengua de signos de Ho Chi Minh"
#. name for hot #. name for hot
msgid "Hote" msgid "Hote"
msgstr "" msgstr "Hote"
#. name for hov #. name for hov
msgid "Hovongan" msgid "Hovongan"
msgstr "" msgstr "Hovongan"
#. name for how #. name for how
msgid "Honi" msgid "Honi"
msgstr "" msgstr "Honi"
#. name for hoy #. name for hoy
msgid "Holiya" msgid "Holiya"
msgstr "" msgstr "Holiya"
#. name for hoz #. name for hoz
msgid "Hozo" msgid "Hozo"
msgstr "" msgstr "Hozo"
#. name for hpo #. name for hpo
msgid "Hpon" msgid "Hpon"
msgstr "" msgstr "Hpon"
#. name for hps #. name for hps
msgid "Hawai'i Pidgin Sign Language" msgid "Hawai'i Pidgin Sign Language"
@ -9640,15 +9640,15 @@ msgstr "Lengua de signos pidyin hawaiana"
#. name for hra #. name for hra
msgid "Hrangkhol" msgid "Hrangkhol"
msgstr "" msgstr "Hrangkhol"
#. name for hre #. name for hre
msgid "Hre" msgid "Hre"
msgstr "" msgstr "Hre"
#. name for hrk #. name for hrk
msgid "Haruku" msgid "Haruku"
msgstr "" msgstr "Haruku"
#. name for hrm #. name for hrm
msgid "Miao; Horned" msgid "Miao; Horned"
@ -9656,19 +9656,19 @@ msgstr ""
#. name for hro #. name for hro
msgid "Haroi" msgid "Haroi"
msgstr "" msgstr "Haroi"
#. name for hrr #. name for hrr
msgid "Horuru" msgid "Horuru"
msgstr "" msgstr "Horuru"
#. name for hrt #. name for hrt
msgid "Hértevin" msgid "Hértevin"
msgstr "" msgstr "Hértevin"
#. name for hru #. name for hru
msgid "Hruso" msgid "Hruso"
msgstr "" msgstr "Hruso"
#. name for hrv #. name for hrv
msgid "Croatian" msgid "Croatian"

View File

@ -9,14 +9,14 @@ msgstr ""
"Report-Msgid-Bugs-To: Debian iso-codes team <pkg-isocodes-" "Report-Msgid-Bugs-To: Debian iso-codes team <pkg-isocodes-"
"devel@lists.alioth.debian.org>\n" "devel@lists.alioth.debian.org>\n"
"POT-Creation-Date: 2011-11-25 14:01+0000\n" "POT-Creation-Date: 2011-11-25 14:01+0000\n"
"PO-Revision-Date: 2012-12-14 00:48+0000\n" "PO-Revision-Date: 2012-12-21 03:31+0000\n"
"Last-Translator: Fábio Malcher Miranda <mirand863@hotmail.com>\n" "Last-Translator: Fábio Malcher Miranda <mirand863@hotmail.com>\n"
"Language-Team: Brazilian Portuguese\n" "Language-Team: Brazilian Portuguese\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-12-15 05:02+0000\n" "X-Launchpad-Export-Date: 2012-12-22 04:59+0000\n"
"X-Generator: Launchpad (build 16372)\n" "X-Generator: Launchpad (build 16378)\n"
"Language: \n" "Language: \n"
#. name for aaa #. name for aaa
@ -1189,7 +1189,7 @@ msgstr ""
#. name for anz #. name for anz
msgid "Anem" msgid "Anem"
msgstr "" msgstr "Anem"
#. name for aoa #. name for aoa
msgid "Angolar" msgid "Angolar"
@ -1197,27 +1197,27 @@ msgstr ""
#. name for aob #. name for aob
msgid "Abom" msgid "Abom"
msgstr "" msgstr "Abom"
#. name for aoc #. name for aoc
msgid "Pemon" msgid "Pemon"
msgstr "" msgstr "Pemon"
#. name for aod #. name for aod
msgid "Andarum" msgid "Andarum"
msgstr "" msgstr "Andarum"
#. name for aoe #. name for aoe
msgid "Angal Enen" msgid "Angal Enen"
msgstr "" msgstr "Angal Enen"
#. name for aof #. name for aof
msgid "Bragat" msgid "Bragat"
msgstr "" msgstr "Bragat"
#. name for aog #. name for aog
msgid "Angoram" msgid "Angoram"
msgstr "" msgstr "Angoram"
#. name for aoh #. name for aoh
msgid "Arma" msgid "Arma"

View File

@ -4,7 +4,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
__appname__ = u'calibre' __appname__ = u'calibre'
numeric_version = (0, 9, 11) numeric_version = (0, 9, 12)
__version__ = u'.'.join(map(unicode, numeric_version)) __version__ = u'.'.join(map(unicode, numeric_version))
__author__ = u"Kovid Goyal <kovid@kovidgoyal.net>" __author__ = u"Kovid Goyal <kovid@kovidgoyal.net>"
@ -100,6 +100,7 @@ class Plugins(collections.Mapping):
'freetype', 'freetype',
'woff', 'woff',
'unrar', 'unrar',
'qt_hack',
] ]
if iswindows: if iswindows:
plugins.extend(['winutil', 'wpd', 'winfonts']) plugins.extend(['winutil', 'wpd', 'winfonts'])

View File

@ -661,7 +661,7 @@ from calibre.devices.nuut2.driver import NUUT2
from calibre.devices.iriver.driver import IRIVER_STORY from calibre.devices.iriver.driver import IRIVER_STORY
from calibre.devices.binatone.driver import README from calibre.devices.binatone.driver import README
from calibre.devices.hanvon.driver import (N516, EB511, ALEX, AZBOOKA, THEBOOK, from calibre.devices.hanvon.driver import (N516, EB511, ALEX, AZBOOKA, THEBOOK,
LIBREAIR, ODYSSEY) LIBREAIR, ODYSSEY, KIBANO)
from calibre.devices.edge.driver import EDGE from calibre.devices.edge.driver import EDGE
from calibre.devices.teclast.driver import (TECLAST_K3, NEWSMY, IPAPYRUS, from calibre.devices.teclast.driver import (TECLAST_K3, NEWSMY, IPAPYRUS,
SOVOS, PICO, SUNSTECH_EB700, ARCHOS7O, STASH, WEXLER) SOVOS, PICO, SUNSTECH_EB700, ARCHOS7O, STASH, WEXLER)
@ -712,7 +712,7 @@ plugins += [
BOOQ, BOOQ,
EB600, EB600,
README, README,
N516, N516, KIBANO,
THEBOOK, LIBREAIR, THEBOOK, LIBREAIR,
EB511, EB511,
ELONEX, ELONEX,

View File

@ -41,6 +41,20 @@ class N516(USBMS):
def can_handle(self, device_info, debug=False): def can_handle(self, device_info, debug=False):
return not is_alex(device_info) return not is_alex(device_info)
class KIBANO(N516):
name = 'Kibano driver'
gui_name = 'Kibano'
description = _('Communicate with the Kibano eBook reader.')
FORMATS = ['epub', 'pdf', 'txt']
BCD = [0x323]
VENDOR_NAME = 'EBOOK'
# We use EXTERNAL_SD_CARD for main mem as some devices have not working
# main memories
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['INTERNAL_SD_CARD',
'EXTERNAL_SD_CARD']
class THEBOOK(N516): class THEBOOK(N516):
name = 'The Book driver' name = 'The Book driver'
gui_name = 'The Book' gui_name = 'The Book'

View File

@ -13,6 +13,7 @@ const calibre_device_entry_t calibre_mtp_device_table[] = {
// Amazon Kindle Fire HD // Amazon Kindle Fire HD
, { "Amazon", 0x1949, "Fire HD", 0x0007, DEVICE_FLAGS_ANDROID_BUGS} , { "Amazon", 0x1949, "Fire HD", 0x0007, DEVICE_FLAGS_ANDROID_BUGS}
, { "Amazon", 0x1949, "Fire HD", 0x0008, DEVICE_FLAGS_ANDROID_BUGS}
, { "Amazon", 0x1949, "Fire HD", 0x000a, DEVICE_FLAGS_ANDROID_BUGS} , { "Amazon", 0x1949, "Fire HD", 0x000a, DEVICE_FLAGS_ANDROID_BUGS}
// Nexus 10 // Nexus 10

View File

@ -8,7 +8,9 @@
#define UNICODE #define UNICODE
#include <Python.h> #include <Python.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h> #include <stdlib.h>
#include <libmtp.h> #include <libmtp.h>
@ -728,7 +730,20 @@ initlibmtp(void) {
if (MTPError == NULL) return; if (MTPError == NULL) return;
PyModule_AddObject(m, "MTPError", MTPError); PyModule_AddObject(m, "MTPError", MTPError);
// Redirect stdout to get rid of the annoying message about mtpz. Really,
// who designs a library without anyway to control/redirect the debugging
// output, and hardcoded paths that cannot be changed?
int bak, new;
fflush(stdout);
bak = dup(STDOUT_FILENO);
new = open("/dev/null", O_WRONLY);
dup2(new, STDOUT_FILENO);
close(new);
LIBMTP_Init(); LIBMTP_Init();
fflush(stdout);
dup2(bak, STDOUT_FILENO);
close(bak);
LIBMTP_Set_Debug(LIBMTP_DEBUG_NONE); LIBMTP_Set_Debug(LIBMTP_DEBUG_NONE);
Py_INCREF(&DeviceType); Py_INCREF(&DeviceType);

View File

@ -19,9 +19,10 @@ class TECLAST_K3(USBMS):
PRODUCT_ID = [0x3203] PRODUCT_ID = [0x3203]
BCD = [0x0000, 0x0100] BCD = [0x0000, 0x0100]
VENDOR_NAME = ['TECLAST', 'IMAGIN', 'RK28XX', 'PER3274B', 'BEBOOK'] VENDOR_NAME = ['TECLAST', 'IMAGIN', 'RK28XX', 'PER3274B', 'BEBOOK',
'RK2728', 'MR700']
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['DIGITAL_PLAYER', 'TL-K5', WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['DIGITAL_PLAYER', 'TL-K5',
'EREADER', 'USB-MSC', 'PER3274B', 'BEBOOK'] 'EREADER', 'USB-MSC', 'PER3274B', 'BEBOOK', 'USER']
MAIN_MEMORY_VOLUME_LABEL = 'K3 Main Memory' MAIN_MEMORY_VOLUME_LABEL = 'K3 Main Memory'
STORAGE_CARD_VOLUME_LABEL = 'K3 Storage Card' STORAGE_CARD_VOLUME_LABEL = 'K3 Storage Card'

View File

@ -14,50 +14,32 @@ import os
from calibre.customize.conversion import OutputFormatPlugin, \ from calibre.customize.conversion import OutputFormatPlugin, \
OptionRecommendation OptionRecommendation
from calibre.ptempfile import TemporaryDirectory from calibre.ptempfile import TemporaryDirectory
from calibre.constants import iswindows
UNITS = [ UNITS = ['millimeter', 'centimeter', 'point', 'inch' , 'pica' , 'didot',
'millimeter', 'cicero', 'devicepixel']
'point',
'inch' ,
'pica' ,
'didot',
'cicero',
'devicepixel',
]
PAPER_SIZES = ['b2', PAPER_SIZES = [u'a0', u'a1', u'a2', u'a3', u'a4', u'a5', u'a6', u'b0', u'b1',
'a9', u'b2', u'b3', u'b4', u'b5', u'b6', u'legal', u'letter']
'executive',
'tabloid',
'b4',
'b5',
'b6',
'b7',
'b0',
'b1',
'letter',
'b3',
'a7',
'a8',
'b8',
'b9',
'a3',
'a1',
'folio',
'c5e',
'dle',
'a0',
'ledger',
'legal',
'a6',
'a2',
'b10',
'a5',
'comm10e',
'a4']
ORIENTATIONS = ['portrait', 'landscape'] class PDFMetadata(object): # {{{
def __init__(self, oeb_metadata=None):
from calibre import force_unicode
from calibre.ebooks.metadata import authors_to_string
self.title = _(u'Unknown')
self.author = _(u'Unknown')
self.tags = u''
if oeb_metadata != None:
if len(oeb_metadata.title) >= 1:
self.title = oeb_metadata.title[0].value
if len(oeb_metadata.creator) >= 1:
self.author = authors_to_string([x.value for x in oeb_metadata.creator])
if oeb_metadata.subject:
self.tags = u', '.join(map(unicode, oeb_metadata.subject))
self.title = force_unicode(self.title)
self.author = force_unicode(self.author)
# }}}
class PDFOutput(OutputFormatPlugin): class PDFOutput(OutputFormatPlugin):
@ -66,9 +48,14 @@ class PDFOutput(OutputFormatPlugin):
file_type = 'pdf' file_type = 'pdf'
options = set([ options = set([
OptionRecommendation(name='override_profile_size', recommended_value=False,
help=_('Normally, the PDF page size is set by the output profile'
' chosen under page options. This option will cause the '
' page size settings under PDF Output to override the '
' size specified by the output profile.')),
OptionRecommendation(name='unit', recommended_value='inch', OptionRecommendation(name='unit', recommended_value='inch',
level=OptionRecommendation.LOW, short_switch='u', choices=UNITS, level=OptionRecommendation.LOW, short_switch='u', choices=UNITS,
help=_('The unit of measure. Default is inch. Choices ' help=_('The unit of measure for page sizes. Default is inch. Choices '
'are %s ' 'are %s '
'Note: This does not override the unit for margins!') % UNITS), 'Note: This does not override the unit for margins!') % UNITS),
OptionRecommendation(name='paper_size', recommended_value='letter', OptionRecommendation(name='paper_size', recommended_value='letter',
@ -80,10 +67,6 @@ class PDFOutput(OutputFormatPlugin):
help=_('Custom size of the document. Use the form widthxheight ' help=_('Custom size of the document. Use the form widthxheight '
'EG. `123x321` to specify the width and height. ' 'EG. `123x321` to specify the width and height. '
'This overrides any specified paper-size.')), 'This overrides any specified paper-size.')),
OptionRecommendation(name='orientation', recommended_value='portrait',
level=OptionRecommendation.LOW, choices=ORIENTATIONS,
help=_('The orientation of the page. Default is portrait. Choices '
'are %s') % ORIENTATIONS),
OptionRecommendation(name='preserve_cover_aspect_ratio', OptionRecommendation(name='preserve_cover_aspect_ratio',
recommended_value=False, recommended_value=False,
help=_('Preserve the aspect ratio of the cover, instead' help=_('Preserve the aspect ratio of the cover, instead'
@ -108,6 +91,12 @@ class PDFOutput(OutputFormatPlugin):
OptionRecommendation(name='pdf_mono_font_size', OptionRecommendation(name='pdf_mono_font_size',
recommended_value=16, help=_( recommended_value=16, help=_(
'The default font size for monospaced text')), 'The default font size for monospaced text')),
# OptionRecommendation(name='old_pdf_engine', recommended_value=False,
# help=_('Use the old, less capable engine to generate the PDF')),
# OptionRecommendation(name='uncompressed_pdf',
# recommended_value=False, help=_(
# 'Generate an uncompressed PDF, useful for debugging, '
# 'only works with the new PDF engine.')),
]) ])
def convert(self, oeb_book, output_path, input_plugin, opts, log): def convert(self, oeb_book, output_path, input_plugin, opts, log):
@ -200,33 +189,19 @@ class PDFOutput(OutputFormatPlugin):
if k in family_map: if k in family_map:
val[i].value = family_map[k] val[i].value = family_map[k]
def remove_font_specification(self):
# Qt produces image based pdfs on windows when non-generic fonts are specified
# This might change in Qt WebKit 2.3+ you will have to test.
for item in self.oeb.manifest:
if not hasattr(item.data, 'cssRules'): continue
for i, rule in enumerate(item.data.cssRules):
if rule.type != rule.STYLE_RULE: continue
ff = rule.style.getProperty('font-family')
if ff is None: continue
val = ff.propertyValue
for i in xrange(val.length):
k = icu_lower(val[i].value)
if k not in {'serif', 'sans', 'sans-serif', 'sansserif',
'monospace', 'cursive', 'fantasy'}:
val[i].value = ''
def convert_text(self, oeb_book): def convert_text(self, oeb_book):
from calibre.ebooks.pdf.writer import PDFWriter from calibre.utils.config import tweaks
if tweaks.get('new_pdf_engine', False):
from calibre.ebooks.pdf.render.from_html import PDFWriter
PDFWriter
else:
from calibre.ebooks.pdf.writer import PDFWriter
from calibre.ebooks.metadata.opf2 import OPF from calibre.ebooks.metadata.opf2 import OPF
self.log.debug('Serializing oeb input to disk for processing...') self.log.debug('Serializing oeb input to disk for processing...')
self.get_cover_data() self.get_cover_data()
if iswindows: self.handle_embedded_fonts()
self.remove_font_specification()
else:
self.handle_embedded_fonts()
with TemporaryDirectory('_pdf_out') as oeb_dir: with TemporaryDirectory('_pdf_out') as oeb_dir:
from calibre.customize.ui import plugin_for_output_format from calibre.customize.ui import plugin_for_output_format
@ -240,9 +215,9 @@ class PDFOutput(OutputFormatPlugin):
'toc', None)) 'toc', None))
def write(self, Writer, items, toc): def write(self, Writer, items, toc):
from calibre.ebooks.pdf.writer import PDFMetadata
writer = Writer(self.opts, self.log, cover_data=self.cover_data, writer = Writer(self.opts, self.log, cover_data=self.cover_data,
toc=toc) toc=toc)
writer.report_progress = self.report_progress
close = False close = False
if not hasattr(self.output_path, 'write'): if not hasattr(self.output_path, 'write'):

View File

@ -75,6 +75,20 @@ class Worker(Thread): # Get details {{{
9: ['sept'], 9: ['sept'],
12: ['déc'], 12: ['déc'],
}, },
'br': {
1: ['janeiro'],
2: ['fevereiro'],
3: ['março'],
4: ['abril'],
5: ['maio'],
6: ['junho'],
7: ['julho'],
8: ['agosto'],
9: ['setembro'],
10: ['outubro'],
11: ['novembro'],
12: ['dezembro'],
},
'es': { 'es': {
1: ['enero'], 1: ['enero'],
2: ['febrero'], 2: ['febrero'],
@ -89,7 +103,7 @@ class Worker(Thread): # Get details {{{
11: ['noviembre'], 11: ['noviembre'],
12: ['diciembre'], 12: ['diciembre'],
}, },
'jp': { 'jp': {
1: [u'1月'], 1: [u'1月'],
2: [u'2月'], 2: [u'2月'],
3: [u'3月'], 3: [u'3月'],
@ -117,6 +131,7 @@ class Worker(Thread): # Get details {{{
text()="Product details" or \ text()="Product details" or \
text()="Détails sur le produit" or \ text()="Détails sur le produit" or \
text()="Detalles del producto" or \ text()="Detalles del producto" or \
text()="Detalhes do produto" or \
text()="登録情報"]/../div[@class="content"] text()="登録情報"]/../div[@class="content"]
''' '''
# Editor: is for Spanish # Editor: is for Spanish
@ -126,6 +141,7 @@ class Worker(Thread): # Get details {{{
starts-with(text(), "Editore:") or \ starts-with(text(), "Editore:") or \
starts-with(text(), "Editeur") or \ starts-with(text(), "Editeur") or \
starts-with(text(), "Editor:") or \ starts-with(text(), "Editor:") or \
starts-with(text(), "Editora:") or \
starts-with(text(), "出版社:")] starts-with(text(), "出版社:")]
''' '''
self.language_xpath = ''' self.language_xpath = '''
@ -141,7 +157,7 @@ class Worker(Thread): # Get details {{{
''' '''
self.ratings_pat = re.compile( self.ratings_pat = re.compile(
r'([0-9.]+) ?(out of|von|su|étoiles sur|つ星のうち|de un máximo de) ([\d\.]+)( (stars|Sternen|stelle|estrellas)){0,1}') r'([0-9.]+) ?(out of|von|su|étoiles sur|つ星のうち|de un máximo de|de) ([\d\.]+)( (stars|Sternen|stelle|estrellas|estrelas)){0,1}')
lm = { lm = {
'eng': ('English', 'Englisch'), 'eng': ('English', 'Englisch'),
@ -150,6 +166,7 @@ class Worker(Thread): # Get details {{{
'deu': ('German', 'Deutsch'), 'deu': ('German', 'Deutsch'),
'spa': ('Spanish', 'Espa\xf1ol', 'Espaniol'), 'spa': ('Spanish', 'Espa\xf1ol', 'Espaniol'),
'jpn': ('Japanese', u'日本語'), 'jpn': ('Japanese', u'日本語'),
'por': ('Portuguese', 'Português'),
} }
self.lang_map = {} self.lang_map = {}
for code, names in lm.iteritems(): for code, names in lm.iteritems():
@ -505,6 +522,7 @@ class Amazon(Source):
'it' : _('Italy'), 'it' : _('Italy'),
'jp' : _('Japan'), 'jp' : _('Japan'),
'es' : _('Spain'), 'es' : _('Spain'),
'br' : _('Brazil'),
} }
options = ( options = (
@ -570,6 +588,8 @@ class Amazon(Source):
url = 'http://amzn.com/'+asin url = 'http://amzn.com/'+asin
elif domain == 'uk': elif domain == 'uk':
url = 'http://www.amazon.co.uk/dp/'+asin url = 'http://www.amazon.co.uk/dp/'+asin
elif domain == 'br':
url = 'http://www.amazon.com.br/dp/'+asin
else: else:
url = 'http://www.amazon.%s/dp/%s'%(domain, asin) url = 'http://www.amazon.%s/dp/%s'%(domain, asin)
if url: if url:
@ -629,7 +649,7 @@ class Amazon(Source):
q['field-isbn'] = isbn q['field-isbn'] = isbn
else: else:
# Only return book results # Only return book results
q['search-alias'] = 'stripbooks' q['search-alias'] = 'digital-text' if domain == 'br' else 'stripbooks'
if title: if title:
title_tokens = list(self.get_title_tokens(title)) title_tokens = list(self.get_title_tokens(title))
if title_tokens: if title_tokens:
@ -661,6 +681,8 @@ class Amazon(Source):
udomain = 'co.uk' udomain = 'co.uk'
elif domain == 'jp': elif domain == 'jp':
udomain = 'co.jp' udomain = 'co.jp'
elif domain == 'br':
udomain = 'com.br'
url = 'http://www.amazon.%s/s/?'%udomain + urlencode(encoded_q) url = 'http://www.amazon.%s/s/?'%udomain + urlencode(encoded_q)
return url, domain return url, domain
@ -978,6 +1000,16 @@ if __name__ == '__main__': # tests {{{
), ),
] # }}} ] # }}}
br_tests = [ # {{{
(
{'title':'Guerra dos Tronos'},
[title_test('A Guerra dos Tronos - As Crônicas de Gelo e Fogo',
exact=True), authors_test(['George R. R. Martin'])
]
),
] # }}}
def do_test(domain, start=0, stop=None): def do_test(domain, start=0, stop=None):
tests = globals().get(domain+'_tests') tests = globals().get(domain+'_tests')
if stop is None: if stop is None:
@ -988,7 +1020,7 @@ if __name__ == '__main__': # tests {{{
do_test('com') do_test('com')
#do_test('de') # do_test('de')
# }}} # }}}

View File

@ -92,6 +92,31 @@ class BookIndexing
this.last_check = [body.scrollWidth, body.scrollHeight] this.last_check = [body.scrollWidth, body.scrollHeight]
return ans return ans
all_links_and_anchors: () ->
body = document.body
links = []
anchors = {}
for a in document.querySelectorAll("body a[href], body [id], body a[name]")
if window.paged_display?.in_paged_mode
geom = window.paged_display.column_location(a)
else
br = a.getBoundingClientRect()
[left, top] = viewport_to_document(br.left, br.top, a.ownerDocument)
geom = {'left':left, 'top':top, 'width':br.right-br.left, 'height':br.bottom-br.top}
href = a.getAttribute('href')
if href
links.push([href, geom])
id = a.getAttribute("id")
if id and id not in anchors
anchors[id] = geom
if a.tagName in ['A', "a"]
name = a.getAttribute("name")
if name and name not in anchors
anchors[name] = geom
return {'links':links, 'anchors':anchors}
if window? if window?
window.book_indexing = new BookIndexing() window.book_indexing = new BookIndexing()

View File

@ -242,6 +242,18 @@ class PagedDisplay
# Return the number of the column that contains xpos # Return the number of the column that contains xpos
return Math.floor(xpos/this.page_width) return Math.floor(xpos/this.page_width)
column_location: (elem) ->
# Return the location of elem relative to its containing column
br = elem.getBoundingClientRect()
[left, top] = calibre_utils.viewport_to_document(br.left, br.top, elem.ownerDocument)
c = this.column_at(left)
width = Math.min(br.right, (c+1)*this.page_width) - br.left
if br.bottom < br.top
br.bottom = window.innerHeight
height = Math.min(br.bottom, window.innerHeight) - br.top
left -= c*this.page_width
return {'column':c, 'left':left, 'top':top, 'width':width, 'height':height}
column_boundaries: () -> column_boundaries: () ->
# Return the column numbers at the left edge and after the right edge # Return the column numbers at the left edge and after the right edge
# of the viewport # of the viewport

View File

@ -18,6 +18,8 @@ inch = 72.0
cm = inch / 2.54 cm = inch / 2.54
mm = cm * 0.1 mm = cm * 0.1
pica = 12.0 pica = 12.0
didot = 0.375 * mm
cicero = 12 * didot
_W, _H = (21*cm, 29.7*cm) _W, _H = (21*cm, 29.7*cm)
@ -41,6 +43,10 @@ B3 = (_BH*2, _BW)
B2 = (_BW*2, _BH*2) B2 = (_BW*2, _BH*2)
B1 = (_BH*4, _BW*2) B1 = (_BH*4, _BW*2)
B0 = (_BW*4, _BH*4) B0 = (_BW*4, _BH*4)
PAPER_SIZES = {k:globals()[k.upper()] for k in ('a0 a1 a2 a3 a4 a5 a6 b0 b1 b2'
' b3 b4 b5 b6 letter legal').split()}
# }}} # }}}
# Basic PDF datatypes {{{ # Basic PDF datatypes {{{
@ -79,19 +85,12 @@ class String(unicode):
raw = codecs.BOM_UTF16_BE + s.encode('utf-16-be') raw = codecs.BOM_UTF16_BE + s.encode('utf-16-be')
stream.write(b'('+raw+b')') stream.write(b'('+raw+b')')
class GlyphIndex(object): class GlyphIndex(int):
def __init__(self, code, compress):
self.code = code
self.compress = compress
def pdf_serialize(self, stream): def pdf_serialize(self, stream):
if self.compress: byts = bytearray(pack(b'>H', self))
stream.write(pack(b'>sHs', b'(', self.code, b')')) stream.write('<%s>'%''.join(map(
else: lambda x: bytes(hex(x)[2:]).rjust(2, b'0'), byts)))
byts = bytearray(pack(b'>H', self.code))
stream.write('<%s>'%''.join(map(
lambda x: bytes(hex(int(x))[2:]).rjust(2, b'0'), byts)))
class Dictionary(dict): class Dictionary(dict):
@ -132,6 +131,7 @@ class Stream(BytesIO):
def __init__(self, compress=False): def __init__(self, compress=False):
BytesIO.__init__(self) BytesIO.__init__(self)
self.compress = compress self.compress = compress
self.filters = Array()
def add_extra_keys(self, d): def add_extra_keys(self, d):
pass pass
@ -139,7 +139,7 @@ class Stream(BytesIO):
def pdf_serialize(self, stream): def pdf_serialize(self, stream):
raw = self.getvalue() raw = self.getvalue()
dl = len(raw) dl = len(raw)
filters = Array() filters = self.filters
if self.compress: if self.compress:
filters.append(Name('FlateDecode')) filters.append(Name('FlateDecode'))
raw = zlib.compress(raw) raw = zlib.compress(raw)

View File

@ -7,23 +7,22 @@ __license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import sys, traceback, unicodedata import sys, traceback
from math import sqrt from math import sqrt
from collections import namedtuple from collections import namedtuple
from functools import wraps from functools import wraps, partial
import sip
from PyQt4.Qt import (QPaintEngine, QPaintDevice, Qt, QApplication, QPainter, from PyQt4.Qt import (QPaintEngine, QPaintDevice, Qt, QApplication, QPainter,
QTransform, QPainterPath, QTextOption, QTextLayout) QTransform, QPainterPath, QImage, QByteArray, QBuffer,
qRgba)
from calibre.constants import DEBUG from calibre.constants import plugins
from calibre.ebooks.pdf.render.serialize import (Color, PDFStream, Path) from calibre.ebooks.pdf.render.serialize import (Color, PDFStream, Path)
from calibre.ebooks.pdf.render.common import inch, A4 from calibre.ebooks.pdf.render.common import inch, A4
from calibre.utils.fonts.sfnt.container import Sfnt from calibre.utils.fonts.sfnt.container import Sfnt
from calibre.utils.fonts.sfnt.metrics import FontMetrics from calibre.utils.fonts.sfnt.metrics import FontMetrics
XDPI = 1200
YDPI = 1200
Point = namedtuple('Point', 'x y') Point = namedtuple('Point', 'x y')
ColorState = namedtuple('ColorState', 'color opacity do') ColorState = namedtuple('ColorState', 'color opacity do')
@ -34,7 +33,8 @@ def store_error(func):
try: try:
func(self, *args, **kwargs) func(self, *args, **kwargs)
except: except:
self.errors.append(traceback.format_exc()) self.errors_occurred = True
self.errors(traceback.format_exc())
return errh return errh
@ -42,7 +42,7 @@ class GraphicsState(object): # {{{
def __init__(self): def __init__(self):
self.ops = {} self.ops = {}
self.current_state = self.initial_state = { self.initial_state = {
'fill': ColorState(Color(0., 0., 0., 1.), 1.0, False), 'fill': ColorState(Color(0., 0., 0., 1.), 1.0, False),
'transform': QTransform(), 'transform': QTransform(),
'dash': [], 'dash': [],
@ -52,9 +52,10 @@ class GraphicsState(object): # {{{
'line_join': 'miter', 'line_join': 'miter',
'clip': (Qt.NoClip, QPainterPath()), 'clip': (Qt.NoClip, QPainterPath()),
} }
self.current_state = self.initial_state.copy()
def reset(self): def reset(self):
self.current_state = self.initial_state self.current_state = self.initial_state.copy()
def update_color_state(self, which, color=None, opacity=None, def update_color_state(self, which, color=None, opacity=None,
brush_style=None, pen_style=None): brush_style=None, pen_style=None):
@ -77,7 +78,6 @@ class GraphicsState(object): # {{{
self.ops[which] = n self.ops[which] = n
def read(self, state): def read(self, state):
self.ops = {}
flags = state.state() flags = state.state()
if flags & QPaintEngine.DirtyTransform: if flags & QPaintEngine.DirtyTransform:
@ -109,15 +109,12 @@ class GraphicsState(object): # {{{
self.update_color_state('fill', opacity=state.opacity()) self.update_color_state('fill', opacity=state.opacity())
self.update_color_state('stroke', opacity=state.opacity()) self.update_color_state('stroke', opacity=state.opacity())
if flags & QPaintEngine.DirtyClipPath: if flags & QPaintEngine.DirtyClipPath or flags & QPaintEngine.DirtyClipRegion:
self.ops['clip'] = (state.clipOperation(), state.clipPath()) self.ops['clip'] = True
elif flags & QPaintEngine.DirtyClipRegion:
path = QPainterPath()
for rect in state.clipRegion().rects():
path.addRect(rect)
self.ops['clip'] = (state.clipOperation(), path)
def __call__(self, engine): def __call__(self, engine):
if not self.ops:
return
pdf = engine.pdf pdf = engine.pdf
ops = self.ops ops = self.ops
current_transform = self.current_state['transform'] current_transform = self.current_state['transform']
@ -127,58 +124,34 @@ class GraphicsState(object): # {{{
if reset_stack: if reset_stack:
pdf.restore_stack() pdf.restore_stack()
pdf.save_stack() pdf.save_stack()
# We apply clip before transform as the clip may have to be merged with
# the previous clip path so it is easiest to work with clips that are
# pre-transformed
prev_op, prev_clip_path = self.current_state['clip']
if 'clip' in ops:
op, path = ops['clip']
self.current_state['clip'] = (op, path)
transform = ops.get('transform', QTransform())
if not transform.isIdentity() and path is not None:
# Pre transform the clip path
path = current_transform.map(path)
self.current_state['clip'] = (op, path)
if op == Qt.ReplaceClip:
pass
elif op == Qt.IntersectClip:
if prev_op != Qt.NoClip:
self.current_state['clip'] = (op, path.intersected(prev_clip_path))
elif op == Qt.UniteClip:
if prev_clip_path is not None:
path.addPath(prev_clip_path)
else:
self.current_state['clip'] = (Qt.NoClip, QPainterPath())
op, path = self.current_state['clip']
if op != Qt.NoClip:
engine.add_clip(path)
elif reset_stack and prev_op != Qt.NoClip:
# Re-apply the previous clip path since no clipping operation was
# specified
engine.add_clip(prev_clip_path)
if reset_stack:
# Since we have reset the stack we need to re-apply all previous # Since we have reset the stack we need to re-apply all previous
# operations, that are different from the default value (clip is # operations, that are different from the default value (clip is
# handled separately). # handled separately).
for op in set(self.current_state) - (set(ops)|{'clip'}): for op in set(self.initial_state) - {'clip'}:
if self.current_state[op] != self.initial_state[op]: if op in ops: # These will be applied below
self.current_state[op] = self.initial_state[op]
elif self.current_state[op] != self.initial_state[op]:
self.apply(op, self.current_state[op], engine, pdf) self.apply(op, self.current_state[op], engine, pdf)
# Now apply the new operations # Now apply the new operations
for op, val in ops.iteritems(): for op, val in ops.iteritems():
if op != 'clip': if op != 'clip' and self.current_state[op] != val:
self.apply(op, val, engine, pdf) self.apply(op, val, engine, pdf)
self.current_state[op] = val self.current_state[op] = val
if 'clip' in ops:
# Get the current clip
path = engine.painter().clipPath()
if not path.isEmpty():
engine.add_clip(path)
self.ops = {}
def apply(self, op, val, engine, pdf): def apply(self, op, val, engine, pdf):
getattr(self, 'apply_'+op)(val, engine, pdf) getattr(self, 'apply_'+op)(val, engine, pdf)
def apply_transform(self, val, engine, pdf): def apply_transform(self, val, engine, pdf):
engine.qt_system = val if not val.isIdentity():
pdf.transform(val) pdf.transform(val)
def apply_stroke(self, val, engine, pdf): def apply_stroke(self, val, engine, pdf):
self.apply_color_state('stroke', val, engine, pdf) self.apply_color_state('stroke', val, engine, pdf)
@ -214,9 +187,11 @@ class Font(FontMetrics):
class PdfEngine(QPaintEngine): class PdfEngine(QPaintEngine):
def __init__(self, file_object, page_width, page_height, left_margin, def __init__(self, file_object, page_width, page_height, left_margin,
top_margin, right_margin, bottom_margin, width, height): top_margin, right_margin, bottom_margin, width, height,
errors=print, debug=print, compress=True):
QPaintEngine.__init__(self, self.features) QPaintEngine.__init__(self, self.features)
self.file_object = file_object self.file_object = file_object
self.compress = compress
self.page_height, self.page_width = page_height, page_width self.page_height, self.page_width = page_height, page_width
self.left_margin, self.top_margin = left_margin, top_margin self.left_margin, self.top_margin = left_margin, top_margin
self.right_margin, self.bottom_margin = right_margin, bottom_margin self.right_margin, self.bottom_margin = right_margin, bottom_margin
@ -235,26 +210,33 @@ class PdfEngine(QPaintEngine):
self.bottom_margin) / self.pixel_height self.bottom_margin) / self.pixel_height
self.pdf_system = QTransform(sx, 0, 0, -sy, dx, dy) self.pdf_system = QTransform(sx, 0, 0, -sy, dx, dy)
self.qt_system = QTransform()
self.do_stroke = True self.do_stroke = True
self.do_fill = False self.do_fill = False
self.scale = sqrt(sy**2 + sx**2) self.scale = sqrt(sy**2 + sx**2)
self.xscale, self.yscale = sx, sy self.xscale, self.yscale = sx, sy
self.graphics_state = GraphicsState() self.graphics_state = GraphicsState()
self.errors, self.debug = [], [] self.errors_occurred = False
self.text_option = QTextOption() self.errors, self.debug = errors, debug
self.text_option.setWrapMode(QTextOption.NoWrap)
self.fonts = {} self.fonts = {}
i = QImage(1, 1, QImage.Format_ARGB32)
i.fill(qRgba(0, 0, 0, 255))
self.alpha_bit = i.constBits().asstring(4).find(b'\xff')
self.current_page_num = 1
self.current_page_inited = False
self.qt_hack, err = plugins['qt_hack']
if err:
raise RuntimeError('Failed to load qt_hack with err: %s'%err)
def init_page(self): def init_page(self):
self.pdf.transform(self.pdf_system) self.pdf.transform(self.pdf_system)
self.pdf.set_rgb_colorspace() self.pdf.set_rgb_colorspace()
width = self.painter.pen().widthF() if self.isActive() else 0 width = self.painter().pen().widthF() if self.isActive() else 0
self.pdf.set_line_width(width) self.pdf.set_line_width(width)
self.do_stroke = True self.do_stroke = True
self.do_fill = False self.do_fill = False
self.graphics_state.reset() self.graphics_state.reset()
self.pdf.save_stack() self.pdf.save_stack()
self.current_page_inited = True
@property @property
def features(self): def features(self):
@ -264,25 +246,26 @@ class PdfEngine(QPaintEngine):
QPaintEngine.PrimitiveTransform) QPaintEngine.PrimitiveTransform)
def begin(self, device): def begin(self, device):
try: if not hasattr(self, 'pdf'):
self.pdf = PDFStream(self.file_object, (self.page_width, try:
self.page_height), self.pdf = PDFStream(self.file_object, (self.page_width,
compress=not DEBUG) self.page_height),
self.init_page() compress=self.compress)
except: except:
self.errors.append(traceback.format_exc()) self.errors.append(traceback.format_exc())
return False return False
return True return True
def end_page(self, start_new=True): def end_page(self):
self.pdf.restore_stack() if self.current_page_inited:
self.pdf.end_page() self.pdf.restore_stack()
if start_new: self.pdf.end_page()
self.init_page() self.current_page_inited = False
self.current_page_num += 1
def end(self): def end(self):
try: try:
self.end_page(start_new=False) self.end_page()
self.pdf.end() self.pdf.end()
except: except:
self.errors.append(traceback.format_exc()) self.errors.append(traceback.format_exc())
@ -296,16 +279,94 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawPixmap(self, rect, pixmap, source_rect): def drawPixmap(self, rect, pixmap, source_rect):
print ('TODO: drawPixmap() currently unimplemented') self.graphics_state(self)
source_rect = source_rect.toRect()
pixmap = (pixmap if source_rect == pixmap.rect() else
pixmap.copy(source_rect))
image = pixmap.toImage()
ref = self.add_image(image, pixmap.cacheKey())
if ref is not None:
self.pdf.draw_image(rect.x(), rect.height()+rect.y(), rect.width(),
-rect.height(), ref)
@store_error @store_error
def drawImage(self, rect, image, source_rect, flags=Qt.AutoColor): def drawImage(self, rect, image, source_rect, flags=Qt.AutoColor):
print ('TODO: drawImage() currently unimplemented') self.graphics_state(self)
source_rect = source_rect.toRect()
image = (image if source_rect == image.rect() else
image.copy(source_rect))
ref = self.add_image(image, image.cacheKey())
if ref is not None:
self.pdf.draw_image(rect.x(), rect.height()+rect.y(), rect.width(),
-rect.height(), ref)
def add_image(self, img, cache_key):
if img.isNull(): return
ref = self.pdf.get_image(cache_key)
if ref is not None:
return ref
fmt = img.format()
image = QImage(img)
if (image.depth() == 1 and img.colorTable().size() == 2 and
img.colorTable().at(0) == QColor(Qt.black).rgba() and
img.colorTable().at(1) == QColor(Qt.white).rgba()):
if fmt == QImage.Format_MonoLSB:
image = image.convertToFormat(QImage.Format_Mono)
fmt = QImage.Format_Mono
else:
if (fmt != QImage.Format_RGB32 and fmt != QImage.Format_ARGB32):
image = image.convertToFormat(QImage.Format_ARGB32)
fmt = QImage.Format_ARGB32
w = image.width()
h = image.height()
d = image.depth()
if fmt == QImage.Format_Mono:
bytes_per_line = (w + 7) >> 3
data = image.constBits().asstring(bytes_per_line * h)
return self.pdf.write_image(data, w, h, d, cache_key=cache_key)
ba = QByteArray()
buf = QBuffer(ba)
image.save(buf, 'jpeg', 94)
data = bytes(ba.data())
has_alpha = has_mask = False
soft_mask = mask = None
if fmt == QImage.Format_ARGB32:
tmask = image.constBits().asstring(4*w*h)[self.alpha_bit::4]
sdata = bytearray(tmask)
vals = set(sdata)
vals.discard(255)
has_mask = bool(vals)
vals.discard(0)
has_alpha = bool(vals)
if has_alpha:
soft_mask = self.pdf.write_image(tmask, w, h, 8)
elif has_mask:
# dither the soft mask to 1bit and add it. This also helps PDF
# viewers without transparency support
bytes_per_line = (w + 7) >> 3
mdata = bytearray(0 for i in xrange(bytes_per_line * h))
spos = mpos = 0
for y in xrange(h):
for x in xrange(w):
if sdata[spos]:
mdata[mpos + x>>3] |= (0x80 >> (x&7))
spos += 1
mpos += bytes_per_line
mdata = bytes(mdata)
mask = self.pdf.write_image(mdata, w, h, 1)
return self.pdf.write_image(data, w, h, 32, mask=mask, dct=True,
soft_mask=soft_mask, cache_key=cache_key)
@store_error @store_error
def updateState(self, state): def updateState(self, state):
self.graphics_state.read(state) self.graphics_state.read(state)
self.graphics_state(self)
def convert_path(self, path): def convert_path(self, path):
p = Path() p = Path()
@ -333,6 +394,7 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawPath(self, path): def drawPath(self, path):
self.graphics_state(self)
p = self.convert_path(path) p = self.convert_path(path)
fill_rule = {Qt.OddEvenFill:'evenodd', fill_rule = {Qt.OddEvenFill:'evenodd',
Qt.WindingFill:'winding'}[path.fillRule()] Qt.WindingFill:'winding'}[path.fillRule()]
@ -347,6 +409,7 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawPoints(self, points): def drawPoints(self, points):
self.graphics_state(self)
p = Path() p = Path()
for point in points: for point in points:
p.move_to(point.x(), point.y()) p.move_to(point.x(), point.y())
@ -355,105 +418,58 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawRects(self, rects): def drawRects(self, rects):
self.graphics_state(self)
for rect in rects: for rect in rects:
bl = rect.topLeft() bl = rect.topLeft()
self.pdf.draw_rect(bl.x(), bl.y(), rect.width(), rect.height(), self.pdf.draw_rect(bl.x(), bl.y(), rect.width(), rect.height(),
stroke=self.do_stroke, fill=self.do_fill) stroke=self.do_stroke, fill=self.do_fill)
def get_text_layout(self, text_item, text): def create_sfnt(self, text_item):
tl = QTextLayout(text, text_item.font(), self.paintDevice()) get_table = partial(self.qt_hack.get_sfnt_table, text_item)
self.text_option.setTextDirection(Qt.RightToLeft if ans = Font(Sfnt(get_table))
text_item.renderFlags() & text_item.RightToLeft else Qt.LeftToRight) glyph_map = self.qt_hack.get_glyph_map(text_item)
tl.setTextOption(self.text_option) gm = {}
return tl for uc, glyph_id in enumerate(glyph_map):
if glyph_id not in gm:
def update_glyph_map(self, text, indices, text_item, glyph_map): gm[glyph_id] = unichr(uc)
''' ans.full_glyph_map = gm
Map glyphs back to the unicode text they represent. return ans
'''
pos = 0
tl = self.get_text_layout(text_item, '')
indices = list(indices)
def get_glyphs(string):
tl.setText(string)
tl.beginLayout()
line = tl.createLine()
if not line.isValid():
tl.endLayout()
return []
line.setLineWidth(int(1e12))
tl.endLayout()
ans = []
for run in tl.glyphRuns():
ans.extend(run.glyphIndexes())
return ans
ipos = 0
while ipos < len(indices):
if indices[ipos] in glyph_map:
t = glyph_map[indices[ipos]]
if t == text[pos:pos+len(t)]:
pos += len(t)
ipos += 1
continue
found = False
for l in xrange(1, 10):
string = text[pos:pos+l]
g = get_glyphs(string)
if g and g[0] == indices[ipos]:
found = True
glyph_map[g[0]] = string
break
if not found:
self.debug.append(
'Failed to find glyph->unicode mapping for text: %s'%text)
break
ipos += 1
pos += l
return text[pos:]
@store_error @store_error
def drawTextItem(self, point, text_item): def drawTextItem(self, point, text_item):
# super(PdfEngine, self).drawTextItem(point+QPoint(0, 0), text_item) # super(PdfEngine, self).drawTextItem(point, text_item)
text = type(u'')(text_item.text()).replace('\n', ' ') self.graphics_state(self)
text = unicodedata.normalize('NFKC', text) gi = self.qt_hack.get_glyphs(point, text_item)
tl = self.get_text_layout(text_item, text) if not gi.indices:
tl.setPosition(point) sip.delete(gi)
tl.beginLayout()
line = tl.createLine()
if not line.isValid():
tl.endLayout()
return return
line.setLineWidth(int(1e12)) name = hash(bytes(gi.name))
tl.endLayout() if name not in self.fonts:
for run in tl.glyphRuns(): self.fonts[name] = self.create_sfnt(text_item)
rf = run.rawFont() metrics = self.fonts[name]
name = hash(bytes(rf.fontTable('name'))) for glyph_id in gi.indices:
if name not in self.fonts: try:
self.fonts[name] = Font(Sfnt(rf)) metrics.glyph_map[glyph_id] = metrics.full_glyph_map[glyph_id]
metrics = self.fonts[name] except (KeyError, ValueError):
indices = run.glyphIndexes() pass
text = self.update_glyph_map(text, indices, text_item, metrics.glyph_map) glyphs = []
glyphs = [] pdf_pos = point
pdf_pos = point first_baseline = None
first_baseline = None for i, pos in enumerate(gi.positions):
for i, pos in enumerate(run.positions()): if first_baseline is None:
if first_baseline is None: first_baseline = pos.y()
first_baseline = pos.y() glyph_pos = pos
glyph_pos = point + pos delta = glyph_pos - pdf_pos
delta = glyph_pos - pdf_pos glyphs.append((delta.x(), pos.y()-first_baseline, gi.indices[i]))
glyphs.append((delta.x(), pos.y()-first_baseline, indices[i])) pdf_pos = glyph_pos
pdf_pos = glyph_pos
self.pdf.draw_glyph_run([1, 0, 0, -1, point.x(),
point.y()], rf.pixelSize(), metrics, glyphs)
self.pdf.draw_glyph_run([1, 0, 0, -1, point.x(),
point.y()], gi.size, metrics, glyphs)
sip.delete(gi)
@store_error @store_error
def drawPolygon(self, points, mode): def drawPolygon(self, points, mode):
self.graphics_state(self)
if not points: return if not points: return
p = Path() p = Path()
p.move_to(points[0].x(), points[0].y()) p.move_to(points[0].x(), points[0].y())
@ -465,6 +481,9 @@ class PdfEngine(QPaintEngine):
self.pdf.draw_path(p, stroke=True, fill_rule=fill_rule, self.pdf.draw_path(p, stroke=True, fill_rule=fill_rule,
fill=(mode in (self.OddEvenMode, self.WindingMode, self.ConvexMode))) fill=(mode in (self.OddEvenMode, self.WindingMode, self.ConvexMode)))
def set_metadata(self, *args, **kwargs):
self.pdf.set_metadata(*args, **kwargs)
def __enter__(self): def __enter__(self):
self.pdf.save_stack() self.pdf.save_stack()
self.saved_ps = (self.do_stroke, self.do_fill) self.saved_ps = (self.do_stroke, self.do_fill)
@ -477,23 +496,26 @@ class PdfDevice(QPaintDevice): # {{{
def __init__(self, file_object, page_size=A4, left_margin=inch, def __init__(self, file_object, page_size=A4, left_margin=inch,
top_margin=inch, right_margin=inch, bottom_margin=inch): top_margin=inch, right_margin=inch, bottom_margin=inch,
xdpi=1200, ydpi=1200, errors=print, debug=print, compress=True):
QPaintDevice.__init__(self) QPaintDevice.__init__(self)
self.xdpi, self.ydpi = xdpi, ydpi
self.page_width, self.page_height = page_size self.page_width, self.page_height = page_size
self.body_width = self.page_width - left_margin - right_margin self.body_width = self.page_width - left_margin - right_margin
self.body_height = self.page_height - top_margin - bottom_margin self.body_height = self.page_height - top_margin - bottom_margin
self.engine = PdfEngine(file_object, self.page_width, self.page_height, self.engine = PdfEngine(file_object, self.page_width, self.page_height,
left_margin, top_margin, right_margin, left_margin, top_margin, right_margin,
bottom_margin, self.width(), self.height()) bottom_margin, self.width(), self.height(),
errors=errors, debug=debug, compress=compress)
def paintEngine(self): def paintEngine(self):
return self.engine return self.engine
def metric(self, m): def metric(self, m):
if m in (self.PdmDpiX, self.PdmPhysicalDpiX): if m in (self.PdmDpiX, self.PdmPhysicalDpiX):
return XDPI return self.xdpi
if m in (self.PdmDpiY, self.PdmPhysicalDpiY): if m in (self.PdmDpiY, self.PdmPhysicalDpiY):
return YDPI return self.ydpi
if m == self.PdmDepth: if m == self.PdmDepth:
return 32 return 32
if m == self.PdmNumColors: if m == self.PdmNumColors:
@ -503,20 +525,43 @@ class PdfDevice(QPaintDevice): # {{{
if m == self.PdmHeightMM: if m == self.PdmHeightMM:
return int(round(self.body_height * 0.35277777777778)) return int(round(self.body_height * 0.35277777777778))
if m == self.PdmWidth: if m == self.PdmWidth:
return int(round(self.body_width * XDPI / 72.0)) return int(round(self.body_width * self.xdpi / 72.0))
if m == self.PdmHeight: if m == self.PdmHeight:
return int(round(self.body_height * YDPI / 72.0)) return int(round(self.body_height * self.ydpi / 72.0))
return 0 return 0
def end_page(self):
self.engine.end_page()
def init_page(self):
self.engine.init_page()
@property
def current_page_num(self):
return self.engine.current_page_num
@property
def errors_occurred(self):
return self.engine.errors_occurred
def to_px(self, pt, vertical=True):
return pt * (self.height()/self.page_height if vertical else
self.width()/self.page_width)
def set_metadata(self, *args, **kwargs):
self.engine.set_metadata(*args, **kwargs)
# }}} # }}}
if __name__ == '__main__': if __name__ == '__main__':
from PyQt4.Qt import (QBrush, QColor, QPoint) from PyQt4.Qt import (QBrush, QColor, QPoint, QPixmap)
QBrush, QColor, QPoint QBrush, QColor, QPoint, QPixmap
app = QApplication([]) app = QApplication([])
p = QPainter() p = QPainter()
with open('/tmp/painter.pdf', 'wb') as f: with open('/tmp/painter.pdf', 'wb') as f:
dev = PdfDevice(f) dev = PdfDevice(f, compress=False)
p.begin(dev) p.begin(dev)
dev.init_page()
xmax, ymax = p.viewport().width(), p.viewport().height() xmax, ymax = p.viewport().width(), p.viewport().height()
try: try:
p.drawRect(0, 0, xmax, ymax) p.drawRect(0, 0, xmax, ymax)
@ -536,6 +581,11 @@ if __name__ == '__main__':
# p.scale(1, 1.5) # p.scale(1, 1.5)
# p.restore() # p.restore()
# # p.scale(2, 2)
# # p.rotate(45)
# p.drawPixmap(0, 0, 2048, 2048, QPixmap(I('library.png')))
# p.drawRect(0, 0, 2048, 2048)
# p.save() # p.save()
# p.drawLine(0, 0, 5000, 0) # p.drawLine(0, 0, 5000, 0)
# p.rotate(45) # p.rotate(45)
@ -543,22 +593,19 @@ if __name__ == '__main__':
# p.restore() # p.restore()
f = p.font() f = p.font()
f.setPointSize(24) f.setPointSize(20)
# f.setLetterSpacing(f.PercentageSpacing, 200) # f.setLetterSpacing(f.PercentageSpacing, 200)
# f.setUnderline(True) # f.setUnderline(True)
# f.setOverline(True) # f.setOverline(True)
# f.setStrikeOut(True) # f.setStrikeOut(True)
f.setFamily('Calibri') f.setFamily('Calibri')
p.setFont(f) p.setFont(f)
# p.setPen(QColor(0, 0, 255))
# p.scale(2, 2) # p.scale(2, 2)
# p.rotate(45) # p.rotate(45)
# p.setPen(QColor(0, 0, 255)) p.drawText(QPoint(300, 300), 'Some—text not Bys ū --- Д AV ff ff')
p.drawText(QPoint(100, 300), 'Some text ū --- Д AV ff ff')
finally: finally:
p.end() p.end()
for line in dev.engine.debug: if dev.engine.errors_occurred:
print (line)
if dev.engine.errors:
for err in dev.engine.errors: print (err)
raise SystemExit(1) raise SystemExit(1)

View File

@ -0,0 +1,280 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import json, os
from future_builtins import map
from math import floor
from PyQt4.Qt import (QObject, QPainter, Qt, QSize, QString, QTimer,
pyqtProperty, QEventLoop, QPixmap, QRect)
from PyQt4.QtWebKit import QWebView, QWebPage, QWebSettings
from calibre import fit_image
from calibre.ebooks.oeb.display.webview import load_html
from calibre.ebooks.pdf.render.common import (inch, cm, mm, pica, cicero,
didot, PAPER_SIZES)
from calibre.ebooks.pdf.render.engine import PdfDevice
from calibre.ebooks.pdf.render.links import Links
def get_page_size(opts, for_comic=False): # {{{
use_profile = not (opts.override_profile_size or
opts.output_profile.short_name == 'default' or
opts.output_profile.width > 9999)
if use_profile:
w = (opts.output_profile.comic_screen_size[0] if for_comic else
opts.output_profile.width)
h = (opts.output_profile.comic_screen_size[1] if for_comic else
opts.output_profile.height)
dpi = opts.output_profile.dpi
factor = 72.0 / dpi
page_size = (factor * w, factor * h)
else:
page_size = None
if opts.custom_size != None:
width, sep, height = opts.custom_size.partition('x')
if height:
try:
width = float(width)
height = float(height)
except:
pass
else:
if opts.unit == 'devicepixel':
factor = 72.0 / opts.output_profile.dpi
else:
{'point':1.0, 'inch':inch, 'cicero':cicero,
'didot':didot, 'pica':pica, 'millimeter':mm,
'centimeter':cm}[opts.unit]
page_size = (factor*width, factor*height)
if page_size is None:
page_size = PAPER_SIZES[opts.paper_size]
return page_size
# }}}
class Page(QWebPage): # {{{
def __init__(self, opts, log):
self.log = log
QWebPage.__init__(self)
settings = self.settings()
settings.setFontSize(QWebSettings.DefaultFontSize,
opts.pdf_default_font_size)
settings.setFontSize(QWebSettings.DefaultFixedFontSize,
opts.pdf_mono_font_size)
settings.setFontSize(QWebSettings.MinimumLogicalFontSize, 8)
settings.setFontSize(QWebSettings.MinimumFontSize, 8)
std = {'serif':opts.pdf_serif_family, 'sans':opts.pdf_sans_family,
'mono':opts.pdf_mono_family}.get(opts.pdf_standard_font,
opts.pdf_serif_family)
if std:
settings.setFontFamily(QWebSettings.StandardFont, std)
if opts.pdf_serif_family:
settings.setFontFamily(QWebSettings.SerifFont, opts.pdf_serif_family)
if opts.pdf_sans_family:
settings.setFontFamily(QWebSettings.SansSerifFont,
opts.pdf_sans_family)
if opts.pdf_mono_family:
settings.setFontFamily(QWebSettings.FixedFont, opts.pdf_mono_family)
def javaScriptConsoleMessage(self, msg, lineno, msgid):
self.log.debug(u'JS:', unicode(msg))
def javaScriptAlert(self, frame, msg):
self.log(unicode(msg))
# }}}
def draw_image_page(page_rect, painter, p, preserve_aspect_ratio=True):
if preserve_aspect_ratio:
aspect_ratio = float(p.width())/p.height()
nw, nh = page_rect.width(), page_rect.height()
if aspect_ratio > 1:
nh = int(page_rect.width()/aspect_ratio)
else: # Width is smaller than height
nw = page_rect.height()*aspect_ratio
__, nnw, nnh = fit_image(nw, nh, page_rect.width(),
page_rect.height())
dx = int((page_rect.width() - nnw)/2.)
dy = int((page_rect.height() - nnh)/2.)
page_rect.moveTo(dx, dy)
page_rect.setHeight(nnh)
page_rect.setWidth(nnw)
painter.drawPixmap(page_rect, p, p.rect())
class PDFWriter(QObject):
def _pass_json_value_getter(self):
val = json.dumps(self.bridge_value)
return QString(val)
def _pass_json_value_setter(self, value):
self.bridge_value = json.loads(unicode(value))
_pass_json_value = pyqtProperty(QString, fget=_pass_json_value_getter,
fset=_pass_json_value_setter)
def __init__(self, opts, log, cover_data=None, toc=None):
from calibre.gui2 import is_ok_to_use_qt
if not is_ok_to_use_qt():
raise Exception('Not OK to use Qt')
QObject.__init__(self)
self.logger = self.log = log
self.opts = opts
self.cover_data = cover_data
self.paged_js = None
self.toc = toc
self.loop = QEventLoop()
self.view = QWebView()
self.page = Page(opts, self.log)
self.view.setPage(self.page)
self.view.setRenderHints(QPainter.Antialiasing|
QPainter.TextAntialiasing|QPainter.SmoothPixmapTransform)
self.view.loadFinished.connect(self.render_html,
type=Qt.QueuedConnection)
for x in (Qt.Horizontal, Qt.Vertical):
self.view.page().mainFrame().setScrollBarPolicy(x,
Qt.ScrollBarAlwaysOff)
self.report_progress = lambda x, y: x
self.links = Links()
def dump(self, items, out_stream, pdf_metadata):
opts = self.opts
page_size = get_page_size(self.opts)
xdpi, ydpi = self.view.logicalDpiX(), self.view.logicalDpiY()
ml, mr = opts.margin_left, opts.margin_right
margin_side = min(ml, mr)
ml, mr = ml - margin_side, mr - margin_side
self.doc = PdfDevice(out_stream, page_size=page_size, left_margin=ml,
top_margin=0, right_margin=mr, bottom_margin=0,
xdpi=xdpi, ydpi=ydpi, errors=self.log.error,
debug=self.log.debug, compress=not
opts.uncompressed_pdf)
self.page.setViewportSize(QSize(self.doc.width(), self.doc.height()))
self.render_queue = items
self.total_items = len(items)
mt, mb = map(self.doc.to_px, (opts.margin_top, opts.margin_bottom))
ms = self.doc.to_px(margin_side, vertical=False)
self.margin_top, self.margin_size, self.margin_bottom = map(
lambda x:int(floor(x)), (mt, ms, mb))
self.painter = QPainter(self.doc)
self.doc.set_metadata(title=pdf_metadata.title,
author=pdf_metadata.author,
tags=pdf_metadata.tags)
self.painter.save()
try:
if self.cover_data is not None:
p = QPixmap()
p.loadFromData(self.cover_data)
if not p.isNull():
draw_image_page(QRect(0, 0, self.doc.width(), self.doc.height()),
self.painter, p,
preserve_aspect_ratio=self.opts.preserve_cover_aspect_ratio)
self.doc.end_page()
finally:
self.painter.restore()
QTimer.singleShot(0, self.render_book)
self.loop.exec_()
# TODO: Outline and links
self.painter.end()
if self.doc.errors_occurred:
raise Exception('PDF Output failed, see log for details')
def render_book(self):
if self.doc.errors_occurred:
return self.loop.exit(1)
try:
if not self.render_queue:
self.loop.exit()
else:
self.render_next()
except:
self.logger.exception('Rendering failed')
self.loop.exit(1)
def render_next(self):
item = unicode(self.render_queue.pop(0))
self.logger.debug('Processing %s...' % item)
self.current_item = item
load_html(item, self.view)
def render_html(self, ok):
if ok:
try:
self.do_paged_render()
except:
self.log.exception('Rendering failed')
self.loop.exit(1)
else:
# The document is so corrupt that we can't render the page.
self.logger.error('Document cannot be rendered.')
self.loop.exit(1)
return
done = self.total_items - len(self.render_queue)
self.report_progress(done/self.total_items,
_('Rendered %s'%os.path.basename(self.current_item)))
self.render_book()
@property
def current_page_num(self):
return self.doc.current_page_num
def do_paged_render(self):
if self.paged_js is None:
from calibre.utils.resources import compiled_coffeescript
self.paged_js = compiled_coffeescript('ebooks.oeb.display.utils')
self.paged_js += compiled_coffeescript('ebooks.oeb.display.indexing')
self.paged_js += compiled_coffeescript('ebooks.oeb.display.paged')
self.view.page().mainFrame().addToJavaScriptWindowObject("py_bridge", self)
evaljs = self.view.page().mainFrame().evaluateJavaScript
evaljs(self.paged_js)
evaljs('''
py_bridge.__defineGetter__('value', function() {
return JSON.parse(this._pass_json_value);
});
py_bridge.__defineSetter__('value', function(val) {
this._pass_json_value = JSON.stringify(val);
});
document.body.style.backgroundColor = "white";
paged_display.set_geometry(1, %d, %d, %d);
paged_display.layout();
paged_display.fit_images();
py_bridge.value = book_indexing.all_links_and_anchors();
'''%(self.margin_top, self.margin_size, self.margin_bottom))
amap = self.bridge_value
if not isinstance(amap, dict):
amap = {'links':[], 'anchors':{}} # Some javascript error occurred
self.links.add(self.current_item, self.current_page_num, amap['links'],
amap['anchors'])
mf = self.view.page().mainFrame()
while True:
self.doc.init_page()
self.painter.save()
mf.render(self.painter)
self.painter.restore()
nsl = evaljs('paged_display.next_screen_location()').toInt()
self.doc.end_page()
if not nsl[1] or nsl[0] <= 0:
break
evaljs('window.scrollTo(%d, 0)'%nsl[0])
if self.doc.errors_occurred:
break

View File

@ -0,0 +1,32 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os
from calibre.ebooks.pdf.render.common import Array, Name
class Destination(Array):
def __init__(self, start_page, pos):
super(Destination, self).__init__(
[start_page + pos['column'], Name('FitH'), pos['y']])
class Links(object):
def __init__(self):
self.anchors = {}
def add(self, base_path, start_page, links, anchors):
path = os.path.normcase(os.path.abspath(base_path))
self.anchors[path] = a = {}
a[None] = Destination(start_page, {'y':0, 'column':0})
for anchor, pos in anchors.iteritems():
a[anchor] = Destination(start_page, pos)

View File

@ -0,0 +1,66 @@
/*
* qt_hack.cpp
* Copyright (C) 2012 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "qt_hack.h"
#include <QtEndian>
#include "private/qtextengine_p.h"
#include "private/qfontengine_p.h"
GlyphInfo* get_glyphs(QPointF &p, const QTextItem &text_item) {
QTextItemInt ti = static_cast<const QTextItemInt &>(text_item);
QFontEngine *fe = ti.fontEngine;
qreal size = ti.fontEngine->fontDef.pixelSize;
#ifdef Q_WS_WIN
if (ti.fontEngine->type() == QFontEngine::Win) {
QFontEngineWin *fe = static_cast<QFontEngineWin *>(ti.fontEngine);
size = fe->tm.tmHeight;
}
#endif
QVarLengthArray<glyph_t> glyphs;
QVarLengthArray<QFixedPoint> positions;
QTransform m = QTransform::fromTranslate(p.x(), p.y());
fe->getGlyphPositions(ti.glyphs, m, ti.flags, glyphs, positions);
QVector<QPointF> points = QVector<QPointF>(positions.count());
for (int i = 0; i < positions.count(); i++) {
points[i].setX(positions[i].x.toReal());
points[i].setY(positions[i].y.toReal());
}
QVector<unsigned int> indices = QVector<unsigned int>(glyphs.count());
for (int i = 0; i < glyphs.count(); i++)
indices[i] = (unsigned int)glyphs[i];
const quint32 *tag = reinterpret_cast<const quint32 *>("name");
return new GlyphInfo(fe->getSfntTable(qToBigEndian(*tag)), size, points, indices);
}
GlyphInfo::GlyphInfo(const QByteArray& name, qreal size, const QVector<QPointF> &positions, const QVector<unsigned int> &indices) :name(name), positions(positions), size(size), indices(indices) {
}
QByteArray get_sfnt_table(const QTextItem &text_item, const char* tag_name) {
QTextItemInt ti = static_cast<const QTextItemInt &>(text_item);
const quint32 *tag = reinterpret_cast<const quint32 *>(tag_name);
return ti.fontEngine->getSfntTable(qToBigEndian(*tag));
}
QVector<unsigned int>* get_glyph_map(const QTextItem &text_item) {
QTextItemInt ti = static_cast<const QTextItemInt &>(text_item);
QVector<unsigned int> *ans = new QVector<unsigned int>(0x10000);
QGlyphLayoutArray<10> glyphs;
int nglyphs = 10;
for (uint uc = 0; uc < 0x10000; ++uc) {
QChar ch(uc);
ti.fontEngine->stringToCMap(&ch, 1, &glyphs, &nglyphs, QTextEngine::GlyphIndicesOnly);
(*ans)[uc] = glyphs.glyphs[0];
}
return ans;
}

View File

@ -0,0 +1,34 @@
/*
* qt_hack.h
* Copyright (C) 2012 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#pragma once
#include <QGlyphRun>
#include <QTextItem>
#include <QPointF>
class GlyphInfo {
public:
QByteArray name;
QVector<QPointF> positions;
qreal size;
QVector<unsigned int> indices;
GlyphInfo(const QByteArray &name, qreal size, const QVector<QPointF> &positions, const QVector<unsigned int> &indices);
private:
GlyphInfo(const GlyphInfo&);
GlyphInfo &operator=(const GlyphInfo&);
};
GlyphInfo* get_glyphs(QPointF &p, const QTextItem &text_item);
QByteArray get_sfnt_table(const QTextItem &text_item, const char* tag_name);
QVector<unsigned int>* get_glyph_map(const QTextItem &text_item);

View File

@ -0,0 +1,28 @@
//Define the SIP wrapper to the qt_hack code
//Author - Kovid Goyal <kovid@kovidgoyal.net>
%Module(name=qt_hack, version=1)
%Import QtCore/QtCoremod.sip
%Import QtGui/QtGuimod.sip
class GlyphInfo {
%TypeHeaderCode
#include <qt_hack.h>
%End
public:
QByteArray name;
qreal size;
QVector<QPointF> &positions;
QVector<unsigned int> indices;
GlyphInfo(const QByteArray &name, qreal size, const QVector<QPointF> &positions, const QVector<unsigned int> &indices);
private:
GlyphInfo(const GlyphInfo& g);
};
GlyphInfo* get_glyphs(QPointF &p, const QTextItem &text_item);
QByteArray get_sfnt_table(const QTextItem &text_item, const char* tag_name);
QVector<unsigned int>* get_glyph_map(const QTextItem &text_item);

View File

@ -88,6 +88,7 @@ class Page(Stream):
}) })
self.opacities = {} self.opacities = {}
self.fonts = {} self.fonts = {}
self.xobjects = {}
def set_opacity(self, opref): def set_opacity(self, opref):
if opref not in self.opacities: if opref not in self.opacities:
@ -101,6 +102,11 @@ class Page(Stream):
self.fonts[fontref] = 'F%d'%len(self.fonts) self.fonts[fontref] = 'F%d'%len(self.fonts)
return self.fonts[fontref] return self.fonts[fontref]
def add_image(self, imgref):
if imgref not in self.xobjects:
self.xobjects[imgref] = 'Image%d'%len(self.xobjects)
return self.xobjects[imgref]
def add_resources(self): def add_resources(self):
r = Dictionary() r = Dictionary()
if self.opacities: if self.opacities:
@ -113,6 +119,11 @@ class Page(Stream):
for ref, name in self.fonts.iteritems(): for ref, name in self.fonts.iteritems():
fonts[name] = ref fonts[name] = ref
r['Font'] = fonts r['Font'] = fonts
if self.xobjects:
xobjects = Dictionary()
for ref, name in self.xobjects.iteritems():
xobjects[name] = ref
r['XObject'] = xobjects
if r: if r:
self.page_dict['Resources'] = r self.page_dict['Resources'] = r
@ -122,7 +133,7 @@ class Page(Stream):
self.page_dict['Contents'] = contents self.page_dict['Contents'] = contents
self.add_resources() self.add_resources()
ret = objects.add(self.page_dict) ret = objects.add(self.page_dict)
objects.commit(ret, stream) # objects.commit(ret, stream)
return ret return ret
class Path(object): class Path(object):
@ -223,6 +234,35 @@ class HashingStream(object):
if raw: if raw:
self.last_char = raw[-1] self.last_char = raw[-1]
class Image(Stream):
def __init__(self, data, w, h, depth, mask, soft_mask, dct):
Stream.__init__(self)
self.width, self.height, self.depth = w, h, depth
self.mask, self.soft_mask = mask, soft_mask
if dct:
self.filters.append(Name('DCTDecode'))
else:
self.compress = True
self.write(data)
def add_extra_keys(self, d):
d['Type'] = Name('XObject')
d['Subtype']= Name('Image')
d['Width'] = self.width
d['Height'] = self.height
if self.depth == 1:
d['ImageMask'] = True
d['Decode'] = Array([1, 0])
else:
d['BitsPerComponent'] = 8
d['ColorSpace'] = Name('Device' + ('RGB' if self.depth == 32 else
'Gray'))
if self.mask is not None:
d['Mask'] = self.mask
if self.soft_mask is not None:
d['SMask'] = self.soft_mask
class PDFStream(object): class PDFStream(object):
PATH_OPS = { PATH_OPS = {
@ -253,6 +293,7 @@ class PDFStream(object):
'Producer':String(creator)}) 'Producer':String(creator)})
self.stroke_opacities, self.fill_opacities = {}, {} self.stroke_opacities, self.fill_opacities = {}, {}
self.font_manager = FontManager(self.objects, self.compress) self.font_manager = FontManager(self.objects, self.compress)
self.image_cache = {}
@property @property
def page_tree(self): def page_tree(self):
@ -262,6 +303,14 @@ class PDFStream(object):
def catalog(self): def catalog(self):
return self.objects[1] return self.objects[1]
def set_metadata(self, title=None, author=None, tags=None):
if title:
self.info['Title'] = String(title)
if author:
self.info['Author'] = String(author)
if tags:
self.info['Keywords'] = String(tags)
def write_line(self, byts=b''): def write_line(self, byts=b''):
byts = byts if isinstance(byts, bytes) else byts.encode('ascii') byts = byts if isinstance(byts, bytes) else byts.encode('ascii')
self.stream.write(byts + EOL) self.stream.write(byts + EOL)
@ -368,10 +417,26 @@ class PDFStream(object):
self.current_page.write('%s Tm '%' '.join(map(type(u''), transform))) self.current_page.write('%s Tm '%' '.join(map(type(u''), transform)))
for x, y, glyph_id in glyphs: for x, y, glyph_id in glyphs:
self.current_page.write('%g %g Td '%(x, y)) self.current_page.write('%g %g Td '%(x, y))
serialize(GlyphIndex(glyph_id, self.compress), self.current_page) serialize(GlyphIndex(glyph_id), self.current_page)
self.current_page.write(' Tj ') self.current_page.write(' Tj ')
self.current_page.write_line(b' ET') self.current_page.write_line(b' ET')
def get_image(self, cache_key):
return self.image_cache.get(cache_key, None)
def write_image(self, data, w, h, depth, dct=False, mask=None,
soft_mask=None, cache_key=None):
imgobj = Image(data, w, h, depth, mask, soft_mask, dct)
self.image_cache[cache_key] = r = self.objects.add(imgobj)
self.objects.commit(r, self.stream)
return r
def draw_image(self, x, y, xscale, yscale, imgref):
name = self.current_page.add_image(imgref)
self.current_page.write('q %g 0 0 %g %g %g cm '%(xscale, yscale, x, y))
serialize(Name(name), self.current_page)
self.current_page.write_line(' Do Q')
def end(self): def end(self):
if self.current_page.getvalue(): if self.current_page.getvalue():
self.end_page() self.end_page()

View File

@ -9,18 +9,16 @@ Write content to PDF.
''' '''
import os, shutil, json import os, shutil, json
from future_builtins import map
from PyQt4.Qt import (QEventLoop, QObject, QPrinter, QSizeF, Qt, QPainter, from PyQt4.Qt import (QEventLoop, QObject, QPrinter, QSizeF, Qt, QPainter,
QPixmap, QTimer, pyqtProperty, QString, QSize) QPixmap, QTimer, pyqtProperty, QString, QSize)
from PyQt4.QtWebKit import QWebView, QWebPage, QWebSettings from PyQt4.QtWebKit import QWebView, QWebPage, QWebSettings
from calibre.ptempfile import PersistentTemporaryDirectory from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.ebooks.pdf.pageoptions import (unit, paper_size, orientation) from calibre.ebooks.pdf.pageoptions import (unit, paper_size)
from calibre.ebooks.pdf.outline_writer import Outline from calibre.ebooks.pdf.outline_writer import Outline
from calibre.ebooks.metadata import authors_to_string
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre import (__appname__, __version__, fit_image, isosx, force_unicode) from calibre import (__appname__, __version__, fit_image, isosx)
from calibre.ebooks.oeb.display.webview import load_html from calibre.ebooks.oeb.display.webview import load_html
def get_custom_size(opts): def get_custom_size(opts):
@ -52,7 +50,7 @@ def get_pdf_printer(opts, for_comic=False, output_file_name=None): # {{{
printer.setPaperSize(paper_size(opts.paper_size)) printer.setPaperSize(paper_size(opts.paper_size))
else: else:
if opts.output_profile.short_name == 'default' or \ if opts.output_profile.short_name == 'default' or \
opts.output_profile.width > 9999: opts.output_profile.width > 9999 or opts.override_profile_size:
if custom_size is None: if custom_size is None:
printer.setPaperSize(paper_size(opts.paper_size)) printer.setPaperSize(paper_size(opts.paper_size))
else: else:
@ -72,7 +70,6 @@ def get_pdf_printer(opts, for_comic=False, output_file_name=None): # {{{
else: else:
printer.setPageMargins(opts.margin_left, opts.margin_top, printer.setPageMargins(opts.margin_left, opts.margin_top,
opts.margin_right, opts.margin_bottom, QPrinter.Point) opts.margin_right, opts.margin_bottom, QPrinter.Point)
printer.setOrientation(orientation(opts.orientation))
printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFormat(QPrinter.PdfFormat)
printer.setFullPage(for_comic) printer.setFullPage(for_comic)
if output_file_name: if output_file_name:
@ -103,24 +100,6 @@ def draw_image_page(printer, painter, p, preserve_aspect_ratio=True):
painter.drawPixmap(page_rect, p, p.rect()) painter.drawPixmap(page_rect, p, p.rect())
class PDFMetadata(object): # {{{
def __init__(self, oeb_metadata=None):
self.title = _(u'Unknown')
self.author = _(u'Unknown')
self.tags = u''
if oeb_metadata != None:
if len(oeb_metadata.title) >= 1:
self.title = oeb_metadata.title[0].value
if len(oeb_metadata.creator) >= 1:
self.author = authors_to_string([x.value for x in oeb_metadata.creator])
if oeb_metadata.subject:
self.tags = u', '.join(map(unicode, oeb_metadata.subject))
self.title = force_unicode(self.title)
self.author = force_unicode(self.author)
# }}}
class Page(QWebPage): # {{{ class Page(QWebPage): # {{{
def __init__(self, opts, log): def __init__(self, opts, log):

View File

@ -151,7 +151,7 @@ class AddAction(InterfaceAction):
Add an empty book item to the library. This does not import any formats Add an empty book item to the library. This does not import any formats
from a book file. from a book file.
''' '''
author = None author = series = None
index = self.gui.library_view.currentIndex() index = self.gui.library_view.currentIndex()
if index.isValid(): if index.isValid():
raw = index.model().db.authors(index.row()) raw = index.model().db.authors(index.row())
@ -159,16 +159,27 @@ class AddAction(InterfaceAction):
authors = [a.strip().replace('|', ',') for a in raw.split(',')] authors = [a.strip().replace('|', ',') for a in raw.split(',')]
if authors: if authors:
author = authors[0] author = authors[0]
dlg = AddEmptyBookDialog(self.gui, self.gui.library_view.model().db, author) series = index.model().db.series(index.row())
dlg = AddEmptyBookDialog(self.gui, self.gui.library_view.model().db,
author, series)
if dlg.exec_() == dlg.Accepted: if dlg.exec_() == dlg.Accepted:
num = dlg.qty_to_add num = dlg.qty_to_add
series = dlg.selected_series
db = self.gui.library_view.model().db
ids = []
for x in xrange(num): for x in xrange(num):
mi = MetaInformation(_('Unknown'), dlg.selected_authors) mi = MetaInformation(_('Unknown'), dlg.selected_authors)
self.gui.library_view.model().db.import_book(mi, []) if series:
mi.series = series
mi.series_index = db.get_next_series_num_for(series)
ids.append(db.import_book(mi, []))
self.gui.library_view.model().books_added(num) self.gui.library_view.model().books_added(num)
if hasattr(self.gui, 'db_images'): if hasattr(self.gui, 'db_images'):
self.gui.db_images.reset() self.gui.db_images.reset()
self.gui.tags_view.recount() self.gui.tags_view.recount()
if ids:
ids.reverse()
self.gui.library_view.select_rows(ids)
def add_isbns(self, books, add_tags=[]): def add_isbns(self, books, add_tags=[]):
self.isbn_books = list(books) self.isbn_books = list(books)

View File

@ -10,12 +10,14 @@ from functools import partial
from threading import Thread from threading import Thread
from contextlib import closing from contextlib import closing
from PyQt4.Qt import QToolButton from PyQt4.Qt import (QToolButton, QDialog, QGridLayout, QIcon, QLabel,
QCheckBox, QDialogButtonBox)
from calibre.gui2.actions import InterfaceAction from calibre.gui2.actions import InterfaceAction
from calibre.gui2 import (error_dialog, Dispatcher, warning_dialog, gprefs, from calibre.gui2 import (error_dialog, Dispatcher, warning_dialog, gprefs,
info_dialog) info_dialog, choose_dir)
from calibre.gui2.dialogs.progress import ProgressDialog from calibre.gui2.dialogs.progress import ProgressDialog
from calibre.gui2.widgets import HistoryLineEdit
from calibre.utils.config import prefs, tweaks from calibre.utils.config import prefs, tweaks
from calibre.utils.date import now from calibre.utils.date import now
@ -125,6 +127,45 @@ class Worker(Thread): # {{{
# }}} # }}}
class ChooseLibrary(QDialog): # {{{
def __init__(self, parent):
super(ChooseLibrary, self).__init__(parent)
d = self
d.l = l = QGridLayout()
d.setLayout(l)
d.setWindowTitle(_('Choose library'))
la = d.la = QLabel(_('Library &path:'))
l.addWidget(la, 0, 0)
le = d.le = HistoryLineEdit(d)
le.initialize('choose_library_for_copy')
l.addWidget(le, 0, 1)
la.setBuddy(le)
b = d.b = QToolButton(d)
b.setIcon(QIcon(I('document_open.png')))
b.setToolTip(_('Browse for library'))
b.clicked.connect(self.browse)
l.addWidget(b, 0, 2)
self.c = c = QCheckBox(_('&Delete after copy'))
l.addWidget(c, 1, 0, 1, 3)
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
bb.accepted.connect(self.accept)
bb.rejected.connect(self.reject)
l.addWidget(bb, 2, 0, 1, 3)
le.setMinimumWidth(350)
self.resize(self.sizeHint())
def browse(self):
d = choose_dir(self, 'choose_library_for_copy',
_('Choose Library'))
if d:
self.le.setText(d)
@property
def args(self):
return (unicode(self.le.text()), self.c.isChecked())
# }}}
class CopyToLibraryAction(InterfaceAction): class CopyToLibraryAction(InterfaceAction):
name = 'Copy To Library' name = 'Copy To Library'
@ -166,8 +207,20 @@ class CopyToLibraryAction(InterfaceAction):
partial(self.copy_to_library, loc, delete_after=True)) partial(self.copy_to_library, loc, delete_after=True))
self.menu.addSeparator() self.menu.addSeparator()
self.menu.addAction(_('Choose library by path...'), self.choose_library)
self.qaction.setVisible(bool(locations)) self.qaction.setVisible(bool(locations))
def choose_library(self):
d = ChooseLibrary(self.gui)
if d.exec_() == d.Accepted:
path, delete_after = d.args
db = self.gui.library_view.model().db
current = os.path.normcase(os.path.abspath(db.library_path))
if current == os.path.normcase(os.path.abspath(path)):
return error_dialog(self.gui, _('Cannot copy'),
_('Cannot copy to current library.'), show=True)
self.copy_to_library(path, delete_after)
def copy_to_library(self, loc, delete_after=False): def copy_to_library(self, loc, delete_after=False):
rows = self.gui.library_view.selectionModel().selectedRows() rows = self.gui.library_view.selectionModel().selectedRows()
if not rows or len(rows) == 0: if not rows or len(rows) == 0:

View File

@ -18,16 +18,17 @@ class PluginWidget(Widget, Ui_Form):
ICON = I('mimetypes/pdf.png') ICON = I('mimetypes/pdf.png')
def __init__(self, parent, get_option, get_help, db=None, book_id=None): def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent, ['paper_size', 'custom_size', Widget.__init__(self, parent, [
'orientation', 'preserve_cover_aspect_ratio', 'pdf_serif_family', 'override_profile_size', 'paper_size', 'custom_size',
'preserve_cover_aspect_ratio', 'pdf_serif_family', 'unit',
'pdf_sans_family', 'pdf_mono_family', 'pdf_standard_font', 'pdf_sans_family', 'pdf_mono_family', 'pdf_standard_font',
'pdf_default_font_size', 'pdf_mono_font_size']) 'pdf_default_font_size', 'pdf_mono_font_size'])
self.db, self.book_id = db, book_id self.db, self.book_id = db, book_id
for x in get_option('paper_size').option.choices: for x in get_option('paper_size').option.choices:
self.opt_paper_size.addItem(x) self.opt_paper_size.addItem(x)
for x in get_option('orientation').option.choices: for x in get_option('unit').option.choices:
self.opt_orientation.addItem(x) self.opt_unit.addItem(x)
for x in get_option('pdf_standard_font').option.choices: for x in get_option('pdf_standard_font').option.choices:
self.opt_pdf_standard_font.addItem(x) self.opt_pdf_standard_font.addItem(x)

View File

@ -14,7 +14,27 @@
<string>Form</string> <string>Form</string>
</property> </property>
<layout class="QFormLayout" name="formLayout"> <layout class="QFormLayout" name="formLayout">
<item row="1" column="0"> <property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
</property>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_10">
<property name="text">
<string>&lt;b&gt;Note:&lt;/b&gt; The paper size settings below only take effect if you enable the &quot;Override&quot; checkbox below. Otherwise the size from the output profile will be used.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="opt_override_profile_size">
<property name="text">
<string>&amp;Override paper size set in output profile</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">
<string>&amp;Paper Size:</string> <string>&amp;Paper Size:</string>
@ -24,21 +44,8 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1">
<widget class="QComboBox" name="opt_paper_size"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Orientation:</string>
</property>
<property name="buddy">
<cstring>opt_orientation</cstring>
</property>
</widget>
</item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="QComboBox" name="opt_orientation"/> <widget class="QComboBox" name="opt_paper_size"/>
</item> </item>
<item row="3" column="0"> <item row="3" column="0">
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">
@ -51,7 +58,24 @@
</widget> </widget>
</item> </item>
<item row="3" column="1"> <item row="3" column="1">
<widget class="QLineEdit" name="opt_custom_size"/> <layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="opt_custom_size"/>
</item>
<item>
<widget class="QLabel" name="label_11">
<property name="text">
<string>&amp;Unit:</string>
</property>
<property name="buddy">
<cstring>opt_unit</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="opt_unit"/>
</item>
</layout>
</item> </item>
<item row="4" column="0" colspan="2"> <item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="opt_preserve_cover_aspect_ratio"> <widget class="QCheckBox" name="opt_preserve_cover_aspect_ratio">
@ -60,19 +84,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="11" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>213</height>
</size>
</property>
</spacer>
</item>
<item row="5" column="0"> <item row="5" column="0">
<widget class="QLabel" name="label_4"> <widget class="QLabel" name="label_4">
<property name="text"> <property name="text">
@ -159,15 +170,18 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="0" colspan="2"> <item row="11" column="0">
<widget class="QLabel" name="label_10"> <spacer name="verticalSpacer">
<property name="text"> <property name="orientation">
<string>&lt;b&gt;Note:&lt;/b&gt; The paper size settings below only take effect if you have set the output profile to the default output profile. Otherwise the output profile will override these settings.</string> <enum>Qt::Vertical</enum>
</property> </property>
<property name="wordWrap"> <property name="sizeHint" stdset="0">
<bool>true</bool> <size>
<width>20</width>
<height>213</height>
</size>
</property> </property>
</widget> </spacer>
</item> </item>
</layout> </layout>
</widget> </widget>

View File

@ -12,7 +12,7 @@ from calibre.utils.config import tweaks
class AddEmptyBookDialog(QDialog): class AddEmptyBookDialog(QDialog):
def __init__(self, parent, db, author): def __init__(self, parent, db, author, series=None):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
self.db = db self.db = db
@ -45,6 +45,22 @@ class AddEmptyBookDialog(QDialog):
self.clear_button.clicked.connect(self.reset_author) self.clear_button.clicked.connect(self.reset_author)
self._layout.addWidget(self.clear_button, 3, 1, 1, 1) self._layout.addWidget(self.clear_button, 3, 1, 1, 1)
self.series_label = QLabel(_('Set the series of the new books to:'))
self._layout.addWidget(self.series_label, 4, 0, 1, 2)
self.series_combo = EditWithComplete(self)
self.authors_combo.setSizeAdjustPolicy(
self.authors_combo.AdjustToMinimumContentsLengthWithIcon)
self.series_combo.setEditable(True)
self._layout.addWidget(self.series_combo, 5, 0, 1, 1)
self.initialize_series(db, series)
self.sclear_button = QToolButton(self)
self.sclear_button.setIcon(QIcon(I('trash.png')))
self.sclear_button.setToolTip(_('Reset series'))
self.sclear_button.clicked.connect(self.reset_series)
self._layout.addWidget(self.sclear_button, 5, 1, 1, 1)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self.accept) button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject) button_box.rejected.connect(self.reject)
@ -54,6 +70,9 @@ class AddEmptyBookDialog(QDialog):
def reset_author(self, *args): def reset_author(self, *args):
self.authors_combo.setEditText(_('Unknown')) self.authors_combo.setEditText(_('Unknown'))
def reset_series(self):
self.series_combo.setEditText('')
def initialize_authors(self, db, author): def initialize_authors(self, db, author):
au = author au = author
if not au: if not au:
@ -65,6 +84,11 @@ class AddEmptyBookDialog(QDialog):
self.authors_combo.set_add_separator(tweaks['authors_completer_append_separator']) self.authors_combo.set_add_separator(tweaks['authors_completer_append_separator'])
self.authors_combo.update_items_cache(db.all_author_names()) self.authors_combo.update_items_cache(db.all_author_names())
def initialize_series(self, db, series):
self.series_combo.show_initial_value(series or '')
self.series_combo.update_items_cache(db.all_series_names())
self.series_combo.set_separator(None)
@property @property
def qty_to_add(self): def qty_to_add(self):
return self.qty_spinbox.value() return self.qty_spinbox.value()
@ -73,6 +97,10 @@ class AddEmptyBookDialog(QDialog):
def selected_authors(self): def selected_authors(self):
return string_to_authors(unicode(self.authors_combo.text())) return string_to_authors(unicode(self.authors_combo.text()))
@property
def selected_series(self):
return unicode(self.series_combo.text())
if __name__ == '__main__': if __name__ == '__main__':
app = QApplication([]) app = QApplication([])
d = AddEmptyBookDialog() d = AddEmptyBookDialog()

View File

@ -227,7 +227,7 @@ p, li { white-space: pre-wrap; }
<number>1</number> <number>1</number>
</property> </property>
<property name="maximum"> <property name="maximum">
<number>365</number> <number>36500</number>
</property> </property>
<property name="value"> <property name="value">
<number>7</number> <number>7</number>

View File

@ -52,10 +52,9 @@ class ColumnColor(object):
self.mi = None self.mi = None
def __call__(self, id_, key, fmt, db, formatter, color_cache, colors): def __call__(self, id_, key, fmt, db, formatter, color_cache, colors):
if id_ in color_cache: if id_ in color_cache and key in color_cache[id_]:
if key in color_cache[id_]: self.mi = None
self.mi = None return color_cache[id_][key]
return color_cache[id_][key]
try: try:
if self.mi is None: if self.mi is None:
self.mi = db.get_metadata(id_, index_is_id=True) self.mi = db.get_metadata(id_, index_is_id=True)
@ -763,16 +762,15 @@ class BooksModel(QAbstractTableModel): # {{{
self.column_color.mi = None self.column_color.mi = None
if self.color_row_fmt_cache is None: if self.color_row_fmt_cache is None:
d = dict(self.db.prefs['column_color_rules']) self.color_row_fmt_cache = tuple(fmt for key, fmt in
self.color_row_fmt_cache = d.get(color_row_key, '') self.db.prefs['column_color_rules'] if key == color_row_key)
for k, fmt in self.db.prefs['column_color_rules']: for k, fmt in self.db.prefs['column_color_rules']:
if k == key: if k == key:
col = self.column_color(id_, key, fmt, self.db, ccol = self.column_color(id_, key, fmt, self.db,
self.formatter, self.color_cache, self.colors) self.formatter, self.color_cache, self.colors)
if col is not None: if ccol is not None:
return col return ccol
if self.is_custom_column(key) and \ if self.is_custom_column(key) and \
self.custom_columns[key]['datatype'] == 'enumeration': self.custom_columns[key]['datatype'] == 'enumeration':
@ -789,12 +787,11 @@ class BooksModel(QAbstractTableModel): # {{{
except: except:
pass pass
if self.color_row_fmt_cache: for fmt in self.color_row_fmt_cache:
key = color_row_key ccol = self.column_color(id_, color_row_key, fmt, self.db,
col = self.column_color(id_, key, self.color_row_fmt_cache, self.formatter, self.color_cache, self.colors)
self.db, self.formatter, self.color_cache, self.colors) if ccol is not None:
if col is not None: return ccol
return col
self.column_color.mi = None self.column_color.mi = None
return NONE return NONE

View File

@ -106,8 +106,8 @@ class ConditionEditor(QWidget): # {{{
self.column_box.addItem('', '') self.column_box.addItem('', '')
for key in sorted( for key in sorted(
conditionable_columns(fm), conditionable_columns(fm),
key=sort_key): key=lambda(key): sort_key(fm[key]['name'])):
self.column_box.addItem(key, key) self.column_box.addItem(fm[key]['name'], key)
self.column_box.setCurrentIndex(0) self.column_box.setCurrentIndex(0)
self.column_box.currentIndexChanged.connect(self.init_action_box) self.column_box.currentIndexChanged.connect(self.init_action_box)
@ -314,7 +314,8 @@ class RuleEditor(QDialog): # {{{
b.setSizeAdjustPolicy(b.AdjustToMinimumContentsLengthWithIcon) b.setSizeAdjustPolicy(b.AdjustToMinimumContentsLengthWithIcon)
b.setMinimumContentsLength(15) b.setMinimumContentsLength(15)
for key in sorted(displayable_columns(fm), key=sort_key): for key in sorted(displayable_columns(fm),
key=lambda(k): sort_key(fm[k]['name']) if k != color_row_key else 0):
name = all_columns_string if key == color_row_key else fm[key]['name'] name = all_columns_string if key == color_row_key else fm[key]['name']
if name: if name:
self.column_box.addItem(name, key) self.column_box.addItem(name, key)
@ -427,9 +428,11 @@ class RulesModel(QAbstractListModel): # {{{
col, rule = self.rules[row] col, rule = self.rules[row]
except: except:
return None return None
if col == color_row_key:
col = all_columns_string
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
if col == color_row_key:
col = all_columns_string
else:
col = self.fm[col]['name']
return self.rule_to_html(col, rule) return self.rule_to_html(col, rule)
if role == Qt.UserRole: if role == Qt.UserRole:
return (col, rule) return (col, rule)
@ -486,6 +489,7 @@ class RulesModel(QAbstractListModel): # {{{
def condition_to_html(self, condition): def condition_to_html(self, condition):
c, a, v = condition c, a, v = condition
c = self.fm[c]['name']
action_name = a action_name = a
for actions in ConditionEditor.ACTION_MAP.itervalues(): for actions in ConditionEditor.ACTION_MAP.itervalues():
for trans, ac in actions: for trans, ac in actions:
@ -569,9 +573,9 @@ class EditRules(QWidget): # {{{
self.changed.emit() self.changed.emit()
def add_rule(self): def add_rule(self):
d = RuleEditor(self.model.fm) d = RuleEditor(self.model.fm)
d.add_blank_condition() d.add_blank_condition()
self._add_rule(d) self._add_rule(d)
def add_advanced(self): def add_advanced(self):
td = TemplateDialog(self, '', mi=self.mi, fm=self.fm, color_field='') td = TemplateDialog(self, '', mi=self.mi, fm=self.fm, color_field='')

View File

@ -544,7 +544,7 @@ class DocumentView(QWebView): # {{{
self.goto_location_action.setMenu(self.goto_location_menu) self.goto_location_action.setMenu(self.goto_location_menu)
self.grabGesture(Qt.SwipeGesture) self.grabGesture(Qt.SwipeGesture)
self.restore_fonts_action = QAction(_('Normal font size'), self) self.restore_fonts_action = QAction(_('Default font size'), self)
self.restore_fonts_action.setCheckable(True) self.restore_fonts_action.setCheckable(True)
self.restore_fonts_action.triggered.connect(self.restore_font_size) self.restore_fonts_action.triggered.connect(self.restore_font_size)

View File

@ -697,11 +697,11 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.view.shrink_fonts() self.view.shrink_fonts()
def magnification_changed(self, val): def magnification_changed(self, val):
tt = _('Make font size %(which)s\nCurrent magnification: %(mag).1f') tt = _('%(which)s font size\nCurrent magnification: %(mag).1f')
self.action_font_size_larger.setToolTip( self.action_font_size_larger.setToolTip(
tt %dict(which=_('larger'), mag=val)) tt %dict(which=_('Increase'), mag=val))
self.action_font_size_smaller.setToolTip( self.action_font_size_smaller.setToolTip(
tt %dict(which=_('smaller'), mag=val)) tt %dict(which=_('Decrease'), mag=val))
self.action_font_size_larger.setEnabled(self.view.multiplier < 3) self.action_font_size_larger.setEnabled(self.view.multiplier < 3)
self.action_font_size_smaller.setEnabled(self.view.multiplier > 0.2) self.action_font_size_smaller.setEnabled(self.view.multiplier > 0.2)

View File

@ -198,7 +198,7 @@
<normaloff>:/images/font_size_larger.png</normaloff>:/images/font_size_larger.png</iconset> <normaloff>:/images/font_size_larger.png</normaloff>:/images/font_size_larger.png</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Font size larger</string> <string>Increase font size</string>
</property> </property>
</action> </action>
<action name="action_font_size_smaller"> <action name="action_font_size_smaller">
@ -207,7 +207,7 @@
<normaloff>:/images/font_size_smaller.png</normaloff>:/images/font_size_smaller.png</iconset> <normaloff>:/images/font_size_smaller.png</normaloff>:/images/font_size_smaller.png</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Font size smaller</string> <string>Decrease font size</string>
</property> </property>
</action> </action>
<action name="action_table_of_contents"> <action name="action_table_of_contents">

View File

@ -11,11 +11,11 @@ from collections import OrderedDict
import cherrypy import cherrypy
from calibre.constants import filesystem_encoding from calibre.constants import filesystem_encoding, config_dir
from calibre import isbytestring, force_unicode, fit_image, \ from calibre import (isbytestring, force_unicode, fit_image,
prepare_string_for_xml prepare_string_for_xml, sanitize_file_name2)
from calibre.utils.filenames import ascii_filename from calibre.utils.filenames import ascii_filename
from calibre.utils.config import prefs from calibre.utils.config import prefs, JSONConfig
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.utils.magick import Image from calibre.utils.magick import Image
from calibre.library.comments import comments_to_html from calibre.library.comments import comments_to_html
@ -242,6 +242,8 @@ class BrowseServer(object):
connect('browse_category_icon', base_href+'/icon/{name}', connect('browse_category_icon', base_href+'/icon/{name}',
self.browse_icon) self.browse_icon)
self.icon_map = JSONConfig('gui').get('tags_browser_category_icons', {})
# Templates {{{ # Templates {{{
def browse_template(self, sort, category=True, initial_search=''): def browse_template(self, sort, category=True, initial_search=''):
@ -321,10 +323,18 @@ class BrowseServer(object):
if not hasattr(self, '__browse_icon_cache__'): if not hasattr(self, '__browse_icon_cache__'):
self.__browse_icon_cache__ = {} self.__browse_icon_cache__ = {}
if name not in self.__browse_icon_cache__: if name not in self.__browse_icon_cache__:
try: if name.startswith('_'):
data = I(name, data=True) name = sanitize_file_name2(name[1:])
except: try:
raise cherrypy.HTTPError(404, 'no icon named: %r'%name) with open(os.path.join(config_dir, 'tb_icons', name), 'rb') as f:
data = f.read()
except:
raise cherrypy.HTTPError(404, 'no icon named: %r'%name)
else:
try:
data = I(name, data=True)
except:
raise cherrypy.HTTPError(404, 'no icon named: %r'%name)
img = Image() img = Image()
img.load(data) img.load(data)
width, height = img.size width, height = img.size
@ -359,7 +369,9 @@ class BrowseServer(object):
if meta['is_custom'] and category not in displayed_custom_fields: if meta['is_custom'] and category not in displayed_custom_fields:
continue continue
# get the icon files # get the icon files
if category in category_icon_map: if category in self.icon_map:
icon = '_'+quote(self.icon_map[category])
elif category in category_icon_map:
icon = category_icon_map[category] icon = category_icon_map[category]
elif meta['is_custom']: elif meta['is_custom']:
icon = category_icon_map['custom:'] icon = category_icon_map['custom:']

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

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

Some files were not shown because too many files have changed in this diff Show More