Écrire vos propres extensions pour étendre les fonctionnalités de calibre

calibre a un design hautement modulaire. Presque toutes les fonctionnalités dans calibre viennent sous la forme d’extensions. Les extensions sont utilisées pour la conversion, pour télécharger des actualités (bien que celles-ci s’appellent des recettes), pour divers composants de l’interface utilisateur, pour connecter différents périphériques, pour traiter les fichiers lorsque vous les ajouter à calibre. Vous pouvez obtenir une liste complète de toutes les extensions intégrées dans calibre en allant à Préférences → Avancé → Extensions.

Ici, nous vous apprendrons comment créer vos propres extensions pour ajouter de nouvelles fonctionnalités à calibre.

Note

Ceci s’applique uniquement aux versions de calibre >=0.8.60

Anatomie d’une extension calibre

Une extension calibre est très simple, c’est juste un fichier ZIP qui contient un peu de code Python et toutes les autres ressources comme les fichiers images nécessaires à l’extension. Sans plus tarder, voyons un exemple basique.

Supposons que vous avez une installation de calibre que vous utilisez pour l’auto publication de divers e-documents aux formats EPUB et MOBI. Vous aimeriez que tous les fichiers générés par calibre aient leur éditeur paramétré à Salut le monde, voici comment faire. Créez un fichier nommé __init__.py (c’est un nom spécial et doit toujours être utilisé pour le fichier principal de votre extension) entrez-y le code Python suivant :

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

C’est tout. Pour ajouter ce code à calibre en tant qu’extension, exécuter simplement ce qui suit dans le dossier dans lequel vous avez créé __init__.py:

calibre-customize -b .

Note

Sous macOS, les outils en de ligne de commande sont à l’intérieur de la suite logicielle calibre, par exemple, si vous installez calibre dans /Applications les outils de ligne de commande sont dans /Applications/calibre.app/Contents/MacOS/.

Vous pouvez télécharger le plugin Hello World depuis helloworld_plugin.zip.

A chaque fois que vous utilisez calibre pour convertir un livre, la méthode de l’extension run() sera appelée et les livres convertis auront leur éditeur établi à Salut le monde. C’est une extension banale, passons à un exemple plus complexe qui habituellement ajoute un composant à l’interface utilisateur.

Une extension d’Interface Utilisateur

Cette extension sera répartie sur quelques fichiers (pour maintenir le code propre). Il vous montrera comment obtenir des ressources (des fichiers d’images ou de données) à partir du fichier extension ZIP, permettra à des utilisateurs de configurer votre extension, comment créer des éléments dans l’Interface Utilisateur de calibre et comment accéder et questionner la base de données de livres de calibre.

Vous pouvez télécharger ce plugin à partir de interface_demo_plugin.zip.

La première chose à noter est que le fichier ZIP contient beaucoup plus de fichiers, expliqués ci-dessous, prêtez particulièrement attention à plugin-import-name-interface_demo.txt.

plugin-import-name-interface_demo.txt

Un fichier texte vide utilisé pour activer la magie de l’extension multi fichiers. Ce fichier doit être présent dans toutes les extensions qui utilisent plus d’un fichier .py. Il devra être vide et son nom de fichier doit être de la forme : plugin-import-name-un_nom.txt. La présence de ce fichier vous permet d’importer du code à partir de fichiers .py présents à l’intérieur du fichier ZIP, utilisant une déclaration comme:

from calibre_plugins.some_name.some_module import some_object

Le préfixe calibre_plugins doit toujours être présent. some_name provient du nom de fichier du fichier texte vide. some_module se réfère au fichier some_module.py dans le fichier ZIP. Notez que cette importation est tout aussi puissante que les importations Python régulières. Vous pouvez créer des paquets et des sous paquets de modules .py à l’intérieur du fichier ZIP, tout comme vous le feriez normalement (en définissant __init__.py dans chaque sous dossier), et tout devrait « fonctionner correctement ».

Le nom que vous utilisez pour un_nom introduit un espace de nom global partagé par toutes les extensions. Aussi rendez le aussi unique que possible. Mais rappelez-vous qu’il doit être un identifiant Python valide(uniquement des lettres, des nombres et le tiret bas).

__init__.py

Comme précédemment, le fichier qui définit la classe de l’extension

main.py

Ce fichier contient le code actuel qui fait quelque chose d’utile

ui.py

Ce fichier définit la partie interface de l’extension

images/icon.png

L’icône pour ce plugin

about.txt

Un fichier texte contenant des informations sur le plugin

traductions

Un répertoire contenant des fichiers .mo avec les traductions de l’interface utilisateur de votre extension dans différentes langues. Voir ci-dessous pour les détails.

Maintenant regardons le code

__init__.py

Tout d’abord, l’obligatoirement __init__.py pour définir les métadonnées de l’extension :

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


