Sync to trunk

This commit is contained in:
Kovid Goyal 2009-01-30 20:58:48 -08:00
commit 94cf3eae9b
57 changed files with 15829 additions and 14042 deletions

View File

@ -441,15 +441,19 @@ def entity_to_unicode(match, exceptions=[], encoding='cp1252'):
if isosx:
fdir = os.path.expanduser('~/.fonts')
if not os.path.exists(fdir):
os.makedirs(fdir)
if not os.path.exists(os.path.join(fdir, 'LiberationSans_Regular.ttf')):
from calibre.ebooks.lrf.fonts.liberation import __all__ as fonts
for font in fonts:
l = {}
exec 'from calibre.ebooks.lrf.fonts.liberation.'+font+' import font_data' in l
open(os.path.join(fdir, font+'.ttf'), 'wb').write(l['font_data'])
try:
if not os.path.exists(fdir):
os.makedirs(fdir)
if not os.path.exists(os.path.join(fdir, 'LiberationSans_Regular.ttf')):
from calibre.ebooks.lrf.fonts.liberation import __all__ as fonts
for font in fonts:
l = {}
exec 'from calibre.ebooks.lrf.fonts.liberation.'+font+' import font_data' in l
open(os.path.join(fdir, font+'.ttf'), 'wb').write(l['font_data'])
except:
import traceback
traceback.print_exc()
# Migrate from QSettings based config system
from calibre.utils.config import migrate
migrate()

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
__version__ = '0.4.131'
__version__ = '0.4.132'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
'''
Various run time constants.

View File

@ -181,11 +181,11 @@ class Device(_Device):
elif self.windows_match_device(str(drive.PNPDeviceID), self.WINDOWS_CARD_MEM):
drives['card'] = self.windows_get_drive_prefix(drive)
if 'main' and 'card' in drives.keys():
if 'main' in drives.keys() and 'card' in drives.keys():
break
self._main_prefix = drives.get('main', None)
self._card_prefix = drives.get('card', None)
self._main_prefix = drives.get('main')
self._card_prefix = drives.get('card')
if not self._main_prefix:
raise DeviceError(_('Unable to detect the %s disk drive. Try rebooting.') % self.__class__.__name__)

View File

@ -142,6 +142,8 @@ to auto-generate a Table of Contents.
help=_('XPath expression that specifies all tags that should be added to the Table of Contents at level one. If this is specified, it takes precedence over other forms of auto-detection.'))
toc('level2_toc', ['--level2-toc'], default=None,
help=_('XPath expression that specifies all tags that should be added to the Table of Contents at level two. Each entry is added under the previous level one entry.'))
toc('level3_toc', ['--level3-toc'], default=None,
help=_('XPath expression that specifies all tags that should be added to the Table of Contents at level three. Each entry is added under the previous level two entry.'))
toc('from_ncx', ['--from-ncx'], default=None,
help=_('Path to a .ncx file that contains the table of contents to use for this ebook. The NCX file should contain links relative to the directory it is placed in. See http://www.niso.org/workrooms/daisy/Z39-86-2005.html#NCX for an overview of the NCX format.'))
toc('use_auto_toc', ['--use-auto-toc'], default=False,

View File

@ -377,16 +377,13 @@ def convert(htmlfile, opts, notification=None, create_epub=True,
mi = merge_metadata(htmlfile, opf, opts)
opts.chapter = XPath(opts.chapter,
namespaces={'re':'http://exslt.org/regular-expressions'})
if opts.level1_toc:
opts.level1_toc = XPath(opts.level1_toc,
namespaces={'re':'http://exslt.org/regular-expressions'})
else:
opts.level1_toc = None
if opts.level2_toc:
opts.level2_toc = XPath(opts.level2_toc,
namespaces={'re':'http://exslt.org/regular-expressions'})
else:
opts.level2_toc = None
for x in (1, 2, 3):
attr = 'level%d_toc'%x
if getattr(opts, attr):
setattr(opts, attr, XPath(getattr(opts, attr),
namespaces={'re':'http://exslt.org/regular-expressions'}))
else:
setattr(opts, attr, None)
with TemporaryDirectory(suffix='_html2epub', keep=opts.keep_intermediate) as tdir:
if opts.keep_intermediate:

View File

@ -558,31 +558,22 @@ class Processor(Parser):
def detect_chapters(self):
self.detected_chapters = self.opts.chapter(self.root)
chapter_mark = self.opts.chapter_mark
page_break_before = 'display: block; page-break-before: always'
page_break_after = 'display: block; page-break-after: always'
for elem in self.detected_chapters:
text = u' '.join([t.strip() for t in elem.xpath('descendant::text()')])
self.log_info('\tDetected chapter: %s', text[:50])
if self.opts.chapter_mark != 'none':
hr = etree.Element('hr')
if elem.getprevious() is None:
elem.getparent()[:0] = [hr]
elif elem.getparent() is not None:
insert = None
for i, c in enumerate(elem.getparent()):
if c is elem:
insert = i
break
elem.getparent()[insert:insert] = [hr]
if self.opts.chapter_mark != 'rule':
hr.set('style', 'width:0pt;page-break-before:always')
if self.opts.chapter_mark == 'both':
hr2 = etree.Element('hr')
hr2.tail = u'\u00a0'
p = hr.getparent()
i = p.index(hr)
p[i:i] = [hr2]
if chapter_mark == 'none':
continue
elif chapter_mark == 'rule':
mark = etree.Element('hr')
elif chapter_mark == 'pagebreak':
mark = etree.Element('div', style=page_break_after)
else: # chapter_mark == 'both':
mark = etree.Element('hr', style=page_break_before)
elem.addprevious(mark)
def save(self):
style_path = os.path.splitext(os.path.basename(self.save_path()))[0]
for i, sheet in enumerate([self.stylesheet, self.font_css, self.override_css]):
@ -647,6 +638,7 @@ class Processor(Parser):
added[elem] = add_item(_href, frag, text, toc, type='chapter')
add_item(_href, frag, 'Top', added[elem], type='chapter')
if self.opts.level2_toc is not None:
added2 = {}
level2 = list(self.opts.level2_toc(self.root))
for elem in level2:
level1 = None
@ -657,7 +649,21 @@ class Processor(Parser):
text, _href, frag = elem_to_link(elem, href, counter)
counter += 1
if text:
added2[elem] = \
add_item(_href, frag, text, level1, type='chapter')
if self.opts.level3_toc is not None:
level3 = list(self.opts.level3_toc(self.root))
for elem in level3:
level2 = None
for item in self.root.iterdescendants():
if item in added2.keys():
level2 = added2[item]
elif item == elem and level2 is not None:
text, _href, frag = elem_to_link(elem, href, counter)
counter += 1
if text:
add_item(_href, frag, text, level2, type='chapter')
if len(toc) > 0:
return
@ -892,7 +898,7 @@ def config(defaults=None, config_name='html',
metadata('title', ['-t', '--title'], default=None,
help=_('Set the title. Default is to autodetect.'))
metadata('authors', ['-a', '--authors'], default=None,
help=_('The author(s) of the ebook, as a comma separated list.'))
help=_('The author(s) of the ebook, as a & separated list.'))
metadata('tags', ['--subjects'], default=None,
help=_('The subject(s) of this book, as a comma separated list.'))
metadata('publisher', ['--publisher'], default=None,
@ -988,7 +994,9 @@ def merge_metadata(htmlfile, opf, opts):
val = getattr(opts, attr, None)
if val is None or val == _('Unknown') or val == [_('Unknown')]:
continue
if attr in ('authors', 'tags'):
if attr =='authors':
val = [i.strip() for i in val.split('&') if i.strip()]
elif attr == 'tags':
val = [i.strip() for i in val.split(',') if i.strip()]
setattr(mi, attr, val)
@ -997,7 +1005,10 @@ def merge_metadata(htmlfile, opf, opts):
mi.cover = os.path.abspath(cover)
if not mi.title:
mi.title = os.path.splitext(os.path.basename(htmlfile))[0]
if htmlfile:
mi.title = os.path.splitext(os.path.basename(htmlfile))[0]
else:
mi.title = _('Unknown')
if not mi.authors:
mi.authors = [_('Unknown')]
return mi

View File

@ -170,7 +170,7 @@ def generate_html(rtfpath, tdir):
f.write(res)
f.close()
try:
mi = get_metadata(open(rtfpath, 'rb'))
mi = get_metadata(open(rtfpath, 'rb'), 'rtf')
except:
mi = MetaInformation(None, None)
if not mi.title:

View File

@ -0,0 +1,185 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
ebook-meta
'''
USAGE='%%prog ebook_file [' + _('options') + ']\n' + \
_('''
Read/Write metadata from/to ebook files.
Supported formats for reading metadata: %s
Supported formats for writing metadata: %s
Different file types support different kinds of metadata. If you try to set
some metadata on a file type that does not support it, the metadata will be
silently ignored.
''')
import sys, os
from calibre.utils.config import StringConfig
from calibre.customize.ui import metadata_readers, metadata_writers
from calibre.ebooks.metadata.meta import get_metadata, set_metadata
from calibre.ebooks.metadata import string_to_authors, authors_to_sort_string, \
title_sort, MetaInformation
from calibre import prints
def config():
c = StringConfig('')
c.add_opt('title', ['-t', '--title'],
help=_('Set the title.'))
c.add_opt('authors', ['-a', '--authors'],
help=_('Set the authors. Multiple authors should be separated '
'by the & character. Author names should be in the order '
'Firstname Lastname.'))
c.add_opt('title_sort', ['--title-sort'],
help=_('The version of the title to be used for sorting. '
'If unspecified, and the title is specified, it will '
'be auto-generated from the title.'))
c.add_opt('author_sort', ['--author-sort'],
help=_('String to be used when sorting by author. '
'If unspecified, and the author(s) are specified, it will '
'be auto-generated from the author(s).'))
c.add_opt('cover', ['--cover'],
help=_('Set the cover to the specified file.'))
c.add_opt('comments', ['-c', '--comments'],
help=_('Set the ebook description.'))
c.add_opt('publisher', ['-p', '--publisher'],
help=_('Set the ebook publisher.'))
c.add_opt('series', ['-s', '--series'],
help=_('Set the series this ebook belongs to.'))
c.add_opt('series_index', ['-i', '--index'],
help=_('Set the index of the book in this series.'))
c.add_opt('rating', ['-r', '--rating'],
help=_('Set the rating. Should be a number between 1 and 5.'))
c.add_opt('isbn', ['--isbn'],
help=_('Set the ISBN of the book.'))
c.add_opt('tags', ['--tags'],
help=_('Set the tags for the book. Should be a comma separated list.'))
c.add_opt('book_producer', ['-k', '--book-producer'],
help=_('Set the book producer.'))
c.add_opt('language', ['-l', '--language'],
help=_('Set the language.'))
c.add_opt('get_cover', ['--get-cover'],
help=_('Get the cover from the ebook and save it at as the '
'specified file.'))
c.add_opt('to_opf', ['--to-opf'],
help=_('Specify the name of an OPF file. The metadata will '
'be written to the OPF file.'))
c.add_opt('from_opf', ['--from-opf'],
help=_('Read metadata from the specified OPF file and use it to '
'set metadata in the ebook. Metadata specified on the'
'command line will override metadata read from the OPF file'))
return c
def filetypes():
readers = set([])
for r in metadata_readers():
readers = readers.union(set(r.file_types))
return readers
def option_parser():
writers = set([])
for w in metadata_writers():
writers = writers.union(set(w.file_types))
return config().option_parser(USAGE%(list(filetypes()), list(writers)))
def do_set_metadata(opts, mi, stream, stream_type):
mi = MetaInformation(mi)
for x in ('guide', 'toc', 'manifest', 'spine'):
setattr(mi, x, None)
from_opf = getattr(opts, 'from_opf', None)
if from_opf is not None:
from calibre.ebooks.metadata.opf2 import OPF
opf_mi = MetaInformation(OPF(open(from_opf, 'rb')))
mi.smart_update(opf_mi)
for pref in config().option_set.preferences:
if pref.name in ('to_opf', 'from_opf', 'authors', 'title_sort',
'author_sort', 'get_cover', 'cover', 'tags'):
continue
val = getattr(opts, pref.name, None)
if val is not None:
setattr(mi, pref.name, getattr())
if getattr(opts, 'authors', None) is not None:
mi.authors = string_to_authors(opts.authors)
mi.author_sort = authors_to_sort_string(mi.authors)
if getattr(opts, 'author_sort', None) is not None:
mi.author_sort = opts.author_sort
if getattr(opts, 'title_sort', None) is not None:
mi.title_sort = opts.title_sort
elif getattr(opts, 'title', None) is not None:
mi.title_sort = title_sort(opts.title)
if getattr(opts, 'tags', None) is not None:
mi.tags = [t.strip() for t in opts.tags.split(',')]
if getattr(opts, 'cover', None) is not None:
ext = os.path.splitext(opts.cover)[1].replace('.', '').upper()
mi.cover_data = (ext, open(opts.cover, 'rb').read())
set_metadata(stream, mi, stream_type)
def main(args=sys.argv):
parser = option_parser()
opts, args = parser.parse_args(args)
if len(args) < 2:
parser.print_help()
prints(_('No file specified'), file=sys.stderr)
return 1
path = args[1]
stream = open(path, 'r+b')
stream_type = os.path.splitext(path)[1].replace('.', '').lower()
trying_to_set = False
for pref in config().option_set.preferences:
if pref.name in ('to_opf', 'get_cover'):
continue
if getattr(opts, pref.name) is not None:
trying_to_set = True
break
mi = get_metadata(stream, stream_type)
if trying_to_set:
prints(_('Original metadata')+'::')
metadata = unicode(mi)
if trying_to_set:
metadata = '\t'+'\n\t'.join(metadata.split('\n'))
prints(metadata)
if trying_to_set:
stream.seek(0)
do_set_metadata(opts, mi, stream, stream_type)
stream.seek(0)
stream.flush()
mi = get_metadata(stream, stream_type)
prints(_('Changed metadata')+'::')
metadata = unicode(mi)
metadata = '\t'+'\n\t'.join(metadata.split('\n'))
prints(metadata)
if opts.to_opf is not None:
from calibre.ebooks.metadata.opf2 import OPFCreator
opf = OPFCreator(os.getcwdu(), mi)
with open(opts.opf, 'wb') as f:
opf.render(f)
prints(_('OPF created in'), opts.opf)
if opts.get_cover is not None:
if mi.cover_data and mi.cover_data[1]:
with open(opts.get_cover, 'wb') as f:
f.write(mi.cover_data[1])
prints(_('Cover saved to'), f.name)
else:
prints(_('No cover found'), file=sys.stderr)
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -106,9 +106,11 @@ class CoverRenderer(QObject):
WIDTH = 600
HEIGHT = 800
def __init__(self, url, size, loop):
def __init__(self, path):
if QApplication.instance() is None:
QApplication([])
QObject.__init__(self)
self.loop = loop
self.loop = QEventLoop()
self.page = QWebPage()
pal = self.page.palette()
pal.setBrush(QPalette.Background, Qt.white)
@ -117,33 +119,43 @@ class CoverRenderer(QObject):
self.page.mainFrame().setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff)
self.page.mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
QObject.connect(self.page, SIGNAL('loadFinished(bool)'), self.render_html)
self.image_data = None
self._image_data = None
self.rendered = False
url = QUrl.fromLocalFile(os.path.normpath(path))
self.page.mainFrame().load(url)
def render_html(self, ok):
self.rendered = True
try:
if not ok:
self.rendered = True
return
#size = self.page.mainFrame().contentsSize()
#width, height = fit_image(size.width(), size.height(), self.WIDTH, self.HEIGHT)[1:]
#self.page.setViewportSize(QSize(width, height))
image = QImage(self.page.viewportSize(), QImage.Format_ARGB32)
image.setDotsPerMeterX(96*(100/2.54))
image.setDotsPerMeterY(96*(100/2.54))
painter = QPainter(image)
self.page.mainFrame().render(painter)
painter.end()
ba = QByteArray()
buf = QBuffer(ba)
buf.open(QBuffer.WriteOnly)
image.save(buf, 'JPEG')
self.image_data = str(ba.data())
self._image_data = str(ba.data())
finally:
self.loop.exit(0)
self.rendered = True
def image_data():
def fget(self):
if not self.rendered:
self.loop.exec_()
count = 0
while count < 50 and not self.rendered:
time.sleep(0.1)
count += 1
return self._image_data
return property(fget=fget)
image_data = image_data()
def get_cover(opf, opf_path, stream):
spine = list(opf.spine_items())
@ -155,20 +167,11 @@ def get_cover(opf, opf_path, stream):
stream.seek(0)
ZipFile(stream).extractall()
opf_path = opf_path.replace('/', os.sep)
cpage = os.path.join(tdir, os.path.dirname(opf_path), *cpage.split('/'))
cpage = os.path.join(tdir, os.path.dirname(opf_path), cpage)
if not os.path.exists(cpage):
return
if QApplication.instance() is None:
QApplication([])
url = QUrl.fromLocalFile(cpage)
loop = QEventLoop()
cr = CoverRenderer(url, os.stat(cpage).st_size, loop)
loop.exec_()
count = 0
while count < 50 and not cr.rendered:
time.sleep(0.1)
count += 1
return cr.image_data
cr = CoverRenderer(cpage)
return cr.image_data
def get_metadata(stream, extract_cover=True):
""" Return metadata as a :class:`MetaInformation` object """

