1
0
mirror of https://github.com/gryf/ebook-converter.git synced 2026-03-05 00:15:54 +01:00

Initial import

This commit is contained in:
2020-03-31 17:15:23 +02:00
commit d97ea9b0bc
311 changed files with 131419 additions and 0 deletions

View File

@@ -0,0 +1,759 @@
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, sys, zipfile, importlib
from calibre.constants import numeric_version, iswindows, isosx
from calibre.ptempfile import PersistentTemporaryFile
from polyglot.builtins import unicode_type
platform = 'linux'
if iswindows:
platform = 'windows'
elif isosx:
platform = 'osx'
class PluginNotFound(ValueError):
pass
class InvalidPlugin(ValueError):
pass
class Plugin(object): # {{{
'''
A calibre plugin. Useful members include:
* ``self.plugin_path``: Stores path to the ZIP file that contains
this plugin or None if it is a builtin
plugin
* ``self.site_customization``: Stores a customization string entered
by the user.
Methods that should be overridden in sub classes:
* :meth:`initialize`
* :meth:`customization_help`
Useful methods:
* :meth:`temporary_file`
* :meth:`__enter__`
* :meth:`load_resources`
'''
#: List of platforms this plugin works on.
#: For example: ``['windows', 'osx', 'linux']``
supported_platforms = []
#: The name of this plugin. You must set it something other
#: than Trivial Plugin for it to work.
name = 'Trivial Plugin'
#: The version of this plugin as a 3-tuple (major, minor, revision)
version = (1, 0, 0)
#: A short string describing what this plugin does
description = _('Does absolutely nothing')
#: The author of this plugin
author = _('Unknown')
#: When more than one plugin exists for a filetype,
#: the plugins are run in order of decreasing priority.
#: Plugins with higher priority will be run first.
#: The highest possible priority is ``sys.maxsize``.
#: Default priority is 1.
priority = 1
#: The earliest version of calibre this plugin requires
minimum_calibre_version = (0, 4, 118)
#: If False, the user will not be able to disable this plugin. Use with
#: care.
can_be_disabled = True
#: The type of this plugin. Used for categorizing plugins in the
#: GUI
type = _('Base')
def __init__(self, plugin_path):
self.plugin_path = plugin_path
self.site_customization = None
def initialize(self):
'''
Called once when calibre plugins are initialized. Plugins are
re-initialized every time a new plugin is added. Also note that if the
plugin is run in a worker process, such as for adding books, then the
plugin will be initialized for every new worker process.
Perform any plugin specific initialization here, such as extracting
resources from the plugin ZIP file. The path to the ZIP file is
available as ``self.plugin_path``.
Note that ``self.site_customization`` is **not** available at this point.
'''
pass
def config_widget(self):
'''
Implement this method and :meth:`save_settings` in your plugin to
use a custom configuration dialog, rather then relying on the simple
string based default customization.
This method, if implemented, must return a QWidget. The widget can have
an optional method validate() that takes no arguments and is called
immediately after the user clicks OK. Changes are applied if and only
if the method returns True.
If for some reason you cannot perform the configuration at this time,
return a tuple of two strings (message, details), these will be
displayed as a warning dialog to the user and the process will be
aborted.
'''
raise NotImplementedError()
def save_settings(self, config_widget):
'''
Save the settings specified by the user with config_widget.
:param config_widget: The widget returned by :meth:`config_widget`.
'''
raise NotImplementedError()
def do_user_config(self, parent=None):
'''
This method shows a configuration dialog for this plugin. It returns
True if the user clicks OK, False otherwise. The changes are
automatically applied.
'''
from PyQt5.Qt import QDialog, QDialogButtonBox, QVBoxLayout, \
QLabel, Qt, QLineEdit
from calibre.gui2 import gprefs
prefname = 'plugin config dialog:'+self.type + ':' + self.name
geom = gprefs.get(prefname, None)
config_dialog = QDialog(parent)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
v = QVBoxLayout(config_dialog)
def size_dialog():
if geom is None:
config_dialog.resize(config_dialog.sizeHint())
else:
from PyQt5.Qt import QApplication
QApplication.instance().safe_restore_geometry(config_dialog, geom)
button_box.accepted.connect(config_dialog.accept)
button_box.rejected.connect(config_dialog.reject)
config_dialog.setWindowTitle(_('Customize') + ' ' + self.name)
try:
config_widget = self.config_widget()
except NotImplementedError:
config_widget = None
if isinstance(config_widget, tuple):
from calibre.gui2 import warning_dialog
warning_dialog(parent, _('Cannot configure'), config_widget[0],
det_msg=config_widget[1], show=True)
return False
if config_widget is not None:
v.addWidget(config_widget)
v.addWidget(button_box)
size_dialog()
config_dialog.exec_()
if config_dialog.result() == QDialog.Accepted:
if hasattr(config_widget, 'validate'):
if config_widget.validate():
self.save_settings(config_widget)
else:
self.save_settings(config_widget)
else:
from calibre.customize.ui import plugin_customization, \
customize_plugin
help_text = self.customization_help(gui=True)
help_text = QLabel(help_text, config_dialog)
help_text.setWordWrap(True)
help_text.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
help_text.setOpenExternalLinks(True)
v.addWidget(help_text)
sc = plugin_customization(self)
if not sc:
sc = ''
sc = sc.strip()
sc = QLineEdit(sc, config_dialog)
v.addWidget(sc)
v.addWidget(button_box)
size_dialog()
config_dialog.exec_()
if config_dialog.result() == QDialog.Accepted:
sc = unicode_type(sc.text()).strip()
customize_plugin(self, sc)
geom = bytearray(config_dialog.saveGeometry())
gprefs[prefname] = geom
return config_dialog.result()
def load_resources(self, names):
'''
If this plugin comes in a ZIP file (user added plugin), this method
will allow you to load resources from the ZIP file.
For example to load an image::
pixmap = QPixmap()
pixmap.loadFromData(self.load_resources(['images/icon.png'])['images/icon.png'])
icon = QIcon(pixmap)
:param names: List of paths to resources in the ZIP file using / as separator
:return: A dictionary of the form ``{name: file_contents}``. Any names
that were not found in the ZIP file will not be present in the
dictionary.
'''
if self.plugin_path is None:
raise ValueError('This plugin was not loaded from a ZIP file')
ans = {}
with zipfile.ZipFile(self.plugin_path, 'r') as zf:
for candidate in zf.namelist():
if candidate in names:
ans[candidate] = zf.read(candidate)
return ans
def customization_help(self, gui=False):
'''
Return a string giving help on how to customize this plugin.
By default raise a :class:`NotImplementedError`, which indicates that
the plugin does not require customization.
If you re-implement this method in your subclass, the user will
be asked to enter a string as customization for this plugin.
The customization string will be available as
``self.site_customization``.
Site customization could be anything, for example, the path to
a needed binary on the user's computer.
:param gui: If True return HTML help, otherwise return plain text help.
'''
raise NotImplementedError()
def temporary_file(self, suffix):
'''
Return a file-like object that is a temporary file on the file system.
This file will remain available even after being closed and will only
be removed on interpreter shutdown. Use the ``name`` member of the
returned object to access the full path to the created temporary file.
:param suffix: The suffix that the temporary file will have.
'''
return PersistentTemporaryFile(suffix)
def is_customizable(self):
try:
self.customization_help()
return True
except NotImplementedError:
return False
def __enter__(self, *args):
'''
Add this plugin to the python path so that it's contents become directly importable.
Useful when bundling large python libraries into the plugin. Use it like this::
with plugin:
import something
'''
if self.plugin_path is not None:
from calibre.utils.zipfile import ZipFile
zf = ZipFile(self.plugin_path)
extensions = {x.rpartition('.')[-1].lower() for x in
zf.namelist()}
zip_safe = True
for ext in ('pyd', 'so', 'dll', 'dylib'):
if ext in extensions:
zip_safe = False
break
if zip_safe:
sys.path.insert(0, self.plugin_path)
self.sys_insertion_path = self.plugin_path
else:
from calibre.ptempfile import TemporaryDirectory
self._sys_insertion_tdir = TemporaryDirectory('plugin_unzip')
self.sys_insertion_path = self._sys_insertion_tdir.__enter__(*args)
zf.extractall(self.sys_insertion_path)
sys.path.insert(0, self.sys_insertion_path)
zf.close()
def __exit__(self, *args):
ip, it = getattr(self, 'sys_insertion_path', None), getattr(self,
'_sys_insertion_tdir', None)
if ip in sys.path:
sys.path.remove(ip)
if hasattr(it, '__exit__'):
it.__exit__(*args)
def cli_main(self, args):
'''
This method is the main entry point for your plugins command line
interface. It is called when the user does: calibre-debug -r "Plugin
Name". Any arguments passed are present in the args variable.
'''
raise NotImplementedError('The %s plugin has no command line interface'
%self.name)
# }}}
class FileTypePlugin(Plugin): # {{{
'''
A plugin that is associated with a particular set of file types.
'''
#: Set of file types for which this plugin should be run.
#: Use '*' for all file types.
#: For example: ``{'lit', 'mobi', 'prc'}``
file_types = set()
#: If True, this plugin is run when books are added
#: to the database
on_import = False
#: If True, this plugin is run after books are added
#: to the database. In this case the postimport and postadd
#: methods of the plugin are called.
on_postimport = False
#: If True, this plugin is run just before a conversion
on_preprocess = False
#: If True, this plugin is run after conversion
#: on the final file produced by the conversion output plugin.
on_postprocess = False
type = _('File type')
def run(self, path_to_ebook):
'''
Run the plugin. Must be implemented in subclasses.
It should perform whatever modifications are required
on the e-book and return the absolute path to the
modified e-book. If no modifications are needed, it should
return the path to the original e-book. If an error is encountered
it should raise an Exception. The default implementation
simply return the path to the original e-book. Note that the path to
the original file (before any file type plugins are run, is available as
self.original_path_to_file).
The modified e-book file should be created with the
:meth:`temporary_file` method.
:param path_to_ebook: Absolute path to the e-book.
:return: Absolute path to the modified e-book.
'''
# Default implementation does nothing
return path_to_ebook
def postimport(self, book_id, book_format, db):
'''
Called post import, i.e., after the book file has been added to the database. Note that
this is different from :meth:`postadd` which is called when the book record is created for
the first time. This method is called whenever a new file is added to a book record. It is
useful for modifying the book record based on the contents of the newly added file.
:param book_id: Database id of the added book.
:param book_format: The file type of the book that was added.
:param db: Library database.
'''
pass # Default implementation does nothing
def postadd(self, book_id, fmt_map, db):
'''
Called post add, i.e. after a book has been added to the db. Note that
this is different from :meth:`postimport`, which is called after a single book file
has been added to a book. postadd() is called only when an entire book record
with possibly more than one book file has been created for the first time.
This is useful if you wish to modify the book record in the database when the
book is first added to calibre.
:param book_id: Database id of the added book.
:param fmt_map: Map of file format to path from which the file format
was added. Note that this might or might not point to an actual
existing file, as sometimes files are added as streams. In which case
it might be a dummy value or a non-existent path.
:param db: Library database
'''
pass # Default implementation does nothing
# }}}
class MetadataReaderPlugin(Plugin): # {{{
'''
A plugin that implements reading metadata from a set of file types.
'''
#: Set of file types for which this plugin should be run.
#: For example: ``set(['lit', 'mobi', 'prc'])``
file_types = set()
supported_platforms = ['windows', 'osx', 'linux']
version = numeric_version
author = 'Kovid Goyal'
type = _('Metadata reader')
def __init__(self, *args, **kwargs):
Plugin.__init__(self, *args, **kwargs)
self.quick = False
def get_metadata(self, stream, type):
'''
Return metadata for the file represented by stream (a file like object
that supports reading). Raise an exception when there is an error
with the input data.
:param type: The type of file. Guaranteed to be one of the entries
in :attr:`file_types`.
:return: A :class:`calibre.ebooks.metadata.book.Metadata` object
'''
return None
# }}}
class MetadataWriterPlugin(Plugin): # {{{
'''
A plugin that implements reading metadata from a set of file types.
'''
#: Set of file types for which this plugin should be run.
#: For example: ``set(['lit', 'mobi', 'prc'])``
file_types = set()
supported_platforms = ['windows', 'osx', 'linux']
version = numeric_version
author = 'Kovid Goyal'
type = _('Metadata writer')
def __init__(self, *args, **kwargs):
Plugin.__init__(self, *args, **kwargs)
self.apply_null = False
def set_metadata(self, stream, mi, type):
'''
Set metadata for the file represented by stream (a file like object
that supports reading). Raise an exception when there is an error
with the input data.
:param type: The type of file. Guaranteed to be one of the entries
in :attr:`file_types`.
:param mi: A :class:`calibre.ebooks.metadata.book.Metadata` object
'''
pass
# }}}
class CatalogPlugin(Plugin): # {{{
'''
A plugin that implements a catalog generator.
'''
resources_path = None
#: Output file type for which this plugin should be run.
#: For example: 'epub' or 'xml'
file_types = set()
type = _('Catalog generator')
#: CLI parser options specific to this plugin, declared as namedtuple Option:
#:
#: from collections import namedtuple
#: Option = namedtuple('Option', 'option, default, dest, help')
#: cli_options = [Option('--catalog-title', default = 'My Catalog',
#: dest = 'catalog_title', help = (_('Title of generated catalog. \nDefault:') + " '" + '%default' + "'"))]
#: cli_options parsed in calibre.db.cli.cmd_catalog:option_parser()
#:
cli_options = []
def _field_sorter(self, key):
'''
Custom fields sort after standard fields
'''
if key.startswith('#'):
return '~%s' % key[1:]
else:
return key
def search_sort_db(self, db, opts):
db.search(opts.search_text)
if opts.sort_by:
# 2nd arg = ascending
db.sort(opts.sort_by, True)
return db.get_data_as_dict(ids=opts.ids)
def get_output_fields(self, db, opts):
# Return a list of requested fields
all_std_fields = {'author_sort','authors','comments','cover','formats',
'id','isbn','library_name','ondevice','pubdate','publisher',
'rating','series_index','series','size','tags','timestamp',
'title_sort','title','uuid','languages','identifiers'}
all_custom_fields = set(db.custom_field_keys())
for field in list(all_custom_fields):
fm = db.field_metadata[field]
if fm['datatype'] == 'series':
all_custom_fields.add(field+'_index')
all_fields = all_std_fields.union(all_custom_fields)
if opts.fields != 'all':
# Make a list from opts.fields
of = [x.strip() for x in opts.fields.split(',')]
requested_fields = set(of)
# Validate requested_fields
if requested_fields - all_fields:
from calibre.library import current_library_name
invalid_fields = sorted(list(requested_fields - all_fields))
print("invalid --fields specified: %s" % ', '.join(invalid_fields))
print("available fields in '%s': %s" %
(current_library_name(), ', '.join(sorted(list(all_fields)))))
raise ValueError("unable to generate catalog with specified fields")
fields = [x for x in of if x in all_fields]
else:
fields = sorted(all_fields, key=self._field_sorter)
if not opts.connected_device['is_device_connected'] and 'ondevice' in fields:
fields.pop(int(fields.index('ondevice')))
return fields
def initialize(self):
'''
If plugin is not a built-in, copy the plugin's .ui and .py files from
the ZIP file to $TMPDIR.
Tab will be dynamically generated and added to the Catalog Options dialog in
calibre.gui2.dialogs.catalog.py:Catalog
'''
from calibre.customize.builtins import plugins as builtin_plugins
from calibre.customize.ui import config
from calibre.ptempfile import PersistentTemporaryDirectory
if not type(self) in builtin_plugins and self.name not in config['disabled_plugins']:
files_to_copy = ["%s.%s" % (self.name.lower(),ext) for ext in ["ui","py"]]
resources = zipfile.ZipFile(self.plugin_path,'r')
if self.resources_path is None:
self.resources_path = PersistentTemporaryDirectory('_plugin_resources', prefix='')
for file in files_to_copy:
try:
resources.extract(file, self.resources_path)
except:
print(" customize:__init__.initialize(): %s not found in %s" % (file, os.path.basename(self.plugin_path)))
continue
resources.close()
def run(self, path_to_output, opts, db, ids, notification=None):
'''
Run the plugin. Must be implemented in subclasses.
It should generate the catalog in the format specified
in file_types, returning the absolute path to the
generated catalog file. If an error is encountered
it should raise an Exception.
The generated catalog file should be created with the
:meth:`temporary_file` method.
:param path_to_output: Absolute path to the generated catalog file.
:param opts: A dictionary of keyword arguments
:param db: A LibraryDatabase2 object
'''
# Default implementation does nothing
raise NotImplementedError('CatalogPlugin.generate_catalog() default '
'method, should be overridden in subclass')
# }}}
class InterfaceActionBase(Plugin): # {{{
supported_platforms = ['windows', 'osx', 'linux']
author = 'Kovid Goyal'
type = _('User interface action')
can_be_disabled = False
actual_plugin = None
def __init__(self, *args, **kwargs):
Plugin.__init__(self, *args, **kwargs)
self.actual_plugin_ = None
def load_actual_plugin(self, gui):
'''
This method must return the actual interface action plugin object.
'''
ac = self.actual_plugin_
if ac is None:
mod, cls = self.actual_plugin.split(':')
ac = getattr(importlib.import_module(mod), cls)(gui,
self.site_customization)
self.actual_plugin_ = ac
return ac
# }}}
class PreferencesPlugin(Plugin): # {{{
'''
A plugin representing a widget displayed in the Preferences dialog.
This plugin has only one important method :meth:`create_widget`. The
various fields of the plugin control how it is categorized in the UI.
'''
supported_platforms = ['windows', 'osx', 'linux']
author = 'Kovid Goyal'
type = _('Preferences')
can_be_disabled = False
#: Import path to module that contains a class named ConfigWidget
#: which implements the ConfigWidgetInterface. Used by
#: :meth:`create_widget`.
config_widget = None
#: Where in the list of categories the :attr:`category` of this plugin should be.
category_order = 100
#: Where in the list of names in a category, the :attr:`gui_name` of this
#: plugin should be
name_order = 100
#: The category this plugin should be in
category = None
#: The category name displayed to the user for this plugin
gui_category = None
#: The name displayed to the user for this plugin
gui_name = None
#: The icon for this plugin, should be an absolute path
icon = None
#: The description used for tooltips and the like
description = None
def create_widget(self, parent=None):
'''
Create and return the actual Qt widget used for setting this group of
preferences. The widget must implement the
:class:`calibre.gui2.preferences.ConfigWidgetInterface`.
The default implementation uses :attr:`config_widget` to instantiate
the widget.
'''
base, _, wc = self.config_widget.partition(':')
if not wc:
wc = 'ConfigWidget'
base = importlib.import_module(base)
widget = getattr(base, wc)
return widget(parent)
# }}}
class StoreBase(Plugin): # {{{
supported_platforms = ['windows', 'osx', 'linux']
author = 'John Schember'
type = _('Store')
# Information about the store. Should be in the primary language
# of the store. This should not be translatable when set by
# a subclass.
description = _('An e-book store.')
minimum_calibre_version = (0, 8, 0)
version = (1, 0, 1)
actual_plugin = None
# Does the store only distribute e-books without DRM.
drm_free_only = False
# This is the 2 letter country code for the corporate
# headquarters of the store.
headquarters = ''
# All formats the store distributes e-books in.
formats = []
# Is this store on an affiliate program?
affiliate = False
def load_actual_plugin(self, gui):
'''
This method must return the actual interface action plugin object.
'''
mod, cls = self.actual_plugin.split(':')
self.actual_plugin_object = getattr(importlib.import_module(mod), cls)(gui, self.name)
return self.actual_plugin_object
def customization_help(self, gui=False):
if getattr(self, 'actual_plugin_object', None) is not None:
return self.actual_plugin_object.customization_help(gui)
raise NotImplementedError()
def config_widget(self):
if getattr(self, 'actual_plugin_object', None) is not None:
return self.actual_plugin_object.config_widget()
raise NotImplementedError()
def save_settings(self, config_widget):
if getattr(self, 'actual_plugin_object', None) is not None:
return self.actual_plugin_object.save_settings(config_widget)
raise NotImplementedError()
# }}}
class EditBookToolPlugin(Plugin): # {{{
type = _('Edit book tool')
minimum_calibre_version = (1, 46, 0)
# }}}
class LibraryClosedPlugin(Plugin): # {{{
'''
LibraryClosedPlugins are run when a library is closed, either at shutdown,
when the library is changed, or when a library is used in some other way.
At the moment these plugins won't be called by the CLI functions.
'''
type = _('Library closed')
# minimum version 2.54 because that is when support was added
minimum_calibre_version = (2, 54, 0)
def run(self, db):
'''
The db will be a reference to the new_api (db.cache.py).
The plugin must run to completion. It must not use the GUI, threads, or
any signals.
'''
raise NotImplementedError('LibraryClosedPlugin '
'run method must be overridden in subclass')
# }}}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,376 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals
'''
Defines the plugin system for conversions.
'''
import re, os, shutil, numbers
from calibre import CurrentDir
from calibre.customize import Plugin
from polyglot.builtins import unicode_type
class ConversionOption(object):
'''
Class representing conversion options
'''
def __init__(self, name=None, help=None, long_switch=None,
short_switch=None, choices=None):
self.name = name
self.help = help
self.long_switch = long_switch
self.short_switch = short_switch
self.choices = choices
if self.long_switch is None:
self.long_switch = self.name.replace('_', '-')
self.validate_parameters()
def validate_parameters(self):
'''
Validate the parameters passed to :meth:`__init__`.
'''
if re.match(r'[a-zA-Z_]([a-zA-Z0-9_])*', self.name) is None:
raise ValueError(self.name + ' is not a valid Python identifier')
if not self.help:
raise ValueError('You must set the help text')
def __hash__(self):
return hash(self.name)
def __eq__(self, other):
return self.name == getattr(other, 'name', other)
def clone(self):
return ConversionOption(name=self.name, help=self.help,
long_switch=self.long_switch, short_switch=self.short_switch,
choices=self.choices)
class OptionRecommendation(object):
LOW = 1
MED = 2
HIGH = 3
def __init__(self, recommended_value=None, level=LOW, **kwargs):
'''
An option recommendation. That is, an option as well as its recommended
value and the level of the recommendation.
'''
self.level = level
self.recommended_value = recommended_value
self.option = kwargs.pop('option', None)
if self.option is None:
self.option = ConversionOption(**kwargs)
self.validate_parameters()
@property
def help(self):
return self.option.help
def clone(self):
return OptionRecommendation(recommended_value=self.recommended_value,
level=self.level, option=self.option.clone())
def validate_parameters(self):
if self.option.choices and self.recommended_value not in \
self.option.choices:
raise ValueError('OpRec: %s: Recommended value not in choices'%
self.option.name)
if not (isinstance(self.recommended_value, (numbers.Number, bytes, unicode_type)) or self.recommended_value is None):
raise ValueError('OpRec: %s:'%self.option.name + repr(
self.recommended_value) + ' is not a string or a number')
class DummyReporter(object):
def __init__(self):
self.cancel_requested = False
def __call__(self, percent, msg=''):
pass
def gui_configuration_widget(name, parent, get_option_by_name,
get_option_help, db, book_id, for_output=True):
import importlib
def widget_factory(cls):
return cls(parent, get_option_by_name,
get_option_help, db, book_id)
if for_output:
try:
output_widget = importlib.import_module(
'calibre.gui2.convert.'+name)
pw = output_widget.PluginWidget
pw.ICON = I('back.png')
pw.HELP = _('Options specific to the output format.')
return widget_factory(pw)
except ImportError:
pass
else:
try:
input_widget = importlib.import_module(
'calibre.gui2.convert.'+name)
pw = input_widget.PluginWidget
pw.ICON = I('forward.png')
pw.HELP = _('Options specific to the input format.')
return widget_factory(pw)
except ImportError:
pass
return None
class InputFormatPlugin(Plugin):
'''
InputFormatPlugins are responsible for converting a document into
HTML+OPF+CSS+etc.
The results of the conversion *must* be encoded in UTF-8.
The main action happens in :meth:`convert`.
'''
type = _('Conversion input')
can_be_disabled = False
supported_platforms = ['windows', 'osx', 'linux']
commit_name = None # unique name under which options for this plugin are saved
ui_data = None
#: Set of file types for which this plugin should be run
#: For example: ``set(['azw', 'mobi', 'prc'])``
file_types = set()
#: If True, this input plugin generates a collection of images,
#: one per HTML file. This can be set dynamically, in the convert method
#: if the input files can be both image collections and non-image collections.
#: If you set this to True, you must implement the get_images() method that returns
#: a list of images.
is_image_collection = False
#: Number of CPU cores used by this plugin.
#: A value of -1 means that it uses all available cores
core_usage = 1
#: If set to True, the input plugin will perform special processing
#: to make its output suitable for viewing
for_viewer = False
#: The encoding that this input plugin creates files in. A value of
#: None means that the encoding is undefined and must be
#: detected individually
output_encoding = 'utf-8'
#: Options shared by all Input format plugins. Do not override
#: in sub-classes. Use :attr:`options` instead. Every option must be an
#: instance of :class:`OptionRecommendation`.
common_options = {
OptionRecommendation(name='input_encoding',
recommended_value=None, level=OptionRecommendation.LOW,
help=_('Specify the character encoding of the input document. If '
'set this option will override any encoding declared by the '
'document itself. Particularly useful for documents that '
'do not declare an encoding or that have erroneous '
'encoding declarations.')
)}
#: Options to customize the behavior of this plugin. Every option must be an
#: instance of :class:`OptionRecommendation`.
options = set()
#: A set of 3-tuples of the form
#: (option_name, recommended_value, recommendation_level)
recommendations = set()
def __init__(self, *args):
Plugin.__init__(self, *args)
self.report_progress = DummyReporter()
def get_images(self):
'''
Return a list of absolute paths to the images, if this input plugin
represents an image collection. The list of images is in the same order
as the spine and the TOC.
'''
raise NotImplementedError()
def convert(self, stream, options, file_ext, log, accelerators):
'''
This method must be implemented in sub-classes. It must return
the path to the created OPF file or an :class:`OEBBook` instance.
All output should be contained in the current directory.
If this plugin creates files outside the current
directory they must be deleted/marked for deletion before this method
returns.
:param stream: A file like object that contains the input file.
:param options: Options to customize the conversion process.
Guaranteed to have attributes corresponding
to all the options declared by this plugin. In
addition, it will have a verbose attribute that
takes integral values from zero upwards. Higher numbers
mean be more verbose. Another useful attribute is
``input_profile`` that is an instance of
:class:`calibre.customize.profiles.InputProfile`.
:param file_ext: The extension (without the .) of the input file. It
is guaranteed to be one of the `file_types` supported
by this plugin.
:param log: A :class:`calibre.utils.logging.Log` object. All output
should use this object.
:param accelarators: A dictionary of various information that the input
plugin can get easily that would speed up the
subsequent stages of the conversion.
'''
raise NotImplementedError()
def __call__(self, stream, options, file_ext, log,
accelerators, output_dir):
try:
log('InputFormatPlugin: %s running'%self.name)
if hasattr(stream, 'name'):
log('on', stream.name)
except:
# In case stdout is broken
pass
with CurrentDir(output_dir):
for x in os.listdir('.'):
shutil.rmtree(x) if os.path.isdir(x) else os.remove(x)
ret = self.convert(stream, options, file_ext,
log, accelerators)
return ret
def postprocess_book(self, oeb, opts, log):
'''
Called to allow the input plugin to perform postprocessing after
the book has been parsed.
'''
pass
def specialize(self, oeb, opts, log, output_fmt):
'''
Called to allow the input plugin to specialize the parsed book
for a particular output format. Called after postprocess_book
and before any transforms are performed on the parsed book.
'''
pass
def gui_configuration_widget(self, parent, get_option_by_name,
get_option_help, db, book_id=None):
'''
Called to create the widget used for configuring this plugin in the
calibre GUI. The widget must be an instance of the PluginWidget class.
See the builtin input plugins for examples.
'''
name = self.name.lower().replace(' ', '_')
return gui_configuration_widget(name, parent, get_option_by_name,
get_option_help, db, book_id, for_output=False)
class OutputFormatPlugin(Plugin):
'''
OutputFormatPlugins are responsible for converting an OEB document
(OPF+HTML) into an output e-book.
The OEB document can be assumed to be encoded in UTF-8.
The main action happens in :meth:`convert`.
'''
type = _('Conversion output')
can_be_disabled = False
supported_platforms = ['windows', 'osx', 'linux']
commit_name = None # unique name under which options for this plugin are saved
ui_data = None
#: The file type (extension without leading period) that this
#: plugin outputs
file_type = None
#: Options shared by all Input format plugins. Do not override
#: in sub-classes. Use :attr:`options` instead. Every option must be an
#: instance of :class:`OptionRecommendation`.
common_options = {
OptionRecommendation(name='pretty_print',
recommended_value=False, level=OptionRecommendation.LOW,
help=_('If specified, the output plugin will try to create output '
'that is as human readable as possible. May not have any effect '
'for some output plugins.')
)}
#: Options to customize the behavior of this plugin. Every option must be an
#: instance of :class:`OptionRecommendation`.
options = set()
#: A set of 3-tuples of the form
#: (option_name, recommended_value, recommendation_level)
recommendations = set()
@property
def description(self):
return _('Convert e-books to the %s format')%self.file_type
def __init__(self, *args):
Plugin.__init__(self, *args)
self.report_progress = DummyReporter()
def convert(self, oeb_book, output, input_plugin, opts, log):
'''
Render the contents of `oeb_book` (which is an instance of
:class:`calibre.ebooks.oeb.OEBBook`) to the file specified by output.
:param output: Either a file like object or a string. If it is a string
it is the path to a directory that may or may not exist. The output
plugin should write its output into that directory. If it is a file like
object, the output plugin should write its output into the file.
:param input_plugin: The input plugin that was used at the beginning of
the conversion pipeline.
:param opts: Conversion options. Guaranteed to have attributes
corresponding to the OptionRecommendations of this plugin.
:param log: The logger. Print debug/info messages etc. using this.
'''
raise NotImplementedError()
@property
def is_periodical(self):
return self.oeb.metadata.publication_type and \
unicode_type(self.oeb.metadata.publication_type[0]).startswith('periodical:')
def specialize_options(self, log, opts, input_fmt):
'''
Can be used to change the values of conversion options, as used by the
conversion pipeline.
'''
pass
def specialize_css_for_output(self, log, opts, item, stylizer):
'''
Can be used to make changes to the css during the CSS flattening
process.
:param item: The item (HTML file) being processed
:param stylizer: A Stylizer object containing the flattened styles for
item. You can get the style for any element by
stylizer.style(element).
'''
pass
def gui_configuration_widget(self, parent, get_option_by_name,
get_option_help, db, book_id=None):
'''
Called to create the widget used for configuring this plugin in the
calibre GUI. The widget must be an instance of the PluginWidget class.
See the builtin output plugins for examples.
'''
name = self.name.lower().replace(' ', '_')
return gui_configuration_widget(name, parent, get_option_by_name,
get_option_help, db, book_id, for_output=True)

