직접 플러그인을 작성하여 calibre 기능 확장하기¶
calibre는 매우 모듈화된 설계를 가지고 있습니다. calibre의 거의 모든 기능이 플러그인 형태로 제공됩니다. 플러그인은 변환, 뉴스 다운로드(이것은 레시피라고 합니다), 사용자 인터페이스의 다양한 구성 요소, 다양한 장치 연결, calibre에 파일 추가 시 파일 처리 등에 사용됩니다. :guilabel:`환경설정->고급->플러그인`으로 이동하여 calibre의 모든 내장 플러그인의 전체 목록을 확인할 수 있습니다.
여기에서는 calibre에 새 기능을 추가하기 위해 직접 플러그인을 만드는 방법을 설명합니다.
참고
이것은 calibre 릴리스 >= 0.8.60에만 적용됩니다
calibre 플러그인의 구조¶
calibre 플러그인은 매우 간단합니다. 일부 Python 코드와 플러그인에 필요한 이미지 파일과 같은 기타 리소스가 포함된 ZIP 파일일 뿐입니다. 바로 기본 예시를 살펴보겠습니다.
EPUB 및 MOBI 형식의 다양한 전자 문서를 자체 발행하는 데 사용하는 calibre 설치가 있다고 가정합니다. calibre에서 생성된 모든 파일의 발행자를 “Hello world”로 설정하고 싶다면 다음 방법을 따르세요. :file:`__init__.py`라는 파일을 만드세요(이것은 특별한 이름이며 플러그인의 메인 파일에 항상 사용해야 합니다). 그리고 다음 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
이것이 전부입니다. 이 코드를 플러그인으로 calibre에 추가하려면 :file:`__init__.py`를 만든 폴더에서 다음을 실행하세요:
calibre-customize -b .
참고
macOS에서는 명령줄 도구가 calibre 번들 안에 있습니다. 예를 들어, calibre를 /Applications`에 설치한 경우 명령줄 도구는 :file:/Applications/calibre.app/Contents/MacOS/`에 있습니다.
:download_file:`helloworld_plugin.zip`에서 Hello World 플러그인을 다운로드할 수 있습니다.
calibre를 사용하여 책을 변환할 때마다 플러그인의 run() 메서드가 호출되고 변환된 책의 발행자가 “Hello World”로 설정됩니다. 이것은 간단한 플러그인이며, 이제 실제로 사용자 인터페이스에 구성 요소를 추가하는 더 복잡한 예시로 넘어가겠습니다.
사용자 인터페이스 플러그인¶
이 플러그인은 여러 파일에 걸쳐 있습니다(코드를 깔끔하게 유지하기 위해). 플러그인 ZIP 파일에서 리소스(이미지 또는 데이터 파일)를 가져오는 방법, 사용자가 플러그인을 구성할 수 있게 하는 방법, calibre 사용자 인터페이스에 요소를 만드는 방법, calibre의 책 데이터베이스에 접근하고 쿼리하는 방법을 보여줍니다.
:download_file:`interface_demo_plugin.zip`에서 이 플러그인을 다운로드할 수 있습니다
먼저 이 ZIP 파일에는 아래 설명된 것처럼 더 많은 파일이 포함되어 있습니다. ``plugin-import-name-interface_demo.txt``에 특히 주의하세요.
- plugin-import-name-interface_demo.txt
다중 파일 플러그인 기능을 활성화하는 데 사용되는 빈 텍스트 파일입니다. 이 파일은 하나 이상의 .py 파일을 사용하는 모든 플러그인에 있어야 합니다. 비어 있어야 하며 파일명은
plugin-import-name-**some_name**.txt형식이어야 합니다. 이 파일이 있으면 다음과 같은 문을 사용하여 ZIP 파일 안에 있는 .py 파일에서 코드를 가져올 수 있습니다:from calibre_plugins.some_name.some_module import some_object접두사 ``calibre_plugins``는 항상 있어야 합니다. ``some_name``은 빈 텍스트 파일의 파일명에서 옵니다. ``some_module``은 ZIP 파일 안의
some_module.py파일을 가리킵니다. 이 가져오기는 일반 Python 가져오기만큼 강력합니다. ZIP 파일 안에 .py 모듈의 패키지와 하위 패키지를 평소처럼(각 하위 폴더에 __init__.py를 정의하여) 만들 수 있으며, 모든 것이 “그냥 작동”해야 합니다.``some_name``에 사용하는 이름은 모든 플러그인이 공유하는 전역 네임스페이스에 들어갑니다. 따라서 가능한 한 고유하게 만드세요. 유효한 Python 식별자(알파벳, 숫자, 밑줄만 가능)여야 한다는 점을 기억하세요.
- __init__.py
이전과 같이, 플러그인 클래스를 정의하는 파일
- main.py
이 파일에는 유용한 작업을 수행하는 실제 코드가 포함되어 있습니다
- ui.py
이 파일은 플러그인의 인터페이스 부분을 정의합니다
- images/icon.png
이 플러그인의 아이콘
- about.txt
플러그인에 대한 정보가 포함된 텍스트 파일
- 번역
플러그인의 사용자 인터페이스를 다양한 언어로 번역한 .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에는 명령줄과 GUI 인터페이스가 모두 있으므로, 이와 같은 GUI 플러그인은 __init__.py에서 GUI 라이브러리를 로드하면 안 됩니다. actual_plugin 필드는 실제 플러그인이 ZIP 아카이브 안의 다른 파일에 있으며 GUI 컨텍스트에서만 로드된다는 것을 calibre에 알려줌으로써 이를 대신 처리합니다.
이것이 작동하려면 위에서 설명한 것처럼 플러그인 ZIP 파일에 plugin-import-name-some_name.txt 파일이 있어야 합니다.
또한 플러그인의 사용자 구성을 활성화하는 몇 가지 방법이 있습니다. 이러한 내용은 아래에서 설명합니다.
ui.py¶
이제 실제 GUI 플러그인을 정의하는 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 파일 안의 파일 경로 목록과 함께 호출해야 합니다. 예를 들어 ZIP 파일의 images 폴더에 있는
icon.png파일에 접근하려면 ``images/icon.png``를 사용하세요. Windows에서도 항상 정방향 슬래시를 경로 구분자로 사용하세요. 단일 이름을 전달하면 해당 파일의 원시 바이트를 반환하고, ZIP 파일에서 이름을 찾을 수 없으면 None을 반환합니다. 둘 이상의 이름을 전달하면 이름을 바이트에 매핑하는 딕셔너리를 반환합니다. 이름을 찾을 수 없으면 반환된 딕셔너리에 포함되지 않습니다.- get_icons(name_or_list_of_names, plugin_name=’’)
get_resources()의 래퍼로, get_resources가 반환한 원시 바이트에서 QIcon 객체를 생성합니다. ZIP 파일에서 이름을 찾을 수 없으면 해당 QIcon은 null이 됩니다. 아이콘 테마를 지원하려면 플러그인의 사용자 친화적인 이름을 ``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에는 구성 데이터를 저장하는 다양한 방법이 있습니다(오랜 역사의 유산). 권장되는 방법은 구성 정보를 .json 파일에 저장하는 JSONConfig 클래스를 사용하는 것입니다.
데모 플러그인에서 구성 데이터를 관리하는 코드는 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
main.py에서 prefs 객체가 사용되는 것을 확인할 수 있습니다:
def config(self):
self.do_user_config(parent=self)
# Apply the changes
self.label.setText(prefs['hello_world_msg'])
책 편집 플러그인¶
이제 방향을 바꾸어 calibre 책 편집기에 도구를 추가하는 플러그인을 만드는 방법을 살펴보겠습니다. 플러그인은 여기서 다운로드할 수 있습니다: editor_demo_plugin.zip.
모든 플러그인과 마찬가지로 첫 번째 단계는 :ref:`위 <import_name_txt>`에서 설명한 대로 가져오기 이름 빈 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``를 살펴보겠습니다. 소스 코드에 주석이 많이 달려 있어 자체적으로 설명이 될 것입니다. 자세한 내용은 calibre.gui2.tweak_book.plugin.Tool 클래스의 API 문서를 참조하세요.
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``를 분석해 보겠습니다. *글꼴 확대*라는 단일 도구를 정의하는 것을 볼 수 있습니다. 이 도구는 사용자에게 숫자를 요청하고 책의 모든 글꼴 크기를 해당 숫자로 곱합니다.
첫 번째 중요한 것은 도구 이름입니다. 이 도구의 키로 사용되므로 비교적 고유한 문자열로 설정해야 합니다.
다음 중요한 진입점은 :meth:`calibre.gui2.tweak_book.plugin.Tool.create_action`입니다. 이 메서드는 플러그인 도구 모음과 플러그인 메뉴에 나타나는 QAction 객체를 생성합니다. 또한 선택적으로 사용자가 맞춤 설정할 수 있는 키보드 단축키를 할당합니다. QAction의 triggered 시그널은 사용자에게 글꼴 크기 배수를 요청한 다음 확대 코드를 실행하는 ask_user() 메서드에 연결됩니다.
확대 코드는 주석이 잘 달려 있고 꽤 간단합니다. 주목할 점은 편집기 창의 참조를 ``self.gui``로, 편집기 *Boss*를 ``self.boss``로 얻는 것입니다. *Boss*는 편집기 사용자 인터페이스를 제어하는 객체입니다. calibre.gui2.tweak_book.boss.Boss 클래스에 문서화된 많은 유용한 메서드를 가지고 있습니다.
마지막으로, 편집 중인 책에 대한 참조인 ``self.current_container``가 있습니다. calibre.ebooks.oeb.polish.container.Container 객체입니다. 이것은 책을 구성하는 HTML/CSS/이미지 파일의 모음으로 표현하며, 많은 유용한 작업을 수행하는 편의 메서드를 가지고 있습니다. 컨테이너 객체와 플러그인 코드에서 재사용할 수 있는 다양한 유용한 유틸리티 함수는 :ref:`polish_api`에 문서화되어 있습니다.
플러그인에 번역 추가¶
플러그인의 모든 사용자 인터페이스 문자열을 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 폴더에 넣으세요.
마지막 단계는 플러그인의 .py 파일 상단에서 load_translations() 함수를 호출하는 것입니다. 성능상의 이유로 실제로 번역 가능한 문자열이 있는 .py 파일에서만 이 함수를 호출해야 합니다. 따라서 일반적인 사용자 인터페이스 플러그인에서는 ui.py 상단에서 호출하되 ``__init__.py``에서는 호출하지 않습니다.
calibre의 환경설정 → 인터페이스 → 모양과 느낌`에서 사용자 인터페이스 언어를 변경하거나 ``CALIBRE_OVERRIDE_LANG` 환경 변수를 설정하여 calibre를 실행하여 플러그인의 번역을 테스트할 수 있습니다. 예:
CALIBRE_OVERRIDE_LANG=de
``de``를 테스트하려는 언어의 언어 코드로 바꾸세요.
복수형이 있는 번역의 경우 _() 대신 ngettext() 함수를 사용하세요. 예:
ngettext('Delete a book', 'Delete {} books', num_books).format(num_books)
플러그인 API¶
위에서 눈치채셨겠지만, calibre의 플러그인은 클래스입니다. calibre에는 다양한 유형의 플러그인에 대한 서로 다른 클래스가 있습니다. 모든 플러그인의 기본 클래스를 포함한 각 클래스에 대한 세부 정보는 :ref:`plugins`에서 확인할 수 있습니다.
플러그인은 거의 확실히 calibre의 코드를 사용하게 됩니다. calibre 코드베이스에서 다양한 기능을 찾는 방법을 알아보려면 calibre 코드 구성 섹션을 읽어보세요.
플러그인 디버깅¶
첫 번째이자 가장 중요한 단계는 디버그 모드에서 calibre를 실행하는 것입니다. 명령줄에서 다음을 실행할 수 있습니다:
calibre-debug -g
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.
명령줄에서 실행할 경우 디버그 출력은 콘솔에 표시되고, Calibre 내에서 실행할 경우 출력은 텍스트 파일에 저장됩니다.
플러그인 코드 어디에나 print 문을 삽입할 수 있으며, 디버그 모드에서 출력됩니다. 기억하세요, 이것은 Python이므로 디버깅에 print 문 이상의 것이 필요하지 않습니다 ;) 저는 이 디버깅 기법만으로 calibre 전체를 개발했습니다.
다음 명령줄을 사용하면 플러그인 변경 사항을 빠르게 테스트할 수 있습니다:
calibre-debug -s; calibre-customize -b /path/to/your/plugin/folder; calibre
실행 중인 calibre를 종료하고, 종료가 완료될 때까지 기다린 다음, calibre에서 플러그인을 업데이트하고 calibre를 다시 시작합니다.
더 많은 플러그인 예시¶
많은 정교한 calibre 플러그인 목록은 `여기 <https://www.mobileread.com/forums/showthread.php?t=118764>`_에서 확인할 수 있습니다.