View File

@ -5,7 +5,7 @@
unique-identifier="${__appname__}_id"
>
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xmlns:calibre="http://calibre.kovidgoyal.net/2009/metadata">
<dc:title py:with="attrs={'opf:files-as':mi.title_sort}" py:attrs="attrs">${mi.title}</dc:title>
<dc:creator opf:role="aut" py:for="i, author in enumerate(mi.authors)" py:attrs="{'opf:file-as':mi.author_sort} if mi.author_sort and i == 0 else {}">${author}</dc:creator>
<dc:contributor opf:role="bkp" py:with="attrs={'opf:files-as':__appname__}" py:attrs="attrs">${'%s (%s)'%(__appname__, __version__)} [http://${__appname__}.kovidgoyal.net]</dc:contributor>
@ -16,9 +16,9 @@
<dc:description py:if="mi.comments">${mi.comments}</dc:description>
<dc:publisher py:if="mi.publisher">${mi.publisher}</dc:publisher>
<dc:identifier opf:scheme="ISBN" py:if="mi.isbn">${mi.isbn}</dc:identifier>
<series py:if="mi.series">${mi.series}</series>
<series_index py:if="mi.series_index is not None">${mi.series_index}</series_index>
<rating py:if="mi.rating is not None">${mi.rating}</rating>
<meta py:if="mi.series is not None" name="calibre:series" content="${mi.series}"/>
<meta py:if="mi.series_index is not None" name="calibre:series_index" content="${mi.series_index}"/>
<meta py:if="mi.rating is not None" name="calibre:rating" content="${mi.rating}"/>
<py:for each="tag in mi.tags">
<dc:subject py:if="mi.tags is not None">${tag}</dc:subject>
</py:for>

View File

@ -392,8 +392,8 @@ class MetadataField(object):
def __set__(self, obj, val):
elem = obj.get_metadata_element(self.name)
if elem is None:
elem = obj.create_metadata_element(self.name, ns='dc' if self.is_dc else 'opf')
elem.text = unicode(val)
elem = obj.create_metadata_element(self.name, is_dc=self.is_dc)
obj.set_text(elem, unicode(val))
class OPF(object):
MIMETYPE = 'application/oebps-package+xml'
@ -403,16 +403,17 @@ class OPF(object):
'dc' : "http://purl.org/dc/elements/1.1/",
'opf' : "http://www.idpf.org/2007/opf",
}
META = '{%s}meta' % NAMESPACES['opf']
xpn = NAMESPACES.copy()
xpn.pop(None)
xpn['re'] = 'http://exslt.org/regular-expressions'
XPath = functools.partial(etree.XPath, namespaces=xpn)
CONTENT = XPath('self::*[re:match(name(), "meta$", "i")]/@content')
TEXT = XPath('string()')
metadata_path = XPath('descendant::*[re:match(name(), "metadata", "i")]')
metadata_elem_path = XPath('descendant::*[re:match(name(), $name, "i")]')
series_path = XPath('descendant::*[re:match(name(), "series$", "i")]')
metadata_elem_path = XPath('descendant::*[re:match(name(), concat($name, "$"), "i") or (re:match(name(), "meta$", "i") and re:match(@name, concat("^calibre:", $name, "$"), "i"))]')
authors_path = XPath('descendant::*[re:match(name(), "creator", "i") and (@role="aut" or @opf:role="aut" or (not(@role) and not(@opf:role)))]')
bkp_path = XPath('descendant::*[re:match(name(), "contributor", "i") and (@role="bkp" or @opf:role="bkp")]')
tags_path = XPath('descendant::*[re:match(name(), "subject", "i")]')
@ -431,6 +432,7 @@ class OPF(object):
language = MetadataField('language')
comments = MetadataField('description')
category = MetadataField('category')
series = MetadataField('series', is_dc=False)
series_index = MetadataField('series_index', is_dc=False, formatter=int, none_is=1)
rating = MetadataField('rating', is_dc=False, formatter=int)
@ -497,7 +499,13 @@ class OPF(object):
def get_text(self, elem):
return u''.join(self.TEXT(elem))
return u''.join(self.CONTENT(elem) or self.TEXT(elem))
def set_text(self, elem, content):
if elem.tag == self.META:
elem.attib['content'] = content
else:
elem.text = content
def itermanifest(self):
return self.manifest_path(self.root)
@ -611,9 +619,9 @@ class OPF(object):
for elem in remove:
self.metadata.remove(elem)
for author in val:
elem = self.create_metadata_element('creator', ns='dc',
attrib={'{%s}role'%self.NAMESPACES['opf']:'aut'})
elem.text = author
attrib = {'{%s}role'%self.NAMESPACES['opf']: 'aut'}
elem = self.create_metadata_element('creator', attrib=attrib)
self.set_text(elem, author)
return property(fget=fget, fset=fset)
@ -650,8 +658,8 @@ class OPF(object):
for tag in list(self.tags_path(self.metadata)):
self.metadata.remove(tag)
for tag in val:
elem = self.create_metadata_element('subject', ns='dc')
elem.text = unicode(tag)
elem = self.create_metadata_element('subject')
self.set_text(elem, unicode(tag))
return property(fget=fget, fset=fset)
@ -660,14 +668,15 @@ class OPF(object):
def fget(self):
for match in self.isbn_path(self.metadata):
return match.text if match.text else None
return self.get_text(match) or None
def fset(self, val):
matches = self.isbn_path(self.metadata)
if not matches:
matches = [self.create_metadata_element('identifier', ns='dc',
attrib={'{%s}scheme'%self.NAMESPACES['opf']:'ISBN'})]
matches[0].text = unicode(val)
attrib = {'{%s}scheme'%self.NAMESPACES['opf']: 'ISBN'}
matches = [self.create_metadata_element('identifier',
attrib=attrib)]
self.set_text(matches[0], unicode(val))
return property(fget=fget, fset=fset)
@ -676,48 +685,32 @@ class OPF(object):
def fget(self):
for match in self.application_id_path(self.metadata):
return match.text if match.text else None
return self.get_text(match) or None
def fset(self, val):
matches = self.application_id_path(self.metadata)
if not matches:
matches = [self.create_metadata_element('identifier', ns='dc',
attrib={'{%s}scheme'%self.NAMESPACES['opf']:'calibre'})]
matches[0].text = unicode(val)
attrib = {'{%s}scheme'%self.NAMESPACES['opf']: 'calibre'}
matches = [self.create_metadata_element('identifier',
attrib=attrib)]
self.set_text(matches[0], unicode(val))
return property(fget=fget, fset=fset)
@apply
def series():
def fget(self):
for match in self.series_path(self.metadata):
return match.text if match.text else None
def fset(self, val):
matches = self.series_path(self.metadata)
if not matches:
matches = [self.create_metadata_element('series')]
matches[0].text = unicode(val)
return property(fget=fget, fset=fset)
@apply
def book_producer():
def fget(self):
for match in self.bkp_path(self.metadata):
return match.text if match.text else None
return self.get_text(match) or None
def fset(self, val):
matches = self.bkp_path(self.metadata)
if not matches:
matches = [self.create_metadata_element('contributor', ns='dc',
attrib={'{%s}role'%self.NAMESPACES['opf']:'bkp'})]
matches[0].text = unicode(val)
attrib = {'{%s}role'%self.NAMESPACES['opf']: 'bkp'}
matches = [self.create_metadata_element('contributor',
attrib=attrib)]
self.set_text(matches[0], unicode(val))
return property(fget=fget, fset=fset)
@ -783,9 +776,15 @@ class OPF(object):
if matches:
return matches[-1]
def create_metadata_element(self, name, attrib=None, ns='opf'):
elem = etree.SubElement(self.metadata, '{%s}%s'%(self.NAMESPACES[ns], name),
attrib=attrib, nsmap=self.NAMESPACES)
def create_metadata_element(self, name, attrib=None, is_dc=True):
if is_dc:
name = '{%s}%s' % (self.NAMESPACES['dc'], name)
else:
attrib = attrib or {}
attrib['name'] = 'calibre:' + name
name = '{%s}%s' % (self.NAMESPACES['opf'], 'meta')
elem = etree.SubElement(self.metadata, name, attrib=attrib,
nsmap=self.NAMESPACES)
elem.tail = '\n'
return elem

