Sigil 插件编写入门


本文最后更新于:2020年7月24日 下午

最近想要阅读几本繁体中文版的epub书籍,于是想一键翻译成简体中文方便阅读,本以为Sigil会有相关功能,却发现似乎连相关的插件都没有。

然而Sigil的插件是基于Python的,这就很容易寻找到相关的开源库——OpenCC,说来萌发编写的想法也不见为奇了。

但网上对于 Sigil 插件编写的相关资料很少,基本只有官方的一本不完全的插件开发的epub电子书和github上的一些开源的插件代码。

简单写写此文,希望对想要入门 Sigil 插件编写的朋友提供一些帮助。

插件文件夹结构

[Plugin Name]
├── plugin.py
└── plugin.xml

你的插件目录下至少需要有plugin.pyplugin.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了。

如果要程序与用户交互,最好使用图形界面,所以至少掌握到PyQtTkinter等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上,可以用来对照下。

spaceskynet/Sigil-Plugins


文章作者: SpaceSkyNet
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 SpaceSkyNet !
  目录
评论