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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM