Skriv dina egna insticksmoduler för att utöka calibres funktionalitet

calibre är en väldigt modulär konstruktion. Nästan alla funktioner i calibre kommer i form av insticksmoduler. Insticksmoduler används för konvertering, för hämtning av nyheter (även om dessa kallas för recept), för diverse komponenter av användargränssnitt, för anslutning av olika enheter, för bearbetning av filer när de läggs till calibre och så vidare. Du kan få en komplett lista med alla inbyggda insticksmoduler i calibre genom att gå till Inställningar → Avancerat → Insticksmoduler.

Här kommer vi att lära dig att skapa dina egna insticksmoduler för att lägga till nya funktioner i calibre.

Observera

Detta gäller bara calibre-utgåvor >= 0.8.60

Anatomi av en calibre-insticksmodul

En calibre-insticksmodul är väldigt enkel, det är bara en ZIP-fil som innehåller lite Python-kod och andra resurser som bildfiler som behövs av insticksmodulen. Utan vidare, låt oss se ett grundläggande exempel.

Anta att du har en installation av calibre som du använder för att själv publicera olika e-dokument i EPUB- och MOBI-format. Du vill att alla filer som skapas av calibre har förläggaren inställd som ”Hej världen”, här är hur man gör det. Skapa en fil med namnet __init __ py (detta är ett speciellt namn och ska alltid användas för huvudfilen för din insticksmodul) och ange följande Python-kod i den:


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

Det är allt. För att lägga till den här koden i calibre som en insticksmodul, kör helt enkelt följande i mappen där du skapade __init__.py:

calibre-customize -b .

Observera

På macOS finns kommandoradsverktygen inuti calibre paketet, till exempel om du installerade calibre i /Applications kommandoradsverktygen finns i /Applications/calibre.app/Contents/MacOS/.

Du kan hämta insticksmodulen Hello World från helloworld_plugin.zip.

Varje gång du använder calibre att konvertera en bok, kommer insticksmodulens run() metod att anropas och den konverterade boken kommer att ha sin förläggare satt till ”Hello World”. Denna är en trivial insticksmodul, låt oss gå vidare till ett mer komplext exempel som faktiskt lägger till en komponent till användargränssnittet.

En användargränssnitts insticksmodul

Denna insticksmodul kommer att spridas över några filer (för att hålla koden ren). Den kommer att visa dig hur du får resurser (bilder eller datafiler) från insticksmodulens ZIP-fil, tillåter användare att anpassa din insticksmodul, hur man skapar element i calibre-användargränssnittet och hur man får åtkomst till och söker i bokdatabasen i calibre.

Du kan hämta denna insticksmodul från interface_demo_plugin.zip

Det första att observera är att denna ZIP-filen har många fler filer i sig, som förklaras nedan, ta särskild hänsyn till plugin-import-name-interface_demo.txt.

plugin-import-name-interface_demo.txt

En tom textfil som används för att aktivera insticksmodulsmagin för flera filer. Den här filen måste finnas i alla insticksmoduler som använder mer än en .py-fil. Den ska vara tom och filnamnet måste ha formen: plugin-import-name-**some_name**.txt. Förekomsten av den här filen låter dig importera kod från .py-filerna som finns i ZIP-filen med en sats som:

from calibre_plugins.some_name.some_module import some_object

Prefixet calibre_plugins måste alltid vara närvarande. some_name kommer från filnamnet på den tomma textfilen. some_module avser some_module.py fil inuti ZIP-filen. Observera att denna import är lika kraftfull som vanlig Python importering. Du kan skapa paket och underpaket för .py moduler inuti ZIP filen, precis som du normalt skulle (genom att definiera __init__.py i varje undermapp), och allt ska bara fungera.

Namnet du använda för some_name går in i en globala namnrymden delas av alla insticksmodul, så gör det så unikt som möjligt. Men kom ihåg att det måste vara en giltigt Python-identifierare (endast alfabet, siffror och understreck).

__init__.py

Som tidigare, filen som definierar insticksmodulsklassen

main.py

Den här filen innehåller den faktiska koden som gör något användbart

ui.py

Denna fil definierar gränssnittsdelen av insticksmodulen

bilder/ikon.png

Ikonen för denna insticksmodul

about.txt

En textfil med information om insticksmodulen

översättningar

En mapp innehåller .mo-filer med översättningar av användargränssnittet av din insticksmodul till olika språk. Se nedan för detaljer.

Låt oss nu titta på koden.

__init__.py

Först det obligatoriska __init__.py för att definiera insticksmodulens metadata:

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


Det enda anmärkningsvärda funktionen är fältet actual_plugin. Eftersom calibre har både kommandorad och användargränssnitt bör användargränssnittsinsticksmoduler som denna inte läsa in några biblioteksanvändargränssnitt i __init__.py. Fältet actual_plugin gör detta åt dig, genom att berätta för calibre att den egentliga insticksmodulen finns i en annan fil i ditt ZIP-arkiv, som endast kommer att läsas in i ett användargränssnittssammanhang.

Kom ihåg att för att detta ska fungera måste du ha en plugin-import-name-some_name.txt fil i din insticksmoduls ZIP-fil, som diskuterats ovan.

Också finns det ett par olika metoder för att möjliggöra användarkonfiguration av insticksmodulen. Dessa diskuteras nedan.

ui.py

Låt oss nu titta på ui.py som definierar själva användargränssnitts insticksmodul. Källkoden är kraftigt kommenterade och bör vara självförklarande:

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

Den faktiska logiken för att implementera dialogrutan insticksmodulsdemo för gränssnittet.

from qt.core import QDialog, QLabel, QMessageBox, QPushButton, QVBoxLayout


class DemoDialog(QDialog):

    def __init__(self, gui, icon, do_user_config):
        QDialog.__init__(self, gui)
        self.gui = gui
        self.do_user_config = do_user_config

        # The current database shown in the GUI
        # db is an instance of the class LibraryDatabase from db/legacy.py
        # This class has many, many methods that allow you to do a lot of
        # things. For most purposes you should use db.new_api, which has
        # a much nicer interface from db/cache.py
        self.db = gui.current_db

        self.l = QVBoxLayout()
        self.setLayout(self.l)

        self.label = QLabel(prefs['hello_world_msg'])
        self.l.addWidget(self.label)

        self.setWindowTitle('Interface Plugin Demo')
        self.setWindowIcon(icon)

        self.about_button = QPushButton('About', self)
        self.about_button.clicked.connect(self.about)
        self.l.addWidget(self.about_button)

        self.marked_button = QPushButton(
            'Show books with only one format in the calibre GUI', self)
        self.marked_button.clicked.connect(self.marked)
        self.l.addWidget(self.marked_button)

        self.view_button = QPushButton(
            'View the most recently added book', self)
        self.view_button.clicked.connect(self.view)
        self.l.addWidget(self.view_button)

        self.update_metadata_button = QPushButton(
            'Update metadata in a book\'s files', self)
        self.update_metadata_button.clicked.connect(self.update_metadata)
        self.l.addWidget(self.update_metadata_button)

        self.conf_button = QPushButton(
                'Configure this plugin', self)
        self.conf_button.clicked.connect(self.config)
        self.l.addWidget(self.conf_button)

        self.resize(self.sizeHint())

    def about(self):
        # Get the about text from a file inside the plugin zip file
        # The get_resources function is a builtin function defined for all your
        # plugin code. It loads files from the plugin zip file. It returns
        # the bytes from the specified file.
        #
        # Note that if you are loading more than one file, for performance, you
        # should pass a list of names to get_resources. In this case,
        # get_resources will return a dictionary mapping names to bytes. Names that
        # are not found in the zip file will not be in the returned dictionary.
        text = get_resources('about.txt')
        QMessageBox.about(self, 'About the Interface Plugin Demo',
                text.decode('utf-8'))

    def marked(self):
        ''' Show books with only one format '''
        db = self.db.new_api
        matched_ids = {book_id for book_id in db.all_book_ids() if len(db.formats(book_id)) == 1}
        # Mark the records with the matching ids
        # new_api does not know anything about marked books, so we use the full
        # db object
        self.db.set_marked_ids(matched_ids)

        # Tell the GUI to search for all marked records
        self.gui.search.setEditText('marked:true')
        self.gui.search.do_search()

    def view(self):
        ''' View the most recently added book '''
        most_recent = most_recent_id = None
        db = self.db.new_api
        for book_id, timestamp in db.all_field_for('timestamp', db.all_book_ids()).items():
            if most_recent is None or timestamp > most_recent:
                most_recent = timestamp
                most_recent_id = book_id

        if most_recent_id is not None:
            # Get a reference to the View plugin
            view_plugin = self.gui.iactions['View']
            # Ask the view plugin to launch the viewer for row_number
            view_plugin._view_calibre_books([most_recent_id])

    def update_metadata(self):
        '''
        Set the metadata in the files in the selected book's record to
        match the current metadata in the database.
        '''
        from calibre.ebooks.metadata.meta import set_metadata
        from calibre.gui2 import error_dialog, info_dialog

        # Get currently selected books
        rows = self.gui.library_view.selectionModel().selectedRows()
        if not rows or len(rows) == 0:
            return error_dialog(self.gui, 'Cannot update metadata',
                             'No books selected', show=True)
        # Map the rows to book ids
        ids = list(map(self.gui.library_view.model().id, rows))
        db = self.db.new_api
        for book_id in ids:
            # Get the current metadata for this book from the db
            mi = db.get_metadata(book_id, get_cover=True, cover_as_data=True)
            fmts = db.formats(book_id)
            if not fmts:
                continue
            for fmt in fmts:
                fmt = fmt.lower()
                # Get a python file object for the format. This will be either
                # an in memory file or a temporary on disk file
                ffile = db.format(book_id, fmt, as_file=True)
                ffile.seek(0)
                # Set metadata in the format
                set_metadata(ffile, mi, fmt)
                ffile.seek(0)
                # Now replace the file in the calibre library with the updated
                # file. We dont use add_format_with_hooks as the hooks were
                # already run when the file was first added to calibre.
                db.add_format(book_id, fmt, ffile, run_hooks=False)

        info_dialog(self, 'Updated files',
                'Updated the metadata in the files of %d book(s)'%len(ids),
                show=True)

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

