amazonはkindle用書籍を作成するツールを提供している
GUIのKindle Previewer 3とCUIのkindlegen
生成されるmobiファイルには古いkindle端末でも読める旧フォーマット、
今の端末用の新フォーマット、そしてmobiを作成する際に使用した
ソースファイルが格納(注)される
(注)別途記事に書くがkindlegenでソースファイルを格納しない方法がある
このためファイルサイズが大きくなるのだが、自分に不要なデータを
削除してダイエットするのがKindleUnpack(旧名mobiUnpack)
もとは.mobilereadのフォーラムが発祥
https://www.mobileread.com/forums/showthread.php?t=61986
現在はgithubでリポジトリ管理されている
https://github.com/kevinhendricks/KindleUnpack
KindleUnpackにはGUI用とCUI用が用意されているが、windows環境では
コードページの関係でCUI用がちゃんと動かない
このためGUI用に手を加えて引数で処理ファイル名を指定できるようにした
Pythonは勉強を始めたばかりなのでもっと最適化できるのではないか思う
<使い方> ※.pywファイルにはpythonw.exeを関連付け済
KindleUnpack.pyw ARG1 ARG2 ARG3 ARG1 変換元ファイル名(ファイルの存在チェックはなし) ARG2 変換後ファイル出力先(出力先パスの妥当性チェックはなし) ARG3 変換処理後に自動終了指示(存在チェックのみなのでなんでもよい)
KindleUnpack.pyw ※epub2になっていたのを修正 10/21
#!/usr/bin/env python # -*- coding: utf-8 -*- # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab from __future__ import unicode_literals, division, absolute_import, print_function import sys #引数で処理対象ファイルを指定できるように修正 argvs = sys.argv argvc = len(argvs) from lib.compatibility_utils import PY2, text_type, unicode_str from lib.compatibility_utils import unicode_argv, add_cp65001_codec import lib.unipath as unipath from lib.unipath import pathof import os import traceback import codecs add_cp65001_codec() try: from queue import Full from queue import Empty except ImportError: from Queue import Full from Queue import Empty if PY2 and sys.platform.startswith("win"): from libgui.askfolder_ed import AskFolder from multiprocessing import Process, Queue if PY2: import Tkinter as tkinter import Tkconstants as tkinter_constants import tkFileDialog as tkinter_filedialog import ttk as tkinter_ttk else: import tkinter import tkinter.constants as tkinter_constants import tkinter.filedialog as tkinter_filedialog import tkinter.ttk as tkinter_ttk from libgui.scrolltextwidget import ScrolledText import lib.kindleunpack as kindleunpack # 各種設定を.jsonファイルに保存しプログラム開始時に読み込みます # 設定を.jsonファイルに保存しない場合は、falseに設定します # falseに設定しても.jsonファイルの削除は自動で行いません # 手動で削除してください PERSISTENT_PREFS = True from inspect import getfile, currentframe from libgui.prefs import getprefs, saveprefs # おそらく過剰ですがスクリプトがどのプラットフォームで呼び出されても大丈夫なように念のため SCRIPT_NAME = unicode_str(getfile(currentframe())) SCRIPT_DIR = unicode_str(os.path.dirname(unipath.abspath(getfile(currentframe())))) PROGNAME = unicode_str(os.path.splitext(SCRIPT_NAME)[0]) # .jsonファイル名にプラットフォームを含めることで設定が引き継がれます # 異なるOSがネットワーク共有/フラッシュドライブを介して同じスクリプトにアクセスする場合に発生します CONFIGFILE = unicode_str(os.path.join(SCRIPT_DIR, '{0}_{1}.json'.format(PROGNAME, sys.platform[:3]))) # 出力が共有キューに追加されるようにストリームをラップする # utf-8エンコーディングを使用 class QueuedStream: def __init__(self, stream, q): self.stream = stream self.encoding = stream.encoding self.q = q if self.encoding == None: self.encoding = 'utf-8' def write(self, data): if isinstance(data,text_type): data = data.encode('utf-8') elif self.encoding not in ['utf-8','UTF-8','cp65001','CP65001']: udata = data.decode(self.encoding) data = udata.encode('utf-8') self.q.put(data) def __getattr__(self, attr): if attr == 'mode': return 'wb' if attr == 'encoding': return 'utf-8' return getattr(self.stream, attr) class MainDialog(tkinter.Frame): def __init__(self, root): tkinter.Frame.__init__(self, root, border=5) self.root = root self.interval = 50 self.p2 = None self.q = Queue() # 今後の設定の追加/削除を容易に行うために: # フォーマットを守ろう - TK Widget name = prefs dictionary key = ini.get|set name. # 例: mobipath = prefs['mobipath'] = config.get('Defaults', mobipath). self.prefs = getprefs(CONFIGFILE, self.root, PERSISTENT_PREFS) self.status = tkinter.StringVar() tkinter.Label(self, textvariable=self.status, justify='center').grid(row=0, columnspan=3, sticky=tkinter_constants.N) self.status.set('Upack a non-DRM Kindle eBook') sticky = tkinter_constants.E + tkinter_constants.W ALL = tkinter_constants.E+tkinter_constants.W+tkinter_constants.N+tkinter_constants.S # テキストボックスに初期値を設定します。 self.grid_columnconfigure(1, weight=1) # デバッグログを設定します。 self.grid_rowconfigure(10, weight=1) tkinter.Label(self, text='').grid(row=1, sticky=tkinter_constants.E) tkinter.Label(self, text='Unencrypted Kindle eBook input file', wraplength=200).grid(row=2, sticky=tkinter_constants.E) self.mobipath = tkinter.Entry(self, width=50) self.mobipath.grid(row=2, column=1, sticky=sticky) #引数指定がある場合に処理対象ファイルをセット #SJISのままだと文字化けするのでUNICODEに変換 if (argvc > 1): self.mobipath.insert(0,unicode(argvs[1], 'cp932')) else: self.mobipath.insert(0, '') button = tkinter.Button(self, text="Browse...", command=self.get_mobipath) button.grid(row=2, column=2, sticky=sticky) tkinter.Label(self, text='Output Directory', wraplength=200).grid(row=3, sticky=tkinter_constants.E) self.outpath = tkinter.Entry(self, width=50) self.outpath.grid(row=3, column=1, sticky=sticky) #引数指定がある場合に出力フォルダをセット #SJISのままだと文字化けするのでUNICODEに変換 if (argvc > 2): outpath = pathof(os.path.normpath(unicode(argvs[2], 'cp932'))) self.outpath.insert(0, outpath) else: if self.prefs['outpath'] and PERSISTENT_PREFS and unipath.exists(CONFIGFILE): outpath = pathof(os.path.normpath(self.prefs['outpath'])) self.outpath.insert(0, outpath) else: self.outpath.insert(0, '') button = tkinter.Button(self, text="Browse...", command=self.get_outpath) button.grid(row=3, column=2, sticky=sticky) tkinter.Label(self, text='OPTIONAL: APNX file Associated with AZW3', wraplength=200).grid(row=4, sticky=tkinter_constants.E) self.apnxpath = tkinter.Entry(self, width=50) self.apnxpath.grid(row=4, column=1, sticky=sticky) self.apnxpath.insert(0, '') button = tkinter.Button(self, text="Browse...", command=self.get_apnxpath) button.grid(row=4, column=2, sticky=sticky) self.splitvar = tkinter.IntVar() checkbox = tkinter.Checkbutton(self, text="Split Combination Kindlegen eBooks", variable=self.splitvar) #無条件チェックに変更 checkbox.select() # if self.prefs['splitvar'] and PERSISTENT_PREFS: # checkbox.select() checkbox.grid(row=5, column=1, columnspan=2, sticky=tkinter_constants.W) self.rawvar = tkinter.IntVar() checkbox = tkinter.Checkbutton(self, text="Write Raw Data", variable=self.rawvar) if self.prefs['rawvar'] and PERSISTENT_PREFS: checkbox.select() checkbox.grid(row=6, column=1, columnspan=2, sticky=tkinter_constants.W) self.dbgvar = tkinter.IntVar() checkbox = tkinter.Checkbutton(self, text="Dump Mode", variable=self.dbgvar) if self.prefs['dbgvar'] and PERSISTENT_PREFS: checkbox.select() checkbox.grid(row=7, column=1, columnspan=2, sticky=tkinter_constants.W) self.hdvar = tkinter.IntVar() checkbox = tkinter.Checkbutton(self, text="Use HD Images If Present", variable=self.hdvar) #無条件チェックに変更 checkbox.select() # if self.prefs['hdvar'] and PERSISTENT_PREFS: # checkbox.select() checkbox.grid(row=8, column=1, columnspan=2, sticky=tkinter_constants.W) tkinter.Label(self, text='ePub Output Type:').grid(row=9, sticky=tkinter_constants.E) self.epubver_val = tkinter.StringVar() self.epubver = tkinter_ttk.Combobox(self, textvariable=self.epubver_val, state='readonly') self.epubver['values'] = ('ePub 2', 'ePub 3', 'Auto-detect', 'Force ePub 2') #無条件 epub3 self.epubver.current(1) # self.epubver.current(0) # if self.prefs['epubver'] and PERSISTENT_PREFS: # self.epubver.grid(row=9, column=1, columnspan=2, pady=(3,5), sticky=tkinter_constants.W) msg1 = 'Conversion Log \n\n' self.stext = ScrolledText(self, bd=5, relief=tkinter_constants.RIDGE, wrap=tkinter_constants.WORD) self.stext.grid(row=10, column=0, columnspan=3, sticky=ALL) self.stext.insert(tkinter_constants.END,msg1) self.sbotton = tkinter.Button( self, text="Start", width=10, command=self.convertit) self.sbotton.grid(row=11, column=1, sticky=tkinter_constants.S+tkinter_constants.E) #引数が指定されている場合にはStartボタンにフォーカス移動 if (argvc > 1): self.sbotton.focus_set() #Returnを割り当てるとなぜかそのまま処理に入る self.sbotton.bind('<Return>',self.convertit()) self.qbutton = tkinter.Button( self, text="Quit", width=10, command=self.quitting) self.qbutton.grid(row=11, column=2, sticky=tkinter_constants.S+tkinter_constants.W) if self.prefs['windowgeometry'] and PERSISTENT_PREFS: self.root.geometry(self.prefs['windowgeometry']) else: self.root.update_idletasks() w = self.root.winfo_screenwidth() h = self.root.winfo_screenheight() rootsize = (605, 575) x = w//2 - rootsize[0]//2 y = h//2 - rootsize[1]//2 self.root.geometry('%dx%d+%d+%d' % (rootsize + (x, y))) self.root.protocol('WM_DELETE_WINDOW', self.quitting) # このメインプロセスと生成された子プロセスとの間で共有されるキューの読み取り def readQueueUntilEmpty(self): done = False text = '' while not done: try: data = self.q.get_nowait() text += unicode_str(data, 'utf-8') except Empty: done = True pass return text # ブロックせずにサブプロセスパイプから読み取る # ウィジェット "after"を介して間隔ごとに呼び出されます # オプションが使用されているため、次回リセットする必要があります def processQueue(self): poll = self.p2.exitcode if poll != None: text = self.readQueueUntilEmpty() msg = text + '\n\n' + 'eBook successfully unpacked\n' if poll != 0: msg = text + '\n\n' + 'Error: Unpacking Failed\n' self.p2.join() self.showCmdOutput(msg) self.p2 = None self.sbotton.configure(state='normal') #展開後、Quitボタンにフォーカス移動 self.qbutton.focus_set() if (argvc > 3): #Returnを割り当てるとなぜかそのまま処理に入る self.sbotton.bind('<Return>',self.quitting()) return text = self.readQueueUntilEmpty() self.showCmdOutput(text) # make sure we get invoked again by event loop after interval self.stext.after(self.interval,self.processQueue) return # スクロールされたテキストウィジェットのサブプロセスからの出力をポストする def showCmdOutput(self, msg): if msg and msg !='': if sys.platform.startswith('win'): msg = msg.replace('\r\n','\n') self.stext.insert(tkinter_constants.END,msg) self.stext.yview_pickplace(tkinter_constants.END) return def get_mobipath(self): cwd = unipath.getcwd() mobipath = tkinter_filedialog.askopenfilename( parent=None, title='Select Unencrypted Kindle eBook File', initialdir=self.prefs['mobipath'] or cwd, initialfile=None, defaultextension=('.mobi', '.prc', '.azw', '.azw4', '.azw3'), filetypes=[('All Kindle formats', ('.mobi', '.prc', '.azw', '.azw4', '.azw3')), ('Kindle Mobi eBook File', '.mobi'), ('Kindle PRC eBook File', '.prc'), ('Kindle AZW eBook File', '.azw'), ('Kindle AZW4 Print Replica', '.azw4'), ('Kindle Version 8', '.azw3'),('All Files', '.*')]) if mobipath: self.prefs['mobipath'] = pathof(os.path.dirname(mobipath)) mobipath = pathof(os.path.normpath(mobipath)) self.mobipath.delete(0, tkinter_constants.END) self.mobipath.insert(0, mobipath) return def get_apnxpath(self): cwd = unipath.getcwd() apnxpath = tkinter_filedialog.askopenfilename( parent=None, title='Optional APNX file associated with AZW3', initialdir=self.prefs['apnxpath'] or cwd, initialfile=None, defaultextension='.apnx', filetypes=[('Kindle APNX Page Information File', '.apnx'), ('All Files', '.*')]) if apnxpath: self.prefs['apnxpath'] = pathof(os.path.dirname(apnxpath)) apnxpath = pathof(os.path.normpath(apnxpath)) self.apnxpath.delete(0, tkinter_constants.END) self.apnxpath.insert(0, apnxpath) return def get_outpath(self): cwd = unipath.getcwd() if sys.platform.startswith("win") and PY2: # tk_chooseDirectory is horribly broken for unicode paths # on windows - bug has been reported but not fixed for years # workaround by using our own unicode aware version outpath = AskFolder(message="Folder to Store Output into", defaultLocation=self.prefs['outpath'] or unipath.getcwd()) else: outpath = tkinter_filedialog.askdirectory( parent=None, title='Folder to Store Output into', initialdir=self.prefs['outpath'] or cwd, initialfile=None) if outpath: self.prefs['outpath'] = outpath outpath = pathof(os.path.normpath(outpath)) self.outpath.delete(0, tkinter_constants.END) self.outpath.insert(0, outpath) return # 終了処理 def quitting(self): # 実行中のサブプロセスを強制終了する if self.p2 != None: if (self.p2.exitcode == None): self.p2.terminate() if PERSISTENT_PREFS: # .jsonに書き出し if not saveprefs(CONFIGFILE, self.prefs, self): print("Couldn't save INI file.") self.root.destroy() self.quit() # 展開処理(子プロセスで実行) def convertit(self): # 多重起動を防ぐためボタンを無効にする self.sbotton.configure(state='disabled') mobipath = unicode_str(self.mobipath.get()) apnxpath = unicode_str(self.apnxpath.get()) outdir = unicode_str(self.outpath.get()) if not mobipath or not unipath.exists(mobipath): self.status.set('Specified eBook file does not exist') self.sbotton.configure(state='normal') return apnxfile = None if apnxpath != "" and unipath.exists(apnxpath): apnxfile = apnxpath if not outdir: self.status.set('No output directory specified') self.sbotton.configure(state='normal') return q = self.q log = 'Input Path = "'+ mobipath + '"\n' log += 'Output Path = "' + outdir + '"\n' if apnxfile != None: log += 'APNX Path = "' + apnxfile + '"\n' dump = False writeraw = False splitcombos = False use_hd = False if self.dbgvar.get() == 1: dump = True log += 'Debug = True\n' if self.rawvar.get() == 1: writeraw = True log += 'WriteRawML = True\n' if self.splitvar.get() == 1: splitcombos = True log += 'Split Combo KF8 Kindle eBooks = True\n' if self.epubver.current() == 0: epubversion = '2' elif self.epubver.current() == 1: epubversion = '3' elif self.epubver.current() == 2: epubversion = 'A' else: epubversion = 'F' log += 'Epub Output Type Set To: {0}\n'.format(self.epubver_val.get()) if self.hdvar.get(): use_hd = True # stub for processing the Use HD Images setting log += 'Use HD Images If Present = True\n' log += '\n\n' log += 'Please Wait ...\n\n' self.stext.insert(tkinter_constants.END,log) self.p2 = Process(target=unpackEbook, args=(q, mobipath, outdir, apnxfile, epubversion, use_hd, dump, writeraw, splitcombos)) self.p2.start() # Pythonはあなたが他のすべてのGUIが行う独自のイベントループを # 作成することを許可していないようです # 仕方がないのでウィジェット "after"を使用して # non-guiイベントを実行するイベント・ループを強制起動する self.stext.after(self.interval,self.processQueue) return # 子プロセス/マルチプロセッシングスレッドはここから開始します def unpackEbook(q, infile, outdir, apnxfile, epubversion, use_hd, dump, writeraw, splitcombos): sys.stdout = QueuedStream(sys.stdout, q) sys.stderr = QueuedStream(sys.stderr, q) rv = 0 try: kindleunpack.unpackBook(infile, outdir, apnxfile, epubversion, use_hd, dodump=dump, dowriteraw=writeraw, dosplitcombos=splitcombos) except Exception as e: print("Error: %s" % e) print(traceback.format_exc()) rv = 1 sys.exit(rv) def main(argv=unicode_argv()): root = tkinter.Tk() root.title('Kindle eBook Unpack Tool') root.minsize(440, 350) root.resizable(True, True) MainDialog(root).pack(fill=tkinter_constants.BOTH, expand=tkinter_constants.YES) root.mainloop() return 0 if __name__ == "__main__": sys.exit(main())