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] toc = m[0]
self.read_ncx_toc(toc) 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) self.base_path = os.path.dirname(toc)
if root is None:
raw = xml_to_unicode(open(toc, 'rb').read(), assume_utf8=True, raw = xml_to_unicode(open(toc, 'rb').read(), assume_utf8=True,
strip_encoding_pats=True)[0] strip_encoding_pats=True)[0]
root = etree.fromstring(raw, parser=etree.XMLParser(recover=True, 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' __docformat__ = 'restructuredtext en'
import os, logging, sys, hashlib, uuid, re import os, logging, sys, hashlib, uuid, re
from collections import defaultdict
from io import BytesIO from io import BytesIO
from urllib import unquote as urlunquote, quote as urlquote from urllib import unquote as urlunquote, quote as urlquote
from urlparse import urlparse 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) 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]')} 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 @property
def guide_type_map(self): def guide_type_map(self):
return {item.get('type', ''):self.href_to_name(item.get('href'), self.opf_name) 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_()