Få resurser från insticksmodulens ZIP-fil

calibres insticksmodulsladdningssystem definierar ett par inbyggda funktioner som gör att du bekvämt kan hämta filer från insticksmodulens ZIP-fil.

get_resources(name_or_list_of_names)

Den här funktionen bör anropas med en lista av sökvägar till filer i ZIP-filen. Till exempel för att komma åt filen icon.png i mappbilderna i ZIP-filen, skulle du använda: images/icon.png. Använd alltid ett framåt snedstreck som sökvägsavskiljare, även på Windows. När du anger ett enda namn kommer funktionen att returnera råa byte för filen eller None om namnet inte hittades i ZIP-filen. Om du anger mer än ett namn så returneras en ordbok som mappar namnen till byte. Om ett namn inte hittas kommer det inte att finnas i den returnerade ordboken.

get_icons(name_or_list_of_names, plugin_name=’’)

En wrapper för get_resources() som skapar QIcon-objekt från de råbyte som returneras av get_resources. Om ett namn inte hittas i ZIP-filen kommer motsvarande QIcon att vara null. För att stödja ikontema, skicka in det människovänliga namnet på din insticksmodul som plugin_name. Om användaren använder ett ikontema med ikoner för din insticksmodul, kommer de att läsas in med företräde.

Aktivera användarkonfiguration för din insticksmodul

För att tillåta användare att anpassa din insticksmodul måste du definiera tre metoder i din bas insticksmodulsklass, is_customizable, config_widget och save_settings enligt nedan:

    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 har många olika sätt att lagra konfigurationsdata (ett arv av sin långa historia). Det rekommenderade sättet är att använda JSONConfig klass, som lagrar din konfigurationsinformation i en .json-fil.

Koden för att hantera konfigurationsdata i demo-insticksmodulen finns i config.py:

