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 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
if not xpath(data, '/h:html/h:body'):
self.oeb.logger.warn(
From 773ab64a765b746f963a3d3b6b51adc7e4eaf978 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
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
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
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
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 @@
See the <a href="http://calibre.kovidgoyal.net/user_manual/gui.html#the-search-interface">User Manual</a> for more help
+
+ true
+
-
From 4ecec00044835212624057bd26d1c76eb7ad9f47 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
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']: