mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Notes editor: When pasting HTML with images offer to download remote images in the pasted content
This commit is contained in:
parent
c2a2185dbe
commit
dca1d9a3ac
@ -7,6 +7,7 @@ import re
|
||||
import sys
|
||||
import weakref
|
||||
from collections import defaultdict
|
||||
from threading import Thread
|
||||
from contextlib import contextmanager
|
||||
from functools import partial
|
||||
from html5_parser import parse
|
||||
@ -21,14 +22,16 @@ from qt.core import (
|
||||
QVBoxLayout, QWidget, pyqtSignal, pyqtSlot,
|
||||
)
|
||||
|
||||
from calibre import fit_image, xml_replace_entities
|
||||
from calibre import browser, fit_image, xml_replace_entities
|
||||
from calibre.db.constants import DATA_DIR_NAME
|
||||
from calibre.ebooks.chardet import xml_to_unicode
|
||||
from calibre.gui2 import (
|
||||
NO_URL_FORMATTING, choose_dir, choose_files, error_dialog, gprefs, is_dark_theme,
|
||||
safe_open_url,
|
||||
question_dialog, safe_open_url,
|
||||
)
|
||||
from calibre.gui2 import FunctionDispatcher
|
||||
from calibre.gui2.book_details import resolved_css
|
||||
from calibre.gui2.dialogs.progress import ProgressDialog
|
||||
from calibre.gui2.flow_toolbar import create_flow_toolbar
|
||||
from calibre.gui2.widgets import LineEditECM
|
||||
from calibre.gui2.widgets2 import to_plain_text
|
||||
@ -282,6 +285,7 @@ class EditorWidget(QTextEdit, LineEditECM): # {{{
|
||||
|
||||
data_changed = pyqtSignal()
|
||||
insert_images_separately = False
|
||||
can_store_images = False
|
||||
|
||||
@property
|
||||
def readonly(self):
|
||||
@ -561,9 +565,87 @@ class EditorWidget(QTextEdit, LineEditECM): # {{{
|
||||
self.focus_self()
|
||||
|
||||
def do_paste(self):
|
||||
images_before = set()
|
||||
for fmt in self.document().allFormats():
|
||||
if fmt.isImageFormat():
|
||||
images_before.add(fmt.toImageFormat().name())
|
||||
self.paste()
|
||||
if self.can_store_images:
|
||||
added = set()
|
||||
for fmt in self.document().allFormats():
|
||||
if fmt.isImageFormat():
|
||||
name = fmt.toImageFormat().name()
|
||||
if name not in images_before and name.partition(':')[0] in ('https', 'http'):
|
||||
added.add(name)
|
||||
if added and question_dialog(
|
||||
self, _('Download images'), _(
|
||||
'Download all remote images in the pasted content?')):
|
||||
self.download_images(added)
|
||||
self.focus_self()
|
||||
|
||||
def download_images(self, urls):
|
||||
from calibre.web import get_download_filename_from_response
|
||||
br = browser()
|
||||
d = self.document()
|
||||
c = self.textCursor()
|
||||
c.setPosition(0)
|
||||
pos_map = {}
|
||||
while True:
|
||||
c = d.find(OBJECT_REPLACEMENT_CHAR, c, QTextDocument.FindFlag.FindCaseSensitively)
|
||||
if c.isNull():
|
||||
break
|
||||
fmt = c.charFormat()
|
||||
if fmt.isImageFormat():
|
||||
name = fmt.toImageFormat().name()
|
||||
if name in urls:
|
||||
pos_map[name] = c.position()
|
||||
|
||||
pd = ProgressDialog(title=_('Downloading images...'), min=0, max=0, parent=self)
|
||||
pd.canceled_signal.connect(pd.accept)
|
||||
data_map = {}
|
||||
error_map = {}
|
||||
set_msg = FunctionDispatcher(pd.set_msg, parent=pd)
|
||||
accept = FunctionDispatcher(pd.accept, parent=pd)
|
||||
|
||||
def do_download():
|
||||
for url, pos in pos_map.items():
|
||||
if pd.canceled:
|
||||
return
|
||||
set_msg(_('Downloading {}...').format(url))
|
||||
try:
|
||||
res = br.open_novisit(url, timeout=30)
|
||||
fname = get_download_filename_from_response(res)
|
||||
data = res.read()
|
||||
except Exception as err:
|
||||
error_map[url] = err
|
||||
else:
|
||||
data_map[url] = fname, data
|
||||
accept()
|
||||
Thread(target=do_download, daemon=True).start()
|
||||
pd.exec()
|
||||
if pd.canceled:
|
||||
return
|
||||
|
||||
for url, pos in pos_map.items():
|
||||
fname, data = data_map[url]
|
||||
newname = self.commit_downloaded_image(data, fname)
|
||||
c = self.textCursor()
|
||||
c.setPosition(pos)
|
||||
alignment = QTextFrameFormat.Position.InFlow
|
||||
f = self.frame_for_cursor(c)
|
||||
if f is not None:
|
||||
alignment = f.frameFormat().position()
|
||||
fmt = c.charFormat()
|
||||
i = fmt.toImageFormat()
|
||||
i.setName(newname)
|
||||
c.deletePreviousChar()
|
||||
c.insertImage(i, alignment)
|
||||
if error_map:
|
||||
m = '\n'.join(
|
||||
_('Could not download {0} with error:').format(url) + '\n\t' + str(err) + '\n\n' for url, err in error_map.items())
|
||||
error_dialog(self, _('Failed to download some images'), _(
|
||||
'Some images could not be downloaded, click "Show details" to see which ones'), det_msg=m, show=True)
|
||||
|
||||
def do_paste_and_match_style(self):
|
||||
text = QApplication.instance().clipboard().text()
|
||||
if text:
|
||||
|
@ -202,6 +202,7 @@ class NoteEditorWidget(EditorWidget):
|
||||
insert_images_separately = True
|
||||
db = field = item_id = item_val = None
|
||||
images = None
|
||||
can_store_images = True
|
||||
|
||||
def resource_digest_from_qurl(self, qurl):
|
||||
alg = qurl.host()
|
||||
@ -240,6 +241,15 @@ class NoteEditorWidget(EditorWidget):
|
||||
self.document().addResource(rtype, qurl, r) # cache the resource
|
||||
return r
|
||||
|
||||
def commit_downloaded_image(self, data, suggested_filename):
|
||||
digest = hash_data(data)
|
||||
if digest in self.images:
|
||||
ir = self.images[digest]
|
||||
else:
|
||||
self.images[digest] = ir = ImageResource(suggested_filename, digest, data=data)
|
||||
alg, digest = ir.digest.split(':', 1)
|
||||
return RESOURCE_URL_SCHEME + f'://{alg}/{digest}?placement={uuid4()}'
|
||||
|
||||
def get_html_callback(self, root, text):
|
||||
self.searchable_text = text.replace(OBJECT_REPLACEMENT_CHAR, '')
|
||||
self.referenced_resources = set()
|
||||
|
Loading…
x
Reference in New Issue
Block a user