from qt.core import QHBoxLayout, QLabel, QLineEdit, QWidget

# This is where all preferences for this plugin will be stored
# Remember that this name (i.e. plugins/interface_demo) is also
# in a global namespace, so make it as unique as possible.
# You should always prefix your config file name with plugins/,
# so as to ensure you dont accidentally clobber a calibre config file
prefs = JSONConfig('plugins/interface_demo')

# Set defaults
prefs.defaults['hello_world_msg'] = 'Hello, World!'


class ConfigWidget(QWidget):

    def __init__(self):
        QWidget.__init__(self)
        self.l = QHBoxLayout()
        self.setLayout(self.l)

        self.label = QLabel('Hello world &message:')
        self.l.addWidget(self.label)

        self.msg = QLineEdit(self)
        self.msg.setText(prefs['hello_world_msg'])
        self.l.addWidget(self.msg)
        self.label.setBuddy(self.msg)

    def save_settings(self):
        prefs['hello_world_msg'] = self.msg.text()

prefs-objekt äret nu tillgängligt i hela insticksmodulskoden med en enkel:

from calibre_plugins.interface_demo.config import prefs

Du kan se prefs-objekt som används i main.py:

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

Redigera bokinsticksmodul

Låt oss nu byta växel för ett tag och se hur man skapar en insticksmodul för att lägga till verktyg till calibres bokredigerare. Instickmodulen finns här: editor_demo_plugin.zip.

Det första steget, som för alla insticksmodul är att skapa importnamnet tom TXT-fil, som beskrivs ovan. Vi ska namnge filen plugin-import-name-editor_plugin_demo.txt.

Nu skapar vi den obligatoriska __init __ py filen som innehåller metadata om insticksmodulen – dess namn, utvecklare, version o.s.v.

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)

En enda insticksmodul redigerare kan ge flera verktyg varje verktyg motsvarar en knapp i verktygsfältet och posten i Insticksmoduler-menyn i redigeraren. Dessa kan ha undermenyer om verktyget har flera relaterade åtgärder.

Verktygen bör definieras i filen main.py i din insticksmodul. Varje verktyg är en klass som ärvs från klassen calibre.gui2.tweak_book.plugin.Tool. Låt oss titta på main.py från demo-insticksmodulen, källkoden är tungt kommenterade och bör vara självförklarande. Läs API-dokumenten av klassen calibre.gui2.tweak_book.plugin.Tool för mer detaljer.

main.py

Här kommer vi att se definitionen av ett enda verktyg som kommer att multiplicera alla teckensnittsstorlekar i boken av ett nummer som användaren ger. Detta verktyg visar olika viktiga begrepp som du behöver för att utveckla dina egna insticksmodul, så du bör läsa (tungt kommenterade) källkoden noggrant.

import re

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

# The base class that all tools must inherit from
from calibre.gui2.tweak_book.plugin import Tool
from css_parser.css import CSSRule
from qt.core import QAction, QInputDialog


class DemoTool(Tool):

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

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

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

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

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

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

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

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

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

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

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

Låt oss bryta ner main.py. Vi ser att den definierar ett enda verktyg, som heter Förstora teckensnitt. Detta verktyg kommer att be användaren om ett nummer och multiplicera alla teckensnittsstorlekar i boken med det numret.

Det första viktiga är verktygsnamnet som du måste ställa in till någon relativt unik sträng eftersom det kommer att användas som nyckel för detta verktyg.

Nästa viktiga startpunkt är calibre.gui2.tweak_book.plugin.Tool.create_action(). Denna metod skapar QAction-objekt som visas i insticksmodulsverktygsfältet och insticksmodulsmenyn. Den tilldelar också valfritt en tangentbordsgenväg som användaren kan skräddarsy. Den utlösta signalen från QAction är ansluten till metoden ask_user() som frågar användaren om teckensnittsstorlekensmultiplikatorn och sedan kör förstoringskoden.

Förstoringskoden är väl kommenterad och ganska enkel. De viktigaste sakerna att observera är att du får en referens till redigeringsfönstret som self.gui och redigeraren Boss som self.boss. Boss är objektet som styr redigerarens användargränssnitt. Den har många användbara metoder, som dokumenteras klassen i calibre.gui2.tweak_book.boss.Boss.