View File

@ -148,10 +148,6 @@ class MobiMLizer(object):
if bstate.pbreak:
etree.SubElement(body, MBP('pagebreak'))
bstate.pbreak = False
if istate.ids:
for id in istate.ids:
etree.SubElement(body, XHTML('a'), attrib={'id': id})
istate.ids.clear()
bstate.istate = None
bstate.anchor = None
parent = bstate.nested[-1] if bstate.nested else bstate.body
@ -186,14 +182,17 @@ class MobiMLizer(object):
wrapper.attrib['height'] = self.mobimlize_measure(vspace)
para.attrib['width'] = self.mobimlize_measure(indent)
elif tag == 'table' and vspace > 0:
body = bstate.body
vspace = int(round(vspace / self.profile.fbase))
index = max((0, len(body) - 1))
while vspace > 0:
body.insert(index, etree.Element(XHTML('br')))
wrapper.addprevious(etree.Element(XHTML('br')))
vspace -= 1
if istate.halign != 'auto':
para.attrib['align'] = istate.halign
if istate.ids:
last = bstate.body[-1]
for id in istate.ids:
last.addprevious(etree.Element(XHTML('a'), attrib={'id': id}))
istate.ids.clear()
pstate = bstate.istate
if tag in CONTENT_TAGS:
bstate.inline = para

View File

