diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py index dd4e7e41b1..1b1ffeee3e 100644 --- a/src/calibre/ebooks/metadata/opf2.py +++ b/src/calibre/ebooks/metadata/opf2.py @@ -1492,7 +1492,7 @@ def metadata_to_opf(mi, as_string=True, default_lang=None): root = etree.fromstring(textwrap.dedent( ''' - + %(id)s %(uuid)s diff --git a/src/calibre/ebooks/oeb/polish/create.py b/src/calibre/ebooks/oeb/polish/create.py new file mode 100644 index 0000000000..2a678fe629 --- /dev/null +++ b/src/calibre/ebooks/oeb/polish/create.py @@ -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 ' + +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 = '''\ + + + + + + + '''.format(prepare_string_for_xml(opf_name, True)).encode('utf-8') + HTML = '''\ + + + + + {0} + + + +

{0}

+ + + '''.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) diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index da66652e7d..1771e158c8 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -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.toc import TOCEditor 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 def get_container(*args, **kwargs): @@ -135,7 +135,7 @@ class Boss(QObject): self.container_count += 1 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 not question_dialog(self.gui, _('Unsaved changes'), _( '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'), _('The current book is being saved, you cannot open a new book until' ' 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'): 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) diff --git a/src/calibre/gui2/tweak_book/editor/insert_resource.py b/src/calibre/gui2/tweak_book/editor/insert_resource.py index b6bd72ec74..1949c739d1 100644 --- a/src/calibre/gui2/tweak_book/editor/insert_resource.py +++ b/src/calibre/gui2/tweak_book/editor/insert_resource.py @@ -7,6 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' import sys, os +from functools import partial from PyQt4.Qt import ( QDialog, QGridLayout, QDialogButtonBox, QSize, QListView, QStyledItemDelegate, @@ -16,9 +17,13 @@ from PyQt4.Qt import ( from calibre import fit_image 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.languages import LanguagesEdit from calibre.gui2.tweak_book import current_container, tprefs 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 class Dialog(QDialog): @@ -54,7 +59,9 @@ class Dialog(QDialog): tprefs.set(self.name + '-splitter-state', bytearray(self.splitter.saveState())) 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): self.candidate = candidate @@ -91,7 +98,9 @@ class ChooseName(Dialog): name, ext = n.rpartition('.')[0::2] self.filename = name + '.' + ext.lower() super(ChooseName, self).accept() +# }}} +# Images {{{ class ImageDelegate(QStyledItemDelegate): MARGIN = 4 @@ -291,6 +300,7 @@ class InsertImage(Dialog): def filter_changed(self, *args): f = unicode(self.filter.text()) self.fm.setFilterFixedString(f) +# }}} def get_resource_data(rtype, parent): if rtype == 'image': @@ -298,6 +308,54 @@ def get_resource_data(rtype, parent): if d.exec_() == d.Accepted: 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__': app = QApplication([]) # noqa from calibre.gui2.tweak_book import set_current_container diff --git a/src/calibre/gui2/tweak_book/ui.py b/src/calibre/gui2/tweak_book/ui.py index 9c4a9c0d24..833e2734d0 100644 --- a/src/calibre/gui2/tweak_book/ui.py +++ b/src/calibre/gui2/tweak_book/ui.py @@ -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_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_new_book = reg('book.png', _('Create &new, empty book'), self.boss.new_book, 'new-book', (), _('Create a new, empty book')) # Editor actions group = _('Editor actions') @@ -391,6 +392,7 @@ class Main(MainWindow): f = b.addMenu(_('&File')) f.addAction(self.action_new_file) f.addAction(self.action_open_book) + f.addAction(self.action_new_book) self.recent_books_menu = f.addMenu(_('&Recently opened books')) self.update_recent_books() f.addSeparator()