View File

@@ -0,0 +1,873 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL 3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from calibre.customize import Plugin as _Plugin
from polyglot.builtins import zip
FONT_SIZES = [('xx-small', 1),
('x-small', None),
('small', 2),
('medium', 3),
('large', 4),
('x-large', 5),
('xx-large', 6),
(None, 7)]
class Plugin(_Plugin):
fbase = 12
fsizes = [5, 7, 9, 12, 13.5, 17, 20, 22, 24]
screen_size = (1600, 1200)
dpi = 100
def __init__(self, *args, **kwargs):
_Plugin.__init__(self, *args, **kwargs)
self.width, self.height = self.screen_size
fsizes = list(self.fsizes)
self.fkey = list(self.fsizes)
self.fsizes = []
for (name, num), size in zip(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)
self.width_pts = self.width * 72./self.dpi
self.height_pts = self.height * 72./self.dpi
# Input profiles {{{
class InputProfile(Plugin):
author = 'Kovid Goyal'
supported_platforms = {'windows', 'osx', 'linux'}
can_be_disabled = False
type = _('Input profile')
name = 'Default Input Profile'
short_name = 'default' # Used in the CLI so dont use spaces etc. in it
description = _('This profile tries to provide sane defaults and is useful '
'if you know nothing about the input document.')
class SonyReaderInput(InputProfile):
name = 'Sony Reader'
short_name = 'sony'
description = _('This profile is intended for the SONY PRS line. '
'The 500/505/600/700 etc.')
screen_size = (584, 754)
dpi = 168.451
fbase = 12
fsizes = [7.5, 9, 10, 12, 15.5, 20, 22, 24]
class SonyReader300Input(SonyReaderInput):
name = 'Sony Reader 300'
short_name = 'sony300'
description = _('This profile is intended for the SONY PRS 300.')
dpi = 200
class SonyReader900Input(SonyReaderInput):
author = 'John Schember'
name = 'Sony Reader 900'
short_name = 'sony900'
description = _('This profile is intended for the SONY PRS-900.')
screen_size = (584, 978)
class MSReaderInput(InputProfile):
name = 'Microsoft Reader'
short_name = 'msreader'
description = _('This profile is intended for the Microsoft Reader.')
screen_size = (480, 652)
dpi = 96
fbase = 13
fsizes = [10, 11, 13, 16, 18, 20, 22, 26]
class MobipocketInput(InputProfile):
name = 'Mobipocket Books'
short_name = 'mobipocket'
description = _('This profile is intended for the Mobipocket books.')
# Unfortunately MOBI books are not narrowly targeted, so this information is
# quite likely to be spurious
screen_size = (600, 800)
dpi = 96
fbase = 18
fsizes = [14, 14, 16, 18, 20, 22, 24, 26]
class HanlinV3Input(InputProfile):
name = 'Hanlin V3'
short_name = 'hanlinv3'
description = _('This profile is intended for the Hanlin V3 and its clones.')
# Screen size is a best guess
screen_size = (584, 754)
dpi = 168.451
fbase = 16
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
class HanlinV5Input(HanlinV3Input):
name = 'Hanlin V5'
short_name = 'hanlinv5'
description = _('This profile is intended for the Hanlin V5 and its clones.')
# Screen size is a best guess
screen_size = (584, 754)
dpi = 200
class CybookG3Input(InputProfile):
name = 'Cybook G3'
short_name = 'cybookg3'
description = _('This profile is intended for the Cybook G3.')
# Screen size is a best guess
screen_size = (600, 800)
dpi = 168.451
fbase = 16
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
class CybookOpusInput(InputProfile):
author = 'John Schember'
name = 'Cybook Opus'
short_name = 'cybook_opus'
description = _('This profile is intended for the Cybook Opus.')
# Screen size is a best guess
screen_size = (600, 800)
dpi = 200
fbase = 16
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
class KindleInput(InputProfile):
name = 'Kindle'
short_name = 'kindle'
description = _('This profile is intended for the Amazon Kindle.')
# Screen size is a best guess
screen_size = (525, 640)
dpi = 168.451
fbase = 16
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
class IlliadInput(InputProfile):
name = 'Illiad'
short_name = 'illiad'
description = _('This profile is intended for the Irex Illiad.')
screen_size = (760, 925)
dpi = 160.0
fbase = 12
fsizes = [7.5, 9, 10, 12, 15.5, 20, 22, 24]
class IRexDR1000Input(InputProfile):
author = 'John Schember'
name = 'IRex Digital Reader 1000'
short_name = 'irexdr1000'
description = _('This profile is intended for the IRex Digital Reader 1000.')
# Screen size is a best guess
screen_size = (1024, 1280)
dpi = 160
fbase = 16
fsizes = [12, 14, 16, 18, 20, 22, 24]
class IRexDR800Input(InputProfile):
author = 'Eric Cronin'
name = 'IRex Digital Reader 800'
short_name = 'irexdr800'
description = _('This profile is intended for the IRex Digital Reader 800.')
screen_size = (768, 1024)
dpi = 160
fbase = 16
fsizes = [12, 14, 16, 18, 20, 22, 24]
class NookInput(InputProfile):
author = 'John Schember'
name = 'Nook'
short_name = 'nook'
description = _('This profile is intended for the B&N Nook.')
# Screen size is a best guess
screen_size = (600, 800)
dpi = 167
fbase = 16
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
input_profiles = [InputProfile, SonyReaderInput, SonyReader300Input,
SonyReader900Input, MSReaderInput, MobipocketInput, HanlinV3Input,
HanlinV5Input, CybookG3Input, CybookOpusInput, KindleInput, IlliadInput,
IRexDR1000Input, IRexDR800Input, NookInput]
input_profiles.sort(key=lambda x: x.name.lower())
# }}}
class OutputProfile(Plugin):
author = 'Kovid Goyal'
supported_platforms = {'windows', 'osx', 'linux'}
can_be_disabled = False
type = _('Output profile')
name = 'Default Output Profile'
short_name = 'default' # Used in the CLI so dont use spaces etc. in it
description = _('This profile tries to provide sane defaults and is useful '
'if you want to produce a document intended to be read at a '
'computer or on a range of devices.')
#: The image size for comics
comic_screen_size = (584, 754)
#: If True the MOBI renderer on the device supports MOBI indexing
supports_mobi_indexing = False
#: If True output should be optimized for a touchscreen interface
touchscreen = False
touchscreen_news_css = ''
#: A list of extra (beyond CSS 2.1) modules supported by the device
#: Format is a css_parser profile dictionary (see iPad for example)
extra_css_modules = []
#: If True, the date is appended to the title of downloaded news
periodical_date_in_title = True
#: Characters used in jackets and catalogs
ratings_char = '*'
empty_ratings_char = ' '
#: Unsupported unicode characters to be replaced during preprocessing
unsupported_unicode_chars = []
#: Number of ems that the left margin of a blockquote is rendered as
mobi_ems_per_blockquote = 1.0
#: Special periodical formatting needed in EPUB
epub_periodical_format = None
class iPadOutput(OutputProfile):
name = 'iPad'
short_name = 'ipad'
description = _('Intended for the iPad and similar devices with a '
'resolution of 768x1024')
screen_size = (768, 1024)
comic_screen_size = (768, 1024)
dpi = 132.0
extra_css_modules = [
{
'name':'webkit',
'props': {'-webkit-border-bottom-left-radius':'{length}',
'-webkit-border-bottom-right-radius':'{length}',
'-webkit-border-top-left-radius':'{length}',
'-webkit-border-top-right-radius':'{length}',
'-webkit-border-radius': r'{border-width}(\s+{border-width}){0,3}|inherit',
},
'macros': {'border-width': '{length}|medium|thick|thin'}
}
]
ratings_char = '\u2605' # filled star
empty_ratings_char = '\u2606' # hollow star
touchscreen = True
# touchscreen_news_css {{{
touchscreen_news_css = '''
/* hr used in articles */
.article_articles_list {
width:18%;
}
.article_link {
color: #593f29;
font-style: italic;
}
.article_next {
-webkit-border-top-right-radius:4px;
-webkit-border-bottom-right-radius:4px;
font-style: italic;
width:32%;
}
.article_prev {
-webkit-border-top-left-radius:4px;
-webkit-border-bottom-left-radius:4px;
font-style: italic;
width:32%;
}
.article_sections_list {
width:18%;
}
.articles_link {
font-weight: bold;
}
.sections_link {
font-weight: bold;
}
.caption_divider {
border:#ccc 1px solid;
}
.touchscreen_navbar {
background:#c3bab2;
border:#ccc 0px solid;
border-collapse:separate;
border-spacing:1px;
margin-left: 5%;
margin-right: 5%;
page-break-inside:avoid;
width: 90%;
-webkit-border-radius:4px;
}
.touchscreen_navbar td {
background:#fff;
font-family:Helvetica;
font-size:80%;
/* UI touchboxes use 8px padding */
padding: 6px;
text-align:center;
}
.touchscreen_navbar td a:link {
color: #593f29;
text-decoration: none;
}
/* Index formatting */
.publish_date {
text-align:center;
}
.divider {
border-bottom:1em solid white;
border-top:1px solid gray;
}
hr.caption_divider {
border-color:black;
border-style:solid;
border-width:1px;
}
/* Feed summary formatting */
.article_summary {
display:inline-block;
padding-bottom:0.5em;
}
.feed {
font-family:sans-serif;
font-weight:bold;
font-size:larger;
}
.feed_link {
font-style: italic;
}
.feed_next {
-webkit-border-top-right-radius:4px;
-webkit-border-bottom-right-radius:4px;
font-style: italic;
width:40%;
}
.feed_prev {
-webkit-border-top-left-radius:4px;
-webkit-border-bottom-left-radius:4px;
font-style: italic;
width:40%;
}
.feed_title {
text-align: center;
font-size: 160%;
}
.feed_up {
font-weight: bold;
width:20%;
}
.summary_headline {
font-weight:bold;
text-align:left;
}
.summary_byline {
text-align:left;
font-family:monospace;
}
.summary_text {
text-align:left;
}
'''
# }}}
class iPad3Output(iPadOutput):
screen_size = comic_screen_size = (2048, 1536)
dpi = 264.0
name = 'iPad 3'
short_name = 'ipad3'
description = _('Intended for the iPad 3 and similar devices with a '
'resolution of 1536x2048')
class TabletOutput(iPadOutput):
name = 'Tablet'
short_name = 'tablet'
description = _('Intended for generic tablet devices, does no resizing of images')
screen_size = (10000, 10000)
comic_screen_size = (10000, 10000)
class SamsungGalaxy(TabletOutput):
name = 'Samsung Galaxy'
short_name = 'galaxy'
description = _('Intended for the Samsung Galaxy and similar tablet devices with '
'a resolution of 600x1280')
screen_size = comic_screen_size = (600, 1280)
class NookHD(TabletOutput):
name = 'Nook HD+'
short_name = 'nook_hd_plus'
description = _('Intended for the Nook HD+ and similar tablet devices with '
'a resolution of 1280x1920')
screen_size = comic_screen_size = (1280, 1920)
class SonyReaderOutput(OutputProfile):
name = 'Sony Reader'
short_name = 'sony'
description = _('This profile is intended for the SONY PRS line. '
'The 500/505/600/700 etc.')
screen_size = (590, 775)
dpi = 168.451
fbase = 12
fsizes = [7.5, 9, 10, 12, 15.5, 20, 22, 24]
unsupported_unicode_chars = [u'\u201f', u'\u201b']
epub_periodical_format = 'sony'
# periodical_date_in_title = False
class KoboReaderOutput(OutputProfile):
name = 'Kobo Reader'
short_name = 'kobo'
description = _('This profile is intended for the Kobo Reader.')
screen_size = (536, 710)
comic_screen_size = (536, 710)
dpi = 168.451
fbase = 12
fsizes = [7.5, 9, 10, 12, 15.5, 20, 22, 24]
class SonyReader300Output(SonyReaderOutput):
author = 'John Schember'
name = 'Sony Reader 300'
short_name = 'sony300'
description = _('This profile is intended for the SONY PRS-300.')
dpi = 200
class SonyReader900Output(SonyReaderOutput):
author = 'John Schember'
name = 'Sony Reader 900'
short_name = 'sony900'
description = _('This profile is intended for the SONY PRS-900.')
screen_size = (600, 999)
comic_screen_size = screen_size
class SonyReaderT3Output(SonyReaderOutput):
author = 'Kovid Goyal'
name = 'Sony Reader T3'
short_name = 'sonyt3'
description = _('This profile is intended for the SONY PRS-T3.')
screen_size = (758, 934)
comic_screen_size = screen_size
class GenericEink(SonyReaderOutput):
name = 'Generic e-ink'
short_name = 'generic_eink'
description = _('Suitable for use with any e-ink device')
epub_periodical_format = None
class GenericEinkLarge(GenericEink):
name = 'Generic e-ink large'
short_name = 'generic_eink_large'
description = _('Suitable for use with any large screen e-ink device')
screen_size = (600, 999)
comic_screen_size = screen_size
class GenericEinkHD(GenericEink):
name = 'Generic e-ink HD'
short_name = 'generic_eink_hd'
description = _('Suitable for use with any modern high resolution e-ink device')
screen_size = (10000, 10000)
comic_screen_size = (10000, 10000)
class JetBook5Output(OutputProfile):
name = 'JetBook 5-inch'
short_name = 'jetbook5'
description = _('This profile is intended for the 5-inch JetBook.')
screen_size = (480, 640)
dpi = 168.451
class SonyReaderLandscapeOutput(SonyReaderOutput):
name = 'Sony Reader Landscape'
short_name = 'sony-landscape'
description = _('This profile is intended for the SONY PRS line. '
'The 500/505/700 etc, in landscape mode. Mainly useful '
'for comics.')
screen_size = (784, 1012)
comic_screen_size = (784, 1012)
class MSReaderOutput(OutputProfile):
name = 'Microsoft Reader'
short_name = 'msreader'
description = _('This profile is intended for the Microsoft Reader.')
screen_size = (480, 652)
dpi = 96
fbase = 13
fsizes = [10, 11, 13, 16, 18, 20, 22, 26]
class MobipocketOutput(OutputProfile):
name = 'Mobipocket Books'
short_name = 'mobipocket'
description = _('This profile is intended for the Mobipocket books.')
# Unfortunately MOBI books are not narrowly targeted, so this information is
# quite likely to be spurious
screen_size = (600, 800)
dpi = 96
fbase = 18
fsizes = [14, 14, 16, 18, 20, 22, 24, 26]
class HanlinV3Output(OutputProfile):
name = 'Hanlin V3'
short_name = 'hanlinv3'
description = _('This profile is intended for the Hanlin V3 and its clones.')
# Screen size is a best guess
screen_size = (584, 754)
dpi = 168.451
fbase = 16
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
class HanlinV5Output(HanlinV3Output):
name = 'Hanlin V5'
short_name = 'hanlinv5'
description = _('This profile is intended for the Hanlin V5 and its clones.')
dpi = 200
class CybookG3Output(OutputProfile):
name = 'Cybook G3'
short_name = 'cybookg3'
description = _('This profile is intended for the Cybook G3.')
# Screen size is a best guess
screen_size = (600, 800)
comic_screen_size = (600, 757)
dpi = 168.451
fbase = 16
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
class CybookOpusOutput(SonyReaderOutput):
author = 'John Schember'
name = 'Cybook Opus'
short_name = 'cybook_opus'
description = _('This profile is intended for the Cybook Opus.')
# Screen size is a best guess
dpi = 200
fbase = 16
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
epub_periodical_format = None
class KindleOutput(OutputProfile):
name = 'Kindle'
short_name = 'kindle'
description = _('This profile is intended for the Amazon Kindle.')
# Screen size is a best guess
screen_size = (525, 640)
dpi = 168.451
fbase = 16
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
supports_mobi_indexing = True
periodical_date_in_title = False
empty_ratings_char = '\u2606'
ratings_char = '\u2605'
mobi_ems_per_blockquote = 2.0
class KindleDXOutput(OutputProfile):
name = 'Kindle DX'
short_name = 'kindle_dx'
description = _('This profile is intended for the Amazon Kindle DX.')
# Screen size is a best guess
screen_size = (744, 1022)
dpi = 150.0
comic_screen_size = (771, 1116)
# comic_screen_size = (741, 1022)
supports_mobi_indexing = True
periodical_date_in_title = False
empty_ratings_char = '\u2606'
ratings_char = '\u2605'
mobi_ems_per_blockquote = 2.0
class KindlePaperWhiteOutput(KindleOutput):
name = 'Kindle PaperWhite'
short_name = 'kindle_pw'
description = _('This profile is intended for the Amazon Kindle PaperWhite 1 and 2')
# Screen size is a best guess
screen_size = (658, 940)
dpi = 212.0
comic_screen_size = screen_size
class KindleVoyageOutput(KindleOutput):
name = 'Kindle Voyage'
short_name = 'kindle_voyage'
description = _('This profile is intended for the Amazon Kindle Voyage')
# Screen size is currently just the spec size, actual renderable area will
# depend on someone with the device doing tests.
screen_size = (1080, 1430)
dpi = 300.0
comic_screen_size = screen_size
class KindlePaperWhite3Output(KindleVoyageOutput):
name = 'Kindle PaperWhite 3'
short_name = 'kindle_pw3'
description = _('This profile is intended for the Amazon Kindle PaperWhite 3 and above')
# Screen size is currently just the spec size, actual renderable area will
# depend on someone with the device doing tests.
screen_size = (1072, 1430)
dpi = 300.0
comic_screen_size = screen_size
class KindleOasisOutput(KindlePaperWhite3Output):
name = 'Kindle Oasis'
short_name = 'kindle_oasis'
description = _('This profile is intended for the Amazon Kindle Oasis 2017 and above')
# Screen size is currently just the spec size, actual renderable area will
# depend on someone with the device doing tests.
screen_size = (1264, 1680)
dpi = 300.0
comic_screen_size = screen_size
class KindleFireOutput(KindleDXOutput):
name = 'Kindle Fire'
short_name = 'kindle_fire'
description = _('This profile is intended for the Amazon Kindle Fire.')
screen_size = (570, 1016)
dpi = 169.0
comic_screen_size = (570, 1016)
class IlliadOutput(OutputProfile):
name = 'Illiad'
short_name = 'illiad'
description = _('This profile is intended for the Irex Illiad.')
screen_size = (760, 925)
comic_screen_size = (760, 925)
dpi = 160.0
fbase = 12
fsizes = [7.5, 9, 10, 12, 15.5, 20, 22, 24]
class IRexDR1000Output(OutputProfile):
author = 'John Schember'
name = 'IRex Digital Reader 1000'
short_name = 'irexdr1000'
description = _('This profile is intended for the IRex Digital Reader 1000.')
# Screen size is a best guess
screen_size = (1024, 1280)
comic_screen_size = (996, 1241)
dpi = 160
fbase = 16
fsizes = [12, 14, 16, 18, 20, 22, 24]
class IRexDR800Output(OutputProfile):
author = 'Eric Cronin'
name = 'IRex Digital Reader 800'
short_name = 'irexdr800'
description = _('This profile is intended for the IRex Digital Reader 800.')
# Screen size is a best guess
screen_size = (768, 1024)
comic_screen_size = (768, 1024)
dpi = 160
fbase = 16
fsizes = [12, 14, 16, 18, 20, 22, 24]
class NookOutput(OutputProfile):
author = 'John Schember'
name = 'Nook'
short_name = 'nook'
description = _('This profile is intended for the B&N Nook.')
# Screen size is a best guess
screen_size = (600, 730)
comic_screen_size = (584, 730)
dpi = 167
fbase = 16
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
class NookColorOutput(NookOutput):
name = 'Nook Color'
short_name = 'nook_color'
description = _('This profile is intended for the B&N Nook Color.')
screen_size = (600, 900)
comic_screen_size = (594, 900)
dpi = 169
class PocketBook900Output(OutputProfile):
author = 'Chris Lockfort'
name = 'PocketBook Pro 900'
short_name = 'pocketbook_900'
description = _('This profile is intended for the PocketBook Pro 900 series of devices.')
screen_size = (810, 1180)
dpi = 150.0
comic_screen_size = screen_size
class PocketBookPro912Output(OutputProfile):
author = 'Daniele Pizzolli'
name = 'PocketBook Pro 912'
short_name = 'pocketbook_pro_912'
description = _('This profile is intended for the PocketBook Pro 912 series of devices.')
# According to http://download.pocketbook-int.com/user-guides/E_Ink/912/User_Guide_PocketBook_912(EN).pdf
screen_size = (825, 1200)
dpi = 155.0
comic_screen_size = screen_size
output_profiles = [
OutputProfile, SonyReaderOutput, SonyReader300Output, SonyReader900Output,
SonyReaderT3Output, MSReaderOutput, MobipocketOutput, HanlinV3Output,
HanlinV5Output, CybookG3Output, CybookOpusOutput, KindleOutput, iPadOutput,
iPad3Output, KoboReaderOutput, TabletOutput, SamsungGalaxy,
SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput, NookHD,
IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput,
NookColorOutput, PocketBook900Output,
PocketBookPro912Output, GenericEink, GenericEinkLarge, GenericEinkHD,
KindleFireOutput, KindlePaperWhiteOutput, KindleVoyageOutput,
KindlePaperWhite3Output, KindleOasisOutput
]
output_profiles.sort(key=lambda x: x.name.lower())