@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
Read data from .mobi files
'''
import sys, struct, os, cStringIO, re, atexit, shutil, tempfile
import sys, struct, os, cStringIO, re
try:
from PIL import Image as PILImage
@ -14,7 +14,7 @@ except ImportError:
from lxml import html, etree
from calibre import __appname__, entity_to_unicode
from calibre import entity_to_unicode
from calibre.ebooks import DRMError
from calibre.ebooks.chardet import ENCODING_PATS
from calibre.ebooks.mobi import MobiError
@ -28,7 +28,7 @@ from calibre import sanitize_file_name
class EXTHHeader(object):
def __init__(self, raw, codec):
def __init__(self, raw, codec, title):
self.doctype = raw[:4]
self.length, self.num_items = struct.unpack('>LL', raw[4:12])
raw = raw[12:]
@ -45,22 +45,16 @@ class EXTHHeader(object):
elif id == 203:
self.has_fake_cover = bool(struct.unpack('>L', content)[0])
elif id == 201:
self.cover_offset, = struct.unpack('>L', content)
co, = struct.unpack('>L', content)
if co < 1e7:
self.cover_offset = co
elif id == 202:
self.thumbnail_offset, = struct.unpack('>L', content)
#else:
# print 'unknown record', id, repr(content)
title = re.search(r'\0+([^\0]+)\0+', raw[pos:])
if title:
title = title.group(1).decode(codec, 'replace')
if len(title) > 2:
self.mi.title = title
else:
title = re.search(r'\0+([^\0]+)\0+', ''.join(reversed(raw[pos:])))
if title:
self.mi.title = ''.join(reversed(title.group(1).decode(codec, 'replace')))
self.mi.title = title
def process_metadata(self, id, content, codec):
if id == 100:
if self.mi.authors == [_('Unknown')]:
@ -119,6 +113,9 @@ class BookHeader(object):
if self.compression_type == 'DH':
self.huff_offset, self.huff_number = struct.unpack('>LL', raw[0x70:0x78])
toff, tlen = struct.unpack('>II', raw[0x54:0x5c])
tend = toff + tlen
self.title = raw[toff:tend] if tend < len(raw) else _('Unknown')
langcode = struct.unpack('!L', raw[0x5C:0x60])[0]
langid = langcode & 0xFF
sublangid = (langcode >> 10) & 0xFF
@ -129,7 +126,7 @@ class BookHeader(object):
self.exth_flag, = struct.unpack('>L', raw[0x80:0x84])
self.exth = None
if self.exth_flag & 0x40:
self.exth = EXTHHeader(raw[16+self.length:], self.codec)
self.exth = EXTHHeader(raw[16+self.length:], self.codec, self.title)
self.exth.mi.uid = self.unique_id
self.exth.mi.language = self.language
@ -480,7 +477,7 @@ def get_metadata(stream):
try:
if hasattr(mr.book_header.exth, 'cover_offset'):
cover_index = mr.book_header.first_image_index + mr.book_header.exth.cover_offset
data = mr.sections[cover_index][0]
data = mr.sections[int(cover_index)][0]
else:
data = mr.sections[mr.book_header.first_image_index][0]
buf = cStringIO.StringIO(data)

View File

@ -23,6 +23,7 @@ from PIL import Image
from calibre.ebooks.oeb.base import XML_NS, XHTML, XHTML_NS, OEB_DOCS, \
OEB_RASTER_IMAGES
from calibre.ebooks.oeb.base import xpath, barename, namespace, prefixname
from calibre.ebooks.oeb.base import urlnormalize
from calibre.ebooks.oeb.base import Logger, OEBBook
from calibre.ebooks.oeb.profile import Context
from calibre.ebooks.oeb.transforms.flatcss import CSSFlattener
@ -178,7 +179,7 @@ class Serializer(object):
def serialize_href(self, href, base=None):
hrefs = self.oeb.manifest.hrefs
path, frag = urldefrag(href)
path, frag = urldefrag(urlnormalize(href))
if path and base:
path = base.abshref(path)
if path and path not in hrefs:
@ -196,6 +197,7 @@ class Serializer(object):
def serialize_body(self):
buffer = self.buffer
self.anchor_offset = buffer.tell()
buffer.write('<body>')
# CybookG3 'Start Reading' link
if 'text' in self.oeb.guide:
@ -224,14 +226,17 @@ class Serializer(object):
or namespace(elem.tag) not in nsrmap:
return
tag = prefixname(elem.tag, nsrmap)
for attr in ('name', 'id'):
if attr in elem.attrib:
href = '#'.join((item.href, elem.attrib[attr]))
self.id_offsets[href] = buffer.tell()
del elem.attrib[attr]
if tag == 'a' and not elem.attrib \
and not len(elem) and not elem.text:
# Previous layers take care of @name
id = elem.attrib.pop('id', None)
if id is not None:
href = '#'.join((item.href, id))
offset = self.anchor_offset or buffer.tell()
self.id_offsets[href] = offset
if self.anchor_offset is not None and \
tag == 'a' and not elem.attrib and \
not len(elem) and not elem.text:
return
self.anchor_offset = buffer.tell()
buffer.write('<')
buffer.write(tag)
if elem.attrib:
@ -256,10 +261,12 @@ class Serializer(object):
if elem.text or len(elem) > 0:
buffer.write('>')
if elem.text:
self.anchor_offset = None
self.serialize_text(elem.text)
for child in elem:
self.serialize_elem(child, item)
if child.tail:
self.anchor_offset = None
self.serialize_text(child.tail)
buffer.write('</%s>' % tag)
else:

View File

@ -23,12 +23,15 @@ from calibre import LoggingInterface
from calibre.translations.dynamic import translate
from calibre.startup import get_lang
from calibre.ebooks.oeb.entitydefs import ENTITYDEFS
from calibre.ebooks.metadata.epub import CoverRenderer
from calibre.ptempfile import TemporaryDirectory
XML_NS = 'http://www.w3.org/XML/1998/namespace'
XHTML_NS = 'http://www.w3.org/1999/xhtml'
OEB_DOC_NS = 'http://openebook.org/namespaces/oeb-document/1.0/'
OPF1_NS = 'http://openebook.org/namespaces/oeb-package/1.0/'
OPF2_NS = 'http://www.idpf.org/2007/opf'
OPF_NSES = set([OPF1_NS, OPF2_NS])
DC09_NS = 'http://purl.org/metadata/dublin_core'
DC10_NS = 'http://purl.org/dc/elements/1.0/'
DC11_NS = 'http://purl.org/dc/elements/1.1/'
@ -38,6 +41,7 @@ DCTERMS_NS = 'http://purl.org/dc/terms/'
NCX_NS = 'http://www.daisy.org/z3986/2005/ncx/'
SVG_NS = 'http://www.w3.org/2000/svg'
XLINK_NS = 'http://www.w3.org/1999/xlink'
CALIBRE_NS = 'http://calibre.kovidgoyal.net/2009/metadata'
XPNSMAP = {'h': XHTML_NS, 'o1': OPF1_NS, 'o2': OPF2_NS,
'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS,
'xsi': XSI_NS, 'dt': DCTERMS_NS, 'ncx': NCX_NS,
@ -51,6 +55,7 @@ def DC(name): return '{%s}%s' % (DC11_NS, name)
def NCX(name): return '{%s}%s' % (NCX_NS, name)
def SVG(name): return '{%s}%s' % (SVG_NS, name)
def XLINK(name): return '{%s}%s' % (XLINK_NS, name)
def CALIBRE(name): return '{%s}%s' % (CALIBRE_NS, name)
EPUB_MIME = 'application/epub+zip'
XHTML_MIME = 'application/xhtml+xml'
@ -75,6 +80,8 @@ MS_COVER_TYPE = 'other.ms-coverimage-standard'
ENTITY_RE = re.compile(r'&([a-zA-Z_:][a-zA-Z0-9.-_:]+);')
COLLAPSE_RE = re.compile(r'[ \t\r\n\v]+')
QNAME_RE = re.compile(r'^[{][^{}]+[}][^{}]+$')
PREFIXNAME_RE = re.compile(r'^[^:]+[:][^:]+')
def element(parent, *args, **kwargs):
if parent is not None:
@ -92,11 +99,30 @@ def barename(name):
return name
def prefixname(name, nsrmap):
prefix = nsrmap[namespace(name)]
if not isqname(name):
return name
ns = namespace(name)
if ns not in nsrmap:
return name
prefix = nsrmap[ns]
if not prefix:
return barename(name)
return ':'.join((prefix, barename(name)))
def isprefixname(name):
return name and PREFIXNAME_RE.match(name) is not None
def qname(name, nsmap):
if not isprefixname(name):
return name
prefix, local = name.split(':', 1)
if prefix not in nsmap:
return name
return '{%s}%s' % (nsmap[prefix], local)
def isqname(name):
return name and QNAME_RE.match(name) is not None
def XPath(expr):
return etree.XPath(expr, namespaces=XPNSMAP)
@ -185,48 +211,65 @@ class DirWriter(object):
class Metadata(object):
TERMS = set(['contributor', 'coverage', 'creator', 'date', 'description',
'format', 'identifier', 'language', 'publisher', 'relation',
'rights', 'source', 'subject', 'title', 'type'])
ATTRS = set(['role', 'file-as', 'scheme'])
DC_TERMS = set(['contributor', 'coverage', 'creator', 'date', 'description',
'format', 'identifier', 'language', 'publisher', 'relation',
'rights', 'source', 'subject', 'title', 'type'])
CALIBRE_TERMS = set(['series', 'series_index', 'rating'])
OPF_ATTRS = set(['role', 'file-as', 'scheme', 'event'])
OPF1_NSMAP = {'dc': DC11_NS, 'oebpackage': OPF1_NS}
OPF2_NSMAP = {'opf': OPF2_NS, 'dc': DC11_NS, 'dcterms': DCTERMS_NS,
'xsi': XSI_NS}
'xsi': XSI_NS, 'calibre': CALIBRE_NS}
class Item(object):
def __init__(self, term, value, fq_attrib={}, **kwargs):
self.fq_attrib = fq_attrib = dict(fq_attrib)
fq_attrib.update(kwargs)
if barename(term).lower() in Metadata.TERMS and \
(not namespace(term) or namespace(term) in DC_NSES):
# Anything looking like Dublin Core is coerced
term = DC(barename(term).lower())
elif namespace(term) == OPF2_NS:
def __init__(self, term, value, attrib={}, nsmap={}, **kwargs):
self.attrib = attrib = dict(attrib)
self.nsmap = nsmap = dict(nsmap)
attrib.update(kwargs)
if namespace(term) == OPF2_NS:
term = barename(term)
ns = namespace(term)
local = barename(term).lower()
if local in Metadata.DC_TERMS and (not ns or ns in DC_NSES):
# Anything looking like Dublin Core is coerced
term = DC(local)
elif local in Metadata.CALIBRE_TERMS and ns in (CALIBRE_NS, ''):
# Ditto for Calibre-specific metadata
term = CALIBRE(local)
self.term = term
self.value = value
self.attrib = attrib = {}
for fq_attr in fq_attrib:
if fq_attr in Metadata.ATTRS:
attr = fq_attr
fq_attr = OPF(fq_attr)
fq_attrib[fq_attr] = fq_attrib.pop(attr)
else:
attr = barename(fq_attr)
attrib[attr] = fq_attrib[fq_attr]
for attr, value in attrib.items():
if isprefixname(value):
attrib[attr] = qname(value, nsmap)
if attr in Metadata.OPF_ATTRS:
attrib[OPF(attr)] = attrib.pop(attr)
self.__setattr__ = self._setattr
def __getattr__(self, name):
name = name.replace('_', '-')
attr = name.replace('_', '-')
if attr in Metadata.OPF_ATTRS:
attr = OPF(attr)
try:
return self.attrib[name]
return self.attrib[attr]
except KeyError:
raise AttributeError(
'%r object has no attribute %r' \
% (self.__class__.__name__, name))
def _setattr(self, name, value):
attr = name.replace('_', '-')
if attr in Metadata.OPF_ATTRS:
attr = OPF(attr)
if attr in self.attrib:
self.attrib[attr] = value
return
super(Item, self).__setattr__(self, name, value)
def __getitem__(self, key):
return self.attrib[key]
def __setitem__(self, key, value):
self.attrib[key] = value
def __contains__(self, key):
return key in self.attrib
@ -243,33 +286,41 @@ class Metadata(object):
def __unicode__(self):
return unicode(self.value)
def to_opf1(self, dcmeta=None, xmeta=None):
def to_opf1(self, dcmeta=None, xmeta=None, nsrmap={}):
attrib = {}
for key, value in self.attrib.items():
if namespace(key) == OPF2_NS:
key = barename(key)
attrib[key] = prefixname(value, nsrmap)
if namespace(self.term) == DC11_NS:
name = DC(barename(self.term).title())
elem = element(dcmeta, name, attrib=self.attrib)
elem = element(dcmeta, name, attrib=attrib)
elem.text = self.value
else:
elem = element(xmeta, 'meta', attrib=self.attrib)
elem.attrib['name'] = self.term
elem.attrib['content'] = self.value
elem = element(xmeta, 'meta', attrib=attrib)
elem.attrib['name'] = prefixname(self.term, nsrmap)
elem.attrib['content'] = prefixname(self.value, nsrmap)
return elem
def to_opf2(self, parent=None):
def to_opf2(self, parent=None, nsrmap={}):
attrib = {}
for key, value in self.attrib.items():
attrib[key] = prefixname(value, nsrmap)
if namespace(self.term) == DC11_NS:
elem = element(parent, self.term, attrib=self.fq_attrib)
elem = element(parent, self.term, attrib=attrib)
elem.text = self.value
else:
elem = element(parent, OPF('meta'), attrib=self.fq_attrib)
elem.attrib['name'] = self.term
elem.attrib['content'] = self.value
elem = element(parent, OPF('meta'), attrib=attrib)
elem.attrib['name'] = prefixname(self.term, nsrmap)
elem.attrib['content'] = prefixname(self.value, nsrmap)
return elem
def __init__(self, oeb):
self.oeb = oeb
self.items = defaultdict(list)
def add(self, term, value, attrib={}, **kwargs):
item = self.Item(term, value, attrib, **kwargs)
def add(self, term, value, attrib={}, nsmap={}, **kwargs):
item = self.Item(term, value, attrib, nsmap, **kwargs)
items = self.items[barename(item.term)]
items.append(item)
return item
@ -288,23 +339,55 @@ class Metadata(object):
def __getattr__(self, term):
return self.items[term]
def _nsmap():
def fget(self):
nsmap = {}
for term in self.items:
for item in self.items[term]:
nsmap.update(item.nsmap)
return nsmap
return property(fget=fget)
_nsmap = _nsmap()
def _opf1_nsmap():
def fget(self):
nsmap = self._nsmap
for key, value in nsmap.items():
if value in OPF_NSES or value in DC_NSES:
del nsmap[key]
return nsmap
return property(fget=fget)
_opf1_nsmap = _opf1_nsmap()
def _opf2_nsmap():
def fget(self):
nsmap = self._nsmap
nsmap.update(self.OPF2_NSMAP)
return nsmap
return property(fget=fget)
_opf2_nsmap = _opf2_nsmap()
def to_opf1(self, parent=None):
elem = element(parent, 'metadata')
nsmap = self._opf1_nsmap
nsrmap = dict((value, key) for key, value in nsmap.items())
elem = element(parent, 'metadata', nsmap=nsmap)
dcmeta = element(elem, 'dc-metadata', nsmap=self.OPF1_NSMAP)
xmeta = element(elem, 'x-metadata')
for term in self.items:
for item in self.items[term]:
item.to_opf1(dcmeta, xmeta)
item.to_opf1(dcmeta, xmeta, nsrmap=nsrmap)
if 'ms-chaptertour' not in self.items:
chaptertour = self.Item('ms-chaptertour', 'chaptertour')
chaptertour.to_opf1(dcmeta, xmeta)
chaptertour.to_opf1(dcmeta, xmeta, nsrmap=nsrmap)
return elem
def to_opf2(self, parent=None):
elem = element(parent, OPF('metadata'), nsmap=self.OPF2_NSMAP)
nsmap = self._opf2_nsmap
nsrmap = dict((value, key) for key, value in nsmap.items())
elem = element(parent, OPF('metadata'), nsmap=nsmap)
for term in self.items:
for item in self.items[term]:
item.to_opf2(elem)
item.to_opf2(elem, nsrmap=nsrmap)
return elem
@ -351,9 +434,13 @@ class Manifest(object):
try:
data = etree.fromstring(data)
except etree.XMLSyntaxError:
# TODO: Factor out HTML->XML coercion
self.oeb.logger.warn('Parsing file %r as HTML' % self.href)
data = html.fromstring(data)
data.attrib.pop('xmlns', None)
for elem in data.iter(tag=etree.Comment):
if elem.text:
elem.text = elem.text.strip('-')
data = etree.tostring(data, encoding=unicode)
data = etree.fromstring(data)
# Force into the XHTML namespace
@ -447,7 +534,7 @@ class Manifest(object):
return cmp(skey, okey)
def relhref(self, href):
if '/' not in self.href:
if '/' not in self.href or ':' in href:
return href
base = os.path.dirname(self.href).split('/')
target, frag = urldefrag(href)
@ -463,7 +550,7 @@ class Manifest(object):
return relhref
def abshref(self, href):
if '/' not in self.href:
if '/' not in self.href or ':' in href:
return href
dirname = os.path.dirname(self.href)
href = os.path.join(dirname, href)
@ -546,7 +633,7 @@ class Manifest(object):
elif media_type in OEB_STYLES:
media_type = CSS_MIME
attrib = {'id': item.id, 'href': item.href,
'media-type': item.media_type}
'media-type': media_type}
if item.fallback:
attrib['fallback'] = item.fallback
element(elem, OPF('item'), attrib=attrib)
@ -796,6 +883,9 @@ class TOC(object):
class OEBBook(object):
COVER_SVG_XP = XPath('h:body//svg:svg[position() = 1]')
COVER_OBJECT_XP = XPath('h:body//h:object[@data][position() = 1]')
def __init__(self, opfpath=None, container=None, encoding=None,
logger=FauxLogger()):
if opfpath and not container:
@ -809,27 +899,27 @@ class OEBBook(object):
self._all_from_opf(opf)
def _clean_opf(self, opf):
for elem in opf.iter():
if isinstance(elem.tag, basestring) \
and namespace(elem.tag) in ('', OPF1_NS):
nsmap = {}
for elem in opf.iter(tag=etree.Element):
nsmap.update(elem.nsmap)
for elem in opf.iter(tag=etree.Element):
if namespace(elem.tag) in ('', OPF1_NS):
elem.tag = OPF(barename(elem.tag))
nsmap.update(Metadata.OPF2_NSMAP)
attrib = dict(opf.attrib)
nroot = etree.Element(OPF('package'),
nsmap={None: OPF2_NS}, attrib=attrib)
metadata = etree.SubElement(nroot, OPF('metadata'),
nsmap={'opf': OPF2_NS, 'dc': DC11_NS,
'xsi': XSI_NS, 'dcterms': DCTERMS_NS})
dc = lambda prefix: xpath(opf, 'o2:metadata//%s:*' % prefix)
for element in chain(*(dc(prefix) for prefix in DC_PREFIXES)):
if not element.text: continue
tag = barename(element.tag).lower()
element.tag = '{%s}%s' % (DC11_NS, tag)
for name in element.attrib:
if name in ('role', 'file-as', 'scheme'):
metadata = etree.SubElement(nroot, OPF('metadata'), nsmap=nsmap)
ignored = (OPF('dc-metadata'), OPF('x-metadata'))
for elem in xpath(opf, 'o2:metadata//*'):
if namespace(elem.tag) in DC_NSES:
tag = barename(elem.tag).lower()
elem.tag = '{%s}%s' % (DC11_NS, tag)
for name in elem.attrib:
if name in ('role', 'file-as', 'scheme', 'event'):
nsname = '{%s}%s' % (OPF2_NS, name)
element.attrib[nsname] = element.attrib[name]
del element.attrib[name]
metadata.append(element)
elem.attrib[nsname] = elem.attrib.pop(name)
metadata.append(elem)
for element in xpath(opf, 'o2:metadata//o2:meta'):
metadata.append(element)
for tag in ('o2:manifest', 'o2:spine', 'o2:tours', 'o2:guide'):
@ -856,18 +946,18 @@ class OEBBook(object):
uid = opf.get('unique-identifier', 'calibre-uuid')
self.uid = None
self.metadata = metadata = Metadata(self)
ignored = (OPF('dc-metadata'), OPF('x-metadata'))
for elem in xpath(opf, '/o2:package/o2:metadata//*'):
if elem.tag in ignored: continue
term = elem.tag
value = elem.text
attrib = dict(elem.attrib)
nsmap = elem.nsmap
if term == OPF('meta'):
term = elem.attrib.pop('name', None)
value = elem.attrib.pop('content', None)
term = qname(attrib.pop('name', None), nsmap)
value = attrib.pop('content', None)
if value:
value = COLLAPSE_RE.sub(' ', value.strip())
if term and (value or elem.attrib):
metadata.add(term, value, elem.attrib)
if term and (value or attrib):
metadata.add(term, value, attrib, nsmap=nsmap)
haveuuid = haveid = False
for ident in metadata.identifier:
if unicode(ident).startswith('urn:uuid:'):
@ -928,7 +1018,7 @@ class OEBBook(object):
spine.add(item, elem.get('linear'))
extras = []
for item in self.manifest.values():
if item.media_type == XHTML_MIME \
if item.media_type in OEB_DOCS \
and item not in spine:
extras.append(item)
extras.sort()
@ -971,7 +1061,7 @@ class OEBBook(object):
ncx = item.data
self.manifest.remove(item)
title = xpath(ncx, 'ncx:docTitle/ncx:text/text()')
title = title[0].strip() if title else unicode(self.metadata.title)
title = title[0].strip() if title else unicode(self.metadata.title[0])
self.toc = toc = TOC(title)
navmaps = xpath(ncx, 'ncx:navMap')
for navmap in navmaps:
@ -1051,42 +1141,59 @@ class OEBBook(object):
if self._toc_from_html(opf): return
self._toc_from_spine(opf)
def _ensure_cover_image(self):
cover = None
def _cover_from_html(self, hcover):
with TemporaryDirectory('_html_cover') as tdir:
writer = DirWriter()
writer.dump(self, tdir)
path = os.path.join(tdir, hcover.href)
renderer = CoverRenderer(path)
data = renderer.image_data
id, href = self.manifest.generate('cover', 'cover.jpeg')
item = self.manifest.add(id, href, JPEG_MIME, data=data)
return item
def _locate_cover_image(self):
if self.metadata.cover:
id = str(self.metadata.cover[0])
item = self.manifest.ids.get(id, None)
if item is not None:
return item
hcover = self.spine[0]
if 'cover' in self.guide:
href = self.guide['cover'].href
item = self.manifest.hrefs[href]
media_type = item.media_type
if media_type in OEB_RASTER_IMAGES:
cover = item
if media_type in OEB_IMAGES:
return item
elif media_type in OEB_DOCS:
hcover = item
html = hcover.data
if cover is not None:
pass
elif self.metadata.cover:
id = str(self.metadata.cover[0])
cover = self.manifest.ids[id]
elif MS_COVER_TYPE in self.guide:
if MS_COVER_TYPE in self.guide:
href = self.guide[MS_COVER_TYPE].href
cover = self.manifest.hrefs[href]
elif xpath(html, '//h:img[position()=1]'):
img = xpath(html, '//h:img[position()=1]')[0]
href = hcover.abshref(img.get('src'))
cover = self.manifest.hrefs[href]
elif xpath(html, '//h:object[position()=1]'):
object = xpath(html, '//h:object[position()=1]')[0]
href = hcover.abshref(object.get('data'))
cover = self.manifest.hrefs[href]
elif xpath(html, '//svg:svg[position()=1]'):
svg = copy.deepcopy(xpath(html, '//svg:svg[position()=1]')[0])
item = self.manifest.hrefs.get(href, None)
if item is not None and item.media_type in OEB_IMAGES:
return item
if self.COVER_SVG_XP(html):
svg = copy.deepcopy(self.COVER_SVG_XP(html)[0])
href = os.path.splitext(hcover.href)[0] + '.svg'
id, href = self.manifest.generate(hcover.id, href)
cover = self.manifest.add(id, href, SVG_MIME, data=svg)
if cover and not self.metadata.cover:
self.metadata.add('cover', cover.id)
item = self.manifest.add(id, href, SVG_MIME, data=svg)
return item
if self.COVER_OBJECT_XP(html):
object = self.COVER_OBJECT_XP(html)[0]
href = hcover.abshref(object.get('data'))
item = self.manifest.hrefs.get(href, None)
if item is not None and item.media_type in OEB_IMAGES:
return item
return self._cover_from_html(hcover)
def _ensure_cover_image(self):
cover = self._locate_cover_image()
if self.metadata.cover:
self.metadata.cover[0].value = cover.id
return
self.metadata.add('cover', cover.id)
def _all_from_opf(self, opf):
self._metadata_from_opf(opf)
self._manifest_from_opf(opf)

View File

@ -265,6 +265,8 @@ class Stylizer(object):
class Style(object):
UNIT_RE = re.compile(r'^(-*[0-9]*[.]?[0-9]*)\s*(%|em|px|mm|cm|in|pt|pc)$')
def __init__(self, element, stylizer):
self._element = element
self._profile = stylizer.profile
@ -319,13 +321,11 @@ class Style(object):
if isinstance(value, (int, long, float)):
return value
try:
if float(value) == 0:
return 0.0
return float(value) * 72.0 / self._profile.dpi
except:
pass
result = value
m = re.search(
r"^(-*[0-9]*\.?[0-9]*)\s*(%|em|px|mm|cm|in|pt|pc)$", value)
m = self.UNIT_RE.match(value)
if m is not None and m.group(1):
value = float(m.group(1))
unit = m.group(2)

View File

@ -23,6 +23,12 @@ from calibre.ebooks.oeb.stylizer import Stylizer
COLLAPSE = re.compile(r'[ \t\r\n\v]+')
STRIPNUM = re.compile(r'[-0-9]+$')
def asfloat(value, default):
if not isinstance(value, (int, long, float)):
value = default
return float(value)
class KeyMapper(object):
def __init__(self, sbase, dbase, dkey):
self.sbase = float(sbase)
@ -179,12 +185,13 @@ class CSSFlattener(object):
if cssdict:
if self.lineh and self.fbase and tag != 'body':
self.clean_edges(cssdict, style, psize)
margin = style['margin-left']
left += margin if isinstance(margin, float) else 0
if (left + style['text-indent']) < 0:
percent = (margin - style['text-indent']) / style['width']
margin = asfloat(style['margin-left'], 0)
indent = asfloat(style['text-indent'], 0)
left += margin
if (left + indent) < 0:
percent = (margin - indent) / style['width']
cssdict['margin-left'] = "%d%%" % (percent * 100)
left -= style['text-indent']
left -= indent
if 'display' in cssdict and cssdict['display'] == 'in-line':
cssdict['display'] = 'inline'
if self.unfloat and 'float' in cssdict \

View File

@ -23,6 +23,7 @@ from PyQt4.QtGui import QApplication
from calibre.ebooks.oeb.base import XHTML_NS, XHTML, SVG_NS, SVG, XLINK
from calibre.ebooks.oeb.base import SVG_MIME, PNG_MIME, JPEG_MIME
from calibre.ebooks.oeb.base import xml2str, xpath, namespace, barename
from calibre.ebooks.oeb.base import urlnormalize
from calibre.ebooks.oeb.stylizer import Stylizer
IMAGE_TAGS = set([XHTML('img'), XHTML('object')])
@ -78,7 +79,7 @@ class SVGRasterizer(object):
svg = item.data
hrefs = self.oeb.manifest.hrefs
for elem in xpath(svg, '//svg:*[@xl:href]'):
href = elem.attrib[XLINK('href')]
href = urlnormalize(elem.attrib[XLINK('href')])
path, frag = urldefrag(href)
if not path:
continue
@ -100,15 +101,15 @@ class SVGRasterizer(object):
def rasterize_item(self, item, stylizer):
html = item.data
hrefs = self.oeb.manifest.hrefs
for elem in xpath(html, '//h:img'):
src = elem.get('src', None)
image = hrefs.get(item.abshref(src), None) if src else None
for elem in xpath(html, '//h:img[@src]'):
src = urlnormalize(elem.attrib['src'])
image = hrefs.get(item.abshref(src), None)
if image and image.media_type == SVG_MIME:
style = stylizer.style(elem)
self.rasterize_external(elem, style, item, image)
for elem in xpath(html, '//h:object[@type="%s"]' % SVG_MIME):
data = elem.get('data', None)
image = hrefs.get(item.abshref(data), None) if data else None
for elem in xpath(html, '//h:object[@type="%s" and @data]' % SVG_MIME):
data = urlnormalize(elem.attrib['data'])
image = hrefs.get(item.abshref(data), None)
if image and image.media_type == SVG_MIME:
style = stylizer.style(elem)
self.rasterize_external(elem, style, item, image)

View File

@ -54,7 +54,7 @@ class ManifestTrimmer(object):
new.add(found)
elif item.media_type == CSS_MIME:
def replacer(uri):
absuri = item.abshref(uri)
absuri = item.abshref(urlnormalize(uri))
if absuri in oeb.manifest.hrefs:
found = oeb.manifest.hrefs[href]
if found not in used:

View File

@ -15,7 +15,8 @@
# #
# #
#########################################################################
import sys, os
import sys, os, shutil
class Copy:
"""Copy each changed file to a directory for debugging purposes"""
__dir = ""
@ -64,25 +65,7 @@ class Copy:
of cp. Otherwise, use a safe python method.
"""
write_file = os.path.join(Copy.__dir,new_file)
platform = sys.platform
if platform[:5] == 'linux':
command = 'cp %(file)s %(write_file)s' % vars()
os.system(command)
else:
read_obj = open(file,'r')
write_obj = open(write_file, 'w')
line = "dummy"
while line:
line = read_obj.read(1000)
write_obj.write(line )
read_obj.close()
write_obj.close()
shutil.copyfile(file, write_file)
def rename(self, source, dest):
read_obj = open(source, 'r')
write_obj = open(dest, 'w')
line = 1
while line:
line = read_obj.readline()
write_obj.write(line)
read_obj.close()
write_obj.close()
shutil.copyfile(source, dest)

View File

@ -252,7 +252,7 @@ class Config(ResizableDialog, Ui_Dialog):
self.source_format = d.format()
def accept(self):
for opt in ('chapter', 'level1_toc', 'level2_toc'):
for opt in ('chapter', 'level1_toc', 'level2_toc', 'level3_toc'):
text = unicode(getattr(self, 'opt_'+opt).text())
if text:
try:

View File

@ -93,7 +93,7 @@
<item>
<widget class="QStackedWidget" name="stack" >
<property name="currentIndex" >
<number>1</number>
<number>0</number>
</property>
<widget class="QWidget" name="metadata_page" >
<layout class="QGridLayout" name="gridLayout_4" >
@ -105,36 +105,6 @@
<string>Book Cover</string>
</property>
<layout class="QGridLayout" name="_2" >
<item row="0" column="0" >
<layout class="QHBoxLayout" name="_3" >
<item>
<widget class="ImageView" name="cover" >
<property name="text" >
<string/>
</property>
<property name="pixmap" >
<pixmap resource="../images.qrc" >:/images/book.svg</pixmap>
</property>
<property name="scaledContents" >
<bool>true</bool>
</property>
<property name="alignment" >
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0" >
<widget class="QCheckBox" name="opt_prefer_metadata_cover" >
<property name="text" >
<string>Use cover from &amp;source file</string>
</property>
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" >
<layout class="QVBoxLayout" name="_4" >
<property name="spacing" >
@ -186,6 +156,36 @@
</item>
</layout>
</item>
<item row="2" column="0" >
<widget class="QCheckBox" name="opt_prefer_metadata_cover" >
<property name="text" >
<string>Use cover from &amp;source file</string>
</property>
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0" >
<layout class="QHBoxLayout" name="_3" >
<item>
<widget class="ImageView" name="cover" >
<property name="text" >
<string/>
</property>
<property name="pixmap" >
<pixmap resource="../images.qrc" >:/images/book.svg</pixmap>
</property>
<property name="scaledContents" >
<bool>true</bool>
</property>
<property name="alignment" >
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
<zorder>opt_prefer_metadata_cover</zorder>
<zorder></zorder>
@ -777,10 +777,10 @@ p, li { white-space: pre-wrap; }
<item row="5" column="1" >
<widget class="QLineEdit" name="opt_level2_toc" />
</item>
<item row="6" column="1" >
<item row="7" column="1" >
<widget class="QLineEdit" name="opt_toc_title" />
</item>
<item row="6" column="0" >
<item row="7" column="0" >
<widget class="QLabel" name="toc_title_label" >
<property name="text" >
<string>&amp;Title for generated TOC</string>
@ -790,6 +790,19 @@ p, li { white-space: pre-wrap; }
</property>
</widget>
</item>
<item row="6" column="1" >
<widget class="QLineEdit" name="opt_level3_toc" />
</item>
<item row="6" column="0" >
<widget class="QLabel" name="label_11" >
<property name="text" >
<string>Level &amp;3 TOC</string>
</property>
<property name="buddy" >
<cstring>opt_level3_toc</cstring>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@ -253,6 +253,7 @@ class LRFSingleDialog(QDialog, Ui_LRFSingleDialog):
state = Qt.Checked if default else Qt.Unchecked
obj.setCheckState(state)
self.gui_headerformat.setDisabled(True)
self.gui_header_separation.setDisabled(True)
self.gui_use_metadata_cover.setCheckState(Qt.Checked)
self.preprocess.addItem('No preprocessing')
for opt in self.PREPROCESS_OPTIONS:

View File

@ -1055,5 +1055,37 @@ p, li { white-space: pre-wrap; }
</hint>
</hints>
</connection>
<connection>
<sender>gui_header</sender>
<signal>toggled(bool)</signal>
<receiver>gui_header_separation</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel" >
<x>235</x>
<y>298</y>
</hint>
<hint type="destinationlabel" >
<x>361</x>
<y>321</y>
</hint>
</hints>
</connection>
<connection>
<sender>gui_header</sender>
<signal>toggled(bool)</signal>
<receiver>gui_headerformat</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel" >
<x>307</x>
<y>300</y>
</hint>
<hint type="destinationlabel" >
<x>363</x>
<y>362</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -8,7 +8,7 @@ Scheduler for automated recipe downloads
'''
import sys, copy, time
from datetime import datetime, timedelta
from datetime import datetime, timedelta, date
from PyQt4.Qt import QDialog, QApplication, QLineEdit, QPalette, SIGNAL, QBrush, \
QColor, QAbstractListModel, Qt, QVariant, QFont, QIcon, \
QFile, QObject, QTimer, QMutex, QMenu, QAction, QTime
@ -289,7 +289,8 @@ class SchedulerDialog(QDialog, Ui_Dialog):
recipe.last_downloaded = datetime.fromordinal(1)
recipes.append(recipe)
if recipe.needs_subscription and not config['recipe_account_info_%s'%recipe.id]:
error_dialog(self, _('Must set account information'), _('This recipe requires a username and password')).exec_()
error_dialog(self, _('Must set account information'),
_('This recipe requires a username and password')).exec_()
self.schedule.setCheckState(Qt.Unchecked)
return
if self.interval_button.isChecked():
@ -350,9 +351,11 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.username.blockSignals(False)
self.password.blockSignals(False)
d = datetime.utcnow() - recipe.last_downloaded
ld = '%.2f'%(d.days + d.seconds/(24.*3600))
def hm(x): return (x-x%3600)//3600, (x%3600 - (x%3600)%60)//60
hours, minutes = hm(d.seconds)
tm = _('%d days, %d hours and %d minutes ago')%(d.days, hours, minutes)
if d < timedelta(days=366):
self.last_downloaded.setText(_('Last downloaded: %s days ago')%ld)
self.last_downloaded.setText(_('Last downloaded')+': '+tm)
else:
self.last_downloaded.setText(_('Last downloaded: never'))
@ -431,7 +434,7 @@ class Scheduler(QObject):
day_matches = day > 6 or day == now.tm_wday
tnow = now.tm_hour*60 + now.tm_min
matches = day_matches and (hour*60+minute) < tnow
if matches and delta >= timedelta(days=1):
if matches and nowt.toordinal() < date.today().toordinal():
needs_downloading.add(recipe)
self.debug('Needs downloading:', needs_downloading)

