Uw eigen plugins schrijven om de functionaliteit van Calibre uit te breiden

Calibre heeft een zeer modulair ontwerp. Bijna alle functionaliteit in Calibre komt in de vorm van plug-ins. Plug-ins worden gebruikt voor conversie, voor het downloaden van nieuws (hoewel dit recepten worden genoemd), voor verschillende componenten van de gebruikersinterface, om verbinding te maken met verschillende apparaten, om bestanden te verwerken wanneer ze worden toegevoegd aan Calibre enzovoort. U kunt een volledige lijst van alle ingebouwde plug-ins in kaliber krijgen door naar: guilabel: `Voorkeuren-> Geavanceerd-> Plug-ins ‘te gaan.

Hier laten we zien hoe u uw eigen plugins kunt maken om nieuwe functies toe te voegen aan Calibre.

Notitie

Dit is enkel van toepassing op Calibre releases >= 0.8.60

Opbouw van een Calibre plugin

Een Calibre plug-in is heel eenvoudig, het is gewoon een ZIP-bestand dat een Python-code bevat en andere bronnen zoals afbeeldingsbestanden die de plug-in nodig heeft. Zonder meer, laten we een eenvoudig voorbeeld bekijken.

Aangenomen u heeft Calibre geïnstalleerd om diverse e-documenten zelf te publiceren in EPUB en MOBI-formaten. U wilt alle bestanden genereren in Calibre en als uitgever als “Hello world”, hier volgt hoe u dit kunt doen. Maak een bestand aan genaamd __init__.py (dit is een speciale naa en moet altijd gebruikt worden voor het hoofdbestand van uw plugin) en geef de volgende Python code in:

from calibre.customize import FileTypePlugin

class HelloWorld(FileTypePlugin):

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

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


Dat is alles. Om deze code toe te voegen aan Calibre als een pluging, laat eenvoudig het volgende lopen in de map waarin u het bestand __init__.py heeft gemaakt:

calibre-customize -b .

Notitie

Op macOS bevinden de commandoregelprogramma’s zich in de calibre-bundel, bijvoorbeeld, als u calibre hebt geïnstalleerd in /Applications bevinden de commandoregelprogramma’s zich in /Applications/calibre.app/Contents/MacOS/.

U kunt de Hello World plugin downloaden van helloworld_plugin.zip.

Elke keer als u Calibre gebruikt om een boek te converteren, wordt plugin run() methode opgeroepen en het geconverteerde boek heeft als uitgever “Hello World”. Dit is een onbenullig plugin. Laten we verdergaan naar een ingewikkelder voorbeeld dat daadwerkelijk een component toevoegt aan het gebruikersinterface.

Een gebruikersinterface plugin

Deze plug-in zal verspreid zijn over een paar bestanden (om de code schoon te houden). Het toont u hoe u bronnen (afbeeldingen of gegevensbestanden) kunt ophalen uit het ZIP-bestand van de plug-in, gebruikers toestaan uw plug-in in te stellen, hoe elementen in de gebruikersinterface voor Calibre te maken en hoe u de gegevensbank voor boeken inkomt en raadpleegt in Calibre.

U kunt dit plugin downloaden op interface_demo_plugin.zip

Het eerste ding om op te merken is dat dit ZIP-bestand veel meer bestanden bevat, zoals hieronder uitgelegd, besteed bijzondere aandacht aan``plugin-import-name-interface_demo.txt``.

plugin-import-name-interface_demo.txt

Een leeg tekstbestand dat wordt gebruikt om de multi-file plugin magie in te schakelen. Dit bestand moet aanwezig zijn in alle plug-ins die meer dan één .py-bestand gebruiken. Het moet leeg zijn en de bestandsnaam moet de volgende vorm hebben: `` plugin-import-naam-**een_naam **.Txt``. Met de aanwezigheid van dit bestand kunt u code importeren uit de .py-bestanden die in het ZIP-bestand aanwezig zijn, met behulp van een statement als:

from calibre_plugins.some_name.some_module import some_object

Het voorvoegsel calibre_plugins moet altijd aanwezig zijn. `` een_naam`` komt van de bestandsnaam van het lege tekstbestand. `` een_module`` verwijst naar: bestand: een_module.py bestand in het ZIP-bestand. Merk op dat dit importeren net zo krachtig is als normale invoer van Python. U kunt pakketten en subpakketten van .py-modules in het ZIP-bestand maken, net zoals u dat normaal zou doen (door __init__.py in elke submap te definiëren) en alles zou “gewoon moeten werken”.

De naam die u gebruikt voor een_name voert een algemene naamruimte in die door alle plug-ins wordt gedeeld, dus maak deze zo uniek mogelijk. Maar vergeet niet dat het een geldige Python-id moet zijn (alleen alfabetten, cijfers en het onderstrepingsteken).

__init__.py

Zoals hiervoor, dit bestand definieërd de klasse van de plugin

main.py

Dit bestand bevat de daadwerkelijke code die iets nuttigs doet

ui.py

Dit bestand definieërd de interface van de plugin

images/icon.png

Het icoon voor deze plugin

about.txt

Een tekstbestand met informatie over uw plugin

vertalingen

Een map die .mo-bestanden bevat met de vertalingen van het gebruikersinterface van uw plugin in verschillende talen. Zie hieronder voor details.

Laten we naar de code kijken

__init__.py

Eerst de verplichte __init__.py om de metagegevens voor de plugin te definiëren:

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


De enige noemenswaardige eigenschap is het veld: attr:actual_plugin. Omdat Calibre zowel opdrachtregel- als GUI-interfaces heeft, mogen GUI-plug-ins zoals deze geen enkele GUI-bibliotheek in __init__.py laden. Het veld actual_plugin doet dit voor u, door Calibre te zeggen dat de daadwerkelijke plugin in een ander bestand in uw ZIP-archief te vinden is, dat alleen in een GUI-context wordt geladen.

Vergeet niet dat om dit te laten werken, je een plugin-import-naam-een_naam.txt bestand nodig hebt in je ZIP-plugin-plug-in, zoals hierboven besproken.

Er zijn meerdere methoden om gebruikers-configuratie van de plugin mogelijk te maken. Deze worden hieronder besproken.

ui.py

Laten we nu ul.py bekijken dat de eigenlijke GUI plugin definiëert. De broncode is voorzien van commentaar en zou zichzelf moeten verklaren:

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

class InterfacePlugin(InterfaceAction):

    name = 'Interface Plugin Demo'

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

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

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

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

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

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

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

main.py

De eigenlijke opzet om het Interface Plugin Demo dialoogvenster te implementeren.


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

Hulpbronnen ophalen uit het ZIP-bestand voor de plug-in

Calibre plugin laadsysteem definieert een aantal ingebouwde functies waarmee u gemakkelijk bestanden kunt ophalen uit het plug-in ZIP bestand.

get_resources(name_or_list_of_names)

Deze functie moet worden aangeroepen met een lijst met paden naar bestanden in het ZIP-bestand. Als u bijvoorbeeld het bestand icon.png wilt openen in de map afbeeldingen in het ZIP-bestand, gebruikt u: afbeeldingen/icon.png. Gebruik altijd een schuine streep als pad scheider, zelfs in Windows. Wanneer u één enkele naam doorgeeft, retourneert de functie de onbewerkte bytes van dat bestand of None als de naam niet is gevonden in het ZIP-bestand. Als u meer dan één naam doorgeeft, retourneert het een dictaat dat de namen toewijst aan bytes. Als een naam niet wordt gevonden, is deze niet aanwezig in het geretourneerde dictaat.

get_icons(name_or_list_of_names)

Een handige wrapper voor get_resources() die QIcon-objecten maakt van de onbewerkte bytes die worden geretourneerd door get_resources. Als een naam niet in het ZIP-bestand wordt gevonden, is de bijbehorende QIcon nul.

Gebruikers-configuratie van uw plugin toevoegen

Om gebruikers in staat te stellen uw plug-in te configureren, moet u drie methoden in uw basis-plugin-klasse definiëren, ** is_customizable **, ** config_widget ** en ** save_settings ** zoals hieronder weergegeven:

    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 heeft vele manieren om configuratie-gegevens te bewaren (een erfenis van vroeger). De aanbevolen manier is het gebruik van de JSONConfig class, welke de informatie bewaard in een .json-bestand.

De code om configuratie-gegevens te beheren in de demo-plugin is in 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()

Het prefs-object is nu beschikbaar in de plugincode met een simpele:

from calibre_plugins.interface_demo.config import prefs

U kunt het gebruik van prefs-object zien in main.py:

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

Boek-invoegtoepassingen bewerken

Laten we in een hogere versnelling schakelen en het maken van een plugin bekijken dat gereedschappen toevoegt aan Calibre’s boek-editor. De plugin is hier beschikbaar: editor_demo_plugin.zip.

De eerste stap zoals voor alle plugins is het maken van het lege import name txt-bestand zoals boven beschreven. We noemen het bestand plugin-import-name-editor_plugin_demo.txt.

Nu maken we het verplichte bestand __init__.py waarin de metagegevens over de plugin staan – zijn naam, maker, versie enz.

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)

Een enkel editor-plugin kan meerdere gereedschappen leveren, ieder gereedschap correspondeert met een enkele knop in de werkbalk en trefwoord in het Plugins-menu in de editor. Deze kunnen sub-menu’s hebben indien het gereedschap meerdere acties heeft.

De gereedschappen moeten allemaal gedefiniëerd zijn in het bestand main.py in uw plugin. Ieder gereedschap in een class, dat erft van de Calibre.gui2.tweak_book.plugin.Tool class. Laten we een kijkje nemen in main.py van de demo-plugin, de broncode is voorzien van commentaar en zou zichzelf moeten verklaren. Lees de API-documenten van de Calibre.gui2.tweak_book.plugin.Tool class voor meer details.

main.py

Hier zien we de definitie van een enkel gereedschap dat alle lettergroottes in het boek multipliceert met een getal ingesteld door de gebruiker. Dit gereedschap demonstreert meerdere belangrijke concepten die u nodig heeft omuw eigen plugin te ontwikkelen. Lees daarom de (van commentaar voorziene) broncode grondig.

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

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

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


class DemoTool(Tool):

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

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

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

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

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

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

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

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

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

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

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

Laten we main.py in onderdelen bekijken. We zien dat een enkel gereedschap wordt gefeniniëerd genoemd Magnify fonts - letters vergroten. Dit gereedschap vraagt aan de gebruiker om een getal in te geven en vermenigvuldigt alle lettergroottes in het boek met dit getal.

Het eerste belangrijke ding is de naam van het gereedschap die u moet bepalen op een relatief unieke tekenreeks omdat deze wordt gebruikt als toets voor dit gereedschap.

Het volgende belangrijke toegangspunt is calibre.gui2.tweak_book.plugin.Tool.create_action(). Deze methode maakt de QAction-objecten die op de plug-inwerkbalk en het plug-inmenu verschijnen. Optioneel wordt ook een sneltoets toegewezen die de gebruiker kan aanpassen. Het getriggerde signaal van de QAction is verbonden met de ask_user() methode die de gebruiker vraagt naar de vergrotingsfactor voor de lettergrootte en voert vervolgens de vergrotingscode uit.

De vergrotingscode is goed becommentarieerd en vrij eenvoudig. De belangrijkste dingen om op te merken zijn dat u een verwijzing naar het editorvenster krijgt als self.gui en de editor Boss als self.boss. De Boss is het object dat de gebruikersinterface van de editor bestuurt. Het heeft veel nuttige methoden, die zijn gedocumenteerd in de klasse calibre.gui2.tweak_book.boss.Boss.

Ten slotte is er ``self.current_container’’, een verwijzing naar het boek dat wordt bewerkt als calibre.ebooks.oeb.polish.container.Container object. Dit vertegenwoordigt het boek als een verzameling van de samenstellende HTML/CSS/afbeeldingsbestanden en heeft handige methoden om veel nuttige dingen te doen. Het containerobject en verschillende handige hulpprogramma’s die kunnen worden hergebruikt in uw plug-incode zijn gedocumenteerd in :ref: polish_api.

Plug-ins voor gebruikersinterface uitvoeren in een afzonderlijk proces

Als u een plug-in voor een gebruikersinterface schrijft die gebruik moet maken van Qt WebEngine, kan deze niet worden uitgevoerd in het hoofdproces omdat het daar niet mogelijk is om WebEngine te gebruiken. In plaats daarvan kunt u de gegevens die uw plug-in nodig heeft naar een tijdelijke map kopiëren en de plug-in met die gegevens in een afzonderlijk proces uitvoeren. Een eenvoudige voorbeeldplugin volgt die laat zien hoe dit te doen.

U kunt de plug-in downloaden van webengine_demo_plugin.zip.

Het belangrijke deel van de plug-in bestaat uit twee functies:


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

De functie show_demo() vraagt de gebruiker om een URL en voert vervolgens de functie main ()'' met deze URL. De functie ``main() geeft de URL weer in een ``QWebEngineView’.

Vertalingen toevoegen aan uw plugin

U kunt de teksten in het gebruikersinterface in uw plugin laten vertalen in elke taal die is ingesteld voor het Calibre gebruikersinterface.

De eerste stap in de broncodes in uw plugin te doorzoeken naar voor gebruikers zichtbare teksten te doorzoeken en deze te markeren als vertaalbaar door ze te omgeven met _(). Bijvoorbeeld:

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

Gebruik vervolgens een programma om .po-bestanden te genereren uit de broncode van uw plug-in. Er moet één .po-bestand zijn voor elke taal waarin u wilt vertalen. Bijvoorbeeld: nl.po voor Nederlands, fr.po voor Frans enzovoort. U kunt hiervoor het `Poedit <https://poedit.net/>`_program gebruiken.

