Создание собственных плагинов для расширения функциональности calibre

calibre имеет очень модульную конструкцию. Практически все функциональные возможности calibre поставляются в виде плагинов. Плагины используются для преобразования, для загрузки новостей (там они называются рецептами), для различных компонентов пользовательского интерфейса, для подключения к различным устройствам, для обработки файлов при добавлении их в calibre и так далее. Вы можете получить полный список всех встроенных плагинов в calibre, перейдя к :guilabel: Preferences-> Advanced-> Plugins.

Здесь мы научим вас, как создавать свои собственные плагины чтобы добавить новые функции в calibre.

Примечание

Это относится только к calibre releases >= 0.8.60

Анатомия плагина calibre

Плагин calibre очень прост, это просто ZIP-файл, который содержит некоторый код Python и любые другие ресурсы, такие как файлы изображений, необходимые плагину. Без дальнейших церемоний, давайте посмотрим на основной пример.

Предположим, у вас установлена calibre, которую вы используете для самостоятельной публикации различных электронных документов в форматах EPUB и MOBI. Вы хотите, чтобы для всех файлов, сгенерированных calibre, их издатель был установлен как «Hello world», вот как это сделать. Создайте файл с именем __init__.py (это специальное имя, которое всегда должно использоваться для основного файла вашего плагина) и введите в него следующий код Python:

from calibre.customize import FileTypePlugin

class HelloWorld(FileTypePlugin):

    name                = 'Hello World Plugin' # Name of the plugin
    description         = 'Set the publisher to Hello World for all new conversions'
    supported_platforms = ['windows', 'osx', 'linux'] # Platforms this plugin will run on
    author              = 'Acme Inc.' # The author of this plugin
    version             = (1, 0, 0)   # The version number of this plugin
    file_types          = set(['epub', 'mobi']) # The file types that this plugin will be applied to
    on_postprocess      = True # Run this plugin after conversion is complete
    minimum_calibre_version = (0, 7, 53)

    def run(self, path_to_ebook):
        from calibre.ebooks.metadata.meta import get_metadata, set_metadata
        file = open(path_to_ebook, 'r+b')
        ext  = os.path.splitext(path_to_ebook)[-1][1:].lower()
        mi = get_metadata(file, ext)
        mi.publisher = 'Hello World'
        set_metadata(file, mi, ext)
        return path_to_ebook


Это все. Чтобы добавить этот код в calibre в качестве плагина, просто запустите следующее в каталоге, в котором вы создали __init__.py:

calibre-customize -b .

Примечание

В macOS инструменты командной строки находятся внутри пакета calibre, например, если вы установили calibre в /Applications, то инструменты командной строки будут в /Applications/calibre.app/Contents/MacOS/.

Вы можете загрузить плагин Hello World с сайта helloworld_plugin.zip.

Каждый раз, когда вы используете calibre для конвертации книги, вызывается метод плагина run(), и для конвертированной книги издателю присваивается значение «Hello World». Это тривиальный плагин, давайте перейдем к более сложному примеру, который фактически добавляет компонент в пользовательский интерфейс.

Плагин пользовательского интерфейса

Этот плагин будет распространяться на несколько файлов (чтобы сохранить код в чистоте). Он покажет вам, как получить ресурсы (изображения или файлы данных) из ZIP-файла плагина, позволит пользователям настраивать ваш плагин, как создавать элементы в пользовательском интерфейсе calibre и как получить доступ и запросить базу данных книг в calibre.

Вы можете загрузить этот плагин из interface_demo_plugin.zip

Первое, на что нужно обратить внимание, это то, что в этом ZIP-файле содержится гораздо больше файлов. Как объясняется ниже, обратите особое внимание на plugin-import-name-interface_demo.txt.

plugin-import-name-interface_demo.txt

