Start work on ToC editor

This commit is contained in:
Kovid Goyal 2013-03-05 11:47:43 +05:30
parent 7b6db40323
commit e6a7357e66
5 changed files with 241 additions and 5 deletions

View File

@ -157,8 +157,9 @@ 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)
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,

View File

@ -8,6 +8,7 @@ __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__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)

View File

@ -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 <kovid at kovidgoyal.net>'
__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)

View File

@ -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 <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

View File

@ -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 <kovid at kovidgoyal.net>'
__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_()