From 66d897893a73b3c54014fe0025f8dcc7f644d500 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 4 Aug 2013 15:07:03 +0530 Subject: [PATCH] Support adding images into the comments field Allow adding images into the comments field, by clicking on the insert link button in the comments editor in the edit metadata dialog. When generating a metadata jacket during book polishing or conversion, embed any images referenced in the comments. --- src/calibre/ebooks/oeb/polish/jacket.py | 31 +++++++++++-- src/calibre/ebooks/oeb/transforms/jacket.py | 27 +++++++++-- src/calibre/gui2/comments_editor.py | 51 ++++++++++++++++----- 3 files changed, 90 insertions(+), 19 deletions(-) diff --git a/src/calibre/ebooks/oeb/polish/jacket.py b/src/calibre/ebooks/oeb/polish/jacket.py index 8ae65f3f9f..ee795c0011 100644 --- a/src/calibre/ebooks/oeb/polish/jacket.py +++ b/src/calibre/ebooks/oeb/polish/jacket.py @@ -11,14 +11,26 @@ from calibre.customize.ui import output_profiles from calibre.ebooks.conversion.config import load_defaults from calibre.ebooks.oeb.base import XPath, OPF from calibre.ebooks.oeb.polish.cover import find_cover_page -from calibre.ebooks.oeb.transforms.jacket import render_jacket as render +from calibre.ebooks.oeb.transforms.jacket import render_jacket as render, referenced_images -def render_jacket(mi): +def render_jacket(container, jacket): + mi = container.mi ps = load_defaults('page_setup') op = ps.get('output_profile', 'default') opmap = {x.short_name:x for x in output_profiles()} output_profile = opmap.get(op, opmap['default']) - return render(mi, output_profile) + root = render(mi, output_profile) + for img, path in referenced_images(root): + container.log('Embedding referenced image: %s into jacket' % path) + ext = path.rpartition('.')[-1] + jacket_item = container.generate_item('jacket_image.'+ext, id_prefix='jacket_img') + name = container.href_to_name(jacket_item.get('href'), container.opf_name) + with open(path, 'rb') as f: + container.parsed_cache[name] = f.read() + container.commit_item(name) + href = container.name_to_href(name, jacket) + img.set('src', href) + return root def is_legacy_jacket(root): return len(root.xpath( @@ -42,17 +54,25 @@ def find_existing_jacket(container): return name def replace_jacket(container, name): - root = render_jacket(container.mi) + root = render_jacket(container, name) container.parsed_cache[name] = root container.dirty(name) def remove_jacket(container): name = find_existing_jacket(container) if name is not None: + remove_jacket_images(container, name) container.remove_item(name) return True return False +def remove_jacket_images(container, name): + root = container.parsed_cache[name] + for img in root.xpath('//*[local-name() = "img" and @src]'): + iname = container.href_to_name(img.get('src'), name) + if container.has_name(iname): + container.remove_item(iname) + def add_or_replace_jacket(container): name = find_existing_jacket(container) found = True @@ -60,6 +80,9 @@ def add_or_replace_jacket(container): jacket_item = container.generate_item('jacket.xhtml', id_prefix='jacket') name = container.href_to_name(jacket_item.get('href'), container.opf_name) found = False + if found: + remove_jacket_images(container, name) + replace_jacket(container, name) if not found: # Insert new jacket into spine diff --git a/src/calibre/ebooks/oeb/transforms/jacket.py b/src/calibre/ebooks/oeb/transforms/jacket.py index 02abda0927..a9cc10dc3d 100644 --- a/src/calibre/ebooks/oeb/transforms/jacket.py +++ b/src/calibre/ebooks/oeb/transforms/jacket.py @@ -6,12 +6,13 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import sys +import sys, os from xml.sax.saxutils import escape from lxml import etree from calibre import guess_type, strftime +from calibre.constants import iswindows from calibre.ebooks.BeautifulSoup import BeautifulSoup from calibre.ebooks.oeb.base import XPath, XHTML_NS, XHTML, xml2text, urldefrag from calibre.library.comments import comments_to_html @@ -84,9 +85,17 @@ class Jacket(object): alt_comments=comments) id, href = self.oeb.manifest.generate('calibre_jacket', 'jacket.xhtml') - item = self.oeb.manifest.add(id, href, guess_type(href)[0], data=root) - self.oeb.spine.insert(0, item, True) - self.oeb.inserted_metadata_jacket = item + jacket = self.oeb.manifest.add(id, href, guess_type(href)[0], data=root) + self.oeb.spine.insert(0, jacket, True) + self.oeb.inserted_metadata_jacket = jacket + for img, path in referenced_images(root): + self.oeb.log('Embedding referenced image %s into jacket' % path) + ext = path.rpartition('.')[-1].lower() + item_id, href = self.oeb.manifest.generate('jacket_image', 'jacket_img.'+ext) + with open(path, 'rb') as f: + item = self.oeb.manifest.add(item_id, href, guess_type(href)[0], data=f.read()) + item.unload_data_from_memory() + img.set('src', jacket.relhref(item.href)) def remove_existing_jacket(self): for x in self.oeb.spine[:4]: @@ -262,3 +271,13 @@ def linearize_jacket(oeb): e.tag = XHTML('span') break +def referenced_images(root): + for img in XPath('//h:img[@src]')(root): + src = img.get('src') + if src.startswith('file://'): + path = src[7:] + if iswindows and path.startswith('/'): + path = path[1:] + if os.path.exists(path): + yield img, path + diff --git a/src/calibre/gui2/comments_editor.py b/src/calibre/gui2/comments_editor.py index ac8ab13d20..921c8676e9 100644 --- a/src/calibre/gui2/comments_editor.py +++ b/src/calibre/gui2/comments_editor.py @@ -12,15 +12,16 @@ import sip from PyQt4.Qt import (QApplication, QFontInfo, QSize, QWidget, QPlainTextEdit, QToolBar, QVBoxLayout, QAction, QIcon, Qt, QTabWidget, QUrl, QFormLayout, - QSyntaxHighlighter, QColor, QChar, QColorDialog, QMenu, QDialog, - QHBoxLayout, QKeySequence, QLineEdit, QDialogButtonBox) + QSyntaxHighlighter, QColor, QChar, QColorDialog, QMenu, QDialog, QLabel, + QHBoxLayout, QKeySequence, QLineEdit, QDialogButtonBox, QPushButton) from PyQt4.QtWebKit import QWebView, QWebPage from calibre.ebooks.chardet import xml_to_unicode from calibre import xml_replace_entities, prepare_string_for_xml -from calibre.gui2 import open_url, error_dialog +from calibre.gui2 import open_url, error_dialog, choose_files from calibre.utils.soupparser import fromstring from calibre.utils.config import tweaks +from calibre.utils.imghdr import what class PageAction(QAction): # {{{ @@ -156,7 +157,7 @@ class EditorWidget(QWebView): # {{{ self.block_style_actions.append(ac) self.action_insert_link = QAction(QIcon(I('insert-link.png')), - _('Insert link'), self) + _('Insert link or image'), self) self.action_insert_link.triggered.connect(self.insert_link) self.pageAction(QWebPage.ToggleBold).changed.connect(self.update_link_action) self.action_insert_link.setEnabled(False) @@ -203,14 +204,18 @@ class EditorWidget(QWebView): # {{{ self.exec_command('hiliteColor', unicode(col.name())) def insert_link(self, *args): - link, name = self.ask_link() + link, name, is_image = self.ask_link() if not link: return - url = self.parse_link(unicode(link)) + url = self.parse_link(link) if url.isValid(): url = unicode(url.toString()) self.setFocus(Qt.OtherFocusReason) - if name: + if is_image: + self.exec_command('insertHTML', + '%s'%(prepare_string_for_xml(url, True), + prepare_string_for_xml(name or '', True))) + elif name: self.exec_command('insertHTML', '%s'%(prepare_string_for_xml(url, True), prepare_string_for_xml(name))) @@ -218,7 +223,7 @@ class EditorWidget(QWebView): # {{{ self.exec_command('createLink', url) else: error_dialog(self, _('Invalid URL'), - _('The url %r is invalid') % unicode(link), show=True) + _('The url %r is invalid') % link, show=True) def ask_link(self): d = QDialog(self) @@ -227,19 +232,43 @@ class EditorWidget(QWebView): # {{{ d.setLayout(l) d.url = QLineEdit(d) d.name = QLineEdit(d) + d.setMinimumWidth(600) d.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) + d.br = b = QPushButton(_('&Browse')) + b.setIcon(QIcon(I('document_open.png'))) + def cf(): + files = choose_files(d, 'select link file', _('Choose file'), select_only_single_file=True) + if files: + d.url.setText(files[0]) + b.clicked.connect(cf) + d.la = la = QLabel(_( + 'Enter a URL. You can also choose to create a link to a file on ' + 'your computer. If the selected file is an image, it will be ' + 'inserted as an image. Note that if you create a link to a file on ' + 'your computer, it will stop working if the file is moved.')) + la.setWordWrap(True) + la.setStyleSheet('QLabel { margin-bottom: 1.5ex }') + l.setWidget(0, l.SpanningRole, la) l.addRow(_('Enter &URL:'), d.url) - l.addRow(_('Enter name (optional):'), d.name) + l.addRow(_('Enter &name (optional):'), d.name) + l.addRow(_('Choose a file on your computer:'), d.br) l.addRow(d.bb) d.bb.accepted.connect(d.accept) d.bb.rejected.connect(d.reject) - link, name = None, None + d.resize(d.sizeHint()) + link, name, is_image = None, None, False if d.exec_() == d.Accepted: link, name = unicode(d.url.text()).strip(), unicode(d.name.text()).strip() - return link, name + if link and os.path.exists(link): + with lopen(link, 'rb') as f: + q = what(f) + is_image = q in {'jpeg', 'png', 'gif'} + return link, name, is_image def parse_link(self, link): link = link.strip() + if link and os.path.exists(link): + return QUrl.fromLocalFile(link) has_schema = re.match(r'^[a-zA-Z]+:', link) if has_schema is not None: url = QUrl(link, QUrl.TolerantMode)