View File

@ -5,7 +5,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>726</width>
<width>738</width>
<height>575</height>
</rect>
</property>
@ -194,6 +194,9 @@
<property name="text" >
<string/>
</property>
<property name="wordWrap" >
<bool>true</bool>
</property>
</widget>
</item>
<item>

View File

@ -20,4 +20,5 @@ class BookView(QGraphicsView):
def resize_for(self, width, height):
self.preferred_size = QSize(width, height)

View File

@ -80,8 +80,8 @@ class Main(MainWindow, Ui_MainWindow):
QObject.connect(self.search, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), self.find)
self.action_next_page.setShortcuts(QKeySequence.MoveToNextPage)
self.action_previous_page.setShortcuts(QKeySequence.MoveToPreviousPage)
self.action_next_page.setShortcuts([QKeySequence.MoveToNextPage, QKeySequence(Qt.Key_Space)])
self.action_previous_page.setShortcuts([QKeySequence.MoveToPreviousPage, QKeySequence(Qt.Key_Backspace)])
self.action_next_match.setShortcuts(QKeySequence.FindNext)
self.addAction(self.action_next_match)
QObject.connect(self.action_next_page, SIGNAL('triggered(bool)'), self.next)
@ -191,6 +191,7 @@ class Main(MainWindow, Ui_MainWindow):
self.spin_box.setSuffix(' of %d'%(self.document.num_of_pages,))
self.spin_box.updateGeometry()
self.stack.setCurrentIndex(0)
self.graphics_view.setFocus(Qt.OtherFocusReason)
elif self.renderer.exception is not None:
exception = self.renderer.exception
print >>sys.stderr, 'Error rendering document'