Пустой текстовый файл, используемый для включения магии многофайлового плагина. Этот файл должен присутствовать во всех плагинах, которые используют более одного файла .py. Он должен быть пустым, а его имя файла должно иметь вид: plugin-import-name-** some_name**.txt. Наличие этого файла позволяет вам импортировать код из файлов .py, присутствующих в ZIP-файле, используя оператор вроде:

from calibre_plugins.some_name.some_module import some_object

Префикс calibre_plugins должен присутствовать всегда. some_name происходит от имени файла пустого текстового файла. some_module относится к some_module.py файлу внутри ZIP-файла. Обратите внимание, что этот импорт такой же мощный, как и обычный импорт Python. Вы можете создавать пакеты и подпакеты модулей .py внутри ZIP-файла, как обычно (определяя __init__.py в каждом подкаталоге), и все должно «просто работать».

Имя, которое вы используете для some_name, входит в глобальное пространство имен, общее для всех плагинов, поэтому сделайте его как можно более уникальным. Но помните, что это должен быть действительный идентификатор Python (только буквы, цифры и подчеркивание).

__init__.py

Как и прежде, файл, который определяет класс плагина

main.py

Этот файл содержит реальный код, который делает что-то полезное

ui.py

Этот файл определяет интерфейсную часть плагина

images/icon.png

Значок для этого плагина

about.txt

Текстовый файл с информацией о плагине

translations

Папка, содержащая файлы .mo с переводами пользовательского интерфейса вашего плагина на разные языки. Смотрите ниже для деталей.

Теперь давайте посмотрим на код.

__init__.py

Во-первых, обязательный __init__.py для определения метаданных плагина:

from calibre.customize import InterfaceActionBase

