Sigil 插件编写入门
本文最后更新于:2020年7月24日 下午
最近想要阅读几本繁体中文版的epub
书籍,于是想一键翻译成简体中文方便阅读,本以为Sigil
会有相关功能,却发现似乎连相关的插件都没有。
然而Sigil
的插件是基于Python
的,这就很容易寻找到相关的开源库——OpenCC
,说来萌发编写的想法也不见为奇了。
但网上对于 Sigil
插件编写的相关资料很少,基本只有官方的一本不完全的插件开发的epub
电子书和github
上的一些开源的插件代码。
简单写写此文,希望对想要入门 Sigil
插件编写的朋友提供一些帮助。
插件文件夹结构
[Plugin Name]
├── plugin.py
└── plugin.xml
你的插件目录下至少需要有plugin.py
、plugin.xml
两个文件,plugin.py
是插件的具体实现,plugin.xml
记录插件的一些具体信息,如版本号。
对于plugin.xml
,它的内容应该像下面一样。
<?xml version=""1.0"" encoding=""UTF-8""?>
<plugin>
<name>[Plugin Name]</name>
<type>[type]</type>
<author>SpaceSkyNet</author>
<description>[description]</description>
<engine>[engine]</engine>
<version>0.1</version>
<oslist>[oslist]</oslist>
<autostart>true</autostart>
</plugin>
变量 | 内容 |
---|---|
[Plugin Name] | 插件名称,与文件夹名一致(且不能使用中文) |
[type] | 插件类型,Sigil 已定义(从input, validation, edit, output中选择) |
[description] | 插件简介 |
[engine] | 插件运行引擎,一般来说从python2.7, python3.4中选择一个或者以逗号分隔填入两个(取决于plugin.py 的运行环境) |
[oslist] | 插件运行操作系统,从osx, unx, win中选择并组合,两个及以上以逗号分隔 |
插件有四种类型输入
、验证
、编辑
、输出
(input, validation, edit, output),不同类型有一些差别,本文仅讲述相同之处。(毕竟是入门教程)
插件的编写
将plugin.xml
处理好了后,就可以开始编写plugin.py
了。
如果要程序与用户交互,最好使用图形界面,所以至少掌握到PyQt
、Tkinter
等GUI开发框架的入门级别(不用交互可以忽略这句话)。
当然,也得了解一下epub的文件结构。
程序入口
Sigil 会默认将plugin.py
中的run
函数作为程序的入口,并默认传入bk
参数(bk
是 Sigil 提供的 epub 书籍内容的一个对象,可以利用它访问并操作 epub 内的文件)。
def run(bk):
return yourFunction(bk)
返回值为 $0$ 表示程序正常结束。
程序实现
为了程序显得比较模块化,我把它分成了三个部分。这里的GUI开发框架使用PyQt。
主体
用来统一GUI的交互和后台处理的函数。
def yourFunction(bk):
item = {""some_argv"": 0}
app = QApplication(sys.argv)
QtGUI = youtQtGUI(app=app, items=items, bk=bk)
QtGUI.show()
rtnCode = app.exec_()
if rtnCode != 1:
print('User abort by closing Setting dialog')
return -1
return yourProcessFunction(bk, item)
可以通过一个item
字典和用户在GUI上交互,获得或传出需要的信息。
可以通过指定GUI类的返回值判断GUI是否是正常按照既定的程序逻辑退出。
GUI
用来与用户交互。
class youtQtGUI(QDialog):
def __init__(self,
app=None,
parent=None,
bk=None,
items=None):
super(youtQtGUI, self).__init__(parent)
self.app = app
self.items = items
self.setWindowIcon(QIcon(os.path.join(bk._w.plugin_dir, '[Plugin Name]', 'plugin.png')))
layout = QVBoxLayout()
choice_info = '选择对象:'
layout.addWidget(QLabel(choice_info))
self.choice_list = ['Test1', 'Test2', 'Test3',]
self.combobox = QComboBox(self)
self.combobox.addItems(self.choice_list)
self.combobox.setCurrentIndex(items['some_argv'])
layout.addWidget(self.combobox)
self.combobox.currentIndexChanged.connect(lambda: self.on_combobox_func())
self.btn = QPushButton('确定', self)
self.btn.clicked.connect(lambda: (self.bye(items)))
self.btn.setFocusPolicy(Qt.StrongFocus)
layout.addWidget(self.btn)
self.setLayout(layout)
self.setWindowTitle(' Test ')
def on_combobox_func(self):
self.items['current_index'] = self.combobox.currentIndex()
def bye(self, items):
self.close()
self.app.exit(1)
可以通过bk._w.plugin_dir
获取插件所在的绝对目录,并通过setWindowIcon
设定插件的图标。
后台处理
def Test1(bk):
# process html/xhtml files
for (file_id, _) in bk.text_iter():
file_href = bk.id_to_href(file_id)
file_basename = bk.href_to_basename(file_href)
file_mime = bk.id_to_mime(file_id)
html_original = bk.readfile(file_id)
'''
your code for processing
'''
bk.writefile(file_id, html_original_conv)
print('Changed:', file_basename, file_mime)
return 0
def Test2(bk):
# process ncx file
NCX_id = bk.gettocid()
if not NCX_id:
print('ncx file is not exists!')
return -1
NCX_mime = bk.id_to_mime(NCX_id)
NCX_href = bk.id_to_href(NCX_id)
NCX_original = bk.readfile(NCX_id)
'''
your code for processing
'''
bk.writefile(NCX_id, NCX_original)
print('Changed:', NCX_href, NCX_mime)
return 0
def Test3(bk):
# process opf file
OPF_basename = 'content.opf'
OPF_mime = 'application/oebps-package+xml'
metadata = bk.getmetadataxml()
'''
your code for processing
'''
bk.setmetadataxml(metadata)
print('Changed:', OPF_basename, OPF_mime)
return 0
def yourProcessFunction(bk, items):
c_index = items['some_argv']
print(""Selected:"", c_index)
if c_index == 1:
return Test1(bk)
elif c_index == 2:
return Test2(bk)
else:
return Test3(bk)
以上演示的是通过GUI选择一个选项并运行相应函数。
对于bk
对象常用的函数,附在最后。
总代码
#!/usr/bin/env python3
#-*- coding: utf-8 -*-
# By: SpaceSkyNet
from lxml import etree
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import (QDialog, QPushButton, QComboBox,
QLabel, QApplication, QVBoxLayout)
from PyQt5.QtCore import Qt
import sys, os
class youtQtGUI(QDialog):
def __init__(self,
app=None,
parent=None,
bk=None,
items=None):
super(youtQtGUI, self).__init__(parent)
self.app = app
self.items = items
self.setWindowIcon(QIcon(os.path.join(bk._w.plugin_dir, '[Plugin Name]', 'plugin.png')))
layout = QVBoxLayout()
choice_info = '选择对象:'
layout.addWidget(QLabel(choice_info))
self.choice_list = ['Test1', 'Test2', 'Test3',]
self.combobox = QComboBox(self)
self.combobox.addItems(self.choice_list)
self.combobox.setCurrentIndex(items['some_argv'])
layout.addWidget(self.combobox)
self.combobox.currentIndexChanged.connect(lambda: self.on_combobox_func())
self.btn = QPushButton('确定', self)
self.btn.clicked.connect(lambda: (self.bye(items)))
self.btn.setFocusPolicy(Qt.StrongFocus)
layout.addWidget(self.btn)
self.setLayout(layout)
self.setWindowTitle(' Test ')
def on_combobox_func(self):
self.items['current_index'] = self.combobox.currentIndex()
def bye(self, items):
self.close()
self.app.exit(1)
def Test1(bk):
# process html/xhtml files
for (file_id, _) in bk.text_iter():
file_href = bk.id_to_href(file_id)
file_basename = bk.href_to_basename(file_href)
file_mime = bk.id_to_mime(file_id)
html_original = bk.readfile(file_id)
'''
your code for processing
'''
bk.writefile(file_id, html_original_conv)
print('Changed:', file_basename, file_mime)
return 0
def Test2(bk):
# process ncx file
NCX_id = bk.gettocid()
if not NCX_id:
print('ncx file is not exists!')
return -1
NCX_mime = bk.id_to_mime(NCX_id)
NCX_href = bk.id_to_href(NCX_id)
NCX_original = bk.readfile(NCX_id)
'''
your code for processing
'''
bk.writefile(NCX_id, NCX_original)
print('Changed:', NCX_href, NCX_mime)
return 0
def Test3(bk):
# process opf file
OPF_basename = 'content.opf'
OPF_mime = 'application/oebps-package+xml'
metadata = bk.getmetadataxml()
'''
your code for processing
'''
bk.setmetadataxml(metadata)
print('Changed:', OPF_basename, OPF_mime)
return 0
def yourProcessFunction(bk, items):
c_index = items['some_argv']
print(""Selected:"", c_index)
if c_index == 1:
return Test1(bk)
elif c_index == 2:
return Test2(bk)
else:
return Test3(bk)
def yourFunction(bk):
item = {""some_argv"": 0}
app = QApplication(sys.argv)
QtGUI = youtQtGUI(app=app, items=items, bk=bk)
QtGUI.show()
rtnCode = app.exec_()
if rtnCode != 1:
print('User abort by closing Setting dialog')
return -1
return yourProcessFunction(bk, item)
def run(bk):
return yourFunction(bk)
bk对象常用函数
bk.readfile(manifest_id)
通过文件的 manifest_id 读取文件,id 可通过 href 等获取,或者使用迭代器获取。
返回一个包含文件原始内容string对象
bk.writefile(manifest_id, data)
通过文件的 manifest_id 写入文件。
可传入文件原始内容的string或bytes对象,string对象必须以 utf-8 编码。
bk.text_iter()
返回一个 python 迭代器对象,迭代所有 xhtml/html 文件,每个元素是一个元组 (manifest_id, OPF_href)。
类似的还有bk.css_iter()
、bk.image_iter()
、bk.font_iter()
、bk.manifest_iter()
,具体信息可查看官方插件开发文档。
bk.id_to_href(id)
通过 manifest_id 获取文件 href。
类似的还有bk.href_to_id(OPF_href)
、bk.id_to_mime(manifest_id)
、bk.basename_to_id(basename)
、bk.href_to_basename(href)
,具体信息可查看官方插件开发文档。
bk.gettocid()
获取目录文件toc.ncx
的 manifest_id。
bk.getmetadataxml()
以string对象返回 OPF 文件中 metadata 的部分。
bk.setmetadataxml(new_metadata)
修改 OPF 文件中 metadata 的部分。
OPF 文件被修改会被打上标记,Sigil 会自动修改,不需要去writefile。
其他
更多详见官方插件开发文档.
如果下载过慢或者不能下载,可在gitee
上寻找 Sigil 同名仓库并寻找Sigil_Plugin_Framework_rev12.epub
文件。
后记
这次发现其实官方文档也是不太完整,通过阅读Sigil的插件启动器Python源代码加上他人的插件源代码,我才完成了 Sigil 插件的编写。
所以,有时候不妨看看源代码,或许比官方文档更有帮助。
我也写了一些 Sigil 的插件,放在了github
上,可以用来对照下。