Schreibe Deine eigenen Erweiterungen zur Anpassung von Calibres Funktionalität¶
Calibre ist Modular aufgebaut. Fast alle Funktionen in Calibre sind mit Plugin’s erstellt. Sie werden zum Umwandeln, zum herunterladen von Newsfeeds (dann nennen wir sie Rezepte - recipes), als Teil der Benutzeroberfläche, zum Verbinden zwischen verschiedenen Komponenten, zum Bearbeiten von Dateien beim Import und so weiter genutzt. Eine komplette Liste aller eingebauten Plugin’s sehen Sie unter: guilabel:Preferences->Advanced->Plugins.
Hier werden Sie lernen, wie Sie ihre eigene Erweiterung erstellen, um neue Funktionen zu |app| hinzuzufügen.
Bemerkung
Dies gilt nur für Calibre Versionen >= 0.8.60
Aufbau einer Calibre-Erweiterung¶
Ein Calibre Plugin ist sehr einfach. Es besteht aus einer Zip-Datei, die eine Python - (vgl. Programmiersprache) Datei und weiteren Teilen die dafür notwendig sind - wie Bilder - enthält. Am besten Sie sehen sich ein Beispiel an.
Angenommen Sie haben eine Installation von Calibre die Sie dafür nutzen eigene e-Dokumente im EPUB und MOBI format zu publizieren. Für alle Ihre Dateien soll Calibre den ‚publisher‘ -Metatag auf ‚Hello world‘ setzen. Unten sehen Sie wie das gemacht wird. Erstellen Sie eine (Text-) Datei mit dem Namen file:__init__.py (Diese Bezeichnung muß jedem Plugin als Dateiname gegeben werden) öffnen Sie die Datei in einem Editor und schreiben Sie folgenden Programmcode hinein:
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
Das war es schon. Um dieses kleine Programm nun als Plugin zu Calibre hinzuzufügen, wechseln Sie in den Ordner Ihrer kleinen Datei und führen Sie folgenden Befehl (auf der Komandozeile) aus.:
calibre-customize -b .
Bemerkung
Für macOS sind die Komandozeilentools in Calibre integriert. Sollte Calibre z.B. in /Applications
installiert sein, dann finden Sie die Tools unter /Applications/calibre.app/Contents/MacOS/
.
You can download the Hello World plugin from helloworld_plugin.zip.
Jedes Mal wenn Sie nun mit Calibre ein Buch konvertieren, wird die Methode run()
des Plugin’s ausgeführt und damit der ‚publisher‘ Metatag auf „Hello World“ geändert. Das ist ein einfaches Plugin, machen Sie sich an ein etwas komplexeres mit dem Sie ein Element zur Benutzeroberfläche hinzufügen.
Ein Benutzeroberflächen-Plugin¶
Diese Plugin wir auf ein paar Dateien verteilt (damit die einzelnen Teile überschaubar bleiben). Sie erkennen daran wie Sie auf, in der Plugin-Datei enthaltene, Resourcen (Bilder oder Daten) zugreifen, Nutzern erlauben Ihr Plugin zu konfigurieren, wie Sie Elemente der Benutzeroberfläche erstellen und wie Sie die Datenbank der Bücher in Calibre ansprechen und darin suchen.
You can download this plugin from interface_demo_plugin.zip
Wichtig ist das die ZIP-Datei, die Ihr Plugin ist, nicht ganz wenige Dateien enthält. Unten sehen Sie eine Beschreibung achten Sie besonders auf plugin-import-name-interface_demo.txt
.
- plugin-import-name-interface_demo.txt
Um in einem Plugin mehrere (Programm-) Dateien zu nutzen (Module nennt man das in Python) müssen diese einem genauen Namensschema entsprechen. Die Datei an sich hat keinen Inhalt. Die Form des Dateinamens ist :
plugin-import-name-**some_name**.txt
. Immer wenn in der ZIP-Datei eine solche Datei existiert können Sie innerhalb des Plugins Programmcode aus allen anderen enthaltenen .py Dateien auf folgende Art nutzen:from calibre_plugins.some_name.some_module import some_objectDer Präfix
calibre_plugins
muss immer in der Datei stehen.some_name
wird mit dme Namen der oben beschriebenen leeren Datei ersetzt.some_module
ist der Name einer weiteren Dateisome_module.py
innerhalb der ZIP-Datei. Beachten Sie das das genau so nützlich und nutzbar ist wie Importe in der Programmiersprache Python selbst. Sie können ‚packages‘ und ‚subpackages‘ von .py Modulen innerhalb der ZIP-Datei erstellen wie Sie das mit Python auch tun würden. Das Funktioniert genau so wie die Nutzung von __init__.py Dateien. (vgl. python.org)Der Name den Sie für eine Datei bei
some_name
wählen, ist in allen Plugins verfügbar und damit global! Er sollte natürlich nicht schon vorhanden sein, da Sie ansonsten die schon vorhandenen Funktionen ersetzen. Wählen Sie also den Name möglichst eindeutig und „beschreibend“ aus. Zusätzlich muss der Name den Python vorschriften entsprechen: Nur Buchstaben, Nummern und der Unterstrich sind erlaubt.- __init__.py
Wie schon zuvor ist das eine Datei, die eine Plugin Klasse definiert
- main.py
Diese Datei enthält den eigentlichen Code, der etwas nützliches tut
- ui.py
Diese Datei definiert das Benutzer Interface des Plugin’s
- images/icon.png
Das Symbol für diese Erweiterung
- about.txt
Eine Textdatei mit einer Beschreibung des Plugin
- translations
Ein Ordner, der .mo-Dateien mit Übersetzungen der Benutzeroberfläche Ihrer Erweiterung in verschiedenen Sprachen enthält. Details nachfolgend.
Nun ein Blick auf den Code.
__init__.py¶
Zuerst die obligatorische ``__init__.py`, in dieser werden plugin metadaten gesetzt.
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()
Das einzig erwähnenswerte Datum hier ist das Datenfeld actual_plugin
. Da Calibre sowohl ein Komandozeilen- als auch ein grafisches Interface hat, sollte nicht gleich in der ersten Datei schon eine grafische Schnittstelle geladen werden. Das Feld „actual_plugin“ weist darauf hin das sich eben dort ein User Interface befindet. Dieses wird eben nur dann ausgeführt, wenn das Plugin innerhalb der grafischen Oberfläche gestartet wird.
Denke daran, damit das funktionieren kann, muß in der Plugin - ZIP- Datei eine Datei mit dem Namen wie: plugin-import-name-some_name.txt file enthalten sein, wie weiter oben gezeigt.
Zudem gibt es ein paar Methoden die das Konfigurieren des Plugin durch die Nutzer ermöglichen. Wie das geht erfahren Sie weiter unten.
ui.py¶
Schauen Sie nun die Datei ui.py an. In dieser definieren Sie wie eine grafische Benutzeroberfläche Ihres Plugin aussehen wird. Der Sourcecode (Quellcode) ist sehr gut kommentiert und sollte selbsterklärend sein:
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¶
Die aktuelle Logik um das Benutzerinterface für einen Plugin Demo Dialog zu implementieren.
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'])
Ressourcen aus der Plugin ZIP-Datei nutzen¶
Calibres Plugin ‚loading system‘ stellt eine Reihe von eingebauten Funktionen bereit, mit denen man Dateien und Inhalte aus der Plugin ZIP-Datei bekommt.
- get_resources(name_or_list_of_names)
This function should be called with a list of paths to files inside the ZIP file. For example to access the file
icon.png
in the folder images in the ZIP file, you would use:images/icon.png
. Always use a forward slash as the path separator, even on Windows. When you pass in a single name, the function will return the raw bytes of that file or None if the name was not found in the ZIP file. If you pass in more than one name then it returns a dictionary mapping the names to bytes. If a name is not found, it will not be present in the returned dictionary.- get_icons(name_or_list_of_names, plugin_name=‘‘)
A wrapper for get_resources() that creates QIcon objects from the raw bytes returned by get_resources. If a name is not found in the ZIP file the corresponding QIcon will be null. In order to support icon theme-ing, pass in the human friendly name of your plugin as
plugin_name
. If the user is using an icon theme with icons for your plugin, they will be loaded preferentially.
Enabling user configuration of your plugin¶
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 has many different ways to store configuration data (a legacy of its long history). The recommended way is to use the JSONConfig class, which stores your configuration information in a .json file.
The code to manage configuration data in the demo plugin is in 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()
Das prefs
-Objekt ist nun überall im gesamten Erweiterungs-Code verfügbar über ein einfaches:
from calibre_plugins.interface_demo.config import prefs
You can see the prefs
object being used in main.py:
self.do_user_config(parent=self)
# Apply the changes
self.label.setText(prefs['hello_world_msg'])
Edit book plugins¶
Now let’s change gears for a bit and look at creating a plugin to add tools to the calibre book editor. The plugin is available here: editor_demo_plugin.zip.
The first step, as for all plugins is to create the
import name empty txt file, as described above.
We shall name the file plugin-import-name-editor_plugin_demo.txt
.
Now we create the mandatory __init__.py
file that contains metadata about
the plugin – its name, author, version, etc.
from calibre.customize import EditBookToolPlugin
class DemoPlugin(EditBookToolPlugin):
name = 'Edit Book plugin demo'
version = (1, 0, 0)
author = 'Kovid Goyal'
supported_platforms = ['windows', 'osx', 'linux']
description = 'A demonstration of the plugin interface for the ebook editor'
minimum_calibre_version = (1, 46, 0)
A single editor plugin can provide multiple tools each tool corresponds to a single button in the toolbar and entry in the Plugins menu in the editor. These can have sub-menus in case the tool has multiple related actions.
The tools must all be defined in the file main.py
in your plugin. Every
tool is a class that inherits from the
calibre.gui2.tweak_book.plugin.Tool
class. Let’s look at main.py
from the demo plugin, the source code is heavily commented and should be
self-explanatory. Read the API documents of the
calibre.gui2.tweak_book.plugin.Tool
class for more details.
main.py¶
Here we will see the definition of a single tool that will multiply all font sizes in the book by a number provided by the user. This tool demonstrates various important concepts that you will need in developing your own plugins, so you should read the (heavily commented) source code carefully.
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
Let’s break down main.py
. We see that it defines a single tool, named
Magnify fonts. This tool will ask the user for a number and multiply all font
sizes in the book by that number.
The first important thing is the tool name which you must set to some relatively unique string as it will be used as the key for this tool.
The next important entry point is the
calibre.gui2.tweak_book.plugin.Tool.create_action()
. This method creates
the QAction objects that appear in the plugins toolbar and plugin menu.
It also, optionally, assigns a keyboard shortcut that the user can customize.
The triggered signal from the QAction is connected to the ask_user() method
that asks the user for the font size multiplier, and then runs the
magnification code.
The magnification code is well commented and fairly simple. The main things to
note are that you get a reference to the editor window as self.gui
and the
editor Boss as self.boss
. The Boss is the object that controls the editor
user interface. It has many useful methods, that are documented in the
calibre.gui2.tweak_book.boss.Boss
class.
Finally, there is self.current_container
which is a reference to the book
being edited as a calibre.ebooks.oeb.polish.container.Container
object. This represents the book as a collection of its constituent
HTML/CSS/image files and has convenience methods for doing many useful things.
The container object and various useful utility functions that can be reused in
your plugin code are documented in API documentation for the e-book editing tools.
Hinzufügen von Übersetzungen zu Ihrer Erweiterung¶
You can have all the user interface strings in your plugin translated and displayed in whatever language is set for the main calibre user interface.
The first step is to go through your plugin’s source code and mark all user visible strings as translatable, by surrounding them in _(). For example:
action_spec = (_('My plugin'), None, _('My plugin is cool'), None)
Then use some program to generate .po files from your plugin source code. There should be one .po file for every language you want to translate into. For example: de.po for German, fr.po for French and so on. You can use the Poedit program for this.
Send these .po files to your translators. Once you get them back, compile them into .mo files. You can again use Poedit for that, or just do:
calibre-debug -c "from calibre.translations.msgfmt import main; main()" filename.po
Verschieben Sie die .mo-Dateien in den translations
-Ordner Ihrer Erweiterung.
The last step is to simply call the function load_translations() at the top
of your plugin’s .py files. For performance reasons you should only call this
function in those .py files that actually have translatable strings. So in a
typical User Interface plugin you would call it at the top of ui.py
but not
__init__.py
.
You can test the translations of your plugins by changing the user interface
language in calibre under Preferences → Interface → Look & feel or by running calibre
with the CALIBRE_OVERRIDE_LANG
environment variable set. For example:
CALIBRE_OVERRIDE_LANG=de
„de“ mit dem Sprachcode der zu testenden Sprache ersetzen.
For translations with plurals, use the ngettext()
function instead of
_()
. For example:
ngettext('Delete a book', 'Delete {} books', num_books).format(num_books)
Die Erweiterungs-API¶
As you may have noticed above, a plugin in calibre is a class. There are different classes for the different types of plugins in calibre. Details on each class, including the base class of all plugins can be found in API-Dokumentation für Erweiterungen.
Your plugin is almost certainly going to use code from calibre. To learn how to find various bits of functionality in the calibre code base, read the section on the calibre Code-Layout.
Fehlerdiagnose bei Erweiterungen¶
The first, most important step is to run calibre in debug mode. You can do this from the command line with:
calibre-debug -g
Or from within calibre by right-clicking the Preferences button or using the Ctrl+Shift+R keyboard shortcut.
When running from the command line, debug output will be printed to the console, when running from within calibre the output will go to a txt file.
You can insert print statements anywhere in your plugin code, they will be output in debug mode. Remember, this is Python, you really shouldn’t need anything more than print statements to debug ;) I developed all of calibre using just this debugging technique.
You can quickly test changes to your plugin by using the following command line:
calibre-debug -s; calibre-customize -b /path/to/your/plugin/folder; calibre
Damit wird das laufende Calibre Programm beendet. Warten Sie, bis sich Calibre vollständig beendet hat. Dann aktualisieren Sie Ihre Erweiterung und starten Calibre neu.
Weitere Erweiterungs-Beispiele¶
You can find a list of many sophisticated calibre plugins here.