View File

@@ -0,0 +1,835 @@
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, shutil, traceback, functools, sys
from collections import defaultdict
from itertools import chain
from calibre.customize import (CatalogPlugin, FileTypePlugin, PluginNotFound,
MetadataReaderPlugin, MetadataWriterPlugin,
InterfaceActionBase as InterfaceAction,
PreferencesPlugin, platform, InvalidPlugin,
StoreBase as Store, EditBookToolPlugin,
LibraryClosedPlugin)
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
from calibre.customize.zipplugin import loader
from calibre.customize.profiles import InputProfile, OutputProfile
from calibre.customize.builtins import plugins as builtin_plugins
from calibre.devices.interface import DevicePlugin
from calibre.ebooks.metadata import MetaInformation
from calibre.utils.config import (make_config_dir, Config, ConfigProxy,
plugin_dir, OptionParser)
from calibre.ebooks.metadata.sources.base import Source
from calibre.constants import DEBUG, numeric_version
from polyglot.builtins import iteritems, itervalues, unicode_type
builtin_names = frozenset(p.name for p in builtin_plugins)
BLACKLISTED_PLUGINS = frozenset({'Marvin XD', 'iOS reader applications'})
class NameConflict(ValueError):
pass
def _config():
c = Config('customize')
c.add_opt('plugins', default={}, help=_('Installed plugins'))
c.add_opt('filetype_mapping', default={}, help=_('Mapping for filetype plugins'))
c.add_opt('plugin_customization', default={}, help=_('Local plugin customization'))
c.add_opt('disabled_plugins', default=set(), help=_('Disabled plugins'))
c.add_opt('enabled_plugins', default=set(), help=_('Enabled plugins'))
return ConfigProxy(c)
config = _config()
def find_plugin(name):
for plugin in _initialized_plugins:
if plugin.name == name:
return plugin
def load_plugin(path_to_zip_file): # {{{
'''
Load plugin from ZIP file or raise InvalidPlugin error
:return: A :class:`Plugin` instance.
'''
return loader.load(path_to_zip_file)
# }}}
# Enable/disable plugins {{{
def disable_plugin(plugin_or_name):
x = getattr(plugin_or_name, 'name', plugin_or_name)
plugin = find_plugin(x)
if not plugin.can_be_disabled:
raise ValueError('Plugin %s cannot be disabled'%x)
dp = config['disabled_plugins']
dp.add(x)
config['disabled_plugins'] = dp
ep = config['enabled_plugins']
if x in ep:
ep.remove(x)
config['enabled_plugins'] = ep
def enable_plugin(plugin_or_name):
x = getattr(plugin_or_name, 'name', plugin_or_name)
dp = config['disabled_plugins']
if x in dp:
dp.remove(x)
config['disabled_plugins'] = dp
ep = config['enabled_plugins']
ep.add(x)
config['enabled_plugins'] = ep
def restore_plugin_state_to_default(plugin_or_name):
x = getattr(plugin_or_name, 'name', plugin_or_name)
dp = config['disabled_plugins']
if x in dp:
dp.remove(x)
config['disabled_plugins'] = dp
ep = config['enabled_plugins']
if x in ep:
ep.remove(x)
config['enabled_plugins'] = ep
default_disabled_plugins = {
'Overdrive', 'Douban Books', 'OZON.ru', 'Edelweiss', 'Google Images', 'Big Book Search',
}
def is_disabled(plugin):
if plugin.name in config['enabled_plugins']:
return False
return plugin.name in config['disabled_plugins'] or \
plugin.name in default_disabled_plugins
# }}}
# File type plugins {{{
_on_import = {}
_on_postimport = {}
_on_preprocess = {}
_on_postprocess = {}
_on_postadd = []
def reread_filetype_plugins():
global _on_import, _on_postimport, _on_preprocess, _on_postprocess, _on_postadd
_on_import = defaultdict(list)
_on_postimport = defaultdict(list)
_on_preprocess = defaultdict(list)
_on_postprocess = defaultdict(list)
_on_postadd = []
for plugin in _initialized_plugins:
if isinstance(plugin, FileTypePlugin):
for ft in plugin.file_types:
if plugin.on_import:
_on_import[ft].append(plugin)
if plugin.on_postimport:
_on_postimport[ft].append(plugin)
_on_postadd.append(plugin)
if plugin.on_preprocess:
_on_preprocess[ft].append(plugin)
if plugin.on_postprocess:
_on_postprocess[ft].append(plugin)
def plugins_for_ft(ft, occasion):
op = {
'import':_on_import, 'preprocess':_on_preprocess, 'postprocess':_on_postprocess, 'postimport':_on_postimport,
}[occasion]
for p in chain(op.get(ft, ()), op.get('*', ())):
if not is_disabled(p):
yield p
def _run_filetype_plugins(path_to_file, ft=None, occasion='preprocess'):
customization = config['plugin_customization']
if ft is None:
ft = os.path.splitext(path_to_file)[-1].lower().replace('.', '')
nfp = path_to_file
for plugin in plugins_for_ft(ft, occasion):
plugin.site_customization = customization.get(plugin.name, '')
oo, oe = sys.stdout, sys.stderr # Some file type plugins out there override the output streams with buggy implementations
with plugin:
try:
plugin.original_path_to_file = path_to_file
except Exception:
pass
try:
nfp = plugin.run(nfp) or nfp
except:
print('Running file type plugin %s failed with traceback:'%plugin.name, file=oe)
traceback.print_exc(file=oe)
sys.stdout, sys.stderr = oo, oe
x = lambda j: os.path.normpath(os.path.normcase(j))
if occasion == 'postprocess' and x(nfp) != x(path_to_file):
shutil.copyfile(nfp, path_to_file)
nfp = path_to_file
return nfp
run_plugins_on_import = functools.partial(_run_filetype_plugins, occasion='import')
run_plugins_on_preprocess = functools.partial(_run_filetype_plugins, occasion='preprocess')
run_plugins_on_postprocess = functools.partial(_run_filetype_plugins, occasion='postprocess')
def run_plugins_on_postimport(db, book_id, fmt):
customization = config['plugin_customization']
fmt = fmt.lower()
for plugin in plugins_for_ft(fmt, 'postimport'):
plugin.site_customization = customization.get(plugin.name, '')
with plugin:
try:
plugin.postimport(book_id, fmt, db)
except:
print('Running file type plugin %s failed with traceback:'%
plugin.name)
traceback.print_exc()
def run_plugins_on_postadd(db, book_id, fmt_map):
customization = config['plugin_customization']
for plugin in _on_postadd:
if is_disabled(plugin):
continue
plugin.site_customization = customization.get(plugin.name, '')
with plugin:
try:
plugin.postadd(book_id, fmt_map, db)
except Exception:
print('Running file type plugin %s failed with traceback:'%
plugin.name)
traceback.print_exc()
# }}}
# Plugin customization {{{
def customize_plugin(plugin, custom):
d = config['plugin_customization']
d[plugin.name] = custom.strip()
config['plugin_customization'] = d
def plugin_customization(plugin):
return config['plugin_customization'].get(plugin.name, '')
# }}}
# Input/Output profiles {{{
def input_profiles():
for plugin in _initialized_plugins:
if isinstance(plugin, InputProfile):
yield plugin
def output_profiles():
for plugin in _initialized_plugins:
if isinstance(plugin, OutputProfile):
yield plugin
# }}}
# Interface Actions # {{{
def interface_actions():
customization = config['plugin_customization']
for plugin in _initialized_plugins:
if isinstance(plugin, InterfaceAction):
if not is_disabled(plugin):
plugin.site_customization = customization.get(plugin.name, '')
yield plugin
# }}}
# Preferences Plugins # {{{
def preferences_plugins():
customization = config['plugin_customization']
for plugin in _initialized_plugins:
if isinstance(plugin, PreferencesPlugin):
if not is_disabled(plugin):
plugin.site_customization = customization.get(plugin.name, '')
yield plugin
# }}}
# Library Closed Plugins # {{{
def available_library_closed_plugins():
customization = config['plugin_customization']
for plugin in _initialized_plugins:
if isinstance(plugin, LibraryClosedPlugin):
if not is_disabled(plugin):
plugin.site_customization = customization.get(plugin.name, '')
yield plugin
def has_library_closed_plugins():
for plugin in _initialized_plugins:
if isinstance(plugin, LibraryClosedPlugin):
if not is_disabled(plugin):
return True
return False
# }}}
# Store Plugins # {{{
def store_plugins():
customization = config['plugin_customization']
for plugin in _initialized_plugins:
if isinstance(plugin, Store):
plugin.site_customization = customization.get(plugin.name, '')
yield plugin
def available_store_plugins():
for plugin in store_plugins():
if not is_disabled(plugin):
yield plugin
def stores():
stores = set()
for plugin in store_plugins():
stores.add(plugin.name)
return stores
def available_stores():
stores = set()
for plugin in available_store_plugins():
stores.add(plugin.name)
return stores
# }}}
# Metadata read/write {{{
_metadata_readers = {}
_metadata_writers = {}
def reread_metadata_plugins():
global _metadata_readers
global _metadata_writers
_metadata_readers = defaultdict(list)
_metadata_writers = defaultdict(list)
for plugin in _initialized_plugins:
if isinstance(plugin, MetadataReaderPlugin):
for ft in plugin.file_types:
_metadata_readers[ft].append(plugin)
elif isinstance(plugin, MetadataWriterPlugin):
for ft in plugin.file_types:
_metadata_writers[ft].append(plugin)
# Ensure custom metadata plugins are used in preference to builtin
# ones for a given filetype
def key(plugin):
return (1 if plugin.plugin_path is None else 0), plugin.name
for group in (_metadata_readers, _metadata_writers):
for plugins in itervalues(group):
if len(plugins) > 1:
plugins.sort(key=key)
def metadata_readers():
ans = set()
for plugins in _metadata_readers.values():
for plugin in plugins:
ans.add(plugin)
return ans
def metadata_writers():
ans = set()
for plugins in _metadata_writers.values():
for plugin in plugins:
ans.add(plugin)
return ans
class QuickMetadata(object):
def __init__(self):
self.quick = False
def __enter__(self):
self.quick = True
def __exit__(self, *args):
self.quick = False
quick_metadata = QuickMetadata()
class ApplyNullMetadata(object):
def __init__(self):
self.apply_null = False
def __enter__(self):
self.apply_null = True
def __exit__(self, *args):
self.apply_null = False
apply_null_metadata = ApplyNullMetadata()
class ForceIdentifiers(object):
def __init__(self):
self.force_identifiers = False
def __enter__(self):
self.force_identifiers = True
def __exit__(self, *args):
self.force_identifiers = False
force_identifiers = ForceIdentifiers()
def get_file_type_metadata(stream, ftype):
mi = MetaInformation(None, None)
ftype = ftype.lower().strip()
if ftype in _metadata_readers:
for plugin in _metadata_readers[ftype]:
if not is_disabled(plugin):
with plugin:
try:
plugin.quick = quick_metadata.quick
if hasattr(stream, 'seek'):
stream.seek(0)
mi = plugin.get_metadata(stream, ftype.lower().strip())
break
except:
traceback.print_exc()
continue
return mi
def set_file_type_metadata(stream, mi, ftype, report_error=None):
ftype = ftype.lower().strip()
if ftype in _metadata_writers:
customization = config['plugin_customization']
for plugin in _metadata_writers[ftype]:
if not is_disabled(plugin):
with plugin:
try:
plugin.apply_null = apply_null_metadata.apply_null
plugin.force_identifiers = force_identifiers.force_identifiers
plugin.site_customization = customization.get(plugin.name, '')
plugin.set_metadata(stream, mi, ftype.lower().strip())
break
except:
if report_error is None:
from calibre import prints
prints('Failed to set metadata for the', ftype.upper(), 'format of:', getattr(mi, 'title', ''), file=sys.stderr)
traceback.print_exc()
else:
report_error(mi, ftype, traceback.format_exc())
def can_set_metadata(ftype):
ftype = ftype.lower().strip()
for plugin in _metadata_writers.get(ftype, ()):
if not is_disabled(plugin):
return True
return False
# }}}
# Add/remove plugins {{{
def add_plugin(path_to_zip_file):
make_config_dir()
plugin = load_plugin(path_to_zip_file)
if plugin.name in builtin_names:
raise NameConflict(
'A builtin plugin with the name %r already exists' % plugin.name)
plugin = initialize_plugin(plugin, path_to_zip_file)
plugins = config['plugins']
zfp = os.path.join(plugin_dir, plugin.name+'.zip')
if os.path.exists(zfp):
os.remove(zfp)
shutil.copyfile(path_to_zip_file, zfp)
plugins[plugin.name] = zfp
config['plugins'] = plugins
initialize_plugins()
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:
removed = True
try:
zfp = os.path.join(plugin_dir, name+'.zip')
if os.path.exists(zfp):
os.remove(zfp)
zfp = plugins[name]
if os.path.exists(zfp):
os.remove(zfp)
except:
pass
plugins.pop(name)
config['plugins'] = plugins
initialize_plugins()
return removed
# }}}
# Input/Output format plugins {{{
def input_format_plugins():
for plugin in _initialized_plugins:
if isinstance(plugin, InputFormatPlugin):
yield plugin
def plugin_for_input_format(fmt):
customization = config['plugin_customization']
for plugin in input_format_plugins():
if fmt.lower() in plugin.file_types:
plugin.site_customization = customization.get(plugin.name, None)
return plugin
def all_input_formats():
formats = set()
for plugin in input_format_plugins():
for format in plugin.file_types:
formats.add(format)
return formats
def available_input_formats():
formats = set()
for plugin in input_format_plugins():
if not is_disabled(plugin):
for format in plugin.file_types:
formats.add(format)
formats.add('zip'), formats.add('rar')
return formats
def output_format_plugins():
for plugin in _initialized_plugins:
if isinstance(plugin, OutputFormatPlugin):
yield plugin
def plugin_for_output_format(fmt):
customization = config['plugin_customization']
for plugin in output_format_plugins():
if fmt.lower() == plugin.file_type:
plugin.site_customization = customization.get(plugin.name, None)
return plugin
def available_output_formats():
formats = set()
for plugin in output_format_plugins():
if not is_disabled(plugin):
formats.add(plugin.file_type)
return formats
# }}}
# Catalog plugins {{{
def catalog_plugins():
for plugin in _initialized_plugins:
if isinstance(plugin, CatalogPlugin):
yield plugin
def available_catalog_formats():
formats = set()
for plugin in catalog_plugins():
if not is_disabled(plugin):
for format in plugin.file_types:
formats.add(format)
return formats
def plugin_for_catalog_format(fmt):
for plugin in catalog_plugins():
if fmt.lower() in plugin.file_types:
return plugin
# }}}
# Device plugins {{{
def device_plugins(include_disabled=False):
for plugin in _initialized_plugins:
if isinstance(plugin, DevicePlugin):
if include_disabled or not is_disabled(plugin):
if platform in plugin.supported_platforms:
if getattr(plugin, 'plugin_needs_delayed_initialization',
False):
plugin.do_delayed_plugin_initialization()
yield plugin
def disabled_device_plugins():
for plugin in _initialized_plugins:
if isinstance(plugin, DevicePlugin):
if is_disabled(plugin):
if platform in plugin.supported_platforms:
yield plugin
# }}}
# Metadata sources2 {{{
def metadata_plugins(capabilities):
capabilities = frozenset(capabilities)
for plugin in all_metadata_plugins():
if plugin.capabilities.intersection(capabilities) and \
not is_disabled(plugin):
yield plugin
def all_metadata_plugins():
for plugin in _initialized_plugins:
if isinstance(plugin, Source):
yield plugin
def patch_metadata_plugins(possibly_updated_plugins):
patches = {}
for i, plugin in enumerate(_initialized_plugins):
if isinstance(plugin, Source) and plugin.name in builtin_names:
pup = possibly_updated_plugins.get(plugin.name)
if pup is not None:
if pup.version > plugin.version and pup.minimum_calibre_version <= numeric_version:
patches[i] = pup(None)
# Metadata source plugins dont use initialize() but that
# might change in the future, so be safe.
patches[i].initialize()
for i, pup in iteritems(patches):
_initialized_plugins[i] = pup
# }}}
# Editor plugins {{{
def all_edit_book_tool_plugins():
for plugin in _initialized_plugins:
if isinstance(plugin, EditBookToolPlugin):
yield plugin
# }}}
# Initialize plugins {{{
_initialized_plugins = []
def initialize_plugin(plugin, path_to_zip_file):
try:
p = plugin(path_to_zip_file)
p.initialize()
return p
except Exception:
print('Failed to initialize plugin:', plugin.name, plugin.version)
tb = traceback.format_exc()
raise InvalidPlugin((_('Initialization of plugin %s failed with traceback:')
%tb) + '\n'+tb)
def has_external_plugins():
'True if there are updateable (ZIP file based) plugins'
return bool(config['plugins'])
def initialize_plugins(perf=False):
global _initialized_plugins
_initialized_plugins = []
conflicts = [name for name in config['plugins'] if name in
builtin_names]
for p in conflicts:
remove_plugin(p)
external_plugins = config['plugins'].copy()
for name in BLACKLISTED_PLUGINS:
external_plugins.pop(name, None)
ostdout, ostderr = sys.stdout, sys.stderr
if perf:
from collections import defaultdict
import time
times = defaultdict(lambda:0)
for zfp in list(external_plugins) + builtin_plugins:
try:
if not isinstance(zfp, type):
# We have a plugin name
pname = zfp
zfp = os.path.join(plugin_dir, zfp+'.zip')
if not os.path.exists(zfp):
zfp = external_plugins[pname]
try:
plugin = load_plugin(zfp) if not isinstance(zfp, type) else zfp
except PluginNotFound:
continue
if perf:
st = time.time()
plugin = initialize_plugin(plugin, None if isinstance(zfp, type) else zfp)
if perf:
times[plugin.name] = time.time() - st
_initialized_plugins.append(plugin)
except:
print('Failed to initialize plugin:', repr(zfp))
if DEBUG:
traceback.print_exc()
# Prevent a custom plugin from overriding stdout/stderr as this breaks
# ipython
sys.stdout, sys.stderr = ostdout, ostderr
if perf:
for x in sorted(times, key=lambda x: times[x]):
print('%50s: %.3f'%(x, times[x]))
_initialized_plugins.sort(key=lambda x: x.priority, reverse=True)
reread_filetype_plugins()
reread_metadata_plugins()
initialize_plugins()
def initialized_plugins():
for plugin in _initialized_plugins:
yield plugin
# }}}
# CLI {{{
def build_plugin(path):
from calibre import prints
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.zipfile import ZipFile, ZIP_STORED
path = unicode_type(path)
names = frozenset(os.listdir(path))
if '__init__.py' not in names:
prints(path, ' is not a valid plugin')
raise SystemExit(1)
t = PersistentTemporaryFile(u'.zip')
with ZipFile(t, 'w', ZIP_STORED) as zf:
zf.add_dir(path, simple_filter=lambda x:x in {'.git', '.bzr', '.svn', '.hg'})
t.close()
plugin = add_plugin(t.name)
os.remove(t.name)
prints('Plugin updated:', plugin.name, plugin.version)
def option_parser():
parser = OptionParser(usage=_('''\
%prog options
Customize calibre by loading external plugins.
'''))
parser.add_option('-a', '--add-plugin', default=None,
help=_('Add a plugin by specifying the path to the ZIP file containing it.'))
parser.add_option('-b', '--build-plugin', default=None,
help=_('For plugin developers: Path to the directory where you are'
' developing the plugin. This command will automatically zip '
'up the plugin and update it in calibre.'))
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,
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',
help=_('List all installed plugins'))
parser.add_option('--enable-plugin', default=None,
help=_('Enable the named plugin'))
parser.add_option('--disable-plugin', default=None,
help=_('Disable the named plugin'))
return parser
def main(args=sys.argv):
parser = option_parser()
if len(args) < 2:
parser.print_help()
return 1
opts, args = parser.parse_args(args)
if opts.add_plugin is not None:
plugin = add_plugin(opts.add_plugin)
print('Plugin added:', plugin.name, plugin.version)
if opts.build_plugin is not None:
build_plugin(opts.build_plugin)
if opts.remove_plugin is not None:
if remove_plugin(opts.remove_plugin):
print('Plugin removed')
else:
print('No custom plugin named', opts.remove_plugin)
if opts.customize_plugin is not None:
name, custom = opts.customize_plugin.split(',')
plugin = find_plugin(name.strip())
if plugin is None:
print('No plugin with the name %s exists'%name)
return 1
customize_plugin(plugin, custom)
if opts.enable_plugin is not None:
enable_plugin(opts.enable_plugin.strip())
if opts.disable_plugin is not None:
disable_plugin(opts.disable_plugin.strip())
if opts.list_plugins:
type_len = name_len = 0
for plugin in initialized_plugins():
type_len, name_len = max(type_len, len(plugin.type)), max(name_len, len(plugin.name))
fmt = '%-{}s%-{}s%-15s%-15s%s'.format(type_len+1, name_len+1)
print(fmt%tuple(('Type|Name|Version|Disabled|Site Customization'.split('|'))))
print()
for plugin in initialized_plugins():
print(fmt%(
plugin.type, plugin.name,
plugin.version, is_disabled(plugin),
plugin_customization(plugin)
))
print('\t', plugin.description)
if plugin.is_customizable():
try:
print('\t', plugin.customization_help())
except NotImplementedError:
pass
print()
return 0
if __name__ == '__main__':
sys.exit(main())
# }}}

