Створення власних додатків для розширення функціональних можливостей calibre

У calibre дуже багато модулів. Майже усі функціональні можливості calibre реалізовано у формі додатків. Додатки використовуються для перетворення даних, для отримання новин (у формі так званих рецептів), для різноманітних компонентів інтерфейсу користувача, для встановлення з’єднання з різними пристроями, для обробки файлів під час додавання їх до бібліотеки calibre тощо. Із повним списком вбудованих додатків calibre можна ознайомитися на сторінці Налаштування  →  Додатково  →  Додатки.

У цьому розділи ми навчимо вас створювати власні додатки для додавання нових можливостей до calibre.

Примітка

Наведені тут дані стосуються Calibre версії 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          = {'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
        with open(path_to_ebook, 'r+b') as file:
            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 тут.

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

Додаток із інтерфейсом користувача

Код цього додатка складатиметься із декількох файлів (так зроблено для кращого структурування коду). За його допомогою ви зможете ознайомитися з тим, як отримувати ресурси (зображення або файли даних) з файла ZIP додатка, як надати змогу користувачам налаштовувати ваш додаток, як створювати елементи інтерфейсу користувача calibre та як отримувати доступ до бази даних книг calibre і надсилати запити до цієї бази даних.

Ви можете отримати цей додаток тут.

Першим, що слід зауважити, є те, що цей архів ZIP містить набагато більше файлів. Їхнє призначення описано нижче. Зверніть особливу увагу на файл plugin-import-name-interface_demo.txt.

plugin-import-name-interface_demo.txt

Порожній текстовий файл використовується для вмикання багатофайлової структури додатка. Цей файл має бути в усіх додатках, які складаються з декількох файлів .py. Він має бути порожнім, а його назва повинна мати таку формат: «plugin-import-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 є одразу два інтерфейси, командний рядок і графічний інтерфейс, додатки з графічним інтерфейсом, як цей, не повинні завантажувати жодних бібліотек графічного інтерфейсу а __init__.py. За вас це робить поле actual_plugin, повідомляючи calibre, що сам код додатка можна знайти у іншому файлі у вашому архіві ZIP. Цей код буде завантажено лише у контексті графічного інтерфейсу.

Не забудьте, що для того, щоб це спрацювало, вам слід додати до вашого архіву ZIP додатка файл plugin-import-name-some_name.txt, про який ми писали вище.

Крім того, існує декілька методів для уможливлення налаштовування додатка користувачем. Їх опис наведено нижче.

ui.py

Тепер погляньмо на ui.py, де визначається сам код додатка з графічним інтерфейсом. Початковий код містить повний набір коментарів, отже вам буде доволі просто у ньому розібратися, якщо ви знаєте англійську:

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', 'Interface Demo Plugin')

        # 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 qt.core import QDialog, QLabel, QMessageBox, QPushButton, QVBoxLayout


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(назва_або_список_назв)

Цій функції слід передати список шляхів до файлів у архіві ZIP. Наприклад, щоб отримати доступ до файла icon.png у теці images архіву ZIP, вам слід скористатися таким шляхом: images/icon.png. Завжди використовуйте звичайну (не зворотну) похилу риску як роздільник шляхів, навіть у Windows. Якщо ви передасте функції одну назву, вона поверне необроблений байтовий вміст файла або None, якщо у архіві ZIP не буде знайдено відповідного файла. Якщо ви передасте декілька назв, функція поверне словник із прив’язкою назв до байтового вмісту. Якщо файла знайдено не буде, його не буде і у повернутому словнику.

get_icons(назва_або_список_назв, назва_додатка=““)

Обгортка до 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 qt.core import QHBoxLayout, QLabel, QLineEdit, QWidget

# 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:

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

Додатки редагування книг

Тепер, давайте трохи змінимо тему і поглянемо на те, як створити додаток для додавання інструментів до редактора книг calibre. Сам код додатка можна знайти :download_file: тут <editor_demo_plugin.zip>.

Першим кроком, як і для всіх інших додатків, є створення порожнього текстового файла із імпортованою назвою, як це описано вище. Назвемо цей файл 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)

