'
__docformat__ = 'restructuredtext en'
-import textwrap
+import sys
from xml.sax.saxutils import escape
-from itertools import repeat
from lxml import etree
-from calibre.ebooks.oeb.base import XPath, XPNSMAP
-from calibre import guess_type
+from calibre import guess_type, strftime
+from calibre.ebooks.BeautifulSoup import BeautifulSoup
+from calibre.ebooks.oeb.base import XPath, XHTML_NS, XHTML
from calibre.library.comments import comments_to_html
+
+JACKET_XPATH = '//h:meta[@name="calibre-content" and @content="jacket"]'
+
class Jacket(object):
'''
Book jacket manipulation. Remove first image and insert comments at start of
book.
'''
- JACKET_TEMPLATE = textwrap.dedent(u'''\
-
-
- %(title)s
-
-
-
-
-
-
%(title)s
-
%(jacket)s
-
%(series)s
-
%(rating)s
-
%(tags)s
-
-
- %(comments)s
-
-
-
-
- ''')
+ def remove_images(self, item, limit=1):
+ path = XPath('//h:img[@src]')
+ removed = 0
+ for img in path(item.data):
+ if removed >= limit:
+ break
+ href = item.abshref(img.get('src'))
+ image = self.oeb.manifest.hrefs.get(href, None)
+ if image is not None:
+ self.oeb.manifest.remove(image)
+ img.getparent().remove(img)
+ removed += 1
+ return removed
def remove_first_image(self):
- path = XPath('//h:img[@src]')
- for i, item in enumerate(self.oeb.spine):
- if i > 2: break
- for img in path(item.data):
- href = item.abshref(img.get('src'))
- image = self.oeb.manifest.hrefs.get(href, None)
- if image is not None:
- self.log('Removing first image', img.get('src'))
- self.oeb.manifest.remove(image)
- img.getparent().remove(img)
- return
-
- def get_rating(self, rating):
- ans = ''
- if rating is None:
- return
- try:
- num = float(rating)/2
- except:
- return ans
- num = max(0, num)
- num = min(num, 5)
- if num < 1:
- return ans
- id, href = self.oeb.manifest.generate('star', 'star.png')
- self.oeb.manifest.add(id, href, 'image/png', data=I('star.png', data=True))
- ans = 'Rating: ' + ''.join(repeat('
'%href, num))
- return ans
+ for item in self.oeb.spine:
+ removed = self.remove_images(item)
+ if removed > 0:
+ self.log('Removed first image')
+ break
def insert_metadata(self, mi):
self.log('Inserting metadata into book...')
- comments = mi.comments
- if not comments:
- try:
- comments = unicode(self.oeb.metadata.description[0])
- except:
- comments = ''
- if not comments.strip():
- comments = ''
- orig_comments = comments
- if comments:
- comments = comments_to_html(comments)
- series = 'Series: ' + escape(mi.series if mi.series else '')
- if mi.series and mi.series_index is not None:
- series += escape(' [%s]'%mi.format_series_index())
- if not mi.series:
- series = ''
- tags = mi.tags
- if not tags:
- try:
- tags = map(unicode, self.oeb.metadata.subject)
- except:
- tags = []
- if tags:
- tags = 'Tags: ' + self.opts.dest.tags_to_string(tags)
- else:
- tags = ''
+
try:
- title = mi.title if mi.title else unicode(self.oeb.metadata.title[0])
+ tags = map(unicode, self.oeb.metadata.subject)
+ except:
+ tags = []
+
+ try:
+ comments = unicode(self.oeb.metadata.description[0])
+ except:
+ comments = ''
+
+ try:
+ title = unicode(self.oeb.metadata.title[0])
except:
title = _('Unknown')
- def generate_html(comments):
- return self.JACKET_TEMPLATE%dict(xmlns=XPNSMAP['h'],
- title=escape(title), comments=comments,
- jacket=escape(_('Book Jacket')), series=series,
- tags=tags, rating=self.get_rating(mi.rating))
- id, href = self.oeb.manifest.generate('jacket', 'jacket.xhtml')
- from calibre.ebooks.oeb.base import RECOVER_PARSER, XPath
- try:
- root = etree.fromstring(generate_html(comments), parser=RECOVER_PARSER)
- except:
- root = etree.fromstring(generate_html(escape(orig_comments)),
- parser=RECOVER_PARSER)
- jacket = XPath('//h:meta[@name="calibre-content" and @content="jacket"]')
- found = None
- for item in list(self.oeb.spine)[:4]:
- try:
- if jacket(item.data):
- found = item
- break
- except:
- continue
- if found is None:
- item = self.oeb.manifest.add(id, href, guess_type(href)[0], data=root)
- self.oeb.spine.insert(0, item, True)
- else:
- self.log('Found existing book jacket, replacing...')
- found.data = root
+ root = render_jacket(mi, self.opts.output_profile,
+ alt_title=title, alt_tags=tags,
+ alt_comments=comments)
+ id, href = self.oeb.manifest.generate('calibre_jacket', 'jacket.xhtml')
+ item = self.oeb.manifest.add(id, href, guess_type(href)[0], data=root)
+ self.oeb.spine.insert(0, item, True)
+
+ def remove_existing_jacket(self):
+ for x in self.oeb.spine[:4]:
+ if XPath(JACKET_XPATH)(x.data):
+ self.remove_images(x, limit=sys.maxint)
+ self.oeb.manifest.remove(x)
+ self.log('Removed existing jacket')
+ break
def __call__(self, oeb, opts, metadata):
+ '''
+ Add metadata in jacket.xhtml if specified in opts
+ If not specified, remove previous jacket instance
+ '''
self.oeb, self.opts, self.log = oeb, opts, oeb.log
+ self.remove_existing_jacket()
if opts.remove_first_image:
self.remove_first_image()
if opts.insert_metadata:
self.insert_metadata(metadata)
+
+# Render Jacket {{{
+
+def get_rating(rating, rchar):
+ ans = ''
+ try:
+ num = float(rating)/2
+ except:
+ return ans
+ num = max(0, num)
+ num = min(num, 5)
+ if num < 1:
+ return ans
+
+ ans = rchar * int(num)
+ return ans
+
+
+def render_jacket(mi, output_profile,
+ alt_title=_('Unknown'), alt_tags=[], alt_comments=''):
+ css = P('jacket/stylesheet.css', data=True).decode('utf-8')
+
+ try:
+ title_str = mi.title if mi.title else alt_title
+ except:
+ title_str = _('Unknown')
+ title = '%s' % (escape(title_str))
+
+ series = escape(mi.series if mi.series else '')
+ if mi.series and mi.series_index is not None:
+ series += escape(' [%s]'%mi.format_series_index())
+ if not mi.series:
+ series = ''
+
+ try:
+ pubdate = strftime(u'%Y', mi.pubdate.timetuple())
+ except:
+ pubdate = ''
+
+ rating = get_rating(mi.rating, output_profile.ratings_char)
+
+ tags = mi.tags if mi.tags else alt_tags
+ if tags:
+ tags = output_profile.tags_to_string(tags)
+ else:
+ tags = ''
+
+ comments = mi.comments if mi.comments else alt_comments
+ comments = comments.strip()
+ orig_comments = comments
+ if comments:
+ comments = comments_to_html(comments)
+
+ def generate_html(comments):
+ args = dict(xmlns=XHTML_NS,
+ title_str=title_str,
+ css=css,
+ title=title,
+ pubdate_label=_('Published'), pubdate=pubdate,
+ series_label=_('Series'), series=series,
+ rating_label=_('Rating'), rating=rating,
+ tags_label=_('Tags'), tags=tags,
+ comments=comments,
+ footer=''
+ )
+
+ generated_html = P('jacket/template.xhtml',
+ data=True).decode('utf-8').format(**args)
+
+ # Post-process the generated html to strip out empty header items
+ soup = BeautifulSoup(generated_html)
+ if not series:
+ series_tag = soup.find('tr', attrs={'class':'cbj_series'})
+ series_tag.extract()
+ if not rating:
+ rating_tag = soup.find('tr', attrs={'class':'cbj_rating'})
+ rating_tag.extract()
+ if not tags:
+ tags_tag = soup.find('tr', attrs={'class':'cbj_tags'})
+ tags_tag.extract()
+ if not pubdate:
+ pubdate_tag = soup.find('tr', attrs={'class':'cbj_pubdate'})
+ pubdate_tag.extract()
+ if output_profile.short_name != 'kindle':
+ hr_tag = soup.find('hr', attrs={'class':'cbj_kindle_banner_hr'})
+ hr_tag.extract()
+
+ return soup.renderContents(None)
+
+ from calibre.ebooks.oeb.base import RECOVER_PARSER
+
+ try:
+ root = etree.fromstring(generate_html(comments), parser=RECOVER_PARSER)
+ except:
+ try:
+ root = etree.fromstring(generate_html(escape(orig_comments)),
+ parser=RECOVER_PARSER)
+ except:
+ root = etree.fromstring(generate_html(''),
+ parser=RECOVER_PARSER)
+ return root
+
+# }}}
+
+def linearize_jacket(oeb):
+ for x in oeb.spine[:4]:
+ if XPath(JACKET_XPATH)(x.data):
+ for e in XPath('//h:table|//h:tr|//h:th')(x.data):
+ e.tag = XHTML('div')
+ for e in XPath('//h:td')(x.data):
+ e.tag = XHTML('span')
+ break
+
diff --git a/src/calibre/ebooks/oeb/transforms/rescale.py b/src/calibre/ebooks/oeb/transforms/rescale.py
index 55aafded5c..c3b4d6d40c 100644
--- a/src/calibre/ebooks/oeb/transforms/rescale.py
+++ b/src/calibre/ebooks/oeb/transforms/rescale.py
@@ -72,10 +72,13 @@ class RescaleImages(object):
Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
data = pixmap_to_data(img, format=ext)
else:
- im = im.resize((int(new_width), int(new_height)), PILImage.ANTIALIAS)
- of = cStringIO.StringIO()
- im.convert('RGB').save(of, ext)
- data = of.getvalue()
+ try:
+ im = im.resize((int(new_width), int(new_height)), PILImage.ANTIALIAS)
+ of = cStringIO.StringIO()
+ im.convert('RGB').save(of, ext)
+ data = of.getvalue()
+ except:
+ self.log.exception('Failed to rescale image')
if data is not None:
item.data = data
item.unload_data_from_memory()
diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py
index 1b61404589..e58dce5559 100644
--- a/src/calibre/gui2/__init__.py
+++ b/src/calibre/gui2/__init__.py
@@ -50,6 +50,7 @@ gprefs.defaults['action-layout-context-menu-device'] = (
gprefs.defaults['show_splash_screen'] = True
gprefs.defaults['toolbar_icon_size'] = 'medium'
gprefs.defaults['toolbar_text'] = 'auto'
+gprefs.defaults['show_child_bar'] = False
# }}}
diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py
index 57ad900fba..b2d1656367 100644
--- a/src/calibre/gui2/actions/__init__.py
+++ b/src/calibre/gui2/actions/__init__.py
@@ -71,6 +71,12 @@ class InterfaceAction(QObject):
all_locations = frozenset(['toolbar', 'toolbar-device', 'context-menu',
'context-menu-device'])
+ #: Type of action
+ #: 'current' means acts on the current view
+ #: 'global' means an action that does not act on the current view, but rather
+ #: on calibre as a whole
+ action_type = 'global'
+
def __init__(self, parent, site_customization):
QObject.__init__(self, parent)
self.setObjectName(self.name)
diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py
index f0ff794fab..add7bf1d5b 100644
--- a/src/calibre/gui2/actions/add.py
+++ b/src/calibre/gui2/actions/add.py
@@ -25,6 +25,7 @@ class AddAction(InterfaceAction):
action_spec = (_('Add books'), 'add_book.png',
_('Add books to the calibre library/device from files on your computer')
, _('A'))
+ action_type = 'current'
def genesis(self):
self._add_filesystem_book = self.Dispatcher(self.__add_filesystem_book)
diff --git a/src/calibre/gui2/actions/add_to_library.py b/src/calibre/gui2/actions/add_to_library.py
index 6fc0d5fb1f..05aea8f1dd 100644
--- a/src/calibre/gui2/actions/add_to_library.py
+++ b/src/calibre/gui2/actions/add_to_library.py
@@ -13,6 +13,7 @@ class AddToLibraryAction(InterfaceAction):
action_spec = (_('Add books to library'), 'add_book.png',
_('Add books to your calibre library from the connected device'), None)
dont_add_to = frozenset(['toolbar', 'context-menu'])
+ action_type = 'current'
def genesis(self):
self.qaction.triggered.connect(self.add_books_to_library)
diff --git a/src/calibre/gui2/actions/annotate.py b/src/calibre/gui2/actions/annotate.py
index 5356d63e98..dfafcd1a39 100644
--- a/src/calibre/gui2/actions/annotate.py
+++ b/src/calibre/gui2/actions/annotate.py
@@ -18,6 +18,7 @@ class FetchAnnotationsAction(InterfaceAction):
name = 'Fetch Annotations'
action_spec = (_('Fetch annotations (experimental)'), None, None, None)
+ action_type = 'current'
def genesis(self):
pass
diff --git a/src/calibre/gui2/actions/convert.py b/src/calibre/gui2/actions/convert.py
index ee0f06ab71..29acfc52b1 100644
--- a/src/calibre/gui2/actions/convert.py
+++ b/src/calibre/gui2/actions/convert.py
@@ -21,6 +21,7 @@ class ConvertAction(InterfaceAction):
name = 'Convert Books'
action_spec = (_('Convert books'), 'convert.png', None, _('C'))
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
+ action_type = 'current'
def genesis(self):
cm = QMenu()
diff --git a/src/calibre/gui2/actions/copy_to_library.py b/src/calibre/gui2/actions/copy_to_library.py
index 7127c91e8c..6b7654f644 100644
--- a/src/calibre/gui2/actions/copy_to_library.py
+++ b/src/calibre/gui2/actions/copy_to_library.py
@@ -80,6 +80,7 @@ class CopyToLibraryAction(InterfaceAction):
_('Copy selected books to the specified library'), None)
popup_type = QToolButton.InstantPopup
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
+ action_type = 'current'
def genesis(self):
self.menu = QMenu(self.gui)
diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py
index 0343c6df84..406860e4ec 100644
--- a/src/calibre/gui2/actions/delete.py
+++ b/src/calibre/gui2/actions/delete.py
@@ -16,6 +16,7 @@ class DeleteAction(InterfaceAction):
name = 'Remove Books'
action_spec = (_('Remove books'), 'trash.png', None, _('Del'))
+ action_type = 'current'
def genesis(self):
self.qaction.triggered.connect(self.delete_books)
diff --git a/src/calibre/gui2/actions/edit_collections.py b/src/calibre/gui2/actions/edit_collections.py
index e45d36fc62..7f5dd76538 100644
--- a/src/calibre/gui2/actions/edit_collections.py
+++ b/src/calibre/gui2/actions/edit_collections.py
@@ -13,6 +13,7 @@ class EditCollectionsAction(InterfaceAction):
action_spec = (_('Manage collections'), None,
_('Manage the collections on this device'), None)
dont_add_to = frozenset(['toolbar', 'context-menu'])
+ action_type = 'current'
def genesis(self):
self.qaction.triggered.connect(self.edit_collections)
diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py
index 878ba77a43..ac04652efa 100644
--- a/src/calibre/gui2/actions/edit_metadata.py
+++ b/src/calibre/gui2/actions/edit_metadata.py
@@ -22,6 +22,7 @@ class EditMetadataAction(InterfaceAction):
name = 'Edit Metadata'
action_spec = (_('Edit metadata'), 'edit_input.png', None, _('E'))
+ action_type = 'current'
def genesis(self):
self.create_action(spec=(_('Merge book records'), 'merge_books.png',
diff --git a/src/calibre/gui2/actions/open.py b/src/calibre/gui2/actions/open.py
index 106bfa24f6..141ff01a66 100644
--- a/src/calibre/gui2/actions/open.py
+++ b/src/calibre/gui2/actions/open.py
@@ -14,6 +14,7 @@ class OpenFolderAction(InterfaceAction):
action_spec = (_('Open containing folder'), 'document_open.png', None,
_('O'))
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
+ action_type = 'current'
def genesis(self):
self.qaction.triggered.connect(self.gui.iactions['View'].view_folder)
diff --git a/src/calibre/gui2/actions/save_to_disk.py b/src/calibre/gui2/actions/save_to_disk.py
index bfcc02e130..e9664b9980 100644
--- a/src/calibre/gui2/actions/save_to_disk.py
+++ b/src/calibre/gui2/actions/save_to_disk.py
@@ -38,6 +38,7 @@ class SaveToDiskAction(InterfaceAction):
name = "Save To Disk"
action_spec = (_('Save to disk'), 'save.png', None, _('S'))
+ action_type = 'current'
def genesis(self):
self.qaction.triggered.connect(self.save_to_disk)
diff --git a/src/calibre/gui2/actions/show_book_details.py b/src/calibre/gui2/actions/show_book_details.py
index d17d0998f1..18b0a694bf 100644
--- a/src/calibre/gui2/actions/show_book_details.py
+++ b/src/calibre/gui2/actions/show_book_details.py
@@ -16,6 +16,7 @@ class ShowBookDetailsAction(InterfaceAction):
action_spec = (_('Show book details'), 'dialog_information.png', None,
_('I'))
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
+ action_type = 'current'
def genesis(self):
self.qaction.triggered.connect(self.show_book_info)
diff --git a/src/calibre/gui2/actions/similar_books.py b/src/calibre/gui2/actions/similar_books.py
index 1a14869a9c..644cd3160a 100644
--- a/src/calibre/gui2/actions/similar_books.py
+++ b/src/calibre/gui2/actions/similar_books.py
@@ -16,6 +16,7 @@ class SimilarBooksAction(InterfaceAction):
name = 'Similar Books'
action_spec = (_('Similar books...'), None, None, None)
popup_type = QToolButton.InstantPopup
+ action_type = 'current'
def genesis(self):
m = QMenu(self.gui)
diff --git a/src/calibre/gui2/actions/view.py b/src/calibre/gui2/actions/view.py
index 2f6be24e5b..0fbf86c567 100644
--- a/src/calibre/gui2/actions/view.py
+++ b/src/calibre/gui2/actions/view.py
@@ -22,6 +22,7 @@ class ViewAction(InterfaceAction):
name = 'View'
action_spec = (_('View'), 'view.png', None, _('V'))
+ action_type = 'current'
def genesis(self):
self.persistent_files = []
diff --git a/src/calibre/gui2/convert/structure_detection.py b/src/calibre/gui2/convert/structure_detection.py
index 68f820bda4..3f350d4508 100644
--- a/src/calibre/gui2/convert/structure_detection.py
+++ b/src/calibre/gui2/convert/structure_detection.py
@@ -28,6 +28,8 @@ class StructureDetectionWidget(Widget, Ui_Form):
'preprocess_html', 'remove_header', 'header_regex',
'remove_footer', 'footer_regex','html_unwrap_factor']
)
+ self.opt_html_unwrap_factor.setEnabled(False)
+ self.huf_label.setEnabled(False)
self.db, self.book_id = db, book_id
for x in ('pagebreak', 'rule', 'both', 'none'):
self.opt_chapter_mark.addItem(x)
@@ -66,6 +68,6 @@ class StructureDetectionWidget(Widget, Ui_Form):
return True
def set_value_handler(self, g, val):
- if val is None and isinstance(g, QDoubleSpinBox):
+ if val is None and g is self.opt_html_unwrap_factor:
g.setValue(0.0)
- return True
\ No newline at end of file
+ return True
diff --git a/src/calibre/gui2/convert/structure_detection.ui b/src/calibre/gui2/convert/structure_detection.ui
index 54534af950..21fe365e99 100644
--- a/src/calibre/gui2/convert/structure_detection.ui
+++ b/src/calibre/gui2/convert/structure_detection.ui
@@ -14,10 +14,10 @@
Form
- -
+
-
- -
+
-
Chapter &mark:
@@ -27,28 +27,31 @@
- -
+
-
20
- -
+
-
Remove first &image
- -
+
-
Insert &metadata as page at start of book
- -
+
-
+
+
+ -
Qt::Vertical
@@ -61,52 +64,41 @@
- -
+
-
Remove F&ooter
- -
+
-
Remove H&eader
- -
+
-
- -
-
- opt_page_breaks_before
-
+
-
+
- -
-
- opt_footer_regex
-
-
- -
-
+
-
+
- &Preprocess input file to possibly improve structure detection
+ Line &un-wrap factor during preprocess:
+
+
+ opt_html_unwrap_factor
- -
-
-
- Qt::RightToLeft
-
-
- Line Un-Wrapping Factor
-
-
-
- -
+
-
+
+
+
1.000000000000000
@@ -118,6 +110,26 @@
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ &Preprocess input file to possibly improve structure detection
+
+
+
@@ -135,5 +147,38 @@
-
+
+
+ opt_preprocess_html
+ toggled(bool)
+ opt_html_unwrap_factor
+ setEnabled(bool)
+
+
+ 328
+ 87
+
+
+ 481
+ 113
+
+
+
+
+ opt_preprocess_html
+ toggled(bool)
+ huf_label
+ setEnabled(bool)
+
+
+ 295
+ 88
+
+
+ 291
+ 105
+
+
+
+
diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py
index 45c78ce6da..b20cd7594f 100644
--- a/src/calibre/gui2/device.py
+++ b/src/calibre/gui2/device.py
@@ -627,12 +627,11 @@ class DeviceMixin(object): # {{{
def connect_to_folder(self):
dir = choose_dir(self, 'Select Device Folder',
_('Select folder to open as device'))
- kls = FOLDER_DEVICE
- self.device_manager.mount_device(kls=kls, kind='folder', path=dir)
+ if dir is not None:
+ self.device_manager.mount_device(kls=FOLDER_DEVICE, kind='folder', path=dir)
def connect_to_itunes(self):
- kls = ITUNES_ASYNC
- self.device_manager.mount_device(kls=kls, kind='itunes', path=None)
+ self.device_manager.mount_device(kls=ITUNES_ASYNC, kind='itunes', path=None)
# disconnect from both folder and itunes devices
def disconnect_mounted_device(self):
@@ -746,6 +745,7 @@ class DeviceMixin(object): # {{{
if job.failed:
self.device_job_exception(job)
return
+ # set_books_in_library might schedule a sync_booklists job
self.set_books_in_library(job.result, reset=True)
mainlist, cardalist, cardblist = job.result
self.memory_view.set_database(mainlist)
@@ -790,11 +790,12 @@ class DeviceMixin(object): # {{{
self.device_manager.remove_books_from_metadata(paths,
self.booklists())
model.paths_deleted(paths)
- self.upload_booklists()
# Force recomputation the library's ondevice info. We need to call
# set_books_in_library even though books were not added because
- # the deleted book might have been an exact match.
- self.set_books_in_library(self.booklists(), reset=True)
+ # the deleted book might have been an exact match. Upload the booklists
+ # if set_books_in_library did not.
+ if not self.set_books_in_library(self.booklists(), reset=True):
+ self.upload_booklists()
self.book_on_device(None, None, reset=True)
# We need to reset the ondevice flags in the library. Use a big hammer,
# so we don't need to worry about whether some succeeded or not.
@@ -1231,7 +1232,7 @@ class DeviceMixin(object): # {{{
self.location_manager.update_devices(cp, fs,
self.device_manager.device.icon)
# reset the views so that up-to-date info is shown. These need to be
- # here because the sony driver updates collections in sync_booklists
+ # here because some drivers update collections in sync_booklists
self.memory_view.reset()
self.card_a_view.reset()
self.card_b_view.reset()
@@ -1281,8 +1282,6 @@ class DeviceMixin(object): # {{{
self.device_manager.add_books_to_metadata(job.result,
metadata, self.booklists())
- self.upload_booklists()
-
books_to_be_deleted = []
if memory and memory[1]:
books_to_be_deleted = memory[1]
@@ -1292,12 +1291,15 @@ class DeviceMixin(object): # {{{
# book already there with a different book. This happens frequently in
# news. When this happens, the book match indication will be wrong
# because the UUID changed. Force both the device and the library view
- # to refresh the flags.
- self.set_books_in_library(self.booklists(), reset=True)
+ # to refresh the flags. Set_books_in_library could upload the booklists.
+ # If it does not, then do it here.
+ if not self.set_books_in_library(self.booklists(), reset=True):
+ self.upload_booklists()
self.book_on_device(None, reset=True)
self.refresh_ondevice_info(device_connected = True)
- view = self.card_a_view if on_card == 'carda' else self.card_b_view if on_card == 'cardb' else self.memory_view
+ view = self.card_a_view if on_card == 'carda' else \
+ self.card_b_view if on_card == 'cardb' else self.memory_view
view.model().resort(reset=False)
view.model().research()
for f in files:
@@ -1372,7 +1374,7 @@ class DeviceMixin(object): # {{{
try:
db = self.library_view.model().db
except:
- return
+ return False
# Build a cache (map) of the library, so the search isn't On**2
self.db_book_title_cache = {}
self.db_book_uuid_cache = {}
@@ -1467,10 +1469,13 @@ class DeviceMixin(object): # {{{
# Set author_sort if it isn't already
asort = getattr(book, 'author_sort', None)
if not asort and book.authors:
- book.author_sort = self.library_view.model().db.author_sort_from_authors(book.authors)
+ book.author_sort = self.library_view.model().db.\
+ author_sort_from_authors(book.authors)
if update_metadata:
if self.device_manager.is_device_connected:
- self.device_manager.sync_booklists(None, booklists)
+ self.device_manager.sync_booklists(
+ Dispatcher(self.metadata_synced), booklists)
+ return update_metadata
# }}}
diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py
index 3d79b01c14..d07eac7670 100644
--- a/src/calibre/gui2/dialogs/metadata_single.py
+++ b/src/calibre/gui2/dialogs/metadata_single.py
@@ -6,10 +6,7 @@ The dialog used to edit meta information for a book as well as
add/remove formats
'''
-import os
-import re
-import time
-import traceback
+import os, re, time, traceback, textwrap
from PyQt4.Qt import SIGNAL, QObject, Qt, QTimer, QThread, QDate, \
QPixmap, QListWidgetItem, QDialog, pyqtSignal
@@ -331,6 +328,14 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
ResizableDialog.__init__(self, window)
self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter)
self.cancel_all = False
+ base = unicode(self.author_sort.toolTip())
+ self.ok_aus_tooltip = '' + textwrap.fill(base+'
'+
+ _(' The green color indicates that the current '
+ 'author sort matches the current author'))
+ self.bad_aus_tooltip = '
'+textwrap.fill(base + '
'+
+ _(' The red color indicates that the current '
+ 'author sort does not match the current author'))
+
if cancel_all:
self.__abort_button = self.button_box.addButton(self.button_box.Abort)
self.__abort_button.setToolTip(_('Abort the editing of all remaining books'))
@@ -375,6 +380,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.remove_unused_series)
QObject.connect(self.auto_author_sort, SIGNAL('clicked()'),
self.deduce_author_sort)
+ self.connect(self.author_sort, SIGNAL('textChanged(const QString&)'),
+ self.author_sort_box_changed)
+ self.connect(self.authors, SIGNAL('editTextChanged(const QString&)'),
+ self.authors_box_changed)
self.connect(self.formats, SIGNAL('itemDoubleClicked(QListWidgetItem*)'),
self.show_format)
self.connect(self.formats, SIGNAL('delete_format()'), self.remove_format)
@@ -467,6 +476,28 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
for c in range(2, len(ans[i].widgets), 2):
w.setTabOrder(ans[i].widgets[c-1], ans[i].widgets[c+1])
+ def authors_box_changed(self, txt):
+ aus = unicode(txt)
+ aus = re.sub(r'\s+et al\.$', '', aus)
+ aus = self.db.author_sort_from_authors(string_to_authors(aus))
+ self.mark_author_sort(normal=(unicode(self.author_sort.text()) == aus))
+
+ def author_sort_box_changed(self, txt):
+ au = unicode(self.authors.text())
+ au = re.sub(r'\s+et al\.$', '', au)
+ au = self.db.author_sort_from_authors(string_to_authors(au))
+ self.mark_author_sort(normal=(au == txt))
+
+ def mark_author_sort(self, normal=True):
+ if normal:
+ col = 'rgb(0, 255, 0, 20%)'
+ else:
+ col = 'rgb(255, 0, 0, 20%)'
+ self.author_sort.setStyleSheet('QLineEdit { color: black; '
+ 'background-color: %s; }'%col)
+ tt = self.ok_aus_tooltip if normal else self.bad_aus_tooltip
+ self.author_sort.setToolTip(tt)
+
def validate_isbn(self, isbn):
isbn = unicode(isbn).strip()
if not isbn:
diff --git a/src/calibre/gui2/dialogs/metadata_single.ui b/src/calibre/gui2/dialogs/metadata_single.ui
index 7184192eba..74febf9c29 100644
--- a/src/calibre/gui2/dialogs/metadata_single.ui
+++ b/src/calibre/gui2/dialogs/metadata_single.ui
@@ -151,14 +151,16 @@
-
- Specify how the author(s) of this book should be sorted. For example Charles Dickens should be sorted as Dickens, Charles.
+ Specify how the author(s) of this book should be sorted. For example Charles Dickens should be sorted as Dickens, Charles.
+If the box is colored green, then text matches the individual author's sort strings. If it is colored red, then the authors and this text do not match.
-
- Automatically create the author sort entry based on the current author entry
+ Automatically create the author sort entry based on the current author entry.
+Using this button to create author sort will change author sort from red to green.
...
diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py
index 58d5267c8e..ec7e023dc1 100644
--- a/src/calibre/gui2/layout.py
+++ b/src/calibre/gui2/layout.py
@@ -61,7 +61,7 @@ class LocationManager(QObject): # {{{
ac('library', _('Library'), 'lt.png',
_('Show books in calibre library'))
- ac('main', _('Reader'), 'reader.png',
+ ac('main', _('Device'), 'reader.png',
_('Show books in the main memory of the device'))
ac('carda', _('Card A'), 'sd.png',
_('Show books in storage card A'))
@@ -197,11 +197,21 @@ class SearchBar(QWidget): # {{{
# }}}
+class Spacer(QWidget):
+
+ def __init__(self, parent):
+ QWidget.__init__(self, parent)
+ self.l = QHBoxLayout()
+ self.setLayout(self.l)
+ self.l.addStretch(10)
+
+
class ToolBar(QToolBar): # {{{
- def __init__(self, donate, location_manager, parent):
+ def __init__(self, donate, location_manager, child_bar, parent):
QToolBar.__init__(self, parent)
self.gui = parent
+ self.child_bar = child_bar
self.setContextMenuPolicy(Qt.PreventContextMenu)
self.setMovable(False)
self.setFloatable(False)
@@ -223,16 +233,19 @@ class ToolBar(QToolBar): # {{{
sz = gprefs['toolbar_icon_size']
sz = {'small':24, 'medium':48, 'large':64}[sz]
self.setIconSize(QSize(sz, sz))
+ self.child_bar.setIconSize(QSize(sz, sz))
style = Qt.ToolButtonTextUnderIcon
if gprefs['toolbar_text'] == 'never':
style = Qt.ToolButtonIconOnly
self.setToolButtonStyle(style)
+ self.child_bar.setToolButtonStyle(style)
self.donate_button.set_normal_icon_size(sz, sz)
def contextMenuEvent(self, *args):
pass
def build_bar(self):
+ self.child_bar.setVisible(gprefs['show_child_bar'])
self.showing_donate = False
showing_device = self.location_manager.has_device
actions = '-device' if showing_device else ''
@@ -244,10 +257,16 @@ class ToolBar(QToolBar): # {{{
m.setVisible(False)
self.clear()
+ self.child_bar.clear()
self.added_actions = []
+ self.spacers = [Spacer(self.child_bar), Spacer(self.child_bar),
+ Spacer(self), Spacer(self)]
+ self.child_bar.addWidget(self.spacers[0])
+ if gprefs['show_child_bar']:
+ self.addWidget(self.spacers[2])
for what in actions:
- if what is None:
+ if what is None and not gprefs['show_child_bar']:
self.addSeparator()
elif what == 'Location Manager':
for ac in self.location_manager.available_actions:
@@ -262,12 +281,21 @@ class ToolBar(QToolBar): # {{{
self.showing_donate = True
elif what in self.gui.iactions:
action = self.gui.iactions[what]
- self.addAction(action.qaction)
+ bar = self
+ if action.action_type == 'current' and gprefs['show_child_bar']:
+ bar = self.child_bar
+ bar.addAction(action.qaction)
self.added_actions.append(action.qaction)
self.setup_tool_button(action.qaction, action.popup_type)
+ self.child_bar.addWidget(self.spacers[1])
+ if gprefs['show_child_bar']:
+ self.addWidget(self.spacers[3])
+
def setup_tool_button(self, ac, menu_mode=None):
ch = self.widgetForAction(ac)
+ if ch is None:
+ ch = self.child_bar.widgetForAction(ac)
ch.setCursor(Qt.PointingHandCursor)
ch.setAutoRaise(True)
if ac.menu() is not None and menu_mode is not None:
@@ -280,7 +308,8 @@ class ToolBar(QToolBar): # {{{
if p == 'never':
style = Qt.ToolButtonIconOnly
- if p == 'auto' and self.preferred_width > self.width()+35:
+ if p == 'auto' and self.preferred_width > self.width()+35 and \
+ not gprefs['show_child_bar']:
style = Qt.ToolButtonIconOnly
self.setToolButtonStyle(style)
@@ -309,9 +338,11 @@ class MainWindowMixin(object): # {{{
self.iactions['Fetch News'].init_scheduler(db)
self.search_bar = SearchBar(self)
+ self.child_bar = QToolBar(self)
self.tool_bar = ToolBar(self.donate_button,
- self.location_manager, self)
+ self.location_manager, self.child_bar, self)
self.addToolBar(Qt.TopToolBarArea, self.tool_bar)
+ self.addToolBar(Qt.BottomToolBarArea, self.child_bar)
l = self.centralwidget.layout()
l.addWidget(self.search_bar)
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py
index c746a5aa56..3370fd4b75 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -1027,7 +1027,9 @@ class DeviceBooksModel(BooksModel): # {{{
def resort(self, reset=True):
if self.sorted_on:
self.sort(self.column_map.index(self.sorted_on[0]),
- self.sorted_on[1], reset=reset)
+ self.sorted_on[1], reset=False)
+ if reset:
+ self.reset()
def columnCount(self, parent):
if parent and parent.isValid():
diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py
index f30b2fddbb..10c2fcfe95 100644
--- a/src/calibre/gui2/preferences/look_feel.py
+++ b/src/calibre/gui2/preferences/look_feel.py
@@ -46,6 +46,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('use_roman_numerals_for_series_number', config)
r('separate_cover_flow', config, restart_required=True)
r('search_as_you_type', config)
+ r('show_child_bar', gprefs)
choices = [(_('Small'), 'small'), (_('Medium'), 'medium'),
(_('Large'), 'large')]
diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui
index 7c6c736b24..1de55d51ef 100644
--- a/src/calibre/gui2/preferences/look_feel.ui
+++ b/src/calibre/gui2/preferences/look_feel.ui
@@ -173,6 +173,13 @@
+ -
+
+
+ &Split the toolbar into two toolbars
+
+
+
diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py
index 519d533ff6..6c50a71b92 100644
--- a/src/calibre/gui2/tag_view.py
+++ b/src/calibre/gui2/tag_view.py
@@ -376,7 +376,7 @@ class TagsModel(QAbstractItemModel): # {{{
'series' : QIcon(I('series.png')),
'formats' : QIcon(I('book.png')),
'publisher' : QIcon(I('publisher.png')),
- 'rating' : QIcon(I('star.png')),
+ 'rating' : QIcon(I('rating.png')),
'news' : QIcon(I('news.png')),
'tags' : QIcon(I('tags.png')),
':custom' : QIcon(I('column.png')),
diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py
index bd2160aff1..e14d092727 100644
--- a/src/calibre/library/catalog.py
+++ b/src/calibre/library/catalog.py
@@ -2523,6 +2523,10 @@ class EPUB_MOBI(CatalogPlugin):
# Fetch the database as a dictionary
self.booksBySeries = self.plugin.search_sort_db(self.db, self.opts)
+ if not self.booksBySeries:
+ self.opts.generate_series = False
+ self.opts.log(" no series found in selected books, cancelling series generation")
+ return
friendly_name = "Series"
@@ -2586,7 +2590,7 @@ class EPUB_MOBI(CatalogPlugin):
aTag = Tag(soup, 'a')
aTag['name'] = "%s_series" % re.sub('\W','',book['series']).lower()
pSeriesTag.insert(0,aTag)
- pSeriesTag.insert(1,NavigableString(self.NOT_READ_SYMBOL + '%s' % book['series']))
+ pSeriesTag.insert(1,NavigableString('%s' % book['series']))
divTag.insert(dtc,pSeriesTag)
dtc += 1
@@ -2595,7 +2599,14 @@ class EPUB_MOBI(CatalogPlugin):
ptc = 0
# book with read/reading/unread symbol
- if 'read' in book and book['read']:
+ for tag in book['tags']:
+ if tag == self.opts.read_tag:
+ book['read'] = True
+ break
+ else:
+ book['read'] = False
+
+ if book['read']:
# check mark
pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL))
pBookTag['class'] = "read_book"
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index 8a5ab75c3c..f5f0f724ba 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -597,8 +597,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return identical_book_ids
def has_cover(self, index, index_is_id=False):
- id = index if index_is_id else self.id(index)
- path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
+ id = index if index_is_id else self.id(index)
+ try:
+ path = os.path.join(self.abspath(id, index_is_id=True), 'cover.jpg')
+ except:
+ # Can happen if path has not yet been set
+ return False
return os.access(path, os.R_OK)
def remove_cover(self, id, notify=True):
@@ -609,6 +613,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
except (IOError, OSError):
time.sleep(0.2)
os.remove(path)
+ self.data.set(id, self.FIELD_MAP['cover'], False, row_is_id=True)
if notify:
self.notify('cover', [id])
@@ -629,6 +634,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
except (IOError, OSError):
time.sleep(0.2)
save_cover_data_to(data, path)
+ self.data.set(id, self.FIELD_MAP['cover'], True, row_is_id=True)
if notify:
self.notify('cover', [id])
@@ -1087,8 +1093,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.set_path(id, True)
self.notify('metadata', [id])
- # Given a book, return the list of author sort strings for the book's authors
def authors_sort_strings(self, id, index_is_id=False):
+ '''
+ Given a book, return the list of author sort strings
+ for the book's authors
+ '''
id = id if index_is_id else self.id(id)
aut_strings = self.conn.get('''
SELECT sort
@@ -1744,10 +1753,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
series_index = 1.0 if mi.series_index is None else mi.series_index
aus = mi.author_sort if mi.author_sort else self.author_sort_from_authors(mi.authors)
title = mi.title
- if isinstance(aus, str):
+ if isbytestring(aus):
aus = aus.decode(preferred_encoding, 'replace')
- if isinstance(title, str):
- title = title.decode(preferred_encoding)
+ if isbytestring(title):
+ title = title.decode(preferred_encoding, 'replace')
obj = self.conn.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)',
(title, series_index, aus))
id = obj.lastrowid
diff --git a/src/calibre/manual/conversion.rst b/src/calibre/manual/conversion.rst
index c8bc3ef665..cfc2871396 100644
--- a/src/calibre/manual/conversion.rst
+++ b/src/calibre/manual/conversion.rst
@@ -329,6 +329,17 @@ There are a few more options in this section.
of as a separate cover. If you also specify a cover in |app|, then the converted book will have
two covers. This option will simply remove the first image from the source document, thereby
ensuring that the converted book has only one cover, the one specified in |app|.
+
+:guilabel:`Preprocess input`
+ This option activates various algorithms that try to detect and correct common cases of
+ badly formatted input documents. Things like hard line breaks, large blocks of text with no formatting, etc.
+ Turn this option on if your input document suffers from bad formatting. But be aware that in
+ some cases, this option can lead to worse results, so use with care.
+
+:guilabel:`Line-unwrap factor`
+ This option control the algorithm |app| uses to remove hard line breaks. For example, if the value of this
+ option is 0.4, that means calibre will remove hard line breaks from the end of lines whose lengths are less
+ than the length of 40% of all lines in the document.
Table of Contents
------------------
diff --git a/src/calibre/utils/magick/__init__.py b/src/calibre/utils/magick/__init__.py
index 073a030361..2707430c67 100644
--- a/src/calibre/utils/magick/__init__.py
+++ b/src/calibre/utils/magick/__init__.py
@@ -194,7 +194,7 @@ class Image(_magick.Image): # {{{
# }}}
-def create_canvas(width, height, bgcolor='white'):
+def create_canvas(width, height, bgcolor='#ffffff'):
canvas = Image()
canvas.create_canvas(int(width), int(height), str(bgcolor))
return canvas
diff --git a/src/calibre/utils/magick/draw.py b/src/calibre/utils/magick/draw.py
index 82a0237b8d..ed9e3d3d83 100644
--- a/src/calibre/utils/magick/draw.py
+++ b/src/calibre/utils/magick/draw.py
@@ -5,12 +5,14 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal '
__docformat__ = 'restructuredtext en'
+import os
from calibre.utils.magick import Image, DrawingWand, create_canvas
from calibre.constants import __appname__, __version__
from calibre import fit_image
-def save_cover_data_to(data, path, bgcolor='white', resize_to=None):
+def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None,
+ return_data=False):
'''
Saves image in data to path, in the format specified by the path
extension. Composes the image onto a blank canvas so as to
@@ -22,9 +24,11 @@ def save_cover_data_to(data, path, bgcolor='white', resize_to=None):
img.size = (resize_to[0], resize_to[1])
canvas = create_canvas(img.size[0], img.size[1], bgcolor)
canvas.compose(img)
+ if return_data:
+ return canvas.export(os.path.splitext(path)[1][1:])
canvas.save(path)
-def thumbnail(data, width=120, height=120, bgcolor='white', fmt='jpg'):
+def thumbnail(data, width=120, height=120, bgcolor='#ffffff', fmt='jpg'):
img = Image()
img.load(data)
owidth, oheight = img.size
@@ -57,7 +61,7 @@ def identify(path):
return identify_data(data)
def add_borders_to_image(path_to_image, left=0, top=0, right=0, bottom=0,
- border_color='white'):
+ border_color='#ffffff'):
img = Image()
img.open(path_to_image)
lwidth, lheight = img.size
@@ -76,7 +80,7 @@ def create_text_wand(font_size, font_path=None):
ans.text_alias = True
return ans
-def create_text_arc(text, font_size, font=None, bgcolor='white'):
+def create_text_arc(text, font_size, font=None, bgcolor='#ffffff'):
if isinstance(text, unicode):
text = text.encode('utf-8')
@@ -144,7 +148,7 @@ class TextLine(object):
def create_cover_page(top_lines, logo_path, width=590, height=750,
- bgcolor='white', output_format='jpg'):
+ bgcolor='#ffffff', output_format='jpg'):
'''
Create the standard calibre cover page and return it as a byte string in
the specified output_format.
diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py
index 9ba9583c73..a140dfbf05 100644
--- a/src/calibre/web/feeds/news.py
+++ b/src/calibre/web/feeds/news.py
@@ -290,10 +290,12 @@ class BasicNewsRecipe(Recipe):
#: the cover for the periodical. Overriding this in your recipe instructs
#: calibre to render the downloaded cover into a frame whose width and height
#: are expressed as a percentage of the downloaded cover.
- #: cover_margins = (10,15,'white') pads the cover with a white margin
+ #: cover_margins = (10, 15, '#ffffff') pads the cover with a white margin
#: 10px on the left and right, 15px on the top and bottom.
- #: Colors name defined at http://www.imagemagick.org/script/color.php
- cover_margins = (0,0,'white')
+ #: Color names defined at http://www.imagemagick.org/script/color.php
+ #: Note that for some reason, white does not always work on windows. Use
+ #: #ffffff instead
+ cover_margins = (0, 0, '#ffffff')
#: Set to a non empty string to disable this recipe
#: The string will be used as the disabled message