From e6a7357e666ef1886fc8b3f2f95736428ced0eb1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 5 Mar 2013 11:47:43 +0530 Subject: [PATCH] Start work on ToC editor --- src/calibre/ebooks/metadata/toc.py | 11 +- src/calibre/ebooks/oeb/polish/container.py | 9 ++ src/calibre/ebooks/oeb/polish/toc.py | 83 +++++++++++++ src/calibre/gui2/toc/__init__.py | 11 ++ src/calibre/gui2/toc/main.py | 132 +++++++++++++++++++++ 5 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 src/calibre/ebooks/oeb/polish/toc.py create mode 100644 src/calibre/gui2/toc/__init__.py create mode 100644 src/calibre/gui2/toc/main.py diff --git a/src/calibre/ebooks/metadata/toc.py b/src/calibre/ebooks/metadata/toc.py index 0b8d3dc68b..f2f49e2c63 100644 --- a/src/calibre/ebooks/metadata/toc.py +++ b/src/calibre/ebooks/metadata/toc.py @@ -157,12 +157,13 @@ class TOC(list): toc = m[0] self.read_ncx_toc(toc) - def read_ncx_toc(self, toc): + def read_ncx_toc(self, toc, root=None): self.base_path = os.path.dirname(toc) - raw = xml_to_unicode(open(toc, 'rb').read(), assume_utf8=True, - strip_encoding_pats=True)[0] - root = etree.fromstring(raw, parser=etree.XMLParser(recover=True, - no_network=True)) + if root is None: + raw = xml_to_unicode(open(toc, 'rb').read(), assume_utf8=True, + strip_encoding_pats=True)[0] + root = etree.fromstring(raw, parser=etree.XMLParser(recover=True, + no_network=True)) xpn = {'re': 'http://exslt.org/regular-expressions'} XPath = functools.partial(etree.XPath, namespaces=xpn) diff --git a/src/calibre/ebooks/oeb/polish/container.py b/src/calibre/ebooks/oeb/polish/container.py index db55f9579d..cf499990a5 100644 --- a/src/calibre/ebooks/oeb/polish/container.py +++ b/src/calibre/ebooks/oeb/polish/container.py @@ -8,6 +8,7 @@ __copyright__ = '2013, Kovid Goyal ' __docformat__ = 'restructuredtext en' import os, logging, sys, hashlib, uuid, re +from collections import defaultdict from io import BytesIO from urllib import unquote as urlunquote, quote as urlquote from urlparse import urlparse @@ -230,6 +231,14 @@ class Container(object): return {item.get('id'):self.href_to_name(item.get('href'), self.opf_name) for item in self.opf_xpath('//opf:manifest/opf:item[@href and @id]')} + @property + def manifest_type_map(self): + ans = defaultdict(list) + for item in self.opf_xpath('//opf:manifest/opf:item[@href and @media-type]'): + ans[item.get('media-type').lower()].append(self.href_to_name( + item.get('href'), self.opf_name)) + return {mt:tuple(v) for mt, v in ans.iteritems()} + @property def guide_type_map(self): return {item.get('type', ''):self.href_to_name(item.get('href'), self.opf_name) diff --git a/src/calibre/ebooks/oeb/polish/toc.py b/src/calibre/ebooks/oeb/polish/toc.py new file mode 100644 index 0000000000..b2a0c5780d --- /dev/null +++ b/src/calibre/ebooks/oeb/polish/toc.py @@ -0,0 +1,83 @@ +#!/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__ = '2013, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from urlparse import urlparse + +from lxml import etree + +from calibre.ebooks.oeb.polish.container import guess_type + +ns = etree.FunctionNamespace('calibre_xpath_extensions') +ns.prefix = 'calibre' +ns['lower-case'] = lambda c, x: x.lower() if hasattr(x, 'lower') else x + +class TOC(object): + + def __init__(self, title=None, dest=None, frag=None): + self.title, self.dest, self.frag = title, dest, frag + self.parent = None + self.children = [] + + def add(self, title, dest, frag=None): + c = TOC(title, dest, frag) + self.children.append(c) + c.parent = self + return c + + def __iter__(self): + for c in self.children: + yield c + +def child_xpath(tag, name): + return tag.xpath('./*[calibre:lower-case(local-name()) = "%s"]'%name) + +def add_from_navpoint(container, navpoint, parent, ncx_name): + dest = frag = text = None + nl = child_xpath(navpoint, 'navlabel') + if nl: + nl = nl[0] + text = '' + for txt in child_xpath(nl, 'text'): + text += etree.tostring(txt, method='text', + encoding=unicode, with_tail=False) + content = child_xpath(navpoint, 'content') + if content: + content = content[0] + href = content.get('src', None) + if href: + dest = container.href_to_name(href, base=ncx_name) + frag = urlparse(href).fragment or None + return parent.add(text or None, dest or None, frag or None) + +def process_ncx_node(container, node, toc_parent, ncx_name): + for navpoint in node.xpath('./*[calibre:lower-case(local-name()) = "navpoint"]'): + child = add_from_navpoint(container, navpoint, toc_parent, ncx_name) + if child is not None: + process_ncx_node(container, navpoint, child, ncx_name) + +def parse_ncx(container, ncx_name): + root = container.parsed(ncx_name) + toc_root = TOC() + navmaps = root.xpath('//*[calibre:lower-case(local-name()) = "navmap"]') + if navmaps: + process_ncx_node(container, navmaps[0], toc_root, ncx_name) + return toc_root + +def get_toc(container): + toc = container.opf_xpath('//opf:spine/@toc') + if toc: + toc = container.manifest_id_map.get(toc[0], None) + if not toc: + ncx = guess_type('a.ncx') + toc = container.manifest_type_map.get(ncx, [None])[0] + if not toc: + return None + return parse_ncx(container, toc) + + diff --git a/src/calibre/gui2/toc/__init__.py b/src/calibre/gui2/toc/__init__.py new file mode 100644 index 0000000000..07138c49b8 --- /dev/null +++ b/src/calibre/gui2/toc/__init__.py @@ -0,0 +1,11 @@ +#!/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__ = '2013, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + + diff --git a/src/calibre/gui2/toc/main.py b/src/calibre/gui2/toc/main.py new file mode 100644 index 0000000000..0b57d88b6f --- /dev/null +++ b/src/calibre/gui2/toc/main.py @@ -0,0 +1,132 @@ +#!/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__ = '2013, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import sys, os +from threading import Thread + +from PyQt4.Qt import (QDialog, QVBoxLayout, QDialogButtonBox, QSize, + QStackedWidget, QWidget, QLabel, Qt, pyqtSignal, QIcon, + QTreeWidget, QHBoxLayout, QTreeWidgetItem) + +from calibre.ebooks.oeb.polish.container import get_container +from calibre.ebooks.oeb.polish.toc import get_toc +from calibre.gui2 import Application +from calibre.gui2.progress_indicator import ProgressIndicator +from calibre.utils.logging import GUILog + +class TOCView(QWidget): + + def __init__(self, parent): + QWidget.__init__(self, parent) + l = self.l = QHBoxLayout() + self.setLayout(l) + self.tocw = t = QTreeWidget(self) + t.setHeaderLabel(_('Table of Contents')) + icon_size = 16 + t.setIconSize(QSize(icon_size, icon_size)) + t.setDragEnabled(True) + t.setSelectionMode(t.SingleSelection) + t.viewport().setAcceptDrops(True) + t.setDropIndicatorShown(True) + t.setDragDropMode(t.InternalMove) + t.setAutoScroll(True) + t.setAutoScrollMargin(icon_size*2) + t.setDefaultDropAction(Qt.MoveAction) + l.addWidget(t) + + def data_changed(self, top_left, bottom_right): + for r in xrange(top_left.row(), bottom_right.row()+1): + idx = self.tocw.model().index(r, 0, top_left.parent()) + new_title = unicode(idx.data(Qt.DisplayRole).toString()).strip() + toc = idx.data(Qt.UserRole).toPyObject() + toc.title = new_title or _('(Untitled)') + + def __call__(self, ebook): + self.ebook = ebook + self.toc = get_toc(self.ebook) + blank = QIcon(I('blank.png')) + + def process_item(node, parent): + for child in node: + c = QTreeWidgetItem(parent) + c.setData(0, Qt.DisplayRole, child.title or _('(Untitled)')) + c.setData(0, Qt.UserRole, child) + c.setFlags(Qt.ItemIsDragEnabled|Qt.ItemIsEditable|Qt.ItemIsEnabled| + Qt.ItemIsSelectable|Qt.ItemIsDropEnabled) + c.setData(0, Qt.DecorationRole, blank) + process_item(child, c) + + root = self.tocw.invisibleRootItem() + root.setData(0, Qt.UserRole, self.toc) + process_item(self.toc, root) + self.tocw.model().dataChanged.connect(self.data_changed) + +class TOCEditor(QDialog): + + explode_done = pyqtSignal() + + def __init__(self, pathtobook, title=None, parent=None): + QDialog.__init__(self, parent) + self.pathtobook = pathtobook + + t = title or os.path.basename(pathtobook) + self.setWindowTitle(_('Edit the ToC in %s')%t) + self.setWindowIcon(QIcon(I('highlight_only_on.png'))) + + l = self.l = QVBoxLayout() + self.setLayout(l) + + self.stacks = s = QStackedWidget(self) + l.addWidget(s) + self.loading_widget = lw = QWidget(self) + s.addWidget(lw) + ll = QVBoxLayout() + lw.setLayout(ll) + self.pi = pi = ProgressIndicator() + pi.setDisplaySize(200) + pi.startAnimation() + ll.addWidget(pi, alignment=Qt.AlignHCenter|Qt.AlignCenter) + la = QLabel(_('Loading %s, please wait...')%t) + la.setStyleSheet('QLabel { font-size: 20pt }') + ll.addWidget(la, alignment=Qt.AlignHCenter|Qt.AlignTop) + self.toc_view = TOCView(self) + s.addWidget(self.toc_view) + + bb = self.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) + l.addWidget(bb) + bb.accepted.connect(self.accept) + bb.rejected.connect(self.reject) + + self.explode_done.connect(self.read_toc, type=Qt.QueuedConnection) + + self.resize(950, 630) + + def start(self): + t = Thread(target=self.explode) + t.daemon = True + self.log = GUILog() + t.start() + + def explode(self): + self.ebook = get_container(self.pathtobook, log=self.log) + if not self.isVisible(): + return + self.explode_done.emit() + + def read_toc(self): + self.toc_view(self.ebook) + self.stacks.setCurrentIndex(1) + +if __name__ == '__main__': + app = Application([], force_calibre_style=True) + app + d = TOCEditor(sys.argv[-1]) + d.start() + d.exec_() +