View File

@ -35,7 +35,9 @@ class DebugWindow(ConversionErrorDialog):
class MainWindow(QMainWindow):
_menu_bar = None
___menu_bar = None
___menu = None
__actions = []
@classmethod
def create_application_menubar(cls):
@ -43,9 +45,11 @@ class MainWindow(QMainWindow):
menu = QMenu()
for action in cls.get_menubar_actions():
menu.addAction(action)
cls.__actions.append(action)
yield action
mb.addMenu(menu)
cls._menu_bar = mb
cls.___menu_bar = mb
cls.___menu = menu
@classmethod

View File

@ -299,10 +299,10 @@ To learn more about writing advanced recipes using some of the facilities, avail
:ref:`API Documentation <news_recipe>`
Documentation of the ``BasicNewsRecipe`` class and all its important methods and fields.
`BasicNewsRecipe <http://bazaar.launchpad.net/~kovid/calibre/trunk/annotate/kovid%40kovidgoyal.net-20080509231359-le3xf7ynwc6eew90?file_id=1245%40b0dd1a5d-880a-0410-ada5-a57097536bc1%3Alibprs500%252Ftrunk%3Asrc%252Flibprs500%252Fweb%252Ffeeds%252Fnews.py>`_
`BasicNewsRecipe <http://bazaar.launchpad.net/~kovid/calibre/trunk/annotate/head:/src/calibre/web/feeds/news.py>`_
The source code of ``BasicNewsRecipe``
`Built-in recipes <http://bazaar.launchpad.net/~kovid/calibre/trunk/files/kovid%40kovidgoyal.net-20080509231359-le3xf7ynwc6eew90?file_id=1298%40b0dd1a5d-880a-0410-ada5-a57097536bc1%3Alibprs500%252Ftrunk%3Asrc%252Flibprs500%252Fweb%252Ffeeds%252Frecipes>`_
`Built-in recipes <http://bazaar.launchpad.net/~kovid/calibre/trunk/files/head:/src/calibre/web/feeds/recipes/>`_
The source code for the built-in recipes that come with |app|
Migrating old style profiles to recipes

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -32,3 +32,8 @@ class LondonReviewOfBooks(BasicNewsRecipe):
def print_version(self, url):
main, split, rest = url.rpartition('/')
return main + '/print/' + rest
def postprocess_html(self, soup, first_fetch):
for t in soup.findAll(['table', 'tr', 'td']):
t.name = 'div'
return soup