View File

@@ -0,0 +1,320 @@
#!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os, zipfile, posixpath, importlib, threading, re, imp, sys
from collections import OrderedDict
from functools import partial
from calibre import as_unicode
from calibre.constants import ispy3
from calibre.customize import (Plugin, numeric_version, platform,
InvalidPlugin, PluginNotFound)
from polyglot.builtins import (itervalues, map, string_or_bytes,
unicode_type, reload)
# PEP 302 based plugin loading mechanism, works around the bug in zipimport in
# python 2.x that prevents importing from zip files in locations whose paths
# have non ASCII characters
def get_resources(zfp, name_or_list_of_names):
'''
Load resources from the plugin zip file
:param name_or_list_of_names: List of paths to resources in the zip file using / as
separator, or a single path
:return: A dictionary of the form ``{name : file_contents}``. Any names
that were not found in the zip file will not be present in the
dictionary. If a single path is passed in the return value will
be just the bytes of the resource or None if it wasn't found.
'''
names = name_or_list_of_names
if isinstance(names, string_or_bytes):
names = [names]
ans = {}
with zipfile.ZipFile(zfp) as zf:
for name in names:
try:
ans[name] = zf.read(name)
except:
import traceback
traceback.print_exc()
if len(names) == 1:
ans = ans.pop(names[0], None)
return ans
def get_icons(zfp, name_or_list_of_names):
'''
Load icons from the plugin zip file
:param name_or_list_of_names: List of paths to resources in the zip file using / as
separator, or a single path
:return: A dictionary of the form ``{name : QIcon}``. Any names
that were not found in the zip file will be null QIcons.
If a single path is passed in the return value will
be A QIcon.
'''
from PyQt5.Qt import QIcon, QPixmap
names = name_or_list_of_names
ans = get_resources(zfp, names)
if isinstance(names, string_or_bytes):
names = [names]
if ans is None:
ans = {}
if isinstance(ans, string_or_bytes):
ans = dict([(names[0], ans)])
ians = {}
for name in names:
p = QPixmap()
raw = ans.get(name, None)
if raw:
p.loadFromData(raw)
ians[name] = QIcon(p)
if len(names) == 1:
ians = ians.pop(names[0])
return ians
_translations_cache = {}
def load_translations(namespace, zfp):
null = object()
trans = _translations_cache.get(zfp, null)
if trans is None:
return
if trans is null:
from calibre.utils.localization import get_lang
lang = get_lang()
if not lang or lang == 'en': # performance optimization
_translations_cache[zfp] = None
return
with zipfile.ZipFile(zfp) as zf:
try:
mo = zf.read('translations/%s.mo' % lang)
except KeyError:
mo = None # No translations for this language present
if mo is None:
_translations_cache[zfp] = None
return
from gettext import GNUTranslations
from io import BytesIO
trans = _translations_cache[zfp] = GNUTranslations(BytesIO(mo))
namespace['_'] = getattr(trans, 'gettext' if ispy3 else 'ugettext')
namespace['ngettext'] = getattr(trans, 'ngettext' if ispy3 else 'ungettext')
class PluginLoader(object):
def __init__(self):
self.loaded_plugins = {}
self._lock = threading.RLock()
self._identifier_pat = re.compile(r'[a-zA-Z][_0-9a-zA-Z]*')
def _get_actual_fullname(self, fullname):
parts = fullname.split('.')
if parts[0] == 'calibre_plugins':
if len(parts) == 1:
return parts[0], None
plugin_name = parts[1]
with self._lock:
names = self.loaded_plugins.get(plugin_name, None)
if names is None:
raise ImportError('No plugin named %r loaded'%plugin_name)
names = names[1]
fullname = '.'.join(parts[2:])
if not fullname:
fullname = '__init__'
if fullname in names:
return fullname, plugin_name
if fullname+'.__init__' in names:
return fullname+'.__init__', plugin_name
return None, None
def find_module(self, fullname, path=None):
fullname, plugin_name = self._get_actual_fullname(fullname)
if fullname is None and plugin_name is None:
return None
return self
def load_module(self, fullname):
import_name, plugin_name = self._get_actual_fullname(fullname)
if import_name is None and plugin_name is None:
raise ImportError('No plugin named %r is loaded'%fullname)
mod = sys.modules.setdefault(fullname, imp.new_module(fullname))
mod.__file__ = "<calibre Plugin Loader>"
mod.__loader__ = self
if import_name.endswith('.__init__') or import_name in ('__init__',
'calibre_plugins'):
# We have a package
mod.__path__ = []
if plugin_name is not None:
# We have some actual code to load
with self._lock:
zfp, names = self.loaded_plugins.get(plugin_name, (None, None))
if names is None:
raise ImportError('No plugin named %r loaded'%plugin_name)
zinfo = names.get(import_name, None)
if zinfo is None:
raise ImportError('Plugin %r has no module named %r' %
(plugin_name, import_name))
with zipfile.ZipFile(zfp) as zf:
try:
code = zf.read(zinfo)
except:
# Maybe the zip file changed from under us
code = zf.read(zinfo.filename)
compiled = compile(code, 'calibre_plugins.%s.%s'%(plugin_name,
import_name), 'exec', dont_inherit=True)
mod.__dict__['get_resources'] = partial(get_resources, zfp)
mod.__dict__['get_icons'] = partial(get_icons, zfp)
mod.__dict__['load_translations'] = partial(load_translations, mod.__dict__, zfp)
exec(compiled, mod.__dict__)
return mod
def load(self, path_to_zip_file):
if not os.access(path_to_zip_file, os.R_OK):
raise PluginNotFound('Cannot access %r'%path_to_zip_file)
with zipfile.ZipFile(path_to_zip_file) as zf:
plugin_name = self._locate_code(zf, path_to_zip_file)
try:
ans = None
plugin_module = 'calibre_plugins.%s'%plugin_name
m = sys.modules.get(plugin_module, None)
if m is not None:
reload(m)
else:
m = importlib.import_module(plugin_module)
plugin_classes = []
for obj in itervalues(m.__dict__):
if isinstance(obj, type) and issubclass(obj, Plugin) and \
obj.name != 'Trivial Plugin':
plugin_classes.append(obj)
if not plugin_classes:
raise InvalidPlugin('No plugin class found in %s:%s'%(
as_unicode(path_to_zip_file), plugin_name))
if len(plugin_classes) > 1:
plugin_classes.sort(key=lambda c:(getattr(c, '__module__', None) or '').count('.'))
ans = plugin_classes[0]
if ans.minimum_calibre_version > numeric_version:
raise InvalidPlugin(
'The plugin at %s needs a version of calibre >= %s' %
(as_unicode(path_to_zip_file), '.'.join(map(unicode_type,
ans.minimum_calibre_version))))
if platform not in ans.supported_platforms:
raise InvalidPlugin(
'The plugin at %s cannot be used on %s' %
(as_unicode(path_to_zip_file), platform))
return ans
except:
with self._lock:
del self.loaded_plugins[plugin_name]
raise
def _locate_code(self, zf, path_to_zip_file):
names = [x if isinstance(x, unicode_type) else x.decode('utf-8') for x in
zf.namelist()]
names = [x[1:] if x[0] == '/' else x for x in names]
plugin_name = None
for name in names:
name, ext = posixpath.splitext(name)
if name.startswith('plugin-import-name-') and ext == '.txt':
plugin_name = name.rpartition('-')[-1]
if plugin_name is None:
c = 0
while True:
c += 1
plugin_name = 'dummy%d'%c
if plugin_name not in self.loaded_plugins:
break
else:
if self._identifier_pat.match(plugin_name) is None:
raise InvalidPlugin((
'The plugin at %r uses an invalid import name: %r' %
(path_to_zip_file, plugin_name)))
pynames = [x for x in names if x.endswith('.py')]
candidates = [posixpath.dirname(x) for x in pynames if
x.endswith('/__init__.py')]
candidates.sort(key=lambda x: x.count('/'))
valid_packages = set()
for candidate in candidates:
parts = candidate.split('/')
parent = '.'.join(parts[:-1])
if parent and parent not in valid_packages:
continue
valid_packages.add('.'.join(parts))
names = OrderedDict()
for candidate in pynames:
parts = posixpath.splitext(candidate)[0].split('/')
package = '.'.join(parts[:-1])
if package and package not in valid_packages:
continue
name = '.'.join(parts)
names[name] = zf.getinfo(candidate)
# Legacy plugins
if '__init__' not in names:
for name in tuple(names):
if '.' not in name and name.endswith('plugin'):
names['__init__'] = names[name]
break
if '__init__' not in names:
raise InvalidPlugin(('The plugin in %r is invalid. It does not '
'contain a top-level __init__.py file')
% path_to_zip_file)
with self._lock:
self.loaded_plugins[plugin_name] = (path_to_zip_file, names)
return plugin_name
loader = PluginLoader()
sys.meta_path.insert(0, loader)
if __name__ == '__main__':
from tempfile import NamedTemporaryFile
from calibre.customize.ui import add_plugin
from calibre import CurrentDir
path = sys.argv[-1]
with NamedTemporaryFile(suffix='.zip') as f:
with zipfile.ZipFile(f, 'w') as zf:
with CurrentDir(path):
for x in os.listdir('.'):
if x[0] != '.':
print('Adding', x)
zf.write(x)
if os.path.isdir(x):
for y in os.listdir(x):
zf.write(os.path.join(x, y))
add_plugin(f.name)
print('Added plugin from', sys.argv[-1])