Psaní vlastních modulů pro rozšíření funkcí Calibre

Calibre má velice modulární provedení. Téměř všechny funkce jsou ve formě modulů. Moduly jsou použity pro převod, stahování zpráv (ačkoliv ty jsou nazvány předpisy), různé komponenty uživatelského rozhraní, připojení k různým zařízením, zpracování souborů při jejich přidávání do Calibre a tak dále. Kompletní seznam všech vestavěných modulů můžete získat v |calibre| v Předvolby->Moduly.

Zde vás naučíme, jak vytvořit vlastní moduly pro přidání nových funkcí do Calibre.

Poznámka

Toto platí pouze pro vydání Calibre >= 0.8.60

Anatomie modulu Calibre

Modul Calibre je velice jednoduchý, je to prostě soubor zip, který obsahuje nějaký kód Pythonu a jakékoliv jiné zdroje, jako soubory obrázků vyžadované modulem. Bez dalších okolků se pojďme podívat na základní příklad.

Předpokládejme, že máte instalaci Calibre, kterou používáte pro vlastní publikaci různých elektronických dokumentů ve formátech EPUB a MOBI. Chtěli byste, aby všechny soubory generované v Calibre měli vydavatele nastaveného jako “Hello World”, zde je návod, jak to udělat. Vytvořte soubor s názvem __init__.py (toto je speciální název a musí být vždy použit pro hlavní soubor vašeho modulu) a zadejte do něj následující kód Pythonu:

import os
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


To je vše. Pro přidání tohoto kódu do Calibre jako modulu stačí spustit následující v adresáři, ve kterém jste vytvořili __init__.py:

calibre-customize -b .

Poznámka

Na OS X jsou nástroje příkazového řádku uvnitř sady calibre, například pokud jste nainstalovali Calibre do /Applications, nástroje příkazového řádku jsou v /Applications/calibre.app/Contents/console.app/Contents/MacOS/.

Modul Hello World můžete stáhnout z helloworld_plugin.zip.

Pokaždé, když použijete Calibre, aby převedlo knihu, bude zavolána metoda run() modulu a převedená kniha bude mít vydavatele nastaveného na “Hello World”. Toto je jednoduchý modul, pojďme se přesunout na složitější příklad, který skutečně přidá komponentu do uživatelského rozhraní.

Modul uživatelského rozhraní

Tento modul bude rozdělen do několika souborů (aby kód zůstal čistý). Ukáže vám, jak získat zdroje (soubory obrázků nebo dat) ze souboru zip modulu, což umožňuje uživatelům konfigurovat váš modul, jak vytvořit prvky v uživatelském rozhraní Calibre a jak přistupovat k a dotazovat se databáze knih v Calibre.

Tento modul můžete stáhnout z interface_demo_plugin.zip.

První věc, na kterou pamatujte, je to, že tento soubor zip má v sobě mnohem více souborů, vysvětleno níže, věnujte zvláštní pozornost plugin-import-name-interface_demo.txt.

plugin-import-name-interface_demo.txt

Prázdný textový soubor použitý pro umožnění kouzla vícesouborového modulu. Tento soubor musí být přítomen ve všech modulech, které používají více než jeden soubor .py. Měl by být prázdný a jeho název souboru musí být ve formě: plugin-import-name-some_name.txt. Přítomnost tohoto souboru vám umožňuje importovat kód ze souborů .py přítomných uvnitř souboru zip pomocí příkazu, jako je:

from calibre_plugins.some_name.some_module import some_object

Předpona calibre_plugins musí být vždy přítomná. some_name pochází z názvu prázdného textového souboru. some_module odkazuje na soubor some_module.py uvnitř souboru zip. Pamatujte, že toto importování je právě tak výkonné jako obyčejné importy Pythonu. Uvnitř souboru zip můžete vytvářet balíčky a podbalíčky modulů .py, jako byste to dělali normálně (definováním __init__.py v každém podadresáři) a všechno by mělo prostě fungovat.

Název, který použijete pro some_name, zadá globální obor názvů sdílený všemi moduly, takže by měl být co nejjedinečnější. Ale nezapomeňte, že to musí být platný identifikátor Pythonu (pouze abecední znaky, číslice a podtržítko).

__init__.py

Jako předtím, soubor, který definuje třídu modulu

main.py

Tento soubor obsahuje skutečný kód, který dělá něco užitečného

ui.py

Tento soubor definuje součást rozhraní modulu

images/icon.png

Ikona pro tento modul

about.txt

Textový soubor s informacemi o modulu

translations

Složka obsahující soubory .mo s překlady uživatelského rozhraní vašeho modulu do různých jazyků. Podívejte se níže na podrobnosti.

Nyní se podívejme na kód.

__init__.py

Nejdříve povinný __init__.py pro definování metadat modulu:

# The class that all Interface Action plugin wrappers must inherit from
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()