Slutligen finns det self.current_container som är en referens till boken som redigeras som calibre.ebooks.oeb.polish.container.Container objekt. Detta representerar boken som en samling av sina konstituerande HTML/CSS/bildfiler och har bekvämliga metoder för att göra många användbara saker. Objektbehållaren och olika användbara nyttofunktioner som kan återanvändas i din insticksmodulskod är dokumenterade i API-dokumentation för e-bokredigeringsverktygen.

Lägga till översättningar till din insticksmodul

Du kan ha alla användargränssnittsträngar i din insticksmodul översatta och visas på vilket språk som är inställt för huvudanvändargränssnittet i calibre.

Det första steget är att gå igenom din insticksmoduls källkod och markera alla användarsynliga strängar som översättningsbara, genom att omge dem _(). Till exempel:

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

Använd sedan något program för att skapa .po-filer från din insticksmodulskällkod. Det bör finnas en .po-fil för varje språk du vill översätta till. Till exempel: de.po för tyska, fr.po för franska och så vidare. Du kan använda programmet Poedit för detta.

Skicka dessa .po filer till dina översättare. När du får tillbaka dem, kompilera dem i .mo filer. Du kan återigen använda Poedit för det, eller bara göra:

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

Placera .mo-filer i translations-mappen i din insticksmodul.

Det sista steget är att helt enkelt anropa funktionen load_translations() överst av dina insticksmoduls .py-filer. Av prestandaskäl bör du bara anropa den här funktionen i de .py-filerna som faktiskt har översättningsbara strängar. Så i en typiskt användargränssnittsinsticksmodul skulle du anropa det överst i ui.py men inte __init __. py.

Du kan testa översättningarna av dina insticksmodul genom att ändra språk i användargränssnittet i calibre i Inställningar → Gränssnitt → Utseende & känsla eller genom att köra calibre med miljövariabeluppsättningen CALIBRE_OVERRIDE_LANG. Till exempel:

CALIBRE_OVERRIDE_LANG=de

Ersätt sv med språkkoden för det språk som du vill testa.

För översättningar med plural, använd funktionen ngettext() istället för _(). Till exempel:

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

Insticksmoduls API

Som ni kanske har märkt ovan, en insticksmodul i calibre är en klass. Det finns olika klasser för de olika typerna av insticksmoduler i calibre. Detaljer om varje klass, inklusive basklassen för alla insticksmoduler kan hittas i API-dokumentation för insticksmoduler.

Din insticksmodul kommer nästan säkert att använda kod från calibre. För att lära dig hur man hittar olika funktioner i calibre-kodbasen, läs avsnittet om calibre Kodutformning.

Felsöka insticksmodul

Den första och viktigaste steget är att köra calibre i felsökningsläge. Du kan göra det från kommandoraden med:

calibre-debug -g

Eller inifrån calibre genom att högerklicka på Inställningar eller genom att använda tangentbordsgenvägen Ctrl+Skift+R.

När du kör från kommandoraden, kommer felsökningsutmatning skrivas till konsolen, när man kör inifrån calibre kommer utmatning gå till en TXT-fil.

Du kan infoga utskriftssatser var som helst i din insticksmodulskod, de matas ut i felsökningsläge. Kom ihåg, det här är Python, du borde verkligen inte behöva mer än att skriva ut uttalanden för att felsöka ;) Jag utvecklade hela calibre med just den här felsökningstekniken.

Du kan snabbt testa ändringar i din insticksmodul med hjälp av följande kommandorad:

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

Detta kommer att stänga en körande calibre, vänta på avstängning är slutförd och sedan uppdatera din insticksmodul i calibre och starta om calibre.

Fler insticksmodul exempel

Du kan hitta en lista med många sofistikerade calibre-insticksmoduler här.

Dela dina insticksmodul med andra

Om du vill dela insticksmodulen som du har gjort med andra calibre-användare, posta din insticksmodul i en ny tråd i calibre-insticksmodulsforumet.