La seule caractéristique remarquable est le champ actual_plugin. Depuis que calibre a la ligne de commande et les interfaces de GUI, les extensions avec GUI comme celle-ci ne devrait charger aucune bibliothèques de GUI dans __init__.py. Le champ actual_plugin fait ceci pour vous, en disant à calibre que l’extension actuelle doit être trouvée dans un autre fichier à l’intérieur de votre archive ZIP, qui seront seulement chargées dans un contexte GUI.

Rappelez-vous que pour que cela fonctionne, vous devez avoir un fichier plugin-import-name-un_nom.txt dans le fichier ZIP de votre extension, comme discuté plus haut.

Il y a également quelques méthodes pour permettre la configuration utilisateur de l’extension. Celles-ci sont discutées ci-dessous.

ui.py

Maintenant regardons à ui.py qui définit le GUI habituel de l’extension. Le code source est fortement commenté et devrait être explicite :

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

La logique actuelle pour mettre en application le dialogue de démo de l’interface de l’extension.


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'])

Obtention des ressources à partir du fichier ZIP de l’extension

Le système de chargement d’une extension calibre définit quelques fonctions intégrées qui permettent d’obtenir confortablement des fichiers à partir du fichier ZIP de l’extension.

get_resources(name_or_list_of_names)

Cette fonction devra être appelée avec une liste de chemins vers les fichiers à l’intérieur du fichier ZIP. Par exemple pour accéder au fichier icon.png dans le dossier images dans le fichier ZIP, vous devriez utiliser : images/icon.png. Toujours utiliser une barre oblique comme séparateur de chemin, même sous Windows. Quand vous analysez un nom seul, la fonction retournera les octets bruts ou Aucun si le nom n’est pas trouvé dans le fichier ZIP. Si vous analysez plus d’un nom alors il renvoie un dictionnaire mappant les noms aux octets. Si un nom n’est pas trouvé, il ne sera pas présent dans le dictionnaire retourné.

get_icons(name_or_list_of_names, plugin_name=””)

Un enveloppeur pour get_resources() qui crée des objets QIcon à partir des octets bruts retournés par get_resources. Si un nom n’est pas trouvé dans le fichier ZIP, le QIcon correspondant sera nul. Afin de supporter les thèmes d’icônes, entrez le nom convivial de votre plugin comme plugin_name. Si l’utilisateur utilise un thème d’icônes avec des icônes pour votre plugin, ils seront chargés de préférence.

Activation de la configuration utilisateur pour votre extension

Pour permettre aux utilisateurs de configurer votre extension, vous devez définir trois méthodes dans votre classe de base de l’extension, is_customizable, config_widget et save_settings comme montré ci-dessous :

    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 a beaucoup de manières différentes pour stocker les données de configuration (un héritage de sa longue histoire). La manière recommandée est d’utiliser la classe JSONConfig, qui stocke votre information de configuration dans un fichier .json.

Le code pour gérer le données de configuration dans l’extension démo est dans 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()

L’objet prefs est maintenant disponible dans tout le code de l’extension par un simple:

from calibre_plugins.interface_demo.config import prefs

Vous pouvez voir l’objet prefs en cours d’utilisation dans main.py :

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

Extensions d’édition de livre

Maintenant changeons de direction pour un moment et regardons à la création d’une extension pour ajouter des outils à l’éditeur de livre calibre. L’extension est disponible ici : editor_demo_plugin.zip.

La première étape comme pour toutes les extensions est de créer le nom du fichier txt vide d’importation, comme décrit plus haut. Nous appellerons vle fichier plugin-import-name-editor_plugin_demo.txt.

Maintenant nous créons le fichier mandataire __init__.py qui contient les métadonnées à propos de l’extension – ses nom, auteur, version, etc.

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)

Une simple extension éditeur peut procurer de multiples outils chaque outil correspond à un seul bouton dans la barre d’outils et une entrée dans le menu Extensions dans l’éditeur. Celle-ci peut avoir des sous menus dans le cas où l’outil à de multiples actions liées.

Les outils doivent tous être définis dans le fichier main.py dans votre extension. Chaque outil est une classe qui hérite de la classe calibre.gui2.tweak_book.plugin.Tool. Regardons attentivement le main.py de l’extention démo, le code source est fortement commenté et devrait être explicite. Lisez les documents API de la classe calibre.gui2.tweak_book.plugin.Tool pour plus de détails.

main.py

Nous verrons ici la définition d’un outil simple qui multipliera toutes les tailles de police dans le livre par un nombre fourni par l’utilisateur? Cet outil démontre les différents concepts importants dont vous aurez besoin dans le développement de vos propres extensions, aussi vous devriez lire attentivement le code source (fortement commenté).

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

Décomposons main.py. Nous voyons qu’il définit une seul outil, nommé Magnify fonts. Cet outil demandera à l’utilisateur un nombre et multipliera toutes les tailles de police dans le livre par ce nombre.

La première chose importante est le nom d’outil que vous devez établir à une certaine chaîne relativement unique car elle sera employée comme clé pour cet outil.