Один додаток редактора може містити декілька інструментів. Кожному інструменту відповідає одна кнопка на панелі інструментів і один запис у меню Додатки редактора. Ці пункти можуть мати підменю, якщо із інструментом пов’язано декілька дій.

Усі інструменти має бути визначено у файлі main.py вашого додатка. Кожен інструмент є класом, який успадковує властивості від класу calibre.gui2.tweak_book.plugin.Tool. Погляньмо на main.py з демонстраційного додатка. У коді багато коментарів, тому його призначення буде зрозумілим кожному, хто володіє англійською мовою. Докладніші відомості можна знайти у документації з програмного інтерфейсу, а саме класу calibre.gui2.tweak_book.plugin.Tool.

main.py

Тут ми бачимо визначення одного інструмента, який змінює розміри усіх шрифтів у книзі на коефіцієнт, вказаний користувачем. Цей інструмент демонструє різноманітні важливі концепції, які знадобляться вам для розробки власних додатків. Отже, вам варто уважно ознайомитися із початковим кодом (рясно перемежованим коментарями).

import re

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

# The base class that all tools must inherit from
from calibre.gui2.tweak_book.plugin import Tool
from css_parser.css import CSSRule
from qt.core import QAction, QInputDialog


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, зображень та містить зручні методи для виконання багатьох корисних речей. Об’єкт контейнера та різні корисні допоміжні функції, якими ви можете скористатися у коді вашого власного додатка, документовано у розділі Документація з програмного інтерфейсу засобів редагування електронних книг.

Додавання перекладів вашого додатка

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

Спочатку слід пройтися програмним кодом вашого додатка і позначити усі видимі для користувача рядки як придатні для перекладу за допомогою обгортання їх конструкцією _(). Приклад:

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 за допомогою сторінки Налаштування  →  Інтерфейс  →  Зовнішній вигляд або запустивши calibre зі встановленою змінною середовища CALIBRE_OVERRIDE_LANG . Приклад:

CALIBRE_OVERRIDE_LANG=de

Замініть «de» на код мови, яку ви хочете тестувати.

Для перекладу із формами множини скористайтеся функцією ngettext() замість _(). Приклад:

ngettext('Delete a book', 'Delete {} books', num_books).format(num_books)

Програмний інтерфейс для роботи з додатками

Як ви вже могли помітити раніше, додаток у calibre є класом. У calibre різним класам відповідають різні типи додатків. Подробиці щодо усіх класів, зокрема назви базових класів усіх додатків, наведено у розділі Документація з програмного інтерфейсу для роботи з додатками.

Ваш додаток, майже напевне, використовуватиме код з calibre. Щоб навчитися шукати потрібні вам частини у програмному коді calibre, ознайомтеся із розділом Структура коду.

Діагностування проблем у додатках

Першим, найважливішим кроком є запуск calibre у режимі діагностики. Зробити це можна за допомогою такої команди, відданої з командного рядка:

calibre-debug -g

Або з вікна calibre: натиснути стрілку праворуч від кнопки Налаштування або скористатися комбінацією клавіш Ctrl+Shift+R.

Якщо додаток запущено з командного рядка, діагностичні повідомлення виводитимуться до консолі. Якщо ж запуск виконувався з calibre, виведені дані буде записано до текстового файла.

Ви можете вставити інструкції виведення даних (print) будь-де у вашому коді додатка. Ці інструкції виводитимуть дані у режимі діагностики. Не забувайте, це Python, отже для діагностики вам не потрібно нічого окрім інструкцій print ;) Усі додатки, створені автором calibre, було написано з використанням саме цієї методики діагностики.

Ви можете швидко виконати тестування змін у вашому додатку за допомогою такої команди:

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

Ця команда завершить роботу запущеної програми calibre, зачекає на завершення роботи, далі оновить ваш додаток у calibre і перезапустить calibre.

Інші приклади додатків

Список багатьох складних додатків до calibre можна знайти тут.

Оприлюднення ваших додатків для використання іншими користувачами

Якщо ви хочете поділитися створеними вами додатками з іншими користувачами програми, створіть новий допису на форумі додатків calibre і долучіть до нього ваш додаток.