Skriv egna insticksmoduler för att utöka calibres funktioner¶
calibre är modulärt uppbyggt. Nästan alla funktioner i calibre finns i form av insticksmoduler. Insticksmoduler används för konvertering, för hämtning av nyheter (även om dessa kallas recept), för olika delar av användargränssnittet, för anslutning till olika enheter, för bearbetning av filer när de läggs till i calibre och så vidare. Du kan få en komplett lista över 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.
Anteckning
Detta gäller bara calibre-utgåvor >= 0.8.60
Så är en calibre-insticksmodul uppbyggd¶
En calibre-insticksmodul är mycket enkel: en ZIP-fil som innehåller Python-kod och andra resurser som insticksmodulen behöver, såsom bildfiler. Låt oss titta på 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 calibre skapar ska ha utgivaren inställd på ”Hello world”. Så här gör du: skapa en fil med namnet __init__.py (detta är ett särskilt namn som alltid måste användas för insticksmodulens huvudfil) och skriv följande Python-kod i den:
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
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 .
Anteckning
I macOS finns kommandoradsverktygen inuti calibre-paketet. Om du till exempel har installerat calibre i /Applications, finns kommandoradsverktygen 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 för att konvertera en bok anropas insticksmodulens run()-metod, och den konverterade boken får utgivaren inställd på ”Hello World”. Detta är en mycket enkel insticksmodul. Låt oss gå vidare till ett mer komplext exempel som lägger till en komponent i användargränssnittet.
En insticksmodul för användargränssnittet¶
Denna insticksmodul fördelas över några filer (för att hålla koden lättöverskådlig). Den visar hur du hämtar resurser (bilder eller datafiler) från insticksmodulens ZIP-fil, låter användare anpassa insticksmodulen, skapar element i calibres användargränssnitt samt får åtkomst till och söker i calibres bokdatabas.
Du kan hämta denna insticksmodul från interface_demo_plugin.zip
Observera först att denna ZIP-fil innehåller många fler filer, som förklaras nedan. Lägg särskilt märke 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_objectPrefixet
calibre_pluginsmåste alltid finnas.some_namekommer från filnamnet på den tomma textfilen.some_modulerefererar tillsome_module.py-filen inuti ZIP-filen. Observera att denna import är lika kraftfull som vanlig Python-import. Du kan skapa paket och underpaket av .py-moduler inuti ZIP-filen, precis som du normalt skulle göra (genom att definiera __init__.py i varje undermapp), och allt borde ”bara fungera”.Namnet du använder för
some_nameingår i en global namnrymd som delas av alla insticksmoduler, så gör det så unikt som möjligt. Kom dock ihåg att det måste vara en giltig Python-identifierare (endast bokstäver, 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
- images/icon.png
Ikonen för denna insticksmodul
- about.txt
En textfil med information om insticksmodulen
- translations
En mapp som innehåller .mo-filer med översättningar av insticksmodulens användargränssnitt till olika språk. Se nedan för detaljer.
Låt oss nu titta på koden.
__init__.py¶
Först den obligatoriska filen __init__.py, som definierar insticksmodulens metadata:
# 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()
Den enda anmärkningsvärda funktionen är fältet actual_plugin. Eftersom calibre har både ett kommandoradsgränssnitt och ett grafiskt gränssnitt bör GUI-insticksmoduler som denna inte läsa in några GUI-bibliotek i __init__.py. Fältet actual_plugin hanterar detta genom att tala om för calibre att den egentliga insticksmodulen finns i en annan fil i ZIP-arkivet. Den filen läses endast in i ett GUI-sammanhang.
Kom ihåg att du måste ha en fil med namnet plugin-import-name-some_name.txt i insticksmodulens ZIP-fil för att detta ska fungera, som beskrivits ovan.
Det finns också ett par metoder som låter användaren anpassa insticksmodulen. Dessa diskuteras nedan.
ui.py¶
Låt oss nu titta på ui.py, som definierar själva insticksmodulen för användargränssnittet. Källkoden är utförligt kommenterad och bör vara självförklarande:
# 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¶
Den faktiska logiken som implementerar dialogrutan 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'])
Få resurser från insticksmodulens ZIP-fil¶
calibres system för inläsning av insticksmoduler definierar ett par inbyggda funktioner som gör det enkelt att hämta filer från insticksmodulens ZIP-fil.
- get_resources(name_or_list_of_names)
Den här funktionen ska anropas med en lista över sökvägar till filer i ZIP-filen. För att till exempel komma åt filen
icon.pngi mappen images i ZIP-filen använder duimages/icon.png. Använd alltid snedstreck som sökvägsavskiljare, även i Windows. Om du skickar in ett enda namn returnerar funktionen filens råa byte, eller None om namnet inte hittas i ZIP-filen. Om du skickar in mer än ett namn returnerar den en uppslagstabell som kopplar namnen till byte. Om ett namn inte hittas finns det inte med i den returnerade uppslagstabellen.- get_icons(name_or_list_of_names, plugin_name=’’)
En omslutande funktion för get_resources() som skapar QIcon-objekt från de råa byte som get_resources returnerar. Om ett namn inte hittas i ZIP-filen blir motsvarande QIcon null. För att stödja ikonteman ska du skicka in insticksmodulens namn i läsbar form som
plugin_name. Om användaren använder ett ikontema med ikoner för din insticksmodul prioriteras dessa vid inläsningen.
Aktivera användarkonfiguration för din insticksmodul¶
För att låta användare anpassa din insticksmodul måste du definiera tre metoder i insticksmodulens basklass: 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 från dess långa historia). Det rekommenderade sättet är att använda klassen JSONConfig, som lagrar konfigurationsinformationen 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
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()
Objektet prefs är nu tillgängligt i hela insticksmodulens kod med en enkel sats:
from calibre_plugins.interface_demo.config import prefs
Du kan se hur objektet prefs används i main.py:
def config(self):
self.do_user_config(parent=self)
# Apply the changes
self.label.setText(prefs['hello_world_msg'])
Insticksmoduler för bokredigeraren¶
Nu byter vi ämne en stund och tittar på hur du skapar en insticksmodul som lägger till verktyg i calibres bokredigerare. Insticksmodulen finns här: editor_demo_plugin.zip.
Precis som för alla insticksmoduler är det första steget att skapa den tomma textfilen som anger importnamnet, enligt beskrivningen ovan. Vi kallar filen plugin-import-name-editor_plugin_demo.txt.
Nu skapar vi den obligatoriska filen __init__.py, som innehåller metadata om insticksmodulen – namn, upphovsperson, version osv.
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 för redigeraren kan tillhandahålla flera verktyg. Varje verktyg motsvarar en knapp i verktygsfältet och ett alternativ i redigerarens meny Insticksmoduler. Dessa kan ha undermenyer om verktyget har flera relaterade åtgärder.
Alla verktyg måste definieras i filen main.py i din insticksmodul. Varje verktyg är en klass som ärver från klassen calibre.gui2.tweak_book.plugin.Tool. Låt oss titta på main.py från demoinsticksmodulen. Källkoden är utförligt kommenterad och bör vara självförklarande. Läs API-dokumentationen för klassen calibre.gui2.tweak_book.plugin.Tool för mer information.
main.py¶
Här definierar vi ett verktyg som multiplicerar alla teckensnittsstorlekar i boken med ett tal som användaren anger. Verktyget visar flera viktiga begrepp som du behöver när du utvecklar egna insticksmoduler, så läs den utförligt kommenterade källkoden noggrant.
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
Låt oss gå igenom main.py. Vi ser att filen definierar ett enda verktyg, med namnet Magnify fonts. Verktyget ber användaren om ett tal och multiplicerar alla teckensnittsstorlekar i boken med detta tal.
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 ingångspunkt är calibre.gui2.tweak_book.plugin.Tool.create_action(). Denna metod skapar de QAction-objekt som visas i verktygsfältet och menyn för insticksmoduler. Den kan också tilldela ett kortkommando som användaren kan anpassa. Signalen triggered från QAction kopplas till metoden ask_user(), som frågar användaren efter multiplikatorn för teckensnittsstorlekarna och sedan kör förstoringskoden.
Förstoringskoden är väl kommenterad och ganska enkel. Det viktigaste att observera är att du får en referens till redigeringsfönstret som self.gui och till redigerarens Boss som self.boss. Boss är objektet som styr redigerarens användargränssnitt. Det har många användbara metoder, som dokumenteras i klassen calibre.gui2.tweak_book.boss.Boss.
Slutligen finns self.current_container, som är en referens till boken som redigeras i form av ett calibre.ebooks.oeb.polish.container.Container-objekt. Det representerar boken som en samling av de HTML-, CSS- och bildfiler som ingår i den och har hjälpmetoder för många användbara uppgifter. Behållarobjektet och olika användbara hjälpfunktioner som du kan återanvända i insticksmodulens kod dokumenteras i API-dokumentation för e-bokredigeringsverktygen.
Lägga till översättningar i din insticksmodul¶
Du kan översätta alla gränssnittstexter i insticksmodulen och visa dem på det språk som är inställt för calibres huvudsakliga användargränssnitt.
Det första steget är att gå igenom insticksmodulens källkod och markera alla strängar som användaren ser som översättningsbara genom att omge dem med _(). 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 kompilerar du dem till .mo-filer. Du kan använda Poedit även för detta, eller helt enkelt kö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 anropa funktionen load_translations() överst i insticksmodulens .py-filer. Av prestandaskäl bör du bara anropa funktionen i de .py-filer som faktiskt har översättningsbara strängar. I en typisk insticksmodul för användargränssnittet anropar du den alltså överst i ui.py men inte i __init__.py.
Du kan testa översättningarna av dina insticksmoduler genom att ändra gränssnittsspråket i calibre under Inställningar → Gränssnitt → Utseende & känsla eller genom att köra calibre med miljövariabeln CALIBRE_OVERRIDE_LANG angiven. Till exempel:
CALIBRE_OVERRIDE_LANG=de
Ersätt de med språkkoden för det språk som du vill testa.
För översättningar med plural, använd funktionen ngettext() i stället för _(). Till exempel:
ngettext('Delete a book', 'Delete {} books', num_books).format(num_books)
API för insticksmoduler¶
Som du kanske har märkt ovan är en insticksmodul i calibre en klass. Det finns olika klasser för de olika typerna av insticksmoduler i calibre. Information om varje klass, inklusive basklassen för alla insticksmoduler, finns i API-dokumentation för insticksmoduler.
Din insticksmodul kommer nästan säkert att använda kod från calibre. Läs avsnittet Kodstruktur för att lära dig hur du hittar olika funktioner i calibres kodbas.
Felsöka insticksmoduler¶
Det 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å knappen Inställningar eller använda kortkommandot Ctrl+Skift+R.
När du kör från kommandoraden skrivs felsökningsutmatningen till konsolen. När du kör inifrån calibre skrivs den till en txt-fil.
Du kan infoga print-satser var som helst i insticksmodulens kod. Deras utmatning visas i felsökningsläge. Kom ihåg att detta är Python: du borde inte behöva något mer än print-satser för att felsöka ;) Jag utvecklade hela calibre med just denna felsökningsteknik.
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 stänger en körande instans av calibre, väntar tills den har avslutats, uppdaterar sedan insticksmodulen i calibre och startar calibre igen.
Fler exempel på insticksmoduler¶
Du kan hitta en lista med många sofistikerade calibre-insticksmoduler här.