L’entrée importante suivante est la calibre.gui2.tweak_book.plugin.Tool.create_action(). Cette méthode crée les objects QAction qui apparaissent dans la barre d’outils et le menu extensions. Elle assigne aussi, optionnellement, un raccourci clavier que l’utilisateur peut personnaliser. Le signal de déclenchement de QAction est relié à la méthode ask_user () qui demande à l’utilisateur le multiplicateur de taille de la police, et puis exécute le code de grossissement.

Le code de grossissement est bien commenté assez simple. Les choses les plus importantes à noter sont que vous obtenez une référence à la fenêtre de l’éditeur comme self.gui et l’éditeur Boss comme self.boss. Le Boss est l’objet qui contrôle l’interface utilisateur de l’éditeur. Il a beaucoup de méthodes utiles, elles sont documentées dans la classe calibre.gui2.tweak_book.boss.Boss.

Finalement, il y a self.current_container qui est ue référence au livre en cours d’édition comme un objet calibre.ebooks.oeb.polish.container.Container. Ceci représente le livre comme une collection de ses fichiers constituants HTML/CSS/image et a des méthodes commodes pour faire beaucoup de choses utiles. L’objet container et diverses fonctions de service utiles qui peuvent être réutilisées en votre code d’extension sont documentées dans Documentation API pour l’outil d’édition de livre numérique.

Ajout des traductions à votre extension

Vous pouvez avoir toutes les chaînes de l’interface utilisateur dans votre extension traduites et affichées dans n’importe quelle langue paramétrée pour l’interface utilisateur principale de calibre.

La première étape est de faire le tour du code source de votre extension et marquer toutes les chaînes visibles par l’utilisateur comme traduisible, en les entourant dans _(). Par exemple:

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

Utilisez alors un programme pour générer les fichiers .po à partir du code source de votre extension. Il devrait y avoir un fichier .po pour chaque langue dans laquelle vous voulez une traduction. Par exemple de.po pour l’allemand, fr.po pour le français et ainsi de suite. Vous pouvez utiliser le programme Poedit pour cela.

Envoyez ces fichiers .po à vos traducteurs. Une fois que vous les obtenez en retour, compilez les dans des fichiers .mo. Vous pouvez encore utilisez Poedit pour cela, ou juste faire:

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

Déposez les fichiers .mo dans le répertoire translations dans votre extension.

La dernière étape est de simplement appeler la fonction load_translations() en haut de vos fichiers .py d’extension. Pour des raisons de performance, vous devriez seulement appeler cette fonction dans les fichiers .py qui ont réellement des chaînes traduisibles. Aussi dans une Interface Utilisateur typique vous devriez l’appeler en haut de ui.py mais pas de __init__.py.

Vous pouvez tester les traductions de vos extensions en changeant la langue de l’interface utilisateur dans calibre dans Préférences → Interface → Apparence ou en exécutant calibre avec la variable d’environnement CALIBRE_OVERRIDE_LANG. Par exemple :

CALIBRE_OVERRIDE_LANG=de

Remplac``de`` avec le code de langue de la langue que vous voulez tester.

For translations with plurals, use the ngettext() function instead of _(). For example:

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

L’API extension

Comme vous avez pu le constatez plus haut, une extension dans calibre est une classe. Il a différentes classes pour les différents types d’extension dans calibre. Les détails sur chaque classe, incluant la classe de base de toutes les extensions peuvent être trouvés dans Documentation API pour les extensions.

Votre extension va presque certainement employer le code de calibre. Pour apprendre comment trouver les diverses parties de fonctionnalités dans le code de base de calibre, lisez la section sur le Disposition du code calibre.

Dépannage des extensions

Tout d’abord, l’étape la plus importante est d’exécuter calibre en mode dépannage. Vous pouvez faire cela depuis la ligne de commande avec:

calibre-debug -g

Ou, dans calibre, en faisant un clic droit sur le bouton Préférences ou en utilisant le raccourci clavier Ctrl+Shift+R.

Lors de l’exécution en ligne de commande, la sortie de dépannage sera affichée dans la console, lors de l’exécution depuis calibre la sortie ira dans un fichier txt.

Vous pouvez insérer des déclarations n’importe où dans votre code d’extension, elles seront reprises dans le mode dépannage. Rappelez-vous, ceci est du Python, vous ne devriez n’avoir vraiment besoin de rien de plus que les déclarations pour corriger ;) J’ai développé entièrement calibre en utilisant juste cette technique d’élimination des imperfections.

Vous pouvez rapidement tester les changements de votre extension en utilisant la ligne de commande suivante:

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

Ceci arrêtera un calibre en cours d’exécution, attendez que l’arrêt soit complet, mettez alors à jour votre extension à jour dans calibre et relancez calibre.

Plus d’exemples d’extensions

Vous pouvez trouver une liste de nombreuses extensions calibre sophistiquées `ici<https://www.mobileread.com/forums/showthread.php?t=118764>`_.

Partager vos extensions avec les autres

Si vous aimeriez partager les extensions que vous avez créées avec d’autres utilisateurs de calibre, postez votre extension dans un nouveau fil de discussion sur le forum des extensions calibre.