File diff suppressed because it is too large Load Diff

View File

@ -39,15 +39,3 @@ def Algorithm(**args):
def KeyDerivation(**args):
return Element(qname = (MANIFESTNS,'key-derivation'), **args)
if __name__ == "__main__":
import cStringIO
xml=cStringIO.StringIO()
m = Manifest()
f = FileEntry(mediatype="text/xml", fullpath="content.xml")
m.addElement(f)
m.toXml(0,xml)
print xml.getvalue()

View File

@ -17,7 +17,7 @@
#
# Contributor(s):
#
TOOLSVERSION = u"ODFPY/0.8.1dev"
TOOLSVERSION = u"ODFPY/0.8.2dev"
ANIMNS = u"urn:oasis:names:tc:opendocument:xmlns:animation:1.0"
DBNS = u"urn:oasis:names:tc:opendocument:xmlns:database:1.0"
@ -49,7 +49,7 @@ TABLENS = u"urn:oasis:names:tc:opendocument:xmlns:table:1.0"
TEXTNS = u"urn:oasis:names:tc:opendocument:xmlns:text:1.0"
XFORMSNS = u"http://www.w3.org/2002/xforms"
XLINKNS = u"http://www.w3.org/1999/xlink"
XMLNS = "http://www.w3.org/XML/1998/namespace"
XMLNS = u"http://www.w3.org/XML/1998/namespace"
nsdict = {

View File

@ -64,6 +64,12 @@ odmimetypes = {
'application/vnd.oasis.opendocument.text-web': '.oth',
}
class OpaqueObject:
def __init__(self, filename, mediatype, content=None):
self.mediatype = mediatype
self.filename = filename
self.content = content
class OpenDocument:
""" A class to hold the content of an OpenDocument document
Use the xml method to write the XML
@ -76,6 +82,7 @@ class OpenDocument:
def __init__(self, mimetype, add_generator=True):
self.mimetype = mimetype
self.childobjects = []
self._extra = []
self.folder = "" # Always empty for toplevel documents
self.topnode = Document(mimetype=self.mimetype)
self.topnode.ownerDocument = self
@ -303,12 +310,15 @@ class OpenDocument:
else:
self.thumbnail = filecontent
def addObject(self, document):
def addObject(self, document, objectname=None):
""" Add an object. The object must be an OpenDocument class
The return value will be the folder in the zipfile the object is stored in
"""
self.childobjects.append(document)
document.folder = "%s/Object %d" % (self.folder, len(self.childobjects))
if objectname is None:
document.folder = "%s/Object %d" % (self.folder, len(self.childobjects))
else:
document.folder = objectname
return ".%s" % document.folder
def _savePictures(self, object, folder):
@ -348,7 +358,7 @@ class OpenDocument:
else:
if addsuffix:
outputfile = outputfile + odmimetypes.get(self.mimetype,'.xxx')
outputfp = zipfile.ZipFile(outputfile,"w")
outputfp = zipfile.ZipFile(outputfile, "w")
self._zipwrite(outputfp)
outputfp.close()
@ -382,6 +392,14 @@ class OpenDocument:
zi.external_attr = UNIXPERMS
self._z.writestr(zi, self.thumbnail)
# Write any extra files
for op in self._extra:
self.manifest.addElement(manifest.FileEntry(fullpath=op.filename, mediatype=op.mediatype))
zi = zipfile.ZipInfo(op.filename.encode('utf-8'), self._now)
zi.compress_type = zipfile.ZIP_DEFLATED
zi.external_attr = UNIXPERMS
if op.content is not None:
self._z.writestr(zi, op.content)
# Write manifest
zi = zipfile.ZipInfo("META-INF/manifest.xml", self._now)
zi.compress_type = zipfile.ZIP_DEFLATED
@ -528,15 +546,20 @@ def load(odffile):
parser.parse(inpsrc)
del doc._parsing
except KeyError, v: pass
# Add the thumbnail here
# Add the images here
# FIXME: Add subobjects correctly here
for mentry,mvalue in manifest.items():
if mentry[:9] == "Pictures/" and len(mentry) > 9:
doc.addPicture(mvalue['full-path'], mvalue['media-type'], z.read(mentry))
elif mentry == "Thumbnails/thumbnail.png":
doc.addThumbnail(z.read(mentry))
elif mentry in ('settings.xml', 'meta.xml', 'content.xml', 'styles.xml'):
pass
else:
pass # Add the SUN junk here to the struct somewhere
if mvalue['full-path'][-1] == '/':
doc._extra.append(OpaqueObject(mvalue['full-path'], mvalue['media-type'], None))
else:
doc._extra.append(OpaqueObject(mvalue['full-path'], mvalue['media-type'], z.read(mentry)))
# Add the SUN junk here to the struct somewhere
# It is cached data, so it can be out-of-date
z.close()
b = doc.getElementsByType(Body)

View File

@ -231,6 +231,19 @@ class ODFContentParser(xml.sax.saxutils.XMLGenerator):
self._callback_func = callback_func
xml.sax.saxutils.XMLGenerator.__init__(self, out, encoding)
def _qname(self, name):
"""Builds a qualified name from a (ns_url, localname) pair"""
if name[0]:
if name[0] == u'http://www.w3.org/XML/1998/namespace':
return u'xml' + ":" + name[1]
# The name is in a non-empty namespace
prefix = self._current_context[name[0]]
if prefix:
# If it is not the default namespace, prepend the prefix
return prefix + ":" + name[1]
# Return the unqualified name
return name[1]
def startElementNS(self, name, qname, attrs):
if name == (TEXTNS, u'user-field-decl'):
field_name = attrs.get((TEXTNS, u'name'))