Jediná funkce, která stojí za pozornost, je pole actual_plugin. Protože Calibre má jak příkazový řádek, tak grafická rozhraní, moduly grafického rozhraní, jako je tento, by v __init__.py neměly načítat žádné knihovny grafického rozhraní. Pole actual_plugin to dělá za vás tím, že řekne Calibre, že skutečný modul je třeba hledat v jiném souboru uvnitř vašeho archivu zip, který bude načten pouze v kontextu grafického rozhraní.

Nezapomeňte, že aby toto fungovalo, musíte mít ve svém souboru zip soubor plugin-import-name-some_name.txt, jak bylo probráno výše.

Také existuje pár metod pro povolení uživatelské konfigurace modulu. Ty jsou popsány níže.

ui.py

Nyní se podívejme na ui.py, který definuje skutečné grafické rozhraní modulu. Zdrojový kód je silně okomentovaný a měl by být sebevysvětlující:

# The class that all interface action plugins must inherit from
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

Skutečná logika pro implementaci dialogového okna rozhraní ukázkového modulu.

from PyQt5.Qt import QDialog, QVBoxLayout, QPushButton, QMessageBox, QLabel

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()).iteritems():
            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'])

Získání zdrojů ze souboru zip modulu

Systém načítání doplňků Calibre definuje pár vestavěných funkcí, které vám umožňují pohodlně získat soubory ze souboru zip modulu.

get_resources(name_or_list_of_names)

Tato funkce by měla být volána se seznamem cest k souborům uvnitř souboru zip. Například pro přístup k souboru icon.png v adresáři images v souboru zip byste použili: images/icon.png. Vždy používejte lomítko jako oddělovač cesty, dokonce i ve Windows. Při předání jednoho názvu vrátí funkce nezpracované bajty tohoto souboru nebo None, pokud název nebyl v souboru zip nalezen. Pokud předáte více než jeden název, pak vrací slovník mapující názvy na bajty. Pokud název není nalezen, nebude přítomen ve vráceném slovníku.

get_icons(name_or_list_of_names)

Pohodlný obal pro get_resources(), který vytvoří objekty QIcon z nezpracovaných bajtů vrácených get_resources. Pokud název není v souboru zip nalezen, odpovídající QIcon bude nulový.

Povolení uživatelské konfigurace vašeho modulu

To allow users to configure your plugin, you must define three methods in your base plugin class, is_customizable, config_widget and save_settings as shown below:

    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 má mnoho různých způsobů, jak ukládat konfigurační data (starší verze jeho dlouhé historie). Doporučený způsob je použít třídu JSONConfig, která ukládá vaše konfigurační informace v souboru .json.

Kód pro správu konfiguračních dat v ukázkovém modulu je v config.py:

from PyQt5.Qt import QWidget, QHBoxLayout, QLabel, QLineEdit

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'] = unicode(self.msg.text())

Object prefs je nyní dostupný v celém kódu modulu jednoduchým:

from calibre_plugins.interface_demo.config import prefs

Můžete vidět objekt prefs použitý v main.py:

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

Moduly pro úpravu knihy

Nyní pojďme na chvíli přeřadit a podívejme se na vytvoření modulu pro přidání nástrojů do editoru knih Calibre. Modul je dostupný zde: editor_demo_plugin.zip.

Prvním krokem, stejně jako pro všechny moduly, je vytvořit název importu prázdného souboru txt, jak je popsáno výše. Pojmenujeme soubor plugin-import-name-editor_plugin_demo.txt.

Nyní vytvoříme povinný soubor __init__.py, který obsahuje metadata o modulu – jeho název, autora, verzi atd.


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)