class InterfacePluginDemo(InterfaceActionBase):
    '''
    This class is a simple wrapper that provides information about the actual
    plugin class. The actual interface plugin class is called InterfacePlugin
    and is defined in the ui.py file, as specified in the actual_plugin field
    below.

    The reason for having two classes is that it allows the command line
    calibre utilities to run without needing to load the GUI libraries.
    '''
    name                = 'Interface Plugin Demo'
    description         = 'An advanced plugin demo'
    supported_platforms = ['windows', 'osx', 'linux']
    author              = 'Kovid Goyal'
    version             = (1, 0, 0)
    minimum_calibre_version = (0, 7, 53)

    #: This field defines the GUI plugin class that contains all the code
    #: that actually does something. Its format is module_path:class_name
    #: The specified class must be defined in the specified module.
    actual_plugin       = 'calibre_plugins.interface_demo.ui:InterfacePlugin'

    def is_customizable(self):
        '''
        This method must return True to enable customization via
        Preferences->Plugins
        '''
        return True

    def config_widget(self):
        '''
        Implement this method and :meth:`save_settings` in your plugin to
        use a custom configuration dialog.

        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.

        The base class implementation of this method raises NotImplementedError
        so by default no user configuration is possible.
        '''
        # It is important to put this import statement here rather than at the
        # top of the module as importing the config class will also cause the
        # GUI libraries to be loaded, which we do not want when using calibre
        # from the command line
        from calibre_plugins.interface_demo.config import ConfigWidget
        return ConfigWidget()

    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`.
        '''
        config_widget.save_settings()

        # Apply the changes
        ac = self.actual_plugin_
        if ac is not None:
            ac.apply_settings()


Единственная заслуживающая внимания особенность - это поле actual_plugin. Так как calibre имеет и командную строку, и интерфейсы GUI, плагины GUI, подобные этому, не должны загружать библиотеки GUI в __init__.py. Поле actual_plugin сделает это за вас, сообщив calibre, что фактический плагин находится в другом файле внутри вашего ZIP-архива, который будет загружен только в контексте графического интерфейса пользователя.

Помните, что для этого вам понадобится файл plugin-import-name-some_name.txt в вашем ZIP-файле плагина, как обсуждалось выше.

Также есть несколько способов включения пользовательской конфигурации плагина. Они обсуждаются ниже.

ui.py

Теперь давайте посмотрим на ui.py, который определяет реальный плагин GUI. Исходный код тщательно прокомментирован и должен быть понятен:

from calibre.gui2.actions import InterfaceAction
from calibre_plugins.interface_demo.main import DemoDialog

class InterfacePlugin(InterfaceAction):

    name = 'Interface Plugin Demo'

    # Declare the main action associated with this plugin
    # The keyboard shortcut can be None if you dont want to use a keyboard
    # shortcut. Remember that currently calibre has no central management for
    # keyboard shortcuts, so try to use an unusual/unused shortcut.
    action_spec = ('Interface Plugin Demo', None,
            'Run the Interface Plugin Demo', 'Ctrl+Shift+F1')

    def genesis(self):
        # This method is called once per plugin, do initial setup here

        # Set the icon for this interface action
        # The get_icons function is a builtin function defined for all your
        # plugin code. It loads icons from the plugin zip file. It returns
        # QIcon objects, if you want the actual data, use the analogous
        # get_resources builtin function.
        #
        # Note that if you are loading more than one icon, for performance, you
        # should pass a list of names to get_icons. In this case, get_icons
        # will return a dictionary mapping names to QIcons. Names that
        # are not found in the zip file will result in null QIcons.
        icon = get_icons('images/icon.png')

        # The qaction is automatically created from the action_spec defined
        # above
        self.qaction.setIcon(icon)
        self.qaction.triggered.connect(self.show_dialog)

    def show_dialog(self):
        # The base plugin object defined in __init__.py
        base_plugin_object = self.interface_action_base_plugin
        # Show the config dialog
        # The config dialog can also be shown from within
        # Preferences->Plugins, which is why the do_user_config
        # method is defined on the base plugin class
        do_user_config = base_plugin_object.do_user_config

        # self.gui is the main calibre GUI. It acts as the gateway to access
        # all the elements of the calibre user interface, it should also be the
        # parent of the dialog
        d = DemoDialog(self.gui, self.qaction.icon(), do_user_config)
        d.show()

    def apply_settings(self):
        from calibre_plugins.interface_demo.config import prefs
        # In an actual non trivial plugin, you would probably need to
        # do something based on the settings in prefs
        prefs

main.py

Фактическая логика для реализации диалогового окна Interface Plugin Demo.


from calibre_plugins.interface_demo.config import prefs


class DemoDialog(QDialog):

    def __init__(self, gui, icon, do_user_config):
        QDialog.__init__(self, gui)
        self.gui = gui
        self.do_user_config = do_user_config

        # The current database shown in the GUI
        # db is an instance of the class LibraryDatabase from db/legacy.py
        # This class has many, many methods that allow you to do a lot of
        # things. For most purposes you should use db.new_api, which has
        # a much nicer interface from db/cache.py
        self.db = gui.current_db

        self.l = QVBoxLayout()
        self.setLayout(self.l)

        self.label = QLabel(prefs['hello_world_msg'])
        self.l.addWidget(self.label)

        self.setWindowTitle('Interface Plugin Demo')
        self.setWindowIcon(icon)

        self.about_button = QPushButton('About', self)
        self.about_button.clicked.connect(self.about)
        self.l.addWidget(self.about_button)

        self.marked_button = QPushButton(
            'Show books with only one format in the calibre GUI', self)
        self.marked_button.clicked.connect(self.marked)
        self.l.addWidget(self.marked_button)

        self.view_button = QPushButton(
            'View the most recently added book', self)
        self.view_button.clicked.connect(self.view)
        self.l.addWidget(self.view_button)

        self.update_metadata_button = QPushButton(
            'Update metadata in a book\'s files', self)
        self.update_metadata_button.clicked.connect(self.update_metadata)
        self.l.addWidget(self.update_metadata_button)

        self.conf_button = QPushButton(
                'Configure this plugin', self)
        self.conf_button.clicked.connect(self.config)
        self.l.addWidget(self.conf_button)

        self.resize(self.sizeHint())

    def about(self):
        # Get the about text from a file inside the plugin zip file
        # The get_resources function is a builtin function defined for all your
        # plugin code. It loads files from the plugin zip file. It returns
        # the bytes from the specified file.
        #
        # Note that if you are loading more than one file, for performance, you
        # should pass a list of names to get_resources. In this case,
        # get_resources will return a dictionary mapping names to bytes. Names that
        # are not found in the zip file will not be in the returned dictionary.
        text = get_resources('about.txt')
        QMessageBox.about(self, 'About the Interface Plugin Demo',
                text.decode('utf-8'))

    def marked(self):
        ''' Show books with only one format '''
        db = self.db.new_api
        matched_ids = {book_id for book_id in db.all_book_ids() if len(db.formats(book_id)) == 1}
        # Mark the records with the matching ids
        # new_api does not know anything about marked books, so we use the full
        # db object
        self.db.set_marked_ids(matched_ids)

        # Tell the GUI to search for all marked records
        self.gui.search.setEditText('marked:true')
        self.gui.search.do_search()

    def view(self):
        ''' View the most recently added book '''
        most_recent = most_recent_id = None
        db = self.db.new_api
        for book_id, timestamp in db.all_field_for('timestamp', db.all_book_ids()).items():
            if most_recent is None or timestamp > most_recent:
                most_recent = timestamp
                most_recent_id = book_id

        if most_recent_id is not None:
            # Get a reference to the View plugin
            view_plugin = self.gui.iactions['View']
            # Ask the view plugin to launch the viewer for row_number
            view_plugin._view_calibre_books([most_recent_id])

    def update_metadata(self):
        '''
        Set the metadata in the files in the selected book's record to
        match the current metadata in the database.
        '''
        from calibre.ebooks.metadata.meta import set_metadata
        from calibre.gui2 import error_dialog, info_dialog

        # Get currently selected books
        rows = self.gui.library_view.selectionModel().selectedRows()
        if not rows or len(rows) == 0:
            return error_dialog(self.gui, 'Cannot update metadata',
                             'No books selected', show=True)
        # Map the rows to book ids
        ids = list(map(self.gui.library_view.model().id, rows))
        db = self.db.new_api
        for book_id in ids:
            # Get the current metadata for this book from the db
            mi = db.get_metadata(book_id, get_cover=True, cover_as_data=True)
            fmts = db.formats(book_id)
            if not fmts:
                continue
            for fmt in fmts:
                fmt = fmt.lower()
                # Get a python file object for the format. This will be either
                # an in memory file or a temporary on disk file
                ffile = db.format(book_id, fmt, as_file=True)
                ffile.seek(0)
                # Set metadata in the format
                set_metadata(ffile, mi, fmt)
                ffile.seek(0)
                # Now replace the file in the calibre library with the updated
                # file. We dont use add_format_with_hooks as the hooks were
                # already run when the file was first added to calibre.
                db.add_format(book_id, fmt, ffile, run_hooks=False)

        info_dialog(self, 'Updated files',
                'Updated the metadata in the files of %d book(s)'%len(ids),
                show=True)

    def config(self):
        self.do_user_config(parent=self)
        # Apply the changes
        self.label.setText(prefs['hello_world_msg'])

Получение ресурсов из плагина ZIP-файла

Система загрузки плагинов calibre определяет несколько встроенных функций, которые позволяют вам удобно получать файлы из ZIP-файла плагина.

get_resources(name_or_list_of_names)

Эта функция должна вызываться со списком путей к файлам внутри ZIP-файла. Например, чтобы получить доступ к файлу icon.png в каталоге изображений в ZIP-файле, вы должны использовать: images/icon.png. Всегда используйте косую черту в качестве разделителя пути, даже в Windows. Когда вы передаете одно имя, функция возвращает необработанные байты этого файла или None, если имя не было найдено в ZIP-файле. Если вы передаете более одного имени, то он возвращает dict, отображающий имена в байты. Если имя не найдено, оно не будет присутствовать в возвращенном dict.

get_icons(name_or_list_of_names)

Удобная оболочка для get_resources(), которая создает объекты QIcon из необработанных байтов, возвращаемых get_resources. Если имя не найдено в ZIP-файле, соответствующий QIcon будет нулевым.

Включение пользовательской конфигурации вашего плагина

Чтобы позволить пользователям настраивать ваш плагин, вы должны определить три метода в вашем базовом классе плагинов, is_customizable, config_widget и save_settings, как показано ниже:

    def is_customizable(self):
        '''
        This method must return True to enable customization via
        Preferences->Plugins
        '''
        return True
    def config_widget(self):
        '''
        Implement this method and :meth:`save_settings` in your plugin to
        use a custom configuration dialog.

        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.

        The base class implementation of this method raises NotImplementedError
        so by default no user configuration is possible.
        '''
        # It is important to put this import statement here rather than at the
        # top of the module as importing the config class will also cause the
        # GUI libraries to be loaded, which we do not want when using calibre
        # from the command line
        from calibre_plugins.interface_demo.config import ConfigWidget
        return ConfigWidget()
    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`.
        '''
        config_widget.save_settings()

        # Apply the changes
        ac = self.actual_plugin_
        if ac is not None:
            ac.apply_settings()

calibre имеет много разных способов хранения данных конфигурации (наследие его длинной истории). Рекомендуемый способ - использовать класс JSONConfig, который хранит информацию о вашей конфигурации в файле .json.

Код для управления данными конфигурации в демо-плагине находится в config.py:


from calibre.utils.config import JSONConfig

# This is where all preferences for this plugin will be stored
# Remember that this name (i.e. plugins/interface_demo) is also
# in a global namespace, so make it as unique as possible.
# You should always prefix your config file name with plugins/,
# so as to ensure you dont accidentally clobber a calibre config file
prefs = JSONConfig('plugins/interface_demo')

# Set defaults
prefs.defaults['hello_world_msg'] = 'Hello, World!'


class ConfigWidget(QWidget):

    def __init__(self):
        QWidget.__init__(self)
        self.l = QHBoxLayout()
        self.setLayout(self.l)

        self.label = QLabel('Hello world &message:')
        self.l.addWidget(self.label)

        self.msg = QLineEdit(self)
        self.msg.setText(prefs['hello_world_msg'])
        self.l.addWidget(self.msg)
        self.label.setBuddy(self.msg)

    def save_settings(self):
        prefs['hello_world_msg'] = self.msg.text()

Объект prefs теперь доступен во всем коде плагина с помощью простого:

from calibre_plugins.interface_demo.config import prefs

Вы можете увидеть объект prefs, используемый в main.py:

    def config(self):
        self.do_user_config(parent=self)
        # Apply the changes
        self.label.setText(prefs['hello_world_msg'])

Редактировать книжные плагины

Теперь давайте немного изменим механизм и посмотрим на создание плагина для добавления инструментов в редактор книг calibre. Плагин доступен здесь: editor_demo_plugin.zip.

Первым шагом, как и для всех плагинов, является создание пустого текстового файла с именем для импорта, как описано above. Мы назовем файл plugin-import-name-editor_plugin_demo.txt.

Теперь мы создаем обязательный файл __init__.py, который содержит метаданные о плагине – его имя, автора, версию и т. д.

from calibre.customize import EditBookToolPlugin


class DemoPlugin(EditBookToolPlugin):

    name = 'Edit Book plugin demo'
    version = (1, 0, 0)
    author = 'Kovid Goyal'
    supported_platforms = ['windows', 'osx', 'linux']
    description = 'A demonstration of the plugin interface for the ebook editor'
    minimum_calibre_version = (1, 46, 0)

Один плагин редактора может предоставлять несколько инструментов, каждый инструмент соответствует одной кнопке на панели инструментов и записи в меню Plugins в редакторе. Они могут иметь подменю, если инструмент имеет несколько связанных действий.

Все инструменты должны быть определены в файле main.py вашего плагина. Каждый инструмент - это класс, который наследуется от класса calibre.gui2.tweak_book.plugin.Tool. Давайте посмотрим на main.py из демо-плагина, исходный код тщательно прокомментирован и не требует пояснений. Прочитайте документы API класса calibre.gui2.tweak_book.plugin.Tool для получения более подробной информации.

main.py

Здесь мы увидим определение единственного инструмента, который умножит все размеры шрифта в книге на число, предоставленное пользователем. Этот инструмент демонстрирует различные важные концепции, которые вам понадобятся при разработке ваших собственных плагинов, поэтому вы должны внимательно прочитать (сильно комментированный) исходный код.

import re
from PyQt5.Qt import QAction, QInputDialog
from css_parser.css import CSSRule

# The base class that all tools must inherit from
from calibre.gui2.tweak_book.plugin import Tool

from calibre import force_unicode
from calibre.gui2 import error_dialog
from calibre.ebooks.oeb.polish.container import OEB_DOCS, OEB_STYLES, serialize


class DemoTool(Tool):

    #: Set this to a unique name it will be used as a key
    name = 'demo-tool'

    #: If True the user can choose to place this tool in the plugins toolbar
    allowed_in_toolbar = True

    #: If True the user can choose to place this tool in the plugins menu
    allowed_in_menu = True

    def create_action(self, for_toolbar=True):
        # Create an action, this will be added to the plugins toolbar and
        # the plugins menu
        ac = QAction(get_icons('images/icon.png'), 'Magnify fonts', self.gui)  # noqa
        if not for_toolbar:
            # Register a keyboard shortcut for this toolbar action. We only
            # register it for the action created for the menu, not the toolbar,
            # to avoid a double trigger
            self.register_shortcut(ac, 'magnify-fonts-tool', default_keys=('Ctrl+Shift+Alt+D',))
        ac.triggered.connect(self.ask_user)
        return ac

    def ask_user(self):
        # Ask the user for a factor by which to multiply all font sizes
        factor, ok = QInputDialog.getDouble(
            self.gui, 'Enter a magnification factor', 'Allow font sizes in the book will be multiplied by the specified factor',
            value=2, min=0.1, max=4
        )
        if ok:
            # Ensure any in progress editing the user is doing is present in the container
            self.boss.commit_all_editors_to_container()
            try:
                self.magnify_fonts(factor)
            except Exception:
                # Something bad happened report the error to the user
                import traceback
                error_dialog(self.gui, _('Failed to magnify fonts'), _(
                    'Failed to magnify fonts, click "Show details" for more info'),
                    det_msg=traceback.format_exc(), show=True)
                # Revert to the saved restore point
                self.boss.revert_requested(self.boss.global_undo.previous_container)
            else:
                # Show the user what changes we have made, allowing her to
                # revert them if necessary
                self.boss.show_current_diff()
                # Update the editor UI to take into account all the changes we
                # have made
                self.boss.apply_container_update_to_gui()

    def magnify_fonts(self, factor):
        # Magnify all font sizes defined in the book by the specified factor
        # First we create a restore point so that the user can undo all changes
        # we make.
        self.boss.add_savepoint('Before: Magnify fonts')

        container = self.current_container  # The book being edited as a container object

        # Iterate over all style declarations in the book, this means css
        # stylesheets, <style> tags and style="" attributes
        for name, media_type in container.mime_map.items():
            if media_type in OEB_STYLES:
                # A stylesheet. Parsed stylesheets are css_parser CSSStylesheet
                # objects.
                self.magnify_stylesheet(container.parsed(name), factor)
                container.dirty(name)  # Tell the container that we have changed the stylesheet
            elif media_type in OEB_DOCS:
                # A HTML file. Parsed HTML files are lxml elements

                for style_tag in container.parsed(name).xpath('//*[local-name="style"]'):
                    if style_tag.text and style_tag.get('type', None) in {None, 'text/css'}:
                        # We have an inline CSS <style> tag, parse it into a
                        # stylesheet object
                        sheet = container.parse_css(style_tag.text)
                        self.magnify_stylesheet(sheet, factor)
                        style_tag.text = serialize(sheet, 'text/css', pretty_print=True)
                        container.dirty(name)  # Tell the container that we have changed the stylesheet
                for elem in container.parsed(name).xpath('//*[@style]'):
                    # Process inline style attributes
                    block = container.parse_css(elem.get('style'), is_declaration=True)
                    self.magnify_declaration(block, factor)
                    elem.set('style', force_unicode(block.getCssText(separator=' '), 'utf-8'))

    def magnify_stylesheet(self, sheet, factor):
        # Magnify all fonts in the specified stylesheet by the specified
        # factor.
        for rule in sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE):
            self.magnify_declaration(rule.style, factor)

    def magnify_declaration(self, style, factor):
        # Magnify all fonts in the specified style declaration by the specified
        # factor
        val = style.getPropertyValue('font-size')
        if not val:
            return
        # see if the font-size contains a number
        num = re.search(r'[0-9.]+', val)
        if num is not None:
            num = num.group()
            val = val.replace(num, '%f' % (float(num) * factor))
            style.setProperty('font-size', val)
        # We should also be dealing with the font shorthand property and
        # font sizes specified as non numbers, but those are left as exercises
        # for the reader

Давайте разберемся с main.py. Мы видим, что он определяет один инструмент с именем Magnify fonts. Этот инструмент запросит у пользователя число и умножит все размеры шрифтов в книге на это число.

Первым важным моментом является имя инструмента, которое вы должны установить для некоторой относительно уникальной строки, поскольку она будет использоваться в качестве ключа для этого инструмента.

Следующая важная точка входа - это calibre.gui2.tweak_book.plugin.Tool.create_action(). Этот метод создает объекты QAction, которые появляются на панели инструментов плагинов и меню плагинов. Также, по желанию, назначается сочетание клавиш, которое пользователь может настроить. Инициированный сигнал от QAction подключается к методу ask_user(), который запрашивает у пользователя множитель размера шрифта, а затем запускает код увеличения.

Код увеличения хорошо прокомментирован и довольно прост. Главное, на что следует обратить внимание, это то, что вы получаете ссылку на окно редактора как self.gui, а редактор Boss как self.boss. Boss - это объект, который управляет пользовательским интерфейсом редактора. У него есть много полезных методов, которые описаны в классе calibre.gui2.tweak_book.boss.Boss.

Наконец, есть self.current_container, который является ссылкой на книгу, редактируемую как объект calibre.ebooks.oeb.polish.container.Container. Это представляет книгу как коллекцию составляющих её HTML/CSS/графических файлов и имеет удобные методы для выполнения многих полезных задач. Контейнерный объект и различные полезные служебные функции, которые можно повторно использовать в коде вашего плагина, описаны в Документация API инструмента редактирования электронных книг.

Запуск плагинов пользовательского интерфейса в отдельном процессе

Если вы пишете плагин пользовательского интерфейса, который должен использовать Qt WebEngine, он не может быть запущен в основном процессе calibre, так как там невозможно использовать WebEngine. Вместо этого вы можете скопировать данные, необходимые вашему плагину, во временный каталог и запустить плагин с этими данными в отдельном процессе. Ниже приведен простой пример плагина, который показывает, как это сделать.

Вы можете загрузить плагин из webengine_demo_plugin.zip.

Важной частью плагина являются две функции:


    def show_dialog(self):
        # Ask the user for a URL
        url, ok = QInputDialog.getText(self.gui, 'Enter a URL', 'Enter a URL to browse below', text='https://calibre-ebook.com')
        if not ok or not url:
            return
        # Launch a separate process to view the URL in WebEngine
        self.gui.job_manager.launch_gui_app('webengine-dialog', kwargs={
            'module':'calibre_plugins.webengine_demo.main', 'url':url})
def main(url):
    # This function is run in a separate process and can do anything it likes,
    # including use QWebEngine. Here it simply opens the passed in URL
    # in a QWebEngineView
    app = Application([])
    w = QWebEngineView()
    w.setUrl(QUrl(url))
    w.show()
    w.raise_()
    app.exec_()

Функция show_demo() запрашивает у пользователя URL, а затем запускает функцию main(), передавая ей этот URL. Функция main() отображает URL в `QWebEngineView.

Добавление переводов в ваш плагин

Вы можете перевести и отобразить все строки пользовательского интерфейса в вашем плагине на любом языке, установленном для основного пользовательского интерфейса.

Первый шаг - просмотреть исходный код вашего плагина и пометить все видимые пользователю строки как переводимые, заключив их в _(). Например:

action_spec = (_('My plugin'), None, _('My plugin is cool'), None)

Затем используйте какую-нибудь программу для генерации .po файлов из исходного кода вашего плагина. Для каждого языка, на который вы хотите перевести, должен быть один .po-файл. Например: de.po для немецкого, fr.po для французского и так далее. Для этого вы можете использовать программу Poedit.

Отправьте эти .po файлы своим переводчикам. Как только вы получите их обратно, скомпилируйте их в файлы .mo. Вы можете снова использовать Poedit для этого или просто сделать:

calibre-debug -c "from calibre.translations.msgfmt import main; main()" filename.po

Поместите файлы .mo в папку translations вашего плагина.

Последний шаг - просто вызвать функцию load_translations() вверху .py файлов вашего плагина. Из соображений производительности эту функцию следует вызывать только в тех файлах .py, которые на самом деле содержат переводимые строки. Таким образом, в типичном плагине интерфейса пользователя вы бы вызвали его в верхней части ui.py, но не в __init __.py.

Вы можете проверить переводы ваших плагинов, изменив язык интерфейса пользователя в calibre в Preferences →  Interface →  Look & feel или запустив calibre следующим образом:

CALIBRE_OVERRIDE_LANG=de calibre

Замените de на код языка, который вы хотите проверить.

API плагина

Как вы могли заметить выше, плагин calibre - это класс. Существуют разные классы для разных типов плагинов в calibre. Подробности о каждом классе, включая базовый класс всех плагинов, можно найти в Документация API плагинов.

Ваш плагин почти наверняка будет использовать код из calibre. Чтобы узнать, как найти различные функциональные возможности в кодовой базе calibre, прочитайте раздел о calibre Макет кода.

Отладка плагинов

Первый, самый важный шаг - запустить calibre в режиме отладки. Вы можете сделать это из командной строки с помощью:

calibre-debug -g

Или изнутри calibre, щелкнув правой кнопкой мыши по кнопке Preferences или используя сочетание клавиш Ctrl + Shift + R.

При запуске из командной строки выходные данные отладки будут выводиться на консоль, а при запуске из calibre выходные данные будут передаваться в текстовый файл.

Вы можете вставить операторы печати в любом месте вашего кода плагина, они будут выводиться в режиме отладки. Помните, что это Python, вам действительно не нужно ничего, кроме операторов print для отладки;) Я разработал все версии calibre, используя только эту технику отладки.

Вы можете быстро протестировать изменения в вашем плагине, используя следующую командную строку:

calibre-debug -s; calibre-customize -b /path/to/your/plugin/directory; calibre

Это отключит работающий calibre, дождется завершения выключения, затем обновит ваш плагин в calibre и перезапустит calibre.

Больше примеров плагинов

Вы можете найти список многих сложных плагинов calibre здесь.

Поделиться своими плагинами с другими

Если вы хотите поделиться плагинами, которые вы создали, с другими пользователями calibre, опубликуйте свой плагин в новой теме на `форуме плагинов calibre <https://www.mobileread.com/forums/forumdisplay.php?f=237. >`_.