Stuur deze .po-bestanden naar uw vertalers. Zodra je ze terug krijgt, compileer ze in .mo-bestanden. Je kunt Poedit daarvoor opnieuw gebruiken, of gewoon doen:

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

Plaats de .mo-bestanden in de vertalingen-map in uw plugin.

De laatste stap is om eenvoudig de functie load_translations() op te roepen bovenin uw .py-bestanden in uw plugin. Om de prestatie niet te vertragen moet u deze functie alleen aanroepen in die .py-bestanden die daadwerkelijk vertaalbare teksten bevatten. Dus in een normaal Gebruikersinterface plugin roept u deze functie wel aan bovenin ui.py maar niet in __init__.py.

U kunt de vertalingen van uw plug-ins testen door de taal van de gebruikersinterface in calibre te wijzigen onder Preferences → Interface → Look & feel of door calibre als volgt uit te voeren:

CALIBRE_OVERRIDE_LANG=de calibre

Vervang de met de taalcode van de taal die u wilt testen.

De plugin API

Zoals u misschien heeft opgemerkt is een plugin in Calibre een class. Er zijn verschillende classes voor verschillende types plugin. Details van iedere class, inclusief de basis class van alle plugins zijn te vinden in API documentatie voor plugins.

Uw plugin zal bijna zeker code gebruiken van Calibre. Om te leren hoe u diverse stukjes functionaliteit vindt in de Calibre code basis, lees de sectie Code opmaak.

Plugins debuggen

De eerste en belangrijkste stap is het laten werken van Calibre in foutenopsporingsmodus. U kunt dit doen in de opdrachtregel met:

calibre-debug -g

Of vanuit calibre door met de rechtermuisknop op de Preferences knop te klikken of de Ctrl+Shift+R sneltoets te gebruiken.

Wanneer u werkt met de opdrachtregel verschijnt de foutoplossen op de console, wanneer u werkt in calbre zal dit worden geschreven naar een .txt-bestand.

U kunt overal printinstructies invoegen in uw plug-incode, deze worden uitgevoerd in de foutopsporingsmodus. Vergeet niet, dit is Python, je zou echt niets meer nodig moeten hebben dan print commando’s om te debuggen ;) Ik heb heel calibre ontwikkeld met alleen deze debugtechniek.

U kunt snel wijzigingen aan uw plugin testen met behulp van de volgende opdrachtregel:

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

Dit sluit een lopende Calibre af, wacht totdat het programma geheel afgesloten is, dan uw plugin updaten in Calibre en Calibre herstarten.

Meer voorbeelden van plugins

Je kan een lijst vinden van vele, gesoftikeerde plugins hier terug vinden: here.

Uw plugins delen met anderen

Als je wenst de plugins te delen die je gemaakt hebt, met andere gebruiks van Calibre, maak een nieuw onderwerp aan in Calibre plugins forum.