编写自己的插件来扩展 calibre 的功能

Calibre 是以模块化的形式设计的,其所有功能几乎都以插件的形式实现。这些插件用作于文本转换、新闻(或称之为诀窍)下载,和用户界面的各种组件,来连接到不同的设备,或在将文件添加到Calibre时处理他们等等。您可以在“选项→高级→插件”中完整地得到在Cailbre里已经内建的插件清单。

在此,我们将会教您如何创造您自己的插件,给Calibre 添加新的功能。

备注

此功能仅适用于0.8.60或更高的版本。

Calibre 插件的剖析

一个Calibre 插件的组成是非常简单的,其只是一个包含了一些Python 代码与一些其他资源文件如插件所需的贴图文件的ZIP 文件。现在,让我们直入主题,看看一个简单的例子。

假设您已安装了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/`.中。

您可以从 helloworld_plugin.zip 下载 Hello World 插件。

每次您使用 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_module 指的是 ZIP 文件中的 some_module.py 文件。 请注意,此导入与常规 Python 导入一样强大。 您可以在 ZIP 文件内创建 .py 模块的包和子包,就像通常一样(通过在每个子文件夹中定义 __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 插件不应在 __init__.py 中加载任何 GUI 库。 ual_plugin 字段可以为您完成此操作,告诉 calibre 实际插件可以在 ZIP 存档内的另一个文件中找到,该文件只会在 GUI 上下文中加载。

请记住,要使其正常工作,您的插件 ZIP 文件中必须有一个 plugin-import-name-some_name.txt 文件,如上所述。

还有几种方法可以启用插件的用户配置。 这些将在下面讨论。

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', '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

实现界面插件演示对话框的实际逻辑。

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(name_or_list_of_names)

应使用 ZIP 文件内文件的路径列表来调用此函数。 例如,要访问 ZIP 文件中图像文件夹中的文件“icon.png”,您可以使用:“images/icon.png”。 始终使用正斜杠作为路径分隔符,即使在 Windows 上也是如此。 当您传入单个名称时,该函数将返回该文件的原始字节,如果在 ZIP 文件中找不到该名称,则返回 None。 如果您传入多个名称,那么它会返回一个将名称映射到字节的字典。 如果未找到名称,则该名称不会出现在返回的字典中。

get_icons(name_or_list_of_names, plugin_name=’’)

get_resources() 的包装器,用于根据 get_resources 返回的原始字节创建 QIcon 对象。 如果在 ZIP 文件中找不到名称,则相应的 QIcon 将为空。 为了支持图标主题,请将插件的人类友好名称传递为“plugin_name”。 如果用户使用带有插件图标的图标主题,它们将优先加载。

启用插件的用户配置

要允许用户配置您的插件,您必须在插件基类中定义三个方法:is_customizableconfig_widgetsave_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

您可以看到 main.py 中使用了“prefs”对象:

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

编辑书籍插件

现在让我们稍微改变一下,看看创建一个插件来向 calibre 书籍编辑器添加工具。 该插件可在此处获取:editor_demo_plugin.zip

对于所有插件来说,第一步是创建导入名称空 txt 文件,如“上面 <import_name_txt>”所述。 我们将文件命名为“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 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``。 我们看到它定义了一个工具,名为*放大字体*。 该工具将要求用户提供一个数字,并将书中所有字体大小乘以该数字。

第一件重要的事情是工具名称,您必须将其设置为一些相对唯一的字符串,因为它将用作该工具的密钥。

下一个重要的入口点是“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/图像文件的集合,并且具有执行许多有用操作的便捷方法。 容器对象和可以在插件代码中重用的各种有用的实用程序函数记录在“polish_api”中。

将翻译添加到您的插件中

您可以翻译插件中的所有用户界面字符串,并以为主 calibre 用户界面设置的任何语言显示。

第一步是检查插件的源代码,并通过将所有用户可见的字符串括在 _() 中,将它们标记为可翻译。 例如:

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

然后使用一些程序从插件源代码生成 .po 文件。 您要翻译的每种语言都应该有一个 .po 文件。 例如:德语为 de.po,法语为 fr.po 等。 您可以使用“Poedit <https://poedit.net/>”程序来实现此目的。

将这些 .po 文件发送给您的翻译人员。 取回它们后,将它们编译成 .mo 文件。 您可以再次使用 Poedit 来实现此目的,或者只需执行以下操作:

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

将 .mo 文件放入插件的“translations”文件夹中。

最后一步是简单地调用插件 .py 文件顶部的函数“load_translations()”。 出于性能原因,您应该只在那些实际具有可翻译字符串的 .py 文件中调用此函数。 因此,在典型的用户界面插件中,您可以在“ui.py”的顶部调用它,而不是“__init__.py”。

您可以通过在 “首选项->界面->外观” 下更改 calibre 中的用户界面语言或通过设置 CALIBRE_OVERRIDE_LANG 环境变量集运行 calibre 来测试插件的翻译。 例如:

CALIBRE_OVERRIDE_LANG=de

将“de”替换为您要测试的语言的语言代码。

对于复数翻译,请使用“ngettext()”函数而不是“_()”。 例如:

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

插件 API

正如您在上面可能已经注意到的,calibre 中的插件是一个类。 calibre 中不同类型的插件有不同的类。 每个类的详细信息,包括所有插件的基类,可以在“插件”中找到。

您的插件几乎肯定会使用 calibre 中的代码。 要了解如何在 calibre 代码库中查找各种功能,请阅读有关 calibre“code_layout”的部分。

调试插件

第一步,也是最重要的一步是在调试模式下运行 calibre。 您可以使用以下命令从命令行执行此操作:

calibre-debug -g

或者在 calibre 中右键单击“首选项”按钮或使用“Ctrl+Shift+R”键盘快捷键。

从命令行运行时,调试输出将打印到控制台,从 calibre 内运行时,输出将打印到 txt 文件。

您可以在插件代码中的任何位置插入打印语句,它们将以调试模式输出。 请记住,这是 Python,除了打印语句之外,您实际上不需要任何其他东西来调试;)我仅使用这种调试技术开发了所有 calibre。

您可以使用以下命令行快速测试对插件的更改:

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

这将关闭正在运行的 calibre,等待关闭完成,然后更新 calibre 中的插件并重新启动 calibre。

更多插件示例

您可以在“此处 <https://www.mobileread.com/forums/showthread.php?t=118764>”找到许多复杂的 calibre 插件列表。

与他人分享您的插件

如果您想与 calibre 的其他用户分享您创建的插件,请将您的插件发布到“calibre 插件论坛”的新主题中 <https://www.mobileread.com/forums/forumdisplay.php?f=237 >`_。