Merge oeb2mobi state.
@ -16,6 +16,7 @@ def freeze():
|
|||||||
from calibre.linux import entry_points
|
from calibre.linux import entry_points
|
||||||
from calibre import walk
|
from calibre import walk
|
||||||
from calibre.web.feeds.recipes import recipe_modules
|
from calibre.web.feeds.recipes import recipe_modules
|
||||||
|
from calibre.ebooks.lrf.fonts import FONT_MAP
|
||||||
import calibre
|
import calibre
|
||||||
|
|
||||||
|
|
||||||
@ -37,6 +38,7 @@ def freeze():
|
|||||||
'/usr/lib/libxml2.so.2',
|
'/usr/lib/libxml2.so.2',
|
||||||
'/usr/lib/libxslt.so.1',
|
'/usr/lib/libxslt.so.1',
|
||||||
'/usr/lib/libxslt.so.1',
|
'/usr/lib/libxslt.so.1',
|
||||||
|
'/usr/lib/libexslt.so.0',
|
||||||
'/usr/lib/libMagickWand.so',
|
'/usr/lib/libMagickWand.so',
|
||||||
'/usr/lib/libMagickCore.so',
|
'/usr/lib/libMagickCore.so',
|
||||||
]
|
]
|
||||||
@ -72,6 +74,7 @@ def freeze():
|
|||||||
os.makedirs(DIST_DIR)
|
os.makedirs(DIST_DIR)
|
||||||
|
|
||||||
includes = [x[0] for x in executables.values()]
|
includes = [x[0] for x in executables.values()]
|
||||||
|
includes += ['calibre.ebooks.lrf.fonts.prs500.'+x for x in FONT_MAP.values()]
|
||||||
|
|
||||||
excludes = ['matplotlib', "Tkconstants", "Tkinter", "tcl", "_imagingtk",
|
excludes = ['matplotlib', "Tkconstants", "Tkinter", "tcl", "_imagingtk",
|
||||||
"ImageTk", "FixTk", 'wx', 'PyQt4.QtAssistant', 'PyQt4.QtOpenGL.so',
|
"ImageTk", "FixTk", 'wx', 'PyQt4.QtAssistant', 'PyQt4.QtOpenGL.so',
|
||||||
|
@ -326,7 +326,7 @@ def main():
|
|||||||
'genshi', 'calibre.web.feeds.recipes.*',
|
'genshi', 'calibre.web.feeds.recipes.*',
|
||||||
'calibre.ebooks.lrf.any.*', 'calibre.ebooks.lrf.feeds.*',
|
'calibre.ebooks.lrf.any.*', 'calibre.ebooks.lrf.feeds.*',
|
||||||
'keyword', 'codeop', 'pydoc', 'readline',
|
'keyword', 'codeop', 'pydoc', 'readline',
|
||||||
'BeautifulSoup'
|
'BeautifulSoup', 'calibre.ebooks.lrf.fonts.prs500.*',
|
||||||
],
|
],
|
||||||
'packages' : ['PIL', 'Authorization', 'lxml'],
|
'packages' : ['PIL', 'Authorization', 'lxml'],
|
||||||
'excludes' : ['IPython'],
|
'excludes' : ['IPython'],
|
||||||
|
@ -176,6 +176,7 @@ def main(args=sys.argv):
|
|||||||
'BeautifulSoup', 'pyreadline',
|
'BeautifulSoup', 'pyreadline',
|
||||||
'pydoc', 'IPython.Extensions.*',
|
'pydoc', 'IPython.Extensions.*',
|
||||||
'calibre.web.feeds.recipes.*',
|
'calibre.web.feeds.recipes.*',
|
||||||
|
'calibre.ebooks.lrf.fonts.prs500.*',
|
||||||
'PyQt4.QtWebKit', 'PyQt4.QtNetwork',
|
'PyQt4.QtWebKit', 'PyQt4.QtNetwork',
|
||||||
],
|
],
|
||||||
'packages' : ['PIL', 'lxml', 'cherrypy'],
|
'packages' : ['PIL', 'lxml', 'cherrypy'],
|
||||||
|
9
setup.py
@ -380,12 +380,15 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
class build(_build):
|
class build(_build):
|
||||||
|
|
||||||
sub_commands = \
|
sub_commands = [
|
||||||
[
|
|
||||||
('resources', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
|
('resources', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
|
||||||
('translations', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
|
('translations', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
|
||||||
('gui', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
|
('gui', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
|
||||||
] + _build.sub_commands
|
('build_ext', lambda self: True),
|
||||||
|
('build_py', lambda self: True),
|
||||||
|
('build_clib', _build.has_c_libraries),
|
||||||
|
('build_scripts', _build.has_scripts),
|
||||||
|
]
|
||||||
|
|
||||||
entry_points['console_scripts'].append('calibre_postinstall = calibre.linux:post_install')
|
entry_points['console_scripts'].append('calibre_postinstall = calibre.linux:post_install')
|
||||||
ext_modules = [
|
ext_modules = [
|
||||||
|
@ -13,7 +13,8 @@ from calibre.startup import plugins, winutil, winutilerror
|
|||||||
from calibre.constants import iswindows, isosx, islinux, isfrozen, \
|
from calibre.constants import iswindows, isosx, islinux, isfrozen, \
|
||||||
terminal_controller, preferred_encoding, \
|
terminal_controller, preferred_encoding, \
|
||||||
__appname__, __version__, __author__, \
|
__appname__, __version__, __author__, \
|
||||||
win32event, win32api, winerror, fcntl
|
win32event, win32api, winerror, fcntl, \
|
||||||
|
filesystem_encoding
|
||||||
import mechanize
|
import mechanize
|
||||||
|
|
||||||
mimetypes.add_type('application/epub+zip', '.epub')
|
mimetypes.add_type('application/epub+zip', '.epub')
|
||||||
@ -41,6 +42,28 @@ def osx_version():
|
|||||||
return int(m.group(1)), int(m.group(2)), int(m.group(3))
|
return int(m.group(1)), int(m.group(2)), int(m.group(3))
|
||||||
|
|
||||||
|
|
||||||
|
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+\[\]/]')
|
||||||
|
|
||||||
|
def sanitize_file_name(name, substitute='_', as_unicode=False):
|
||||||
|
'''
|
||||||
|
Sanitize the filename `name`. All invalid characters are replaced by `substitute`.
|
||||||
|
The set of invalid characters is the union of the invalid characters in Windows,
|
||||||
|
OS X and Linux. Also removes leading an trailing whitespace.
|
||||||
|
**WARNING:** This function also replaces path separators, so only pass file names
|
||||||
|
and not full paths to it.
|
||||||
|
*NOTE:* This function always returns byte strings, not unicode objects. The byte strings
|
||||||
|
are encoded in the filesystem encoding of the platform, or UTF-8.
|
||||||
|
'''
|
||||||
|
if isinstance(name, unicode):
|
||||||
|
name = name.encode(filesystem_encoding, 'ignore')
|
||||||
|
one = _filename_sanitize.sub(substitute, name)
|
||||||
|
one = re.sub(r'\s', ' ', one).strip()
|
||||||
|
one = re.sub(r'^\.+$', '_', one)
|
||||||
|
if as_unicode:
|
||||||
|
one = one.decode(filesystem_encoding)
|
||||||
|
return one
|
||||||
|
|
||||||
|
|
||||||
class CommandLineError(Exception):
|
class CommandLineError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -201,13 +224,6 @@ class CurrentDir(object):
|
|||||||
def __exit__(self, *args):
|
def __exit__(self, *args):
|
||||||
os.chdir(self.cwd)
|
os.chdir(self.cwd)
|
||||||
|
|
||||||
def sanitize_file_name(name):
|
|
||||||
'''
|
|
||||||
Remove characters that are illegal in filenames from name.
|
|
||||||
Also remove path separators. All illegal characters are replaced by
|
|
||||||
underscores.
|
|
||||||
'''
|
|
||||||
return re.sub(r'\s', ' ', re.sub(r'[\xae"\'\|\~\:\?\\\/]|^-', '_', name.strip()))
|
|
||||||
|
|
||||||
def detect_ncpus():
|
def detect_ncpus():
|
||||||
"""Detects the number of effective CPUs in the system"""
|
"""Detects the number of effective CPUs in the system"""
|
||||||
@ -366,6 +382,12 @@ def strftime(fmt, t=time.localtime()):
|
|||||||
return plugins['winutil'][0].strftime(fmt, t)
|
return plugins['winutil'][0].strftime(fmt, t)
|
||||||
return time.strftime(fmt, t).decode(preferred_encoding, 'replace')
|
return time.strftime(fmt, t).decode(preferred_encoding, 'replace')
|
||||||
|
|
||||||
|
def my_unichr(num):
|
||||||
|
try:
|
||||||
|
return unichr(num)
|
||||||
|
except ValueError:
|
||||||
|
return u'?'
|
||||||
|
|
||||||
def entity_to_unicode(match, exceptions=[], encoding='cp1252'):
|
def entity_to_unicode(match, exceptions=[], encoding='cp1252'):
|
||||||
'''
|
'''
|
||||||
@param match: A match object such that '&'+match.group(1)';' is the entity.
|
@param match: A match object such that '&'+match.group(1)';' is the entity.
|
||||||
@ -381,7 +403,7 @@ def entity_to_unicode(match, exceptions=[], encoding='cp1252'):
|
|||||||
if ent.startswith(u'#x'):
|
if ent.startswith(u'#x'):
|
||||||
num = int(ent[2:], 16)
|
num = int(ent[2:], 16)
|
||||||
if encoding is None or num > 255:
|
if encoding is None or num > 255:
|
||||||
return unichr(num)
|
return my_unichr(num)
|
||||||
return chr(num).decode(encoding)
|
return chr(num).decode(encoding)
|
||||||
if ent.startswith(u'#'):
|
if ent.startswith(u'#'):
|
||||||
try:
|
try:
|
||||||
@ -389,13 +411,13 @@ def entity_to_unicode(match, exceptions=[], encoding='cp1252'):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return '&'+ent+';'
|
return '&'+ent+';'
|
||||||
if encoding is None or num > 255:
|
if encoding is None or num > 255:
|
||||||
return unichr(num)
|
return my_unichr(num)
|
||||||
try:
|
try:
|
||||||
return chr(num).decode(encoding)
|
return chr(num).decode(encoding)
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
return unichr(num)
|
return my_unichr(num)
|
||||||
try:
|
try:
|
||||||
return unichr(name2codepoint[ent])
|
return my_unichr(name2codepoint[ent])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return '&'+ent+';'
|
return '&'+ent+';'
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
__appname__ = 'calibre'
|
__appname__ = 'calibre'
|
||||||
__version__ = '0.4.121'
|
__version__ = '0.4.126'
|
||||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||||
'''
|
'''
|
||||||
Various run time constants.
|
Various run time constants.
|
||||||
@ -29,6 +29,10 @@ winerror = __import__('winerror') if iswindows else None
|
|||||||
win32api = __import__('win32api') if iswindows else None
|
win32api = __import__('win32api') if iswindows else None
|
||||||
fcntl = None if iswindows else __import__('fcntl')
|
fcntl = None if iswindows else __import__('fcntl')
|
||||||
|
|
||||||
|
filesystem_encoding = sys.getfilesystemencoding()
|
||||||
|
if filesystem_encoding is None: filesystem_encoding = 'utf-8'
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
plugins = None
|
plugins = None
|
||||||
if plugins is None:
|
if plugins is None:
|
||||||
|
@ -105,31 +105,44 @@ def reread_metadata_plugins():
|
|||||||
for plugin in _initialized_plugins:
|
for plugin in _initialized_plugins:
|
||||||
if isinstance(plugin, MetadataReaderPlugin):
|
if isinstance(plugin, MetadataReaderPlugin):
|
||||||
for ft in plugin.file_types:
|
for ft in plugin.file_types:
|
||||||
_metadata_readers[ft] = plugin
|
if not _metadata_readers.has_key(ft):
|
||||||
|
_metadata_readers[ft] = []
|
||||||
|
_metadata_readers[ft].append(plugin)
|
||||||
elif isinstance(plugin, MetadataWriterPlugin):
|
elif isinstance(plugin, MetadataWriterPlugin):
|
||||||
for ft in plugin.file_types:
|
for ft in plugin.file_types:
|
||||||
_metadata_writers[ft] = plugin
|
if not _metadata_writers.has_key(ft):
|
||||||
|
_metadata_writers[ft] = []
|
||||||
|
_metadata_writers[ft].append(plugin)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_file_type_metadata(stream, ftype):
|
def get_file_type_metadata(stream, ftype):
|
||||||
mi = MetaInformation(None, None)
|
mi = MetaInformation(None, None)
|
||||||
try:
|
ftype = ftype.lower().strip()
|
||||||
plugin = _metadata_readers[ftype.lower().strip()]
|
if _metadata_readers.has_key(ftype):
|
||||||
|
for plugin in _metadata_readers[ftype]:
|
||||||
if not is_disabled(plugin):
|
if not is_disabled(plugin):
|
||||||
with plugin:
|
with plugin:
|
||||||
|
try:
|
||||||
mi = plugin.get_metadata(stream, ftype.lower().strip())
|
mi = plugin.get_metadata(stream, ftype.lower().strip())
|
||||||
|
break
|
||||||
except:
|
except:
|
||||||
pass
|
continue
|
||||||
return mi
|
return mi
|
||||||
|
|
||||||
def set_file_type_metadata(stream, mi, ftype):
|
def set_file_type_metadata(stream, mi, ftype):
|
||||||
try:
|
ftype = ftype.lower().strip()
|
||||||
plugin = _metadata_writers[ftype.lower().strip()]
|
if _metadata_writers.has_key(ftype):
|
||||||
|
for plugin in _metadata_writers[ftype]:
|
||||||
if not is_disabled(plugin):
|
if not is_disabled(plugin):
|
||||||
with plugin:
|
with plugin:
|
||||||
|
try:
|
||||||
plugin.set_metadata(stream, mi, ftype.lower().strip())
|
plugin.set_metadata(stream, mi, ftype.lower().strip())
|
||||||
|
break
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
def _run_filetype_plugins(path_to_file, ft=None, occasion='preprocess'):
|
def _run_filetype_plugins(path_to_file, ft=None, occasion='preprocess'):
|
||||||
occasion = {'import':_on_import, 'preprocess':_on_preprocess,
|
occasion = {'import':_on_import, 'preprocess':_on_preprocess,
|
||||||
'postprocess':_on_postprocess}[occasion]
|
'postprocess':_on_postprocess}[occasion]
|
||||||
@ -185,6 +198,20 @@ def add_plugin(path_to_zip_file):
|
|||||||
initialize_plugins()
|
initialize_plugins()
|
||||||
return plugin
|
return plugin
|
||||||
|
|
||||||
|
def remove_plugin(plugin_or_name):
|
||||||
|
name = getattr(plugin_or_name, 'name', plugin_or_name)
|
||||||
|
plugins = config['plugins']
|
||||||
|
removed = False
|
||||||
|
if name in plugins.keys():
|
||||||
|
removed = True
|
||||||
|
zfp = plugins[name]
|
||||||
|
if os.path.exists(zfp):
|
||||||
|
os.remove(zfp)
|
||||||
|
plugins.pop(name)
|
||||||
|
config['plugins'] = plugins
|
||||||
|
initialize_plugins()
|
||||||
|
return removed
|
||||||
|
|
||||||
def is_disabled(plugin):
|
def is_disabled(plugin):
|
||||||
return plugin.name in config['disabled_plugins']
|
return plugin.name in config['disabled_plugins']
|
||||||
|
|
||||||
@ -237,6 +264,8 @@ def option_parser():
|
|||||||
'''))
|
'''))
|
||||||
parser.add_option('-a', '--add-plugin', default=None,
|
parser.add_option('-a', '--add-plugin', default=None,
|
||||||
help=_('Add a plugin by specifying the path to the zip file containing it.'))
|
help=_('Add a plugin by specifying the path to the zip file containing it.'))
|
||||||
|
parser.add_option('-r', '--remove-plugin', default=None,
|
||||||
|
help=_('Remove a custom plugin by name. Has no effect on builtin plugins'))
|
||||||
parser.add_option('--customize-plugin', default=None,
|
parser.add_option('--customize-plugin', default=None,
|
||||||
help=_('Customize plugin. Specify name of plugin and customization string separated by a comma.'))
|
help=_('Customize plugin. Specify name of plugin and customization string separated by a comma.'))
|
||||||
parser.add_option('-l', '--list-plugins', default=False, action='store_true',
|
parser.add_option('-l', '--list-plugins', default=False, action='store_true',
|
||||||
@ -267,6 +296,11 @@ def main(args=sys.argv):
|
|||||||
if opts.add_plugin is not None:
|
if opts.add_plugin is not None:
|
||||||
plugin = add_plugin(opts.add_plugin)
|
plugin = add_plugin(opts.add_plugin)
|
||||||
print 'Plugin added:', plugin.name, plugin.version
|
print 'Plugin added:', plugin.name, plugin.version
|
||||||
|
if opts.remove_plugin is not None:
|
||||||
|
if remove_plugin(opts.remove_plugin):
|
||||||
|
print 'Plugin removed'
|
||||||
|
else:
|
||||||
|
print 'No custom pluginnamed', opts.remove_plugin
|
||||||
if opts.customize_plugin is not None:
|
if opts.customize_plugin is not None:
|
||||||
name, custom = opts.customize_plugin.split(',')
|
name, custom = opts.customize_plugin.split(',')
|
||||||
plugin = find_plugin(name.strip())
|
plugin = find_plugin(name.strip())
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Device drivers.
|
Device drivers.
|
||||||
'''
|
'''
|
||||||
@ -8,8 +9,9 @@ def devices():
|
|||||||
from calibre.devices.prs500.driver import PRS500
|
from calibre.devices.prs500.driver import PRS500
|
||||||
from calibre.devices.prs505.driver import PRS505
|
from calibre.devices.prs505.driver import PRS505
|
||||||
from calibre.devices.prs700.driver import PRS700
|
from calibre.devices.prs700.driver import PRS700
|
||||||
|
from calibre.devices.cybookg3.driver import CYBOOKG3
|
||||||
#from calibre.devices.kindle.driver import KINDLE
|
#from calibre.devices.kindle.driver import KINDLE
|
||||||
return (PRS500, PRS505, PRS700)
|
return (PRS500, PRS505, PRS700, CYBOOKG3)
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
0
src/calibre/devices/cybookg3/__init__.py
Normal file
85
src/calibre/devices/cybookg3/books.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2009, John Schember <john at nachtimwald.com>'
|
||||||
|
|
||||||
|
'''
|
||||||
|
'''
|
||||||
|
import os, fnmatch, time
|
||||||
|
|
||||||
|
from calibre.devices.interface import BookList as _BookList
|
||||||
|
|
||||||
|
EBOOK_DIR = "eBooks"
|
||||||
|
EBOOK_TYPES = ['mobi', 'prc', 'html', 'pdf', 'rtf', 'txt']
|
||||||
|
|
||||||
|
class Book(object):
|
||||||
|
def __init__(self, path, title, authors):
|
||||||
|
self.title = title
|
||||||
|
self.authors = authors
|
||||||
|
self.size = os.path.getsize(path)
|
||||||
|
self.datetime = time.gmtime(os.path.getctime(path))
|
||||||
|
self.path = path
|
||||||
|
self.thumbnail = None
|
||||||
|
self.tags = []
|
||||||
|
|
||||||
|
@apply
|
||||||
|
def title_sorter():
|
||||||
|
doc = '''String to sort the title. If absent, title is returned'''
|
||||||
|
def fget(self):
|
||||||
|
return re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', self.title).rstrip()
|
||||||
|
return property(doc=doc, fget=fget)
|
||||||
|
|
||||||
|
@apply
|
||||||
|
def thumbnail():
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
""" Return a utf-8 encoded string with title author and path information """
|
||||||
|
return self.title.encode('utf-8') + " by " + \
|
||||||
|
self.authors.encode('utf-8') + " at " + self.path.encode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
class BookList(_BookList):
|
||||||
|
def __init__(self, mountpath):
|
||||||
|
self._mountpath = mountpath
|
||||||
|
_BookList.__init__(self)
|
||||||
|
self.return_books(mountpath)
|
||||||
|
|
||||||
|
def return_books(self, mountpath):
|
||||||
|
# Get all books in all directories under the root EBOOK_DIR directory
|
||||||
|
for path, dirs, files in os.walk(os.path.join(mountpath, EBOOK_DIR)):
|
||||||
|
# Filter out anything that isn't in the list of supported ebook types
|
||||||
|
for book_type in EBOOK_TYPES:
|
||||||
|
for filename in fnmatch.filter(files, '*.%s' % (book_type)):
|
||||||
|
book_title = ''
|
||||||
|
book_author = ''
|
||||||
|
# Calibre uses a specific format for file names. They take the form
|
||||||
|
# title_-_author_number.extention We want to see if the file name is
|
||||||
|
# in this format.
|
||||||
|
if fnmatch.fnmatchcase(filename, '*_-_*.*'):
|
||||||
|
# Get the title and author from the file name
|
||||||
|
title, sep, author = filename.rpartition('_-_')
|
||||||
|
author, sep, ext = author.rpartition('_')
|
||||||
|
book_title = title.replace('_', ' ')
|
||||||
|
book_author = author.replace('_', ' ')
|
||||||
|
# if the filename did not match just set the title to
|
||||||
|
# the filename without the extension
|
||||||
|
else:
|
||||||
|
book_title = os.path.splitext(filename)[0].replace('_', ' ')
|
||||||
|
|
||||||
|
self.append(Book(os.path.join(path, filename), book_title, book_author))
|
||||||
|
|
||||||
|
def add_book(self, path, title):
|
||||||
|
self.append(Book(path, title, ""))
|
||||||
|
|
||||||
|
def remove_book(self, path):
|
||||||
|
for book in self:
|
||||||
|
if path.endswith(book.path):
|
||||||
|
self.remove(book)
|
||||||
|
break
|
||||||
|
|
||||||
|
def supports_tags(self):
|
||||||
|
''' Return True if the the device supports tags (collections) for this book list. '''
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_tags(self, book, tags):
|
||||||
|
pass
|
||||||
|
|
332
src/calibre/devices/cybookg3/driver.py
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2009, John Schember <john at nachtimwald.com>'
|
||||||
|
|
||||||
|
'''
|
||||||
|
Device driver for Bookeen's Cybook Gen 3
|
||||||
|
'''
|
||||||
|
import os, fnmatch, shutil, time
|
||||||
|
from itertools import cycle
|
||||||
|
|
||||||
|
from calibre.devices.interface import Device
|
||||||
|
from calibre.devices.errors import DeviceError, FreeSpaceError
|
||||||
|
|
||||||
|
from calibre.devices.cybookg3.books import BookList, EBOOK_DIR, EBOOK_TYPES
|
||||||
|
from calibre import iswindows, islinux, isosx, __appname__
|
||||||
|
|
||||||
|
class CYBOOKG3(Device):
|
||||||
|
# Ordered list of supported formats
|
||||||
|
FORMATS = EBOOK_TYPES
|
||||||
|
VENDOR_ID = 0x0bda
|
||||||
|
PRODUCT_ID = 0x0703
|
||||||
|
BCD = 0x110
|
||||||
|
|
||||||
|
VENDOR_NAME = 'BOOKEEN'
|
||||||
|
PRODUCT_NAME = 'CYBOOK_GEN3'
|
||||||
|
|
||||||
|
MAIN_MEMORY_VOLUME_LABEL = 'Cybook Gen 3 Main Memory'
|
||||||
|
STORAGE_CARD_VOLUME_LABEL = 'Cybook Gen 3 Storage Card'
|
||||||
|
|
||||||
|
FDI_TEMPLATE = \
|
||||||
|
'''
|
||||||
|
<device>
|
||||||
|
<match key="info.category" string="volume">
|
||||||
|
<match key="@info.parent:@info.parent:@info.parent:@info.parent:usb.vendor_id" int="%(vendor_id)s">
|
||||||
|
<match key="@info.parent:@info.parent:@info.parent:@info.parent:usb.product_id" int="%(product_id)s">
|
||||||
|
<match key="@info.parent:@info.parent:@info.parent:@info.parent:usb.device_revision_bcd" int="%(bcd)s">
|
||||||
|
<match key="volume.is_partition" bool="false">
|
||||||
|
<merge key="volume.label" type="string">%(main_memory)s</merge>
|
||||||
|
<merge key="%(app)s.mainvolume" type="string">%(deviceclass)s</merge>
|
||||||
|
</match>
|
||||||
|
</match>
|
||||||
|
</match>
|
||||||
|
</match>
|
||||||
|
</match>
|
||||||
|
</device>
|
||||||
|
<device>
|
||||||
|
<match key="info.category" string="volume">
|
||||||
|
<match key="@info.parent:@info.parent:@info.parent:@info.parent:usb.vendor_id" int="%(vendor_id)s">
|
||||||
|
<match key="@info.parent:@info.parent:@info.parent:@info.parent:usb.product_id" int="%(product_id)s">
|
||||||
|
<match key="@info.parent:@info.parent:@info.parent:@info.parent:usb.device_revision_bcd" int="%(bcd)s">
|
||||||
|
<match key="volume.is_partition" bool="true">
|
||||||
|
<merge key="volume.label" type="string">%(storage_card)s</merge>
|
||||||
|
<merge key="%(app)s.cardvolume" type="string">%(deviceclass)s</merge>
|
||||||
|
</match>
|
||||||
|
</match>
|
||||||
|
</match>
|
||||||
|
</match>
|
||||||
|
</match>
|
||||||
|
</device>
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, key='-1', log_packets=False, report_progress=None) :
|
||||||
|
self._main_prefix = self._card_prefix = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_fdi(cls):
|
||||||
|
return cls.FDI_TEMPLATE%dict(
|
||||||
|
app=__appname__,
|
||||||
|
deviceclass=cls.__name__,
|
||||||
|
vendor_id=hex(cls.VENDOR_ID),
|
||||||
|
product_id=hex(cls.PRODUCT_ID),
|
||||||
|
bcd=hex(cls.BCD),
|
||||||
|
main_memory=cls.MAIN_MEMORY_VOLUME_LABEL,
|
||||||
|
storage_card=cls.STORAGE_CARD_VOLUME_LABEL,
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_progress_reporter(self, report_progress):
|
||||||
|
self.report_progress = report_progress
|
||||||
|
|
||||||
|
def get_device_information(self, end_session=True):
|
||||||
|
"""
|
||||||
|
Ask device for device information. See L{DeviceInfoQuery}.
|
||||||
|
@return: (device name, device version, software version on device, mime type)
|
||||||
|
"""
|
||||||
|
return (self.__class__.__name__, '', '', '')
|
||||||
|
|
||||||
|
def card_prefix(self, end_session=True):
|
||||||
|
return self._card_prefix
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _windows_space(cls, prefix):
|
||||||
|
if prefix is None:
|
||||||
|
return 0, 0
|
||||||
|
win32file = __import__('win32file', globals(), locals(), [], -1)
|
||||||
|
try:
|
||||||
|
sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \
|
||||||
|
win32file.GetDiskFreeSpace(prefix[:-1])
|
||||||
|
except Exception, err:
|
||||||
|
if getattr(err, 'args', [None])[0] == 21: # Disk not ready
|
||||||
|
time.sleep(3)
|
||||||
|
sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \
|
||||||
|
win32file.GetDiskFreeSpace(prefix[:-1])
|
||||||
|
else: raise
|
||||||
|
mult = sectors_per_cluster * bytes_per_sector
|
||||||
|
return total_clusters * mult, free_clusters * mult
|
||||||
|
|
||||||
|
def total_space(self, end_session=True):
|
||||||
|
msz = csz = 0
|
||||||
|
print self._main_prefix
|
||||||
|
if not iswindows:
|
||||||
|
if self._main_prefix is not None:
|
||||||
|
stats = os.statvfs(self._main_prefix)
|
||||||
|
msz = stats.f_frsize * (stats.f_blocks + stats.f_bavail - stats.f_bfree)
|
||||||
|
if self._card_prefix is not None:
|
||||||
|
stats = os.statvfs(self._card_prefix)
|
||||||
|
csz = stats.f_frsize * (stats.f_blocks + stats.f_bavail - stats.f_bfree)
|
||||||
|
else:
|
||||||
|
msz = self._windows_space(self._main_prefix)[0]
|
||||||
|
csz = self._windows_space(self._card_prefix)[0]
|
||||||
|
|
||||||
|
return (msz, 0, csz)
|
||||||
|
|
||||||
|
def free_space(self, end_session=True):
|
||||||
|
msz = csz = 0
|
||||||
|
if not iswindows:
|
||||||
|
if self._main_prefix is not None:
|
||||||
|
stats = os.statvfs(self._main_prefix)
|
||||||
|
msz = stats.f_frsize * stats.f_bavail
|
||||||
|
if self._card_prefix is not None:
|
||||||
|
stats = os.statvfs(self._card_prefix)
|
||||||
|
csz = stats.f_frsize * stats.f_bavail
|
||||||
|
else:
|
||||||
|
msz = self._windows_space(self._main_prefix)[1]
|
||||||
|
csz = self._windows_space(self._card_prefix)[1]
|
||||||
|
|
||||||
|
return (msz, 0, csz)
|
||||||
|
|
||||||
|
def books(self, oncard=False, end_session=True):
|
||||||
|
if oncard and self._card_prefix is None:
|
||||||
|
return []
|
||||||
|
prefix = self._card_prefix if oncard else self._main_prefix
|
||||||
|
bl = BookList(prefix)
|
||||||
|
return bl
|
||||||
|
|
||||||
|
def upload_books(self, files, names, on_card=False, end_session=True):
|
||||||
|
if on_card and not self._card_prefix:
|
||||||
|
raise ValueError(_('The reader has no storage card connected.'))
|
||||||
|
|
||||||
|
if not on_card:
|
||||||
|
path = os.path.join(self._main_prefix, EBOOK_DIR)
|
||||||
|
else:
|
||||||
|
path = os.path.join(self._card_prefix, EBOOK_DIR)
|
||||||
|
|
||||||
|
sizes = map(os.path.getsize, files)
|
||||||
|
size = sum(sizes)
|
||||||
|
|
||||||
|
if on_card and size > self.free_space()[2] - 1024*1024:
|
||||||
|
raise FreeSpaceError("There is insufficient free space "+\
|
||||||
|
"on the storage card")
|
||||||
|
if not on_card and size > self.free_space()[0] - 2*1024*1024:
|
||||||
|
raise FreeSpaceError("There is insufficient free space " +\
|
||||||
|
"in main memory")
|
||||||
|
|
||||||
|
paths = []
|
||||||
|
names = iter(names)
|
||||||
|
|
||||||
|
for infile in files:
|
||||||
|
filepath = os.path.join(path, names.next())
|
||||||
|
paths.append(filepath)
|
||||||
|
|
||||||
|
shutil.copy2(infile, filepath)
|
||||||
|
|
||||||
|
return zip(paths, cycle([on_card]))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_books_to_metadata(cls, locations, metadata, booklists):
|
||||||
|
for location in locations:
|
||||||
|
path = location[0]
|
||||||
|
on_card = 1 if location[1] else 0
|
||||||
|
booklists[on_card].add_book(path, os.path.basename(path))
|
||||||
|
|
||||||
|
def delete_books(self, paths, end_session=True):
|
||||||
|
for path in paths:
|
||||||
|
if os.path.exists(path):
|
||||||
|
# Delete the ebook
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
filepath, ext = os.path.splitext(path)
|
||||||
|
basepath, filename = os.path.split(filepath)
|
||||||
|
|
||||||
|
# Delete the ebook auxiliary file
|
||||||
|
if os.path.exists(filepath + '.mbp'):
|
||||||
|
os.unlink(filepath + '.mbp')
|
||||||
|
|
||||||
|
# Delete the thumbnails file auto generated for the ebook
|
||||||
|
for p, d, files in os.walk(basepath):
|
||||||
|
for filen in fnmatch.filter(files, filename + "*.t2b"):
|
||||||
|
os.unlink(os.path.join(p, filen))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def remove_books_from_metadata(cls, paths, booklists):
|
||||||
|
for path in paths:
|
||||||
|
for bl in booklists:
|
||||||
|
bl.remove_book(path)
|
||||||
|
|
||||||
|
def sync_booklists(self, booklists, end_session=True):
|
||||||
|
# There is no meta data on the device to update. The device is treated
|
||||||
|
# as a mass storage device and does not use a meta data xml file like
|
||||||
|
# the Sony Readers.
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_file(self, path, outfile, end_session=True):
|
||||||
|
path = self.munge_path(path)
|
||||||
|
src = open(path, 'rb')
|
||||||
|
shutil.copyfileobj(src, outfile, 10*1024*1024)
|
||||||
|
|
||||||
|
def munge_path(self, path):
|
||||||
|
if path.startswith('/') and not (path.startswith(self._main_prefix) or \
|
||||||
|
(self._card_prefix and path.startswith(self._card_prefix))):
|
||||||
|
path = self._main_prefix + path[1:]
|
||||||
|
elif path.startswith('card:'):
|
||||||
|
path = path.replace('card:', self._card_prefix[:-1])
|
||||||
|
return path
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def windows_match_device(cls, device_id):
|
||||||
|
device_id = device_id.upper()
|
||||||
|
if 'VEN_'+cls.VENDOR_NAME in device_id and \
|
||||||
|
'PROD_'+cls.PRODUCT_NAME in device_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
vid, pid = hex(cls.VENDOR_ID)[2:], hex(cls.PRODUCT_ID)[2:]
|
||||||
|
while len(vid) < 4: vid = '0' + vid
|
||||||
|
while len(pid) < 4: pid = '0' + pid
|
||||||
|
if 'VID_'+vid in device_id and 'PID_'+pid in device_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
# This only supports Windows >= 2000
|
||||||
|
def open_windows(self):
|
||||||
|
drives = []
|
||||||
|
wmi = __import__('wmi', globals(), locals(), [], -1)
|
||||||
|
c = wmi.WMI()
|
||||||
|
for drive in c.Win32_DiskDrive():
|
||||||
|
if self.__class__.windows_match_device(str(drive.PNPDeviceID)):
|
||||||
|
if drive.Partitions == 0:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
partition = drive.associators("Win32_DiskDriveToDiskPartition")[0]
|
||||||
|
logical_disk = partition.associators('Win32_LogicalDiskToPartition')[0]
|
||||||
|
prefix = logical_disk.DeviceID+os.sep
|
||||||
|
drives.append((drive.Index, prefix))
|
||||||
|
except IndexError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not drives:
|
||||||
|
raise DeviceError(_('Unable to detect the %s disk drive. Try rebooting.')%self.__class__.__name__)
|
||||||
|
|
||||||
|
drives.sort(cmp=lambda a, b: cmp(a[0], b[0]))
|
||||||
|
self._main_prefix = drives[0][1]
|
||||||
|
if len(drives) > 1:
|
||||||
|
self._card_prefix = drives[1][1]
|
||||||
|
|
||||||
|
def open_osx(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def open_linux(self):
|
||||||
|
import dbus
|
||||||
|
bus = dbus.SystemBus()
|
||||||
|
hm = dbus.Interface(bus.get_object("org.freedesktop.Hal", "/org/freedesktop/Hal/Manager"), "org.freedesktop.Hal.Manager")
|
||||||
|
|
||||||
|
def conditional_mount(dev):
|
||||||
|
mmo = bus.get_object("org.freedesktop.Hal", dev)
|
||||||
|
label = mmo.GetPropertyString('volume.label', dbus_interface='org.freedesktop.Hal.Device')
|
||||||
|
is_mounted = mmo.GetPropertyString('volume.is_mounted', dbus_interface='org.freedesktop.Hal.Device')
|
||||||
|
mount_point = mmo.GetPropertyString('volume.mount_point', dbus_interface='org.freedesktop.Hal.Device')
|
||||||
|
fstype = mmo.GetPropertyString('volume.fstype', dbus_interface='org.freedesktop.Hal.Device')
|
||||||
|
if is_mounted:
|
||||||
|
return str(mount_point)
|
||||||
|
mmo.Mount(label, fstype, ['umask=077', 'uid='+str(os.getuid()), 'sync'],
|
||||||
|
dbus_interface='org.freedesktop.Hal.Device.Volume')
|
||||||
|
return os.path.normpath('/media/'+label)+'/'
|
||||||
|
|
||||||
|
mm = hm.FindDeviceStringMatch(__appname__+'.mainvolume', self.__class__.__name__)
|
||||||
|
if not mm:
|
||||||
|
raise DeviceError(_('Unable to detect the %s disk drive. Try rebooting.')%(self.__class__.__name__,))
|
||||||
|
self._main_prefix = None
|
||||||
|
for dev in mm:
|
||||||
|
try:
|
||||||
|
self._main_prefix = conditional_mount(dev)+os.sep
|
||||||
|
break
|
||||||
|
except dbus.exceptions.DBusException:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not self._main_prefix:
|
||||||
|
raise DeviceError('Could not open device for reading. Try a reboot.')
|
||||||
|
|
||||||
|
self._card_prefix = None
|
||||||
|
cards = hm.FindDeviceStringMatch(__appname__+'.cardvolume', self.__class__.__name__)
|
||||||
|
|
||||||
|
for dev in cards:
|
||||||
|
try:
|
||||||
|
self._card_prefix = conditional_mount(dev)+os.sep
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
print traceback
|
||||||
|
continue
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
time.sleep(5)
|
||||||
|
self._main_prefix = self._card_prefix = None
|
||||||
|
if islinux:
|
||||||
|
try:
|
||||||
|
self.open_linux()
|
||||||
|
except DeviceError:
|
||||||
|
time.sleep(3)
|
||||||
|
self.open_linux()
|
||||||
|
if iswindows:
|
||||||
|
try:
|
||||||
|
self.open_windows()
|
||||||
|
except DeviceError:
|
||||||
|
time.sleep(3)
|
||||||
|
self.open_windows()
|
||||||
|
if isosx:
|
||||||
|
try:
|
||||||
|
self.open_osx()
|
||||||
|
except DeviceError:
|
||||||
|
time.sleep(3)
|
||||||
|
self.open_osx()
|
||||||
|
|
@ -39,6 +39,18 @@ class Device(object):
|
|||||||
'''Return the FDI description of this device for HAL on linux.'''
|
'''Return the FDI description of this device for HAL on linux.'''
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
'''
|
||||||
|
Perform any device specific initialization. Called after the device is
|
||||||
|
detected but before any other functions that communicate with the device.
|
||||||
|
For example: For devices that present themselves as USB Mass storage
|
||||||
|
devices, this method would be responsible for mounting the device or
|
||||||
|
if the device has been automounted, for finding out where it has been
|
||||||
|
mounted. The driver for the PRS505 has a implementation of this function
|
||||||
|
that should serve as a good example for USB Mass storage devices.
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
def set_progress_reporter(self, report_progress):
|
def set_progress_reporter(self, report_progress):
|
||||||
'''
|
'''
|
||||||
@param report_progress: Function that is called with a % progress
|
@param report_progress: Function that is called with a % progress
|
||||||
|
@ -35,7 +35,7 @@ Conversion of HTML/OPF files follows several stages:
|
|||||||
import os, sys, cStringIO, logging, re, functools, shutil
|
import os, sys, cStringIO, logging, re, functools, shutil
|
||||||
|
|
||||||
from lxml.etree import XPath
|
from lxml.etree import XPath
|
||||||
from lxml import html
|
from lxml import html, etree
|
||||||
from PyQt4.Qt import QApplication, QPixmap
|
from PyQt4.Qt import QApplication, QPixmap
|
||||||
|
|
||||||
from calibre.ebooks.html import Processor, merge_metadata, get_filelist,\
|
from calibre.ebooks.html import Processor, merge_metadata, get_filelist,\
|
||||||
@ -55,13 +55,13 @@ content = functools.partial(os.path.join, u'content')
|
|||||||
|
|
||||||
def remove_bad_link(element, attribute, link, pos):
|
def remove_bad_link(element, attribute, link, pos):
|
||||||
if attribute is not None:
|
if attribute is not None:
|
||||||
if element.tag in ['link', 'img']:
|
if element.tag in ['link']:
|
||||||
element.getparent().remove(element)
|
element.getparent().remove(element)
|
||||||
else:
|
else:
|
||||||
element.set(attribute, '')
|
element.set(attribute, '')
|
||||||
del element.attrib[attribute]
|
del element.attrib[attribute]
|
||||||
|
|
||||||
def check(opf_path, pretty_print):
|
def check_links(opf_path, pretty_print):
|
||||||
'''
|
'''
|
||||||
Find and remove all invalid links in the HTML files
|
Find and remove all invalid links in the HTML files
|
||||||
'''
|
'''
|
||||||
@ -123,6 +123,10 @@ class HTMLProcessor(Processor, Rationalizer):
|
|||||||
if opts.verbose > 2:
|
if opts.verbose > 2:
|
||||||
self.debug_tree('nocss')
|
self.debug_tree('nocss')
|
||||||
|
|
||||||
|
if hasattr(self.body, 'xpath'):
|
||||||
|
for script in list(self.body.xpath('descendant::script')):
|
||||||
|
script.getparent().remove(script)
|
||||||
|
|
||||||
def convert_image(self, img):
|
def convert_image(self, img):
|
||||||
rpath = img.get('src', '')
|
rpath = img.get('src', '')
|
||||||
path = os.path.join(os.path.dirname(self.save_path()), *rpath.split('/'))
|
path = os.path.join(os.path.dirname(self.save_path()), *rpath.split('/'))
|
||||||
@ -280,6 +284,16 @@ def find_oeb_cover(htmlfile):
|
|||||||
if match:
|
if match:
|
||||||
return match.group(1)
|
return match.group(1)
|
||||||
|
|
||||||
|
def condense_ncx(ncx_path):
|
||||||
|
tree = etree.parse(ncx_path)
|
||||||
|
for tag in tree.getroot().iter(tag=etree.Element):
|
||||||
|
if tag.text:
|
||||||
|
tag.text = tag.text.strip()
|
||||||
|
if tag.tail:
|
||||||
|
tag.tail = tag.tail.strip()
|
||||||
|
compressed = etree.tostring(tree.getroot(), encoding='utf-8')
|
||||||
|
open(ncx_path, 'wb').write(compressed)
|
||||||
|
|
||||||
def convert(htmlfile, opts, notification=None, create_epub=True,
|
def convert(htmlfile, opts, notification=None, create_epub=True,
|
||||||
oeb_cover=False, extract_to=None):
|
oeb_cover=False, extract_to=None):
|
||||||
htmlfile = os.path.abspath(htmlfile)
|
htmlfile = os.path.abspath(htmlfile)
|
||||||
@ -362,7 +376,8 @@ def convert(htmlfile, opts, notification=None, create_epub=True,
|
|||||||
if opts.show_ncx:
|
if opts.show_ncx:
|
||||||
print toc
|
print toc
|
||||||
split(opf_path, opts, stylesheet_map)
|
split(opf_path, opts, stylesheet_map)
|
||||||
check(opf_path, opts.pretty_print)
|
check_links(opf_path, opts.pretty_print)
|
||||||
|
|
||||||
opf = OPF(opf_path, tdir)
|
opf = OPF(opf_path, tdir)
|
||||||
opf.remove_guide()
|
opf.remove_guide()
|
||||||
oeb_cover_file = None
|
oeb_cover_file = None
|
||||||
@ -383,6 +398,13 @@ def convert(htmlfile, opts, notification=None, create_epub=True,
|
|||||||
if not raw.startswith('<?xml '):
|
if not raw.startswith('<?xml '):
|
||||||
raw = '<?xml version="1.0" encoding="UTF-8"?>\n'+raw
|
raw = '<?xml version="1.0" encoding="UTF-8"?>\n'+raw
|
||||||
f.write(raw)
|
f.write(raw)
|
||||||
|
ncx_path = os.path.join(os.path.dirname(opf_path), 'toc.ncx')
|
||||||
|
if os.path.exists(ncx_path) and os.stat(ncx_path).st_size > opts.profile.flow_size:
|
||||||
|
logger.info('Condensing NCX from %d bytes...'%os.stat(ncx_path).st_size)
|
||||||
|
condense_ncx(ncx_path)
|
||||||
|
if os.stat(ncx_path).st_size > opts.profile.flow_size:
|
||||||
|
logger.warn('NCX still larger than allowed size at %d bytes. Menu based Table of Contents may not work on device.'%os.stat(ncx_path).st_size)
|
||||||
|
|
||||||
if create_epub:
|
if create_epub:
|
||||||
epub = initialize_container(opts.output)
|
epub = initialize_container(opts.output)
|
||||||
epub.add_dir(tdir)
|
epub.add_dir(tdir)
|
||||||
|
@ -314,10 +314,22 @@ def opf_traverse(opf_reader, verbose=0, encoding=None):
|
|||||||
|
|
||||||
|
|
||||||
convert_entities = functools.partial(entity_to_unicode, exceptions=['quot', 'apos', 'lt', 'gt', 'amp'])
|
convert_entities = functools.partial(entity_to_unicode, exceptions=['quot', 'apos', 'lt', 'gt', 'amp'])
|
||||||
|
_span_pat = re.compile('<span.*?</span>', re.DOTALL|re.IGNORECASE)
|
||||||
|
|
||||||
|
def sanitize_head(match):
|
||||||
|
x = match.group(1)
|
||||||
|
x = _span_pat.sub('', x)
|
||||||
|
return '<head>\n'+x+'\n</head>'
|
||||||
|
|
||||||
class PreProcessor(object):
|
class PreProcessor(object):
|
||||||
PREPROCESS = [
|
PREPROCESS = [
|
||||||
|
# Some idiotic HTML generators (Frontpage I'm looking at you)
|
||||||
|
# Put all sorts of crap into <head>. This messes up lxml
|
||||||
|
(re.compile(r'<head[^>]*>(.*?)</head>', re.IGNORECASE|re.DOTALL),
|
||||||
|
sanitize_head),
|
||||||
# Convert all entities, since lxml doesn't handle them well
|
# Convert all entities, since lxml doesn't handle them well
|
||||||
(re.compile(r'&(\S+?);'), convert_entities),
|
(re.compile(r'&(\S+?);'), convert_entities),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Fix pdftohtml markup
|
# Fix pdftohtml markup
|
||||||
@ -446,6 +458,8 @@ class Parser(PreProcessor, LoggingInterface):
|
|||||||
def parse_html(self):
|
def parse_html(self):
|
||||||
''' Create lxml ElementTree from HTML '''
|
''' Create lxml ElementTree from HTML '''
|
||||||
self.log_info('\tParsing '+os.sep.join(self.htmlfile.path.split(os.sep)[-3:]))
|
self.log_info('\tParsing '+os.sep.join(self.htmlfile.path.split(os.sep)[-3:]))
|
||||||
|
if self.htmlfile.is_binary:
|
||||||
|
raise ValueError('Not a valid HTML file: '+self.htmlfile.path)
|
||||||
src = open(self.htmlfile.path, 'rb').read().decode(self.htmlfile.encoding, 'replace').strip()
|
src = open(self.htmlfile.path, 'rb').read().decode(self.htmlfile.encoding, 'replace').strip()
|
||||||
src = src.replace('\x00', '')
|
src = src.replace('\x00', '')
|
||||||
src = self.preprocess(src)
|
src = self.preprocess(src)
|
||||||
@ -776,7 +790,10 @@ class Processor(Parser):
|
|||||||
size = '3'
|
size = '3'
|
||||||
if size and size.strip() and size.strip()[0] in ('+', '-'):
|
if size and size.strip() and size.strip()[0] in ('+', '-'):
|
||||||
size = 3 + float(size) # Hack assumes basefont=3
|
size = 3 + float(size) # Hack assumes basefont=3
|
||||||
|
try:
|
||||||
setting = 'font-size: %d%%;'%int((float(size)/3) * 100)
|
setting = 'font-size: %d%%;'%int((float(size)/3) * 100)
|
||||||
|
except ValueError:
|
||||||
|
setting = ''
|
||||||
face = font.attrib.pop('face', None)
|
face = font.attrib.pop('face', None)
|
||||||
if face is not None:
|
if face is not None:
|
||||||
setting += 'font-face:%s;'%face
|
setting += 'font-face:%s;'%face
|
||||||
@ -809,7 +826,7 @@ class Processor(Parser):
|
|||||||
|
|
||||||
css = '\n'.join(['.%s {%s;}'%(cn, setting) for \
|
css = '\n'.join(['.%s {%s;}'%(cn, setting) for \
|
||||||
setting, cn in cache.items()])
|
setting, cn in cache.items()])
|
||||||
sheet = self.css_parser.parseString(self.preprocess_css(css))
|
sheet = self.css_parser.parseString(self.preprocess_css(css.replace(';;}', ';}')))
|
||||||
for rule in sheet:
|
for rule in sheet:
|
||||||
self.stylesheet.add(rule)
|
self.stylesheet.add(rule)
|
||||||
css = ''
|
css = ''
|
||||||
@ -875,7 +892,7 @@ def option_parser():
|
|||||||
%prog [options] file.html|opf
|
%prog [options] file.html|opf
|
||||||
|
|
||||||
Follow all links in an HTML file and collect them into the specified directory.
|
Follow all links in an HTML file and collect them into the specified directory.
|
||||||
Also collects any references resources like images, stylesheets, scripts, etc.
|
Also collects any resources like images, stylesheets, scripts, etc.
|
||||||
If an OPF file is specified instead, the list of files in its <spine> element
|
If an OPF file is specified instead, the list of files in its <spine> element
|
||||||
is used.
|
is used.
|
||||||
'''))
|
'''))
|
||||||
|
@ -7,24 +7,20 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net> ' \
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net> ' \
|
||||||
'and Marshall T. Vandegrift <llasram@gmail.com>'
|
'and Marshall T. Vandegrift <llasram@gmail.com>'
|
||||||
|
|
||||||
import sys, struct, os
|
import sys, struct, cStringIO, os
|
||||||
import functools
|
import functools
|
||||||
import re
|
import re
|
||||||
from urlparse import urldefrag
|
from urlparse import urldefrag
|
||||||
from cStringIO import StringIO
|
|
||||||
from urllib import unquote as urlunquote
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from calibre.ebooks.lit import LitError
|
from calibre.ebooks.lit import LitError
|
||||||
from calibre.ebooks.lit.maps import OPF_MAP, HTML_MAP
|
from calibre.ebooks.lit.maps import OPF_MAP, HTML_MAP
|
||||||
import calibre.ebooks.lit.mssha1 as mssha1
|
import calibre.ebooks.lit.mssha1 as mssha1
|
||||||
from calibre.ebooks.oeb.base import XML_PARSER, urlnormalize
|
from calibre.ebooks.oeb.base import urlnormalize
|
||||||
from calibre.ebooks import DRMError
|
from calibre.ebooks import DRMError
|
||||||
from calibre import plugins
|
from calibre import plugins
|
||||||
lzx, lxzerror = plugins['lzx']
|
lzx, lxzerror = plugins['lzx']
|
||||||
msdes, msdeserror = plugins['msdes']
|
msdes, msdeserror = plugins['msdes']
|
||||||
|
|
||||||
__all__ = ["LitReader"]
|
|
||||||
|
|
||||||
XML_DECL = """<?xml version="1.0" encoding="UTF-8" ?>
|
XML_DECL = """<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
"""
|
"""
|
||||||
OPF_DECL = """<?xml version="1.0" encoding="UTF-8" ?>
|
OPF_DECL = """<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
@ -112,9 +108,6 @@ def consume_sized_utf8_string(bytes, zpad=False):
|
|||||||
pos += 1
|
pos += 1
|
||||||
return u''.join(result), bytes[pos:]
|
return u''.join(result), bytes[pos:]
|
||||||
|
|
||||||
def encode(string):
|
|
||||||
return unicode(string).encode('ascii', 'xmlcharrefreplace')
|
|
||||||
|
|
||||||
class UnBinary(object):
|
class UnBinary(object):
|
||||||
AMPERSAND_RE = re.compile(
|
AMPERSAND_RE = re.compile(
|
||||||
r'&(?!(?:#[0-9]+|#x[0-9a-fA-F]+|[a-zA-Z_:][a-zA-Z0-9.-_:]+);)')
|
r'&(?!(?:#[0-9]+|#x[0-9a-fA-F]+|[a-zA-Z_:][a-zA-Z0-9.-_:]+);)')
|
||||||
@ -125,13 +118,13 @@ class UnBinary(object):
|
|||||||
def __init__(self, bin, path, manifest={}, map=HTML_MAP):
|
def __init__(self, bin, path, manifest={}, map=HTML_MAP):
|
||||||
self.manifest = manifest
|
self.manifest = manifest
|
||||||
self.tag_map, self.attr_map, self.tag_to_attr_map = map
|
self.tag_map, self.attr_map, self.tag_to_attr_map = map
|
||||||
self.is_html = map is HTML_MAP
|
self.opf = map is OPF_MAP
|
||||||
|
self.bin = bin
|
||||||
self.dir = os.path.dirname(path)
|
self.dir = os.path.dirname(path)
|
||||||
buf = StringIO()
|
self.buf = cStringIO.StringIO()
|
||||||
self.binary_to_text(bin, buf)
|
self.binary_to_text()
|
||||||
self.raw = buf.getvalue().lstrip()
|
self.raw = self.buf.getvalue().lstrip().decode('utf-8')
|
||||||
self.escape_reserved()
|
self.escape_reserved()
|
||||||
self._tree = None
|
|
||||||
|
|
||||||
def escape_reserved(self):
|
def escape_reserved(self):
|
||||||
raw = self.raw
|
raw = self.raw
|
||||||
@ -158,28 +151,18 @@ class UnBinary(object):
|
|||||||
return '/'.join(relpath)
|
return '/'.join(relpath)
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.raw.decode('utf-8')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.raw
|
return self.raw
|
||||||
|
|
||||||
def tree():
|
def binary_to_text(self, base=0, depth=0):
|
||||||
def fget(self):
|
|
||||||
if not self._tree:
|
|
||||||
self._tree = etree.fromstring(self.raw, parser=XML_PARSER)
|
|
||||||
return self._tree
|
|
||||||
return property(fget=fget)
|
|
||||||
tree = tree()
|
|
||||||
|
|
||||||
def binary_to_text(self, bin, buf, index=0, depth=0):
|
|
||||||
tag_name = current_map = None
|
tag_name = current_map = None
|
||||||
dynamic_tag = errors = 0
|
dynamic_tag = errors = 0
|
||||||
in_censorship = is_goingdown = False
|
in_censorship = is_goingdown = False
|
||||||
state = 'text'
|
state = 'text'
|
||||||
|
index = base
|
||||||
flags = 0
|
flags = 0
|
||||||
|
|
||||||
while index < len(bin):
|
while index < len(self.bin):
|
||||||
c, index = read_utf8_char(bin, index)
|
c, index = read_utf8_char(self.bin, index)
|
||||||
oc = ord(c)
|
oc = ord(c)
|
||||||
|
|
||||||
if state == 'text':
|
if state == 'text':
|
||||||
@ -192,7 +175,7 @@ class UnBinary(object):
|
|||||||
c = '>>'
|
c = '>>'
|
||||||
elif c == '<':
|
elif c == '<':
|
||||||
c = '<<'
|
c = '<<'
|
||||||
buf.write(encode(c))
|
self.buf.write(c.encode('ascii', 'xmlcharrefreplace'))
|
||||||
|
|
||||||
elif state == 'get flags':
|
elif state == 'get flags':
|
||||||
if oc == 0:
|
if oc == 0:
|
||||||
@ -205,7 +188,7 @@ class UnBinary(object):
|
|||||||
state = 'text' if oc == 0 else 'get attr'
|
state = 'text' if oc == 0 else 'get attr'
|
||||||
if flags & FLAG_OPENING:
|
if flags & FLAG_OPENING:
|
||||||
tag = oc
|
tag = oc
|
||||||
buf.write('<')
|
self.buf.write('<')
|
||||||
if not (flags & FLAG_CLOSING):
|
if not (flags & FLAG_CLOSING):
|
||||||
is_goingdown = True
|
is_goingdown = True
|
||||||
if tag == 0x8000:
|
if tag == 0x8000:
|
||||||
@ -222,7 +205,7 @@ class UnBinary(object):
|
|||||||
tag_name = '?'+unichr(tag)+'?'
|
tag_name = '?'+unichr(tag)+'?'
|
||||||
current_map = self.tag_to_attr_map[tag]
|
current_map = self.tag_to_attr_map[tag]
|
||||||
print 'WARNING: tag %s unknown' % unichr(tag)
|
print 'WARNING: tag %s unknown' % unichr(tag)
|
||||||
buf.write(encode(tag_name))
|
self.buf.write(unicode(tag_name).encode('utf-8'))
|
||||||
elif flags & FLAG_CLOSING:
|
elif flags & FLAG_CLOSING:
|
||||||
if depth == 0:
|
if depth == 0:
|
||||||
raise LitError('Extra closing tag')
|
raise LitError('Extra closing tag')
|
||||||
@ -234,14 +217,15 @@ class UnBinary(object):
|
|||||||
if not is_goingdown:
|
if not is_goingdown:
|
||||||
tag_name = None
|
tag_name = None
|
||||||
dynamic_tag = 0
|
dynamic_tag = 0
|
||||||
buf.write(' />')
|
self.buf.write(' />')
|
||||||
else:
|
else:
|
||||||
buf.write('>')
|
self.buf.write('>')
|
||||||
index = self.binary_to_text(bin, buf, index, depth+1)
|
index = self.binary_to_text(base=index, depth=depth+1)
|
||||||
is_goingdown = False
|
is_goingdown = False
|
||||||
if not tag_name:
|
if not tag_name:
|
||||||
raise LitError('Tag ends before it begins.')
|
raise LitError('Tag ends before it begins.')
|
||||||
buf.write(encode(u''.join(('</', tag_name, '>'))))
|
self.buf.write(u''.join(
|
||||||
|
('</', tag_name, '>')).encode('utf-8'))
|
||||||
dynamic_tag = 0
|
dynamic_tag = 0
|
||||||
tag_name = None
|
tag_name = None
|
||||||
state = 'text'
|
state = 'text'
|
||||||
@ -261,7 +245,7 @@ class UnBinary(object):
|
|||||||
in_censorship = True
|
in_censorship = True
|
||||||
state = 'get value length'
|
state = 'get value length'
|
||||||
continue
|
continue
|
||||||
buf.write(' ' + encode(attr) + '=')
|
self.buf.write(' ' + unicode(attr).encode('utf-8') + '=')
|
||||||
if attr in ['href', 'src']:
|
if attr in ['href', 'src']:
|
||||||
state = 'get href length'
|
state = 'get href length'
|
||||||
else:
|
else:
|
||||||
@ -269,39 +253,40 @@ class UnBinary(object):
|
|||||||
|
|
||||||
elif state == 'get value length':
|
elif state == 'get value length':
|
||||||
if not in_censorship:
|
if not in_censorship:
|
||||||
buf.write('"')
|
self.buf.write('"')
|
||||||
count = oc - 1
|
count = oc - 1
|
||||||
if count == 0:
|
if count == 0:
|
||||||
if not in_censorship:
|
if not in_censorship:
|
||||||
buf.write('"')
|
self.buf.write('"')
|
||||||
in_censorship = False
|
in_censorship = False
|
||||||
state = 'get attr'
|
state = 'get attr'
|
||||||
continue
|
continue
|
||||||
state = 'get value'
|
state = 'get value'
|
||||||
if oc == 0xffff:
|
if oc == 0xffff:
|
||||||
continue
|
continue
|
||||||
if count < 0 or count > (len(bin) - index):
|
if count < 0 or count > (len(self.bin) - index):
|
||||||
raise LitError('Invalid character count %d' % count)
|
raise LitError('Invalid character count %d' % count)
|
||||||
|
|
||||||
elif state == 'get value':
|
elif state == 'get value':
|
||||||
if count == 0xfffe:
|
if count == 0xfffe:
|
||||||
if not in_censorship:
|
if not in_censorship:
|
||||||
buf.write('%s"' % (oc - 1))
|
self.buf.write('%s"' % (oc - 1))
|
||||||
in_censorship = False
|
in_censorship = False
|
||||||
state = 'get attr'
|
state = 'get attr'
|
||||||
elif count > 0:
|
elif count > 0:
|
||||||
if not in_censorship:
|
if not in_censorship:
|
||||||
buf.write(encode(c))
|
self.buf.write(c.encode(
|
||||||
|
'ascii', 'xmlcharrefreplace'))
|
||||||
count -= 1
|
count -= 1
|
||||||
if count == 0:
|
if count == 0:
|
||||||
if not in_censorship:
|
if not in_censorship:
|
||||||
buf.write('"')
|
self.buf.write('"')
|
||||||
in_censorship = False
|
in_censorship = False
|
||||||
state = 'get attr'
|
state = 'get attr'
|
||||||
|
|
||||||
elif state == 'get custom length':
|
elif state == 'get custom length':
|
||||||
count = oc - 1
|
count = oc - 1
|
||||||
if count <= 0 or count > len(bin)-index:
|
if count <= 0 or count > len(self.bin)-index:
|
||||||
raise LitError('Invalid character count %d' % count)
|
raise LitError('Invalid character count %d' % count)
|
||||||
dynamic_tag += 1
|
dynamic_tag += 1
|
||||||
state = 'get custom'
|
state = 'get custom'
|
||||||
@ -311,26 +296,26 @@ class UnBinary(object):
|
|||||||
tag_name += c
|
tag_name += c
|
||||||
count -= 1
|
count -= 1
|
||||||
if count == 0:
|
if count == 0:
|
||||||
buf.write(encode(tag_name))
|
self.buf.write(unicode(tag_name).encode('utf-8'))
|
||||||
state = 'get attr'
|
state = 'get attr'
|
||||||
|
|
||||||
elif state == 'get attr length':
|
elif state == 'get attr length':
|
||||||
count = oc - 1
|
count = oc - 1
|
||||||
if count <= 0 or count > (len(bin) - index):
|
if count <= 0 or count > (len(self.bin) - index):
|
||||||
raise LitError('Invalid character count %d' % count)
|
raise LitError('Invalid character count %d' % count)
|
||||||
buf.write(' ')
|
self.buf.write(' ')
|
||||||
state = 'get custom attr'
|
state = 'get custom attr'
|
||||||
|
|
||||||
elif state == 'get custom attr':
|
elif state == 'get custom attr':
|
||||||
buf.write(encode(c))
|
self.buf.write(unicode(c).encode('utf-8'))
|
||||||
count -= 1
|
count -= 1
|
||||||
if count == 0:
|
if count == 0:
|
||||||
buf.write('=')
|
self.buf.write('=')
|
||||||
state = 'get value length'
|
state = 'get value length'
|
||||||
|
|
||||||
elif state == 'get href length':
|
elif state == 'get href length':
|
||||||
count = oc - 1
|
count = oc - 1
|
||||||
if count <= 0 or count > (len(bin) - index):
|
if count <= 0 or count > (len(self.bin) - index):
|
||||||
raise LitError('Invalid character count %d' % count)
|
raise LitError('Invalid character count %d' % count)
|
||||||
href = ''
|
href = ''
|
||||||
state = 'get href'
|
state = 'get href'
|
||||||
@ -344,11 +329,10 @@ class UnBinary(object):
|
|||||||
if frag:
|
if frag:
|
||||||
path = '#'.join((path, frag))
|
path = '#'.join((path, frag))
|
||||||
path = urlnormalize(path)
|
path = urlnormalize(path)
|
||||||
buf.write(encode(u'"%s"' % path))
|
self.buf.write((u'"%s"' % path).encode('utf-8'))
|
||||||
state = 'get attr'
|
state = 'get attr'
|
||||||
return index
|
return index
|
||||||
|
|
||||||
|
|
||||||
class DirectoryEntry(object):
|
class DirectoryEntry(object):
|
||||||
def __init__(self, name, section, offset, size):
|
def __init__(self, name, section, offset, size):
|
||||||
self.name = name
|
self.name = name
|
||||||
@ -363,7 +347,6 @@ class DirectoryEntry(object):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return repr(self)
|
return repr(self)
|
||||||
|
|
||||||
|
|
||||||
class ManifestItem(object):
|
class ManifestItem(object):
|
||||||
def __init__(self, original, internal, mime_type, offset, root, state):
|
def __init__(self, original, internal, mime_type, offset, root, state):
|
||||||
self.original = original
|
self.original = original
|
||||||
@ -391,87 +374,65 @@ class ManifestItem(object):
|
|||||||
% (self.internal, self.path, self.mime_type, self.offset,
|
% (self.internal, self.path, self.mime_type, self.offset,
|
||||||
self.root, self.state)
|
self.root, self.state)
|
||||||
|
|
||||||
|
|
||||||
def preserve(function):
|
def preserve(function):
|
||||||
def wrapper(self, *args, **kwargs):
|
def wrapper(self, *args, **kwargs):
|
||||||
opos = self.stream.tell()
|
opos = self._stream.tell()
|
||||||
try:
|
try:
|
||||||
return function(self, *args, **kwargs)
|
return function(self, *args, **kwargs)
|
||||||
finally:
|
finally:
|
||||||
self.stream.seek(opos)
|
self._stream.seek(opos)
|
||||||
functools.update_wrapper(wrapper, function)
|
functools.update_wrapper(wrapper, function)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
class LitFile(object):
|
class LitReader(object):
|
||||||
PIECE_SIZE = 16
|
PIECE_SIZE = 16
|
||||||
|
XML_PARSER = etree.XMLParser(
|
||||||
def __init__(self, filename_or_stream):
|
recover=True, resolve_entities=False)
|
||||||
if hasattr(filename_or_stream, 'read'):
|
|
||||||
self.stream = filename_or_stream
|
|
||||||
else:
|
|
||||||
self.stream = open(filename_or_stream, 'rb')
|
|
||||||
try:
|
|
||||||
self.opf_path = os.path.splitext(
|
|
||||||
os.path.basename(self.stream.name))[0] + '.opf'
|
|
||||||
except AttributeError:
|
|
||||||
self.opf_path = 'content.opf'
|
|
||||||
if self.magic != 'ITOLITLS':
|
|
||||||
raise LitError('Not a valid LIT file')
|
|
||||||
if self.version != 1:
|
|
||||||
raise LitError('Unknown LIT version %d' % (self.version,))
|
|
||||||
self.read_secondary_header()
|
|
||||||
self.read_header_pieces()
|
|
||||||
self.read_section_names()
|
|
||||||
self.read_manifest()
|
|
||||||
self.read_drm()
|
|
||||||
|
|
||||||
def warn(self, msg):
|
|
||||||
print "WARNING: %s" % (msg,)
|
|
||||||
|
|
||||||
def magic():
|
def magic():
|
||||||
@preserve
|
@preserve
|
||||||
def fget(self):
|
def fget(self):
|
||||||
self.stream.seek(0)
|
self._stream.seek(0)
|
||||||
return self.stream.read(8)
|
return self._stream.read(8)
|
||||||
return property(fget=fget)
|
return property(fget=fget)
|
||||||
magic = magic()
|
magic = magic()
|
||||||
|
|
||||||
def version():
|
def version():
|
||||||
def fget(self):
|
def fget(self):
|
||||||
self.stream.seek(8)
|
self._stream.seek(8)
|
||||||
return u32(self.stream.read(4))
|
return u32(self._stream.read(4))
|
||||||
return property(fget=fget)
|
return property(fget=fget)
|
||||||
version = version()
|
version = version()
|
||||||
|
|
||||||
def hdr_len():
|
def hdr_len():
|
||||||
@preserve
|
@preserve
|
||||||
def fget(self):
|
def fget(self):
|
||||||
self.stream.seek(12)
|
self._stream.seek(12)
|
||||||
return int32(self.stream.read(4))
|
return int32(self._stream.read(4))
|
||||||
return property(fget=fget)
|
return property(fget=fget)
|
||||||
hdr_len = hdr_len()
|
hdr_len = hdr_len()
|
||||||
|
|
||||||
def num_pieces():
|
def num_pieces():
|
||||||
@preserve
|
@preserve
|
||||||
def fget(self):
|
def fget(self):
|
||||||
self.stream.seek(16)
|
self._stream.seek(16)
|
||||||
return int32(self.stream.read(4))
|
return int32(self._stream.read(4))
|
||||||
return property(fget=fget)
|
return property(fget=fget)
|
||||||
num_pieces = num_pieces()
|
num_pieces = num_pieces()
|
||||||
|
|
||||||
def sec_hdr_len():
|
def sec_hdr_len():
|
||||||
@preserve
|
@preserve
|
||||||
def fget(self):
|
def fget(self):
|
||||||
self.stream.seek(20)
|
self._stream.seek(20)
|
||||||
return int32(self.stream.read(4))
|
return int32(self._stream.read(4))
|
||||||
return property(fget=fget)
|
return property(fget=fget)
|
||||||
sec_hdr_len = sec_hdr_len()
|
sec_hdr_len = sec_hdr_len()
|
||||||
|
|
||||||
def guid():
|
def guid():
|
||||||
@preserve
|
@preserve
|
||||||
def fget(self):
|
def fget(self):
|
||||||
self.stream.seek(24)
|
self._stream.seek(24)
|
||||||
return self.stream.read(16)
|
return self._stream.read(16)
|
||||||
return property(fget=fget)
|
return property(fget=fget)
|
||||||
guid = guid()
|
guid = guid()
|
||||||
|
|
||||||
@ -481,27 +442,44 @@ class LitFile(object):
|
|||||||
size = self.hdr_len \
|
size = self.hdr_len \
|
||||||
+ (self.num_pieces * self.PIECE_SIZE) \
|
+ (self.num_pieces * self.PIECE_SIZE) \
|
||||||
+ self.sec_hdr_len
|
+ self.sec_hdr_len
|
||||||
self.stream.seek(0)
|
self._stream.seek(0)
|
||||||
return self.stream.read(size)
|
return self._stream.read(size)
|
||||||
return property(fget=fget)
|
return property(fget=fget)
|
||||||
header = header()
|
header = header()
|
||||||
|
|
||||||
|
def __init__(self, filename_or_stream):
|
||||||
|
if hasattr(filename_or_stream, 'read'):
|
||||||
|
self._stream = filename_or_stream
|
||||||
|
else:
|
||||||
|
self._stream = open(filename_or_stream, 'rb')
|
||||||
|
if self.magic != 'ITOLITLS':
|
||||||
|
raise LitError('Not a valid LIT file')
|
||||||
|
if self.version != 1:
|
||||||
|
raise LitError('Unknown LIT version %d' % (self.version,))
|
||||||
|
self.entries = {}
|
||||||
|
self._read_secondary_header()
|
||||||
|
self._read_header_pieces()
|
||||||
|
self._read_section_names()
|
||||||
|
self._read_manifest()
|
||||||
|
self._read_meta()
|
||||||
|
self._read_drm()
|
||||||
|
|
||||||
@preserve
|
@preserve
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
self.stream.seek(0, 2)
|
self._stream.seek(0, 2)
|
||||||
return self.stream.tell()
|
return self._stream.tell()
|
||||||
|
|
||||||
@preserve
|
@preserve
|
||||||
def read_raw(self, offset, size):
|
def _read_raw(self, offset, size):
|
||||||
self.stream.seek(offset)
|
self._stream.seek(offset)
|
||||||
return self.stream.read(size)
|
return self._stream.read(size)
|
||||||
|
|
||||||
def read_content(self, offset, size):
|
def _read_content(self, offset, size):
|
||||||
return self.read_raw(self.content_offset + offset, size)
|
return self._read_raw(self.content_offset + offset, size)
|
||||||
|
|
||||||
def read_secondary_header(self):
|
def _read_secondary_header(self):
|
||||||
offset = self.hdr_len + (self.num_pieces * self.PIECE_SIZE)
|
offset = self.hdr_len + (self.num_pieces * self.PIECE_SIZE)
|
||||||
bytes = self.read_raw(offset, self.sec_hdr_len)
|
bytes = self._read_raw(offset, self.sec_hdr_len)
|
||||||
offset = int32(bytes[4:])
|
offset = int32(bytes[4:])
|
||||||
while offset < len(bytes):
|
while offset < len(bytes):
|
||||||
blocktype = bytes[offset:offset+4]
|
blocktype = bytes[offset:offset+4]
|
||||||
@ -529,21 +507,21 @@ class LitFile(object):
|
|||||||
if not hasattr(self, 'content_offset'):
|
if not hasattr(self, 'content_offset'):
|
||||||
raise LitError('Could not figure out the content offset')
|
raise LitError('Could not figure out the content offset')
|
||||||
|
|
||||||
def read_header_pieces(self):
|
def _read_header_pieces(self):
|
||||||
src = self.header[self.hdr_len:]
|
src = self.header[self.hdr_len:]
|
||||||
for i in xrange(self.num_pieces):
|
for i in xrange(self.num_pieces):
|
||||||
piece = src[i * self.PIECE_SIZE:(i + 1) * self.PIECE_SIZE]
|
piece = src[i * self.PIECE_SIZE:(i + 1) * self.PIECE_SIZE]
|
||||||
if u32(piece[4:]) != 0 or u32(piece[12:]) != 0:
|
if u32(piece[4:]) != 0 or u32(piece[12:]) != 0:
|
||||||
raise LitError('Piece %s has 64bit value' % repr(piece))
|
raise LitError('Piece %s has 64bit value' % repr(piece))
|
||||||
offset, size = u32(piece), int32(piece[8:])
|
offset, size = u32(piece), int32(piece[8:])
|
||||||
piece = self.read_raw(offset, size)
|
piece = self._read_raw(offset, size)
|
||||||
if i == 0:
|
if i == 0:
|
||||||
continue # Dont need this piece
|
continue # Dont need this piece
|
||||||
elif i == 1:
|
elif i == 1:
|
||||||
if u32(piece[8:]) != self.entry_chunklen or \
|
if u32(piece[8:]) != self.entry_chunklen or \
|
||||||
u32(piece[12:]) != self.entry_unknown:
|
u32(piece[12:]) != self.entry_unknown:
|
||||||
raise LitError('Secondary header does not match piece')
|
raise LitError('Secondary header does not match piece')
|
||||||
self.read_directory(piece)
|
self._read_directory(piece)
|
||||||
elif i == 2:
|
elif i == 2:
|
||||||
if u32(piece[8:]) != self.count_chunklen or \
|
if u32(piece[8:]) != self.count_chunklen or \
|
||||||
u32(piece[12:]) != self.count_unknown:
|
u32(piece[12:]) != self.count_unknown:
|
||||||
@ -554,13 +532,12 @@ class LitFile(object):
|
|||||||
elif i == 4:
|
elif i == 4:
|
||||||
self.piece4_guid = piece
|
self.piece4_guid = piece
|
||||||
|
|
||||||
def read_directory(self, piece):
|
def _read_directory(self, piece):
|
||||||
if not piece.startswith('IFCM'):
|
if not piece.startswith('IFCM'):
|
||||||
raise LitError('Header piece #1 is not main directory.')
|
raise LitError('Header piece #1 is not main directory.')
|
||||||
chunk_size, num_chunks = int32(piece[8:12]), int32(piece[24:28])
|
chunk_size, num_chunks = int32(piece[8:12]), int32(piece[24:28])
|
||||||
if (32 + (num_chunks * chunk_size)) != len(piece):
|
if (32 + (num_chunks * chunk_size)) != len(piece):
|
||||||
raise LitError('IFCM header has incorrect length')
|
raise LitError('IFCM HEADER has incorrect length')
|
||||||
self.entries = {}
|
|
||||||
for i in xrange(num_chunks):
|
for i in xrange(num_chunks):
|
||||||
offset = 32 + (i * chunk_size)
|
offset = 32 + (i * chunk_size)
|
||||||
chunk = piece[offset:offset + chunk_size]
|
chunk = piece[offset:offset + chunk_size]
|
||||||
@ -594,17 +571,17 @@ class LitFile(object):
|
|||||||
entry = DirectoryEntry(name, section, offset, size)
|
entry = DirectoryEntry(name, section, offset, size)
|
||||||
self.entries[name] = entry
|
self.entries[name] = entry
|
||||||
|
|
||||||
def read_section_names(self):
|
def _read_section_names(self):
|
||||||
if '::DataSpace/NameList' not in self.entries:
|
if '::DataSpace/NameList' not in self.entries:
|
||||||
raise LitError('Lit file does not have a valid NameList')
|
raise LitError('Lit file does not have a valid NameList')
|
||||||
raw = self.get_file('::DataSpace/NameList')
|
raw = self.get_file('::DataSpace/NameList')
|
||||||
if len(raw) < 4:
|
if len(raw) < 4:
|
||||||
raise LitError('Invalid Namelist section')
|
raise LitError('Invalid Namelist section')
|
||||||
pos = 4
|
pos = 4
|
||||||
num_sections = u16(raw[2:pos])
|
self.num_sections = u16(raw[2:pos])
|
||||||
self.section_names = [""] * num_sections
|
self.section_names = [""]*self.num_sections
|
||||||
self.section_data = [None] * num_sections
|
self.section_data = [None]*self.num_sections
|
||||||
for section in xrange(num_sections):
|
for section in xrange(self.num_sections):
|
||||||
size = u16(raw[pos:pos+2])
|
size = u16(raw[pos:pos+2])
|
||||||
pos += 2
|
pos += 2
|
||||||
size = size*2 + 2
|
size = size*2 + 2
|
||||||
@ -614,12 +591,11 @@ class LitFile(object):
|
|||||||
raw[pos:pos+size].decode('utf-16-le').rstrip('\000')
|
raw[pos:pos+size].decode('utf-16-le').rstrip('\000')
|
||||||
pos += size
|
pos += size
|
||||||
|
|
||||||
def read_manifest(self):
|
def _read_manifest(self):
|
||||||
if '/manifest' not in self.entries:
|
if '/manifest' not in self.entries:
|
||||||
raise LitError('Lit file does not have a valid manifest')
|
raise LitError('Lit file does not have a valid manifest')
|
||||||
raw = self.get_file('/manifest')
|
raw = self.get_file('/manifest')
|
||||||
self.manifest = {}
|
self.manifest = {}
|
||||||
self.paths = {self.opf_path: None}
|
|
||||||
while raw:
|
while raw:
|
||||||
slen, raw = ord(raw[0]), raw[1:]
|
slen, raw = ord(raw[0]), raw[1:]
|
||||||
if slen == 0: break
|
if slen == 0: break
|
||||||
@ -658,9 +634,28 @@ class LitFile(object):
|
|||||||
for item in mlist:
|
for item in mlist:
|
||||||
if item.path[0] == '/':
|
if item.path[0] == '/':
|
||||||
item.path = os.path.basename(item.path)
|
item.path = os.path.basename(item.path)
|
||||||
self.paths[item.path] = item
|
|
||||||
|
|
||||||
def read_drm(self):
|
def _pretty_print(self, xml):
|
||||||
|
f = cStringIO.StringIO(xml.encode('utf-8'))
|
||||||
|
doc = etree.parse(f, parser=self.XML_PARSER)
|
||||||
|
pretty = etree.tostring(doc, encoding='ascii', pretty_print=True)
|
||||||
|
return XML_DECL + unicode(pretty)
|
||||||
|
|
||||||
|
def _read_meta(self):
|
||||||
|
path = 'content.opf'
|
||||||
|
raw = self.get_file('/meta')
|
||||||
|
xml = OPF_DECL
|
||||||
|
try:
|
||||||
|
xml += unicode(UnBinary(raw, path, self.manifest, OPF_MAP))
|
||||||
|
except LitError:
|
||||||
|
if 'PENGUIN group' not in raw: raise
|
||||||
|
print "WARNING: attempting PENGUIN malformed OPF fix"
|
||||||
|
raw = raw.replace(
|
||||||
|
'PENGUIN group', '\x00\x01\x18\x00PENGUIN group', 1)
|
||||||
|
xml += unicode(UnBinary(raw, path, self.manifest, OPF_MAP))
|
||||||
|
self.meta = xml
|
||||||
|
|
||||||
|
def _read_drm(self):
|
||||||
self.drmlevel = 0
|
self.drmlevel = 0
|
||||||
if '/DRMStorage/Licenses/EUL' in self.entries:
|
if '/DRMStorage/Licenses/EUL' in self.entries:
|
||||||
self.drmlevel = 5
|
self.drmlevel = 5
|
||||||
@ -671,7 +666,7 @@ class LitFile(object):
|
|||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
if self.drmlevel < 5:
|
if self.drmlevel < 5:
|
||||||
msdes.deskey(self.calculate_deskey(), msdes.DE1)
|
msdes.deskey(self._calculate_deskey(), msdes.DE1)
|
||||||
bookkey = msdes.des(self.get_file('/DRMStorage/DRMSealed'))
|
bookkey = msdes.des(self.get_file('/DRMStorage/DRMSealed'))
|
||||||
if bookkey[0] != '\000':
|
if bookkey[0] != '\000':
|
||||||
raise LitError('Unable to decrypt title key!')
|
raise LitError('Unable to decrypt title key!')
|
||||||
@ -679,7 +674,7 @@ class LitFile(object):
|
|||||||
else:
|
else:
|
||||||
raise DRMError("Cannot access DRM-protected book")
|
raise DRMError("Cannot access DRM-protected book")
|
||||||
|
|
||||||
def calculate_deskey(self):
|
def _calculate_deskey(self):
|
||||||
hashfiles = ['/meta', '/DRMStorage/DRMSource']
|
hashfiles = ['/meta', '/DRMStorage/DRMSource']
|
||||||
if self.drmlevel == 3:
|
if self.drmlevel == 3:
|
||||||
hashfiles.append('/DRMStorage/DRMBookplate')
|
hashfiles.append('/DRMStorage/DRMBookplate')
|
||||||
@ -703,18 +698,18 @@ class LitFile(object):
|
|||||||
def get_file(self, name):
|
def get_file(self, name):
|
||||||
entry = self.entries[name]
|
entry = self.entries[name]
|
||||||
if entry.section == 0:
|
if entry.section == 0:
|
||||||
return self.read_content(entry.offset, entry.size)
|
return self._read_content(entry.offset, entry.size)
|
||||||
section = self.get_section(entry.section)
|
section = self.get_section(entry.section)
|
||||||
return section[entry.offset:entry.offset+entry.size]
|
return section[entry.offset:entry.offset+entry.size]
|
||||||
|
|
||||||
def get_section(self, section):
|
def get_section(self, section):
|
||||||
data = self.section_data[section]
|
data = self.section_data[section]
|
||||||
if not data:
|
if not data:
|
||||||
data = self.get_section_uncached(section)
|
data = self._get_section(section)
|
||||||
self.section_data[section] = data
|
self.section_data[section] = data
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def get_section_uncached(self, section):
|
def _get_section(self, section):
|
||||||
name = self.section_names[section]
|
name = self.section_names[section]
|
||||||
path = '::DataSpace/Storage/' + name
|
path = '::DataSpace/Storage/' + name
|
||||||
transform = self.get_file(path + '/Transform/List')
|
transform = self.get_file(path + '/Transform/List')
|
||||||
@ -726,29 +721,29 @@ class LitFile(object):
|
|||||||
raise LitError("ControlData is too short")
|
raise LitError("ControlData is too short")
|
||||||
guid = msguid(transform)
|
guid = msguid(transform)
|
||||||
if guid == DESENCRYPT_GUID:
|
if guid == DESENCRYPT_GUID:
|
||||||
content = self.decrypt(content)
|
content = self._decrypt(content)
|
||||||
control = control[csize:]
|
control = control[csize:]
|
||||||
elif guid == LZXCOMPRESS_GUID:
|
elif guid == LZXCOMPRESS_GUID:
|
||||||
reset_table = self.get_file(
|
reset_table = self.get_file(
|
||||||
'/'.join(('::DataSpace/Storage', name, 'Transform',
|
'/'.join(('::DataSpace/Storage', name, 'Transform',
|
||||||
LZXCOMPRESS_GUID, 'InstanceData/ResetTable')))
|
LZXCOMPRESS_GUID, 'InstanceData/ResetTable')))
|
||||||
content = self.decompress(content, control, reset_table)
|
content = self._decompress(content, control, reset_table)
|
||||||
control = control[csize:]
|
control = control[csize:]
|
||||||
else:
|
else:
|
||||||
raise LitError("Unrecognized transform: %s." % repr(guid))
|
raise LitError("Unrecognized transform: %s." % repr(guid))
|
||||||
transform = transform[16:]
|
transform = transform[16:]
|
||||||
return content
|
return content
|
||||||
|
|
||||||
def decrypt(self, content):
|
def _decrypt(self, content):
|
||||||
length = len(content)
|
length = len(content)
|
||||||
extra = length & 0x7
|
extra = length & 0x7
|
||||||
if extra > 0:
|
if extra > 0:
|
||||||
self.warn("content length not a multiple of block size")
|
self._warn("content length not a multiple of block size")
|
||||||
content += "\0" * (8 - extra)
|
content += "\0" * (8 - extra)
|
||||||
msdes.deskey(self.bookkey, msdes.DE1)
|
msdes.deskey(self.bookkey, msdes.DE1)
|
||||||
return msdes.des(content)
|
return msdes.des(content)
|
||||||
|
|
||||||
def decompress(self, content, control, reset_table):
|
def _decompress(self, content, control, reset_table):
|
||||||
if len(control) < 32 or control[CONTROL_TAG:CONTROL_TAG+4] != "LZXC":
|
if len(control) < 32 or control[CONTROL_TAG:CONTROL_TAG+4] != "LZXC":
|
||||||
raise LitError("Invalid ControlData tag value")
|
raise LitError("Invalid ControlData tag value")
|
||||||
if len(reset_table) < (RESET_INTERVAL + 8):
|
if len(reset_table) < (RESET_INTERVAL + 8):
|
||||||
@ -789,7 +784,7 @@ class LitFile(object):
|
|||||||
result.append(
|
result.append(
|
||||||
lzx.decompress(content[base:size], window_bytes))
|
lzx.decompress(content[base:size], window_bytes))
|
||||||
except lzx.LZXError:
|
except lzx.LZXError:
|
||||||
self.warn("LZX decompression error; skipping chunk")
|
self._warn("LZX decompression error; skipping chunk")
|
||||||
bytes_remaining -= window_bytes
|
bytes_remaining -= window_bytes
|
||||||
base = size
|
base = size
|
||||||
accum += int32(reset_table[RESET_INTERVAL:])
|
accum += int32(reset_table[RESET_INTERVAL:])
|
||||||
@ -799,88 +794,55 @@ class LitFile(object):
|
|||||||
try:
|
try:
|
||||||
result.append(lzx.decompress(content[base:], bytes_remaining))
|
result.append(lzx.decompress(content[base:], bytes_remaining))
|
||||||
except lzx.LZXError:
|
except lzx.LZXError:
|
||||||
self.warn("LZX decompression error; skipping chunk")
|
self._warn("LZX decompression error; skipping chunk")
|
||||||
bytes_remaining = 0
|
bytes_remaining = 0
|
||||||
if bytes_remaining > 0:
|
if bytes_remaining > 0:
|
||||||
raise LitError("Failed to completely decompress section")
|
raise LitError("Failed to completely decompress section")
|
||||||
return ''.join(result)
|
return ''.join(result)
|
||||||
|
|
||||||
|
def get_entry_content(self, entry, pretty_print=False):
|
||||||
class LitReader(object):
|
if 'spine' in entry.state:
|
||||||
def __init__(self, filename_or_stream):
|
name = '/'.join(('/data', entry.internal, 'content'))
|
||||||
self._litfile = LitFile(filename_or_stream)
|
path = entry.path
|
||||||
|
raw = self.get_file(name)
|
||||||
def namelist(self):
|
decl, map = (OPF_DECL, OPF_MAP) \
|
||||||
return self._litfile.paths.keys()
|
if name == '/meta' else (HTML_DECL, HTML_MAP)
|
||||||
|
content = decl + unicode(UnBinary(raw, path, self.manifest, map))
|
||||||
def exists(self, name):
|
|
||||||
return urlunquote(name) in self._litfile.paths
|
|
||||||
|
|
||||||
def read_xml(self, name):
|
|
||||||
entry = self._litfile.paths[urlunquote(name)] if name else None
|
|
||||||
if entry is None:
|
|
||||||
content = self._read_meta()
|
|
||||||
elif 'spine' in entry.state:
|
|
||||||
internal = '/'.join(('/data', entry.internal, 'content'))
|
|
||||||
raw = self._litfile.get_file(internal)
|
|
||||||
unbin = UnBinary(raw, name, self._litfile.manifest, HTML_MAP)
|
|
||||||
content = unbin.tree
|
|
||||||
else:
|
|
||||||
raise LitError('Requested non-XML content as XML')
|
|
||||||
return content
|
|
||||||
|
|
||||||
def read(self, name, pretty_print=False):
|
|
||||||
entry = self._litfile.paths[urlunquote(name)] if name else None
|
|
||||||
if entry is None:
|
|
||||||
meta = self._read_meta()
|
|
||||||
content = OPF_DECL + etree.tostring(
|
|
||||||
meta, encoding='ascii', pretty_print=pretty_print)
|
|
||||||
elif 'spine' in entry.state:
|
|
||||||
internal = '/'.join(('/data', entry.internal, 'content'))
|
|
||||||
raw = self._litfile.get_file(internal)
|
|
||||||
unbin = UnBinary(raw, name, self._litfile.manifest, HTML_MAP)
|
|
||||||
content = HTML_DECL
|
|
||||||
if pretty_print:
|
if pretty_print:
|
||||||
content += etree.tostring(unbin.tree,
|
content = self._pretty_print(content)
|
||||||
encoding='ascii', pretty_print=True)
|
content = content.encode('utf-8')
|
||||||
else:
|
else:
|
||||||
content += str(unbin)
|
name = '/'.join(('/data', entry.internal))
|
||||||
else:
|
content = self.get_file(name)
|
||||||
internal = '/'.join(('/data', entry.internal))
|
|
||||||
content = self._litfile.get_file(internal)
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
def meta():
|
def extract_content(self, output_dir=os.getcwdu(), pretty_print=False):
|
||||||
def fget(self):
|
output_dir = os.path.abspath(output_dir)
|
||||||
return self.read(self._litfile.opf_path)
|
try:
|
||||||
return property(fget=fget)
|
opf_path = os.path.splitext(
|
||||||
meta = meta()
|
os.path.basename(self._stream.name))[0] + '.opf'
|
||||||
|
except AttributeError:
|
||||||
|
opf_path = 'content.opf'
|
||||||
|
opf_path = os.path.join(output_dir, opf_path)
|
||||||
|
self._ensure_dir(opf_path)
|
||||||
|
with open(opf_path, 'wb') as f:
|
||||||
|
xml = self.meta
|
||||||
|
if pretty_print:
|
||||||
|
xml = self._pretty_print(xml)
|
||||||
|
f.write(xml.encode('utf-8'))
|
||||||
|
for entry in self.manifest.values():
|
||||||
|
path = os.path.join(output_dir, entry.path)
|
||||||
|
self._ensure_dir(path)
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(self.get_entry_content(entry, pretty_print))
|
||||||
|
|
||||||
def _ensure_dir(self, path):
|
def _ensure_dir(self, path):
|
||||||
dir = os.path.dirname(path)
|
dir = os.path.dirname(path)
|
||||||
if not os.path.isdir(dir):
|
if not os.path.isdir(dir):
|
||||||
os.makedirs(dir)
|
os.makedirs(dir)
|
||||||
|
|
||||||
def extract_content(self, output_dir=os.getcwdu(), pretty_print=False):
|
def _warn(self, msg):
|
||||||
for name in self.namelist():
|
print "WARNING: %s" % (msg,)
|
||||||
path = os.path.join(output_dir, name)
|
|
||||||
self._ensure_dir(path)
|
|
||||||
with open(path, 'wb') as f:
|
|
||||||
f.write(self.read(name, pretty_print=pretty_print))
|
|
||||||
|
|
||||||
def _read_meta(self):
|
|
||||||
path = 'content.opf'
|
|
||||||
raw = self._litfile.get_file('/meta')
|
|
||||||
try:
|
|
||||||
unbin = UnBinary(raw, path, self._litfile.manifest, OPF_MAP)
|
|
||||||
except LitError:
|
|
||||||
if 'PENGUIN group' not in raw: raise
|
|
||||||
print "WARNING: attempting PENGUIN malformed OPF fix"
|
|
||||||
raw = raw.replace(
|
|
||||||
'PENGUIN group', '\x00\x01\x18\x00PENGUIN group', 1)
|
|
||||||
unbin = UnBinary(raw, path, self._litfile.manifest, OPF_MAP)
|
|
||||||
return unbin.tree
|
|
||||||
|
|
||||||
|
|
||||||
def option_parser():
|
def option_parser():
|
||||||
from calibre.utils.config import OptionParser
|
from calibre.utils.config import OptionParser
|
||||||
@ -890,8 +852,7 @@ def option_parser():
|
|||||||
help=_('Output directory. Defaults to current directory.'))
|
help=_('Output directory. Defaults to current directory.'))
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
'-p', '--pretty-print', default=False, action='store_true',
|
'-p', '--pretty-print', default=False, action='store_true',
|
||||||
help=_('Legibly format extracted markup.' \
|
help=_('Legibly format extracted markup. May modify meaningful whitespace.'))
|
||||||
' May modify meaningful whitespace.'))
|
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
'--verbose', default=False, action='store_true',
|
'--verbose', default=False, action='store_true',
|
||||||
help=_('Useful for debugging.'))
|
help=_('Useful for debugging.'))
|
||||||
|
@ -108,6 +108,8 @@ def option_parser(usage, gui_mode=False):
|
|||||||
help=_('Add a header to all the pages with title and author.'))
|
help=_('Add a header to all the pages with title and author.'))
|
||||||
laf.add_option('--headerformat', default="%t by %a", dest='headerformat', type='string',
|
laf.add_option('--headerformat', default="%t by %a", dest='headerformat', type='string',
|
||||||
help=_('Set the format of the header. %a is replaced by the author and %t by the title. Default is %default'))
|
help=_('Set the format of the header. %a is replaced by the author and %t by the title. Default is %default'))
|
||||||
|
laf.add_option('--header-separation', default=0, type='int',
|
||||||
|
help=_('Add extra spacing below the header. Default is %default px.'))
|
||||||
laf.add_option('--override-css', default=None, dest='_override_css', type='string',
|
laf.add_option('--override-css', default=None, dest='_override_css', type='string',
|
||||||
help=_('Override the CSS. Can be either a path to a CSS stylesheet or a string. If it is a string it is interpreted as CSS.'))
|
help=_('Override the CSS. Can be either a path to a CSS stylesheet or a string. If it is a string it is interpreted as CSS.'))
|
||||||
laf.add_option('--use-spine', default=False, dest='use_spine', action='store_true',
|
laf.add_option('--use-spine', default=False, dest='use_spine', action='store_true',
|
||||||
@ -260,10 +262,11 @@ def Book(options, logger, font_delta=0, header=None,
|
|||||||
hb.append(header)
|
hb.append(header)
|
||||||
hdr.PutObj(hb)
|
hdr.PutObj(hb)
|
||||||
ps['headheight'] = profile.header_height
|
ps['headheight'] = profile.header_height
|
||||||
|
ps['headsep'] = options.header_separation
|
||||||
ps['header'] = hdr
|
ps['header'] = hdr
|
||||||
ps['topmargin'] = 0
|
ps['topmargin'] = 0
|
||||||
ps['textheight'] = profile.screen_height - (options.bottom_margin + ps['topmargin']) \
|
ps['textheight'] = profile.screen_height - (options.bottom_margin + ps['topmargin']) \
|
||||||
- ps['headheight'] - profile.fudge
|
- ps['headheight'] - ps['headsep'] - profile.fudge
|
||||||
|
|
||||||
fontsize = int(10*profile.font_size+font_delta*20)
|
fontsize = int(10*profile.font_size+font_delta*20)
|
||||||
baselineskip = fontsize + 20
|
baselineskip = fontsize + 20
|
||||||
|
@ -2,14 +2,14 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
import os, sys, shutil, logging
|
import os, sys, shutil, logging
|
||||||
from tempfile import mkdtemp
|
|
||||||
from calibre.ebooks.lrf import option_parser as lrf_option_parser
|
from calibre.ebooks.lrf import option_parser as lrf_option_parser
|
||||||
from calibre.ebooks import ConversionError, DRMError
|
from calibre.ebooks import ConversionError, DRMError
|
||||||
from calibre.ebooks.lrf.html.convert_from import process_file as html_process_file
|
from calibre.ebooks.lrf.html.convert_from import process_file as html_process_file
|
||||||
from calibre.ebooks.metadata.opf import OPF
|
from calibre.ebooks.metadata.opf import OPF
|
||||||
from calibre.ebooks.metadata.epub import OCFDirReader
|
from calibre.ebooks.metadata.epub import OCFDirReader
|
||||||
from calibre.utils.zipfile import ZipFile
|
from calibre.utils.zipfile import ZipFile
|
||||||
from calibre import __appname__, setup_cli_handlers
|
from calibre import setup_cli_handlers
|
||||||
|
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||||
|
|
||||||
|
|
||||||
def option_parser():
|
def option_parser():
|
||||||
@ -22,17 +22,16 @@ _('''Usage: %prog [options] mybook.epub
|
|||||||
|
|
||||||
def generate_html(pathtoepub, logger):
|
def generate_html(pathtoepub, logger):
|
||||||
if not os.access(pathtoepub, os.R_OK):
|
if not os.access(pathtoepub, os.R_OK):
|
||||||
raise ConversionError, 'Cannot read from ' + pathtoepub
|
raise ConversionError('Cannot read from ' + pathtoepub)
|
||||||
tdir = mkdtemp(prefix=__appname__+'_')
|
tdir = PersistentTemporaryDirectory('_epub2lrf')
|
||||||
os.rmdir(tdir)
|
#os.rmdir(tdir)
|
||||||
try:
|
try:
|
||||||
ZipFile(pathtoepub).extractall(tdir)
|
ZipFile(pathtoepub).extractall(tdir)
|
||||||
|
except:
|
||||||
|
raise ConversionError, '.epub extraction failed'
|
||||||
if os.path.exists(os.path.join(tdir, 'META-INF', 'encryption.xml')):
|
if os.path.exists(os.path.join(tdir, 'META-INF', 'encryption.xml')):
|
||||||
raise DRMError(os.path.basename(pathtoepub))
|
raise DRMError(os.path.basename(pathtoepub))
|
||||||
except:
|
|
||||||
if os.path.exists(tdir) and os.path.isdir(tdir):
|
|
||||||
shutil.rmtree(tdir)
|
|
||||||
raise ConversionError, '.epub extraction failed'
|
|
||||||
return tdir
|
return tdir
|
||||||
|
|
||||||
def process_file(path, options, logger=None):
|
def process_file(path, options, logger=None):
|
||||||
|
@ -12,24 +12,62 @@ except ImportError:
|
|||||||
'''
|
'''
|
||||||
Default fonts used in the PRS500
|
Default fonts used in the PRS500
|
||||||
'''
|
'''
|
||||||
from calibre.ebooks.lrf.fonts.prs500 import tt0003m_, tt0011m_, tt0419m_
|
|
||||||
|
SYSTEM_FONT_PATH = '/usr/share/fonts/truetype/ttf-liberation/'
|
||||||
|
|
||||||
FONT_MAP = {
|
FONT_MAP = {
|
||||||
'Swis721 BT Roman' : tt0003m_,
|
'Swis721 BT Roman' : 'tt0003m_',
|
||||||
'Dutch801 Rm BT Roman' : tt0011m_,
|
'Dutch801 Rm BT Roman' : 'tt0011m_',
|
||||||
'Courier10 BT Roman' : tt0419m_,
|
'Courier10 BT Roman' : 'tt0419m_',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LIBERATION_FONT_MAP = {
|
||||||
|
'Swis721 BT Roman' : 'LiberationSans_Regular',
|
||||||
|
'Dutch801 Rm BT Roman' : 'LiberationSerif_Regular',
|
||||||
|
'Courier10 BT Roman' : 'LiberationMono_Regular',
|
||||||
|
}
|
||||||
|
|
||||||
|
SYSTEM_FONT_MAP = {}
|
||||||
|
for key, val in LIBERATION_FONT_MAP.items():
|
||||||
|
SYSTEM_FONT_MAP[key] = SYSTEM_FONT_PATH + val.replace('_', '-') + '.ttf'
|
||||||
|
|
||||||
FONT_FILE_MAP = {}
|
FONT_FILE_MAP = {}
|
||||||
|
|
||||||
def get_font_path(name):
|
def get_font_path(name):
|
||||||
if FONT_FILE_MAP.has_key(name) and os.access(FONT_FILE_MAP[name].name, os.R_OK):
|
if FONT_FILE_MAP.has_key(name) and os.access(FONT_FILE_MAP[name].name, os.R_OK):
|
||||||
return FONT_FILE_MAP[name].name
|
return FONT_FILE_MAP[name].name
|
||||||
|
|
||||||
|
# translate font into file name
|
||||||
|
fname = FONT_MAP[name]
|
||||||
|
|
||||||
|
# first, check configuration in /etc/
|
||||||
|
etc_file = os.path.join(os.path.sep, 'etc', 'calibre', 'fonts', fname + '.ttf')
|
||||||
|
if os.access(etc_file, os.R_OK):
|
||||||
|
return etc_file
|
||||||
|
|
||||||
|
# then, try calibre shipped ones
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
font_mod = __import__('calibre.ebooks.lrf.fonts.prs500', {}, {},
|
||||||
|
[fname], -1)
|
||||||
|
getattr(font_mod, fname)
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
font_mod = __import__('calibre.ebooks.lrf.fonts.liberation', {}, {},
|
||||||
|
[LIBERATION_FONT_MAP[name]], -1)
|
||||||
p = PersistentTemporaryFile('.ttf', 'font_')
|
p = PersistentTemporaryFile('.ttf', 'font_')
|
||||||
p.write(FONT_MAP[name].font_data)
|
p.write(getattr(font_mod, fname).font_data)
|
||||||
p.close()
|
p.close()
|
||||||
FONT_FILE_MAP[name] = p
|
FONT_FILE_MAP[name] = p
|
||||||
return p.name
|
return p.name
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# finally, try system default ones
|
||||||
|
if SYSTEM_FONT_MAP.has_key(name) and os.access(SYSTEM_FONT_MAP[name], os.R_OK):
|
||||||
|
return SYSTEM_FONT_MAP[name]
|
||||||
|
|
||||||
|
# not found
|
||||||
|
raise SystemError, 'font %s (in file %s) not installed' % (name, fname)
|
||||||
|
|
||||||
def get_font(name, size, encoding='unic'):
|
def get_font(name, size, encoding='unic'):
|
||||||
'''
|
'''
|
||||||
|
@ -245,7 +245,6 @@ class HTMLConverter(object, LoggingInterface):
|
|||||||
|
|
||||||
self.override_css = {}
|
self.override_css = {}
|
||||||
self.override_pcss = {}
|
self.override_pcss = {}
|
||||||
self.table_render_job_server = None
|
|
||||||
|
|
||||||
if self._override_css is not None:
|
if self._override_css is not None:
|
||||||
if os.access(self._override_css, os.R_OK):
|
if os.access(self._override_css, os.R_OK):
|
||||||
@ -266,7 +265,6 @@ class HTMLConverter(object, LoggingInterface):
|
|||||||
paths = [os.path.abspath(path) for path in paths]
|
paths = [os.path.abspath(path) for path in paths]
|
||||||
paths = [path.decode(sys.getfilesystemencoding()) if not isinstance(path, unicode) else path for path in paths]
|
paths = [path.decode(sys.getfilesystemencoding()) if not isinstance(path, unicode) else path for path in paths]
|
||||||
|
|
||||||
try:
|
|
||||||
while len(paths) > 0 and self.link_level <= self.link_levels:
|
while len(paths) > 0 and self.link_level <= self.link_levels:
|
||||||
for path in paths:
|
for path in paths:
|
||||||
if path in self.processed_files:
|
if path in self.processed_files:
|
||||||
@ -298,9 +296,6 @@ class HTMLConverter(object, LoggingInterface):
|
|||||||
if self.base_font_size > 0:
|
if self.base_font_size > 0:
|
||||||
self.log_info('\tRationalizing font sizes...')
|
self.log_info('\tRationalizing font sizes...')
|
||||||
self.book.rationalize_font_sizes(self.base_font_size)
|
self.book.rationalize_font_sizes(self.base_font_size)
|
||||||
finally:
|
|
||||||
if self.table_render_job_server is not None:
|
|
||||||
self.table_render_job_server.killall()
|
|
||||||
|
|
||||||
def is_baen(self, soup):
|
def is_baen(self, soup):
|
||||||
return bool(soup.find('meta', attrs={'name':'Publisher',
|
return bool(soup.find('meta', attrs={'name':'Publisher',
|
||||||
@ -1732,15 +1727,11 @@ class HTMLConverter(object, LoggingInterface):
|
|||||||
self.process_children(tag, tag_css, tag_pseudo_css)
|
self.process_children(tag, tag_css, tag_pseudo_css)
|
||||||
elif tagname == 'table' and not self.ignore_tables and not self.in_table:
|
elif tagname == 'table' and not self.ignore_tables and not self.in_table:
|
||||||
if self.render_tables_as_images:
|
if self.render_tables_as_images:
|
||||||
if self.table_render_job_server is None:
|
|
||||||
from calibre.parallel import Server
|
|
||||||
self.table_render_job_server = Server(number_of_workers=1)
|
|
||||||
print 'Rendering table...'
|
print 'Rendering table...'
|
||||||
from calibre.ebooks.lrf.html.table_as_image import render_table
|
from calibre.ebooks.lrf.html.table_as_image import render_table
|
||||||
pheight = int(self.current_page.pageStyle.attrs['textheight'])
|
pheight = int(self.current_page.pageStyle.attrs['textheight'])
|
||||||
pwidth = int(self.current_page.pageStyle.attrs['textwidth'])
|
pwidth = int(self.current_page.pageStyle.attrs['textwidth'])
|
||||||
images = render_table(self.table_render_job_server,
|
images = render_table(self.soup, tag, tag_css,
|
||||||
self.soup, tag, tag_css,
|
|
||||||
os.path.dirname(self.target_prefix),
|
os.path.dirname(self.target_prefix),
|
||||||
pwidth, pheight, self.profile.dpi,
|
pwidth, pheight, self.profile.dpi,
|
||||||
self.text_size_multiplier_for_rendered_tables)
|
self.text_size_multiplier_for_rendered_tables)
|
||||||
@ -1906,6 +1897,8 @@ def process_file(path, options, logger=None):
|
|||||||
fpb = re.compile(options.force_page_break, re.IGNORECASE) if options.force_page_break else \
|
fpb = re.compile(options.force_page_break, re.IGNORECASE) if options.force_page_break else \
|
||||||
re.compile('$')
|
re.compile('$')
|
||||||
cq = options.chapter_attr.split(',')
|
cq = options.chapter_attr.split(',')
|
||||||
|
if len(cq) < 3:
|
||||||
|
raise ValueError('The --chapter-attr setting must have 2 commas.')
|
||||||
options.chapter_attr = [re.compile(cq[0], re.IGNORECASE), cq[1],
|
options.chapter_attr = [re.compile(cq[0], re.IGNORECASE), cq[1],
|
||||||
re.compile(cq[2], re.IGNORECASE)]
|
re.compile(cq[2], re.IGNORECASE)]
|
||||||
options.force_page_break = fpb
|
options.force_page_break = fpb
|
||||||
@ -1933,7 +1926,7 @@ def process_file(path, options, logger=None):
|
|||||||
oname = os.path.abspath(os.path.expanduser(oname))
|
oname = os.path.abspath(os.path.expanduser(oname))
|
||||||
conv.writeto(oname, lrs=options.lrs)
|
conv.writeto(oname, lrs=options.lrs)
|
||||||
run_plugins_on_postprocess(oname, 'lrf')
|
run_plugins_on_postprocess(oname, 'lrf')
|
||||||
logger.info('Output written to %s', oname)
|
conv.log_info('Output written to %s', oname)
|
||||||
conv.cleanup()
|
conv.cleanup()
|
||||||
return oname
|
return oname
|
||||||
|
|
||||||
@ -1980,17 +1973,7 @@ def try_opf(path, options, logger):
|
|||||||
PILImage.open(cover)
|
PILImage.open(cover)
|
||||||
options.cover = cover
|
options.cover = cover
|
||||||
except:
|
except:
|
||||||
for prefix in opf.possible_cover_prefixes():
|
pass
|
||||||
if options.cover:
|
|
||||||
break
|
|
||||||
for suffix in ['.jpg', '.jpeg', '.gif', '.png', '.bmp']:
|
|
||||||
cpath = os.path.join(os.path.dirname(path), prefix+suffix)
|
|
||||||
try:
|
|
||||||
PILImage.open(cpath)
|
|
||||||
options.cover = cpath
|
|
||||||
break
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
if not getattr(options, 'cover', None) and orig_cover is not None:
|
if not getattr(options, 'cover', None) and orig_cover is not None:
|
||||||
options.cover = orig_cover
|
options.cover = orig_cover
|
||||||
if getattr(opf, 'spine', False):
|
if getattr(opf, 'spine', False):
|
||||||
|
@ -6,14 +6,11 @@ __docformat__ = 'restructuredtext en'
|
|||||||
'''
|
'''
|
||||||
Render HTML tables as images.
|
Render HTML tables as images.
|
||||||
'''
|
'''
|
||||||
import os, tempfile, atexit, shutil, time
|
import os, tempfile, atexit, shutil
|
||||||
from PyQt4.Qt import QUrl, QApplication, QSize, \
|
from PyQt4.Qt import QUrl, QApplication, QSize, QEventLoop, \
|
||||||
SIGNAL, QPainter, QImage, QObject, Qt
|
SIGNAL, QPainter, QImage, QObject, Qt
|
||||||
from PyQt4.QtWebKit import QWebPage
|
from PyQt4.QtWebKit import QWebPage
|
||||||
|
|
||||||
from calibre.parallel import ParallelJob
|
|
||||||
|
|
||||||
__app = None
|
|
||||||
|
|
||||||
class HTMLTableRenderer(QObject):
|
class HTMLTableRenderer(QObject):
|
||||||
|
|
||||||
@ -27,13 +24,15 @@ class HTMLTableRenderer(QObject):
|
|||||||
self.app = None
|
self.app = None
|
||||||
self.width, self.height, self.dpi = width, height, dpi
|
self.width, self.height, self.dpi = width, height, dpi
|
||||||
self.base_dir = base_dir
|
self.base_dir = base_dir
|
||||||
|
self.images = []
|
||||||
|
self.tdir = tempfile.mkdtemp(prefix='calibre_render_table')
|
||||||
|
self.loop = QEventLoop()
|
||||||
self.page = QWebPage()
|
self.page = QWebPage()
|
||||||
self.connect(self.page, SIGNAL('loadFinished(bool)'), self.render_html)
|
self.connect(self.page, SIGNAL('loadFinished(bool)'), self.render_html)
|
||||||
self.page.mainFrame().setTextSizeMultiplier(factor)
|
self.page.mainFrame().setTextSizeMultiplier(factor)
|
||||||
self.page.mainFrame().setHtml(html,
|
self.page.mainFrame().setHtml(html,
|
||||||
QUrl('file:'+os.path.abspath(self.base_dir)))
|
QUrl('file:'+os.path.abspath(self.base_dir)))
|
||||||
self.images = []
|
|
||||||
self.tdir = tempfile.mkdtemp(prefix='calibre_render_table')
|
|
||||||
|
|
||||||
def render_html(self, ok):
|
def render_html(self, ok):
|
||||||
try:
|
try:
|
||||||
@ -63,7 +62,7 @@ class HTMLTableRenderer(QObject):
|
|||||||
finally:
|
finally:
|
||||||
QApplication.quit()
|
QApplication.quit()
|
||||||
|
|
||||||
def render_table(server, soup, table, css, base_dir, width, height, dpi, factor=1.0):
|
def render_table(soup, table, css, base_dir, width, height, dpi, factor=1.0):
|
||||||
head = ''
|
head = ''
|
||||||
for e in soup.findAll(['link', 'style']):
|
for e in soup.findAll(['link', 'style']):
|
||||||
head += unicode(e)+'\n\n'
|
head += unicode(e)+'\n\n'
|
||||||
@ -83,24 +82,13 @@ def render_table(server, soup, table, css, base_dir, width, height, dpi, factor=
|
|||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
'''%(head, width-10, style, unicode(table))
|
'''%(head, width-10, style, unicode(table))
|
||||||
job = ParallelJob('render_table', lambda j : j, None,
|
images, tdir = do_render(html, base_dir, width, height, dpi, factor)
|
||||||
args=[html, base_dir, width, height, dpi, factor])
|
|
||||||
server.add_job(job)
|
|
||||||
while not job.has_run:
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
if job.exception is not None:
|
|
||||||
print 'Failed to render table'
|
|
||||||
print job.exception
|
|
||||||
print job.traceback
|
|
||||||
images, tdir = job.result
|
|
||||||
atexit.register(shutil.rmtree, tdir)
|
atexit.register(shutil.rmtree, tdir)
|
||||||
return images
|
return images
|
||||||
|
|
||||||
def do_render(html, base_dir, width, height, dpi, factor):
|
def do_render(html, base_dir, width, height, dpi, factor):
|
||||||
app = QApplication.instance()
|
if QApplication.instance() is None:
|
||||||
if app is None:
|
QApplication([])
|
||||||
app = QApplication([])
|
|
||||||
tr = HTMLTableRenderer(html, base_dir, width, height, dpi, factor)
|
tr = HTMLTableRenderer(html, base_dir, width, height, dpi, factor)
|
||||||
app.exec_()
|
tr.loop.exec_()
|
||||||
return tr.images, tr.tdir
|
return tr.images, tr.tdir
|
@ -77,7 +77,7 @@ class LRFDocument(LRFMetaFile):
|
|||||||
for obj in self.image_map.values() + self.font_map.values():
|
for obj in self.image_map.values() + self.font_map.values():
|
||||||
open(obj.file, 'wb').write(obj.stream)
|
open(obj.file, 'wb').write(obj.stream)
|
||||||
|
|
||||||
def to_xml(self):
|
def to_xml(self, write_files=True):
|
||||||
bookinfo = u'<BookInformation>\n<Info version="1.1">\n<BookInfo>\n'
|
bookinfo = u'<BookInformation>\n<Info version="1.1">\n<BookInfo>\n'
|
||||||
bookinfo += u'<Title reading="%s">%s</Title>\n'%(self.metadata.title_reading, self.metadata.title)
|
bookinfo += u'<Title reading="%s">%s</Title>\n'%(self.metadata.title_reading, self.metadata.title)
|
||||||
bookinfo += u'<Author reading="%s">%s</Author>\n'%(self.metadata.author_reading, self.metadata.author)
|
bookinfo += u'<Author reading="%s">%s</Author>\n'%(self.metadata.author_reading, self.metadata.author)
|
||||||
@ -89,8 +89,9 @@ class LRFDocument(LRFMetaFile):
|
|||||||
bookinfo += u'<FreeText reading="">%s</FreeText>\n</BookInfo>\n<DocInfo>\n'%(self.metadata.free_text,)
|
bookinfo += u'<FreeText reading="">%s</FreeText>\n</BookInfo>\n<DocInfo>\n'%(self.metadata.free_text,)
|
||||||
th = self.doc_info.thumbnail
|
th = self.doc_info.thumbnail
|
||||||
if th:
|
if th:
|
||||||
prefix = sanitize_file_name(self.metadata.title)
|
prefix = sanitize_file_name(self.metadata.title, as_unicode=True)
|
||||||
bookinfo += u'<CThumbnail file="%s" />\n'%(prefix+'_thumbnail.'+self.doc_info.thumbnail_extension,)
|
bookinfo += u'<CThumbnail file="%s" />\n'%(prefix+'_thumbnail.'+self.doc_info.thumbnail_extension,)
|
||||||
|
if write_files:
|
||||||
open(prefix+'_thumbnail.'+self.doc_info.thumbnail_extension, 'wb').write(th)
|
open(prefix+'_thumbnail.'+self.doc_info.thumbnail_extension, 'wb').write(th)
|
||||||
bookinfo += u'<Language reading="">%s</Language>\n'%(self.doc_info.language,)
|
bookinfo += u'<Language reading="">%s</Language>\n'%(self.doc_info.language,)
|
||||||
bookinfo += u'<Creator reading="">%s</Creator>\n'%(self.doc_info.creator,)
|
bookinfo += u'<Creator reading="">%s</Creator>\n'%(self.doc_info.creator,)
|
||||||
@ -127,12 +128,16 @@ class LRFDocument(LRFMetaFile):
|
|||||||
objects += unicode(obj)
|
objects += unicode(obj)
|
||||||
styles += '</Style>\n'
|
styles += '</Style>\n'
|
||||||
objects += '</Objects>\n'
|
objects += '</Objects>\n'
|
||||||
|
if write_files:
|
||||||
self.write_files()
|
self.write_files()
|
||||||
return '<BBeBXylog version="1.0">\n' + bookinfo + pages + styles + objects + '</BBeBXylog>'
|
return '<BBeBXylog version="1.0">\n' + bookinfo + pages + styles + objects + '</BBeBXylog>'
|
||||||
|
|
||||||
def option_parser():
|
def option_parser():
|
||||||
parser = OptionParser(usage=_('%prog book.lrf\nConvert an LRF file into an LRS (XML UTF-8 encoded) file'))
|
parser = OptionParser(usage=_('%prog book.lrf\nConvert an LRF file into an LRS (XML UTF-8 encoded) file'))
|
||||||
parser.add_option('--output', '-o', default=None, help=_('Output LRS file'), dest='out')
|
parser.add_option('--output', '-o', default=None, help=_('Output LRS file'), dest='out')
|
||||||
|
parser.add_option('--dont-output-resources', default=True, action='store_false',
|
||||||
|
help=_('Do not save embedded image and font files to disk'),
|
||||||
|
dest='output_resources')
|
||||||
parser.add_option('--verbose', default=False, action='store_true', dest='verbose')
|
parser.add_option('--verbose', default=False, action='store_true', dest='verbose')
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
@ -154,7 +159,7 @@ def main(args=sys.argv, logger=None):
|
|||||||
d = LRFDocument(open(args[1], 'rb'))
|
d = LRFDocument(open(args[1], 'rb'))
|
||||||
d.parse()
|
d.parse()
|
||||||
logger.info(_('Creating XML...'))
|
logger.info(_('Creating XML...'))
|
||||||
o.write(d.to_xml())
|
o.write(d.to_xml(write_files=opts.output_resources))
|
||||||
logger.info(_('LRS written to ')+opts.out)
|
logger.info(_('LRS written to ')+opts.out)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
@ -603,12 +603,18 @@ Show/edit the metadata in an LRF file.\n\n'''),
|
|||||||
parser.add_option("--get-thumbnail", action="store_true", \
|
parser.add_option("--get-thumbnail", action="store_true", \
|
||||||
dest="get_thumbnail", default=False, \
|
dest="get_thumbnail", default=False, \
|
||||||
help=_("Extract thumbnail from LRF file"))
|
help=_("Extract thumbnail from LRF file"))
|
||||||
|
parser.add_option('--publisher', default=None, help=_('Set the publisher'))
|
||||||
|
parser.add_option('--classification', default=None, help=_('Set the book classification'))
|
||||||
|
parser.add_option('--creator', default=None, help=_('Set the book creator'))
|
||||||
|
parser.add_option('--producer', default=None, help=_('Set the book producer'))
|
||||||
parser.add_option('--get-cover', action='store_true', default=False,
|
parser.add_option('--get-cover', action='store_true', default=False,
|
||||||
help=_('Extract cover from LRF file. Note that the LRF format has no defined cover, so we use some heuristics to guess the cover.'))
|
help=_('Extract cover from LRF file. Note that the LRF format has no defined cover, so we use some heuristics to guess the cover.'))
|
||||||
parser.add_option('--bookid', action='store', type='string', default=None,
|
parser.add_option('--bookid', action='store', type='string', default=None,
|
||||||
dest='book_id', help=_('Set book ID'))
|
dest='book_id', help=_('Set book ID'))
|
||||||
parser.add_option("-p", "--page", action="store", type="string", \
|
# The SumPage element specifies the number of "View"s (visible pages for the BookSetting element conditions) of the content.
|
||||||
dest="page", help=_("Don't know what this is for"))
|
# Basically, the total pages per the page size, font size, etc. when the LRF is first created. Since this will change as the book is reflowed, it is probably not worth using.
|
||||||
|
#parser.add_option("-p", "--page", action="store", type="string", \
|
||||||
|
# dest="page", help=_("Don't know what this is for"))
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
@ -624,6 +630,8 @@ def set_metadata(stream, mi):
|
|||||||
lrf.free_text = mi.comments
|
lrf.free_text = mi.comments
|
||||||
if mi.author_sort:
|
if mi.author_sort:
|
||||||
lrf.author_reading = mi.author_sort
|
lrf.author_reading = mi.author_sort
|
||||||
|
if mi.publisher:
|
||||||
|
lrf.publisher = mi.publisher
|
||||||
|
|
||||||
|
|
||||||
def main(args=sys.argv):
|
def main(args=sys.argv):
|
||||||
@ -644,10 +652,16 @@ def main(args=sys.argv):
|
|||||||
lrf.author_reading = options.author_reading
|
lrf.author_reading = options.author_reading
|
||||||
if options.author:
|
if options.author:
|
||||||
lrf.author = options.author
|
lrf.author = options.author
|
||||||
|
if options.publisher:
|
||||||
|
lrf.publisher = options.publisher
|
||||||
|
if options.classification:
|
||||||
|
lrf.classification = options.classification
|
||||||
if options.category:
|
if options.category:
|
||||||
lrf.category = options.category
|
lrf.category = options.category
|
||||||
if options.page:
|
if options.creator:
|
||||||
lrf.page = options.page
|
lrf.creator = options.creator
|
||||||
|
if options.producer:
|
||||||
|
lrf.producer = options.producer
|
||||||
if options.thumbnail:
|
if options.thumbnail:
|
||||||
path = os.path.expanduser(os.path.expandvars(options.thumbnail))
|
path = os.path.expanduser(os.path.expandvars(options.thumbnail))
|
||||||
f = open(path, "rb")
|
f = open(path, "rb")
|
||||||
|
@ -10,9 +10,10 @@ Try to read metadata from an HTML file.
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
|
from calibre.ebooks.chardet import xml_to_unicode
|
||||||
|
|
||||||
def get_metadata(stream):
|
def get_metadata(stream):
|
||||||
src = stream.read()
|
src = xml_to_unicode(stream.read())[0]
|
||||||
|
|
||||||
# Title
|
# Title
|
||||||
title = None
|
title = None
|
||||||
|
@ -418,7 +418,8 @@ class OPF(object):
|
|||||||
tags_path = XPath('descendant::*[re:match(name(), "subject", "i")]')
|
tags_path = XPath('descendant::*[re:match(name(), "subject", "i")]')
|
||||||
isbn_path = XPath('descendant::*[re:match(name(), "identifier", "i") and '+
|
isbn_path = XPath('descendant::*[re:match(name(), "identifier", "i") and '+
|
||||||
'(re:match(@scheme, "isbn", "i") or re:match(@opf:scheme, "isbn", "i"))]')
|
'(re:match(@scheme, "isbn", "i") or re:match(@opf:scheme, "isbn", "i"))]')
|
||||||
application_id_path= XPath('descendant::*[re:match(name(), "identifier", "i") and '+
|
identifier_path = XPath('descendant::*[re:match(name(), "identifier", "i")]')
|
||||||
|
application_id_path = XPath('descendant::*[re:match(name(), "identifier", "i") and '+
|
||||||
'(re:match(@opf:scheme, "calibre|libprs500", "i") or re:match(@scheme, "calibre|libprs500", "i"))]')
|
'(re:match(@opf:scheme, "calibre|libprs500", "i") or re:match(@scheme, "calibre|libprs500", "i"))]')
|
||||||
manifest_path = XPath('descendant::*[re:match(name(), "manifest", "i")]/*[re:match(name(), "item", "i")]')
|
manifest_path = XPath('descendant::*[re:match(name(), "manifest", "i")]/*[re:match(name(), "item", "i")]')
|
||||||
manifest_ppath = XPath('descendant::*[re:match(name(), "manifest", "i")]')
|
manifest_ppath = XPath('descendant::*[re:match(name(), "manifest", "i")]')
|
||||||
@ -719,6 +720,27 @@ class OPF(object):
|
|||||||
return property(fget=fget, fset=fset)
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
|
||||||
|
def guess_cover(self):
|
||||||
|
'''
|
||||||
|
Try to guess a cover. Needed for some old/badly formed OPF files.
|
||||||
|
'''
|
||||||
|
if self.base_dir and os.path.exists(self.base_dir):
|
||||||
|
for item in self.identifier_path(self.metadata):
|
||||||
|
scheme = None
|
||||||
|
for key in item.attrib.keys():
|
||||||
|
if key.endswith('scheme'):
|
||||||
|
scheme = item.get(key)
|
||||||
|
break
|
||||||
|
if scheme is None:
|
||||||
|
continue
|
||||||
|
if item.text:
|
||||||
|
prefix = item.text.replace('-', '')
|
||||||
|
for suffix in ['.jpg', '.jpeg', '.gif', '.png', '.bmp']:
|
||||||
|
cpath = os.access(os.path.join(self.base_dir, prefix+suffix), os.R_OK)
|
||||||
|
if os.access(os.path.join(self.base_dir, prefix+suffix), os.R_OK):
|
||||||
|
return cpath
|
||||||
|
|
||||||
|
|
||||||
@apply
|
@apply
|
||||||
def cover():
|
def cover():
|
||||||
|
|
||||||
@ -728,6 +750,10 @@ class OPF(object):
|
|||||||
for item in self.guide:
|
for item in self.guide:
|
||||||
if item.type.lower() == t:
|
if item.type.lower() == t:
|
||||||
return item.path
|
return item.path
|
||||||
|
try:
|
||||||
|
return self.guess_cover()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
def fset(self, path):
|
def fset(self, path):
|
||||||
if self.guide is not None:
|
if self.guide is not None:
|
||||||
|
@ -305,8 +305,8 @@ IANA_MOBI = \
|
|||||||
'TW': (4, 4)},
|
'TW': (4, 4)},
|
||||||
'zu': {None: (53, 0)}}
|
'zu': {None: (53, 0)}}
|
||||||
|
|
||||||
def iana2mobi(self, icode):
|
def iana2mobi(icode):
|
||||||
subtags = list(code.split('-'))
|
subtags = list(icode.split('-'))
|
||||||
langdict = IANA_MOBI[None]
|
langdict = IANA_MOBI[None]
|
||||||
while len(subtags) > 0:
|
while len(subtags) > 0:
|
||||||
lang = subtags.pop(0).lower()
|
lang = subtags.pop(0).lower()
|
||||||
@ -316,6 +316,8 @@ def iana2mobi(self, icode):
|
|||||||
mcode = langdict[None]
|
mcode = langdict[None]
|
||||||
while len(subtags) > 0:
|
while len(subtags) > 0:
|
||||||
subtag = subtags.pop(0)
|
subtag = subtags.pop(0)
|
||||||
|
if subtag not in langdict:
|
||||||
|
subtag = subtag.title()
|
||||||
if subtag not in langdict:
|
if subtag not in langdict:
|
||||||
subtag = subtag.upper()
|
subtag = subtag.upper()
|
||||||
if subtag in langdict:
|
if subtag in langdict:
|
||||||
|
379
src/calibre/ebooks/mobi/mobiml.py
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
'''
|
||||||
|
Transform XHTML/OPS-ish content into Mobipocket HTML 3.2.
|
||||||
|
'''
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.cam>'
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import copy
|
||||||
|
import re
|
||||||
|
from lxml import etree
|
||||||
|
from calibre.ebooks.oeb.base import namespace, barename
|
||||||
|
from calibre.ebooks.oeb.base import XHTML, XHTML_NS
|
||||||
|
from calibre.ebooks.oeb.stylizer import Stylizer
|
||||||
|
from calibre.ebooks.oeb.transforms.flatcss import KeyMapper
|
||||||
|
|
||||||
|
MBP_NS = 'http://mobipocket.com/ns/mbp'
|
||||||
|
def MBP(name): return '{%s}%s' % (MBP_NS, name)
|
||||||
|
|
||||||
|
MOBI_NSMAP = {None: XHTML_NS, 'mbp': MBP_NS}
|
||||||
|
|
||||||
|
HEADER_TAGS = set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
|
||||||
|
NESTABLE_TAGS = set(['ol', 'ul', 'li', 'table', 'tr', 'td', 'th'])
|
||||||
|
TABLE_TAGS = set(['table', 'tr', 'td', 'th'])
|
||||||
|
SPECIAL_TAGS = set(['hr', 'br'])
|
||||||
|
CONTENT_TAGS = set(['img', 'hr', 'br'])
|
||||||
|
|
||||||
|
PAGE_BREAKS = set(['always', 'odd', 'even'])
|
||||||
|
|
||||||
|
COLLAPSE = re.compile(r'[ \t\r\n\v]+')
|
||||||
|
|
||||||
|
def asfloat(value):
|
||||||
|
if not isinstance(value, (int, long, float)):
|
||||||
|
return 0.0
|
||||||
|
return float(value)
|
||||||
|
|
||||||
|
class BlockState(object):
|
||||||
|
def __init__(self, body):
|
||||||
|
self.body = body
|
||||||
|
self.nested = []
|
||||||
|
self.para = None
|
||||||
|
self.inline = None
|
||||||
|
self.anchor = None
|
||||||
|
self.vpadding = 0.
|
||||||
|
self.vmargin = 0.
|
||||||
|
self.pbreak = False
|
||||||
|
self.istate = None
|
||||||
|
self.content = False
|
||||||
|
|
||||||
|
class FormatState(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.left = 0.
|
||||||
|
self.halign = 'auto'
|
||||||
|
self.indent = 0.
|
||||||
|
self.fsize = 3
|
||||||
|
self.ids = set()
|
||||||
|
self.valign = 'baseline'
|
||||||
|
self.italic = False
|
||||||
|
self.bold = False
|
||||||
|
self.preserve = False
|
||||||
|
self.family = 'serif'
|
||||||
|
self.href = None
|
||||||
|
self.list_num = 0
|
||||||
|
self.attrib = {}
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.fsize == other.fsize \
|
||||||
|
and self.italic == other.italic \
|
||||||
|
and self.bold == other.bold \
|
||||||
|
and self.href == other.href \
|
||||||
|
and self.valign == other.valign \
|
||||||
|
and self.preserve == other.preserve \
|
||||||
|
and self.family == other.family
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
|
||||||
|
class MobiMLizer(object):
|
||||||
|
def transform(self, oeb, context):
|
||||||
|
oeb.logger.info('Converting XHTML to Mobipocket markup...')
|
||||||
|
self.oeb = oeb
|
||||||
|
self.profile = profile = context.dest
|
||||||
|
self.fnums = fnums = dict((v, k) for k, v in profile.fnums.items())
|
||||||
|
self.fmap = KeyMapper(profile.fbase, profile.fbase, fnums.keys())
|
||||||
|
self.remove_html_cover()
|
||||||
|
self.mobimlize_spine()
|
||||||
|
|
||||||
|
def remove_html_cover(self):
|
||||||
|
oeb = self.oeb
|
||||||
|
if not oeb.metadata.cover \
|
||||||
|
or 'cover' not in oeb.guide:
|
||||||
|
return
|
||||||
|
href = oeb.guide['cover'].href
|
||||||
|
del oeb.guide['cover']
|
||||||
|
item = oeb.manifest.hrefs[href]
|
||||||
|
oeb.manifest.remove(item)
|
||||||
|
|
||||||
|
def mobimlize_spine(self):
|
||||||
|
for item in self.oeb.spine:
|
||||||
|
stylizer = Stylizer(item.data, item.href, self.oeb, self.profile)
|
||||||
|
body = item.data.find(XHTML('body'))
|
||||||
|
nroot = etree.Element(XHTML('html'), nsmap=MOBI_NSMAP)
|
||||||
|
nbody = etree.SubElement(nroot, XHTML('body'))
|
||||||
|
self.mobimlize_elem(body, stylizer, BlockState(nbody),
|
||||||
|
[FormatState()])
|
||||||
|
item.data = nroot
|
||||||
|
|
||||||
|
def mobimlize_font(self, ptsize):
|
||||||
|
return self.fnums[self.fmap[ptsize]]
|
||||||
|
|
||||||
|
def mobimlize_measure(self, ptsize):
|
||||||
|
if isinstance(ptsize, basestring):
|
||||||
|
return ptsize
|
||||||
|
fbase = self.profile.fbase
|
||||||
|
if ptsize < fbase:
|
||||||
|
return "%dpt" % int(round(ptsize))
|
||||||
|
return "%dem" % int(round(ptsize / fbase))
|
||||||
|
|
||||||
|
def preize_text(self, text):
|
||||||
|
text = unicode(text).replace(u' ', u'\xa0')
|
||||||
|
text = text.replace('\r\n', '\n')
|
||||||
|
text = text.replace('\r', '\n')
|
||||||
|
lines = text.split('\n')
|
||||||
|
result = lines[:1]
|
||||||
|
for line in lines[1:]:
|
||||||
|
result.append(etree.Element(XHTML('br')))
|
||||||
|
if line:
|
||||||
|
result.append(line)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def mobimlize_content(self, tag, text, bstate, istates):
|
||||||
|
bstate.content = True
|
||||||
|
istate = istates[-1]
|
||||||
|
para = bstate.para
|
||||||
|
if tag in SPECIAL_TAGS and not text:
|
||||||
|
para = para if para is not None else bstate.body
|
||||||
|
elif para is None:
|
||||||
|
body = bstate.body
|
||||||
|
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
|
||||||
|
indent = istate.indent
|
||||||
|
left = istate.left
|
||||||
|
if indent < 0 and abs(indent) < left:
|
||||||
|
left += indent
|
||||||
|
indent = 0
|
||||||
|
elif indent != 0 and abs(indent) < self.profile.fbase:
|
||||||
|
indent = (indent / abs(indent)) * self.profile.fbase
|
||||||
|
if tag in NESTABLE_TAGS:
|
||||||
|
para = wrapper = etree.SubElement(parent, XHTML(tag))
|
||||||
|
bstate.nested.append(para)
|
||||||
|
if tag == 'li' and len(istates) > 1:
|
||||||
|
istates[-2].list_num += 1
|
||||||
|
para.attrib['value'] = str(istates[-2].list_num)
|
||||||
|
elif left > 0 and indent >= 0:
|
||||||
|
para = wrapper = etree.SubElement(parent, XHTML('blockquote'))
|
||||||
|
para = wrapper
|
||||||
|
emleft = int(round(left / self.profile.fbase)) - 1
|
||||||
|
emleft = min((emleft, 10))
|
||||||
|
while emleft > 0:
|
||||||
|
para = etree.SubElement(para, XHTML('blockquote'))
|
||||||
|
emleft -= 1
|
||||||
|
else:
|
||||||
|
ptag = tag if tag in HEADER_TAGS else 'p'
|
||||||
|
para = wrapper = etree.SubElement(parent, XHTML(ptag))
|
||||||
|
bstate.inline = bstate.para = para
|
||||||
|
vspace = bstate.vpadding + bstate.vmargin
|
||||||
|
bstate.vpadding = bstate.vmargin = 0
|
||||||
|
if tag not in TABLE_TAGS:
|
||||||
|
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')))
|
||||||
|
vspace -= 1
|
||||||
|
if istate.halign != 'auto':
|
||||||
|
para.attrib['align'] = istate.halign
|
||||||
|
pstate = bstate.istate
|
||||||
|
if tag in CONTENT_TAGS:
|
||||||
|
bstate.inline = para
|
||||||
|
pstate = bstate.istate = None
|
||||||
|
etree.SubElement(para, XHTML(tag), attrib=istate.attrib)
|
||||||
|
elif tag in TABLE_TAGS:
|
||||||
|
para.attrib['valign'] = 'top'
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
if not pstate or istate != pstate:
|
||||||
|
inline = para
|
||||||
|
valign = istate.valign
|
||||||
|
fsize = istate.fsize
|
||||||
|
href = istate.href
|
||||||
|
if not href:
|
||||||
|
bstate.anchor = None
|
||||||
|
elif pstate and pstate.href == href:
|
||||||
|
inline = bstate.anchor
|
||||||
|
else:
|
||||||
|
inline = etree.SubElement(inline, XHTML('a'), href=href)
|
||||||
|
bstate.anchor = inline
|
||||||
|
if valign == 'super':
|
||||||
|
inline = etree.SubElement(inline, XHTML('sup'))
|
||||||
|
elif valign == 'sub':
|
||||||
|
inline = etree.SubElement(inline, XHTML('sub'))
|
||||||
|
if istate.family == 'monospace':
|
||||||
|
inline = etree.SubElement(inline, XHTML('tt'))
|
||||||
|
if fsize != 3:
|
||||||
|
inline = etree.SubElement(inline, XHTML('font'),
|
||||||
|
size=str(fsize))
|
||||||
|
if istate.italic:
|
||||||
|
inline = etree.SubElement(inline, XHTML('i'))
|
||||||
|
if istate.bold:
|
||||||
|
inline = etree.SubElement(inline, XHTML('b'))
|
||||||
|
bstate.inline = inline
|
||||||
|
bstate.istate = istate
|
||||||
|
inline = bstate.inline
|
||||||
|
content = self.preize_text(text) if istate.preserve else [text]
|
||||||
|
for item in content:
|
||||||
|
if isinstance(item, basestring):
|
||||||
|
if len(inline) == 0:
|
||||||
|
inline.text = (inline.text or '') + item
|
||||||
|
else:
|
||||||
|
last = inline[-1]
|
||||||
|
last.tail = (last.tail or '') + item
|
||||||
|
else:
|
||||||
|
inline.append(item)
|
||||||
|
|
||||||
|
def mobimlize_elem(self, elem, stylizer, bstate, istates):
|
||||||
|
if not isinstance(elem.tag, basestring) \
|
||||||
|
or namespace(elem.tag) != XHTML_NS:
|
||||||
|
return
|
||||||
|
style = stylizer.style(elem)
|
||||||
|
if style['display'] == 'none' \
|
||||||
|
or style['visibility'] == 'hidden':
|
||||||
|
return
|
||||||
|
tag = barename(elem.tag)
|
||||||
|
istate = copy.copy(istates[-1])
|
||||||
|
istate.list_num = 0
|
||||||
|
istates.append(istate)
|
||||||
|
left = 0
|
||||||
|
display = style['display']
|
||||||
|
isblock = not display.startswith('inline')
|
||||||
|
isblock = isblock and style['float'] == 'none'
|
||||||
|
isblock = isblock and tag != 'br'
|
||||||
|
if isblock:
|
||||||
|
bstate.para = None
|
||||||
|
istate.halign = style['text-align']
|
||||||
|
istate.indent = style['text-indent']
|
||||||
|
if style['margin-left'] == 'auto' \
|
||||||
|
and style['margin-right'] == 'auto':
|
||||||
|
istate.halign = 'center'
|
||||||
|
margin = asfloat(style['margin-left'])
|
||||||
|
padding = asfloat(style['padding-left'])
|
||||||
|
if tag != 'body':
|
||||||
|
left = margin + padding
|
||||||
|
istate.left += left
|
||||||
|
vmargin = asfloat(style['margin-top'])
|
||||||
|
bstate.vmargin = max((bstate.vmargin, vmargin))
|
||||||
|
vpadding = asfloat(style['padding-top'])
|
||||||
|
if vpadding > 0:
|
||||||
|
bstate.vpadding += bstate.vmargin
|
||||||
|
bstate.vmargin = 0
|
||||||
|
bstate.vpadding += vpadding
|
||||||
|
elif not istate.href:
|
||||||
|
margin = asfloat(style['margin-left'])
|
||||||
|
padding = asfloat(style['padding-left'])
|
||||||
|
lspace = margin + padding
|
||||||
|
if lspace > 0:
|
||||||
|
spaces = int(round((lspace * 3) / style['font-size']))
|
||||||
|
elem.text = (u'\xa0' * spaces) + (elem.text or '')
|
||||||
|
margin = asfloat(style['margin-right'])
|
||||||
|
padding = asfloat(style['padding-right'])
|
||||||
|
rspace = margin + padding
|
||||||
|
if rspace > 0:
|
||||||
|
spaces = int(round((rspace * 3) / style['font-size']))
|
||||||
|
if len(elem) == 0:
|
||||||
|
elem.text = (elem.text or '') + (u'\xa0' * spaces)
|
||||||
|
else:
|
||||||
|
last = elem[-1]
|
||||||
|
last.text = (last.text or '') + (u'\xa0' * spaces)
|
||||||
|
if bstate.content and style['page-break-before'] in PAGE_BREAKS:
|
||||||
|
bstate.pbreak = True
|
||||||
|
istate.fsize = self.mobimlize_font(style['font-size'])
|
||||||
|
istate.italic = True if style['font-style'] == 'italic' else False
|
||||||
|
weight = style['font-weight']
|
||||||
|
istate.bold = weight in ('bold', 'bolder') or asfloat(weight) > 400
|
||||||
|
istate.preserve = (style['white-space'] in ('pre', 'pre-wrap'))
|
||||||
|
if 'monospace' in style['font-family']:
|
||||||
|
istate.family = 'monospace'
|
||||||
|
elif 'sans-serif' in style['font-family']:
|
||||||
|
istate.family = 'sans-serif'
|
||||||
|
else:
|
||||||
|
istate.family = 'serif'
|
||||||
|
valign = style['vertical-align']
|
||||||
|
if valign in ('super', 'sup') or asfloat(valign) > 0:
|
||||||
|
istate.valign = 'super'
|
||||||
|
elif valign == 'sub' or asfloat(valign) < 0:
|
||||||
|
istate.valign = 'sub'
|
||||||
|
else:
|
||||||
|
istate.valign = 'baseline'
|
||||||
|
if 'id' in elem.attrib:
|
||||||
|
istate.ids.add(elem.attrib['id'])
|
||||||
|
if 'name' in elem.attrib:
|
||||||
|
istate.ids.add(elem.attrib['name'])
|
||||||
|
if tag == 'a' and 'href' in elem.attrib:
|
||||||
|
istate.href = elem.attrib['href']
|
||||||
|
istate.attrib.clear()
|
||||||
|
if tag == 'img' and 'src' in elem.attrib:
|
||||||
|
istate.attrib['src'] = elem.attrib['src']
|
||||||
|
istate.attrib['align'] = 'baseline'
|
||||||
|
for prop in ('width', 'height'):
|
||||||
|
if style[prop] != 'auto':
|
||||||
|
value = style[prop]
|
||||||
|
if value == getattr(self.profile, prop):
|
||||||
|
result = '100%'
|
||||||
|
else:
|
||||||
|
ems = int(round(value / self.profile.fbase))
|
||||||
|
result = "%dem" % ems
|
||||||
|
istate.attrib[prop] = result
|
||||||
|
elif tag == 'hr' and asfloat(style['width']) > 0:
|
||||||
|
prop = style['width'] / self.profile.width
|
||||||
|
istate.attrib['width'] = "%d%%" % int(round(prop * 100))
|
||||||
|
elif display == 'table':
|
||||||
|
tag = 'table'
|
||||||
|
elif display == 'table-row':
|
||||||
|
tag = 'tr'
|
||||||
|
elif display == 'table-cell':
|
||||||
|
tag = 'td'
|
||||||
|
text = None
|
||||||
|
if elem.text:
|
||||||
|
if istate.preserve:
|
||||||
|
text = elem.text
|
||||||
|
elif len(elem) > 0 and elem.text.isspace():
|
||||||
|
text = None
|
||||||
|
else:
|
||||||
|
text = COLLAPSE.sub(' ', elem.text)
|
||||||
|
if text or tag in CONTENT_TAGS or tag in NESTABLE_TAGS:
|
||||||
|
self.mobimlize_content(tag, text, bstate, istates)
|
||||||
|
for child in elem:
|
||||||
|
self.mobimlize_elem(child, stylizer, bstate, istates)
|
||||||
|
tail = None
|
||||||
|
if child.tail:
|
||||||
|
if istate.preserve:
|
||||||
|
tail = child.tail
|
||||||
|
elif bstate.para is None and child.tail.isspace():
|
||||||
|
tail = None
|
||||||
|
else:
|
||||||
|
tail = COLLAPSE.sub(' ', child.tail)
|
||||||
|
if tail:
|
||||||
|
self.mobimlize_content(tag, tail, bstate, istates)
|
||||||
|
if bstate.content and style['page-break-after'] in PAGE_BREAKS:
|
||||||
|
bstate.pbreak = True
|
||||||
|
if isblock:
|
||||||
|
para = bstate.para
|
||||||
|
if para is not None and para.text == u'\xa0':
|
||||||
|
para.getparent().replace(para, etree.Element(XHTML('br')))
|
||||||
|
bstate.para = None
|
||||||
|
bstate.istate = None
|
||||||
|
vmargin = asfloat(style['margin-bottom'])
|
||||||
|
bstate.vmargin = max((bstate.vmargin, vmargin))
|
||||||
|
vpadding = asfloat(style['padding-bottom'])
|
||||||
|
if vpadding > 0:
|
||||||
|
bstate.vpadding += bstate.vmargin
|
||||||
|
bstate.vmargin = 0
|
||||||
|
bstate.vpadding += vpadding
|
||||||
|
if tag in NESTABLE_TAGS and bstate.nested:
|
||||||
|
bstate.nested.pop()
|
||||||
|
istates.pop()
|
@ -74,12 +74,13 @@ def compress_doc(data):
|
|||||||
else:
|
else:
|
||||||
j = i
|
j = i
|
||||||
binseq = [ch]
|
binseq = [ch]
|
||||||
while True:
|
while j < ldata:
|
||||||
ch = data[j]
|
ch = data[j]
|
||||||
och = ord(ch)
|
och = ord(ch)
|
||||||
if och < 1 or (och > 8 and och < 0x80):
|
if och < 1 or (och > 8 and och < 0x80):
|
||||||
break
|
break
|
||||||
binseq.append(ch)
|
binseq.append(ch)
|
||||||
|
j += 1
|
||||||
out.write(pack('>B', len(binseq)))
|
out.write(pack('>B', len(binseq)))
|
||||||
out.write(''.join(binseq))
|
out.write(''.join(binseq))
|
||||||
i += len(binseq) - 1
|
i += len(binseq) - 1
|
||||||
|
@ -22,6 +22,7 @@ from calibre.ebooks.mobi.langcodes import main_language, sub_language
|
|||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
from calibre.ebooks.metadata.opf import OPFCreator
|
from calibre.ebooks.metadata.opf import OPFCreator
|
||||||
from calibre.ebooks.metadata.toc import TOC
|
from calibre.ebooks.metadata.toc import TOC
|
||||||
|
from calibre import sanitize_file_name
|
||||||
|
|
||||||
class EXTHHeader(object):
|
class EXTHHeader(object):
|
||||||
|
|
||||||
@ -200,7 +201,8 @@ class MobiReader(object):
|
|||||||
guide = soup.find('guide')
|
guide = soup.find('guide')
|
||||||
for elem in soup.findAll(['metadata', 'guide']):
|
for elem in soup.findAll(['metadata', 'guide']):
|
||||||
elem.extract()
|
elem.extract()
|
||||||
htmlfile = os.path.join(output_dir, self.name+'.html')
|
htmlfile = os.path.join(output_dir,
|
||||||
|
sanitize_file_name(self.name)+'.html')
|
||||||
try:
|
try:
|
||||||
for ref in guide.findAll('reference', href=True):
|
for ref in guide.findAll('reference', href=True):
|
||||||
ref['href'] = os.path.basename(htmlfile)+ref['href']
|
ref['href'] = os.path.basename(htmlfile)+ref['href']
|
||||||
@ -232,6 +234,15 @@ class MobiReader(object):
|
|||||||
def cleanup_soup(self, soup):
|
def cleanup_soup(self, soup):
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
print 'Replacing height, width and align attributes'
|
print 'Replacing height, width and align attributes'
|
||||||
|
size_map = {
|
||||||
|
'xx-small' : '0.5',
|
||||||
|
'x-small' : '1',
|
||||||
|
'small' : '2',
|
||||||
|
'medium' : '3',
|
||||||
|
'large' : '4',
|
||||||
|
'x-large' : '5',
|
||||||
|
'xx-large' : '6',
|
||||||
|
}
|
||||||
for tag in soup.recursiveChildGenerator():
|
for tag in soup.recursiveChildGenerator():
|
||||||
if not isinstance(tag, Tag): continue
|
if not isinstance(tag, Tag): continue
|
||||||
styles = []
|
styles = []
|
||||||
@ -246,6 +257,8 @@ class MobiReader(object):
|
|||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
styles.append('text-indent: %s' % tag['width'])
|
styles.append('text-indent: %s' % tag['width'])
|
||||||
|
if tag['width'].startswith('-'):
|
||||||
|
styles.append('margin-left: %s'%(tag['width'][1:]))
|
||||||
del tag['width']
|
del tag['width']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
@ -257,6 +270,15 @@ class MobiReader(object):
|
|||||||
if styles:
|
if styles:
|
||||||
tag['style'] = '; '.join(styles)
|
tag['style'] = '; '.join(styles)
|
||||||
|
|
||||||
|
if tag.name.lower() == 'font':
|
||||||
|
sz = tag.get('size', '')
|
||||||
|
try:
|
||||||
|
float(sz)
|
||||||
|
except ValueError:
|
||||||
|
sz = sz.lower()
|
||||||
|
if sz in size_map.keys():
|
||||||
|
tag['size'] = size_map[sz]
|
||||||
|
|
||||||
def create_opf(self, htmlfile, guide=None):
|
def create_opf(self, htmlfile, guide=None):
|
||||||
mi = self.book_header.exth.mi
|
mi = self.book_header.exth.mi
|
||||||
opf = OPFCreator(os.path.dirname(htmlfile), mi)
|
opf = OPFCreator(os.path.dirname(htmlfile), mi)
|
||||||
@ -289,6 +311,7 @@ class MobiReader(object):
|
|||||||
except:
|
except:
|
||||||
text = ''
|
text = ''
|
||||||
text = ent_pat.sub(entity_to_unicode, text)
|
text = ent_pat.sub(entity_to_unicode, text)
|
||||||
|
if a['href'].startswith('#'):
|
||||||
tocobj.add_item(toc.partition('#')[0], a['href'][1:], text)
|
tocobj.add_item(toc.partition('#')[0], a['href'][1:], text)
|
||||||
if tocobj is not None:
|
if tocobj is not None:
|
||||||
opf.set_toc(tocobj)
|
opf.set_toc(tocobj)
|
||||||
@ -314,6 +337,8 @@ class MobiReader(object):
|
|||||||
if flags & 1:
|
if flags & 1:
|
||||||
num += sizeof_trailing_entry(data, size - num)
|
num += sizeof_trailing_entry(data, size - num)
|
||||||
flags >>= 1
|
flags >>= 1
|
||||||
|
if self.book_header.extra_flags & 1:
|
||||||
|
num += (ord(data[size - num - 1]) & 0x3) + 1
|
||||||
return num
|
return num
|
||||||
|
|
||||||
def text_section(self, index):
|
def text_section(self, index):
|
||||||
|
@ -19,14 +19,23 @@ from collections import defaultdict
|
|||||||
from urlparse import urldefrag
|
from urlparse import urldefrag
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from PIL import Image
|
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 FauxLogger, OEBBook
|
||||||
|
from calibre.ebooks.oeb.profile import Context
|
||||||
|
from calibre.ebooks.oeb.transforms.flatcss import CSSFlattener
|
||||||
|
from calibre.ebooks.oeb.transforms.rasterize import SVGRasterizer
|
||||||
|
from calibre.ebooks.oeb.transforms.trimmanifest import ManifestTrimmer
|
||||||
from calibre.ebooks.mobi.palmdoc import compress_doc
|
from calibre.ebooks.mobi.palmdoc import compress_doc
|
||||||
from calibre.ebooks.mobi.langcodes import iana2mobi
|
from calibre.ebooks.mobi.langcodes import iana2mobi
|
||||||
from calibre.ebooks.lit.oeb import XML_NS, XHTML, XHTML_NS, OEB_DOCS
|
from calibre.ebooks.mobi.mobiml import MBP_NS, MBP, MobiMLizer
|
||||||
from calibre.ebooks.lit.oeb import xpath, barename, namespace, prefixname
|
|
||||||
from calibre.ebooks.lit.oeb import FauxLogger, OEBBook
|
|
||||||
|
|
||||||
MBP_NS = 'http://mobipocket.com/ns/mbp'
|
# TODO:
|
||||||
def MBP(name): return '{%s}%s' % (MBP_NS, name)
|
# - Allow override CSS (?)
|
||||||
|
# - Generate index records
|
||||||
|
# - Generate in-content ToC
|
||||||
|
# - Command line options, etc.
|
||||||
|
|
||||||
EXTH_CODES = {
|
EXTH_CODES = {
|
||||||
'creator': 100,
|
'creator': 100,
|
||||||
@ -43,33 +52,54 @@ EXTH_CODES = {
|
|||||||
'title': 503,
|
'title': 503,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RECORD_SIZE = 0x1000
|
||||||
|
|
||||||
UNCOMPRESSED = 1
|
UNCOMPRESSED = 1
|
||||||
PALMDOC = 2
|
PALMDOC = 2
|
||||||
HUFFDIC = 17480
|
HUFFDIC = 17480
|
||||||
|
|
||||||
def encode(data):
|
MAX_IMAGE_SIZE = 63 * 1024
|
||||||
return data.encode('ascii', 'xmlcharrefreplace')
|
MAX_THUMB_SIZE = 16 * 1024
|
||||||
|
MAX_THUMB_DIMEN = (180, 240)
|
||||||
|
|
||||||
|
def encode(data):
|
||||||
|
return data.encode('utf-8')
|
||||||
|
|
||||||
|
# Almost like the one for MS LIT, but not quite.
|
||||||
|
DECINT_FORWARD = 0
|
||||||
|
DECINT_BACKWARD = 1
|
||||||
|
def decint(value, direction):
|
||||||
|
bytes = []
|
||||||
|
while True:
|
||||||
|
b = value & 0x7f
|
||||||
|
value >>= 7
|
||||||
|
bytes.append(b)
|
||||||
|
if value == 0:
|
||||||
|
break
|
||||||
|
if direction == DECINT_FORWARD:
|
||||||
|
bytes[0] |= 0x80
|
||||||
|
elif direction == DECINT_BACKWARD:
|
||||||
|
bytes[-1] |= 0x80
|
||||||
|
return ''.join(chr(b) for b in reversed(bytes))
|
||||||
|
|
||||||
|
|
||||||
class Serializer(object):
|
class Serializer(object):
|
||||||
NSRMAP = {'': None, XML_NS: 'xml', XHTML_NS: '', MBP_NS: 'mbp'}
|
NSRMAP = {'': None, XML_NS: 'xml', XHTML_NS: '', MBP_NS: 'mbp'}
|
||||||
|
|
||||||
def __init__(self, oeb, images):
|
def __init__(self, oeb, images):
|
||||||
|
oeb.logger.info('Serializing markup content...')
|
||||||
self.oeb = oeb
|
self.oeb = oeb
|
||||||
self.images = images
|
self.images = images
|
||||||
self.id_offsets = {}
|
self.id_offsets = {}
|
||||||
self.href_offsets = defaultdict(list)
|
self.href_offsets = defaultdict(list)
|
||||||
|
self.breaks = []
|
||||||
buffer = self.buffer = StringIO()
|
buffer = self.buffer = StringIO()
|
||||||
buffer.write('<html>')
|
buffer.write('<html>')
|
||||||
self.serialize_head()
|
self.serialize_head()
|
||||||
self.serialize_body()
|
self.serialize_body()
|
||||||
buffer.write('</html>')
|
buffer.write('</html>')
|
||||||
self.fixup_links()
|
self.fixup_links()
|
||||||
self.raw = buffer.getvalue()
|
self.text = buffer.getvalue()
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.raw
|
|
||||||
|
|
||||||
def serialize_head(self):
|
def serialize_head(self):
|
||||||
buffer = self.buffer
|
buffer = self.buffer
|
||||||
@ -80,8 +110,12 @@ class Serializer(object):
|
|||||||
|
|
||||||
def serialize_guide(self):
|
def serialize_guide(self):
|
||||||
buffer = self.buffer
|
buffer = self.buffer
|
||||||
|
hrefs = self.oeb.manifest.hrefs
|
||||||
buffer.write('<guide>')
|
buffer.write('<guide>')
|
||||||
for ref in self.oeb.guide.values():
|
for ref in self.oeb.guide.values():
|
||||||
|
path, frag = urldefrag(ref.href)
|
||||||
|
if hrefs[path].media_type not in OEB_DOCS:
|
||||||
|
continue
|
||||||
buffer.write('<reference title="%s" type="%s" '
|
buffer.write('<reference title="%s" type="%s" '
|
||||||
% (ref.title, ref.type))
|
% (ref.title, ref.type))
|
||||||
self.serialize_href(ref.href)
|
self.serialize_href(ref.href)
|
||||||
@ -100,8 +134,7 @@ class Serializer(object):
|
|||||||
if item and item.spine_position is None:
|
if item and item.spine_position is None:
|
||||||
return False
|
return False
|
||||||
id = item.id if item else base.id
|
id = item.id if item else base.id
|
||||||
frag = frag if frag else 'calibre_top'
|
href = '#'.join((id, frag)) if frag else id
|
||||||
href = '#'.join((id, frag))
|
|
||||||
buffer.write('filepos=')
|
buffer.write('filepos=')
|
||||||
self.href_offsets[href].append(buffer.tell())
|
self.href_offsets[href].append(buffer.tell())
|
||||||
buffer.write('0000000000')
|
buffer.write('0000000000')
|
||||||
@ -110,23 +143,26 @@ class Serializer(object):
|
|||||||
def serialize_body(self):
|
def serialize_body(self):
|
||||||
buffer = self.buffer
|
buffer = self.buffer
|
||||||
buffer.write('<body>')
|
buffer.write('<body>')
|
||||||
for item in self.oeb.spine:
|
spine = [item for item in self.oeb.spine if item.linear]
|
||||||
|
spine.extend([item for item in self.oeb.spine if not item.linear])
|
||||||
|
for item in spine:
|
||||||
self.serialize_item(item)
|
self.serialize_item(item)
|
||||||
buffer.write('</body>')
|
buffer.write('</body>')
|
||||||
|
|
||||||
def serialize_item(self, item):
|
def serialize_item(self, item):
|
||||||
buffer = self.buffer
|
buffer = self.buffer
|
||||||
buffer.write('<mbp:pagebreak/>')
|
if not item.linear:
|
||||||
# TODO: Figure out how to make the 'crossable' stuff work for
|
self.breaks.append(buffer.tell() - 1)
|
||||||
# non-"linear" spine items.
|
self.id_offsets[item.id] = buffer.tell()
|
||||||
self.id_offsets[item.id + '#calibre_top'] = buffer.tell()
|
|
||||||
for elem in item.data.find(XHTML('body')):
|
for elem in item.data.find(XHTML('body')):
|
||||||
self.serialize_elem(elem, item)
|
self.serialize_elem(elem, item)
|
||||||
|
buffer.write('<mbp:pagebreak/>')
|
||||||
|
|
||||||
def serialize_elem(self, elem, item, nsrmap=NSRMAP):
|
def serialize_elem(self, elem, item, nsrmap=NSRMAP):
|
||||||
if namespace(elem.tag) not in nsrmap:
|
|
||||||
return
|
|
||||||
buffer = self.buffer
|
buffer = self.buffer
|
||||||
|
if not isinstance(elem.tag, basestring) \
|
||||||
|
or namespace(elem.tag) not in nsrmap:
|
||||||
|
return
|
||||||
hrefs = self.oeb.manifest.hrefs
|
hrefs = self.oeb.manifest.hrefs
|
||||||
tag = prefixname(elem.tag, nsrmap)
|
tag = prefixname(elem.tag, nsrmap)
|
||||||
for attr in ('name', 'id'):
|
for attr in ('name', 'id'):
|
||||||
@ -134,6 +170,9 @@ class Serializer(object):
|
|||||||
id = '#'.join((item.id, elem.attrib[attr]))
|
id = '#'.join((item.id, elem.attrib[attr]))
|
||||||
self.id_offsets[id] = buffer.tell()
|
self.id_offsets[id] = buffer.tell()
|
||||||
del elem.attrib[attr]
|
del elem.attrib[attr]
|
||||||
|
if tag == 'a' and not elem.attrib \
|
||||||
|
and not len(elem) and not elem.text:
|
||||||
|
return
|
||||||
buffer.write('<')
|
buffer.write('<')
|
||||||
buffer.write(tag)
|
buffer.write(tag)
|
||||||
if elem.attrib:
|
if elem.attrib:
|
||||||
@ -149,18 +188,29 @@ class Serializer(object):
|
|||||||
index = self.images[val]
|
index = self.images[val]
|
||||||
buffer.write('recindex="%05d"' % index)
|
buffer.write('recindex="%05d"' % index)
|
||||||
continue
|
continue
|
||||||
buffer.write('%s="%s"' % (attr, val))
|
buffer.write(attr)
|
||||||
|
buffer.write('="')
|
||||||
|
self.serialize_text(val, quot=True)
|
||||||
|
buffer.write('"')
|
||||||
if elem.text or len(elem) > 0:
|
if elem.text or len(elem) > 0:
|
||||||
buffer.write('>')
|
buffer.write('>')
|
||||||
if elem.text:
|
if elem.text:
|
||||||
buffer.write(encode(elem.text))
|
self.serialize_text(elem.text)
|
||||||
for child in elem:
|
for child in elem:
|
||||||
self.serialize_elem(child, item)
|
self.serialize_elem(child, item)
|
||||||
|
if child.tail:
|
||||||
|
self.serialize_text(child.tail)
|
||||||
buffer.write('</%s>' % tag)
|
buffer.write('</%s>' % tag)
|
||||||
else:
|
else:
|
||||||
buffer.write('/>')
|
buffer.write('/>')
|
||||||
if elem.tail:
|
|
||||||
buffer.write(encode(elem.tail))
|
def serialize_text(self, text, quot=False):
|
||||||
|
text = text.replace('&', '&')
|
||||||
|
text = text.replace('<', '<')
|
||||||
|
text = text.replace('>', '>')
|
||||||
|
if quot:
|
||||||
|
text = text.replace('"', '"')
|
||||||
|
self.buffer.write(encode(text))
|
||||||
|
|
||||||
def fixup_links(self):
|
def fixup_links(self):
|
||||||
buffer = self.buffer
|
buffer = self.buffer
|
||||||
@ -172,8 +222,8 @@ class Serializer(object):
|
|||||||
|
|
||||||
|
|
||||||
class MobiWriter(object):
|
class MobiWriter(object):
|
||||||
def __init__(self, compress=None, logger=FauxLogger()):
|
def __init__(self, compression=None, logger=FauxLogger()):
|
||||||
self._compress = compress or UNCOMPRESSED
|
self._compression = compression or UNCOMPRESSED
|
||||||
self._logger = logger
|
self._logger = logger
|
||||||
|
|
||||||
def dump(self, oeb, path):
|
def dump(self, oeb, path):
|
||||||
@ -207,42 +257,113 @@ class MobiWriter(object):
|
|||||||
index = 1
|
index = 1
|
||||||
self._images = images = {}
|
self._images = images = {}
|
||||||
for item in self._oeb.manifest.values():
|
for item in self._oeb.manifest.values():
|
||||||
if item.media_type.startswith('image/'):
|
if item.media_type in OEB_RASTER_IMAGES:
|
||||||
images[item.href] = index
|
images[item.href] = index
|
||||||
index += 1
|
index += 1
|
||||||
|
|
||||||
|
def _read_text_record(self, text):
|
||||||
|
pos = text.tell()
|
||||||
|
text.seek(0, 2)
|
||||||
|
npos = min((pos + RECORD_SIZE, text.tell()))
|
||||||
|
last = ''
|
||||||
|
while not last.decode('utf-8', 'ignore'):
|
||||||
|
size = len(last) + 1
|
||||||
|
text.seek(npos - size)
|
||||||
|
last = text.read(size)
|
||||||
|
extra = 0
|
||||||
|
try:
|
||||||
|
last.decode('utf-8')
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
prev = len(last)
|
||||||
|
while True:
|
||||||
|
text.seek(npos - prev)
|
||||||
|
last = text.read(len(last) + 1)
|
||||||
|
try:
|
||||||
|
last.decode('utf-8')
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
extra = len(last) - prev
|
||||||
|
text.seek(pos)
|
||||||
|
data = text.read(RECORD_SIZE)
|
||||||
|
overlap = text.read(extra)
|
||||||
|
text.seek(npos)
|
||||||
|
return data, overlap
|
||||||
|
|
||||||
def _generate_text(self):
|
def _generate_text(self):
|
||||||
serializer = Serializer(self._oeb, self._images)
|
serializer = Serializer(self._oeb, self._images)
|
||||||
text = str(serializer)
|
breaks = serializer.breaks
|
||||||
|
text = serializer.text
|
||||||
self._text_length = len(text)
|
self._text_length = len(text)
|
||||||
text = StringIO(text)
|
text = StringIO(text)
|
||||||
nrecords = 0
|
nrecords = 0
|
||||||
data = text.read(0x1000)
|
offset = 0
|
||||||
|
data, overlap = self._read_text_record(text)
|
||||||
while len(data) > 0:
|
while len(data) > 0:
|
||||||
nrecords += 1
|
if self._compression == PALMDOC:
|
||||||
if self._compress == PALMDOC:
|
|
||||||
data = compress_doc(data)
|
data = compress_doc(data)
|
||||||
# Without the NUL Mobipocket Desktop 6.2 will thrash. Why?
|
record = StringIO()
|
||||||
self._records.append(data + '\0')
|
record.write(data)
|
||||||
data = text.read(0x1000)
|
record.write(overlap)
|
||||||
|
record.write(pack('>B', len(overlap)))
|
||||||
|
nextra = 0
|
||||||
|
pbreak = 0
|
||||||
|
running = offset
|
||||||
|
while breaks and (breaks[0] - offset) < RECORD_SIZE:
|
||||||
|
pbreak = (breaks.pop(0) - running) >> 3
|
||||||
|
encoded = decint(pbreak, DECINT_FORWARD)
|
||||||
|
record.write(encoded)
|
||||||
|
running += pbreak << 3
|
||||||
|
nextra += len(encoded)
|
||||||
|
lsize = 1
|
||||||
|
while True:
|
||||||
|
size = decint(nextra + lsize, DECINT_BACKWARD)
|
||||||
|
if len(size) == lsize:
|
||||||
|
break
|
||||||
|
lsize += 1
|
||||||
|
record.write(size)
|
||||||
|
self._records.append(record.getvalue())
|
||||||
|
nrecords += 1
|
||||||
|
offset += RECORD_SIZE
|
||||||
|
data, overlap = self._read_text_record(text)
|
||||||
self._text_nrecords = nrecords
|
self._text_nrecords = nrecords
|
||||||
|
|
||||||
def _rescale_image(self, data, maxsizeb, dimen=None):
|
def _rescale_image(self, data, maxsizeb, dimen=None):
|
||||||
|
image = Image.open(StringIO(data))
|
||||||
|
format = image.format
|
||||||
|
changed = False
|
||||||
|
if image.format not in ('JPEG', 'GIF'):
|
||||||
|
format = 'GIF'
|
||||||
|
changed = True
|
||||||
if dimen is not None:
|
if dimen is not None:
|
||||||
image = Image.open(StringIO(data))
|
|
||||||
image.thumbnail(dimen, Image.ANTIALIAS)
|
image.thumbnail(dimen, Image.ANTIALIAS)
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
data = StringIO()
|
data = StringIO()
|
||||||
image.save(data, image.format)
|
image.save(data, format)
|
||||||
data = data.getvalue()
|
data = data.getvalue()
|
||||||
if len(data) < maxsizeb:
|
if len(data) <= maxsizeb:
|
||||||
return data
|
return data
|
||||||
image = Image.open(StringIO(data))
|
image = image.convert('RGBA')
|
||||||
for quality in xrange(95, -1, -1):
|
for quality in xrange(95, -1, -1):
|
||||||
data = StringIO()
|
data = StringIO()
|
||||||
image.save(data, 'JPEG', quality=quality)
|
image.save(data, 'JPEG', quality=quality)
|
||||||
data = data.getvalue()
|
data = data.getvalue()
|
||||||
if len(data) <= maxsizeb:
|
if len(data) <= maxsizeb:
|
||||||
break
|
return data
|
||||||
|
width, height = image.size
|
||||||
|
for scale in xrange(99, 0, -1):
|
||||||
|
scale = scale / 100.
|
||||||
|
data = StringIO()
|
||||||
|
scaled = image.copy()
|
||||||
|
size = (int(width * scale), (height * scale))
|
||||||
|
scaled.thumbnail(size, Image.ANTIALIAS)
|
||||||
|
scaled.save(data, 'JPEG', quality=0)
|
||||||
|
data = data.getvalue()
|
||||||
|
if len(data) <= maxsizeb:
|
||||||
|
return data
|
||||||
|
# Well, we tried?
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _generate_images(self):
|
def _generate_images(self):
|
||||||
@ -252,35 +373,37 @@ class MobiWriter(object):
|
|||||||
coverid = metadata.cover[0] if metadata.cover else None
|
coverid = metadata.cover[0] if metadata.cover else None
|
||||||
for _, href in images:
|
for _, href in images:
|
||||||
item = self._oeb.manifest.hrefs[href]
|
item = self._oeb.manifest.hrefs[href]
|
||||||
maxsizek = 89 if coverid == item.id else 63
|
data = self._rescale_image(item.data, MAX_IMAGE_SIZE)
|
||||||
maxsizeb = maxsizek * 1024
|
|
||||||
data = self._rescale_image(item.data, maxsizeb)
|
|
||||||
self._records.append(data)
|
self._records.append(data)
|
||||||
|
|
||||||
def _generate_record0(self):
|
def _generate_record0(self):
|
||||||
metadata = self._oeb.metadata
|
metadata = self._oeb.metadata
|
||||||
exth = self._build_exth()
|
exth = self._build_exth()
|
||||||
record0 = StringIO()
|
record0 = StringIO()
|
||||||
record0.write(pack('>HHIHHHH', self._compress, 0, self._text_length,
|
record0.write(pack('>HHIHHHH', self._compression, 0,
|
||||||
self._text_nrecords, 0x1000, 0, 0))
|
self._text_length, self._text_nrecords, RECORD_SIZE, 0, 0))
|
||||||
uid = random.randint(0, 0xffffffff)
|
uid = random.randint(0, 0xffffffff)
|
||||||
title = str(metadata.title[0])
|
title = str(metadata.title[0])
|
||||||
record0.write('MOBI')
|
record0.write('MOBI')
|
||||||
record0.write(pack('>IIIII', 0xe8, 2, 65001, uid, 5))
|
record0.write(pack('>IIIII', 0xe8, 2, 65001, uid, 6))
|
||||||
record0.write('\xff' * 40)
|
record0.write('\xff' * 40)
|
||||||
record0.write(pack('>I', self._text_nrecords + 1))
|
record0.write(pack('>I', self._text_nrecords + 1))
|
||||||
record0.write(pack('>II', 0xe8 + 16 + len(exth), len(title)))
|
record0.write(pack('>II', 0xe8 + 16 + len(exth), len(title)))
|
||||||
record0.write(iana2mobi(str(metadata.language[0])))
|
record0.write(iana2mobi(str(metadata.language[0])))
|
||||||
record0.write('\0' * 8)
|
record0.write('\0' * 8)
|
||||||
record0.write(pack('>II', 5, self._text_nrecords + 1))
|
record0.write(pack('>II', 6, self._text_nrecords + 1))
|
||||||
record0.write('\0' * 16)
|
record0.write('\0' * 16)
|
||||||
record0.write(pack('>I', 0x50))
|
record0.write(pack('>I', 0x50))
|
||||||
record0.write('\0' * 32)
|
record0.write('\0' * 32)
|
||||||
record0.write(pack('>IIII', 0xffffffff, 0xffffffff, 0, 0))
|
record0.write(pack('>IIII', 0xffffffff, 0xffffffff, 0, 0))
|
||||||
# TODO: What the hell are these fields?
|
# The '5' is a bitmask of extra record data at the end:
|
||||||
|
# - 0x1: <extra multibyte bytes><size> (?)
|
||||||
|
# - 0x4: <uncrossable breaks><size>
|
||||||
|
# Of course, the formats aren't quite the same.
|
||||||
|
# TODO: What the hell are the rest of these fields?
|
||||||
record0.write(pack('>IIIIIIIIIIIIIIIII',
|
record0.write(pack('>IIIIIIIIIIIIIIIII',
|
||||||
0, 0, 0, 0xffffffff, 0, 0xffffffff, 0, 0xffffffff, 0, 0xffffffff,
|
0, 0, 0, 0xffffffff, 0, 0xffffffff, 0, 0xffffffff, 0, 0xffffffff,
|
||||||
0, 0xffffffff, 0, 0xffffffff, 0xffffffff, 1, 0xffffffff))
|
0, 0xffffffff, 0, 0xffffffff, 0xffffffff, 5, 0xffffffff))
|
||||||
record0.write(exth)
|
record0.write(exth)
|
||||||
record0.write(title)
|
record0.write(title)
|
||||||
record0 = record0.getvalue()
|
record0 = record0.getvalue()
|
||||||
@ -294,13 +417,13 @@ class MobiWriter(object):
|
|||||||
if term not in EXTH_CODES: continue
|
if term not in EXTH_CODES: continue
|
||||||
code = EXTH_CODES[term]
|
code = EXTH_CODES[term]
|
||||||
for item in oeb.metadata[term]:
|
for item in oeb.metadata[term]:
|
||||||
data = str(item)
|
data = unicode(item).encode('utf-8')
|
||||||
exth.write(pack('>II', code, len(data) + 8))
|
exth.write(pack('>II', code, len(data) + 8))
|
||||||
exth.write(data)
|
exth.write(data)
|
||||||
nrecs += 1
|
nrecs += 1
|
||||||
if oeb.metadata.cover:
|
if oeb.metadata.cover:
|
||||||
id = str(oeb.metadata.cover[0])
|
id = str(oeb.metadata.cover[0])
|
||||||
item = oeb.manifest[id]
|
item = oeb.manifest.ids[id]
|
||||||
href = item.href
|
href = item.href
|
||||||
index = self._images[href] - 1
|
index = self._images[href] - 1
|
||||||
exth.write(pack('>III', 0xc9, 0x0c, index))
|
exth.write(pack('>III', 0xc9, 0x0c, index))
|
||||||
@ -315,9 +438,7 @@ class MobiWriter(object):
|
|||||||
return ''.join(exth)
|
return ''.join(exth)
|
||||||
|
|
||||||
def _add_thumbnail(self, item):
|
def _add_thumbnail(self, item):
|
||||||
maxsizeb = 16 * 1024
|
data = self._rescale_image(item.data, MAX_THUMB_SIZE, MAX_THUMB_DIMEN)
|
||||||
dimen = (180, 240)
|
|
||||||
data = self._rescale_image(item.data, maxsizeb, dimen)
|
|
||||||
manifest = self._oeb.manifest
|
manifest = self._oeb.manifest
|
||||||
id, href = manifest.generate('thumbnail', 'thumbnail.jpeg')
|
id, href = manifest.generate('thumbnail', 'thumbnail.jpeg')
|
||||||
manifest.add(id, href, 'image/jpeg', data=data)
|
manifest.add(id, href, 'image/jpeg', data=data)
|
||||||
@ -346,9 +467,24 @@ class MobiWriter(object):
|
|||||||
|
|
||||||
|
|
||||||
def main(argv=sys.argv):
|
def main(argv=sys.argv):
|
||||||
|
from calibre.ebooks.oeb.base import DirWriter
|
||||||
inpath, outpath = argv[1:]
|
inpath, outpath = argv[1:]
|
||||||
|
context = Context('Firefox', 'MobiDesktop')
|
||||||
oeb = OEBBook(inpath)
|
oeb = OEBBook(inpath)
|
||||||
writer = MobiWriter()
|
#writer = MobiWriter(compression=PALMDOC)
|
||||||
|
writer = MobiWriter(compression=UNCOMPRESSED)
|
||||||
|
#writer = DirWriter()
|
||||||
|
fbase = context.dest.fbase
|
||||||
|
fkey = context.dest.fnums.values()
|
||||||
|
flattener = CSSFlattener(
|
||||||
|
fbase=fbase, fkey=fkey, unfloat=True, untable=True)
|
||||||
|
rasterizer = SVGRasterizer()
|
||||||
|
trimmer = ManifestTrimmer()
|
||||||
|
mobimlizer = MobiMLizer()
|
||||||
|
flattener.transform(oeb, context)
|
||||||
|
rasterizer.transform(oeb, context)
|
||||||
|
mobimlizer.transform(oeb, context)
|
||||||
|
trimmer.transform(oeb, context)
|
||||||
writer.dump(oeb, outpath)
|
writer.dump(oeb, outpath)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
@ -14,10 +14,14 @@ from itertools import izip, count
|
|||||||
from urlparse import urldefrag, urlparse, urlunparse
|
from urlparse import urldefrag, urlparse, urlunparse
|
||||||
from urllib import unquote as urlunquote
|
from urllib import unquote as urlunquote
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
import htmlentitydefs
|
||||||
|
import uuid
|
||||||
|
import copy
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from calibre import LoggingInterface
|
from calibre import LoggingInterface
|
||||||
|
|
||||||
XML_PARSER = etree.XMLParser(recover=True, resolve_entities=False)
|
XML_PARSER = etree.XMLParser(recover=True)
|
||||||
XML_NS = 'http://www.w3.org/XML/1998/namespace'
|
XML_NS = 'http://www.w3.org/XML/1998/namespace'
|
||||||
XHTML_NS = 'http://www.w3.org/1999/xhtml'
|
XHTML_NS = 'http://www.w3.org/1999/xhtml'
|
||||||
OPF1_NS = 'http://openebook.org/namespaces/oeb-package/1.0/'
|
OPF1_NS = 'http://openebook.org/namespaces/oeb-package/1.0/'
|
||||||
@ -28,15 +32,20 @@ DC11_NS = 'http://purl.org/dc/elements/1.1/'
|
|||||||
XSI_NS = 'http://www.w3.org/2001/XMLSchema-instance'
|
XSI_NS = 'http://www.w3.org/2001/XMLSchema-instance'
|
||||||
DCTERMS_NS = 'http://purl.org/dc/terms/'
|
DCTERMS_NS = 'http://purl.org/dc/terms/'
|
||||||
NCX_NS = 'http://www.daisy.org/z3986/2005/ncx/'
|
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'
|
||||||
XPNSMAP = {'h': XHTML_NS, 'o1': OPF1_NS, 'o2': OPF2_NS,
|
XPNSMAP = {'h': XHTML_NS, 'o1': OPF1_NS, 'o2': OPF2_NS,
|
||||||
'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS,
|
'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS,
|
||||||
'xsi': XSI_NS, 'dt': DCTERMS_NS, 'ncx': NCX_NS}
|
'xsi': XSI_NS, 'dt': DCTERMS_NS, 'ncx': NCX_NS,
|
||||||
|
'svg': SVG_NS, 'xl': XLINK_NS}
|
||||||
|
|
||||||
def XML(name): return '{%s}%s' % (XML_NS, name)
|
def XML(name): return '{%s}%s' % (XML_NS, name)
|
||||||
def XHTML(name): return '{%s}%s' % (XHTML_NS, name)
|
def XHTML(name): return '{%s}%s' % (XHTML_NS, name)
|
||||||
def OPF(name): return '{%s}%s' % (OPF2_NS, name)
|
def OPF(name): return '{%s}%s' % (OPF2_NS, name)
|
||||||
def DC(name): return '{%s}%s' % (DC11_NS, name)
|
def DC(name): return '{%s}%s' % (DC11_NS, name)
|
||||||
def NCX(name): return '{%s}%s' % (NCX_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)
|
||||||
|
|
||||||
EPUB_MIME = 'application/epub+zip'
|
EPUB_MIME = 'application/epub+zip'
|
||||||
XHTML_MIME = 'application/xhtml+xml'
|
XHTML_MIME = 'application/xhtml+xml'
|
||||||
@ -46,12 +55,24 @@ OPF_MIME = 'application/oebps-package+xml'
|
|||||||
OEB_DOC_MIME = 'text/x-oeb1-document'
|
OEB_DOC_MIME = 'text/x-oeb1-document'
|
||||||
OEB_CSS_MIME = 'text/x-oeb1-css'
|
OEB_CSS_MIME = 'text/x-oeb1-css'
|
||||||
OPENTYPE_MIME = 'font/opentype'
|
OPENTYPE_MIME = 'font/opentype'
|
||||||
|
GIF_MIME = 'image/gif'
|
||||||
|
JPEG_MIME = 'image/jpeg'
|
||||||
|
PNG_MIME = 'image/png'
|
||||||
|
SVG_MIME = 'image/svg+xml'
|
||||||
|
|
||||||
OEB_STYLES = set([CSS_MIME, OEB_CSS_MIME, 'text/x-oeb-css'])
|
OEB_STYLES = set([CSS_MIME, OEB_CSS_MIME, 'text/x-oeb-css'])
|
||||||
OEB_DOCS = set([XHTML_MIME, 'text/html', OEB_DOC_MIME, 'text/x-oeb-document'])
|
OEB_DOCS = set([XHTML_MIME, 'text/html', OEB_DOC_MIME, 'text/x-oeb-document'])
|
||||||
|
OEB_RASTER_IMAGES = set([GIF_MIME, JPEG_MIME, PNG_MIME])
|
||||||
|
OEB_IMAGES = set([GIF_MIME, JPEG_MIME, PNG_MIME, SVG_MIME])
|
||||||
|
|
||||||
MS_COVER_TYPE = 'other.ms-coverimage-standard'
|
MS_COVER_TYPE = 'other.ms-coverimage-standard'
|
||||||
|
|
||||||
|
ENTITYDEFS = dict(htmlentitydefs.entitydefs)
|
||||||
|
del ENTITYDEFS['lt']
|
||||||
|
del ENTITYDEFS['gt']
|
||||||
|
del ENTITYDEFS['quot']
|
||||||
|
del ENTITYDEFS['amp']
|
||||||
|
|
||||||
|
|
||||||
def element(parent, *args, **kwargs):
|
def element(parent, *args, **kwargs):
|
||||||
if parent is not None:
|
if parent is not None:
|
||||||
@ -101,12 +122,20 @@ def urlnormalize(href):
|
|||||||
return urlunparse(parts)
|
return urlunparse(parts)
|
||||||
|
|
||||||
|
|
||||||
|
class OEBError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FauxLogger(object):
|
class FauxLogger(object):
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
return self
|
return self
|
||||||
def __call__(self, message):
|
def __call__(self, message):
|
||||||
print message
|
print message
|
||||||
|
|
||||||
|
class Logger(LoggingInterface, object):
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return object.__getattribute__(self, 'log_' + name)
|
||||||
|
|
||||||
|
|
||||||
class AbstractContainer(object):
|
class AbstractContainer(object):
|
||||||
def read_xml(self, path):
|
def read_xml(self, path):
|
||||||
@ -161,8 +190,9 @@ class Metadata(object):
|
|||||||
'xsi': XSI_NS}
|
'xsi': XSI_NS}
|
||||||
|
|
||||||
class Item(object):
|
class Item(object):
|
||||||
def __init__(self, term, value, fq_attrib={}):
|
def __init__(self, term, value, fq_attrib={}, **kwargs):
|
||||||
self.fq_attrib = dict(fq_attrib)
|
self.fq_attrib = fq_attrib = dict(fq_attrib)
|
||||||
|
fq_attrib.update(kwargs)
|
||||||
if term == OPF('meta') and not value:
|
if term == OPF('meta') and not value:
|
||||||
term = self.fq_attrib.pop('name')
|
term = self.fq_attrib.pop('name')
|
||||||
value = self.fq_attrib.pop('content')
|
value = self.fq_attrib.pop('content')
|
||||||
@ -224,8 +254,8 @@ class Metadata(object):
|
|||||||
self.oeb = oeb
|
self.oeb = oeb
|
||||||
self.items = defaultdict(list)
|
self.items = defaultdict(list)
|
||||||
|
|
||||||
def add(self, term, value, attrib={}):
|
def add(self, term, value, attrib={}, **kwargs):
|
||||||
item = self.Item(term, value, attrib)
|
item = self.Item(term, value, attrib, **kwargs)
|
||||||
items = self.items[barename(item.term)]
|
items = self.items[barename(item.term)]
|
||||||
items.append(item)
|
items.append(item)
|
||||||
return item
|
return item
|
||||||
@ -266,6 +296,9 @@ class Metadata(object):
|
|||||||
|
|
||||||
class Manifest(object):
|
class Manifest(object):
|
||||||
class Item(object):
|
class Item(object):
|
||||||
|
ENTITY_RE = re.compile(r'&([a-zA-Z_:][a-zA-Z0-9.-_:]+);')
|
||||||
|
NUM_RE = re.compile('^(.*)([0-9][0-9.]*)(?=[.]|$)')
|
||||||
|
|
||||||
def __init__(self, id, href, media_type,
|
def __init__(self, id, href, media_type,
|
||||||
fallback=None, loader=str, data=None):
|
fallback=None, loader=str, data=None):
|
||||||
self.id = id
|
self.id = id
|
||||||
@ -281,19 +314,24 @@ class Manifest(object):
|
|||||||
return 'Item(id=%r, href=%r, media_type=%r)' \
|
return 'Item(id=%r, href=%r, media_type=%r)' \
|
||||||
% (self.id, self.href, self.media_type)
|
% (self.id, self.href, self.media_type)
|
||||||
|
|
||||||
|
def _force_xhtml(self, data):
|
||||||
|
repl = lambda m: ENTITYDEFS.get(m.group(1), m.group(0))
|
||||||
|
data = self.ENTITY_RE.sub(repl, data)
|
||||||
|
data = etree.fromstring(data, parser=XML_PARSER)
|
||||||
|
if namespace(data.tag) != XHTML_NS:
|
||||||
|
data.attrib['xmlns'] = XHTML_NS
|
||||||
|
data = etree.tostring(data)
|
||||||
|
data = etree.fromstring(data, parser=XML_PARSER)
|
||||||
|
return data
|
||||||
|
|
||||||
def data():
|
def data():
|
||||||
def fget(self):
|
def fget(self):
|
||||||
if self._data is not None:
|
if self._data is not None:
|
||||||
return self._data
|
return self._data
|
||||||
data = self._loader(self.href)
|
data = self._loader(self.href)
|
||||||
if self.media_type in OEB_DOCS:
|
if self.media_type in OEB_DOCS:
|
||||||
data = etree.fromstring(data, parser=XML_PARSER)
|
data = self._force_xhtml(data)
|
||||||
if namespace(data.tag) != XHTML_NS:
|
elif self.media_type[-4:] in ('+xml', '/xml'):
|
||||||
data.attrib['xmlns'] = XHTML_NS
|
|
||||||
data = etree.tostring(data)
|
|
||||||
data = etree.fromstring(data, parser=XML_PARSER)
|
|
||||||
elif self.media_type.startswith('application/') \
|
|
||||||
and self.media_type.endswith('+xml'):
|
|
||||||
data = etree.fromstring(data, parser=XML_PARSER)
|
data = etree.fromstring(data, parser=XML_PARSER)
|
||||||
self._data = data
|
self._data = data
|
||||||
return data
|
return data
|
||||||
@ -310,11 +348,22 @@ class Manifest(object):
|
|||||||
return xml2str(data)
|
return xml2str(data)
|
||||||
return str(data)
|
return str(data)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return id(self) == id(other)
|
||||||
|
|
||||||
def __cmp__(self, other):
|
def __cmp__(self, other):
|
||||||
result = cmp(self.spine_position, other.spine_position)
|
result = cmp(self.spine_position, other.spine_position)
|
||||||
if result != 0:
|
if result != 0:
|
||||||
return result
|
return result
|
||||||
return cmp(self.id, other.id)
|
smatch = self.NUM_RE.search(self.href)
|
||||||
|
sref = smatch.group(1) if smatch else self.href
|
||||||
|
snum = float(smatch.group(2)) if smatch else 0.0
|
||||||
|
skey = (sref, snum, self.id)
|
||||||
|
omatch = self.NUM_RE.search(other.href)
|
||||||
|
oref = omatch.group(1) if omatch else other.href
|
||||||
|
onum = float(omatch.group(2)) if omatch else 0.0
|
||||||
|
okey = (oref, onum, other.id)
|
||||||
|
return cmp(skey, okey)
|
||||||
|
|
||||||
def relhref(self, href):
|
def relhref(self, href):
|
||||||
if '/' not in self.href:
|
if '/' not in self.href:
|
||||||
@ -519,8 +568,11 @@ class Guide(object):
|
|||||||
for type, ref in self.refs.items():
|
for type, ref in self.refs.items():
|
||||||
yield type, ref
|
yield type, ref
|
||||||
|
|
||||||
def __getitem__(self, index):
|
def __getitem__(self, key):
|
||||||
return self.refs[index]
|
return self.refs[key]
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
del self.refs[key]
|
||||||
|
|
||||||
def __contains__(self, key):
|
def __contains__(self, key):
|
||||||
return key in self.refs
|
return key in self.refs
|
||||||
@ -625,13 +677,22 @@ class OEBBook(object):
|
|||||||
self._all_from_opf(opf)
|
self._all_from_opf(opf)
|
||||||
|
|
||||||
def _convert_opf1(self, opf):
|
def _convert_opf1(self, opf):
|
||||||
|
# Seriously, seriously wrong
|
||||||
|
if namespace(opf.tag) == OPF1_NS:
|
||||||
|
opf.tag = barename(opf.tag)
|
||||||
|
for elem in opf.iterdescendants():
|
||||||
|
if isinstance(elem.tag, basestring) \
|
||||||
|
and namespace(elem.tag) == OPF1_NS:
|
||||||
|
elem.tag = barename(elem.tag)
|
||||||
|
attrib = dict(opf.attrib)
|
||||||
|
attrib['version'] = '2.0'
|
||||||
nroot = etree.Element(OPF('package'),
|
nroot = etree.Element(OPF('package'),
|
||||||
nsmap={None: OPF2_NS}, version="2.0", **dict(opf.attrib))
|
nsmap={None: OPF2_NS}, attrib=attrib)
|
||||||
metadata = etree.SubElement(nroot, OPF('metadata'),
|
metadata = etree.SubElement(nroot, OPF('metadata'),
|
||||||
nsmap={'opf': OPF2_NS, 'dc': DC11_NS,
|
nsmap={'opf': OPF2_NS, 'dc': DC11_NS,
|
||||||
'xsi': XSI_NS, 'dcterms': DCTERMS_NS})
|
'xsi': XSI_NS, 'dcterms': DCTERMS_NS})
|
||||||
for prefix in ('d11', 'd10', 'd09'):
|
for prefix in ('d11', 'd10', 'd09'):
|
||||||
elements = xpath(opf, 'metadata/dc-metadata/%s:*' % prefix)
|
elements = xpath(opf, 'metadata//%s:*' % prefix)
|
||||||
if elements: break
|
if elements: break
|
||||||
for element in elements:
|
for element in elements:
|
||||||
if not element.text: continue
|
if not element.text: continue
|
||||||
@ -643,7 +704,7 @@ class OEBBook(object):
|
|||||||
element.attrib[nsname] = element.attrib[name]
|
element.attrib[nsname] = element.attrib[name]
|
||||||
del element.attrib[name]
|
del element.attrib[name]
|
||||||
metadata.append(element)
|
metadata.append(element)
|
||||||
for element in opf.xpath('metadata/x-metadata/meta'):
|
for element in opf.xpath('metadata//meta'):
|
||||||
metadata.append(element)
|
metadata.append(element)
|
||||||
for item in opf.xpath('manifest/item'):
|
for item in opf.xpath('manifest/item'):
|
||||||
media_type = item.attrib['media-type'].lower()
|
media_type = item.attrib['media-type'].lower()
|
||||||
@ -660,31 +721,48 @@ class OEBBook(object):
|
|||||||
def _read_opf(self, opfpath):
|
def _read_opf(self, opfpath):
|
||||||
opf = self.container.read_xml(opfpath)
|
opf = self.container.read_xml(opfpath)
|
||||||
version = float(opf.get('version', 1.0))
|
version = float(opf.get('version', 1.0))
|
||||||
if version < 2.0:
|
ns = namespace(opf.tag)
|
||||||
|
if ns not in ('', OPF1_NS, OPF2_NS):
|
||||||
|
raise OEBError('Invalid namespace %r for OPF document' % ns)
|
||||||
|
if ns != OPF2_NS or version < 2.0:
|
||||||
opf = self._convert_opf1(opf)
|
opf = self._convert_opf1(opf)
|
||||||
return opf
|
return opf
|
||||||
|
|
||||||
def _metadata_from_opf(self, opf):
|
def _metadata_from_opf(self, opf):
|
||||||
uid = opf.attrib['unique-identifier']
|
uid = opf.get('unique-identifier', 'calibre-uuid')
|
||||||
|
self.uid = None
|
||||||
self.metadata = metadata = Metadata(self)
|
self.metadata = metadata = Metadata(self)
|
||||||
for elem in xpath(opf, '/o2:package/o2:metadata/*'):
|
ignored = (OPF('dc-metadata'), OPF('x-metadata'))
|
||||||
if elem.text or elem.attrib:
|
for elem in xpath(opf, '/o2:package/o2:metadata//*'):
|
||||||
|
if elem.tag not in ignored and (elem.text or elem.attrib):
|
||||||
metadata.add(elem.tag, elem.text, elem.attrib)
|
metadata.add(elem.tag, elem.text, elem.attrib)
|
||||||
|
haveuuid = haveid = False
|
||||||
|
for ident in metadata.identifier:
|
||||||
|
if unicode(ident).startswith('urn:uuid:'):
|
||||||
|
haveuuid = True
|
||||||
|
if 'id' in ident.attrib:
|
||||||
|
haveid = True
|
||||||
|
if not haveuuid and haveid:
|
||||||
|
bookid = "urn:uuid:%s" % str(uuid.uuid4())
|
||||||
|
metadata.add('identifier', bookid, id='calibre-uuid')
|
||||||
for item in metadata.identifier:
|
for item in metadata.identifier:
|
||||||
if item.id == uid:
|
if item.id == uid:
|
||||||
self.uid = item
|
self.uid = item
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
self.logger.log_warn(u'Unique-identifier %r not found.' % uid)
|
self.logger.warn(u'Unique-identifier %r not found.' % uid)
|
||||||
|
for ident in metadata.identifier:
|
||||||
|
if 'id' in ident.attrib:
|
||||||
self.uid = metadata.identifier[0]
|
self.uid = metadata.identifier[0]
|
||||||
|
break
|
||||||
if not metadata.language:
|
if not metadata.language:
|
||||||
self.logger.log_warn(u'Language not specified.')
|
self.logger.warn(u'Language not specified.')
|
||||||
metadata.add('language', 'en')
|
metadata.add('language', 'en')
|
||||||
if not metadata.creator:
|
if not metadata.creator:
|
||||||
self.logger.log_warn(u'Creator not specified.')
|
self.logger.warn(u'Creator not specified.')
|
||||||
metadata.add('creator', 'Unknown')
|
metadata.add('creator', 'Unknown')
|
||||||
if not metadata.title:
|
if not metadata.title:
|
||||||
self.logger.log_warn(u'Title not specified.')
|
self.logger.warn(u'Title not specified.')
|
||||||
metadata.add('title', 'Unknown')
|
metadata.add('title', 'Unknown')
|
||||||
|
|
||||||
def _manifest_from_opf(self, opf):
|
def _manifest_from_opf(self, opf):
|
||||||
@ -692,7 +770,7 @@ class OEBBook(object):
|
|||||||
for elem in xpath(opf, '/o2:package/o2:manifest/o2:item'):
|
for elem in xpath(opf, '/o2:package/o2:manifest/o2:item'):
|
||||||
href = elem.get('href')
|
href = elem.get('href')
|
||||||
if not self.container.exists(href):
|
if not self.container.exists(href):
|
||||||
self.logger.log_warn(u'Manifest item %r not found.' % href)
|
self.logger.warn(u'Manifest item %r not found.' % href)
|
||||||
continue
|
continue
|
||||||
manifest.add(elem.get('id'), href, elem.get('media-type'),
|
manifest.add(elem.get('id'), href, elem.get('media-type'),
|
||||||
elem.get('fallback'))
|
elem.get('fallback'))
|
||||||
@ -702,7 +780,7 @@ class OEBBook(object):
|
|||||||
for elem in xpath(opf, '/o2:package/o2:spine/o2:itemref'):
|
for elem in xpath(opf, '/o2:package/o2:spine/o2:itemref'):
|
||||||
idref = elem.get('idref')
|
idref = elem.get('idref')
|
||||||
if idref not in self.manifest:
|
if idref not in self.manifest:
|
||||||
self.logger.log_warn(u'Spine item %r not found.' % idref)
|
self.logger.warn(u'Spine item %r not found.' % idref)
|
||||||
continue
|
continue
|
||||||
item = self.manifest[idref]
|
item = self.manifest[idref]
|
||||||
spine.add(item, elem.get('linear'))
|
spine.add(item, elem.get('linear'))
|
||||||
@ -721,7 +799,7 @@ class OEBBook(object):
|
|||||||
href = elem.get('href')
|
href = elem.get('href')
|
||||||
path, frag = urldefrag(href)
|
path, frag = urldefrag(href)
|
||||||
if path not in self.manifest.hrefs:
|
if path not in self.manifest.hrefs:
|
||||||
self.logger.log_warn(u'Guide reference %r not found' % href)
|
self.logger.warn(u'Guide reference %r not found' % href)
|
||||||
continue
|
continue
|
||||||
guide.add(elem.get('type'), elem.get('title'), href)
|
guide.add(elem.get('type'), elem.get('title'), href)
|
||||||
|
|
||||||
@ -826,20 +904,27 @@ class OEBBook(object):
|
|||||||
|
|
||||||
def _ensure_cover_image(self):
|
def _ensure_cover_image(self):
|
||||||
cover = None
|
cover = None
|
||||||
|
spine0 = self.spine[0]
|
||||||
|
html = spine0.data
|
||||||
if self.metadata.cover:
|
if self.metadata.cover:
|
||||||
id = str(self.metadata.cover[0])
|
id = str(self.metadata.cover[0])
|
||||||
cover = self.manifest[id]
|
cover = self.manifest.ids[id]
|
||||||
elif MS_COVER_TYPE in self.guide:
|
elif MS_COVER_TYPE in self.guide:
|
||||||
href = self.guide[MS_COVER_TYPE].href
|
href = self.guide[MS_COVER_TYPE].href
|
||||||
cover = self.manifest.hrefs[href]
|
cover = self.manifest.hrefs[href]
|
||||||
elif 'cover' in self.guide:
|
elif xpath(html, '//h:img[position()=1]'):
|
||||||
href = self.guide['cover'].href
|
img = xpath(html, '//h:img[position()=1]')[0]
|
||||||
|
href = img.get('src')
|
||||||
cover = self.manifest.hrefs[href]
|
cover = self.manifest.hrefs[href]
|
||||||
else:
|
elif xpath(html, '//h:object[position()=1]'):
|
||||||
html = self.spine[0].data
|
object = xpath(html, '//h:object[position()=1]')[0]
|
||||||
imgs = xpath(html, '//h:img[position()=1]')
|
href = object.get('data')
|
||||||
href = imgs[0].get('src') if imgs else None
|
cover = self.manifest.hrefs[href]
|
||||||
cover = self.manifest.hrefs[href] if href else None
|
elif xpath(html, '//svg:svg[position()=1]'):
|
||||||
|
svg = copy.deepcopy(xpath(html, '//svg:svg[position()=1]')[0])
|
||||||
|
href = os.path.splitext(spine0.href)[0] + '.svg'
|
||||||
|
id, href = self.manifest.generate(spine0.id, href)
|
||||||
|
cover = self.manifest.add(id, href, SVG_MIME, data=svg)
|
||||||
if cover and not self.metadata.cover:
|
if cover and not self.metadata.cover:
|
||||||
self.metadata.add('cover', cover.id)
|
self.metadata.add('cover', cover.id)
|
||||||
|
|
||||||
@ -913,7 +998,6 @@ class OEBBook(object):
|
|||||||
NCX_MIME: (href, ncx)}
|
NCX_MIME: (href, ncx)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv=sys.argv):
|
def main(argv=sys.argv):
|
||||||
for arg in argv[1:]:
|
for arg in argv[1:]:
|
||||||
oeb = OEBBook(arg)
|
oeb = OEBBook(arg)
|
||||||
|
@ -35,7 +35,8 @@
|
|||||||
*
|
*
|
||||||
* ***** END LICENSE BLOCK ***** */
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
@namespace url(http://www.w3.org/1999/xhtml); /* set default namespace to HTML */
|
@namespace url(http://www.w3.org/1999/xhtml);
|
||||||
|
@namespace svg url(http://www.w3.org/2000/svg);
|
||||||
|
|
||||||
/* blocks */
|
/* blocks */
|
||||||
|
|
||||||
@ -399,3 +400,8 @@ br {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Images, embedded object, and SVG size defaults */
|
||||||
|
img, object, svg|svg {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
65
src/calibre/ebooks/oeb/profile.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
'''
|
||||||
|
Device profiles.
|
||||||
|
'''
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
||||||
|
|
||||||
|
from itertools import izip
|
||||||
|
|
||||||
|
FONT_SIZES = [('xx-small', 1),
|
||||||
|
('x-small', None),
|
||||||
|
('small', 2),
|
||||||
|
('medium', 3),
|
||||||
|
('large', 4),
|
||||||
|
('x-large', 5),
|
||||||
|
('xx-large', 6),
|
||||||
|
(None, 7)]
|
||||||
|
|
||||||
|
|
||||||
|
class Profile(object):
|
||||||
|
def __init__(self, width, height, dpi, fbase, fsizes):
|
||||||
|
self.width = (float(width) / dpi) * 72.
|
||||||
|
self.height = (float(height) / dpi) * 72.
|
||||||
|
self.dpi = float(dpi)
|
||||||
|
self.fbase = float(fbase)
|
||||||
|
self.fsizes = []
|
||||||
|
for (name, num), size in izip(FONT_SIZES, fsizes):
|
||||||
|
self.fsizes.append((name, num, float(size)))
|
||||||
|
self.fnames = dict((name, sz) for name, _, sz in self.fsizes if name)
|
||||||
|
self.fnums = dict((num, sz) for _, num, sz in self.fsizes if num)
|
||||||
|
|
||||||
|
|
||||||
|
PROFILES = {
|
||||||
|
'PRS505':
|
||||||
|
Profile(width=584, height=754, dpi=168.451, fbase=12,
|
||||||
|
fsizes=[7.5, 9, 10, 12, 15.5, 20, 22, 24]),
|
||||||
|
|
||||||
|
'MSReader':
|
||||||
|
Profile(width=480, height=652, dpi=100.0, fbase=13,
|
||||||
|
fsizes=[10, 11, 13, 16, 18, 20, 22, 26]),
|
||||||
|
|
||||||
|
# Not really, but let's pretend
|
||||||
|
'MobiDesktop':
|
||||||
|
Profile(width=280, height=300, dpi=96, fbase=18,
|
||||||
|
fsizes=[14, 14, 16, 18, 20, 22, 22, 24]),
|
||||||
|
|
||||||
|
# No clue on usable screen size and DPI
|
||||||
|
'CybookG3':
|
||||||
|
Profile(width=584, height=754, dpi=168.451, fbase=12,
|
||||||
|
fsizes=[9, 10, 11, 12, 14, 17, 20, 24]),
|
||||||
|
|
||||||
|
'Firefox':
|
||||||
|
Profile(width=800, height=600, dpi=100.0, fbase=12,
|
||||||
|
fsizes=[5, 7, 9, 12, 13.5, 17, 20, 22, 24])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Context(object):
|
||||||
|
def __init__(self, source, dest):
|
||||||
|
if source in PROFILES:
|
||||||
|
source = PROFILES[source]
|
||||||
|
if dest in PROFILES:
|
||||||
|
dest = PROFILES[dest]
|
||||||
|
self.source = source
|
||||||
|
self.dest = dest
|
@ -24,6 +24,7 @@ from lxml import etree
|
|||||||
from lxml.cssselect import css_to_xpath, ExpressionError
|
from lxml.cssselect import css_to_xpath, ExpressionError
|
||||||
from calibre.ebooks.oeb.base import XHTML, XHTML_NS, CSS_MIME, OEB_STYLES
|
from calibre.ebooks.oeb.base import XHTML, XHTML_NS, CSS_MIME, OEB_STYLES
|
||||||
from calibre.ebooks.oeb.base import barename, urlnormalize
|
from calibre.ebooks.oeb.base import barename, urlnormalize
|
||||||
|
from calibre.ebooks.oeb.profile import PROFILES
|
||||||
from calibre.resources import html_css
|
from calibre.resources import html_css
|
||||||
|
|
||||||
XHTML_CSS_NAMESPACE = '@namespace "%s";\n' % XHTML_NS
|
XHTML_CSS_NAMESPACE = '@namespace "%s";\n' % XHTML_NS
|
||||||
@ -75,7 +76,7 @@ DEFAULTS = {'azimuth': 'center', 'background-attachment': 'scroll',
|
|||||||
'50', 'right': 'auto', 'speak': 'normal', 'speak-header': 'once',
|
'50', 'right': 'auto', 'speak': 'normal', 'speak-header': 'once',
|
||||||
'speak-numeral': 'continuous', 'speak-punctuation': 'none',
|
'speak-numeral': 'continuous', 'speak-punctuation': 'none',
|
||||||
'speech-rate': 'medium', 'stress': '50', 'table-layout': 'auto',
|
'speech-rate': 'medium', 'stress': '50', 'table-layout': 'auto',
|
||||||
'text-align': 'left', 'text-decoration': 'none', 'text-indent':
|
'text-align': 'auto', 'text-decoration': 'none', 'text-indent':
|
||||||
0, 'text-transform': 'none', 'top': 'auto', 'unicode-bidi':
|
0, 'text-transform': 'none', 'top': 'auto', 'unicode-bidi':
|
||||||
'normal', 'vertical-align': 'baseline', 'visibility': 'visible',
|
'normal', 'vertical-align': 'baseline', 'visibility': 'visible',
|
||||||
'voice-family': 'default', 'volume': 'medium', 'white-space':
|
'voice-family': 'default', 'volume': 'medium', 'white-space':
|
||||||
@ -85,23 +86,19 @@ DEFAULTS = {'azimuth': 'center', 'background-attachment': 'scroll',
|
|||||||
FONT_SIZE_NAMES = set(['xx-small', 'x-small', 'small', 'medium', 'large',
|
FONT_SIZE_NAMES = set(['xx-small', 'x-small', 'small', 'medium', 'large',
|
||||||
'x-large', 'xx-large'])
|
'x-large', 'xx-large'])
|
||||||
|
|
||||||
FONT_SIZES = [('xx-small', 1),
|
|
||||||
('x-small', None),
|
|
||||||
('small', 2),
|
|
||||||
('medium', 3),
|
|
||||||
('large', 4),
|
|
||||||
('x-large', 5),
|
|
||||||
('xx-large', 6),
|
|
||||||
(None, 7)]
|
|
||||||
|
|
||||||
|
|
||||||
XPNSMAP = {'h': XHTML_NS,}
|
XPNSMAP = {'h': XHTML_NS,}
|
||||||
def xpath(elem, expr):
|
def xpath(elem, expr):
|
||||||
return elem.xpath(expr, namespaces=XPNSMAP)
|
return elem.xpath(expr, namespaces=XPNSMAP)
|
||||||
|
|
||||||
class CSSSelector(etree.XPath):
|
class CSSSelector(etree.XPath):
|
||||||
|
MIN_SPACE_RE = re.compile(r' *([>~+]) *')
|
||||||
|
LOCAL_NAME_RE = re.compile(r"(?<!local-)name[(][)] *= *'[^:]+:")
|
||||||
|
|
||||||
def __init__(self, css, namespaces=XPNSMAP):
|
def __init__(self, css, namespaces=XPNSMAP):
|
||||||
|
css = self.MIN_SPACE_RE.sub(r'\1', css)
|
||||||
path = css_to_xpath(css)
|
path = css_to_xpath(css)
|
||||||
|
path = self.LOCAL_NAME_RE.sub(r"local-name() = '", path)
|
||||||
etree.XPath.__init__(self, path, namespaces=namespaces)
|
etree.XPath.__init__(self, path, namespaces=namespaces)
|
||||||
self.css = css
|
self.css = css
|
||||||
|
|
||||||
@ -112,28 +109,11 @@ class CSSSelector(etree.XPath):
|
|||||||
self.css)
|
self.css)
|
||||||
|
|
||||||
|
|
||||||
class Page(object):
|
|
||||||
def __init__(self, width, height, dpi, fbase, fsizes):
|
|
||||||
self.width = (float(width) / dpi) * 72.
|
|
||||||
self.height = (float(height) / dpi) * 72.
|
|
||||||
self.dpi = float(dpi)
|
|
||||||
self.fbase = float(fbase)
|
|
||||||
self.fsizes = []
|
|
||||||
for (name, num), size in izip(FONT_SIZES, fsizes):
|
|
||||||
self.fsizes.append((name, num, float(size)))
|
|
||||||
self.fnames = dict((name, sz) for name, _, sz in self.fsizes if name)
|
|
||||||
self.fnums = dict((num, sz) for _, num, sz in self.fsizes if num)
|
|
||||||
|
|
||||||
class Profiles(object):
|
|
||||||
PRS505 = Page(584, 754, 168.451, 12, [7.5, 9, 10, 12, 15.5, 20, 22, 24])
|
|
||||||
MSLIT = Page(652, 480, 100.0, 13, [10, 11, 13, 16, 18, 20, 22, 26])
|
|
||||||
|
|
||||||
|
|
||||||
class Stylizer(object):
|
class Stylizer(object):
|
||||||
STYLESHEETS = {}
|
STYLESHEETS = {}
|
||||||
|
|
||||||
def __init__(self, tree, path, oeb, page=Profiles.PRS505):
|
def __init__(self, tree, path, oeb, profile=PROFILES['PRS505']):
|
||||||
self.page = page
|
self.profile = profile
|
||||||
base = os.path.dirname(path)
|
base = os.path.dirname(path)
|
||||||
basename = os.path.basename(path)
|
basename = os.path.basename(path)
|
||||||
cssname = os.path.splitext(basename)[0] + '.css'
|
cssname = os.path.splitext(basename)[0] + '.css'
|
||||||
@ -183,9 +163,8 @@ class Stylizer(object):
|
|||||||
continue
|
continue
|
||||||
for elem in selector(tree):
|
for elem in selector(tree):
|
||||||
self.style(elem)._update_cssdict(cssdict)
|
self.style(elem)._update_cssdict(cssdict)
|
||||||
for elem in tree.xpath('//*[@style]'):
|
for elem in xpath(tree, '//h:*[@style]'):
|
||||||
self.style(elem)._apply_style_tag()
|
self.style(elem)._apply_style_attr()
|
||||||
|
|
||||||
|
|
||||||
def flatten_rule(self, rule, href, index):
|
def flatten_rule(self, rule, href, index):
|
||||||
results = []
|
results = []
|
||||||
@ -215,7 +194,7 @@ class Stylizer(object):
|
|||||||
size = style['font-size']
|
size = style['font-size']
|
||||||
if size == 'normal': size = 'medium'
|
if size == 'normal': size = 'medium'
|
||||||
if size in FONT_SIZE_NAMES:
|
if size in FONT_SIZE_NAMES:
|
||||||
style['font-size'] = "%dpt" % self.page.fnames[size]
|
style['font-size'] = "%dpt" % self.profile.fnames[size]
|
||||||
return style
|
return style
|
||||||
|
|
||||||
def _normalize_edge(self, cssvalue, name):
|
def _normalize_edge(self, cssvalue, name):
|
||||||
@ -284,24 +263,31 @@ class Stylizer(object):
|
|||||||
class Style(object):
|
class Style(object):
|
||||||
def __init__(self, element, stylizer):
|
def __init__(self, element, stylizer):
|
||||||
self._element = element
|
self._element = element
|
||||||
self._page = stylizer.page
|
self._profile = stylizer.profile
|
||||||
self._stylizer = stylizer
|
self._stylizer = stylizer
|
||||||
self._style = {}
|
self._style = {}
|
||||||
|
self._fontSize = None
|
||||||
|
self._width = None
|
||||||
|
self._height = None
|
||||||
stylizer._styles[element] = self
|
stylizer._styles[element] = self
|
||||||
|
|
||||||
def _update_cssdict(self, cssdict):
|
def _update_cssdict(self, cssdict):
|
||||||
self._style.update(cssdict)
|
self._style.update(cssdict)
|
||||||
|
|
||||||
def _apply_style_tag(self):
|
def _apply_style_attr(self):
|
||||||
attrib = self._element.attrib
|
attrib = self._element.attrib
|
||||||
if 'style' in attrib:
|
if 'style' in attrib:
|
||||||
style = CSSStyleDeclaration(attrib['style'])
|
style = CSSStyleDeclaration(attrib['style'])
|
||||||
self._style.update(self._stylizer.flatten_style(style))
|
self._style.update(self._stylizer.flatten_style(style))
|
||||||
|
|
||||||
def _has_parent(self):
|
def _has_parent(self):
|
||||||
parent = self._element.getparent()
|
return (self._element.getparent() is not None)
|
||||||
return (parent is not None) \
|
|
||||||
and (parent in self._stylizer._styles)
|
def _get_parent(self):
|
||||||
|
elem = self._element.getparent()
|
||||||
|
if elem is None:
|
||||||
|
return None
|
||||||
|
return self._stylizer.style(elem)
|
||||||
|
|
||||||
def __getitem__(self, name):
|
def __getitem__(self, name):
|
||||||
domname = cssproperties._toDOMname(name)
|
domname = cssproperties._toDOMname(name)
|
||||||
@ -316,8 +302,8 @@ class Style(object):
|
|||||||
if (result == 'inherit'
|
if (result == 'inherit'
|
||||||
or (result is None and name in INHERITED
|
or (result is None and name in INHERITED
|
||||||
and self._has_parent())):
|
and self._has_parent())):
|
||||||
styles = self._stylizer._styles
|
stylizer = self._stylizer
|
||||||
result = styles[self._element.getparent()]._get(name)
|
result = stylizer.style(self._element.getparent())._get(name)
|
||||||
if result is None:
|
if result is None:
|
||||||
result = DEFAULTS[name]
|
result = DEFAULTS[name]
|
||||||
return result
|
return result
|
||||||
@ -340,7 +326,7 @@ class Style(object):
|
|||||||
base = base or self.width
|
base = base or self.width
|
||||||
result = (value/100.0) * base
|
result = (value/100.0) * base
|
||||||
elif unit == 'px':
|
elif unit == 'px':
|
||||||
result = value * 72.0 / self._page.dpi
|
result = value * 72.0 / self._profile.dpi
|
||||||
elif unit == 'in':
|
elif unit == 'in':
|
||||||
result = value * 72.0
|
result = value * 72.0
|
||||||
elif unit == 'pt':
|
elif unit == 'pt':
|
||||||
@ -358,23 +344,22 @@ class Style(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def fontSize(self):
|
def fontSize(self):
|
||||||
def normalize_fontsize(value, base=None):
|
def normalize_fontsize(value, base):
|
||||||
result = None
|
result = None
|
||||||
factor = None
|
factor = None
|
||||||
if value == 'inherit':
|
if value == 'inherit':
|
||||||
# We should only see this if the root element
|
value = base
|
||||||
value = self._page.fbase
|
|
||||||
if value in FONT_SIZE_NAMES:
|
if value in FONT_SIZE_NAMES:
|
||||||
result = self._page.fnames[value]
|
result = self._profile.fnames[value]
|
||||||
elif value == 'smaller':
|
elif value == 'smaller':
|
||||||
factor = 1.0/1.2
|
factor = 1.0/1.2
|
||||||
for _, _, size in self._page.fsizes:
|
for _, _, size in self._profile.fsizes:
|
||||||
if base <= size: break
|
if base <= size: break
|
||||||
factor = None
|
factor = None
|
||||||
result = size
|
result = size
|
||||||
elif value == 'larger':
|
elif value == 'larger':
|
||||||
factor = 1.2
|
factor = 1.2
|
||||||
for _, _, size in reversed(self._page.fsizes):
|
for _, _, size in reversed(self._profile.fsizes):
|
||||||
if base >= size: break
|
if base >= size: break
|
||||||
factor = None
|
factor = None
|
||||||
result = size
|
result = size
|
||||||
@ -385,39 +370,62 @@ class Style(object):
|
|||||||
if factor:
|
if factor:
|
||||||
result = factor * base
|
result = factor * base
|
||||||
return result
|
return result
|
||||||
|
if self._fontSize is None:
|
||||||
result = None
|
result = None
|
||||||
if self._has_parent():
|
parent = self._get_parent()
|
||||||
styles = self._stylizer._styles
|
if parent is not None:
|
||||||
base = styles[self._element.getparent()].fontSize
|
base = parent.fontSize
|
||||||
else:
|
else:
|
||||||
base = self._page.fbase
|
base = self._profile.fbase
|
||||||
if 'font-size' in self._style:
|
if 'font-size' in self._style:
|
||||||
size = self._style['font-size']
|
size = self._style['font-size']
|
||||||
result = normalize_fontsize(size, base)
|
result = normalize_fontsize(size, base)
|
||||||
else:
|
else:
|
||||||
result = base
|
result = base
|
||||||
self.__dict__['fontSize'] = result
|
self._fontSize = result
|
||||||
return result
|
return self._fontSize
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def width(self):
|
def width(self):
|
||||||
|
if self._width is None:
|
||||||
result = None
|
result = None
|
||||||
base = None
|
base = None
|
||||||
if self._has_parent():
|
parent = self._get_parent()
|
||||||
styles = self._stylizer._styles
|
if parent is not None:
|
||||||
base = styles[self._element.getparent()].width
|
base = parent.width
|
||||||
else:
|
else:
|
||||||
base = self._page.width
|
base = self._profile.width
|
||||||
if 'width' in self._style:
|
if 'width' is self._element.attrib:
|
||||||
|
width = self._element.attrib['width']
|
||||||
|
elif 'width' in self._style:
|
||||||
width = self._style['width']
|
width = self._style['width']
|
||||||
if width == 'auto':
|
|
||||||
result = base
|
|
||||||
else:
|
else:
|
||||||
|
result = base
|
||||||
|
if not result:
|
||||||
result = self._unit_convert(width, base=base)
|
result = self._unit_convert(width, base=base)
|
||||||
|
self._width = result
|
||||||
|
return self._width
|
||||||
|
|
||||||
|
@property
|
||||||
|
def height(self):
|
||||||
|
if self._height is None:
|
||||||
|
result = None
|
||||||
|
base = None
|
||||||
|
parent = self._get_parent()
|
||||||
|
if parent is not None:
|
||||||
|
base = parent.height
|
||||||
|
else:
|
||||||
|
base = self._profile.height
|
||||||
|
if 'height' is self._element.attrib:
|
||||||
|
height = self._element.attrib['height']
|
||||||
|
elif 'height' in self._style:
|
||||||
|
height = self._style['height']
|
||||||
else:
|
else:
|
||||||
result = base
|
result = base
|
||||||
self.__dict__['width'] = result
|
if not result:
|
||||||
return result
|
result = self._unit_convert(height, base=base)
|
||||||
|
self._height = result
|
||||||
|
return self._height
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
items = self._style.items()
|
items = self._style.items()
|
||||||
|
0
src/calibre/ebooks/oeb/transforms/__init__.py
Normal file
265
src/calibre/ebooks/oeb/transforms/flatcss.py
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
'''
|
||||||
|
CSS flattening transform.
|
||||||
|
'''
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import operator
|
||||||
|
import math
|
||||||
|
from itertools import chain
|
||||||
|
from collections import defaultdict
|
||||||
|
from lxml import etree
|
||||||
|
from calibre.ebooks.oeb.base import XHTML, XHTML_NS
|
||||||
|
from calibre.ebooks.oeb.base import CSS_MIME, OEB_STYLES
|
||||||
|
from calibre.ebooks.oeb.base import namespace, barename
|
||||||
|
from calibre.ebooks.oeb.base import OEBBook
|
||||||
|
from calibre.ebooks.oeb.stylizer import Stylizer
|
||||||
|
|
||||||
|
COLLAPSE = re.compile(r'[ \t\r\n\v]+')
|
||||||
|
STRIPNUM = re.compile(r'[-0-9]+$')
|
||||||
|
|
||||||
|
class KeyMapper(object):
|
||||||
|
def __init__(self, sbase, dbase, dkey):
|
||||||
|
self.sbase = float(sbase)
|
||||||
|
self.dprop = [(self.relate(x, dbase), float(x)) for x in dkey]
|
||||||
|
self.cache = {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def relate(size, base):
|
||||||
|
size = float(size)
|
||||||
|
base = float(base)
|
||||||
|
if size == base: return 0
|
||||||
|
sign = -1 if size < base else 1
|
||||||
|
endp = 0 if size < base else 36
|
||||||
|
diff = (abs(base - size) * 3) + ((36 - size) / 100)
|
||||||
|
logb = abs(base - endp)
|
||||||
|
return sign * math.log(diff, logb)
|
||||||
|
|
||||||
|
def __getitem__(self, ssize):
|
||||||
|
if ssize in self.cache:
|
||||||
|
return self.cache[ssize]
|
||||||
|
dsize = self.map(ssize)
|
||||||
|
self.cache[ssize] = dsize
|
||||||
|
return dsize
|
||||||
|
|
||||||
|
def map(self, ssize):
|
||||||
|
sbase = self.sbase
|
||||||
|
prop = self.relate(ssize, sbase)
|
||||||
|
diff = [(abs(prop - p), s) for p, s in self.dprop]
|
||||||
|
dsize = min(diff)[1]
|
||||||
|
return dsize
|
||||||
|
|
||||||
|
class ScaleMapper(object):
|
||||||
|
def __init__(self, sbase, dbase):
|
||||||
|
self.dscale = float(dbase) / float(sbase)
|
||||||
|
|
||||||
|
def __getitem__(self, ssize):
|
||||||
|
dsize = ssize * self.dscale
|
||||||
|
return dsize
|
||||||
|
|
||||||
|
class NullMapper(object):
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __getitem__(self, ssize):
|
||||||
|
return ssize
|
||||||
|
|
||||||
|
def FontMapper(sbase=None, dbase=None, dkey=None):
|
||||||
|
if sbase and dbase and dkey:
|
||||||
|
return KeyMapper(sbase, dbase, dkey)
|
||||||
|
elif sbase and dbase:
|
||||||
|
return ScaleMapper(sbase, dbase)
|
||||||
|
else:
|
||||||
|
return NullMapper()
|
||||||
|
|
||||||
|
|
||||||
|
class CSSFlattener(object):
|
||||||
|
def __init__(self, fbase=None, fkey=None, lineh=None, unfloat=False,
|
||||||
|
untable=False):
|
||||||
|
self.fbase = fbase
|
||||||
|
self.fkey = fkey
|
||||||
|
self.lineh = lineh
|
||||||
|
self.unfloat = unfloat
|
||||||
|
self.untable = untable
|
||||||
|
|
||||||
|
def transform(self, oeb, context):
|
||||||
|
oeb.logger.info('Flattening CSS and remapping font sizes...')
|
||||||
|
self.oeb = oeb
|
||||||
|
self.context = context
|
||||||
|
self.stylize_spine()
|
||||||
|
self.sbase = self.baseline_spine() if self.fbase else None
|
||||||
|
self.fmap = FontMapper(self.sbase, self.fbase, self.fkey)
|
||||||
|
self.flatten_spine()
|
||||||
|
|
||||||
|
def stylize_spine(self):
|
||||||
|
self.stylizers = {}
|
||||||
|
profile = self.context.source
|
||||||
|
for item in self.oeb.spine:
|
||||||
|
html = item.data
|
||||||
|
stylizer = Stylizer(html, item.href, self.oeb, profile)
|
||||||
|
self.stylizers[item] = stylizer
|
||||||
|
|
||||||
|
def baseline_node(self, node, stylizer, sizes, csize):
|
||||||
|
csize = stylizer.style(node)['font-size']
|
||||||
|
if node.text:
|
||||||
|
sizes[csize] += len(COLLAPSE.sub(' ', node.text))
|
||||||
|
for child in node:
|
||||||
|
self.baseline_node(child, stylizer, sizes, csize)
|
||||||
|
if child.tail:
|
||||||
|
sizes[csize] += len(COLLAPSE.sub(' ', child.tail))
|
||||||
|
|
||||||
|
def baseline_spine(self):
|
||||||
|
sizes = defaultdict(float)
|
||||||
|
for item in self.oeb.spine:
|
||||||
|
html = item.data
|
||||||
|
stylizer = self.stylizers[item]
|
||||||
|
body = html.find(XHTML('body'))
|
||||||
|
fsize = self.context.source.fbase
|
||||||
|
self.baseline_node(body, stylizer, sizes, fsize)
|
||||||
|
sbase = max(sizes.items(), key=operator.itemgetter(1))[0]
|
||||||
|
return sbase
|
||||||
|
|
||||||
|
def clean_edges(self, cssdict, style, fsize):
|
||||||
|
slineh = self.sbase * 1.26
|
||||||
|
dlineh = self.lineh
|
||||||
|
for kind in ('margin', 'padding'):
|
||||||
|
for edge in ('bottom', 'top'):
|
||||||
|
property = "%s-%s" % (kind, edge)
|
||||||
|
if property not in cssdict: continue
|
||||||
|
if '%' in cssdict[property]: continue
|
||||||
|
value = style[property]
|
||||||
|
if value == 0:
|
||||||
|
continue
|
||||||
|
elif value <= slineh:
|
||||||
|
cssdict[property] = "%0.5fem" % (dlineh / fsize)
|
||||||
|
else:
|
||||||
|
value = round(value / slineh) * dlineh
|
||||||
|
cssdict[property] = "%0.5fem" % (value / fsize)
|
||||||
|
|
||||||
|
def flatten_node(self, node, stylizer, names, styles, psize, left=0):
|
||||||
|
if not isinstance(node.tag, basestring) \
|
||||||
|
or namespace(node.tag) != XHTML_NS:
|
||||||
|
return
|
||||||
|
tag = barename(node.tag)
|
||||||
|
style = stylizer.style(node)
|
||||||
|
cssdict = style.cssdict()
|
||||||
|
if 'align' in node.attrib:
|
||||||
|
cssdict['text-align'] = node.attrib['align']
|
||||||
|
del node.attrib['align']
|
||||||
|
if node.tag == XHTML('font'):
|
||||||
|
node.tag = XHTML('span')
|
||||||
|
if 'size' in node.attrib:
|
||||||
|
size = node.attrib['size']
|
||||||
|
if size.startswith('+'):
|
||||||
|
cssdict['font-size'] = 'larger'
|
||||||
|
elif size.startswith('-'):
|
||||||
|
cssdict['font-size'] = 'smaller'
|
||||||
|
else:
|
||||||
|
fnums = self.context.source.fnums
|
||||||
|
cssdict['font-size'] = fnums[int(size)]
|
||||||
|
del node.attrib['size']
|
||||||
|
if 'color' in node.attrib:
|
||||||
|
cssdict['color'] = node.attrib['color']
|
||||||
|
del node.attrib['color']
|
||||||
|
if 'bgcolor' in node.attrib:
|
||||||
|
cssdict['background-color'] = node.attrib['bgcolor']
|
||||||
|
del node.attrib['bgcolor']
|
||||||
|
if cssdict:
|
||||||
|
if 'font-size' in cssdict:
|
||||||
|
fsize = self.fmap[style['font-size']]
|
||||||
|
cssdict['font-size'] = "%0.5fem" % (fsize / psize)
|
||||||
|
psize = fsize
|
||||||
|
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']
|
||||||
|
cssdict['margin-left'] = "%d%%" % (percent * 100)
|
||||||
|
left -= style['text-indent']
|
||||||
|
if self.unfloat and 'float' in cssdict \
|
||||||
|
and tag not in ('img', 'object') \
|
||||||
|
and cssdict.get('display', 'none') != 'none':
|
||||||
|
del cssdict['display']
|
||||||
|
if self.untable and 'display' in cssdict \
|
||||||
|
and cssdict['display'].startswith('table'):
|
||||||
|
display = cssdict['display']
|
||||||
|
if display == 'table-cell':
|
||||||
|
cssdict['display'] = 'inline'
|
||||||
|
else:
|
||||||
|
cssdict['display'] = 'block'
|
||||||
|
if 'vertical-align' in cssdict \
|
||||||
|
and cssdict['vertical-align'] == 'sup':
|
||||||
|
cssdict['vertical-align'] = 'super'
|
||||||
|
if self.lineh and 'line-height' not in cssdict:
|
||||||
|
lineh = self.lineh / psize
|
||||||
|
cssdict['line-height'] = "%0.5fem" % lineh
|
||||||
|
if cssdict:
|
||||||
|
items = cssdict.items()
|
||||||
|
items.sort()
|
||||||
|
css = u';\n'.join(u'%s: %s' % (key, val) for key, val in items)
|
||||||
|
klass = STRIPNUM.sub('', node.get('class', 'calibre').split()[0])
|
||||||
|
if css in styles:
|
||||||
|
match = styles[css]
|
||||||
|
else:
|
||||||
|
match = klass + str(names[klass] or '')
|
||||||
|
styles[css] = match
|
||||||
|
names[klass] += 1
|
||||||
|
node.attrib['class'] = match
|
||||||
|
elif 'class' in node.attrib:
|
||||||
|
del node.attrib['class']
|
||||||
|
if 'style' in node.attrib:
|
||||||
|
del node.attrib['style']
|
||||||
|
for child in node:
|
||||||
|
self.flatten_node(child, stylizer, names, styles, psize, left)
|
||||||
|
|
||||||
|
def flatten_head(self, head, stylizer, href):
|
||||||
|
for node in head:
|
||||||
|
if node.tag == XHTML('link') \
|
||||||
|
and node.get('rel', 'stylesheet') == 'stylesheet' \
|
||||||
|
and node.get('type', CSS_MIME) in OEB_STYLES:
|
||||||
|
head.remove(node)
|
||||||
|
elif node.tag == XHTML('style') \
|
||||||
|
and node.get('type', CSS_MIME) in OEB_STYLES:
|
||||||
|
head.remove(node)
|
||||||
|
etree.SubElement(head, XHTML('link'),
|
||||||
|
rel='stylesheet', type=CSS_MIME, href=href)
|
||||||
|
if stylizer.page_rule:
|
||||||
|
items = stylizer.page_rule.items()
|
||||||
|
items.sort()
|
||||||
|
css = '; '.join("%s: %s" % (key, val) for key, val in items)
|
||||||
|
style = etree.SubElement(head, XHTML('style'), type=CSS_MIME)
|
||||||
|
style.text = "@page { %s; }" % css
|
||||||
|
|
||||||
|
def replace_css(self, css):
|
||||||
|
manifest = self.oeb.manifest
|
||||||
|
id, href = manifest.generate('css', 'stylesheet.css')
|
||||||
|
for item in manifest.values():
|
||||||
|
if item.media_type in OEB_STYLES:
|
||||||
|
manifest.remove(item)
|
||||||
|
item = manifest.add(id, href, CSS_MIME, data=css)
|
||||||
|
return href
|
||||||
|
|
||||||
|
def flatten_spine(self):
|
||||||
|
names = defaultdict(int)
|
||||||
|
styles = {}
|
||||||
|
for item in self.oeb.spine:
|
||||||
|
html = item.data
|
||||||
|
stylizer = self.stylizers[item]
|
||||||
|
body = html.find(XHTML('body'))
|
||||||
|
fsize = self.context.dest.fbase
|
||||||
|
self.flatten_node(body, stylizer, names, styles, fsize)
|
||||||
|
items = [(key, val) for (val, key) in styles.items()]
|
||||||
|
items.sort()
|
||||||
|
css = ''.join(".%s {\n%s;\n}\n\n" % (key, val) for key, val in items)
|
||||||
|
href = self.replace_css(css)
|
||||||
|
for item in self.oeb.spine:
|
||||||
|
html = item.data
|
||||||
|
stylizer = self.stylizers[item]
|
||||||
|
head = html.find(XHTML('head'))
|
||||||
|
self.flatten_head(head, stylizer, href)
|
189
src/calibre/ebooks/oeb/transforms/rasterize.py
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
'''
|
||||||
|
SVG rasterization transform.
|
||||||
|
'''
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from urlparse import urldefrag
|
||||||
|
import base64
|
||||||
|
from lxml import etree
|
||||||
|
from PyQt4.QtCore import Qt
|
||||||
|
from PyQt4.QtCore import QByteArray
|
||||||
|
from PyQt4.QtCore import QBuffer
|
||||||
|
from PyQt4.QtCore import QIODevice
|
||||||
|
from PyQt4.QtGui import QColor
|
||||||
|
from PyQt4.QtGui import QImage
|
||||||
|
from PyQt4.QtGui import QPainter
|
||||||
|
from PyQt4.QtSvg import QSvgRenderer
|
||||||
|
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
|
||||||
|
from calibre.ebooks.oeb.base import xml2str, xpath, namespace, barename
|
||||||
|
from calibre.ebooks.oeb.stylizer import Stylizer
|
||||||
|
|
||||||
|
IMAGE_TAGS = set([XHTML('img'), XHTML('object')])
|
||||||
|
|
||||||
|
class SVGRasterizer(object):
|
||||||
|
def __init__(self):
|
||||||
|
if QApplication.instance() is None:
|
||||||
|
QApplication([])
|
||||||
|
|
||||||
|
def transform(self, oeb, context):
|
||||||
|
oeb.logger.info('Rasterizing SVG images...')
|
||||||
|
self.oeb = oeb
|
||||||
|
self.profile = context.dest
|
||||||
|
self.images = {}
|
||||||
|
self.dataize_manifest()
|
||||||
|
self.rasterize_spine()
|
||||||
|
self.rasterize_cover()
|
||||||
|
|
||||||
|
def rasterize_svg(self, elem, width=0, height=0):
|
||||||
|
data = QByteArray(xml2str(elem))
|
||||||
|
svg = QSvgRenderer(data)
|
||||||
|
size = svg.defaultSize()
|
||||||
|
if size.width() == 100 and size.height() == 100 \
|
||||||
|
and 'viewBox' in elem.attrib:
|
||||||
|
box = [float(x) for x in elem.attrib['viewBox'].split()]
|
||||||
|
size.setWidth(box[2] - box[0])
|
||||||
|
size.setHeight(box[3] - box[1])
|
||||||
|
if width or height:
|
||||||
|
size.scale(width, height, Qt.KeepAspectRatio)
|
||||||
|
image = QImage(size, QImage.Format_ARGB32_Premultiplied)
|
||||||
|
image.fill(QColor("white").rgb())
|
||||||
|
painter = QPainter(image)
|
||||||
|
svg.render(painter)
|
||||||
|
painter.end()
|
||||||
|
array = QByteArray()
|
||||||
|
buffer = QBuffer(array)
|
||||||
|
buffer.open(QIODevice.WriteOnly)
|
||||||
|
image.save(buffer, 'PNG')
|
||||||
|
return str(array)
|
||||||
|
|
||||||
|
def dataize_manifest(self):
|
||||||
|
for item in self.oeb.manifest.values():
|
||||||
|
if item.media_type == SVG_MIME:
|
||||||
|
self.dataize_svg(item)
|
||||||
|
|
||||||
|
def dataize_svg(self, item, svg=None):
|
||||||
|
if svg is None:
|
||||||
|
svg = item.data
|
||||||
|
hrefs = self.oeb.manifest.hrefs
|
||||||
|
for elem in xpath(svg, '//svg:*[@xl:href]'):
|
||||||
|
href = elem.attrib[XLINK('href')]
|
||||||
|
path, frag = urldefrag(href)
|
||||||
|
if not path:
|
||||||
|
continue
|
||||||
|
abshref = item.abshref(path)
|
||||||
|
if abshref not in hrefs:
|
||||||
|
continue
|
||||||
|
linkee = hrefs[abshref]
|
||||||
|
data = base64.encodestring(str(linkee))
|
||||||
|
data = "data:%s;base64,%s" % (linkee.media_type, data)
|
||||||
|
elem.attrib[XLINK('href')] = data
|
||||||
|
return svg
|
||||||
|
|
||||||
|
def rasterize_spine(self):
|
||||||
|
for item in self.oeb.spine:
|
||||||
|
html = item.data
|
||||||
|
stylizer = Stylizer(html, item.href, self.oeb, self.profile)
|
||||||
|
self.rasterize_item(item, stylizer)
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
if image and image.media_type == SVG_MIME:
|
||||||
|
style = stylizer.style(elem)
|
||||||
|
self.rasterize_external(elem, style, item, image)
|
||||||
|
for elem in xpath(html, '//svg:svg'):
|
||||||
|
style = stylizer.style(elem)
|
||||||
|
self.rasterize_inline(elem, style, item)
|
||||||
|
|
||||||
|
def rasterize_inline(self, elem, style, item):
|
||||||
|
width = style['width']
|
||||||
|
if width == 'auto':
|
||||||
|
width = self.profile.width
|
||||||
|
height = style['height']
|
||||||
|
if height == 'auto':
|
||||||
|
height = self.profile.height
|
||||||
|
width = (width / 72) * self.profile.dpi
|
||||||
|
height = (height / 72) * self.profile.dpi
|
||||||
|
elem = self.dataize_svg(item, elem)
|
||||||
|
data = self.rasterize_svg(elem, width, height)
|
||||||
|
manifest = self.oeb.manifest
|
||||||
|
href = os.path.splitext(item.href)[0] + '.png'
|
||||||
|
id, href = manifest.generate(item.id, href)
|
||||||
|
manifest.add(id, href, PNG_MIME, data=data)
|
||||||
|
img = etree.Element(XHTML('img'), src=item.relhref(href))
|
||||||
|
elem.getparent().replace(elem, img)
|
||||||
|
for prop in ('width', 'height'):
|
||||||
|
if prop in elem.attrib:
|
||||||
|
img.attrib[prop] = elem.attrib[prop]
|
||||||
|
|
||||||
|
def rasterize_external(self, elem, style, item, svgitem):
|
||||||
|
width = style['width']
|
||||||
|
if width == 'auto':
|
||||||
|
width = self.profile.width
|
||||||
|
height = style['height']
|
||||||
|
if height == 'auto':
|
||||||
|
height = self.profile.height
|
||||||
|
width = (width / 72) * self.profile.dpi
|
||||||
|
height = (height / 72) * self.profile.dpi
|
||||||
|
data = QByteArray(str(svgitem))
|
||||||
|
svg = QSvgRenderer(data)
|
||||||
|
size = svg.defaultSize()
|
||||||
|
size.scale(width, height, Qt.KeepAspectRatio)
|
||||||
|
key = (svgitem.href, size.width(), size.height())
|
||||||
|
if key in self.images:
|
||||||
|
href = self.images[key]
|
||||||
|
else:
|
||||||
|
logger = self.oeb.logger
|
||||||
|
logger.info('Rasterizing %r to %dx%d'
|
||||||
|
% (svgitem.href, size.width(), size.height()))
|
||||||
|
image = QImage(size, QImage.Format_ARGB32_Premultiplied)
|
||||||
|
image.fill(QColor("white").rgb())
|
||||||
|
painter = QPainter(image)
|
||||||
|
svg.render(painter)
|
||||||
|
painter.end()
|
||||||
|
array = QByteArray()
|
||||||
|
buffer = QBuffer(array)
|
||||||
|
buffer.open(QIODevice.WriteOnly)
|
||||||
|
image.save(buffer, 'PNG')
|
||||||
|
data = str(array)
|
||||||
|
manifest = self.oeb.manifest
|
||||||
|
href = os.path.splitext(svgitem.href)[0] + '.png'
|
||||||
|
id, href = manifest.generate(svgitem.id, href)
|
||||||
|
manifest.add(id, href, PNG_MIME, data=data)
|
||||||
|
self.images[key] = href
|
||||||
|
elem.tag = XHTML('img')
|
||||||
|
elem.attrib['src'] = item.relhref(href)
|
||||||
|
elem.text = None
|
||||||
|
for child in elem:
|
||||||
|
elem.remove(child)
|
||||||
|
|
||||||
|
def rasterize_cover(self):
|
||||||
|
covers = self.oeb.metadata.cover
|
||||||
|
if not covers:
|
||||||
|
return
|
||||||
|
cover = self.oeb.manifest.ids[str(covers[0])]
|
||||||
|
if not cover.media_type == SVG_MIME:
|
||||||
|
return
|
||||||
|
logger = self.oeb.logger
|
||||||
|
logger.info('Rasterizing %r to %dx%d' % (cover.href, 600, 800))
|
||||||
|
data = self.rasterize_svg(cover.data, 600, 800)
|
||||||
|
href = os.path.splitext(cover.href)[0] + '.png'
|
||||||
|
id, href = self.oeb.manifest.generate(cover.id, href)
|
||||||
|
self.oeb.manifest.add(id, href, PNG_MIME, data=data)
|
||||||
|
covers[0].value = id
|
40
src/calibre/ebooks/oeb/transforms/trimmanifest.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
'''
|
||||||
|
OPF manifest trimming transform.
|
||||||
|
'''
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from lxml import etree
|
||||||
|
from calibre.ebooks.oeb.base import XPNSMAP, CSS_MIME
|
||||||
|
|
||||||
|
LINK_SELECTORS = []
|
||||||
|
for expr in ('//h:link/@href', '//h:img/@src', '//h:object/@data',
|
||||||
|
'//*/@xl:href'):
|
||||||
|
LINK_SELECTORS.append(etree.XPath(expr, namespaces=XPNSMAP))
|
||||||
|
|
||||||
|
class ManifestTrimmer(object):
|
||||||
|
def transform(self, oeb, context):
|
||||||
|
oeb.logger.info('Trimming unused files from manifest...')
|
||||||
|
used = set()
|
||||||
|
for item in oeb.spine:
|
||||||
|
used.add(item.href)
|
||||||
|
for selector in LINK_SELECTORS:
|
||||||
|
for href in selector(item.data):
|
||||||
|
used.add(item.abshref(href))
|
||||||
|
# TODO: Things mentioned in CSS
|
||||||
|
# TODO: Things mentioned in SVG
|
||||||
|
# Who knows what people will do...
|
||||||
|
for term in oeb.metadata:
|
||||||
|
for item in oeb.metadata[term]:
|
||||||
|
if item.value in oeb.manifest.hrefs:
|
||||||
|
used.add(item.value)
|
||||||
|
elif item.value in oeb.manifest.ids:
|
||||||
|
used.add(oeb.manifest.ids[item.value].href)
|
||||||
|
for item in oeb.manifest.values():
|
||||||
|
if item.href not in used:
|
||||||
|
oeb.logger.info('Trimming %r from manifest' % item.href)
|
||||||
|
oeb.manifest.remove(item)
|
@ -54,8 +54,12 @@ def _config():
|
|||||||
c.add_opt('autolaunch_server', default=False, help=_('Automatically launch content server on application startup'))
|
c.add_opt('autolaunch_server', default=False, help=_('Automatically launch content server on application startup'))
|
||||||
c.add_opt('oldest_news', default=60, help=_('Oldest news kept in database'))
|
c.add_opt('oldest_news', default=60, help=_('Oldest news kept in database'))
|
||||||
c.add_opt('systray_icon', default=True, help=_('Show system tray icon'))
|
c.add_opt('systray_icon', default=True, help=_('Show system tray icon'))
|
||||||
c.add_opt('upload_news_to_device', default=True, help=_('Upload downloaded news to device'))
|
c.add_opt('upload_news_to_device', default=True,
|
||||||
c.add_opt('delete_news_from_library_on_upload', default=False, help=_('Delete books from library after uploading to device'))
|
help=_('Upload downloaded news to device'))
|
||||||
|
c.add_opt('delete_news_from_library_on_upload', default=False,
|
||||||
|
help=_('Delete books from library after uploading to device'))
|
||||||
|
c.add_opt('separate_cover_flow', default=False,
|
||||||
|
help=_('Show the cover flow in a separate window instead of in the main calibre window'))
|
||||||
return ConfigProxy(c)
|
return ConfigProxy(c)
|
||||||
|
|
||||||
config = _config()
|
config = _config()
|
||||||
|
@ -69,11 +69,11 @@ if pictureflow is not None:
|
|||||||
|
|
||||||
class CoverFlow(pictureflow.PictureFlow):
|
class CoverFlow(pictureflow.PictureFlow):
|
||||||
|
|
||||||
def __init__(self, height=300, parent=None):
|
def __init__(self, height=300, parent=None, text_height=25):
|
||||||
pictureflow.PictureFlow.__init__(self, parent,
|
pictureflow.PictureFlow.__init__(self, parent,
|
||||||
config['cover_flow_queue_length']+1)
|
config['cover_flow_queue_length']+1)
|
||||||
self.setSlideSize(QSize(int(2/3. * height), height))
|
self.setSlideSize(QSize(int(2/3. * height), height))
|
||||||
self.setMinimumSize(QSize(int(2.35*0.67*height), (5/3.)*height+25))
|
self.setMinimumSize(QSize(int(2.35*0.67*height), (5/3.)*height+text_height))
|
||||||
self.setFocusPolicy(Qt.WheelFocus)
|
self.setFocusPolicy(Qt.WheelFocus)
|
||||||
self.setSizePolicy(QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum))
|
self.setSizePolicy(QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum))
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ from calibre.ebooks.epub.iterator import is_supported
|
|||||||
from calibre.library import server_config
|
from calibre.library import server_config
|
||||||
from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \
|
from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \
|
||||||
disable_plugin, customize_plugin, \
|
disable_plugin, customize_plugin, \
|
||||||
plugin_customization, add_plugin
|
plugin_customization, add_plugin, remove_plugin
|
||||||
|
|
||||||
class PluginModel(QAbstractItemModel):
|
class PluginModel(QAbstractItemModel):
|
||||||
|
|
||||||
@ -186,7 +186,6 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
single_format = config['save_to_disk_single_format']
|
single_format = config['save_to_disk_single_format']
|
||||||
self.single_format.setCurrentIndex(BOOK_EXTENSIONS.index(single_format))
|
self.single_format.setCurrentIndex(BOOK_EXTENSIONS.index(single_format))
|
||||||
self.cover_browse.setValue(config['cover_flow_queue_length'])
|
self.cover_browse.setValue(config['cover_flow_queue_length'])
|
||||||
self.confirm_delete.setChecked(config['confirm_delete'])
|
|
||||||
from calibre.translations.compiled import translations
|
from calibre.translations.compiled import translations
|
||||||
from calibre.translations import language_codes
|
from calibre.translations import language_codes
|
||||||
from calibre.startup import get_lang
|
from calibre.startup import get_lang
|
||||||
@ -242,8 +241,10 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
self.plugin_view.setModel(self._plugin_model)
|
self.plugin_view.setModel(self._plugin_model)
|
||||||
self.connect(self.toggle_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='toggle'))
|
self.connect(self.toggle_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='toggle'))
|
||||||
self.connect(self.customize_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='customize'))
|
self.connect(self.customize_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='customize'))
|
||||||
|
self.connect(self.remove_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='remove'))
|
||||||
self.connect(self.button_plugin_browse, SIGNAL('clicked()'), self.find_plugin)
|
self.connect(self.button_plugin_browse, SIGNAL('clicked()'), self.find_plugin)
|
||||||
self.connect(self.button_plugin_add, SIGNAL('clicked()'), self.add_plugin)
|
self.connect(self.button_plugin_add, SIGNAL('clicked()'), self.add_plugin)
|
||||||
|
self.separate_cover_flow.setChecked(config['separate_cover_flow'])
|
||||||
|
|
||||||
def add_plugin(self):
|
def add_plugin(self):
|
||||||
path = unicode(self.plugin_path.text())
|
path = unicode(self.plugin_path.text())
|
||||||
@ -287,6 +288,13 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
if ok:
|
if ok:
|
||||||
customize_plugin(plugin, unicode(text))
|
customize_plugin(plugin, unicode(text))
|
||||||
self._plugin_model.refresh_plugin(plugin)
|
self._plugin_model.refresh_plugin(plugin)
|
||||||
|
if op == 'remove':
|
||||||
|
if remove_plugin(plugin):
|
||||||
|
self._plugin_model.populate()
|
||||||
|
self._plugin_model.reset()
|
||||||
|
else:
|
||||||
|
error_dialog(self, _('Cannot remove builtin plugin'),
|
||||||
|
plugin.name + _(' cannot be removed. It is a builtin plugin. Try disabling it instead.')).exec_()
|
||||||
|
|
||||||
|
|
||||||
def up_column(self):
|
def up_column(self):
|
||||||
@ -385,7 +393,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
config['column_map'] = cols
|
config['column_map'] = cols
|
||||||
config['toolbar_icon_size'] = self.ICON_SIZES[self.toolbar_button_size.currentIndex()]
|
config['toolbar_icon_size'] = self.ICON_SIZES[self.toolbar_button_size.currentIndex()]
|
||||||
config['show_text_in_toolbar'] = bool(self.show_toolbar_text.isChecked())
|
config['show_text_in_toolbar'] = bool(self.show_toolbar_text.isChecked())
|
||||||
config['confirm_delete'] = bool(self.confirm_delete.isChecked())
|
config['separate_cover_flow'] = bool(self.separate_cover_flow.isChecked())
|
||||||
pattern = self.filename_pattern.commit()
|
pattern = self.filename_pattern.commit()
|
||||||
prefs['filename_pattern'] = pattern
|
prefs['filename_pattern'] = pattern
|
||||||
p = {0:'normal', 1:'high', 2:'low'}[self.priority.currentIndex()]
|
p = {0:'normal', 1:'high', 2:'low'}[self.priority.currentIndex()]
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>800</width>
|
<width>800</width>
|
||||||
<height>563</height>
|
<height>570</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle" >
|
<property name="windowTitle" >
|
||||||
@ -127,17 +127,10 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<widget class="QCheckBox" name="confirm_delete" >
|
|
||||||
<property name="text" >
|
|
||||||
<string>Ask for &confirmation before deleting files</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="pdf_metadata" >
|
<widget class="QCheckBox" name="pdf_metadata" >
|
||||||
<property name="toolTip" >
|
<property name="toolTip" >
|
||||||
<string>If you disable this setting, metadatas is guessed from the filename instead. This can be configured in the Advanced section.</string>
|
<string>If you disable this setting, metadata is guessed from the filename instead. This can be configured in the Advanced section.</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text" >
|
<property name="text" >
|
||||||
<string>Read &metadata from files</string>
|
<string>Read &metadata from files</string>
|
||||||
@ -363,7 +356,7 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="0" >
|
<item row="7" column="0" >
|
||||||
<widget class="QGroupBox" name="groupBox_2" >
|
<widget class="QGroupBox" name="groupBox_2" >
|
||||||
<property name="title" >
|
<property name="title" >
|
||||||
<string>Toolbar</string>
|
<string>Toolbar</string>
|
||||||
@ -411,7 +404,7 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0" >
|
<item row="8" column="0" >
|
||||||
<widget class="QGroupBox" name="groupBox" >
|
<widget class="QGroupBox" name="groupBox" >
|
||||||
<property name="title" >
|
<property name="title" >
|
||||||
<string>Select visible &columns in library view</string>
|
<string>Select visible &columns in library view</string>
|
||||||
@ -499,20 +492,27 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="0" >
|
<item row="4" column="0" >
|
||||||
<widget class="QCheckBox" name="sync_news" >
|
<widget class="QCheckBox" name="sync_news" >
|
||||||
<property name="text" >
|
<property name="text" >
|
||||||
<string>Automatically send downloaded &news to ebook reader</string>
|
<string>Automatically send downloaded &news to ebook reader</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0" >
|
<item row="5" column="0" >
|
||||||
<widget class="QCheckBox" name="delete_news" >
|
<widget class="QCheckBox" name="delete_news" >
|
||||||
<property name="text" >
|
<property name="text" >
|
||||||
<string>&Delete news from library when it is sent to reader</string>
|
<string>&Delete news from library when it is sent to reader</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="3" column="0" >
|
||||||
|
<widget class="QCheckBox" name="separate_cover_flow" >
|
||||||
|
<property name="text" >
|
||||||
|
<string>Show cover &browser in a separate window (needs restart)</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="page_2" >
|
<widget class="QWidget" name="page_2" >
|
||||||
@ -811,6 +811,13 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="remove_plugin" >
|
||||||
|
<property name="text" >
|
||||||
|
<string>&Remove plugin</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
31
src/calibre/gui2/dialogs/confirm_delete.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from calibre.gui2 import dynamic
|
||||||
|
from calibre.gui2.dialogs.confirm_delete_ui import Ui_Dialog
|
||||||
|
from PyQt4.Qt import QDialog, SIGNAL
|
||||||
|
|
||||||
|
def _config_name(name):
|
||||||
|
return name + '_again'
|
||||||
|
|
||||||
|
class Dialog(QDialog, Ui_Dialog):
|
||||||
|
|
||||||
|
def __init__(self, msg, name, parent):
|
||||||
|
QDialog.__init__(self, parent)
|
||||||
|
self.setupUi(self)
|
||||||
|
|
||||||
|
self.msg.setText(msg)
|
||||||
|
self.name = name
|
||||||
|
self.connect(self.again, SIGNAL('stateChanged(int)'), self.toggle)
|
||||||
|
|
||||||
|
|
||||||
|
def toggle(self, x):
|
||||||
|
dynamic[_config_name(self.name)] = self.again.isChecked()
|
||||||
|
|
||||||
|
def confirm(msg, name, parent=None):
|
||||||
|
if not dynamic.get(_config_name(name), True):
|
||||||
|
return True
|
||||||
|
d = Dialog(msg, name, parent)
|
||||||
|
return d.exec_() == d.Accepted
|
100
src/calibre/gui2/dialogs/confirm_delete.ui
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<ui version="4.0" >
|
||||||
|
<class>Dialog</class>
|
||||||
|
<widget class="QDialog" name="Dialog" >
|
||||||
|
<property name="geometry" >
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>439</width>
|
||||||
|
<height>300</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle" >
|
||||||
|
<string>Are you sure?</string>
|
||||||
|
</property>
|
||||||
|
<property name="windowIcon" >
|
||||||
|
<iconset resource="../images.qrc" >
|
||||||
|
<normaloff>:/images/dialog_warning.svg</normaloff>:/images/dialog_warning.svg</iconset>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout" >
|
||||||
|
<item row="0" column="0" >
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout" >
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label" >
|
||||||
|
<property name="pixmap" >
|
||||||
|
<pixmap resource="../images.qrc" >:/images/dialog_warning.svg</pixmap>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="msg" >
|
||||||
|
<property name="text" >
|
||||||
|
<string>TextLabel</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap" >
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0" >
|
||||||
|
<widget class="QCheckBox" name="again" >
|
||||||
|
<property name="text" >
|
||||||
|
<string>&Show this warning again</string>
|
||||||
|
</property>
|
||||||
|
<property name="checked" >
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0" >
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox" >
|
||||||
|
<property name="orientation" >
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="standardButtons" >
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources>
|
||||||
|
<include location="../images.qrc" />
|
||||||
|
</resources>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>accepted()</signal>
|
||||||
|
<receiver>Dialog</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel" >
|
||||||
|
<x>248</x>
|
||||||
|
<y>254</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel" >
|
||||||
|
<x>157</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>Dialog</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel" >
|
||||||
|
<x>316</x>
|
||||||
|
<y>260</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel" >
|
||||||
|
<x>286</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
@ -6,7 +6,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>866</width>
|
<width>866</width>
|
||||||
<height>671</height>
|
<height>679</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle" >
|
<property name="windowTitle" >
|
||||||
@ -115,7 +115,7 @@
|
|||||||
<item row="0" column="0" >
|
<item row="0" column="0" >
|
||||||
<widget class="QStackedWidget" name="stack" >
|
<widget class="QStackedWidget" name="stack" >
|
||||||
<property name="currentIndex" >
|
<property name="currentIndex" >
|
||||||
<number>0</number>
|
<number>1</number>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QWidget" name="metadata_page" >
|
<widget class="QWidget" name="metadata_page" >
|
||||||
<layout class="QHBoxLayout" >
|
<layout class="QHBoxLayout" >
|
||||||
@ -652,7 +652,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0" >
|
<item row="2" column="0" >
|
||||||
<widget class="QLabel" name="label_10" >
|
<widget class="QLabel" name="label_10" >
|
||||||
<property name="text" >
|
<property name="text" >
|
||||||
<string>&Header format:</string>
|
<string>&Header format:</string>
|
||||||
@ -662,9 +662,26 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="1" >
|
<item row="2" column="1" >
|
||||||
<widget class="QLineEdit" name="gui_headerformat" />
|
<widget class="QLineEdit" name="gui_headerformat" />
|
||||||
</item>
|
</item>
|
||||||
|
<item row="1" column="1" >
|
||||||
|
<widget class="QSpinBox" name="gui_header_separation" >
|
||||||
|
<property name="suffix" >
|
||||||
|
<string> px</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0" >
|
||||||
|
<widget class="QLabel" name="label_29" >
|
||||||
|
<property name="text" >
|
||||||
|
<string>Header &separation:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy" >
|
||||||
|
<cstring>gui_header_separation</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@ -985,8 +1002,8 @@
|
|||||||
<string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
<string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||||
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
|
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
|
||||||
p, li { white-space: pre-wrap; }
|
p, li { white-space: pre-wrap; }
|
||||||
</style></head><body style=" font-family:'DejaVu Sans'; font-size:10pt; font-weight:400; font-style:normal;">
|
</style></head><body style=" font-family:'Sans Serif'; font-size:10pt; font-weight:400; font-style:normal;">
|
||||||
<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p></body></html></string>
|
<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'DejaVu Sans';"></p></body></html></string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@ -1078,12 +1095,12 @@ p, li { white-space: pre-wrap; }
|
|||||||
<slot>setDisabled(bool)</slot>
|
<slot>setDisabled(bool)</slot>
|
||||||
<hints>
|
<hints>
|
||||||
<hint type="sourcelabel" >
|
<hint type="sourcelabel" >
|
||||||
<x>650</x>
|
<x>308</x>
|
||||||
<y>122</y>
|
<y>74</y>
|
||||||
</hint>
|
</hint>
|
||||||
<hint type="destinationlabel" >
|
<hint type="destinationlabel" >
|
||||||
<x>788</x>
|
<x>308</x>
|
||||||
<y>140</y>
|
<y>74</y>
|
||||||
</hint>
|
</hint>
|
||||||
</hints>
|
</hints>
|
||||||
</connection>
|
</connection>
|
||||||
@ -1094,12 +1111,12 @@ p, li { white-space: pre-wrap; }
|
|||||||
<slot>setDisabled(bool)</slot>
|
<slot>setDisabled(bool)</slot>
|
||||||
<hints>
|
<hints>
|
||||||
<hint type="sourcelabel" >
|
<hint type="sourcelabel" >
|
||||||
<x>543</x>
|
<x>308</x>
|
||||||
<y>122</y>
|
<y>74</y>
|
||||||
</hint>
|
</hint>
|
||||||
<hint type="destinationlabel" >
|
<hint type="destinationlabel" >
|
||||||
<x>544</x>
|
<x>308</x>
|
||||||
<y>211</y>
|
<y>74</y>
|
||||||
</hint>
|
</hint>
|
||||||
</hints>
|
</hints>
|
||||||
</connection>
|
</connection>
|
||||||
@ -1110,12 +1127,12 @@ p, li { white-space: pre-wrap; }
|
|||||||
<slot>setEnabled(bool)</slot>
|
<slot>setEnabled(bool)</slot>
|
||||||
<hints>
|
<hints>
|
||||||
<hint type="sourcelabel" >
|
<hint type="sourcelabel" >
|
||||||
<x>298</x>
|
<x>308</x>
|
||||||
<y>398</y>
|
<y>74</y>
|
||||||
</hint>
|
</hint>
|
||||||
<hint type="destinationlabel" >
|
<hint type="destinationlabel" >
|
||||||
<x>660</x>
|
<x>308</x>
|
||||||
<y>435</y>
|
<y>74</y>
|
||||||
</hint>
|
</hint>
|
||||||
</hints>
|
</hints>
|
||||||
</connection>
|
</connection>
|
||||||
@ -1126,12 +1143,12 @@ p, li { white-space: pre-wrap; }
|
|||||||
<slot>setEnabled(bool)</slot>
|
<slot>setEnabled(bool)</slot>
|
||||||
<hints>
|
<hints>
|
||||||
<hint type="sourcelabel" >
|
<hint type="sourcelabel" >
|
||||||
<x>330</x>
|
<x>345</x>
|
||||||
<y>367</y>
|
<y>363</y>
|
||||||
</hint>
|
</hint>
|
||||||
<hint type="destinationlabel" >
|
<hint type="destinationlabel" >
|
||||||
<x>823</x>
|
<x>837</x>
|
||||||
<y>372</y>
|
<y>435</y>
|
||||||
</hint>
|
</hint>
|
||||||
</hints>
|
</hints>
|
||||||
</connection>
|
</connection>
|
||||||
@ -1142,12 +1159,28 @@ p, li { white-space: pre-wrap; }
|
|||||||
<slot>setDisabled(bool)</slot>
|
<slot>setDisabled(bool)</slot>
|
||||||
<hints>
|
<hints>
|
||||||
<hint type="sourcelabel" >
|
<hint type="sourcelabel" >
|
||||||
<x>344</x>
|
<x>308</x>
|
||||||
<y>107</y>
|
<y>74</y>
|
||||||
</hint>
|
</hint>
|
||||||
<hint type="destinationlabel" >
|
<hint type="destinationlabel" >
|
||||||
<x>489</x>
|
<x>308</x>
|
||||||
<y>465</y>
|
<y>74</y>
|
||||||
|
</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>261</x>
|
||||||
|
<y>346</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel" >
|
||||||
|
<x>379</x>
|
||||||
|
<y>378</y>
|
||||||
</hint>
|
</hint>
|
||||||
</hints>
|
</hints>
|
||||||
</connection>
|
</connection>
|
||||||
|
@ -7,7 +7,7 @@ add/remove formats
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from PyQt4.QtCore import SIGNAL, QObject, QCoreApplication, Qt
|
from PyQt4.QtCore import SIGNAL, QObject, QCoreApplication, Qt
|
||||||
from PyQt4.QtGui import QPixmap, QListWidgetItem, QErrorMessage, QDialog
|
from PyQt4.QtGui import QPixmap, QListWidgetItem, QErrorMessage, QDialog, QCompleter
|
||||||
|
|
||||||
|
|
||||||
from calibre.gui2 import qstring_to_unicode, error_dialog, file_icon_provider, \
|
from calibre.gui2 import qstring_to_unicode, error_dialog, file_icon_provider, \
|
||||||
@ -20,6 +20,7 @@ from calibre.ebooks import BOOK_EXTENSIONS
|
|||||||
from calibre.ebooks.metadata import authors_to_sort_string, string_to_authors, authors_to_string
|
from calibre.ebooks.metadata import authors_to_sort_string, string_to_authors, authors_to_string
|
||||||
from calibre.ebooks.metadata.library_thing import login, cover_from_isbn, LibraryThingError
|
from calibre.ebooks.metadata.library_thing import login, cover_from_isbn, LibraryThingError
|
||||||
from calibre import islinux
|
from calibre import islinux
|
||||||
|
from calibre.ebooks.metadata.meta import get_metadata
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
from calibre.customize.ui import run_plugins_on_import
|
from calibre.customize.ui import run_plugins_on_import
|
||||||
|
|
||||||
@ -32,6 +33,13 @@ class Format(QListWidgetItem):
|
|||||||
QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
|
QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
|
||||||
text, parent, QListWidgetItem.UserType)
|
text, parent, QListWidgetItem.UserType)
|
||||||
|
|
||||||
|
class AuthorCompleter(QCompleter):
|
||||||
|
|
||||||
|
def __init__(self, db):
|
||||||
|
all_authors = db.all_authors()
|
||||||
|
all_authors.sort(cmp=lambda x, y : cmp(x[1], y[1]))
|
||||||
|
QCompleter.__init__(self, [x[1] for x in all_authors])
|
||||||
|
|
||||||
class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
|
class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
|
||||||
|
|
||||||
def do_reset_cover(self, *args):
|
def do_reset_cover(self, *args):
|
||||||
@ -102,6 +110,39 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
|
|||||||
self.formats.takeItem(row.row())
|
self.formats.takeItem(row.row())
|
||||||
self.formats_changed = True
|
self.formats_changed = True
|
||||||
|
|
||||||
|
def set_cover(self):
|
||||||
|
row = self.formats.currentRow()
|
||||||
|
fmt = self.formats.item(row)
|
||||||
|
ext = fmt.ext.lower()
|
||||||
|
if fmt.path is None:
|
||||||
|
stream = self.db.format(self.row, ext, as_file=True)
|
||||||
|
else:
|
||||||
|
stream = open(fmt.path, 'r+b')
|
||||||
|
try:
|
||||||
|
mi = get_metadata(stream, ext)
|
||||||
|
except:
|
||||||
|
error_dialog(self, _('Could not read metadata'),
|
||||||
|
_('Could not read metadata from %s format')%ext).exec_()
|
||||||
|
return
|
||||||
|
cdata = None
|
||||||
|
if mi.cover and os.access(mi.cover, os.R_OK):
|
||||||
|
cdata = open(mi.cover).read()
|
||||||
|
elif mi.cover_data[1] is not None:
|
||||||
|
cdata = mi.cover_data[1]
|
||||||
|
if cdata is None:
|
||||||
|
error_dialog(self, _('Could not read cover'),
|
||||||
|
_('Could not read cover from %s format')%ext).exec_()
|
||||||
|
return
|
||||||
|
pix = QPixmap()
|
||||||
|
pix.loadFromData(cdata)
|
||||||
|
if pix.isNull():
|
||||||
|
error_dialog(self, _('Could not read cover'),
|
||||||
|
_('The cover in the %s format is invalid')%ext).exec_()
|
||||||
|
return
|
||||||
|
self.cover.setPixmap(pix)
|
||||||
|
self.cover_changed = True
|
||||||
|
self.cpixmap = pix
|
||||||
|
|
||||||
def sync_formats(self):
|
def sync_formats(self):
|
||||||
old_extensions, new_extensions, paths = set(), set(), {}
|
old_extensions, new_extensions, paths = set(), set(), {}
|
||||||
for row in range(self.formats.count()):
|
for row in range(self.formats.count()):
|
||||||
@ -137,6 +178,8 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
|
|||||||
self.cover_changed = False
|
self.cover_changed = False
|
||||||
self.cpixmap = None
|
self.cpixmap = None
|
||||||
self.cover.setAcceptDrops(True)
|
self.cover.setAcceptDrops(True)
|
||||||
|
self._author_completer = AuthorCompleter(self.db)
|
||||||
|
self.authors.setCompleter(self._author_completer)
|
||||||
self.connect(self.cover, SIGNAL('cover_changed()'), self.cover_dropped)
|
self.connect(self.cover, SIGNAL('cover_changed()'), self.cover_dropped)
|
||||||
QObject.connect(self.cover_button, SIGNAL("clicked(bool)"), \
|
QObject.connect(self.cover_button, SIGNAL("clicked(bool)"), \
|
||||||
self.select_cover)
|
self.select_cover)
|
||||||
@ -155,6 +198,7 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
|
|||||||
self.remove_unused_series)
|
self.remove_unused_series)
|
||||||
QObject.connect(self.auto_author_sort, SIGNAL('clicked()'),
|
QObject.connect(self.auto_author_sort, SIGNAL('clicked()'),
|
||||||
self.deduce_author_sort)
|
self.deduce_author_sort)
|
||||||
|
self.connect(self.button_set_cover, SIGNAL('clicked()'), self.set_cover)
|
||||||
self.connect(self.reset_cover, SIGNAL('clicked()'), self.do_reset_cover)
|
self.connect(self.reset_cover, SIGNAL('clicked()'), self.do_reset_cover)
|
||||||
self.connect(self.swap_button, SIGNAL('clicked()'), self.swap_title_author)
|
self.connect(self.swap_button, SIGNAL('clicked()'), self.swap_title_author)
|
||||||
self.timeout = float(prefs['network_timeout'])
|
self.timeout = float(prefs['network_timeout'])
|
||||||
@ -171,8 +215,6 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
|
|||||||
self.authors.setText('')
|
self.authors.setText('')
|
||||||
aus = self.db.author_sort(row)
|
aus = self.db.author_sort(row)
|
||||||
self.author_sort.setText(aus if aus else '')
|
self.author_sort.setText(aus if aus else '')
|
||||||
pub = self.db.publisher(row)
|
|
||||||
self.publisher.setText(pub if pub else '')
|
|
||||||
tags = self.db.tags(row)
|
tags = self.db.tags(row)
|
||||||
self.tags.setText(tags if tags else '')
|
self.tags.setText(tags if tags else '')
|
||||||
rating = self.db.rating(row)
|
rating = self.db.rating(row)
|
||||||
@ -191,7 +233,8 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
|
|||||||
size = self.db.sizeof_format(row, ext)
|
size = self.db.sizeof_format(row, ext)
|
||||||
Format(self.formats, ext, size)
|
Format(self.formats, ext, size)
|
||||||
|
|
||||||
self.initialize_series()
|
|
||||||
|
self.initialize_series_and_publisher()
|
||||||
|
|
||||||
self.series_index.setValue(self.db.series_index(row))
|
self.series_index.setValue(self.db.series_index(row))
|
||||||
QObject.connect(self.series, SIGNAL('currentIndexChanged(int)'), self.enable_series_index)
|
QObject.connect(self.series, SIGNAL('currentIndexChanged(int)'), self.enable_series_index)
|
||||||
@ -224,7 +267,7 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
|
|||||||
def cover_dropped(self):
|
def cover_dropped(self):
|
||||||
self.cover_changed = True
|
self.cover_changed = True
|
||||||
|
|
||||||
def initialize_series(self):
|
def initialize_series_and_publisher(self):
|
||||||
all_series = self.db.all_series()
|
all_series = self.db.all_series()
|
||||||
all_series.sort(cmp=lambda x, y : cmp(x[1], y[1]))
|
all_series.sort(cmp=lambda x, y : cmp(x[1], y[1]))
|
||||||
series_id = self.db.series_id(self.row)
|
series_id = self.db.series_id(self.row)
|
||||||
@ -248,6 +291,22 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
|
|||||||
l.invalidate()
|
l.invalidate()
|
||||||
l.activate()
|
l.activate()
|
||||||
|
|
||||||
|
all_publishers = self.db.all_publishers()
|
||||||
|
all_publishers.sort(cmp=lambda x, y : cmp(x[1], y[1]))
|
||||||
|
publisher_id = self.db.publisher_id(self.row)
|
||||||
|
idx, c = None, 0
|
||||||
|
for i in all_publishers:
|
||||||
|
id, name = i
|
||||||
|
if id == publisher_id:
|
||||||
|
idx = c
|
||||||
|
self.publisher.addItem(name)
|
||||||
|
c += 1
|
||||||
|
|
||||||
|
self.publisher.setEditText('')
|
||||||
|
if idx is not None:
|
||||||
|
self.publisher.setCurrentIndex(idx)
|
||||||
|
|
||||||
|
|
||||||
self.layout().activate()
|
self.layout().activate()
|
||||||
|
|
||||||
def edit_tags(self):
|
def edit_tags(self):
|
||||||
@ -302,7 +361,7 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
|
|||||||
isbn = qstring_to_unicode(self.isbn.text())
|
isbn = qstring_to_unicode(self.isbn.text())
|
||||||
title = qstring_to_unicode(self.title.text())
|
title = qstring_to_unicode(self.title.text())
|
||||||
author = string_to_authors(unicode(self.authors.text()))[0]
|
author = string_to_authors(unicode(self.authors.text()))[0]
|
||||||
publisher = qstring_to_unicode(self.publisher.text())
|
publisher = qstring_to_unicode(self.publisher.currentText())
|
||||||
if isbn or title or author or publisher:
|
if isbn or title or author or publisher:
|
||||||
d = FetchMetadata(self, isbn, title, author, publisher, self.timeout)
|
d = FetchMetadata(self, isbn, title, author, publisher, self.timeout)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
@ -312,7 +371,7 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
|
|||||||
self.title.setText(book.title)
|
self.title.setText(book.title)
|
||||||
self.authors.setText(authors_to_string(book.authors))
|
self.authors.setText(authors_to_string(book.authors))
|
||||||
if book.author_sort: self.author_sort.setText(book.author_sort)
|
if book.author_sort: self.author_sort.setText(book.author_sort)
|
||||||
if book.publisher: self.publisher.setText(book.publisher)
|
if book.publisher: self.publisher.setEditText(book.publisher)
|
||||||
if book.isbn: self.isbn.setText(book.isbn)
|
if book.isbn: self.isbn.setText(book.isbn)
|
||||||
summ = book.comments
|
summ = book.comments
|
||||||
if summ:
|
if summ:
|
||||||
@ -351,7 +410,7 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
|
|||||||
self.db.set_author_sort(self.id, aus, notify=False)
|
self.db.set_author_sort(self.id, aus, notify=False)
|
||||||
self.db.set_isbn(self.id, qstring_to_unicode(self.isbn.text()), notify=False)
|
self.db.set_isbn(self.id, qstring_to_unicode(self.isbn.text()), notify=False)
|
||||||
self.db.set_rating(self.id, 2*self.rating.value(), notify=False)
|
self.db.set_rating(self.id, 2*self.rating.value(), notify=False)
|
||||||
self.db.set_publisher(self.id, qstring_to_unicode(self.publisher.text()), notify=False)
|
self.db.set_publisher(self.id, qstring_to_unicode(self.publisher.currentText()), notify=False)
|
||||||
self.db.set_tags(self.id, qstring_to_unicode(self.tags.text()).split(','), notify=False)
|
self.db.set_tags(self.id, qstring_to_unicode(self.tags.text()).split(','), notify=False)
|
||||||
self.db.set_series(self.id, qstring_to_unicode(self.series.currentText()), notify=False)
|
self.db.set_series(self.id, qstring_to_unicode(self.series.currentText()), notify=False)
|
||||||
self.db.set_series_index(self.id, self.series_index.value(), notify=False)
|
self.db.set_series_index(self.id, self.series_index.value(), notify=False)
|
||||||
|
@ -95,13 +95,6 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="1" >
|
|
||||||
<widget class="QLineEdit" name="authors" >
|
|
||||||
<property name="toolTip" >
|
|
||||||
<string>Change the author(s) of this book. Multiple authors should be separated by an &. If the author name contains an &, use && to represent it.</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0" >
|
<item row="2" column="0" >
|
||||||
<widget class="QLabel" name="label_8" >
|
<widget class="QLabel" name="label_8" >
|
||||||
<property name="text" >
|
<property name="text" >
|
||||||
@ -111,7 +104,7 @@
|
|||||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
</property>
|
</property>
|
||||||
<property name="buddy" >
|
<property name="buddy" >
|
||||||
<cstring>authors</cstring>
|
<cstring>author_sort</cstring>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@ -185,13 +178,6 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="1" colspan="2" >
|
|
||||||
<widget class="QLineEdit" name="publisher" >
|
|
||||||
<property name="toolTip" >
|
|
||||||
<string>Change the publisher of this book</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="0" >
|
<item row="5" column="0" >
|
||||||
<widget class="QLabel" name="label_4" >
|
<widget class="QLabel" name="label_4" >
|
||||||
<property name="text" >
|
<property name="text" >
|
||||||
@ -330,6 +316,16 @@
|
|||||||
<item row="8" column="1" colspan="2" >
|
<item row="8" column="1" colspan="2" >
|
||||||
<widget class="QLineEdit" name="isbn" />
|
<widget class="QLineEdit" name="isbn" />
|
||||||
</item>
|
</item>
|
||||||
|
<item row="4" column="1" >
|
||||||
|
<widget class="QComboBox" name="publisher" >
|
||||||
|
<property name="editable" >
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1" >
|
||||||
|
<widget class="QLineEdit" name="authors" />
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@ -370,7 +366,7 @@
|
|||||||
<layout class="QVBoxLayout" name="verticalLayout" >
|
<layout class="QVBoxLayout" name="verticalLayout" >
|
||||||
<item>
|
<item>
|
||||||
<layout class="QGridLayout" name="gridLayout" >
|
<layout class="QGridLayout" name="gridLayout" >
|
||||||
<item rowspan="2" row="0" column="0" >
|
<item rowspan="3" row="0" column="0" >
|
||||||
<widget class="QListWidget" name="formats" >
|
<widget class="QListWidget" name="formats" >
|
||||||
<property name="sizePolicy" >
|
<property name="sizePolicy" >
|
||||||
<sizepolicy vsizetype="Minimum" hsizetype="Minimum" >
|
<sizepolicy vsizetype="Minimum" hsizetype="Minimum" >
|
||||||
@ -412,7 +408,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="1" >
|
<item row="2" column="1" >
|
||||||
<widget class="QToolButton" name="remove_format_button" >
|
<widget class="QToolButton" name="remove_format_button" >
|
||||||
<property name="toolTip" >
|
<property name="toolTip" >
|
||||||
<string>Remove the selected formats for this book from the database.</string>
|
<string>Remove the selected formats for this book from the database.</string>
|
||||||
@ -432,6 +428,26 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="1" column="1" >
|
||||||
|
<widget class="QToolButton" name="button_set_cover" >
|
||||||
|
<property name="toolTip" >
|
||||||
|
<string>Set the cover for the book from the selected format</string>
|
||||||
|
</property>
|
||||||
|
<property name="text" >
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon" >
|
||||||
|
<iconset resource="../images.qrc" >
|
||||||
|
<normaloff>:/images/book.svg</normaloff>:/images/book.svg</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize" >
|
||||||
|
<size>
|
||||||
|
<width>32</width>
|
||||||
|
<height>32</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
@ -584,6 +600,33 @@
|
|||||||
<header>widgets.h</header>
|
<header>widgets.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
|
<tabstops>
|
||||||
|
<tabstop>title</tabstop>
|
||||||
|
<tabstop>swap_button</tabstop>
|
||||||
|
<tabstop>authors</tabstop>
|
||||||
|
<tabstop>author_sort</tabstop>
|
||||||
|
<tabstop>auto_author_sort</tabstop>
|
||||||
|
<tabstop>rating</tabstop>
|
||||||
|
<tabstop>publisher</tabstop>
|
||||||
|
<tabstop>tags</tabstop>
|
||||||
|
<tabstop>tag_editor_button</tabstop>
|
||||||
|
<tabstop>series</tabstop>
|
||||||
|
<tabstop>remove_series_button</tabstop>
|
||||||
|
<tabstop>series_index</tabstop>
|
||||||
|
<tabstop>isbn</tabstop>
|
||||||
|
<tabstop>comments</tabstop>
|
||||||
|
<tabstop>fetch_metadata_button</tabstop>
|
||||||
|
<tabstop>fetch_cover_button</tabstop>
|
||||||
|
<tabstop>password_button</tabstop>
|
||||||
|
<tabstop>cover_button</tabstop>
|
||||||
|
<tabstop>reset_cover</tabstop>
|
||||||
|
<tabstop>cover_path</tabstop>
|
||||||
|
<tabstop>add_format_button</tabstop>
|
||||||
|
<tabstop>button_set_cover</tabstop>
|
||||||
|
<tabstop>remove_format_button</tabstop>
|
||||||
|
<tabstop>formats</tabstop>
|
||||||
|
<tabstop>button_box</tabstop>
|
||||||
|
</tabstops>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../images.qrc" />
|
<include location="../images.qrc" />
|
||||||
</resources>
|
</resources>
|
||||||
@ -595,8 +638,8 @@
|
|||||||
<slot>accept()</slot>
|
<slot>accept()</slot>
|
||||||
<hints>
|
<hints>
|
||||||
<hint type="sourcelabel" >
|
<hint type="sourcelabel" >
|
||||||
<x>257</x>
|
<x>261</x>
|
||||||
<y>646</y>
|
<y>710</y>
|
||||||
</hint>
|
</hint>
|
||||||
<hint type="destinationlabel" >
|
<hint type="destinationlabel" >
|
||||||
<x>157</x>
|
<x>157</x>
|
||||||
@ -611,8 +654,8 @@
|
|||||||
<slot>reject()</slot>
|
<slot>reject()</slot>
|
||||||
<hints>
|
<hints>
|
||||||
<hint type="sourcelabel" >
|
<hint type="sourcelabel" >
|
||||||
<x>325</x>
|
<x>329</x>
|
||||||
<y>646</y>
|
<y>710</y>
|
||||||
</hint>
|
</hint>
|
||||||
<hint type="destinationlabel" >
|
<hint type="destinationlabel" >
|
||||||
<x>286</x>
|
<x>286</x>
|
||||||
|
@ -155,10 +155,11 @@ class RecipeModel(QAbstractListModel, SearchQueryParser):
|
|||||||
return recipe
|
return recipe
|
||||||
elif role == Qt.DecorationRole:
|
elif role == Qt.DecorationRole:
|
||||||
icon = self.default_icon
|
icon = self.default_icon
|
||||||
|
icon_path = (':/images/news/%s.png'%recipe.id).replace('recipe_', '')
|
||||||
if not recipe.builtin:
|
if not recipe.builtin:
|
||||||
icon = self.custom_icon
|
icon = self.custom_icon
|
||||||
elif QFile(':/images/news/%s.png'%recipe.id).exists():
|
elif QFile().exists(icon_path):
|
||||||
icon = QIcon(':/images/news/%s.png'%recipe.id)
|
icon = QIcon(icon_path)
|
||||||
return QVariant(icon)
|
return QVariant(icon)
|
||||||
|
|
||||||
return NONE
|
return NONE
|
||||||
|
BIN
src/calibre/gui2/images/news/clarin.png
Normal file
After Width: | Height: | Size: 330 B |
BIN
src/calibre/gui2/images/news/joelonsoftware.png
Normal file
After Width: | Height: | Size: 390 B |
BIN
src/calibre/gui2/images/news/lanacion.png
Normal file
After Width: | Height: | Size: 318 B |
BIN
src/calibre/gui2/images/news/nspm.png
Normal file
After Width: | Height: | Size: 1016 B |
BIN
src/calibre/gui2/images/news/nspm_int.png
Normal file
After Width: | Height: | Size: 1016 B |
BIN
src/calibre/gui2/images/news/pescanik.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
src/calibre/gui2/images/news/spiegel_int.png
Normal file
After Width: | Height: | Size: 811 B |
BIN
src/calibre/gui2/images/news/themarketticker.png
Normal file
After Width: | Height: | Size: 607 B |
BIN
src/calibre/gui2/images/news/tomshardware.png
Normal file
After Width: | Height: | Size: 992 B |
@ -128,7 +128,10 @@ class JobManager(QAbstractTableModel):
|
|||||||
self.emit(SIGNAL('layoutChanged()'))
|
self.emit(SIGNAL('layoutChanged()'))
|
||||||
|
|
||||||
def _status_update(self, job):
|
def _status_update(self, job):
|
||||||
|
try:
|
||||||
row = self.jobs.index(job)
|
row = self.jobs.index(job)
|
||||||
|
except ValueError: # Job has been stopped
|
||||||
|
return
|
||||||
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'),
|
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'),
|
||||||
self.index(row, 0), self.index(row, 3))
|
self.index(row, 0), self.index(row, 3))
|
||||||
|
|
||||||
|
@ -8,14 +8,15 @@ from math import cos, sin, pi
|
|||||||
from PyQt4.QtGui import QTableView, QAbstractItemView, QColor, \
|
from PyQt4.QtGui import QTableView, QAbstractItemView, QColor, \
|
||||||
QItemDelegate, QPainterPath, QLinearGradient, QBrush, \
|
QItemDelegate, QPainterPath, QLinearGradient, QBrush, \
|
||||||
QPen, QStyle, QPainter, QLineEdit, \
|
QPen, QStyle, QPainter, QLineEdit, \
|
||||||
QPalette, QImage, QApplication, QMenu
|
QPalette, QImage, QApplication, QMenu, QStyledItemDelegate
|
||||||
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \
|
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \
|
||||||
SIGNAL, QObject, QSize, QModelIndex
|
SIGNAL, QObject, QSize, QModelIndex, QDate
|
||||||
|
|
||||||
from calibre import strftime
|
from calibre import strftime
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.library.database2 import FIELD_MAP
|
from calibre.library.database2 import FIELD_MAP
|
||||||
from calibre.gui2 import NONE, TableView, qstring_to_unicode, config
|
from calibre.gui2 import NONE, TableView, qstring_to_unicode, config, \
|
||||||
|
error_dialog
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
|
|
||||||
class LibraryDelegate(QItemDelegate):
|
class LibraryDelegate(QItemDelegate):
|
||||||
@ -81,6 +82,17 @@ class LibraryDelegate(QItemDelegate):
|
|||||||
sb.setMaximum(5)
|
sb.setMaximum(5)
|
||||||
return sb
|
return sb
|
||||||
|
|
||||||
|
class DateDelegate(QStyledItemDelegate):
|
||||||
|
|
||||||
|
def displayText(self, val, locale):
|
||||||
|
d = val.toDate()
|
||||||
|
return d.toString('dd MMM yyyy')
|
||||||
|
if d.isNull():
|
||||||
|
return ''
|
||||||
|
d = datetime(d.year(), d.month(), d.day())
|
||||||
|
return strftime(BooksView.TIME_FMT, d.timetuple())
|
||||||
|
|
||||||
|
|
||||||
class BooksModel(QAbstractTableModel):
|
class BooksModel(QAbstractTableModel):
|
||||||
coding = zip(
|
coding = zip(
|
||||||
[1000,900,500,400,100,90,50,40,10,9,5,4,1],
|
[1000,900,500,400,100,90,50,40,10,9,5,4,1],
|
||||||
@ -113,7 +125,8 @@ class BooksModel(QAbstractTableModel):
|
|||||||
QAbstractTableModel.__init__(self, parent)
|
QAbstractTableModel.__init__(self, parent)
|
||||||
self.db = None
|
self.db = None
|
||||||
self.column_map = config['column_map']
|
self.column_map = config['column_map']
|
||||||
self.editable_cols = ['title', 'authors', 'rating', 'publisher', 'tags', 'series']
|
self.editable_cols = ['title', 'authors', 'rating', 'publisher',
|
||||||
|
'tags', 'series', 'timestamp']
|
||||||
self.default_image = QImage(':/images/book.svg')
|
self.default_image = QImage(':/images/book.svg')
|
||||||
self.sorted_on = ('timestamp', Qt.AscendingOrder)
|
self.sorted_on = ('timestamp', Qt.AscendingOrder)
|
||||||
self.last_search = '' # The last search performed on this model
|
self.last_search = '' # The last search performed on this model
|
||||||
@ -135,7 +148,12 @@ class BooksModel(QAbstractTableModel):
|
|||||||
idx = self.column_map.index('rating')
|
idx = self.column_map.index('rating')
|
||||||
except ValueError:
|
except ValueError:
|
||||||
idx = -1
|
idx = -1
|
||||||
self.emit(SIGNAL('columns_sorted(int)'), idx)
|
try:
|
||||||
|
tidx = self.column_map.index('timestamp')
|
||||||
|
except ValueError:
|
||||||
|
tidx = -1
|
||||||
|
|
||||||
|
self.emit(SIGNAL('columns_sorted(int,int)'), idx, tidx)
|
||||||
|
|
||||||
|
|
||||||
def set_database(self, db):
|
def set_database(self, db):
|
||||||
@ -442,7 +460,7 @@ class BooksModel(QAbstractTableModel):
|
|||||||
dt = self.db.data[r][tmdx]
|
dt = self.db.data[r][tmdx]
|
||||||
if dt:
|
if dt:
|
||||||
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
|
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
|
||||||
return strftime(BooksView.TIME_FMT, dt.timetuple())
|
return QDate(dt.year, dt.month, dt.day)
|
||||||
|
|
||||||
def rating(r):
|
def rating(r):
|
||||||
r = self.db.data[r][ridx]
|
r = self.db.data[r][ridx]
|
||||||
@ -507,35 +525,40 @@ class BooksModel(QAbstractTableModel):
|
|||||||
return flags
|
return flags
|
||||||
|
|
||||||
def setData(self, index, value, role):
|
def setData(self, index, value, role):
|
||||||
done = False
|
|
||||||
if role == Qt.EditRole:
|
if role == Qt.EditRole:
|
||||||
row, col = index.row(), index.column()
|
row, col = index.row(), index.column()
|
||||||
column = self.column_map[col]
|
column = self.column_map[col]
|
||||||
if column not in self.editable_cols:
|
if column not in self.editable_cols:
|
||||||
return False
|
return False
|
||||||
val = unicode(value.toString().toUtf8(), 'utf-8').strip() if column != 'rating' else \
|
val = int(value.toInt()[0]) if column == 'rating' else \
|
||||||
int(value.toInt()[0])
|
value.toDate() if column == 'timestamp' else \
|
||||||
|
unicode(value.toString())
|
||||||
|
id = self.db.id(row)
|
||||||
if column == 'rating':
|
if column == 'rating':
|
||||||
val = 0 if val < 0 else 5 if val > 5 else val
|
val = 0 if val < 0 else 5 if val > 5 else val
|
||||||
val *= 2
|
val *= 2
|
||||||
if column == 'series':
|
elif column == 'series':
|
||||||
pat = re.compile(r'\[(\d+)\]')
|
pat = re.compile(r'\[(\d+)\]')
|
||||||
match = pat.search(val)
|
match = pat.search(val)
|
||||||
id = self.db.id(row)
|
|
||||||
if match is not None:
|
if match is not None:
|
||||||
self.db.set_series_index(id, int(match.group(1)))
|
self.db.set_series_index(id, int(match.group(1)))
|
||||||
val = pat.sub('', val)
|
val = pat.sub('', val)
|
||||||
val = val.strip()
|
val = val.strip()
|
||||||
if val:
|
if val:
|
||||||
self.db.set_series(id, val)
|
self.db.set_series(id, val)
|
||||||
|
elif column == 'timestamp':
|
||||||
|
if val.isNull() or not val.isValid():
|
||||||
|
return False
|
||||||
|
dt = datetime(val.year(), val.month(), val.day()) + timedelta(seconds=time.timezone) - timedelta(hours=time.daylight)
|
||||||
|
self.db.set_timestamp(id, dt)
|
||||||
else:
|
else:
|
||||||
self.db.set(row, column, val)
|
self.db.set(row, column, val)
|
||||||
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
|
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
|
||||||
index, index)
|
index, index)
|
||||||
if column == self.sorted_on[0]:
|
if column == self.sorted_on[0]:
|
||||||
self.resort()
|
self.resort()
|
||||||
done = True
|
|
||||||
return done
|
return True
|
||||||
|
|
||||||
class BooksView(TableView):
|
class BooksView(TableView):
|
||||||
TIME_FMT = '%d %b %Y'
|
TIME_FMT = '%d %b %Y'
|
||||||
@ -554,25 +577,29 @@ class BooksView(TableView):
|
|||||||
def __init__(self, parent, modelcls=BooksModel):
|
def __init__(self, parent, modelcls=BooksModel):
|
||||||
TableView.__init__(self, parent)
|
TableView.__init__(self, parent)
|
||||||
self.rating_delegate = LibraryDelegate(self)
|
self.rating_delegate = LibraryDelegate(self)
|
||||||
|
self.timestamp_delegate = DateDelegate(self)
|
||||||
self.display_parent = parent
|
self.display_parent = parent
|
||||||
self._model = modelcls(self)
|
self._model = modelcls(self)
|
||||||
self.setModel(self._model)
|
self.setModel(self._model)
|
||||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
self.setSortingEnabled(True)
|
self.setSortingEnabled(True)
|
||||||
try:
|
try:
|
||||||
self.columns_sorted(self._model.column_map.index('rating'))
|
self.columns_sorted(self._model.column_map.index('rating'),
|
||||||
|
self._model.column_map.index('timestamp'))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
|
QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
|
||||||
self._model.current_changed)
|
self._model.current_changed)
|
||||||
self.connect(self._model, SIGNAL('columns_sorted(int)'), self.columns_sorted, Qt.QueuedConnection)
|
self.connect(self._model, SIGNAL('columns_sorted(int, int)'), self.columns_sorted, Qt.QueuedConnection)
|
||||||
|
|
||||||
def columns_sorted(self, col):
|
def columns_sorted(self, rating_col, timestamp_col):
|
||||||
for i in range(self.model().columnCount(None)):
|
for i in range(self.model().columnCount(None)):
|
||||||
if self.itemDelegateForColumn(i) == self.rating_delegate:
|
if self.itemDelegateForColumn(i) == self.rating_delegate:
|
||||||
self.setItemDelegateForColumn(i, self.itemDelegate())
|
self.setItemDelegateForColumn(i, self.itemDelegate())
|
||||||
if col > -1:
|
if rating_col > -1:
|
||||||
self.setItemDelegateForColumn(col, self.rating_delegate)
|
self.setItemDelegateForColumn(rating_col, self.rating_delegate)
|
||||||
|
if timestamp_col > -1:
|
||||||
|
self.setItemDelegateForColumn(timestamp_col, self.timestamp_delegate)
|
||||||
|
|
||||||
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
|
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
|
||||||
save, open_folder, book_details, similar_menu=None):
|
save, open_folder, book_details, similar_menu=None):
|
||||||
@ -657,6 +684,8 @@ class DeviceBooksView(BooksView):
|
|||||||
self.rating_delegate = None
|
self.rating_delegate = None
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
self.setItemDelegateForColumn(i, self.itemDelegate())
|
self.setItemDelegateForColumn(i, self.itemDelegate())
|
||||||
|
self.setDragDropMode(self.NoDragDrop)
|
||||||
|
self.setAcceptDrops(False)
|
||||||
|
|
||||||
def resizeColumnsToContents(self):
|
def resizeColumnsToContents(self):
|
||||||
QTableView.resizeColumnsToContents(self)
|
QTableView.resizeColumnsToContents(self)
|
||||||
@ -668,6 +697,10 @@ class DeviceBooksView(BooksView):
|
|||||||
def sortByColumn(self, col, order):
|
def sortByColumn(self, col, order):
|
||||||
TableView.sortByColumn(self, col, order)
|
TableView.sortByColumn(self, col, order)
|
||||||
|
|
||||||
|
def dropEvent(self, *args):
|
||||||
|
error_dialog(self, _('Not allowed'),
|
||||||
|
_('Dropping onto a device is not supported. First add the book to the calibre library.')).exec_()
|
||||||
|
|
||||||
class OnDeviceSearch(SearchQueryParser):
|
class OnDeviceSearch(SearchQueryParser):
|
||||||
|
|
||||||
def __init__(self, model):
|
def __init__(self, model):
|
||||||
|
@ -3,11 +3,11 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
import os, sys, textwrap, collections, traceback, time
|
import os, sys, textwrap, collections, traceback, time
|
||||||
from xml.parsers.expat import ExpatError
|
from xml.parsers.expat import ExpatError
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, QUrl, QTimer
|
from PyQt4.Qt import Qt, SIGNAL, QObject, QCoreApplication, QUrl, QTimer, \
|
||||||
from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon, QMessageBox, \
|
QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \
|
||||||
QToolButton, QDialog, QDesktopServices, QFileDialog, \
|
QToolButton, QDialog, QDesktopServices, QFileDialog, \
|
||||||
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
|
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
|
||||||
QProgressDialog
|
QProgressDialog, QMessageBox, QStackedLayout
|
||||||
from PyQt4.QtSvg import QSvgRenderer
|
from PyQt4.QtSvg import QSvgRenderer
|
||||||
|
|
||||||
from calibre import __version__, __appname__, islinux, sanitize_file_name, \
|
from calibre import __version__, __appname__, islinux, sanitize_file_name, \
|
||||||
@ -22,7 +22,8 @@ from calibre.gui2 import APP_UID, warning_dialog, choose_files, error_dialog, \
|
|||||||
pixmap_to_data, choose_dir, ORG_NAME, \
|
pixmap_to_data, choose_dir, ORG_NAME, \
|
||||||
set_sidebar_directories, Dispatcher, \
|
set_sidebar_directories, Dispatcher, \
|
||||||
SingleApplication, Application, available_height, \
|
SingleApplication, Application, available_height, \
|
||||||
max_available_height, config, info_dialog
|
max_available_height, config, info_dialog, \
|
||||||
|
available_width
|
||||||
from calibre.gui2.cover_flow import CoverFlow, DatabaseImages, pictureflowerror
|
from calibre.gui2.cover_flow import CoverFlow, DatabaseImages, pictureflowerror
|
||||||
from calibre.library.database import LibraryDatabase
|
from calibre.library.database import LibraryDatabase
|
||||||
from calibre.gui2.dialogs.scheduler import Scheduler
|
from calibre.gui2.dialogs.scheduler import Scheduler
|
||||||
@ -49,6 +50,7 @@ from calibre.library.database2 import LibraryDatabase2, CoverCache
|
|||||||
from calibre.parallel import JobKilled
|
from calibre.parallel import JobKilled
|
||||||
from calibre.utils.filenames import ascii_filename
|
from calibre.utils.filenames import ascii_filename
|
||||||
from calibre.gui2.widgets import WarningDialog
|
from calibre.gui2.widgets import WarningDialog
|
||||||
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
|
|
||||||
class Main(MainWindow, Ui_MainWindow):
|
class Main(MainWindow, Ui_MainWindow):
|
||||||
|
|
||||||
@ -187,8 +189,8 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
self.metadata_menu = md
|
self.metadata_menu = md
|
||||||
self.add_menu = QMenu()
|
self.add_menu = QMenu()
|
||||||
self.add_menu.addAction(_('Add books from a single directory'))
|
self.add_menu.addAction(_('Add books from a single directory'))
|
||||||
self.add_menu.addAction(_('Add books recursively (One book per directory, assumes every ebook file is the same book in a different format)'))
|
self.add_menu.addAction(_('Add books from directories, including sub-directories (One book per directory, assumes every ebook file is the same book in a different format)'))
|
||||||
self.add_menu.addAction(_('Add books recursively (Multiple books per directory, assumes every ebook file is a different book)'))
|
self.add_menu.addAction(_('Add books from directories, including sub directories (Multiple books per directory, assumes every ebook file is a different book)'))
|
||||||
self.action_add.setMenu(self.add_menu)
|
self.action_add.setMenu(self.add_menu)
|
||||||
QObject.connect(self.action_add, SIGNAL("triggered(bool)"), self.add_books)
|
QObject.connect(self.action_add, SIGNAL("triggered(bool)"), self.add_books)
|
||||||
QObject.connect(self.add_menu.actions()[0], SIGNAL("triggered(bool)"), self.add_books)
|
QObject.connect(self.add_menu.actions()[0], SIGNAL("triggered(bool)"), self.add_books)
|
||||||
@ -307,7 +309,6 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
db = LibraryDatabase2(self.library_path)
|
db = LibraryDatabase2(self.library_path)
|
||||||
self.library_view.set_database(db)
|
self.library_view.set_database(db)
|
||||||
if self.olddb is not None:
|
if self.olddb is not None:
|
||||||
from PyQt4.QtGui import QProgressDialog
|
|
||||||
pd = QProgressDialog('', '', 0, 100, self)
|
pd = QProgressDialog('', '', 0, 100, self)
|
||||||
pd.setWindowModality(Qt.ApplicationModal)
|
pd.setWindowModality(Qt.ApplicationModal)
|
||||||
pd.setCancelButton(None)
|
pd.setCancelButton(None)
|
||||||
@ -342,8 +343,15 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
########################### Cover Flow ################################
|
########################### Cover Flow ################################
|
||||||
self.cover_flow = None
|
self.cover_flow = None
|
||||||
if CoverFlow is not None:
|
if CoverFlow is not None:
|
||||||
self.cover_flow = CoverFlow(height=220 if available_height() > 950 else 170 if available_height() > 850 else 140)
|
text_height = 40 if config['separate_cover_flow'] else 25
|
||||||
|
ah = available_height()
|
||||||
|
cfh = ah-100
|
||||||
|
cfh = 3./5 * cfh - text_height
|
||||||
|
if not config['separate_cover_flow']:
|
||||||
|
cfh = 220 if ah > 950 else 170 if ah > 850 else 140
|
||||||
|
self.cover_flow = CoverFlow(height=cfh, text_height=text_height)
|
||||||
self.cover_flow.setVisible(False)
|
self.cover_flow.setVisible(False)
|
||||||
|
if not config['separate_cover_flow']:
|
||||||
self.library.layout().addWidget(self.cover_flow)
|
self.library.layout().addWidget(self.cover_flow)
|
||||||
self.connect(self.cover_flow, SIGNAL('currentChanged(int)'), self.sync_cf_to_listview)
|
self.connect(self.cover_flow, SIGNAL('currentChanged(int)'), self.sync_cf_to_listview)
|
||||||
self.connect(self.cover_flow, SIGNAL('itemActivated(int)'), self.show_book_info)
|
self.connect(self.cover_flow, SIGNAL('itemActivated(int)'), self.show_book_info)
|
||||||
@ -410,6 +418,29 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
|
|
||||||
def toggle_cover_flow(self, show):
|
def toggle_cover_flow(self, show):
|
||||||
|
if config['separate_cover_flow']:
|
||||||
|
if show:
|
||||||
|
d = QDialog(self)
|
||||||
|
ah, aw = available_height(), available_width()
|
||||||
|
d.resize(int(aw/2.), ah-60)
|
||||||
|
d._layout = QStackedLayout()
|
||||||
|
d.setLayout(d._layout)
|
||||||
|
d.setWindowTitle(_('Browse by covers'))
|
||||||
|
d.layout().addWidget(self.cover_flow)
|
||||||
|
self.cover_flow.setVisible(True)
|
||||||
|
self.cover_flow.setFocus(Qt.OtherFocusReason)
|
||||||
|
self.library_view.scrollTo(self.library_view.currentIndex())
|
||||||
|
d.show()
|
||||||
|
self.connect(d, SIGNAL('finished(int)'),
|
||||||
|
lambda x: self.status_bar.cover_flow_button.setChecked(False))
|
||||||
|
self.cf_dialog = d
|
||||||
|
else:
|
||||||
|
cfd = getattr(self, 'cf_dialog', None)
|
||||||
|
if cfd is not None:
|
||||||
|
self.cover_flow.setVisible(False)
|
||||||
|
cfd.hide()
|
||||||
|
self.cf_dialog = None
|
||||||
|
else:
|
||||||
if show:
|
if show:
|
||||||
self.library_view.setCurrentIndex(self.library_view.currentIndex())
|
self.library_view.setCurrentIndex(self.library_view.currentIndex())
|
||||||
self.cover_flow.setVisible(True)
|
self.cover_flow.setVisible(True)
|
||||||
@ -583,6 +614,7 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
try:
|
try:
|
||||||
duplicates = self.library_view.model().db.recursive_import(root, single, callback=callback)
|
duplicates = self.library_view.model().db.recursive_import(root, single, callback=callback)
|
||||||
finally:
|
finally:
|
||||||
|
progress.hide()
|
||||||
progress.close()
|
progress.close()
|
||||||
if duplicates:
|
if duplicates:
|
||||||
files = _('<p>Books with the same title as the following already exist in the database. Add them anyway?<ul>')
|
files = _('<p>Books with the same title as the following already exist in the database. Add them anyway?<ul>')
|
||||||
@ -702,7 +734,9 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
else:
|
else:
|
||||||
self.upload_books(paths, list(map(sanitize_file_name, names)), infos, on_card=on_card)
|
self.upload_books(paths, list(map(sanitize_file_name, names)), infos, on_card=on_card)
|
||||||
finally:
|
finally:
|
||||||
progress.setValue(len(paths))
|
progress.setValue(progress.maximum())
|
||||||
|
progress.hide()
|
||||||
|
progress.close()
|
||||||
|
|
||||||
def upload_books(self, files, names, metadata, on_card=False, memory=None):
|
def upload_books(self, files, names, metadata, on_card=False, memory=None):
|
||||||
'''
|
'''
|
||||||
@ -758,13 +792,9 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
rows = view.selectionModel().selectedRows()
|
rows = view.selectionModel().selectedRows()
|
||||||
if not rows or len(rows) == 0:
|
if not rows or len(rows) == 0:
|
||||||
return
|
return
|
||||||
if config['confirm_delete']:
|
|
||||||
d = question_dialog(self, _('Confirm delete'),
|
|
||||||
_('Are you sure you want to delete these %d books?')%len(rows))
|
|
||||||
if d.exec_() != QMessageBox.Yes:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.stack.currentIndex() == 0:
|
if self.stack.currentIndex() == 0:
|
||||||
|
if not confirm('<p>'+_('The selected books will be <b>permanently deleted</b> and the files removed from your computer. Are you sure?')+'</p>', 'library_delete_books', self):
|
||||||
|
return
|
||||||
view.model().delete_books(rows)
|
view.model().delete_books(rows)
|
||||||
else:
|
else:
|
||||||
view = self.memory_view if self.stack.currentIndex() == 1 else self.card_view
|
view = self.memory_view if self.stack.currentIndex() == 1 else self.card_view
|
||||||
@ -801,6 +831,7 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
Edit metadata of selected books in library.
|
Edit metadata of selected books in library.
|
||||||
'''
|
'''
|
||||||
rows = self.library_view.selectionModel().selectedRows()
|
rows = self.library_view.selectionModel().selectedRows()
|
||||||
|
previous = self.library_view.currentIndex()
|
||||||
if not rows or len(rows) == 0:
|
if not rows or len(rows) == 0:
|
||||||
d = error_dialog(self, _('Cannot edit metadata'), _('No books selected'))
|
d = error_dialog(self, _('Cannot edit metadata'), _('No books selected'))
|
||||||
d.exec_()
|
d.exec_()
|
||||||
@ -817,6 +848,9 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
self.library_view.model().db,
|
self.library_view.model().db,
|
||||||
accepted_callback=accepted)
|
accepted_callback=accepted)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
|
if rows:
|
||||||
|
current = self.library_view.currentIndex()
|
||||||
|
self.library_view.model().current_changed(current, previous)
|
||||||
|
|
||||||
def edit_bulk_metadata(self, checked):
|
def edit_bulk_metadata(self, checked):
|
||||||
'''
|
'''
|
||||||
@ -1046,6 +1080,8 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
def convert_single(self, checked):
|
def convert_single(self, checked):
|
||||||
r = self.get_books_for_conversion()
|
r = self.get_books_for_conversion()
|
||||||
if r is None: return
|
if r is None: return
|
||||||
|
previous = self.library_view.currentIndex()
|
||||||
|
rows = [x.row() for x in self.library_view.selectionModel().selectedRows()]
|
||||||
comics, others = r
|
comics, others = r
|
||||||
jobs, changed = convert_single_ebook(self, self.library_view.model().db, comics, others)
|
jobs, changed = convert_single_ebook(self, self.library_view.model().db, comics, others)
|
||||||
for func, args, desc, fmt, id, temp_files in jobs:
|
for func, args, desc, fmt, id, temp_files in jobs:
|
||||||
@ -1054,8 +1090,9 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
self.conversion_jobs[job] = (temp_files, fmt, id)
|
self.conversion_jobs[job] = (temp_files, fmt, id)
|
||||||
|
|
||||||
if changed:
|
if changed:
|
||||||
self.library_view.model().resort(reset=False)
|
self.library_view.model().refresh_rows(rows)
|
||||||
self.library_view.model().research()
|
current = self.library_view.currentIndex()
|
||||||
|
self.library_view.model().current_changed(current, previous)
|
||||||
|
|
||||||
def book_converted(self, job):
|
def book_converted(self, job):
|
||||||
temp_files, fmt, book_id = self.conversion_jobs.pop(job)
|
temp_files, fmt, book_id = self.conversion_jobs.pop(job)
|
||||||
@ -1074,6 +1111,9 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
os.remove(f.name)
|
os.remove(f.name)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
if self.current_view() is self.library_view:
|
||||||
|
current = self.library_view.currentIndex()
|
||||||
|
self.library_view.model().current_changed(current, QModelIndex())
|
||||||
|
|
||||||
#############################View book######################################
|
#############################View book######################################
|
||||||
|
|
||||||
@ -1206,7 +1246,6 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
newloc = d.database_location
|
newloc = d.database_location
|
||||||
if not os.path.exists(os.path.join(newloc, 'metadata.db')):
|
if not os.path.exists(os.path.join(newloc, 'metadata.db')):
|
||||||
if os.access(self.library_path, os.R_OK):
|
if os.access(self.library_path, os.R_OK):
|
||||||
from PyQt4.QtGui import QProgressDialog
|
|
||||||
pd = QProgressDialog('', '', 0, 100, self)
|
pd = QProgressDialog('', '', 0, 100, self)
|
||||||
pd.setWindowModality(Qt.ApplicationModal)
|
pd.setWindowModality(Qt.ApplicationModal)
|
||||||
pd.setCancelButton(None)
|
pd.setCancelButton(None)
|
||||||
@ -1385,7 +1424,12 @@ in which you want to store your books files. Any existing books will be automati
|
|||||||
self.memory_view.write_settings()
|
self.memory_view.write_settings()
|
||||||
|
|
||||||
def quit(self, checked, restart=False):
|
def quit(self, checked, restart=False):
|
||||||
if self.shutdown():
|
if not self.confirm_quit():
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.shutdown()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
self.restart_after_quit = restart
|
self.restart_after_quit = restart
|
||||||
QApplication.instance().quit()
|
QApplication.instance().quit()
|
||||||
|
|
||||||
@ -1418,22 +1462,26 @@ in which you want to store your books files. Any existing books will be automati
|
|||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(pt.name))
|
QDesktopServices.openUrl(QUrl.fromLocalFile(pt.name))
|
||||||
|
|
||||||
|
|
||||||
def shutdown(self):
|
def confirm_quit(self):
|
||||||
|
if self.job_manager.has_jobs():
|
||||||
msg = _('There are active jobs. Are you sure you want to quit?')
|
msg = _('There are active jobs. Are you sure you want to quit?')
|
||||||
if self.job_manager.has_device_jobs():
|
if self.job_manager.has_device_jobs():
|
||||||
msg = '<p>'+__appname__ + _(''' is communicating with the device!<br>
|
msg = '<p>'+__appname__ + _(''' is communicating with the device!<br>
|
||||||
'Quitting may cause corruption on the device.<br>
|
'Quitting may cause corruption on the device.<br>
|
||||||
'Are you sure you want to quit?''')+'</p>'
|
'Are you sure you want to quit?''')+'</p>'
|
||||||
if self.job_manager.has_jobs():
|
|
||||||
d = QMessageBox(QMessageBox.Warning, _('WARNING: Active jobs'), msg,
|
d = QMessageBox(QMessageBox.Warning, _('WARNING: Active jobs'), msg,
|
||||||
QMessageBox.Yes|QMessageBox.No, self)
|
QMessageBox.Yes|QMessageBox.No, self)
|
||||||
d.setIconPixmap(QPixmap(':/images/dialog_warning.svg'))
|
d.setIconPixmap(QPixmap(':/images/dialog_warning.svg'))
|
||||||
d.setDefaultButton(QMessageBox.No)
|
d.setDefaultButton(QMessageBox.No)
|
||||||
if d.exec_() != QMessageBox.Yes:
|
if d.exec_() != QMessageBox.Yes:
|
||||||
return False
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
self.job_manager.terminate_all_jobs()
|
|
||||||
|
def shutdown(self):
|
||||||
self.write_settings()
|
self.write_settings()
|
||||||
|
self.job_manager.terminate_all_jobs()
|
||||||
self.device_manager.keep_going = False
|
self.device_manager.keep_going = False
|
||||||
self.cover_cache.stop()
|
self.cover_cache.stop()
|
||||||
self.hide()
|
self.hide()
|
||||||
@ -1459,7 +1507,11 @@ in which you want to store your books files. Any existing books will be automati
|
|||||||
self.hide()
|
self.hide()
|
||||||
e.ignore()
|
e.ignore()
|
||||||
else:
|
else:
|
||||||
if self.shutdown():
|
if self.confirm_quit():
|
||||||
|
try:
|
||||||
|
self.shutdown()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
e.accept()
|
e.accept()
|
||||||
else:
|
else:
|
||||||
e.ignore()
|
e.ignore()
|
||||||
|
@ -199,7 +199,10 @@ class StatusBar(QStatusBar):
|
|||||||
ret = QStatusBar.showMessage(self, msg, timeout)
|
ret = QStatusBar.showMessage(self, msg, timeout)
|
||||||
if self.systray is not None:
|
if self.systray is not None:
|
||||||
if isosx and isinstance(msg, unicode):
|
if isosx and isinstance(msg, unicode):
|
||||||
|
try:
|
||||||
msg = msg.encode(preferred_encoding)
|
msg = msg.encode(preferred_encoding)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
msg = msg.encode('utf-8')
|
||||||
self.systray.showMessage('calibre', msg, self.systray.Information, 10000)
|
self.systray.showMessage('calibre', msg, self.systray.Information, 10000)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ from calibre.utils.config import prefs
|
|||||||
from calibre.gui2.dialogs.lrf_single import LRFSingleDialog, LRFBulkDialog
|
from calibre.gui2.dialogs.lrf_single import LRFSingleDialog, LRFBulkDialog
|
||||||
from calibre.gui2.dialogs.epub import Config as EPUBConvert
|
from calibre.gui2.dialogs.epub import Config as EPUBConvert
|
||||||
import calibre.gui2.dialogs.comicconf as ComicConf
|
import calibre.gui2.dialogs.comicconf as ComicConf
|
||||||
from calibre.gui2 import warning_dialog, dynamic
|
from calibre.gui2 import warning_dialog
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.ebooks.lrf import preferred_source_formats as LRF_PREFERRED_SOURCE_FORMATS
|
from calibre.ebooks.lrf import preferred_source_formats as LRF_PREFERRED_SOURCE_FORMATS
|
||||||
from calibre.ebooks.metadata.opf import OPFCreator
|
from calibre.ebooks.metadata.opf import OPFCreator
|
||||||
@ -22,7 +22,9 @@ from calibre.ebooks.epub.from_any import SOURCE_FORMATS as EPUB_PREFERRED_SOURCE
|
|||||||
def convert_single_epub(parent, db, comics, others):
|
def convert_single_epub(parent, db, comics, others):
|
||||||
changed = False
|
changed = False
|
||||||
jobs = []
|
jobs = []
|
||||||
for row in others:
|
others_ids = [db.id(row) for row in others]
|
||||||
|
comics_ids = [db.id(row) for row in comics]
|
||||||
|
for row, row_id in zip(others, others_ids):
|
||||||
temp_files = []
|
temp_files = []
|
||||||
d = EPUBConvert(parent, db, row)
|
d = EPUBConvert(parent, db, row)
|
||||||
if d.source_format is not None:
|
if d.source_format is not None:
|
||||||
@ -44,10 +46,10 @@ def convert_single_epub(parent, db, comics, others):
|
|||||||
opts.cover = d.cover_file.name
|
opts.cover = d.cover_file.name
|
||||||
temp_files.extend([d.opf_file, pt, of])
|
temp_files.extend([d.opf_file, pt, of])
|
||||||
jobs.append(('any2epub', args, _('Convert book: ')+d.mi.title,
|
jobs.append(('any2epub', args, _('Convert book: ')+d.mi.title,
|
||||||
'EPUB', db.id(row), temp_files))
|
'EPUB', row_id, temp_files))
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
for row in comics:
|
for row, row_id in zip(comics, comics_ids):
|
||||||
mi = db.get_metadata(row)
|
mi = db.get_metadata(row)
|
||||||
title = author = _('Unknown')
|
title = author = _('Unknown')
|
||||||
if mi.title:
|
if mi.title:
|
||||||
@ -76,7 +78,7 @@ def convert_single_epub(parent, db, comics, others):
|
|||||||
args = [pt.name, opts]
|
args = [pt.name, opts]
|
||||||
changed = True
|
changed = True
|
||||||
jobs.append(('comic2epub', args, _('Convert comic: ')+opts.title,
|
jobs.append(('comic2epub', args, _('Convert comic: ')+opts.title,
|
||||||
'EPUB', db.id(row), [pt, of]))
|
'EPUB', row_id, [pt, of]))
|
||||||
|
|
||||||
return jobs, changed
|
return jobs, changed
|
||||||
|
|
||||||
@ -85,7 +87,9 @@ def convert_single_epub(parent, db, comics, others):
|
|||||||
def convert_single_lrf(parent, db, comics, others):
|
def convert_single_lrf(parent, db, comics, others):
|
||||||
changed = False
|
changed = False
|
||||||
jobs = []
|
jobs = []
|
||||||
for row in others:
|
others_ids = [db.id(row) for row in others]
|
||||||
|
comics_ids = [db.id(row) for row in comics]
|
||||||
|
for row, row_id in zip(others, others_ids):
|
||||||
temp_files = []
|
temp_files = []
|
||||||
d = LRFSingleDialog(parent, db, row)
|
d = LRFSingleDialog(parent, db, row)
|
||||||
if d.selected_format:
|
if d.selected_format:
|
||||||
@ -104,10 +108,10 @@ def convert_single_lrf(parent, db, comics, others):
|
|||||||
temp_files.append(d.cover_file)
|
temp_files.append(d.cover_file)
|
||||||
temp_files.extend([pt, of])
|
temp_files.extend([pt, of])
|
||||||
jobs.append(('any2lrf', [cmdline], _('Convert book: ')+d.title(),
|
jobs.append(('any2lrf', [cmdline], _('Convert book: ')+d.title(),
|
||||||
'LRF', db.id(row), temp_files))
|
'LRF', row_id, temp_files))
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
for row in comics:
|
for row, row_id in zip(comics, comics_ids):
|
||||||
mi = db.get_metadata(row)
|
mi = db.get_metadata(row)
|
||||||
title = author = _('Unknown')
|
title = author = _('Unknown')
|
||||||
if mi.title:
|
if mi.title:
|
||||||
@ -138,7 +142,7 @@ def convert_single_lrf(parent, db, comics, others):
|
|||||||
args = [pt.name, opts]
|
args = [pt.name, opts]
|
||||||
changed = True
|
changed = True
|
||||||
jobs.append(('comic2lrf', args, _('Convert comic: ')+opts.title,
|
jobs.append(('comic2lrf', args, _('Convert comic: ')+opts.title,
|
||||||
'LRF', db.id(row), [pt, of]))
|
'LRF', row_id, [pt, of]))
|
||||||
|
|
||||||
return jobs, changed
|
return jobs, changed
|
||||||
|
|
||||||
@ -162,6 +166,7 @@ def convert_bulk_epub(parent, db, comics, others):
|
|||||||
parent.status_bar.showMessage(_('Starting Bulk conversion of %d books')%total, 2000)
|
parent.status_bar.showMessage(_('Starting Bulk conversion of %d books')%total, 2000)
|
||||||
|
|
||||||
for i, row in enumerate(others+comics):
|
for i, row in enumerate(others+comics):
|
||||||
|
row_id = db.id(row)
|
||||||
if row in others:
|
if row in others:
|
||||||
data = None
|
data = None
|
||||||
for fmt in EPUB_PREFERRED_SOURCE_FORMATS:
|
for fmt in EPUB_PREFERRED_SOURCE_FORMATS:
|
||||||
@ -198,7 +203,7 @@ def convert_bulk_epub(parent, db, comics, others):
|
|||||||
desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title))
|
desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title))
|
||||||
temp_files = [cf] if cf is not None else []
|
temp_files = [cf] if cf is not None else []
|
||||||
temp_files.extend([opf_file, pt, of])
|
temp_files.extend([opf_file, pt, of])
|
||||||
jobs.append(('any2epub', args, desc, 'EPUB', db.id(row), temp_files))
|
jobs.append(('any2epub', args, desc, 'EPUB', row_id, temp_files))
|
||||||
else:
|
else:
|
||||||
options = comic_opts.copy()
|
options = comic_opts.copy()
|
||||||
mi = db.get_metadata(row)
|
mi = db.get_metadata(row)
|
||||||
@ -224,7 +229,7 @@ def convert_bulk_epub(parent, db, comics, others):
|
|||||||
options.verbose = 1
|
options.verbose = 1
|
||||||
args = [pt.name, options]
|
args = [pt.name, options]
|
||||||
desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title))
|
desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title))
|
||||||
jobs.append(('comic2epub', args, desc, 'EPUB', db.id(row), [pt, of]))
|
jobs.append(('comic2epub', args, desc, 'EPUB', row_id, [pt, of]))
|
||||||
|
|
||||||
if bad_rows:
|
if bad_rows:
|
||||||
res = []
|
res = []
|
||||||
@ -255,6 +260,7 @@ def convert_bulk_lrf(parent, db, comics, others):
|
|||||||
parent.status_bar.showMessage(_('Starting Bulk conversion of %d books')%total, 2000)
|
parent.status_bar.showMessage(_('Starting Bulk conversion of %d books')%total, 2000)
|
||||||
|
|
||||||
for i, row in enumerate(others+comics):
|
for i, row in enumerate(others+comics):
|
||||||
|
row_id = db.id(row)
|
||||||
if row in others:
|
if row in others:
|
||||||
cmdline = list(d.cmdline)
|
cmdline = list(d.cmdline)
|
||||||
mi = db.get_metadata(row)
|
mi = db.get_metadata(row)
|
||||||
@ -294,7 +300,7 @@ def convert_bulk_lrf(parent, db, comics, others):
|
|||||||
desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title))
|
desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title))
|
||||||
temp_files = [cf] if cf is not None else []
|
temp_files = [cf] if cf is not None else []
|
||||||
temp_files.extend([pt, of])
|
temp_files.extend([pt, of])
|
||||||
jobs.append(('any2lrf', [cmdline], desc, 'LRF', db.id(row), temp_files))
|
jobs.append(('any2lrf', [cmdline], desc, 'LRF', row_id, temp_files))
|
||||||
else:
|
else:
|
||||||
options = comic_opts.copy()
|
options = comic_opts.copy()
|
||||||
mi = db.get_metadata(row)
|
mi = db.get_metadata(row)
|
||||||
@ -320,7 +326,7 @@ def convert_bulk_lrf(parent, db, comics, others):
|
|||||||
options.verbose = 1
|
options.verbose = 1
|
||||||
args = [pt.name, options]
|
args = [pt.name, options]
|
||||||
desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title))
|
desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title))
|
||||||
jobs.append(('comic2lrf', args, desc, 'LRF', db.id(row), [pt, of]))
|
jobs.append(('comic2lrf', args, desc, 'LRF', row_id, [pt, of]))
|
||||||
|
|
||||||
if bad_rows:
|
if bad_rows:
|
||||||
res = []
|
res = []
|
||||||
|
@ -523,7 +523,10 @@ class DocumentView(QWebView):
|
|||||||
self.manager.previous_document()
|
self.manager.previous_document()
|
||||||
event.accept()
|
event.accept()
|
||||||
return
|
return
|
||||||
return QWebView.wheelEvent(self, event)
|
ret = QWebView.wheelEvent(self, event)
|
||||||
|
if self.manager is not None:
|
||||||
|
self.manager.scrolled(self.scroll_fraction)
|
||||||
|
return ret
|
||||||
|
|
||||||
def keyPressEvent(self, event):
|
def keyPressEvent(self, event):
|
||||||
key = event.key()
|
key = event.key()
|
||||||
|
@ -244,6 +244,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
lambda x:self.find(unicode(self.search.text()), True, repeat=True))
|
lambda x:self.find(unicode(self.search.text()), True, repeat=True))
|
||||||
self.connect(self.action_full_screen, SIGNAL('triggered(bool)'),
|
self.connect(self.action_full_screen, SIGNAL('triggered(bool)'),
|
||||||
self.toggle_fullscreen)
|
self.toggle_fullscreen)
|
||||||
|
self.action_full_screen.setShortcuts([Qt.Key_F11, Qt.CTRL+Qt.SHIFT+Qt.Key_F])
|
||||||
self.connect(self.action_back, SIGNAL('triggered(bool)'), self.back)
|
self.connect(self.action_back, SIGNAL('triggered(bool)'), self.back)
|
||||||
self.connect(self.action_bookmark, SIGNAL('triggered(bool)'), self.bookmark)
|
self.connect(self.action_bookmark, SIGNAL('triggered(bool)'), self.bookmark)
|
||||||
self.connect(self.action_forward, SIGNAL('triggered(bool)'), self.forward)
|
self.connect(self.action_forward, SIGNAL('triggered(bool)'), self.forward)
|
||||||
@ -511,7 +512,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
self.metadata.show_opf(self.iterator.opf)
|
self.metadata.show_opf(self.iterator.opf)
|
||||||
title = self.iterator.opf.title
|
title = self.iterator.opf.title
|
||||||
if not title:
|
if not title:
|
||||||
title = os.path.splitext(os.path.basename(pathtoebook))
|
title = os.path.splitext(os.path.basename(pathtoebook))[0]
|
||||||
self.action_table_of_contents.setDisabled(not self.iterator.toc)
|
self.action_table_of_contents.setDisabled(not self.iterator.toc)
|
||||||
if self.iterator.toc:
|
if self.iterator.toc:
|
||||||
self.toc_model = TOC(self.iterator.toc)
|
self.toc_model = TOC(self.iterator.toc)
|
||||||
|
@ -76,13 +76,25 @@ STANZA_TEMPLATE='''\
|
|||||||
<entry>
|
<entry>
|
||||||
<title>${record['title']}</title>
|
<title>${record['title']}</title>
|
||||||
<id>urn:calibre:${record['id']}</id>
|
<id>urn:calibre:${record['id']}</id>
|
||||||
<author><name>${record['authors']}</name></author>
|
<author><name>${record['author_sort']}</name></author>
|
||||||
<updated>${record['timestamp'].strftime('%Y-%m-%dT%H:%M:%SZ')}</updated>
|
<updated>${record['timestamp'].strftime('%Y-%m-%dT%H:%M:%SZ')}</updated>
|
||||||
<link type="application/epub+zip" href="${quote(record['fmt_epub'].replace(sep, '/')).replace('http%3A', 'http:')}" />
|
<link type="application/epub+zip" href="${quote(record['fmt_epub'].replace(sep, '/')).replace('http%3A', 'http:')}" />
|
||||||
<link py:if="record['cover']" rel="x-stanza-cover-image" type="image/png" href="${quote(record['cover'].replace(sep, '/')).replace('http%3A', 'http:')}" />
|
<link py:if="record['cover']" rel="x-stanza-cover-image" type="image/png" href="${quote(record['cover'].replace(sep, '/')).replace('http%3A', 'http:')}" />
|
||||||
<link py:if="record['cover']" rel="x-stanza-cover-image-thumbnail" type="image/png" href="${quote(record['cover'].replace(sep, '/')).replace('http%3A', 'http:')}" />
|
<link py:if="record['cover']" rel="x-stanza-cover-image-thumbnail" type="image/png" href="${quote(record['cover'].replace(sep, '/')).replace('http%3A', 'http:')}" />
|
||||||
<content type="xhtml">
|
<content type="xhtml">
|
||||||
<div xmlns="http://www.w3.org/1999/xhtml">${record['comments']}</div>
|
<div xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<py:for each="f in ('authors', 'publisher', 'rating', 'tags', 'series', 'isbn')">
|
||||||
|
<py:if test="record[f]">
|
||||||
|
${f.capitalize()}:${unicode(', '.join(record[f]) if f=='tags' else record[f])}
|
||||||
|
<py:if test="f =='series'"># ${str(record['series_index'])}</py:if>
|
||||||
|
<br/>
|
||||||
|
</py:if>
|
||||||
|
</py:for>
|
||||||
|
<py:if test="record['comments']">
|
||||||
|
<br/>
|
||||||
|
${record['comments']}
|
||||||
|
</py:if>
|
||||||
|
</div>
|
||||||
</content>
|
</content>
|
||||||
</entry>
|
</entry>
|
||||||
</py:for>
|
</py:for>
|
||||||
@ -221,7 +233,7 @@ NULL = DevNull()
|
|||||||
|
|
||||||
def do_add(db, paths, one_book_per_directory, recurse, add_duplicates):
|
def do_add(db, paths, one_book_per_directory, recurse, add_duplicates):
|
||||||
orig = sys.stdout
|
orig = sys.stdout
|
||||||
sys.stdout = NULL
|
#sys.stdout = NULL
|
||||||
try:
|
try:
|
||||||
files, dirs = [], []
|
files, dirs = [], []
|
||||||
for path in paths:
|
for path in paths:
|
||||||
|
@ -944,6 +944,10 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
|
|||||||
return self.conn.get('SELECT publisher FROM meta WHERE id=?', (index,), all=False)
|
return self.conn.get('SELECT publisher FROM meta WHERE id=?', (index,), all=False)
|
||||||
return self.data[index][3]
|
return self.data[index][3]
|
||||||
|
|
||||||
|
def publisher_id(self, index, index_is_id=False):
|
||||||
|
id = index if index_is_id else self.id(index)
|
||||||
|
return self.conn.get('SELECT publisher from books_publishers_link WHERE book=?', (id,), all=False)
|
||||||
|
|
||||||
def rating(self, index, index_is_id=False):
|
def rating(self, index, index_is_id=False):
|
||||||
if index_is_id:
|
if index_is_id:
|
||||||
return self.conn.get('SELECT rating FROM meta WHERE id=?', (index,), all=False)
|
return self.conn.get('SELECT rating FROM meta WHERE id=?', (index,), all=False)
|
||||||
@ -1042,6 +1046,14 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
|
|||||||
return [ (i[0], i[1]) for i in \
|
return [ (i[0], i[1]) for i in \
|
||||||
self.conn.get('SELECT id, name FROM series')]
|
self.conn.get('SELECT id, name FROM series')]
|
||||||
|
|
||||||
|
def all_authors(self):
|
||||||
|
return [ (i[0], i[1]) for i in \
|
||||||
|
self.conn.get('SELECT id, name FROM authors')]
|
||||||
|
|
||||||
|
def all_publishers(self):
|
||||||
|
return [ (i[0], i[1]) for i in \
|
||||||
|
self.conn.get('SELECT id, name FROM publishers')]
|
||||||
|
|
||||||
def all_tags(self):
|
def all_tags(self):
|
||||||
return [i[0].strip() for i in self.conn.get('SELECT name FROM tags') if i[0].strip()]
|
return [i[0].strip() for i in self.conn.get('SELECT name FROM tags') if i[0].strip()]
|
||||||
|
|
||||||
|
@ -21,13 +21,12 @@ from calibre.library.sqlite import connect, IntegrityError
|
|||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
from calibre.ebooks.metadata import string_to_authors, authors_to_string
|
from calibre.ebooks.metadata import string_to_authors, authors_to_string
|
||||||
from calibre.ebooks.metadata.meta import get_metadata
|
from calibre.ebooks.metadata.meta import get_metadata
|
||||||
from calibre.constants import preferred_encoding, iswindows, isosx
|
from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.customize.ui import run_plugins_on_import
|
from calibre.customize.ui import run_plugins_on_import
|
||||||
|
from calibre import sanitize_file_name
|
||||||
|
|
||||||
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
|
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
|
||||||
filesystem_encoding = sys.getfilesystemencoding()
|
|
||||||
if filesystem_encoding is None: filesystem_encoding = 'utf-8'
|
|
||||||
iscaseinsensitive = iswindows or isosx
|
iscaseinsensitive = iswindows or isosx
|
||||||
|
|
||||||
def normpath(x):
|
def normpath(x):
|
||||||
@ -37,23 +36,6 @@ def normpath(x):
|
|||||||
x = x.lower()
|
x = x.lower()
|
||||||
return x
|
return x
|
||||||
|
|
||||||
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+\[\]/]')
|
|
||||||
|
|
||||||
def sanitize_file_name(name, substitute='_'):
|
|
||||||
'''
|
|
||||||
Sanitize the filename `name`. All invalid characters are replaced by `substitute`.
|
|
||||||
The set of invalid characters is the union of the invalid characters in Windows,
|
|
||||||
OS X and Linux. Also removes leading an trailing whitespace.
|
|
||||||
**WARNING:** This function also replaces path separators, so only pass file names
|
|
||||||
and not full paths to it.
|
|
||||||
*NOTE:* This function always returns byte strings, not unicode objects. The byte strings
|
|
||||||
are encoded in the filesystem encoding of the platform, or UTF-8.
|
|
||||||
'''
|
|
||||||
if isinstance(name, unicode):
|
|
||||||
name = name.encode(filesystem_encoding, 'ignore')
|
|
||||||
one = _filename_sanitize.sub(substitute, name)
|
|
||||||
one = re.sub(r'\s', ' ', one).strip()
|
|
||||||
return re.sub(r'^\.+$', '_', one)
|
|
||||||
|
|
||||||
FIELD_MAP = {'id':0, 'title':1, 'authors':2, 'publisher':3, 'rating':4, 'timestamp':5,
|
FIELD_MAP = {'id':0, 'title':1, 'authors':2, 'publisher':3, 'rating':4, 'timestamp':5,
|
||||||
'size':6, 'tags':7, 'comments':8, 'series':9, 'series_index':10,
|
'size':6, 'tags':7, 'comments':8, 'series':9, 'series_index':10,
|
||||||
@ -489,8 +471,8 @@ class LibraryDatabase2(LibraryDatabase):
|
|||||||
authors = self.authors(id, index_is_id=True)
|
authors = self.authors(id, index_is_id=True)
|
||||||
if not authors:
|
if not authors:
|
||||||
authors = _('Unknown')
|
authors = _('Unknown')
|
||||||
author = sanitize_file_name(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding)
|
author = sanitize_file_name(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding, 'replace')
|
||||||
title = sanitize_file_name(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding)
|
title = sanitize_file_name(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding, 'replace')
|
||||||
name = title + ' - ' + author
|
name = title + ' - ' + author
|
||||||
return name
|
return name
|
||||||
|
|
||||||
@ -896,6 +878,14 @@ class LibraryDatabase2(LibraryDatabase):
|
|||||||
if notify:
|
if notify:
|
||||||
self.notify('metadata', [id])
|
self.notify('metadata', [id])
|
||||||
|
|
||||||
|
def set_timestamp(self, id, dt, notify=True):
|
||||||
|
if dt:
|
||||||
|
self.conn.execute('UPDATE books SET timestamp=? WHERE id=?', (dt, id))
|
||||||
|
self.data.set(id, FIELD_MAP['timestamp'], dt, row_is_id=True)
|
||||||
|
self.conn.commit()
|
||||||
|
if notify:
|
||||||
|
self.notify('metadata', [id])
|
||||||
|
|
||||||
def set_publisher(self, id, publisher, notify=True):
|
def set_publisher(self, id, publisher, notify=True):
|
||||||
self.conn.execute('DELETE FROM books_publishers_link WHERE book=?',(id,))
|
self.conn.execute('DELETE FROM books_publishers_link WHERE book=?',(id,))
|
||||||
self.conn.execute('DELETE FROM publishers WHERE (SELECT COUNT(id) FROM books_publishers_link WHERE publisher=publishers.id) < 1')
|
self.conn.execute('DELETE FROM publishers WHERE (SELECT COUNT(id) FROM books_publishers_link WHERE publisher=publishers.id) < 1')
|
||||||
@ -1118,8 +1108,13 @@ class LibraryDatabase2(LibraryDatabase):
|
|||||||
continue
|
continue
|
||||||
series_index = 1 if mi.series_index is None else mi.series_index
|
series_index = 1 if mi.series_index is None else mi.series_index
|
||||||
aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
|
aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
|
||||||
|
title = mi.title
|
||||||
|
if isinstance(aus, str):
|
||||||
|
aus = aus.decode(preferred_encoding, 'replace')
|
||||||
|
if isinstance(title, str):
|
||||||
|
title = title.decode(preferred_encoding)
|
||||||
obj = self.conn.execute('INSERT INTO books(title, uri, series_index, author_sort) VALUES (?, ?, ?, ?)',
|
obj = self.conn.execute('INSERT INTO books(title, uri, series_index, author_sort) VALUES (?, ?, ?, ?)',
|
||||||
(mi.title, uri, series_index, aus))
|
(title, uri, series_index, aus))
|
||||||
id = obj.lastrowid
|
id = obj.lastrowid
|
||||||
self.data.books_added([id], self.conn)
|
self.data.books_added([id], self.conn)
|
||||||
ids.append(id)
|
ids.append(id)
|
||||||
|
@ -2,10 +2,10 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
''' Post installation script for linux '''
|
''' Post installation script for linux '''
|
||||||
import sys, os, re, shutil
|
import sys, os, re, shutil
|
||||||
|
|
||||||
from subprocess import check_call, call
|
from subprocess import check_call, call
|
||||||
from calibre import __version__, __appname__
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
||||||
|
from calibre import __version__, __appname__
|
||||||
from calibre.devices import devices
|
from calibre.devices import devices
|
||||||
|
|
||||||
DEVICES = devices()
|
DEVICES = devices()
|
||||||
@ -401,9 +401,9 @@ def install_man_pages(fatal_errors):
|
|||||||
import subprocess
|
import subprocess
|
||||||
print 'Installing MAN pages...'
|
print 'Installing MAN pages...'
|
||||||
manpath = '/usr/share/man/man1'
|
manpath = '/usr/share/man/man1'
|
||||||
f = open_file('/tmp/man_extra', 'wb')
|
f = NamedTemporaryFile()
|
||||||
f.write('[see also]\nhttp://%s.kovidgoyal.net\n'%__appname__)
|
f.write('[see also]\nhttp://%s.kovidgoyal.net\n'%__appname__)
|
||||||
f.close()
|
f.flush()
|
||||||
manifest = []
|
manifest = []
|
||||||
os.environ['PATH'] += ':'+os.path.expanduser('~/bin')
|
os.environ['PATH'] += ':'+os.path.expanduser('~/bin')
|
||||||
for src in entry_points['console_scripts']:
|
for src in entry_points['console_scripts']:
|
||||||
|
@ -46,7 +46,7 @@ Create a file name :file:`my_plugin.py` (the file name must end with plugin.py)
|
|||||||
ext = os.path.splitext(path_to_ebook)[-1][1:].lower()
|
ext = os.path.splitext(path_to_ebook)[-1][1:].lower()
|
||||||
mi = get_metadata(file, ext)
|
mi = get_metadata(file, ext)
|
||||||
mi.publisher = 'Hello World'
|
mi.publisher = 'Hello World'
|
||||||
set_metadata(file, ext, mi)
|
set_metadata(file, mi, ext)
|
||||||
return path_to_ebook
|
return path_to_ebook
|
||||||
|
|
||||||
That's all. To add this code to |app| as a plugin, simply create a zip file with::
|
That's all. To add this code to |app| as a plugin, simply create a zip file with::
|
||||||
|
@ -143,6 +143,15 @@ Where are the book files stored?
|
|||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
When you first run |app|, it will ask you for a folder in which to store your books. Whenever you add a book to |app|, it will copy the book into that folder. Books in the folder are nicely arranged into sub-folders by Author and Title. Metadata about the books is stored in the file ``metadata.db`` (which is a sqlite database).
|
When you first run |app|, it will ask you for a folder in which to store your books. Whenever you add a book to |app|, it will copy the book into that folder. Books in the folder are nicely arranged into sub-folders by Author and Title. Metadata about the books is stored in the file ``metadata.db`` (which is a sqlite database).
|
||||||
|
|
||||||
|
Why doesn't |app| let me store books in my own directory structure?
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The whole point if |app|'s library management features is that they provide an interface for locating books that is *much* more efficient than any possible directory scheme you could come up with for your collection. Indeed, once you become comfortable using |app|'s interface to find, sort and browse your collection, you wont ever feel the need to hunt through the files on your disk to find a book again. By managing books in its own directory struture of Author -> Title -> Book files, |app| is able to achieve a high level of reliability and standardization.
|
||||||
|
|
||||||
|
Why doesn't |app| have a column for foo?
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|app| is designed to have columns for the most frequently and widely used fields. If it does not have a coulmn for your favorite field, you can always add a tag to the book for that piece of information. |app| also supports a general purpose "comments" fields for longer items.
|
||||||
|
|
||||||
|
|
||||||
Content From The Web
|
Content From The Web
|
||||||
---------------------
|
---------------------
|
||||||
|
@ -133,7 +133,7 @@ to the recipe. Finally, lets replace some of the :term:`CSS` that we disabled ea
|
|||||||
|
|
||||||
With these additions, our recipe has become "production quality", indeed it is very close to the actual recipe used by |app| for the *BBC*, shown below:
|
With these additions, our recipe has become "production quality", indeed it is very close to the actual recipe used by |app| for the *BBC*, shown below:
|
||||||
|
|
||||||
.. literalinclude:: ../web/feeds/recipes/bbc.py
|
.. literalinclude:: ../web/feeds/recipes/recipe_bbc.py
|
||||||
|
|
||||||
This :term:`recipe` explores only the tip of the iceberg when it comes to the power of |app|. To explore more of the abilities of |app| we'll examine a more complex real life example in the next section.
|
This :term:`recipe` explores only the tip of the iceberg when it comes to the power of |app|. To explore more of the abilities of |app| we'll examine a more complex real life example in the next section.
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ from threading import RLock, Thread, Event
|
|||||||
from math import ceil
|
from math import ceil
|
||||||
|
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre import iswindows, detect_ncpus, isosx
|
from calibre import iswindows, detect_ncpus, isosx, preferred_encoding
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
|
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
@ -615,7 +615,9 @@ class Job(object):
|
|||||||
self.log = unicode(self.log, 'utf-8', 'replace')
|
self.log = unicode(self.log, 'utf-8', 'replace')
|
||||||
ans.extend(self.log.split('\n'))
|
ans.extend(self.log.split('\n'))
|
||||||
|
|
||||||
return '<br>'.join(ans)
|
ans = [x.decode(preferred_encoding, 'replace') if isinstance(x, str) else x for x in ans]
|
||||||
|
|
||||||
|
return u'<br>'.join(ans)
|
||||||
|
|
||||||
|
|
||||||
class ParallelJob(Job):
|
class ParallelJob(Job):
|
||||||
|
@ -21,7 +21,7 @@ from lxml import etree
|
|||||||
|
|
||||||
def range_for_month(year, month):
|
def range_for_month(year, month):
|
||||||
ty, tm = date.today().year, date.today().month
|
ty, tm = date.today().year, date.today().month
|
||||||
min = date(year=year, month=month, day=1)
|
min = max = date(year=year, month=month, day=1)
|
||||||
x = date.today().day if ty == year and tm == month else 31
|
x = date.today().day if ty == year and tm == month else 31
|
||||||
while x > 1:
|
while x > 1:
|
||||||
try:
|
try:
|
||||||
@ -102,6 +102,8 @@ class Stats:
|
|||||||
|
|
||||||
def get_deviation(self, amounts):
|
def get_deviation(self, amounts):
|
||||||
l = float(len(amounts))
|
l = float(len(amounts))
|
||||||
|
if l == 0:
|
||||||
|
return 0
|
||||||
mean = sum(amounts)/l
|
mean = sum(amounts)/l
|
||||||
return sqrt( sum([i**2 for i in amounts])/l - mean**2 )
|
return sqrt( sum([i**2 for i in amounts])/l - mean**2 )
|
||||||
|
|
||||||
@ -199,7 +201,7 @@ class Server(object):
|
|||||||
x = list(range(days-1, -1, -1))
|
x = list(range(days-1, -1, -1))
|
||||||
y = stats.daily_totals
|
y = stats.daily_totals
|
||||||
ax.plot(x, y)#, align='center', width=20, color='g')
|
ax.plot(x, y)#, align='center', width=20, color='g')
|
||||||
ax.set_xlabel('Day')
|
ax.set_xlabel('Days ago')
|
||||||
ax.set_ylabel('Income ($)')
|
ax.set_ylabel('Income ($)')
|
||||||
ax.hlines([stats.daily_average], 0, days-1)
|
ax.hlines([stats.daily_average], 0, days-1)
|
||||||
ax.set_xlim([0, days-1])
|
ax.set_xlim([0, days-1])
|
||||||
|
@ -17,7 +17,9 @@ from calibre.constants import terminal_controller, iswindows, isosx, \
|
|||||||
from calibre.utils.lock import LockError, ExclusiveFile
|
from calibre.utils.lock import LockError, ExclusiveFile
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
if iswindows:
|
if os.environ.has_key('CALIBRE_CONFIG_DIRECTORY'):
|
||||||
|
config_dir = os.path.abspath(os.environ['CALIBRE_CONFIG_DIRECTORY'])
|
||||||
|
elif iswindows:
|
||||||
config_dir = plugins['winutil'][0].special_folder_path(plugins['winutil'][0].CSIDL_APPDATA)
|
config_dir = plugins['winutil'][0].special_folder_path(plugins['winutil'][0].CSIDL_APPDATA)
|
||||||
if not os.access(config_dir, os.W_OK|os.X_OK):
|
if not os.access(config_dir, os.W_OK|os.X_OK):
|
||||||
config_dir = os.path.expanduser('~')
|
config_dir = os.path.expanduser('~')
|
||||||
|
@ -22,7 +22,7 @@ match to a given font specification. The main functions in this module are:
|
|||||||
.. autofunction:: match
|
.. autofunction:: match
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import sys, os, locale, codecs
|
import sys, os, locale, codecs, subprocess, re
|
||||||
from ctypes import cdll, c_void_p, Structure, c_int, POINTER, c_ubyte, c_char, util, \
|
from ctypes import cdll, c_void_p, Structure, c_int, POINTER, c_ubyte, c_char, util, \
|
||||||
pointer, byref, create_string_buffer, Union, c_char_p, c_double
|
pointer, byref, create_string_buffer, Union, c_char_p, c_double
|
||||||
|
|
||||||
@ -34,6 +34,7 @@ except:
|
|||||||
|
|
||||||
iswindows = 'win32' in sys.platform or 'win64' in sys.platform
|
iswindows = 'win32' in sys.platform or 'win64' in sys.platform
|
||||||
isosx = 'darwin' in sys.platform
|
isosx = 'darwin' in sys.platform
|
||||||
|
isbsd = 'bsd' in sys.platform
|
||||||
DISABLED = False
|
DISABLED = False
|
||||||
#if isosx:
|
#if isosx:
|
||||||
# libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('c'))
|
# libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('c'))
|
||||||
@ -57,6 +58,13 @@ def load_library():
|
|||||||
return cdll.LoadLibrary(lib)
|
return cdll.LoadLibrary(lib)
|
||||||
elif iswindows:
|
elif iswindows:
|
||||||
return cdll.LoadLibrary('libfontconfig-1')
|
return cdll.LoadLibrary('libfontconfig-1')
|
||||||
|
elif isbsd:
|
||||||
|
raw = subprocess.Popen('pkg-config --libs-only-L fontconfig'.split(),
|
||||||
|
stdout=subprocess.PIPE).stdout.read().strip()
|
||||||
|
match = re.search(r'-L([^\s,]+)', raw)
|
||||||
|
if not match:
|
||||||
|
return cdll.LoadLibrary('libfontconfig.so')
|
||||||
|
return cdll.LoadLibrary(match.group(1)+'/libfontconfig.so')
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
return cdll.LoadLibrary(util.find_library('fontconfig'))
|
return cdll.LoadLibrary(util.find_library('fontconfig'))
|
||||||
|
@ -1,60 +1,25 @@
|
|||||||
/* crypto/des/spr.h */
|
/*--[crypto/des/spr.h]---------------------------------------------------------
|
||||||
/* Copyright (C) 1995-1997 Eric Young (eay@cryptsoft.com)
|
| Copyright (C) 2002 Dan A. Jackson
|
||||||
* All rights reserved.
|
|
|
||||||
*
|
| This file is part of the "openclit" library for processing .LIT files.
|
||||||
* This package is an SSL implementation written
|
|
|
||||||
* by Eric Young (eay@cryptsoft.com).
|
| "Openclit" is free software; you can redistribute it and/or modify
|
||||||
* The implementation was written so as to conform with Netscapes SSL.
|
| it under the terms of the GNU General Public License as published by
|
||||||
*
|
| the Free Software Foundation; either version 2 of the License, or
|
||||||
* This library is free for commercial and non-commercial use as long as
|
| (at your option) any later version.
|
||||||
* the following conditions are aheared to. The following conditions
|
|
|
||||||
* apply to all code found in this distribution, be it the RC4, RSA,
|
| This program is distributed in the hope that it will be useful,
|
||||||
* lhash, DES, etc., code; not just the SSL code. The SSL documentation
|
| but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* included with this distribution is covered by the same copyright terms
|
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* except that the holder is Tim Hudson (tjh@cryptsoft.com).
|
| GNU General Public License for more details.
|
||||||
*
|
|
|
||||||
* Copyright remains Eric Young's, and as such any Copyright notices in
|
| You should have received a copy of the GNU General Public License
|
||||||
* the code are not to be removed.
|
| along with this program; if not, write to the Free Software
|
||||||
* If this package is used in a product, Eric Young should be given attribution
|
| Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
||||||
* as the author of the parts of the library used.
|
|
|
||||||
* This can be in the form of a textual message at program startup or
|
| The GNU General Public License may also be available at the following
|
||||||
* in documentation (online or textual) provided with the package.
|
| URL: http://www.gnu.org/licenses/gpl.html
|
||||||
*
|
*/
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions
|
|
||||||
* are met:
|
|
||||||
* 1. Redistributions of source code must retain the copyright
|
|
||||||
* notice, this list of conditions and the following disclaimer.
|
|
||||||
* 2. Redistributions in binary form must reproduce the above copyright
|
|
||||||
* notice, this list of conditions and the following disclaimer in the
|
|
||||||
* documentation and/or other materials provided with the distribution.
|
|
||||||
* 3. All advertising materials mentioning features or use of this software
|
|
||||||
* must display the following acknowledgement:
|
|
||||||
* "This product includes cryptographic software written by
|
|
||||||
* Eric Young (eay@cryptsoft.com)"
|
|
||||||
* The word 'cryptographic' can be left out if the rouines from the library
|
|
||||||
* being used are not cryptographic related :-).
|
|
||||||
* 4. If you include any Windows specific code (or a derivative thereof) from
|
|
||||||
* the apps directory (application code) you must include an acknowledgement:
|
|
||||||
* "This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
|
|
||||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
||||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
|
||||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
||||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
||||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
||||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
||||||
* SUCH DAMAGE.
|
|
||||||
*
|
|
||||||
* The licence and distribution terms for any publically available version or
|
|
||||||
* derivative of this code cannot be changed. i.e. this code cannot simply be
|
|
||||||
* copied and put under another distribution licence
|
|
||||||
* [including the GNU Public Licence.]
|
|
||||||
*/
|
|
||||||
|
|
||||||
static unsigned long SP1[64] = {
|
static unsigned long SP1[64] = {
|
||||||
0x02080800L, 0x00080000L, 0x02000002L, 0x02080802L,
|
0x02080800L, 0x00080000L, 0x02000002L, 0x02080802L,
|
||||||
|
@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
'''
|
'''
|
||||||
Builtin recipes.
|
Builtin recipes.
|
||||||
'''
|
'''
|
||||||
recipe_modules = [
|
recipe_modules = ['recipe_' + r for r in (
|
||||||
'newsweek', 'atlantic', 'economist', 'portfolio',
|
'newsweek', 'atlantic', 'economist', 'portfolio',
|
||||||
'nytimes', 'usatoday', 'outlook_india', 'bbc', 'greader', 'wsj',
|
'nytimes', 'usatoday', 'outlook_india', 'bbc', 'greader', 'wsj',
|
||||||
'wired', 'globe_and_mail', 'smh', 'espn', 'business_week',
|
'wired', 'globe_and_mail', 'smh', 'espn', 'business_week',
|
||||||
@ -19,8 +19,10 @@ recipe_modules = [
|
|||||||
'clarin', 'financial_times', 'heise', 'le_monde', 'harpers', 'science_aas',
|
'clarin', 'financial_times', 'heise', 'le_monde', 'harpers', 'science_aas',
|
||||||
'science_news', 'the_nation', 'lrb', 'harpers_full', 'liberation',
|
'science_news', 'the_nation', 'lrb', 'harpers_full', 'liberation',
|
||||||
'linux_magazine', 'telegraph_uk', 'utne', 'sciencedaily', 'forbes',
|
'linux_magazine', 'telegraph_uk', 'utne', 'sciencedaily', 'forbes',
|
||||||
'time_magazine', 'endgadget', 'fudzilla',
|
'time_magazine', 'endgadget', 'fudzilla', 'nspm_int', 'nspm', 'pescanik',
|
||||||
]
|
'spiegel_int', 'themarketticker', 'tomshardware', 'xkcd', 'ftd', 'zdnet',
|
||||||
|
'joelonsoftware',
|
||||||
|
)]
|
||||||
|
|
||||||
import re, imp, inspect, time, os
|
import re, imp, inspect, time, os
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe, CustomIndexRecipe, AutomaticNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe, CustomIndexRecipe, AutomaticNewsRecipe
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
|
||||||
__copyright__ = '2008, Darko Miletic <darko.miletic at gmail.com>'
|
|
||||||
'''
|
|
||||||
b92.net
|
|
||||||
'''
|
|
||||||
import string,re
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
|
||||||
|
|
||||||
class B92(BasicNewsRecipe):
|
|
||||||
title = u'B92'
|
|
||||||
__author__ = 'Darko Miletic'
|
|
||||||
description = 'Dnevne vesti iz Srbije i sveta'
|
|
||||||
oldest_article = 7
|
|
||||||
max_articles_per_feed = 100
|
|
||||||
no_stylesheets = True
|
|
||||||
use_embedded_content = False
|
|
||||||
|
|
||||||
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
|
|
||||||
|
|
||||||
remove_tags_after = dict(name='div', attrs={'class':'gas'})
|
|
||||||
remove_tags = [
|
|
||||||
dict(name='div' , attrs={'class':'interaction clearfix' })
|
|
||||||
,dict(name='div' , attrs={'class':'gas' })
|
|
||||||
,dict(name='ul' , attrs={'class':'comment-nav' })
|
|
||||||
,dict(name='table', attrs={'class':'pages-navigation-form'})
|
|
||||||
]
|
|
||||||
|
|
||||||
feeds = [
|
|
||||||
(u'Vesti' , u'http://www.b92.net/info/rss/vesti.xml' )
|
|
||||||
,(u'Kultura' , u'http://www.b92.net/info/rss/kultura.xml' )
|
|
||||||
,(u'Automobili', u'http://www.b92.net/info/rss/automobili.xml')
|
|
||||||
,(u'Zivot' , u'http://www.b92.net/info/rss/zivot.xml' )
|
|
||||||
,(u'Tehnopolis', u'http://www.b92.net/info/rss/tehnopolis.xml')
|
|
||||||
,(u'Biz' , u'http://www.b92.net/info/rss/biz.xml' )
|
|
||||||
]
|
|
||||||
|
|
||||||
def print_version(self, url):
|
|
||||||
return url + '&version=print'
|
|
@ -1,34 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
|
||||||
__copyright__ = '2008, Darko Miletic <darko.miletic at gmail.com>'
|
|
||||||
'''
|
|
||||||
danas.rs
|
|
||||||
'''
|
|
||||||
import string,re
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
|
||||||
|
|
||||||
class Danas(BasicNewsRecipe):
|
|
||||||
title = u'Danas'
|
|
||||||
__author__ = 'Darko Miletic'
|
|
||||||
description = 'Vesti'
|
|
||||||
oldest_article = 7
|
|
||||||
max_articles_per_feed = 100
|
|
||||||
no_stylesheets = True
|
|
||||||
use_embedded_content = False
|
|
||||||
timefmt = ' [%A, %d %B, %Y]'
|
|
||||||
|
|
||||||
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
|
|
||||||
|
|
||||||
keep_only_tags = [ dict(name='div', attrs={'id':'left'}) ]
|
|
||||||
remove_tags_after = dict(name='div', attrs={'id':'comments'})
|
|
||||||
remove_tags = [
|
|
||||||
dict(name='div', attrs={'class':'width_1_4'})
|
|
||||||
,dict(name='div', attrs={'class':'metaClanka'})
|
|
||||||
,dict(name='div', attrs={'id':'comments'})
|
|
||||||
,dict(name='div', attrs={'class':'baner'})
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
feeds = [ (u'Vesti', u'http://www.danas.rs/rss/rss.asp')]
|
|