diff --git a/src/calibre/ebooks/oeb/polish/replace.py b/src/calibre/ebooks/oeb/polish/replace.py index 8b747686fe..aafa08ba6f 100644 --- a/src/calibre/ebooks/oeb/polish/replace.py +++ b/src/calibre/ebooks/oeb/polish/replace.py @@ -7,9 +7,10 @@ __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import codecs +import codecs, shutil from urlparse import urlparse +from calibre import sanitize_file_name_unicode from calibre.ebooks.chardet import strip_encoding_declarations class LinkReplacer(object): @@ -122,3 +123,25 @@ def rename_files(container, file_map): link_map[current_name] = new_name replace_links(container, link_map, replace_in_opf=True) +def replace_file(container, name, path, basename, force_mt=None): + dirname, base = name.rpartition('/')[0::2] + nname = sanitize_file_name_unicode(basename) + if dirname: + nname = dirname + '/' + nname + with open(path, 'rb') as src: + if name != nname: + count = 0 + b, e = nname.rpartition('.')[0::2] + while container.exists(nname): + count += 1 + nname = b + ('_%d.%s' % (count, e)) + rename_files(container, {name:nname}) + mt = force_mt or container.guess_type(nname) + for itemid, q in container.manifest_id_map.iteritems(): + if q == nname: + for item in container.opf_xpath('//opf:manifest/opf:item[@href and @id="%s"]' % itemid): + item.set('media-type', mt) + container.dirty(container.opf_name) + with container.open(nname, 'wb') as dest: + shutil.copyfileobj(src, dest) + diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index 1b8bf46427..f1061f2cbe 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -21,7 +21,7 @@ from calibre.ebooks.oeb.polish.main import SUPPORTED, tweak_polish from calibre.ebooks.oeb.polish.container import get_container as _gc, clone_container, guess_type from calibre.ebooks.oeb.polish.cover import mark_as_cover, mark_as_titlepage from calibre.ebooks.oeb.polish.pretty import fix_all_html, pretty_all -from calibre.ebooks.oeb.polish.replace import rename_files +from calibre.ebooks.oeb.polish.replace import rename_files, replace_file from calibre.ebooks.oeb.polish.split import split, merge, AbortError from calibre.gui2 import error_dialog, choose_files, question_dialog, info_dialog, choose_save_file from calibre.gui2.dialogs.confirm_delete import confirm @@ -75,6 +75,7 @@ class Boss(QObject): fl.merge_requested.connect(self.merge_requested) fl.mark_requested.connect(self.mark_requested) fl.export_requested.connect(self.export_requested) + fl.replace_requested.connect(self.replace_requested) self.gui.central.current_editor_changed.connect(self.apply_current_editor_state) self.gui.central.close_requested.connect(self.editor_close_requested) self.gui.central.search_panel.search_triggered.connect(self.search) @@ -682,6 +683,13 @@ class Boss(QObject): with current_container().open(name, 'rb') as src, open(path, 'wb') as dest: shutil.copyfileobj(src, dest) + @in_thread_job + def replace_requested(self, name, path, basename, force_mt): + self.commit_all_editors_to_container() + self.add_savepoint(_('Replace %s') % name) + replace_file(current_container(), name, path, basename, force_mt) + self.apply_container_update_to_gui() + def sync_editor_to_preview(self, name, lnum): editor = self.edit_file(name, 'html') self.ignore_preview_to_editor_sync = True diff --git a/src/calibre/gui2/tweak_book/file_list.py b/src/calibre/gui2/tweak_book/file_list.py index fc49530c26..0455ac083e 100644 --- a/src/calibre/gui2/tweak_book/file_list.py +++ b/src/calibre/gui2/tweak_book/file_list.py @@ -86,6 +86,7 @@ class FileList(QTreeWidget): merge_requested = pyqtSignal(object, object, object) mark_requested = pyqtSignal(object, object) export_requested = pyqtSignal(object, object) + replace_requested = pyqtSignal(object, object, object, object) def __init__(self, parent=None): QTreeWidget.__init__(self, parent) @@ -304,6 +305,7 @@ class FileList(QTreeWidget): m = QMenu(self) sel = self.selectedItems() num = len(sel) + container = current_container() if num > 0: m.addAction(QIcon(I('trash.png')), _('&Delete selected files'), self.request_delete) m.addSeparator() @@ -312,13 +314,16 @@ class FileList(QTreeWidget): cn = unicode(ci.data(0, NAME_ROLE).toString()) mt = unicode(ci.data(0, MIME_ROLE).toString()) cat = unicode(ci.data(0, CATEGORY_ROLE).toString()) - m.addAction(QIcon(I('modified.png')), _('&Rename %s') % (elided_text(cn)), self.edit_current_item) + n = elided_text(cn.rpartition('/')[-1]) + m.addAction(QIcon(I('modified.png')), _('&Rename %s') % n, self.edit_current_item) if is_raster_image(mt): - m.addAction(QIcon(I('default_cover.png')), _('Mark %s as cover image') % elided_text(cn), partial(self.mark_as_cover, cn)) + m.addAction(QIcon(I('default_cover.png')), _('Mark %s as cover image') % n, partial(self.mark_as_cover, cn)) elif current_container().SUPPORTS_TITLEPAGES and mt in OEB_DOCS and cat == 'text': - m.addAction(QIcon(I('default_cover.png')), _('Mark %s as title/cover page') % elided_text(cn), partial(self.mark_as_titlepage, cn)) + m.addAction(QIcon(I('default_cover.png')), _('Mark %s as cover page') % n, partial(self.mark_as_titlepage, cn)) m.addSeparator() - m.addAction(QIcon(I('save.png')), _('Export %s') % elided_text(cn), partial(self.export, cn)) + m.addAction(QIcon(I('save.png')), _('Export %s') % n, partial(self.export, cn)) + if cn not in container.names_that_must_not_be_changed and cn not in container.names_that_must_not_be_removed and mt not in OEB_FONTS: + m.addAction(_('Replace %s with file...') % n, partial(self.replace, cn)) m.addSeparator() selected_map = defaultdict(list) @@ -453,6 +458,32 @@ class FileList(QTreeWidget): if path: self.export_requested.emit(name, path) + def replace(self, name): + c = current_container() + mt = c.mime_map[name] + oext = name.rpartition('.')[-1].lower() + filters = [oext] + fname = _('Files') + if mt in OEB_DOCS: + fname = _('HTML Files') + filters = 'html htm xhtm xhtml shtml'.split() + elif is_raster_image(mt): + fname = _('Images') + filters = 'jpeg jpg gif png'.split() + path = choose_files(self, 'tweak_book_import_file', _('Choose file'), filters=[(fname, filters)], select_only_single_file=True) + if not path: + return + path = path[0] + ext = path.rpartition('.')[-1].lower() + force_mt = None + if mt in OEB_DOCS: + force_mt = c.guess_type('a.html') + nname = os.path.basename(path) + nname, ext = nname.rpartition('.')[0::2] + nname = nname + '.' + ext.lower() + self.replace_requested.emit(name, path, nname, force_mt) + + class NewFileDialog(QDialog): # {{{ def __init__(self, initial_choice='html', parent=None): @@ -578,6 +609,7 @@ class FileListWidget(QWidget): merge_requested = pyqtSignal(object, object, object) mark_requested = pyqtSignal(object, object) export_requested = pyqtSignal(object, object) + replace_requested = pyqtSignal(object, object, object, object) def __init__(self, parent=None): QWidget.__init__(self, parent) @@ -585,7 +617,9 @@ class FileListWidget(QWidget): self.file_list = FileList(self) self.layout().addWidget(self.file_list) self.layout().setContentsMargins(0, 0, 0, 0) - for x in ('delete_requested', 'reorder_spine', 'rename_requested', 'edit_file', 'merge_requested', 'mark_requested', 'export_requested'): + for x in ('delete_requested', 'reorder_spine', 'rename_requested', + 'edit_file', 'merge_requested', 'mark_requested', + 'export_requested', 'replace_requested'): getattr(self.file_list, x).connect(getattr(self, x)) for x in ('delete_done', 'select_name'): setattr(self, x, getattr(self.file_list, x))