最近想要閱讀幾本繁體中文版的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
上,可以用來對照下。