Pisanje sopstvenih dodataka za proširivanje mogućnosti programa calibre

calibre ima veoma modularan dizajn. Skoro sva funkcionalnost u calibre-u dolazi u obliku dodataka. Dodaci se koriste za konverziju, za preuzimanje vesti (iako se oni zovu recepti), za razne komponente korisničkog interfejsa, za povezivanje sa različitim uređajima, za obradu fajlova prilikom dodavanja u calibre i tako dalje. Možete dobiti kompletnu listu svih ugrađenih dodataka u calibre-u tako što ćete otići na Podešavanja → Napredno → Dodaci.

Ovde ćemo vas naučiti kako da kreirate sopstvene dodatke da biste dodali nove funkcije u calibre.

Белешка

Ovo se odnosi samo na calibre verzije >= 0.8.60

Anatomija calibre dodatka

Dodatak za calibre je jednostavan: to je ZIP arhiva koja sadrži Python kod i druge potrebne resurse, poput slika. Pogledajmo najpre osnovni primer.

Pretpostavimo da imate instalaciju calibre-a koju koristite za samoizdavanje raznih e-dokumenata u EPUB i MOBI formatima. Želeli biste da svi fajlovi koje generiše calibre imaju postavljenog izdavača kao "Hello world", evo kako to da uradite. Kreirajte fajl pod nazivom __init__.py (ovo je specijalno ime i uvek se mora koristiti za glavni fajl vašeg dodatka) i unesite sledeći Python kod u njega:

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 = {'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

To je sve. Da biste dodali ovaj kod u calibre kao dodatak, jednostavno pokrenite sledeće u folderu u kojem ste kreirali __init__.py:

calibre-customize -b .

Белешка

Na macOS-u, alati komandne linije su unutar calibre paketa, na primer, ako ste instalirali calibre u /Applications alati komandne linije su u /Applications/calibre.app/Contents/MacOS/.

Možete preuzeti Hello World dodatak sa helloworld_plugin.zip.

Svaki put kada koristite calibre za konverziju knjige, metoda run() dodatka će biti pozvana i konvertovana knjiga će imati izdavača postavljenog na "Hello World". Ovo je trivijalan dodatak, pređimo na složeniji primer koji zapravo dodaje komponentu korisničkom interfejsu.

Dodatak za korisnički interfejs

Ovaj dodatak sastoji se od nekoliko fajlova (radi preglednosti koda). Primer pokazuje kako da pristupite resursima (slikama ili datotekama s podacima) u ZIP arhivi dodatka, omogućite korisnicima da podese dodatak, dodate elemente u korisnički interfejs programa calibre i pristupite bazi knjiga i pretražujete je.

Možete preuzeti ovaj dodatak sa interface_demo_plugin.zip

Prva stvar koju treba primetiti je da ovaj ZIP fajl ima mnogo više fajlova u sebi, objašnjenih u nastavku, obratite posebnu pažnju na plugin-import-name-interface_demo.txt.

plugin-import-name-interface_demo.txt

Prazan tekstualni fajl koji omogućava da dodatak sadrži više fajlova. Mora da postoji u svakom dodatku koji koristi više od jednog .py fajla. Mora ostati prazan, a naziv mora imati oblik plugin-import-name-**some_name**.txt. Zahvaljujući njemu, kod iz .py fajlova u ZIP arhivi možete da uvezete naredbom poput ove:

from calibre_plugins.some_name.some_module import some_object

Prefiks calibre_plugins mora uvek da bude prisutan. some_name potiče iz naziva praznog tekstualnog fajla, a some_module označava fajl some_module.py u ZIP arhivi. Ovaj uvoz ima iste mogućnosti kao običan Python uvoz. U arhivi možete praviti pakete i potpakete .py modula na uobičajen način (definisanjem __init__.py u svakom podfolderu) i sve bi trebalo da radi.

Naziv koji izaberete za some_name ulazi u globalni imenski prostor koji dele svi dodaci, zato neka bude što jedinstveniji. Mora da bude važeći Python identifikator (samo slova, brojevi i donja crta).

__init__.py

Kao i ranije, datoteka koja definiše klasu dodatka

main.py

Ova datoteka sadrži stvarni kôd koji radi nešto korisno

ui.py

Ova datoteka definiše deo interfejsa dodatka

images/icon.png

Ikona za ovaj dodatak

about.txt

Tekstualna datoteka sa informacijama o dodatku

translations

Fascikla koja sadrži .mo datoteke sa prevodima korisničkog interfejsa vašeg dodatka na različite jezike. Pogledajte ispod za detalje.

Sada pogledajmo kod.

__init__.py

Prvo, obavezni __init__.py za definisanje metapodataka dodatka:


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

Posebno je važno polje actual_plugin. Pošto calibre ima interfejs komandne linije i grafički interfejs, dodaci za grafički interfejs ne treba da učitavaju njegove biblioteke u fajlu __init__.py. Poljem actual_plugin govorite programu calibre da se pravi dodatak nalazi u drugom fajlu ZIP arhive, koji će biti učitan samo u grafičkom interfejsu.

Da bi ovo radilo, ZIP arhiva dodatka mora da sadrži fajl plugin-import-name-some_name.txt, kao što je prethodno objašnjeno.

Takođe postoji nekoliko metoda za omogućavanje korisničke konfiguracije dodatka. O njima se raspravlja u nastavku.

ui.py

Sada pogledajmo ui.py koji definiše stvarni GUI dodatak. Izvorni kod je detaljno komentarisan i trebalo bi da bude jasan sam po sebi:

# 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 don't 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
        assert base_plugin_object is not None
        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

Stvarna logika za implementaciju dijaloga Interface Plugin Demo.


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

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 don't 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', f'Updated the metadata in the files of {len(ids)} book(s)', show=True)

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

Pristup resursima iz ZIP arhive dodatka

Sistem za učitavanje dodataka u programu calibre definiše nekoliko ugrađenih funkcija koje olakšavaju pristup fajlovima iz ZIP arhive dodatka.

get_resources(name_or_list_of_names)

Ovu funkciju treba pozvati sa listom putanja do datoteka unutar ZIP datoteke. Na primer, da biste pristupili datoteci icon.png u fascikli images u ZIP datoteci, koristili biste: images/icon.png. Uvek koristite kosu crtu kao separator putanje, čak i na Windows-u. Kada prosledite jedno ime, funkcija će vratiti sirove bajtove te datoteke ili None ako ime nije pronađeno u ZIP datoteci. Ako prosledite više od jednog imena, onda vraća rečnik koji mapira imena u bajtove. Ako ime nije pronađeno, neće biti prisutno u vraćenom rečniku.

get_icons(name_or_list_of_names, plugin_name='')

Omotač za get_resources() koji od dobijenih bajtova pravi QIcon objekte. Ako naziv nije pronađen u ZIP arhivi, odgovarajući QIcon biće prazan. Da bi bile podržane teme ikona, prosledite čitljiv naziv dodatka kao plugin_name. Ako korisnikova tema sadrži ikone za vaš dodatak, one će imati prednost.

Omogućavanje korisničke konfiguracije vašeg dodatka

Da biste omogućili korisnicima da konfigurišu vaš dodatak, morate definisati tri metode u vašoj osnovnoj klasi dodatka, is_customizable, config_widget i save_settings kao što je prikazano ispod:

    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 ima mnogo različitih načina za skladištenje konfiguracionih podataka (nasleđe njegove duge istorije). Preporučeni način je korišćenje klase JSONConfig, koja skladišti vaše konfiguracione informacije u .json datoteci.

Kod za upravljanje konfiguracionim podacima u demo dodatku nalazi se u config.py:


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

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 don't 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()

Objekat prefs je sada dostupan u celom kodu dodatka jednostavnim:

from calibre_plugins.interface_demo.config import prefs

Možete videti da se objekat prefs koristi u main.py:

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

Dodaci za uređivanje knjiga

Pogledajmo sada kako da napravite dodatak koji dodaje alate uređivaču knjiga u programu calibre. Primer dodatka možete preuzeti ovde: editor_demo_plugin.zip.

Prvi korak, kao i za sve dodatke, jeste kreiranje prazne txt datoteke sa imenom za uvoz, kao što je opisano iznad. Nazvaćemo datoteku plugin-import-name-editor_plugin_demo.txt.

Sada kreiramo obaveznu datoteku __init__.py koja sadrži metapodatke o dodatku -- njegovo ime, autora, verziju, itd.

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)

Jedan dodatak za uređivač može pružiti više alata, svaki alat odgovara jednom dugmetu na traci sa alatkama i stavci u meniju Dodaci u uređivaču. Oni mogu imati podmenije u slučaju da alat ima više povezanih akcija.

Svi alati moraju biti definisani u datoteci main.py u vašem dodatku. Svaki alat je klasa koja nasleđuje klasu calibre.gui2.tweak_book.plugin.Tool. Hajde da pogledamo main.py iz demo dodatka, izvorni kod je detaljno komentarisan i trebalo bi da bude jasan sam po sebi. Pročitajte API dokumentaciju klase calibre.gui2.tweak_book.plugin.Tool za više detalja.

main.py

Ovde ćemo videti definiciju jednog alata koji će pomnožiti sve veličine fontova u knjizi brojem koji unese korisnik. Ovaj alat demonstrira različite važne koncepte koji će vam biti potrebni u razvoju sopstvenih dodataka, pa bi trebalo pažljivo da pročitate (detaljno komentarisan) izvorni kod.

import re

from css_parser.css import CSSRule
from qt.core import QAction, QInputDialog

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


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: F821
        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:f}')
            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

Pogledajmo main.py. U njemu je definisan jedan alat, Magnify fonts, koji od korisnika traži broj, pa njime množi sve veličine fonta u knjizi.

Prva važna stvar je ime alata koje morate postaviti na neki relativno jedinstven string jer će se koristiti kao ključ za ovaj alat.

Sledeća važna ulazna tačka je calibre.gui2.tweak_book.plugin.Tool.create_action(). Ova metoda pravi QAction objekte koji se prikazuju na traci sa alatkama i u meniju dodataka. Po potrebi dodeljuje i prečicu na tastaturi, koju korisnik može da prilagodi. Signal QAction objekta povezan je s metodom ask_user(), koja traži faktor uvećanja fonta, a zatim pokreće odgovarajući kod.

Kod za uveličavanje je dobro komentarisan i prilično jednostavan. Glavne stvari koje treba primetiti su da dobijate referencu na prozor uređivača kao self.gui i Boss uređivača kao self.boss. Boss je objekat koji kontroliše korisnički interfejs uređivača. Ima mnogo korisnih metoda, koji su dokumentovani u klasi calibre.gui2.tweak_book.boss.Boss.

Na kraju, self.current_container predstavlja knjigu koja se uređuje kao objekat klase calibre.ebooks.oeb.polish.container.Container. Knjiga je predstavljena skupom HTML, CSS i slikovnih fajlova, a ovaj objekat nudi pomoćne metode za mnoge zadatke. Objekat Container i druge pomoćne funkcije koje možete koristiti u svom dodatku opisani su u API dokumentacija za alate za uređivanje e-knjiga.

Dodavanje prevoda u vaš dodatak

Možete imati sve stringove korisničkog interfejsa u vašem dodatku prevedene i prikazane na jeziku koji je postavljen za glavni korisnički interfejs calibre-a.

Prvi korak je da prođete kroz izvorni kod vašeg dodatka i označite sve stringove vidljive korisniku kao prevodive, okružujući ih sa _(). Na primer:

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

Zatim koristite neki program za generisanje .po datoteka iz izvornog koda vašeg dodatka. Trebalo bi da postoji jedna .po datoteka za svaki jezik na koji želite da prevedete. Na primer: de.po za nemački, fr.po za francuski i tako dalje. Za ovo možete koristiti program Poedit.

Pošaljite ove .po datoteke vašim prevodiocima. Kada ih dobijete nazad, kompajlirajte ih u .mo datoteke. Možete ponovo koristiti Poedit za to, ili samo uraditi:

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

Stavite .mo fajlove u fasciklu translations u vašem dodatku.

Poslednji korak je da jednostavno pozovete funkciju load_translations() na vrhu .py fajlova vašeg dodatka. Iz razloga performansi, ovu funkciju bi trebalo da pozivate samo u onim .py fajlovima koji zapravo imaju prevodive stringove. Dakle, u tipičnom dodatku korisničkog interfejsa pozvali biste je na vrhu ui.py, ali ne i u __init__.py.

Prevod dodatka možete proveriti tako što ćete promeniti jezik interfejsa u programu calibre preko Podešavanja → Interfejs → Izgled i ponašanje ili pokrenuti calibre sa podešenom promenljivom okruženja CALIBRE_OVERRIDE_LANG. Na primer:

CALIBRE_OVERRIDE_LANG=de

Zamenite de kodom jezika koji želite da testirate.

Za prevode sa množinom, koristite funkciju ngettext() umesto _(). Na primer:

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

API dodatka

Kao što ste možda primetili iznad, dodatak u calibre-u je klasa. Postoje različite klase za različite tipove dodataka u calibre-u. Detalji o svakoj klasi, uključujući osnovnu klasu svih dodataka, mogu se naći u API dokumentacija za dodatke.

Vaš dodatak će gotovo sigurno koristiti kod iz calibre-a. Da biste naučili kako da pronađete različite delove funkcionalnosti u bazi koda calibre-a, pročitajte odeljak o calibre Raspored koda.

Otklanjanje grešaka u dodacima

Prvi, najvažniji korak je pokretanje calibre-a u režimu za otklanjanje grešaka. To možete uraditi iz komandne linije sa:

calibre-debug -g

Ili u samom programu calibre: kliknite desnim tasterom miša na dugme Podešavanja ili pritisnite Ctrl+Shift+R.

Kada se pokreće iz komandne linije, izlaz za otklanjanje grešaka će biti odštampan na konzoli, kada se pokreće iz calibre-a izlaz će ići u txt datoteku.

Možete ubaciti print naredbe bilo gde u kodu vašeg dodatka, one će biti ispisane u režimu za otklanjanje grešaka. Zapamtite, ovo je Python, zaista vam ne bi trebalo ništa više od print naredbi za otklanjanje grešaka ;) Razvio sam ceo calibre koristeći samo ovu tehniku otklanjanja grešaka.

Možete brzo testirati promene u vašem dodatku koristeći sledeću komandnu liniju:

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

Ova naredba zatvara pokrenuti calibre, čeka da se zatvaranje završi, zatim ažurira dodatak i ponovo pokreće calibre.

Više primera dodataka

Možete pronaći listu mnogih sofisticiranih calibre dodataka ovde.

Deljenje vaših dodataka sa drugima

Ako želite da podelite dodatke koje ste kreirali sa drugim korisnicima calibre-a, postavite vaš dodatak u novoj temi na forumu za calibre dodatke.