Jeden modul editoru může poskytnout více nástrojů, každý nástroj odpovídá jednomu tlačítku na panelu nástrojů a položce v nabídce :guilabel:Moduly` v editoru. Ty mohou mít podnabídky v případě, že nástroj má více souvisejících akcí.

Nástroje musí být všechny definovány v souboru main.py ve vašem modulu. Každý nástroj je třída, která dědí ze třídy calibre.gui2.tweak_book.plugin.Tool. Pojďme se podívat na main.py z ukázkového modulu, zdrojový kód je silně komentován a měl by být samovysvětlující. Pro více informací si přečtěte dokumenty API ke třídě calibre.gui2.tweak_book.plugin.Tool.

main.py

Zde uvidíme definici jednoho nástroje, který se bude násobit všechny velikosti písem v knize číslem zadaným uživatelem. Tento nástroj ukazuje různé důležité pojmy, které budete potřebovat při vývoji svých vlastních modulů, takže byste si měli pečlivě přečíst (silně komentovaný) zdrojový kód.


import re
from PyQt5.Qt import QAction, QInputDialog
from cssutils.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.iteritems():
            if media_type in OEB_STYLES:
                # A stylesheet. Parsed stylesheets are cssutils 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

Pojďme si rozebrat main.py. Vidíme, že definuje jeden nástroj s názvem Zvětšit písma. Tento nástroj požádá uživatele o číslo a vynásobí všechny velikosti písem v knize tímto číslem.

První důležitá věc je název nástroje, který musíte nastavit na nějaký relativně jedinečný řetězec, protože bude použit jako klíč pro tento nástroj.

Dalším důležitým vstupním bodem je calibre.gui2.tweak_book.plugin.Tool.create_action(). Tato metoda vytvoří objekty QAction, které se zobrazí v panelu nástrojů modulů a nabídce modulu. Také to volitelně přiřadí klávesovou zkratku, kterou si uživatel může přizpůsobit. Spuštěný signál z QAction je spojený s metodou ask_user(), která se zeptá uživatele na násobitel velikosti písma, a pak spustí kód zvětšení.

Kód zvětšení kód je dobře okomentován a poměrně jednoduchý. Hlavními věcmi, na které pamatujte, je to, že jste získali odkaz na okno editoru jako self.gui a Boss editoru jako self.boss. Boss je objekt, který řídí uživatelské rozhraní editoru. Má mnoho užitečných metod, které jsou popsány ve třídě calibre.gui2.tweak_book.boss.Boss.

Nakonec je zde self.current_container, což je odkaz na knihu upravovanou jako objekt calibre.ebooks.oeb.polish.container.Container. Znázorňuje knihu jako kolekci jejích základních souborů HTML, CSS nebo obrázků a má pohodlné metody pro provádění mnoha užitečných věcí. Objekt kontejneru a různé užitečné funkce nástrojů, které mohou být znovu použity v kódu vašeho modulu, jsou popsány v API Documentation for the ebook editing tools.

Přidávání překladů do vašeho modulu

Všechny řetězce uživatelského rozhraní ve svém modulu můžete mít přeloženy a zobrazeny v jakémkoliv jazyce, který je nastaven pro hlavní uživatelské rozhraní Calibre.

Prvním krokem je projít zdrojový kód svého modulu a označit všechny řetězce viditelné uživatelem jako přeložitelné jejich obklopením _(). Například:

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

Pak použijte nějaký program pro vygenerování souborů .po ze zdrojového kódu vašeho doplňku. Měl by být jeden soubor .po pro každý jazyk, do kterého chcete překládat. Například: cs.po pro češtinu, de.po pro němčinu, fr.po pro francouzštinu a tak dále. K tomu můžete použít program Poedit.

Tyto soubory .po pošlete svým překladatelům. Jakmile je dostanete zpátky, zkompilovat je do souborů .mo. K tomu můžete znovu použít Poedit, nebo prostě udělejte:

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

Soubory .mo umístěte do složky translations ve svém modulu.

Posledním krokem je jednoduše zavolat funkci load_translations() na začátku souborů .py vašeho modulu. Z výkonnostních důvodů byste měli volat tuto funkci pouze v těch souborech .py, které skutečně mají přeložitelné řetězce. Takže v typickém modulu uživatelského rozhraní byste ji volali na začátku ui.py ale ne __init__.py.

Překlady svých modulů můžete otestovat změnou jazyka uživatelského rozhraní v Calibre pod Předvolby->Vzhled nebo spuštěním Calibre takto:

CALIBRE_OVERRIDE_LANG=de calibre

Nahraďte de kódem jazyka, který chcete otestovat.

Rozhraní API modulu

Jak jste si možná všimli výše, modul Calibre je třída. V Calibre existují různé třídy pro různé typy modulů. Podrobnosti o každé třídě, včetně základní třídy všech modulů, lze nalézt v Dokumentace API pro doplňky.

Váš modul téměř určitě bude používat kód z Calibre. Abyste zjistili, jak najít různé kousky funkcí v základním kódu Calibre, přečtěte si část o Rozložení kódu Calibre.

Ladění modulů

Prvním a nejdůležitějším krokem je spuštění Calibre v režimu ladění. To můžete udělat z příkazového řádku pomocí:

calibre-debug -g

Nebo z Calibre kliknutím pravým tlačítkem myši na tlačítko Předvolby nebo pomocí klávesové zkratky Ctrl+Shift+R.

Při spuštění z příkazového řádku, bude výstup ladění vypsán do konzole, při spuštění z Calibre, půjde výstup do textového souboru.

Do kódu svého modulu můžete kamkoliv vložit příkazy tisku, budou mít výstup v režimu ladění. Nezapomeňte, že je to Python, neměli byste opravdu potřebovat nic víc než příkazy tisku pro ladění ;) Celé Celibre jsem vytvořil pouze pomocí této techniky ladění.

Změny ve svém modulu můžete rychle testovat pomocí následujícího příkazového řádku:

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

Toto vypne spuštěné Calibre, počkejte až bude vypnutí dokončeno, a pak aktualizujte svůj modul v Calibre a znovu spusťte Calibre.

Další příklady modulů

Seznam mnoha propracovaných modulů Calibre můžete nalézt zde.

Sdílení vašeho modulu s ostatními

Pokud byste chtěli sdílet moduly, které jste vytvořili, s ostatními uživateli Calibre, zveřejněte svůj modul v novém vláknu ve fóru modulů Calibre.