プラグインを書いて calibre を機能拡張

calibre の設計は非常にモジュラーです。ほとんどすべての calibre の機能がプラグインという形で提供されます。プラグインは変換、ニュースのダウンロード (ただしこれはレシピと呼ばれます)、ユーザインタフェースのさまざまな部品、いろいろなデバイスへの接続、calibre にファイルを追加する際の処理など、多岐にわたって使用されます。ビルトインのプラグインは、環境設定 → 高度な設定 → プラグイン ですべてを確認できます。

ここでは独自のプラグインを作成して calibre に新しい機能を追加する方法を説明します。

注釈

これが適用されるのは calibre のリリース 0.8.60 以降に限られます。

calibre プラグインの構造

calibre のプラグインはとてもシンプルです。Python のコードと、プラグインが必要とする画像ファイルなどのその他リソースからなる単なる ZIP ファイルにすぎません。これ以上面倒なことは避けて、基本的な例を見てみましょう。

EPUB および MOBI 形式のさまざまな電子ドキュメントを自己出版するために calibre をインストールしているとしましょう。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
        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


これでおしまいです。あとは __init__.py を作成したフォルダで次のコードを実行するだけで、このコードを calibre にプラグインとして追加できます:

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 の形式にする必要があります。このファイルが存在すると ZIP ファイルの中にある .py ファイルからコードをインポートできるようになります。それには次のように記述します:

from calibre_plugins.some_name.some_module import some_object

必ずプリフィックス calibre_plugins をつける必要があります。some_name は空のテキストファイルのファイル名から取得します。some_name は ZIP ファイル内の some_module.py を参照します。このインポートは Python の普通のインポートと同じくらい強力です。通常と同じように (各サボフォルダに __init__.py を定義することにより) ZIP ファイルの中に .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 プラグインは __init__.py の中で GUI ライブラリをロードすべきではありません。actual_plugin が calibre に実際のプラグインが ZIP アーカイブ内の別のファイルに見つかったことを知らせて、GUI コンテキストでのみロードされるようになります。

これを機能させるには、上で説明したようにプラグイン ZIP ファイルの中に plugin-import-name-some_name.txt が必要です。

プラグインのユーザ設定を有効にする方法は、いくつか存在します。それについては下で説明します。

ui.py

では、実際の GUI プラグインを定義している 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 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)

This function should be called with a list of paths to files inside the ZIP file. For example to access the file icon.png in the folder images in the ZIP file, you would use: images/icon.png. Always use a forward slash as the path separator, even on Windows. When you pass in a single name, the function will return the raw bytes of that file or None if the name was not found in the ZIP file. If you pass in more than one name then it returns a dict mapping the names to bytes. If a name is not found, it will not be present in the returned dict.

get_icons(name_or_list_of_names, plugin_name='')

A wrapper for get_resources() that creates QIcon objects from the raw bytes returned by get_resources. If a name is not found in the ZIP file the corresponding QIcon will be null. In order to support icon theme-ing, pass in the human friendly name of your plugin as plugin_name. If the user is using an icon theme with icons for your plugin, they will be loaded preferentially.

プラグインのユーザ設定を有効にする

ユーザがプラグインを設定できるようにするためには、プラグインの基底クラスに 3 つのメソッド、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)

ひとつのエディタプラグインで複数のツールを提供できます。それぞれのツールはツールバーのひとつのボタンとエディタの プラグイン メニューのエントリに対応づけられます。ツールが複数の関連した動作を持つ場合にはサブメニューにすることも可能です。

ツールはすべてプラグインの中の main.py` に定義する必要があります。すべてのツールは calibre.gui2.tweak_book.plugin.Tool クラスを継承するクラスです。デモプラグインの main.py を見てみましょう。ソースコードにはコメントがちりばめられているので、読めばわかるはずです。詳しくは calibre.gui2.tweak_book.plugin.Tool クラスの API ドキュメントをお読みください。

main.py

ここでは、本のすべてのフォントサイズをユーザに指定された数字で掛けた大きさにするような、ひとつのツールの定義を見てみましょう。このツールは自分でプラグインを開発するときに必要となるさまざまな重要な概念を実演しているので、(コメントのちりばめられた) ソースコードをよく読んでください。

import re
from qt.core 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、エディタ Bossself.boss として参照することです。Boss はエディタユーザインタフェースを制御するオブジェクトです。これには便利なメソッドがたくさんありますが、それについては calibre.gui2.tweak_book.boss.Boss クラスに説明があります。

最後に self.current_container があり、これは編集されようとしている本を calibre.ebooks.oeb.polish.container.Container オブジェクトへの参照です。これは本をその構成要素であるHTML/CSS/画像 ファイルの集まりとして表し、役に立ついろいろなことができる便利なメソッドを備えています。プラグインコードで再利用可能なコンテナオブジェクトとさまざまな便利なユーティリティ関数については、電子書籍編集ツール API 説明書 に説明があります。

プラグインに翻訳を追加

プラグインのユーザインタフェース上のすべての文字列は翻訳して、メインの 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 フォルダに置きます。

最後のステップは、ただ単にプラグインの .pu ファイルの先頭で関数 load_translations() を呼び出すだけです。性能上の理由からこの関数を呼び出すのは、実際に翻訳可能な文字列が含まれている .py ファイルに限るべきです。したがって典型的なユーザインタフェースプラグインではこの関数は __init__.py ではなく ui.py の先頭で呼び出します。

プラグインの翻訳をテストするには、環境設定 → インタフェース → 外観 でcalibre のユーザインタフェース言語を変更するか、calibre を次のようにして実行します:

CALIBRE_OVERRIDE_LANG=de calibre

de の部分はテスとしたい言語の言語コードに置き換えてください。

プラグイン API

すでにお気づきかもしれませんが、calibre のプラグインとはクラスです。calibre プラグインの種類ごとに異なるクラスがあります。すsべてのプラグインの基底クラスのを含め、それぞれのクラスについての詳細は プラグインの API 説明書 に記載されています。

プラグインはほぼ確実に calibre のコードを使用します。calibre のコードベースの機能についてすみからすみまで学ぶには、calibre の:ref:code_layout の章をお読みください。

デバッグプラグイン

最初の、そして最も重要なステップは 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 ユーザと共有したければ、 calibre プラグインフォーラム に新しいスレッドとして投稿してください。