كتابة إضافاتك الخاصة لتوسيع وظائف calibre¶
يتميز calibre بتصميم معياري للغاية. تقريبًا جميع الوظائف في calibre تأتي على شكل إضافات. تُستخدم الإضافات للتحويل، ولتنزيل الأخبار (على الرغم من أنها تسمى وصفات)، لمكونات مختلفة من واجهة المستخدم، للاتصال بأجهزة مختلفة، لمعالجة الملفات عند إضافتها إلى calibre وهكذا. يمكنك الحصول على قائمة كاملة بجميع الإضافات المدمجة في calibre بالانتقال إلى التفضيلات → خيارات متقدمة → الإضافات.
هنا، سنعلمك كيفية إنشاء إضافاتك الخاصة لإضافة ميزات جديدة إلى calibre.
ملاحظة
هذا ينطبق فقط على إصدارات calibre >= 0.8.60
تشريح إضافة calibre¶
إضافة calibre بسيطة جدًا، إنها مجرد ملف ZIP يحتوي على بعض أكواد بايثون وأي موارد أخرى مثل ملفات الصور التي تحتاجها الإضافة. دون مزيد من اللغط، دعنا نرى مثالًا أساسيًا.
لنفترض أن لديك تثبيتًا لـ calibre تستخدمه لنشر مستندات إلكترونية مختلفة بتنسيقات EPUB و MOBI. ترغب في أن تكون جميع الملفات التي ينشئها calibre لها ناشر محدد باسم "Hello world"، إليك كيفية القيام بذلك. أنشئ ملفًا باسم __init__.py
(هذا اسم خاص ويجب استخدامه دائمًا للملف الرئيسي لإضافتك) وأدخل كود بايثون التالي فيه:
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
هذا كل شيء. لإضافة هذا الكود إلى calibre كإضافة، ما عليك سوى تشغيل ما يلي في المجلد الذي أنشأت فيه __init__.py
:
calibre-customize -b .
ملاحظة
على نظام macOS، توجد أدوات سطر الأوامر داخل حزمة calibre، على سبيل المثال، إذا قمت بتثبيت calibre في /Applications
فإن أدوات سطر الأوامر موجودة في /Applications/calibre.app/Contents/MacOS/
.
يمكنك تنزيل إضافة Hello World من helloworld_plugin.zip.
في كل مرة تستخدم فيها calibre لتحويل كتاب، سيتم استدعاء طريقة run()
الخاصة بالإضافة وسيكون للكتاب المحول ناشره المحدد بـ "Hello World". هذه إضافة بسيطة، دعنا ننتقل إلى مثال أكثر تعقيدًا يضيف مكونًا إلى واجهة المستخدم.
إضافة واجهة مستخدم¶
سيتم توزيع هذه الإضافة على عدد قليل من الملفات (للحفاظ على الكود نظيفًا). ستوضح لك كيفية الحصول على الموارد (الصور أو ملفات البيانات) من ملف ZIP الخاص بالإضافة، وكيفية السماح للمستخدمين بتكوين إضافتك، وكيفية إنشاء عناصر في واجهة مستخدم calibre، وكيفية الوصول إلى قاعدة بيانات الكتب والاستعلام عنها في calibre.
يمكنك تنزيل هذه الإضافة من interface_demo_plugin.zip
أول شيء يجب ملاحظته هو أن ملف ZIP هذا يحتوي على العديد من الملفات الإضافية، الموضحة أدناه، انتبه بشكل خاص إلى plugin-import-name-interface_demo.txt
.
- plugin-import-name-interface_demo.txt
ملف نصي فارغ يستخدم لتمكين سحر الإضافات متعددة الملفات. يجب أن يكون هذا الملف موجودًا في جميع الإضافات التي تستخدم أكثر من ملف .py واحد. يجب أن يكون فارغًا ويجب أن يكون اسم ملفه على الشكل:
plugin-import-name-**some_name**.txt
. يسمح وجود هذا الملف لك باستيراد الكود من ملفات .py الموجودة داخل ملف ZIP، باستخدام عبارة مثل:from calibre_plugins.some_name.some_module import some_objectيجب أن يكون البادئة
calibre_plugins
موجودة دائمًا. يأتيsome_name
من اسم ملف النص الفارغ. يشيرsome_module
إلى ملفsome_module.py
داخل ملف ZIP. لاحظ أن هذا الاستيراد قوي تمامًا مثل استيرادات بايثون العادية. يمكنك إنشاء حزم وحزم فرعية لوحدات .py داخل ملف ZIP، تمامًا كما تفعل عادةً (عن طريق تعريف __init__.py في كل مجلد فرعي)، ويجب أن يعمل كل شيء "فقط بشكل صحيح".الاسم الذي تستخدمه لـ
some_name
يدخل مساحة اسم عالمية مشتركة بين جميع الإضافات، لذا اجعله فريدًا قدر الإمكان. ولكن تذكر أنه يجب أن يكون معرف بايثون صالحًا (أحرف أبجدية وأرقام وشرطة سفلية فقط).- __init__.py
كما كان من قبل، الملف الذي يحدد فئة الإضافة
- main.py
يحتوي هذا الملف على الكود الفعلي الذي يقوم بشيء مفيد
- ui.py
يحدد هذا الملف جزء الواجهة من الإضافة
- images/icon.png
أيقونة هذه الإضافة
- about.txt
ملف نصي يحتوي على معلومات حول الإضافة
- translations
مجلد يحتوي على ملفات .mo مع ترجمات واجهة المستخدم الخاصة بـ إضافتك إلى لغات مختلفة. انظر أدناه للحصول على التفاصيل.
الآن دعنا نلقي نظرة على الكود.
__init__.py¶
أولاً، ملف __init__.py
الإلزامي لتحديد بيانات الإضافة الوصفية:
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()
الميزة الوحيدة الجديرة بالملاحظة هي الحقل actual_plugin
. بما أن calibre يحتوي على واجهات سطر الأوامر وواجهات رسومية، فإن إضافات الواجهة الرسومية مثل هذه لا يجب أن تقوم بتحميل أي مكتبات واجهة رسومية في __init__.py. يقوم حقل actual_plugin بذلك نيابة عنك، عن طريق إخبار calibre بأن الإضافة الفعلية موجودة في ملف آخر داخل أرشيف ZIP الخاص بك، والذي سيتم تحميله فقط في سياق الواجهة الرسومية.
تذكر أنه لكي يعمل هذا، يجب أن يكون لديك ملف plugin-import-name-some_name.txt في ملف ZIP الخاص بالإضافة، كما نوقش أعلاه.
يوجد أيضًا بعض الطرق لتمكين المستخدم من تكوين الإضافة. سيتم مناقشة هذه الطرق أدناه.
ui.py¶
الآن دعنا نلقي نظرة على ui.py الذي يحدد إضافة واجهة المستخدم الرسومية الفعلية. الكود المصدري مشروح بشكل مكثف ويجب أن يكون واضحًا بذاته:
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
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¶
المنطق الفعلي لتطبيق مربع حوار عرض إضافة الواجهة.
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'])
الحصول على الموارد من ملف ZIP الخاص بالإضافة¶
يحدد نظام تحميل الإضافات في calibre بعض الدوال المدمجة التي تسمح لك بالحصول على الملفات بسهولة من ملف ZIP الخاص بالإضافة.
- get_resources(name_or_list_of_names)
يجب استدعاء هذه الدالة بقائمة من المسارات إلى الملفات داخل ملف ZIP. على سبيل المثال، للوصول إلى الملف
icon.png
في مجلد الصور في ملف ZIP، ستستخدم:images/icon.png
. استخدم دائمًا شرطة مائلة للأمام كفاصل للمسار، حتى على نظام Windows. عندما تمرر اسمًا واحدًا، ستعيد الدالة البايتات الخام لذلك الملف أو None إذا لم يتم العثور على الاسم في ملف ZIP. إذا مررت أكثر من اسم، فستعيد قاموسًا يربط الأسماء بالبايتات. إذا لم يتم العثور على اسم، فلن يكون موجودًا في القاموس المعروض.- get_icons(name_or_list_of_names, plugin_name='')
غلاف لدالة get_resources() ينشئ كائنات QIcon من البايتات الخام التي تعيدها get_resources. إذا لم يتم العثور على اسم في ملف ZIP، فإن QIcon المقابل سيكون فارغًا. لدعم سمات الأيقونات، قم بتمرير الاسم الودي لإضافتك كـ
plugin_name
. إذا كان المستخدم يستخدم سمة أيقونات تحتوي على أيقونات لإضافتك، فسيتم تحميلها تفضيلًا.
تمكين تكوين المستخدم لإضافتك¶
للسماح للمستخدمين بتكوين إضافتك، يجب عليك تعريف ثلاث طرق في فئة الإضافة الأساسية الخاصة بك، is_customizable، config_widget و**save_settings** كما هو موضح أدناه:
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 على العديد من الطرق المختلفة لتخزين بيانات التكوين (إرث من تاريخه الطويل). الطريقة الموصى بها هي استخدام فئة JSONConfig، التي تخزن معلومات التكوين الخاصة بك في ملف .json.
الكود لإدارة بيانات التكوين في الإضافة التجريبية موجود في config.py:
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()
كائن prefs
متاح الآن في جميع أنحاء كود الإضافة بواسطة بسيط:
from calibre_plugins.interface_demo.config import prefs
يمكنك رؤية كائن prefs
المستخدم في main.py:
def config(self):
self.do_user_config(parent=self)
# Apply the changes
self.label.setText(prefs['hello_world_msg'])
تحرير إضافات الكتب¶
الآن دعنا نغير المسار قليلاً وننظر إلى إنشاء إضافة لإضافة أدوات إلى محرر الكتب في calibre. الإضافة متاحة هنا: editor_demo_plugin.zip.
الخطوة الأولى، كما هو الحال مع جميع الإضافات، هي إنشاء ملف txt فارغ لاسم الاستيراد، كما هو موضح أعلاه. سنقوم بتسمية الملف plugin-import-name-editor_plugin_demo.txt
.
الآن نقوم بإنشاء ملف __init__.py
الإلزامي الذي يحتوي على بيانات وصفية حول الإضافة -- اسمها، مؤلفها، إصدارها، إلخ.
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)
يمكن لإضافة محرر واحدة توفير أدوات متعددة، كل أداة تتوافق مع زر واحد في شريط الأدوات ومدخل في قائمة الإضافات في المحرر. يمكن أن تحتوي هذه على قوائم فرعية في حال كان للأداة إجراءات متعددة ذات صلة.
يجب تعريف جميع الأدوات في ملف main.py
في إضافتك. كل أداة هي فئة ترث من الفئة calibre.gui2.tweak_book.plugin.Tool
. دعنا نلقي نظرة على main.py
من الإضافة التجريبية، الكود المصدري مشروح بشكل مكثف ويجب أن يكون واضحًا بذاته. اقرأ وثائق API الخاصة بـ calibre.gui2.tweak_book.plugin.Tool
لمزيد من التفاصيل.
main.py¶
هنا سنرى تعريف أداة واحدة ستقوم بضرب جميع أحجام الخطوط في الكتاب برقم يوفره المستخدم. توضح هذه الأداة مفاهيم مهمة مختلفة ستحتاجها في تطوير إضافاتك الخاصة، لذا يجب عليك قراءة الكود المصدري (المشروح بكثافة) بعناية.
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
دعنا نحلل main.py
. نرى أنه يحدد أداة واحدة، تسمى تكبير الخطوط. ستطلب هذه الأداة من المستخدم رقمًا وتضرب جميع أحجام الخطوط في الكتاب بهذا الرقم.
أول شيء مهم هو اسم الأداة الذي يجب عليك تعيينه إلى سلسلة فريدة نسبيًا حيث سيتم استخدامه كمفتاح لهذه الأداة.
نقطة الدخول المهمة التالية هي calibre.gui2.tweak_book.plugin.Tool.create_action()
. تنشئ هذه الطريقة كائنات QAction التي تظهر في شريط أدوات الإضافات وقائمة الإضافات. كما أنها، اختياريًا، تعين اختصار لوحة مفاتيح يمكن للمستخدم تخصيصه. يتم توصيل إشارة التشغيل من QAction بطريقة ask_user() التي تطلب من المستخدم مضاعف حجم الخط، ثم تقوم بتشغيل كود التكبير.
كود التكبير مشروح جيدًا وبسيط إلى حد ما. الأشياء الرئيسية التي يجب ملاحظتها هي أنك تحصل على مرجع إلى نافذة المحرر كـ self.gui
و مدير المحرر كـ self.boss
. المدير هو الكائن الذي يتحكم في واجهة المستخدم للمحرر. يحتوي على العديد من الطرق المفيدة، الموثقة في فئة calibre.gui2.tweak_book.boss.Boss
.
أخيرًا، هناك self.current_container
وهو مرجع إلى الكتاب الذي يتم تحريره ككائن calibre.ebooks.oeb.polish.container.Container
. يمثل هذا الكتاب كمجموعة من ملفات HTML/CSS/الصور المكونة له ويحتوي على طرق ملائمة للقيام بالعديد من الأشياء المفيدة. كائن الحاوية والعديد من وظائف الأداة المساعدة المفيدة التي يمكن إعادة استخدامها في كود الإضافة الخاص بك موثقة في توثيق واجهة برمجة التطبيقات لأدوات تحرير الكتب الإلكترونية.
إضافة ترجمات إلى إضافتك¶
يمكنك ترجمة جميع سلاسل واجهة المستخدم في إضافتك وعرضها بأي لغة تم تعيينها لواجهة المستخدم الرئيسية لـ calibre.
الخطوة الأولى هي مراجعة الكود المصدري لإضافتك ووضع علامة على جميع السلاسل المرئية للمستخدم كقابلة للترجمة، عن طريق إحاطتها بـ _(). على سبيل المثال:
action_spec = (_('My plugin'), None, _('My plugin is cool'), None)
ثم استخدم برنامجًا لإنشاء ملفات .po من الكود المصدري لإضافتك. يجب أن يكون هناك ملف .po واحد لكل لغة تريد الترجمة إليها. على سبيل المثال: de.po للألمانية، fr.po للفرنسية وهكذا. يمكنك استخدام برنامج Poedit لهذا الغرض.
أرسل ملفات .po هذه إلى مترجميك. بمجرد استعادتها، قم بتجميعها إلى ملفات .mo. يمكنك مرة أخرى استخدام Poedit لذلك، أو ببساطة قم بـ:
calibre-debug -c "from calibre.translations.msgfmt import main; main()" filename.po
ضع ملفات .mo في مجلد translations
في إضافتك.
الخطوة الأخيرة هي ببساطة استدعاء الدالة load_translations() في أعلى ملفات .py لإضافتك. لأسباب تتعلق بالأداء، يجب عليك استدعاء هذه الدالة فقط في ملفات .py التي تحتوي بالفعل على سلاسل قابلة للترجمة. لذلك في إضافة واجهة مستخدم نموذجية، ستستدعيها في أعلى ui.py
ولكن ليس __init__.py
.
يمكنك اختبار ترجمات إضافاتك عن طريق تغيير لغة واجهة المستخدم في calibre ضمن التفضيلات → الواجهة → المظهر والإحساس أو عن طريق تشغيل calibre مع تعيين متغير البيئة CALIBRE_OVERRIDE_LANG
. على سبيل المثال:
CALIBRE_OVERRIDE_LANG=de
استبدل de
برمز اللغة التي تريد اختبارها.
بالنسبة للترجمات التي تحتوي على صيغ الجمع، استخدم الدالة ngettext()
بدلاً من _()
. على سبيل المثال:
ngettext('Delete a book', 'Delete {} books', num_books).format(num_books)
واجهة برمجة تطبيقات الإضافات¶
كما لاحظت أعلاه، الإضافة في calibre هي فئة. توجد فئات مختلفة لأنواع الإضافات المختلفة في calibre. يمكن العثور على التفاصيل حول كل فئة، بما في ذلك الفئة الأساسية لجميع الإضافات، في توثيق API للإضافات.
من المؤكد أن إضافتك ستستخدم كود من calibre. لمعرفة كيفية العثور على أجزاء مختلفة من الوظائف في قاعدة كود calibre، اقرأ القسم الخاص بـ تخطيط الكود في calibre.
تصحيح أخطاء الإضافات¶
الخطوة الأولى والأكثر أهمية هي تشغيل calibre في وضع التصحيح. يمكنك القيام بذلك من سطر الأوامر باستخدام:
calibre-debug -g
أو من داخل calibre بالنقر بزر الفأرة الأيمن على زر التفضيلات أو باستخدام اختصار لوحة المفاتيح Ctrl+Shift+R.
عند التشغيل من سطر الأوامر، سيتم طباعة إخراج التصحيح إلى وحدة التحكم، عند التشغيل من داخل calibre، سيذهب الإخراج إلى ملف txt.
يمكنك إدراج عبارات الطباعة في أي مكان في كود إضافتك، وسيتم إخراجها في وضع التصحيح. تذكر، هذه بايثون، لا يجب أن تحتاج أكثر من عبارات الطباعة للتصحيح ;) لقد طورت calibre بالكامل باستخدام تقنية التصحيح هذه فقط.
يمكنك اختبار التغييرات على إضافتك بسرعة باستخدام سطر الأوامر التالي:
calibre-debug -s; calibre-customize -b /path/to/your/plugin/folder; calibre
سيؤدي هذا إلى إيقاف تشغيل calibre قيد التشغيل، والانتظار حتى يكتمل الإغلاق، ثم تحديث إضافتك في calibre وإعادة تشغيل calibre.
المزيد من أمثلة الإضافات¶
يمكنك العثور على قائمة بالعديد من إضافات calibre المتطورة هنا.