• Sigil 插件编写入门


    欢迎移步我的网站

    最近想要阅读几本繁体中文版的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

  • 相关阅读:
    [luogu3334]抛硬币
    [luogu3706]硬币游戏
    [luogu4548]歌唱王国
    [hdu4652]Dice
    [atAGC013F]Two Faced Cards
    [atAGC045F]Division into Multiples
    [atAGC045E]Fragile Balls
    [atAGC045D]Lamps and Buttons
    [luogu5574]任务分配问题
    [luogu4331]数字序列
  • 原文地址:https://www.cnblogs.com/spaceskynet/p/13374719.html
Copyright © 2020-2023  润新知