mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-07 18:24:30 -04:00
Edit book: Allow creation of new, empty books via File->Create new book
This commit is contained in:
parent
b4abb37e74
commit
c8a2ac65e0
@ -1492,7 +1492,7 @@ def metadata_to_opf(mi, as_string=True, default_lang=None):
|
|||||||
|
|
||||||
root = etree.fromstring(textwrap.dedent(
|
root = etree.fromstring(textwrap.dedent(
|
||||||
'''
|
'''
|
||||||
<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="uuid_id">
|
<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="uuid_id" version="2.0">
|
||||||
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
|
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
|
||||||
<dc:identifier opf:scheme="%(a)s" id="%(a)s_id">%(id)s</dc:identifier>
|
<dc:identifier opf:scheme="%(a)s" id="%(a)s_id">%(id)s</dc:identifier>
|
||||||
<dc:identifier opf:scheme="uuid" id="uuid_id">%(uuid)s</dc:identifier>
|
<dc:identifier opf:scheme="uuid" id="uuid_id">%(uuid)s</dc:identifier>
|
||||||
|
102
src/calibre/ebooks/oeb/polish/create.py
Normal file
102
src/calibre/ebooks/oeb/polish/create.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
|
import sys, os
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
from calibre import prepare_string_for_xml, CurrentDir
|
||||||
|
from calibre.ptempfile import TemporaryDirectory
|
||||||
|
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
||||||
|
from calibre.ebooks.oeb.polish.container import OPF_NAMESPACES, guess_type, opf_to_azw3
|
||||||
|
from calibre.ebooks.oeb.polish.pretty import pretty_xml_tree
|
||||||
|
from calibre.ebooks.oeb.polish.toc import TOC, create_ncx
|
||||||
|
from calibre.utils.localization import lang_as_iso639_1
|
||||||
|
from calibre.utils.logging import DevNull
|
||||||
|
from calibre.utils.zipfile import ZipFile, ZIP_STORED
|
||||||
|
|
||||||
|
def create_toc(mi, opf, html_name, lang):
|
||||||
|
uuid = ''
|
||||||
|
for u in opf.xpath('//*[@id="uuid_id"]'):
|
||||||
|
uuid = u.text
|
||||||
|
toc = TOC()
|
||||||
|
toc.add(_('Start'), html_name)
|
||||||
|
return create_ncx(toc, lambda x:x, mi.title, lang, uuid)
|
||||||
|
|
||||||
|
def create_book(mi, path, fmt='epub', opf_name='metadata.opf', html_name='start.xhtml', toc_name='toc.ncx'):
|
||||||
|
''' Create an empty book in the specified format at the specified location. '''
|
||||||
|
path = os.path.abspath(path)
|
||||||
|
lang = 'und'
|
||||||
|
opf = metadata_to_opf(mi, as_string=False)
|
||||||
|
for l in opf.xpath('//*[local-name()="language"]'):
|
||||||
|
if l.text:
|
||||||
|
lang = l.text
|
||||||
|
break
|
||||||
|
lang = lang_as_iso639_1(lang) or lang
|
||||||
|
|
||||||
|
opfns = OPF_NAMESPACES['opf']
|
||||||
|
m = opf.makeelement('{%s}manifest' % opfns)
|
||||||
|
opf.insert(1, m)
|
||||||
|
i = m.makeelement('{%s}item' % opfns, href=html_name, id='start')
|
||||||
|
i.set('media-type', guess_type('a.xhtml'))
|
||||||
|
m.append(i)
|
||||||
|
i = m.makeelement('{%s}item' % opfns, href=toc_name, id='ncx')
|
||||||
|
i.set('media-type', guess_type(toc_name))
|
||||||
|
m.append(i)
|
||||||
|
s = opf.makeelement('{%s}spine' % opfns, toc="ncx")
|
||||||
|
opf.insert(2, s)
|
||||||
|
i = s.makeelement('{%s}itemref' % opfns, idref='start')
|
||||||
|
s.append(i)
|
||||||
|
CONTAINER = '''\
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
||||||
|
<rootfiles>
|
||||||
|
<rootfile full-path="{0}" media-type="application/oebps-package+xml"/>
|
||||||
|
</rootfiles>
|
||||||
|
</container>
|
||||||
|
'''.format(prepare_string_for_xml(opf_name, True)).encode('utf-8')
|
||||||
|
HTML = '''\
|
||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<html lang="{1}" xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>{0}</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>{0}</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
'''.format(prepare_string_for_xml(mi.title), lang).encode('utf-8')
|
||||||
|
ncx = etree.tostring(create_toc(mi, opf, html_name, lang), encoding='utf-8', xml_declaration=True, pretty_print=True)
|
||||||
|
pretty_xml_tree(opf)
|
||||||
|
opf = etree.tostring(opf, encoding='utf-8', xml_declaration=True, pretty_print=True)
|
||||||
|
if fmt == 'azw3':
|
||||||
|
with TemporaryDirectory('create-azw3') as tdir, CurrentDir(tdir):
|
||||||
|
for name, data in ((opf_name, opf), (html_name, HTML), (toc_name, ncx)):
|
||||||
|
with open(name, 'wb') as f:
|
||||||
|
f.write(data)
|
||||||
|
opf_to_azw3(opf_name, path, DevNull())
|
||||||
|
else:
|
||||||
|
with ZipFile(path, 'w', compression=ZIP_STORED) as zf:
|
||||||
|
zf.writestr('mimetype', b'application/epub+zip', compression=ZIP_STORED)
|
||||||
|
zf.writestr('META-INF/', b'', 0755)
|
||||||
|
zf.writestr('META-INF/container.xml', CONTAINER)
|
||||||
|
zf.writestr(opf_name, opf)
|
||||||
|
zf.writestr(html_name, HTML)
|
||||||
|
zf.writestr(toc_name, ncx)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from calibre.ebooks.metadata.book.base import Metadata
|
||||||
|
mi = Metadata('Test book', authors=('Kovid Goyal',))
|
||||||
|
path = sys.argv[-1]
|
||||||
|
ext = path.rpartition('.')[-1].lower()
|
||||||
|
if ext not in ('epub', 'azw3'):
|
||||||
|
print ('Unsupported format:', ext)
|
||||||
|
raise SystemExit(1)
|
||||||
|
create_book(mi, path, fmt=ext)
|
@ -32,7 +32,7 @@ from calibre.gui2.tweak_book.save import SaveManager, save_container
|
|||||||
from calibre.gui2.tweak_book.preview import parse_worker, font_cache
|
from calibre.gui2.tweak_book.preview import parse_worker, font_cache
|
||||||
from calibre.gui2.tweak_book.toc import TOCEditor
|
from calibre.gui2.tweak_book.toc import TOCEditor
|
||||||
from calibre.gui2.tweak_book.editor import editor_from_syntax, syntax_from_mime
|
from calibre.gui2.tweak_book.editor import editor_from_syntax, syntax_from_mime
|
||||||
from calibre.gui2.tweak_book.editor.insert_resource import get_resource_data
|
from calibre.gui2.tweak_book.editor.insert_resource import get_resource_data, NewBook
|
||||||
from calibre.gui2.tweak_book.preferences import Preferences
|
from calibre.gui2.tweak_book.preferences import Preferences
|
||||||
|
|
||||||
def get_container(*args, **kwargs):
|
def get_container(*args, **kwargs):
|
||||||
@ -135,7 +135,7 @@ class Boss(QObject):
|
|||||||
self.container_count += 1
|
self.container_count += 1
|
||||||
return tempfile.mkdtemp(prefix='%s%05d-' % (prefix, self.container_count), dir=self.tdir)
|
return tempfile.mkdtemp(prefix='%s%05d-' % (prefix, self.container_count), dir=self.tdir)
|
||||||
|
|
||||||
def open_book(self, path=None, edit_file=None, clear_notify_data=True):
|
def _check_before_open(self):
|
||||||
if self.gui.action_save.isEnabled():
|
if self.gui.action_save.isEnabled():
|
||||||
if not question_dialog(self.gui, _('Unsaved changes'), _(
|
if not question_dialog(self.gui, _('Unsaved changes'), _(
|
||||||
'The current book has unsaved changes. If you open a new book, they will be lost'
|
'The current book has unsaved changes. If you open a new book, they will be lost'
|
||||||
@ -145,7 +145,24 @@ class Boss(QObject):
|
|||||||
return info_dialog(self.gui, _('Cannot open'),
|
return info_dialog(self.gui, _('Cannot open'),
|
||||||
_('The current book is being saved, you cannot open a new book until'
|
_('The current book is being saved, you cannot open a new book until'
|
||||||
' the saving is completed'), show=True)
|
' the saving is completed'), show=True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def new_book(self):
|
||||||
|
if not self._check_before_open():
|
||||||
|
return
|
||||||
|
d = NewBook(self.gui)
|
||||||
|
if d.exec_() == d.Accepted:
|
||||||
|
fmt = d.fmt
|
||||||
|
path = choose_save_file(self.gui, 'edit-book-new-book', _('Choose file location'),
|
||||||
|
filters=[(fmt.upper(), (fmt,))], all_files=False)
|
||||||
|
if path is not None:
|
||||||
|
from calibre.ebooks.oeb.polish.create import create_book
|
||||||
|
create_book(d.mi, path, fmt=fmt)
|
||||||
|
self.open_book(path=path)
|
||||||
|
|
||||||
|
def open_book(self, path=None, edit_file=None, clear_notify_data=True):
|
||||||
|
if not self._check_before_open():
|
||||||
|
return
|
||||||
if not hasattr(path, 'rpartition'):
|
if not hasattr(path, 'rpartition'):
|
||||||
path = choose_files(self.gui, 'open-book-for-tweaking', _('Choose book'),
|
path = choose_files(self.gui, 'open-book-for-tweaking', _('Choose book'),
|
||||||
[(_('Books'), [x.lower() for x in SUPPORTED])], all_files=False, select_only_single_file=True)
|
[(_('Books'), [x.lower() for x in SUPPORTED])], all_files=False, select_only_single_file=True)
|
||||||
|
@ -7,6 +7,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
import sys, os
|
import sys, os
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import (
|
from PyQt4.Qt import (
|
||||||
QDialog, QGridLayout, QDialogButtonBox, QSize, QListView, QStyledItemDelegate,
|
QDialog, QGridLayout, QDialogButtonBox, QSize, QListView, QStyledItemDelegate,
|
||||||
@ -16,9 +17,13 @@ from PyQt4.Qt import (
|
|||||||
|
|
||||||
from calibre import fit_image
|
from calibre import fit_image
|
||||||
from calibre.constants import plugins
|
from calibre.constants import plugins
|
||||||
|
from calibre.ebooks.metadata import string_to_authors
|
||||||
|
from calibre.ebooks.metadata.book.base import Metadata
|
||||||
from calibre.gui2 import NONE, choose_files, error_dialog
|
from calibre.gui2 import NONE, choose_files, error_dialog
|
||||||
|
from calibre.gui2.languages import LanguagesEdit
|
||||||
from calibre.gui2.tweak_book import current_container, tprefs
|
from calibre.gui2.tweak_book import current_container, tprefs
|
||||||
from calibre.gui2.tweak_book.file_list import name_is_ok
|
from calibre.gui2.tweak_book.file_list import name_is_ok
|
||||||
|
from calibre.utils.localization import get_lang, canonicalize_lang
|
||||||
from calibre.utils.icu import sort_key
|
from calibre.utils.icu import sort_key
|
||||||
|
|
||||||
class Dialog(QDialog):
|
class Dialog(QDialog):
|
||||||
@ -54,7 +59,9 @@ class Dialog(QDialog):
|
|||||||
tprefs.set(self.name + '-splitter-state', bytearray(self.splitter.saveState()))
|
tprefs.set(self.name + '-splitter-state', bytearray(self.splitter.saveState()))
|
||||||
QDialog.reject(self)
|
QDialog.reject(self)
|
||||||
|
|
||||||
class ChooseName(Dialog):
|
class ChooseName(Dialog): # {{{
|
||||||
|
|
||||||
|
''' Chooses the filename for a newly imported file, with error checking '''
|
||||||
|
|
||||||
def __init__(self, candidate, parent=None):
|
def __init__(self, candidate, parent=None):
|
||||||
self.candidate = candidate
|
self.candidate = candidate
|
||||||
@ -91,7 +98,9 @@ class ChooseName(Dialog):
|
|||||||
name, ext = n.rpartition('.')[0::2]
|
name, ext = n.rpartition('.')[0::2]
|
||||||
self.filename = name + '.' + ext.lower()
|
self.filename = name + '.' + ext.lower()
|
||||||
super(ChooseName, self).accept()
|
super(ChooseName, self).accept()
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Images {{{
|
||||||
class ImageDelegate(QStyledItemDelegate):
|
class ImageDelegate(QStyledItemDelegate):
|
||||||
|
|
||||||
MARGIN = 4
|
MARGIN = 4
|
||||||
@ -291,6 +300,7 @@ class InsertImage(Dialog):
|
|||||||
def filter_changed(self, *args):
|
def filter_changed(self, *args):
|
||||||
f = unicode(self.filter.text())
|
f = unicode(self.filter.text())
|
||||||
self.fm.setFilterFixedString(f)
|
self.fm.setFilterFixedString(f)
|
||||||
|
# }}}
|
||||||
|
|
||||||
def get_resource_data(rtype, parent):
|
def get_resource_data(rtype, parent):
|
||||||
if rtype == 'image':
|
if rtype == 'image':
|
||||||
@ -298,6 +308,54 @@ def get_resource_data(rtype, parent):
|
|||||||
if d.exec_() == d.Accepted:
|
if d.exec_() == d.Accepted:
|
||||||
return d.chosen_image, d.chosen_image_is_external
|
return d.chosen_image, d.chosen_image_is_external
|
||||||
|
|
||||||
|
class NewBook(Dialog): # {{{
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
self.fmt = 'epub'
|
||||||
|
Dialog.__init__(self, _('Create new book'), 'create-new-book', parent=parent)
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
self.l = l = QFormLayout(self)
|
||||||
|
self.setLayout(l)
|
||||||
|
|
||||||
|
self.title = t = QLineEdit(self)
|
||||||
|
l.addRow(_('&Title:'), t)
|
||||||
|
t.setFocus(Qt.OtherFocusReason)
|
||||||
|
|
||||||
|
self.authors = a = QLineEdit(self)
|
||||||
|
l.addRow(_('&Authors:'), a)
|
||||||
|
a.setText(tprefs.get('previous_new_book_authors', ''))
|
||||||
|
|
||||||
|
self.languages = la = LanguagesEdit(self)
|
||||||
|
l.addRow(_('&Language:'), la)
|
||||||
|
la.lang_codes = (tprefs.get('previous_new_book_lang', canonicalize_lang(get_lang())),)
|
||||||
|
|
||||||
|
bb = self.bb
|
||||||
|
l.addRow(bb)
|
||||||
|
bb.clear()
|
||||||
|
bb.addButton(bb.Cancel)
|
||||||
|
b = bb.addButton('&EPUB', bb.AcceptRole)
|
||||||
|
b.clicked.connect(partial(self.set_fmt, 'epub'))
|
||||||
|
b = bb.addButton('&AZW3', bb.AcceptRole)
|
||||||
|
b.clicked.connect(partial(self.set_fmt, 'azw3'))
|
||||||
|
|
||||||
|
def set_fmt(self, fmt):
|
||||||
|
self.fmt = fmt
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
tprefs.set('previous_new_book_authors', unicode(self.authors.text()))
|
||||||
|
tprefs.set('previous_new_book_lang', (self.languages.lang_codes or [get_lang()])[0])
|
||||||
|
super(NewBook, self).accept()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mi(self):
|
||||||
|
mi = Metadata(unicode(self.title.text()).strip() or _('Unknown'))
|
||||||
|
mi.authors = string_to_authors(unicode(self.authors.text()).strip()) or [_('Unknown')]
|
||||||
|
mi.languages = self.languages.lang_codes or [get_lang()]
|
||||||
|
return mi
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app = QApplication([]) # noqa
|
app = QApplication([]) # noqa
|
||||||
from calibre.gui2.tweak_book import set_current_container
|
from calibre.gui2.tweak_book import set_current_container
|
||||||
|
@ -277,6 +277,7 @@ class Main(MainWindow):
|
|||||||
self.action_save_copy = reg('save.png', _('Save a ©'), self.boss.save_copy, 'save-copy', 'Ctrl+Alt+S', _('Save a copy of the book'))
|
self.action_save_copy = reg('save.png', _('Save a ©'), self.boss.save_copy, 'save-copy', 'Ctrl+Alt+S', _('Save a copy of the book'))
|
||||||
self.action_quit = reg('quit.png', _('&Quit'), self.boss.quit, 'quit', 'Ctrl+Q', _('Quit'))
|
self.action_quit = reg('quit.png', _('&Quit'), self.boss.quit, 'quit', 'Ctrl+Q', _('Quit'))
|
||||||
self.action_preferences = reg('config.png', _('&Preferences'), self.boss.preferences, 'preferences', 'Ctrl+P', _('Preferences'))
|
self.action_preferences = reg('config.png', _('&Preferences'), self.boss.preferences, 'preferences', 'Ctrl+P', _('Preferences'))
|
||||||
|
self.action_new_book = reg('book.png', _('Create &new, empty book'), self.boss.new_book, 'new-book', (), _('Create a new, empty book'))
|
||||||
|
|
||||||
# Editor actions
|
# Editor actions
|
||||||
group = _('Editor actions')
|
group = _('Editor actions')
|
||||||
@ -391,6 +392,7 @@ class Main(MainWindow):
|
|||||||
f = b.addMenu(_('&File'))
|
f = b.addMenu(_('&File'))
|
||||||
f.addAction(self.action_new_file)
|
f.addAction(self.action_new_file)
|
||||||
f.addAction(self.action_open_book)
|
f.addAction(self.action_open_book)
|
||||||
|
f.addAction(self.action_new_book)
|
||||||
self.recent_books_menu = f.addMenu(_('&Recently opened books'))
|
self.recent_books_menu = f.addMenu(_('&Recently opened books'))
|
||||||
self.update_recent_books()
|
self.update_recent_books()
|
||||||
f.addSeparator()
|
f.addSeparator()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user