From 28a6f11a94848617311e36d62f6106fd951137a5 Mon Sep 17 00:00:00 2001 From: "Marshall T. Vandegrift" Date: Fri, 6 Feb 2009 14:33:48 -0500 Subject: [PATCH 1/6] Implement adding page-map information to EPUB output. --- src/calibre/ebooks/epub/__init__.py | 10 ++++- src/calibre/ebooks/epub/from_html.py | 3 ++ src/calibre/ebooks/epub/pages.py | 59 ++++++++++++++++++++++++++++ src/calibre/ebooks/oeb/base.py | 17 ++++++-- 4 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 src/calibre/ebooks/epub/pages.py diff --git a/src/calibre/ebooks/epub/__init__.py b/src/calibre/ebooks/epub/__init__.py index 863f2f8db0..aa17024d50 100644 --- a/src/calibre/ebooks/epub/__init__.py +++ b/src/calibre/ebooks/epub/__init__.py @@ -153,6 +153,14 @@ help on using this feature. 'slow and if your source file contains a very large ' 'number of page breaks, you should turn off splitting ' 'on page breaks.')) + structure('page', ['--page'], default=None, + help=_('XPath expression to detect page boundaries for building ' + 'a custom pagination map, as used by AdobeDE. Default is ' + 'not to build an explicit pagination map.')) + structure('page_names', ['--page-names'], default=None, + help=_('XPath expression to find the name of each page in the ' + 'pagination map relative to its boundary element. ' + 'Default is to number all pages staring with 1.')) toc = c.add_group('toc', _('''\ Control the automatic generation of a Table of Contents. If an OPF file is detected @@ -230,4 +238,4 @@ to auto-generate a Table of Contents. c.add_opt('extract_to', ['--extract-to'], group='debug', default=None, help=_('Extract the contents of the produced EPUB file to the ' 'specified directory.')) - return c \ No newline at end of file + return c diff --git a/src/calibre/ebooks/epub/from_html.py b/src/calibre/ebooks/epub/from_html.py index ca50fe7a5d..b8fa3e8fd0 100644 --- a/src/calibre/ebooks/epub/from_html.py +++ b/src/calibre/ebooks/epub/from_html.py @@ -46,6 +46,7 @@ from calibre.ebooks.metadata.toc import TOC from calibre.ebooks.metadata.opf2 import OPF from calibre.ebooks.epub import initialize_container, PROFILES from calibre.ebooks.epub.split import split +from calibre.ebooks.epub.pages import add_page_map from calibre.ebooks.epub.fonts import Rationalizer from calibre.constants import preferred_encoding from calibre.customize.ui import run_plugins_on_postprocess @@ -438,6 +439,8 @@ def convert(htmlfile, opts, notification=None, create_epub=True, if opts.show_ncx: print toc split(opf_path, opts, stylesheet_map) + if opts.page: + add_page_map(opf_path, opts) check_links(opf_path, opts.pretty_print) opf = OPF(opf_path, tdir) diff --git a/src/calibre/ebooks/epub/pages.py b/src/calibre/ebooks/epub/pages.py new file mode 100644 index 0000000000..c1b38b9be1 --- /dev/null +++ b/src/calibre/ebooks/epub/pages.py @@ -0,0 +1,59 @@ +''' +Add page mapping information to an EPUB book. +''' + +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2008, Marshall T. Vandegrift ' +__docformat__ = 'restructuredtext en' + +import os, re +from itertools import count, chain +from calibre.ebooks.oeb.base import XHTML, XHTML_NS +from calibre.ebooks.oeb.base import OEBBook, DirWriter +from lxml import etree, html +from lxml.etree import XPath + +NSMAP = {'h': XHTML_NS, 'html': XHTML_NS, 'xhtml': XHTML_NS} +PAGE_RE = re.compile(r'page', re.IGNORECASE) +ROMAN_RE = re.compile(r'^[ivxlcdm]+$', re.IGNORECASE) + +def filter_name(name): + name = name.strip() + name = PAGE_RE.sub('', name) + for word in name.split(): + if word.isdigit() or ROMAN_RE.match(word): + name = word + break + return name + +def build_name_for(expr): + if expr is None: + counter = count(1) + return lambda elem: str(counter.next()) + selector = XPath(expr, namespaces=NSMAP) + def name_for(elem): + results = selector(elem) + if not results: + return '' + name = ' '.join(results) + return filter_name(name) + return name_for + +def add_page_map(opfpath, opts): + oeb = OEBBook(opfpath) + selector = XPath(opts.page, namespaces=NSMAP) + name_for = build_name_for(opts.page_names) + idgen = ("calibre-page-%d" % n for n in count(1)) + for item in oeb.spine: + data = item.data + for elem in selector(data): + name = name_for(elem) + id = elem.get('id', None) + if id is None: + id = elem.attrib['id'] = idgen.next() + href = '#'.join((item.href, id)) + oeb.pages.add(name, href) + writer = DirWriter(version='2.0', page_map=True) + writer.dump(oeb, opfpath) diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index 778cec54cf..80d4797905 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -246,6 +246,10 @@ class DirWriter(object): def dump(self, oeb, path): version = int(self.version[0]) + opfname = None + if os.path.splitext(path)[1].lower() == '.opf': + opfname = os.path.basename(path) + path = os.path.dirname(path) if not os.path.isdir(path): os.mkdir(path) output = DirContainer(path) @@ -257,7 +261,9 @@ class DirWriter(object): metadata = oeb.to_opf2(page_map=self.page_map) else: raise OEBError("Unrecognized OPF version %r" % self.version) - for href, data in metadata.values(): + for mime, (href, data) in metadata.items(): + if opfname and mime == OPF_MIME: + href = opfname output.write(href, xml2str(data)) return @@ -551,9 +557,6 @@ class Manifest(object): for elem in data: nroot.append(elem) data = nroot - # Remove any encoding-specifying elements - for meta in self.META_XP(data): - meta.getparent().remove(meta) # Ensure has a head = xpath(data, '/h:html/h:head') head = head[0] if head else None @@ -569,6 +572,12 @@ class Manifest(object): 'File %r missing element' % self.href) title = etree.SubElement(head, XHTML('title')) title.text = self.oeb.translate(__('Unknown')) + # Remove any encoding-specifying <meta/> elements + for meta in self.META_XP(data): + meta.getparent().remove(meta) + etree.SubElement(head, XHTML('meta'), + attrib={'http-equiv': 'Content-Type', + 'content': '%s; charset=utf-8' % XHTML_NS}) # Ensure has a <body/> if not xpath(data, '/h:html/h:body'): self.oeb.logger.warn( From 773ab64a765b746f963a3d3b6b51adc7e4eaf978 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Fri, 6 Feb 2009 11:47:24 -0800 Subject: [PATCH 2/6] IGN:... --- src/calibre/library/database2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 209f700820..4a2c669a25 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -15,7 +15,7 @@ from PyQt4.QtCore import QCoreApplication, QThread, QReadWriteLock from PyQt4.QtGui import QApplication, QPixmap, QImage __app = None -from calibre.ebooks.metadata import title_sort +from calibre.library import title_sort from calibre.library.database import LibraryDatabase from calibre.library.sqlite import connect, IntegrityError from calibre.utils.search_query_parser import SearchQueryParser From 0fb612bb2f6b3eb59b3ff857a5c5a6668d68cb75 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Fri, 6 Feb 2009 13:00:43 -0800 Subject: [PATCH 3/6] Fix #1783 (Google Reader Recipe for non-English speakers) --- src/calibre/web/feeds/recipes/recipe_greader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/web/feeds/recipes/recipe_greader.py b/src/calibre/web/feeds/recipes/recipe_greader.py index 011718feae..f222a322f1 100644 --- a/src/calibre/web/feeds/recipes/recipe_greader.py +++ b/src/calibre/web/feeds/recipes/recipe_greader.py @@ -32,5 +32,6 @@ class GoogleReader(BasicNewsRecipe): soup = self.index_to_soup('http://www.google.com/reader/api/0/tag/list') for id in soup.findAll(True, attrs={'name':['id']}): url = id.contents[0] - feeds.append((re.search('/([^/]*)$', url).group(1), self.base_url + urllib.quote(url) + self.get_options)) + feeds.append((re.search('/([^/]*)$', url).group(1), + self.base_url + urllib.quote(url.encode('utf-8')) + self.get_options)) return feeds From cfaa53f8ff927d343f63fb507c0268e7d06a3f08 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Fri, 6 Feb 2009 13:05:11 -0800 Subject: [PATCH 4/6] Allow send to device to send LRX files to the SONY readers. Fixes #1779 (LRX format not supported - Error converting to LRF) --- src/calibre/devices/prs500/driver.py | 2 +- src/calibre/devices/prs505/driver.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/calibre/devices/prs500/driver.py b/src/calibre/devices/prs500/driver.py index cca71376d4..2f1caaee9d 100755 --- a/src/calibre/devices/prs500/driver.py +++ b/src/calibre/devices/prs500/driver.py @@ -96,7 +96,7 @@ class PRS500(Device): # Location of cache.xml on storage card in device CACHE_XML = "/Sony Reader/database/cache.xml" # Ordered list of supported formats - FORMATS = ["lrf", "rtf", "pdf", "txt"] + FORMATS = ["lrf", "lrx", "rtf", "pdf", "txt"] # Height for thumbnails of books/images on the device THUMBNAIL_HEIGHT = 68 # Directory on card to which books are copied diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py index 8d505683aa..9308af2c5a 100644 --- a/src/calibre/devices/prs505/driver.py +++ b/src/calibre/devices/prs505/driver.py @@ -27,12 +27,12 @@ class File(object): class PRS505(Device): - VENDOR_ID = 0x054c #: SONY Vendor Id - PRODUCT_ID = 0x031e #: Product Id for the PRS-505 + VENDOR_ID = 0x054c #: SONY Vendor Id + PRODUCT_ID = 0x031e #: Product Id for the PRS-505 BCD = [0x229] #: Needed to disambiguate 505 and 700 on linux PRODUCT_NAME = 'PRS-505' VENDOR_NAME = 'SONY' - FORMATS = ['lrf', 'epub', "rtf", "pdf", "txt"] + FORMATS = ['lrf', 'epub', 'lrx', 'rtf', 'pdf', 'txt'] MEDIA_XML = 'database/cache/media.xml' CACHE_XML = 'Sony Reader/database/cache.xml' From edae7237d920a756c6f703a9d9985172aaa24d87 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Fri, 6 Feb 2009 14:03:58 -0800 Subject: [PATCH 5/6] Fix #1786 (User Manual link on Advanced Search screen doesn't do anything on Mac OS) --- src/calibre/gui2/dialogs/search.ui | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calibre/gui2/dialogs/search.ui b/src/calibre/gui2/dialogs/search.ui index f5813c18ee..b35ca84aca 100644 --- a/src/calibre/gui2/dialogs/search.ui +++ b/src/calibre/gui2/dialogs/search.ui @@ -114,6 +114,9 @@ <property name="text" > <string>See the <a href="http://calibre.kovidgoyal.net/user_manual/gui.html#the-search-interface">User Manual</a> for more help</string> </property> + <property name="openExternalLinks" > + <bool>true</bool> + </property> </widget> </item> <item> From 4ecec00044835212624057bd26d1c76eb7ad9f47 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Fri, 6 Feb 2009 17:31:40 -0800 Subject: [PATCH 6/6] Fix #1785 (Calibre exits when closing preference window on OS X) --- src/calibre/gui2/main.py | 50 ++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index 5da79794fc..e6475dd020 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -115,16 +115,11 @@ class Main(MainWindow, Ui_MainWindow): self.connect(self.donate_action, SIGNAL('triggered(bool)'), self.donate) self.connect(self.restore_action, SIGNAL('triggered(bool)'), lambda c : self.show()) self.connect(self.action_show_book_details, SIGNAL('triggered(bool)'), self.show_book_info) - def restart_app(c): - self.quit(None, restart=True) - self.connect(self.action_restart, SIGNAL('triggered(bool)'), restart_app) - def sta(r): - if r == QSystemTrayIcon.Trigger: - self.hide() if self.isVisible() else self.show() - self.connect(self.system_tray_icon, SIGNAL('activated(QSystemTrayIcon::ActivationReason)'), sta) - def tcme(self, *args): - pass - self.tool_bar.contextMenuEvent = tcme + self.connect(self.action_restart, SIGNAL('triggered(bool)'), + lambda c : self.quit(None, restart=True)) + self.connect(self.system_tray_icon, SIGNAL('activated(QSystemTrayIcon::ActivationReason)'), + self.system_tray_icon_activated) + self.tool_bar.contextMenuEvent = self.no_op ####################### Location View ######################## QObject.connect(self.location_view, SIGNAL('location_selected(PyQt_PyObject)'), self.location_selected) @@ -165,15 +160,11 @@ class Main(MainWindow, Ui_MainWindow): sm.addSeparator() sm.addAction(_('Send to storage card by default')) sm.actions()[-1].setCheckable(True) - def default_sync(checked): - config.set('send_to_storage_card_by_default', bool(checked)) - QObject.disconnect(self.action_sync, SIGNAL("triggered(bool)"), self.sync_to_main_memory) - QObject.disconnect(self.action_sync, SIGNAL("triggered(bool)"), self.sync_to_card) - QObject.connect(self.action_sync, SIGNAL("triggered(bool)"), self.sync_to_card if checked else self.sync_to_main_memory) - QObject.connect(sm.actions()[-1], SIGNAL('toggled(bool)'), default_sync) + QObject.connect(sm.actions()[-1], SIGNAL('toggled(bool)'), + self.do_default_sync) sm.actions()[-1].setChecked(config.get('send_to_storage_card_by_default')) - default_sync(sm.actions()[-1].isChecked()) + self.do_default_sync(sm.actions()[-1].isChecked()) self.sync_menu = sm # Needed md = QMenu() md.addAction(_('Edit metadata individually')) @@ -371,6 +362,31 @@ class Main(MainWindow, Ui_MainWindow): self.connect(self.action_news, SIGNAL('triggered(bool)'), self.scheduler.show_dialog) self.location_view.setCurrentIndex(self.location_view.model().index(0)) + def no_op(self, *args): + pass + + def system_tray_icon_activated(self, r): + if r == QSystemTrayIcon.Trigger: + if self.isVisible(): + for window in QApplication.topLevelWidgets(): + if isinstance(window, (MainWindow, QDialog)): + window.hide() + else: + for window in QApplication.topLevelWidgets(): + if isinstance(window, (MainWindow, QDialog)): + if window not in (self.device_error_dialog, self.jobs_dialog): + window.show() + + + def do_default_sync(self, checked): + config.set('send_to_storage_card_by_default', bool(checked)) + QObject.disconnect(self.action_sync, SIGNAL("triggered(bool)"), + self.sync_to_main_memory) + QObject.disconnect(self.action_sync, SIGNAL("triggered(bool)"), + self.sync_to_card) + QObject.connect(self.action_sync, SIGNAL("triggered(bool)"), + self.sync_to_card if checked else self.sync_to_main_memory) + def change_output_format(self, x): of = unicode(x).strip() if of != prefs['output_format']: