Scrivere i propri plugin per estendere le funzionalità di calibre¶
calibre ha una struttura estremamente modulare. Quasi tutte le funzionalità di calibre agiscono sotto forma di plugin. Dei plugin sono usati per le conversioni, per scaricare le notizie (sebbene questi siano chiamati ricette), per molte componenti dell’interfaccia utente, per connettersi a svariati dispositivi, per elaborare i file aggiunti a calibre e così via. Puoi ottenere una lista completa dei plugin inclusi in calibre andando in Preferenze → Avanzate → Plugin.
Qui, ti insegneremo come creare i tuoi plugin per aggiungere nuove funzioni a calibre.
Nota
Si applica solo alle versioni di calibre >= 0.8.60
Anatomia di un plugin di calibre¶
Un plugin di calibre è molto semplice, è solo un file ZIP che contiene del codice in Python e qualsiasi altra risorsa necessaria al plugin, come le immagini. Senza indugiare oltre, vediamo un semplice esempio.
Immagina di avere un’installazione di calibre che usi per l’autopubblicazione di vari documenti elettronici nei formati EPUB e MOBI. Vorresti fare in modo che tutti i file generati da calibre avessero «Hello world» impostato come editore, ecco il modo di farlo. Crea un file con il nome __init__.py
(questo è un nome speciale che deve sempre essere usato per il file principale del tuo plugin) e inseriscivi il seguente codice Python:
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
Questo è tutto. Per aggiungere questo codice a calibre come plugin, devi solo eseguire il comando che segue nella cartella in cui hai creato __init__.py
:
calibre-customize -b .
Nota
In macOS, gli strumenti da linea di comando si trovano dentro il bundle di calibre, per esempio, se hai installato calibre in /Applications
gli strumenti si trovano in /Applications/calibre.app/Contents/MacOS/
.
Puoi scaricare il plugin Hello World da helloworld_plugin.zip.
Ogni volta che usi calibre per convertire un libro il metodo run()
del plugin sarà chiamato, e il libro convertito vedrà il suo editore impostato a «Hello World». Questo è un plugin molto semplice, passiamo a un esempio più complesso che aggiunge realmente un componente all’interfaccia utente.
Un plugin dell’interfaccia utente¶
Questo plugin sarà distribuito in pochi file (per mantenere il codice pulito). Vi mostrerà come ottenere risorse (immagini o file di dati) dal file ZIP del plugin, come consentire agli utenti di configurare il plugin, come creare elementi nell’interfaccia utente di calibre e come accedere e interrogare il database dei libri in Calibre.
Il plugin può essere scaricato da interface_demo_plugin.zip.
La prima cosa da notare è che questo file ZIP contiene molti altri file, spiegati di seguito; prestate particolare attenzione a plugin-import-name-interface_demo.txt
.
- plugin-import-name-interface_demo.txt
Un file di testo vuoto usato per abilitare la magia dei plugin multi-file. Questo file deve essere presente in tutti i plugin che utilizzano più di un file .py. Deve essere vuoto e il suo nome deve essere della forma:
plugin-import-name-**some_name**.txt
. La presenza di questo file consente di importare codice dai file .py presenti all’interno del file ZIP, utilizzando un’istruzione del tipo:from calibre_plugins.some_name.some_module import some_objectIl prefisso
calibre_plugins'' deve essere sempre presente. ``some_name
proviene dal nome del file di testo vuoto.some_module
si riferisce al filesome_module.py
all’interno del file ZIP. Si noti che questa importazione è altrettanto potente delle normali importazioni di Python. Si possono creare pacchetti e sottopacchetti di moduli .py all’interno del file ZIP, proprio come si farebbe normalmente (defining __init__.py in ogni sottocartella) e tutto dovrebbe «funzionare».Il nome che si usa per ``some_name”” entra in uno spazio dei nomi globale condiviso da tutti i plugin, quindi deve essere il più unico possibile. Ma ricordate che deve essere un identificatore Python valido (solo alfabeti, numeri e trattino basso).
- __init__.py
Come in precedenza, il file che definisce la classe del plugin
- main.py
Questo file contiene il codice vero e proprio che fa qualcosa di utile
- ui.py
Questo file definisce la parte dell’interfaccia del plugin
- images/icon.png
L’icona per questo plugin
- about.txt
File di testo con informazioni sul plugin
- traduzioni
La cartella contiene file .mo con l’interfaccia utente del tuo plugin tradotta in diverse lingue. Guarda più in basso per i dettagli.
Ora diamo un’occhiata al codice.
__init__.py¶
Innanzitutto, l’obbligatorio __init__.py
per definire i metadati del plugin:
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()
L’unica caratteristica degna di nota è il campo actual_plugin
. Poiché calibre ha sia un’interfaccia a riga di comando che una GUI, i plugin GUI come questo non dovrebbero caricare alcuna libreria GUI in __init__.py. Il campo actual_plugin fa questo per voi, dicendo a calibre che il plugin vero e proprio si trova in un altro file all’interno dell’archivio ZIP, che verrà caricato solo in un contesto GUI.
Ricordare che, per funzionare, è necessario avere un file plugin-import-name-some_name.txt nel file ZIP del plugin, come discusso in precedenza.
Esistono inoltre un paio di metodi per abilitare la configurazione del plugin da parte dell’utente. Questi sono discussi di seguito.
ui.py¶
Ora diamo un’occhiata a ui.py, che definisce il plugin GUI vero e proprio. Il codice sorgente è pesantemente commentato e dovrebbe essere autoesplicativo:
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¶
La logica effettiva per implementare la finestra di dialogo Demo del plugin di interfaccia.
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'])
Ottenere le risorse dal file ZIP del plugin¶
Il sistema di caricamento dei plugin di Calibre definisce un paio di funzioni integrate che consentono di ottenere comodamente i file dal file ZIP del plugin.
- get_resources(name_or_list_of_names)
Questa funzione deve essere chiamata con un elenco di percorsi dei file all’interno del file ZIP. Ad esempio, per accedere al file
icon.png
nella cartella images del file ZIP, si deve usare:images/icon.png
. Usare sempre una barra in avanti come separatore di percorso, anche su Windows. Quando si passa un solo nome, la funzione restituisce i byte grezzi di quel file o Nessuno se il nome non è stato trovato nel file ZIP. Se si inserisce più di un nome, la funzione restituisce un dizionario che mappa i nomi in byte. Se un nome non viene trovato, non sarà presente nel dizionario restituito.- get_icons(nome_o_lista_di_nomi, nome_plugin=””)
Un wrapper per get_resources() che crea oggetti QIcon dai byte grezzi restituiti da get_resources. Se un nome non viene trovato nel file ZIP, la QIcon corrispondente sarà nulla. Per supportare la tematizzazione delle icone, passare il nome umano del plugin come
plugin_name
. Se l’utente utilizza un tema di icone con icone per il plugin, queste verranno caricate in modo preferenziale.
Abilitazione della configurazione del plugin da parte dell’utente¶
Per consentire agli utenti di configurare il plugin, è necessario definire tre metodi nella classe base del plugin, is_customizable, config_widget e save_settings, come mostrato di seguito:
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 ha molti modi diversi per memorizzare i dati di configurazione (un’eredità della sua lunga storia). Il modo consigliato è quello di usare la classe JSONConfig, che memorizza le informazioni di configurazione in un file .json.
Il codice per gestire i dati di configurazione nel plugin demo si trova 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()
L’oggetto ``prefs”” è ora disponibile in tutto il codice del plugin tramite un semplice:
from calibre_plugins.interface_demo.config import prefs
Si può vedere l’oggetto ``prefs”” utilizzato in main.py:
self.do_user_config(parent=self)
# Apply the changes
self.label.setText(prefs['hello_world_msg'])
Modifica dei plugin del libro¶
Ora cambiamo un po” marcia e vediamo di creare un plugin per aggiungere strumenti all’editor di libri di Calibre. Il plugin è disponibile qui: editor_demo_plugin.zip.
Il primo passo, come per tutti i plugin, è creare il file txt vuoto con il nome dell’importazione, come descritto :ref:` sopra<import_name_txt>. Chiameremo il file plugin-import-name-editor_plugin_demo.txt
.
Ora creiamo il file obbligatorio __init__.py
che contiene i metadati sul plugin: il suo nome, l’autore, la versione, ecc.
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)
Un singolo plugin dell’editor può fornire più strumenti, ciascuno dei quali corrisponde a un singolo pulsante nella barra degli strumenti e a una voce nel menu Plugins dell’editor. Questi possono avere dei sottomenu nel caso in cui lo strumento abbia più azioni correlate.
Gli strumenti devono essere tutti definiti nel file main.py
del plugin. Ogni strumento è una classe che eredita dalla classe calibre.gui2.tweak_book.plugin.Tool
. Diamo un’occhiata a main.py
del plugin demo; il codice sorgente è pesantemente commentato e dovrebbe essere autoesplicativo. Per maggiori dettagli, leggete i documenti API della classe calibre.gui2.tweak_book.plugin.Tool
.
main.py¶
Qui vedremo la definizione di un singolo strumento che moltiplica tutte le dimensioni dei caratteri del libro per un numero fornito dall’utente. Questo strumento dimostra vari concetti importanti che vi serviranno per sviluppare i vostri plugin, quindi dovreste leggere attentamente il codice sorgente (pesantemente commentato).
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
Analizziamo main.py
. Vediamo che definisce un singolo strumento, chiamato Magnify fonts. Questo strumento chiede all’utente un numero e moltiplica tutte le dimensioni dei caratteri nel libro per quel numero.
La prima cosa importante è il nome dello strumento, che deve essere impostato con una stringa relativamente unica, in quanto verrà utilizzato come chiave per questo strumento.
Il prossimo punto di ingresso importante è il metodo calibre.gui2.tweak_book.plugin.Tool.create_action()
. Questo metodo crea gli oggetti QAction che appaiono nella barra degli strumenti e nel menu del plugin. Inoltre, opzionalmente, assegna una scorciatoia da tastiera che l’utente può personalizzare. Il segnale di attivazione della QAction è collegato al metodo ask_user() che chiede all’utente il moltiplicatore della dimensione del carattere e poi esegue il codice di ingrandimento.
Il codice dell’ingrandimento è ben commentato e abbastanza semplice. Le cose principali da notare sono che si ottiene un riferimento alla finestra dell’editor come self.gui
e all’editor Boss come self.boss
. Il Boss è l’oggetto che controlla l’interfaccia utente dell’editor. Ha molti metodi utili, documentati nella classe calibre.gui2.tweak_book.boss.Boss
.
Infine, c’è self.current_container
che è un riferimento al libro che si sta editando come oggetto calibre.ebooks.oeb.polish.container.Container
. Questo rappresenta il libro come un insieme di file HTML/CSS/immagine che lo compongono e dispone di metodi di utilità per fare molte cose utili. L’oggetto container e le varie funzioni di utilità che possono essere riutilizzate nel codice del plugin sono documentate in Documentazione API per gli strumenti di modifica di un ebook.
Aggiungi traduzione al tuo plugin¶
È possibile far tradurre tutte le stringhe dell’interfaccia utente del plugin e visualizzarle nella lingua impostata per l’interfaccia utente principale di Calibre.
Il primo passo consiste nell’esaminare il codice sorgente del plugin e contrassegnare tutte le stringhe visibili dall’utente come traducibili, circondandole con _(). Ad esempio:
action_spec = (_('My plugin'), None, _('My plugin is cool'), None)
Quindi utilizzare un programma per generare file .po dal codice sorgente del plugin. Dovrebbe esserci un file .po per ogni lingua in cui si vuole tradurre. Per esempio: de.po per il tedesco, fr.po per il francese e così via. Si può usare il programma Poedit per questo.
Inviate questi file .po ai vostri traduttori. Una volta ricevuti, compilateli in file .mo. A tale scopo si può usare Poedit o semplicemente fare:
calibre-debug -c "from calibre.translations.msgfmt import main; main()" filename.po
Mettere i file .mo nella cartella translations
del plugin.
L’ultimo passo consiste nel chiamare la funzione caricare_traduzioni() all’inizio dei file .py del plugin. Per ragioni di prestazioni, si dovrebbe chiamare questa funzione solo nei file .py che hanno effettivamente stringhe traducibili. Quindi, in un tipico plugin di interfaccia utente, la si dovrebbe chiamare all’inizio di ui.py
, ma non di __init__.py
.
È possibile verificare le traduzioni dei propri plugin cambiando la lingua dell’interfaccia utente in calibre sotto Preferences → Interface → Look & feel o eseguendo calibre con la variabile d’ambiente CALIBRE_OVERRIDE_LANG
. Per esempio:
CALIBRE_OVERRIDE_LANG=de
Sostituire ``de”” con il codice della lingua che si vuole testare.
Per le traduzioni con i plurali, utilizzare la funzione ngettext()
invece di _()
. Ad esempio:
ngettext('Delete a book', 'Delete {} books', num_books).format(num_books)
API del plugin¶
Come avrete notato sopra, un plugin in Calibre è una classe. Esistono diverse classi per i diversi tipi di plugin in Calibre. I dettagli su ciascuna classe, compresa la classe base di tutti i plugin, si trovano in Documentazione API per i plugin.
Il vostro plugin utilizzerà quasi certamente del codice di Calibre. Per sapere come trovare varie funzionalità nella base di codice di Calibre, leggere la sezione sul Vista codice di Calibre.
Debug dei plugin¶
Il primo passo, il più importante, è quello di eseguire Calibre in modalità debug. È possibile farlo dalla riga di comando con:
calibre-debug -g
Oppure dall’interno di Calibre facendo clic con il tasto destro del mouse sul pulsante Preferences o usando la scorciatoia da tastiera Ctrl+Shift+R.
Quando si esegue dalla riga di comando, l’output di debug viene stampato sulla console, mentre quando si esegue dall’interno di Calibre l’output viene inviato a un file txt.
Si possono inserire dichiarazioni di stampa in qualsiasi punto del codice del plugin, che verranno emesse in modalità di debug. Ricordate, questo è Python, non dovrebbe essere necessario altro che dichiarazioni di stampa per il debug ;) Ho sviluppato tutto Calibre utilizzando proprio questa tecnica di debug.
È possibile testare rapidamente le modifiche apportate al plugin utilizzando la seguente riga di comando:
calibre-debug -s; calibre-customize -b /path/to/your/plugin/folder; calibre
In questo modo si spegne un Calibre in esecuzione, si attende il completamento dello spegnimento, quindi si aggiorna il plugin in Calibre e si rilancia Calibre.
Più esempi di plugin¶
È possibile trovare un elenco di molti sofisticati plugin di Calibre qui.