Get basic image insertion working

This commit is contained in:
Kovid Goyal 2023-09-05 21:31:02 +05:30
parent 54c43bc7ef
commit 6a3d7388d7
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 178 additions and 16 deletions

View File

@ -32,6 +32,7 @@ copy_marked_up_text = cmt()
SEP = b'\0\x1c\0' SEP = b'\0\x1c\0'
DOC_NAME = 'doc.html' DOC_NAME = 'doc.html'
METADATA_EXT = '.metadata' METADATA_EXT = '.metadata'
RESOURCE_URL_SCHEME = 'calres'
def hash_data(data: bytes) -> str: def hash_data(data: bytes) -> str:

View File

@ -346,6 +346,7 @@ class EditorWidget(QTextEdit, LineEditECM): # {{{
r('background', 'format-fill-color', _('Background color')) r('background', 'format-fill-color', _('Background color'))
r('insert_link', 'insert-link', _('Insert link') if self.insert_images_separately else _('Insert link or image'), r('insert_link', 'insert-link', _('Insert link') if self.insert_images_separately else _('Insert link or image'),
shortcut=QKeySequence('Ctrl+l', QKeySequence.SequenceFormat.PortableText)) shortcut=QKeySequence('Ctrl+l', QKeySequence.SequenceFormat.PortableText))
r('insert_image', 'view-image', _('Insert image'), shortcut=QKeySequence('Ctrl+p', QKeySequence.SequenceFormat.PortableText))
r('insert_hr', 'format-text-hr', _('Insert separator'),) r('insert_hr', 'format-text-hr', _('Insert separator'),)
r('clear', 'trash', _('Clear')) r('clear', 'trash', _('Clear'))
@ -648,6 +649,14 @@ class EditorWidget(QTextEdit, LineEditECM): # {{{
c.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.MoveAnchor) c.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.MoveAnchor)
c.insertHtml('<hr>') c.insertHtml('<hr>')
def do_insert_image(self):
from calibre.gui2 import choose_images
files = choose_images(self, 'choose-image-for-comments-editor', _('Choose image'), formats='png jpeg jpg gif svg webp'.split())
if files:
self.focus_self()
with self.editing_cursor() as c:
c.insertImage(files[0])
def do_insert_link(self, *args): def do_insert_link(self, *args):
link, name, is_image = self.ask_link() link, name, is_image = self.ask_link()
if not link: if not link:
@ -907,6 +916,8 @@ class EditorWidget(QTextEdit, LineEditECM): # {{{
menu.addMenu(am) menu.addMenu(am)
am.addAction(self.action_block_style) am.addAction(self.action_block_style)
am.addAction(self.action_insert_link) am.addAction(self.action_insert_link)
if self.insert_images_separately:
am.addAction(self.action_insert_image)
am.addAction(self.action_background) am.addAction(self.action_background)
am.addAction(self.action_color) am.addAction(self.action_color)
menu.addAction(_('Smarten punctuation'), parent.smarten_punctuation) menu.addAction(_('Smarten punctuation'), parent.smarten_punctuation)
@ -1206,6 +1217,8 @@ class Editor(QWidget): # {{{
self.toolbar.add_action(self.editor.action_block_style, popup_mode=QToolButton.ToolButtonPopupMode.InstantPopup) self.toolbar.add_action(self.editor.action_block_style, popup_mode=QToolButton.ToolButtonPopupMode.InstantPopup)
self.toolbar.add_action(self.editor.action_insert_link) self.toolbar.add_action(self.editor.action_insert_link)
if self.editor.insert_images_separately:
self.toolbar.add_action(self.editor.action_insert_image)
self.toolbar.add_action(self.editor.action_insert_hr) self.toolbar.add_action(self.editor.action_insert_hr)
self.toolbar.add_separator() self.toolbar.add_separator()

View File

@ -3,13 +3,20 @@
import os import os
from qt.core import ( from qt.core import (
QDialog, QFormLayout, QIcon, QLineEdit, QSize, Qt, QVBoxLayout, QWidget, pyqtSlot, QButtonGroup, QByteArray, QDialog, QFormLayout, QHBoxLayout, QIcon, QLabel,
QLineEdit, QPixmap, QPushButton, QRadioButton, QSize, Qt, QTextFrameFormat,
QTextImageFormat, QVBoxLayout, QWidget, pyqtSlot,
) )
from typing import NamedTuple
from calibre.gui2 import Application from calibre.db.notes.connect import RESOURCE_URL_SCHEME, hash_data
from calibre.gui2 import Application, choose_images, error_dialog
from calibre.gui2.comments_editor import Editor, EditorWidget from calibre.gui2.comments_editor import Editor, EditorWidget
from calibre.gui2.widgets import ImageView
from calibre.gui2.widgets2 import Dialog from calibre.gui2.widgets2 import Dialog
IMAGE_EXTENSIONS = 'png', 'jpeg', 'jpg', 'gif', 'svg', 'webp'
class AskLink(Dialog): # {{{ class AskLink(Dialog): # {{{
@ -47,18 +54,136 @@ class AskLink(Dialog): # {{{
# }}} # }}}
# Images {{{
class ImageResource(NamedTuple):
name: str
digest: str
path: str = ''
data: bytes = b''
from_db: bool = False
class AskImage(Dialog):
def __init__(self, local_images, db, parent=None):
self.local_images = local_images
self.db = db
self.current_digest = ''
super().__init__(_('Insert image'), 'insert-image-for-notes', parent=parent)
self.setWindowIcon(QIcon.ic('view-image.png'))
def setup_ui(self):
self.v = v = QVBoxLayout(self)
self.h = h = QHBoxLayout()
v.addLayout(h)
v.addWidget(self.bb)
self.image_preview = ip = ImageView(self, 'insert-image-for-notes-preview', True)
ip.cover_changed.connect(self.image_pasted_or_dropped)
h.addWidget(ip)
self.vr = vr = QVBoxLayout()
h.addLayout(vr)
self.la = la = QLabel(_('Choose an image:'))
vr.addWidget(la)
self.name_edit = ne = QLineEdit(self)
ne.setPlaceholderText(_('Filename for the image'))
vr.addWidget(ne)
self.la2 = la = QLabel(_('Place image:'))
vr.addWidget(la)
self.hr = hr = QHBoxLayout()
vr.addLayout(hr)
self.image_layout_group = bg = QButtonGroup(self)
self.float_left = r = QRadioButton(_('Float &left'))
bg.addButton(r), hr.addWidget(r)
self.inline = r = QRadioButton(_('Inline'))
bg.addButton(r), hr.addWidget(r)
self.float_right = r = QRadioButton(_('Float &right'))
bg.addButton(r), hr.addWidget(r)
self.inline.setChecked(True)
self.hb = hb = QHBoxLayout()
vr.addLayout(hb)
self.add_file_button = b = QPushButton(QIcon.ic('document_open.png'), _('Choose image &file'), self)
b.clicked.connect(self.add_file)
hb.addWidget(b)
self.existing_button = b = QPushButton(QIcon.ic('view-image.png'), _('Browse &existing'), self)
b.clicked.connect(self.browse_existing)
hb.addWidget(b)
self.paste_button = b = QPushButton(QIcon.ic('edit-paste.png'), _('&Paste from clipboard'), self)
b.clicked.connect(self.paste_image)
hb.addWidget(b)
vr.addStretch(10)
def image_pasted_or_dropped(self, cover_data):
digest = hash_data(cover_data)
if digest in self.local_images:
ir = self.local_images[digest]
else:
self.local_images[digest] = ir = ImageResource('unnamed.png', digest, data=cover_data)
self.name_edit.setText(ir.name)
self.current_digest = digest
def browse_existing(self):
raise NotImplementedError('TODO: Implement me')
def add_file(self):
files = choose_images(self, 'choose-image-for-notes', _('Choose image'), formats=IMAGE_EXTENSIONS)
if files:
with open(files[0], 'rb') as f:
data = f.read()
digest = hash_data(data)
p = QPixmap()
if not p.loadFromData(data) or p.isNull():
return error_dialog(self, _('Bad image'), _(
'Failed to render the image in {}').format(files[0]), show=True)
ir = ImageResource(os.path.basename(files[0]), digest, path=files[0])
self.local_images[digest] = ir
self.image_preview.set_pixmap(p)
self.name_edit.setText(ir.name)
self.current_digest = digest
def paste_image(self):
if not self.image_preview.paste_from_clipboard():
return error_dialog(self, _('Could not paste'), _(
'No image is present int he system clipboard'), show=True)
@property
def image_layout(self) -> 'QTextFrameFormat.Position':
b = self.image_layout_group.checkedButton()
if b is self.inline:
return QTextFrameFormat.Position.InFlow
if b is self.float_left:
return QTextFrameFormat.Position.FloatLeft
return QTextFrameFormat.Position.FloatRight
# }}}
class NoteEditorWidget(EditorWidget): class NoteEditorWidget(EditorWidget):
load_resource = None
insert_images_separately = True insert_images_separately = True
db = field = item_id = item_val = None
def __init__(self, *args, **kw): images = None
super().__init__(*args, **kw)
@pyqtSlot(int, 'QUrl', result='QVariant') @pyqtSlot(int, 'QUrl', result='QVariant')
def loadResource(self, rtype, qurl): def loadResource(self, rtype, qurl):
if self.load_resource is not None: if self.db is None or self.images is None:
return self.load_resource(rtype, qurl) return
if qurl.scheme() != RESOURCE_URL_SCHEME:
return
digest = qurl.path()[1:]
ir = self.images.get(digest)
if ir is not None:
if ir.data:
return QByteArray(ir.data)
if ir.path:
with open(ir.path, 'rb') as f:
return QByteArray(f.read())
def get_html_callback(self, root): def get_html_callback(self, root):
self.searchable_text = '' self.searchable_text = ''
@ -72,6 +197,16 @@ class NoteEditorWidget(EditorWidget):
return d.url, d.link_name, False return d.url, d.link_name, False
return '', '', False return '', '', False
def do_insert_image(self):
d = AskImage(self.images, self.db)
if d.exec() == QDialog.DialogCode.Accepted and d.current_digest:
ir = self.images[d.current_digest]
self.focus_self()
with self.editing_cursor() as c:
fmt = QTextImageFormat()
fmt.setName(RESOURCE_URL_SCHEME + ':///' + ir.digest)
c.insertImage(fmt, d.image_layout)
class NoteEditor(Editor): class NoteEditor(Editor):
@ -87,23 +222,21 @@ class EditNoteWidget(QWidget):
def __init__(self, db, field, item_id, item_val, parent=None): def __init__(self, db, field, item_id, item_val, parent=None):
super().__init__(parent) super().__init__(parent)
self.db, self.field, self.item_id, self.item_val = db, field, item_id, item_val
self.l = l = QVBoxLayout(self) self.l = l = QVBoxLayout(self)
l.setContentsMargins(0, 0, 0, 0) l.setContentsMargins(0, 0, 0, 0)
self.editor = e = NoteEditor(self, toolbar_prefs_name='edit-notes-for-category-ce') self.editor = e = NoteEditor(self, toolbar_prefs_name='edit-notes-for-category-ce')
e.editor.load_resource = self.load_resource e.editor.db, e.editor.field, e.editor.item_id, e.editor.item_val = db, field, item_id, item_val
e.editor.images = {}
l.addWidget(e) l.addWidget(e)
e.html = self.db.notes_for(field, item_id) or '' e.html = db.notes_for(field, item_id) or ''
def load_resource(self, resource_type, qurl):
pass
def sizeHint(self): def sizeHint(self):
return QSize(800, 600) return QSize(800, 600)
def commit(self): def commit(self):
doc, searchable_text, resources = self.editor.get_doc() doc, searchable_text, resources = self.editor.get_doc()
self.db.set_notes_for(self.field, self.item_id, doc, searchable_text, resources, remove_unused_resources=True) s = self.editor.editor
s.db.set_notes_for(s.field, s.item_id, doc, searchable_text, resources)
return True return True
@ -130,9 +263,22 @@ class EditNoteDialog(Dialog):
super().accept() super().accept()
if __name__ == '__main__': def develop_edit_note():
from calibre.library import db as dbc from calibre.library import db as dbc
app = Application([]) app = Application([])
d = EditNoteDialog('authors', 1, dbc(os.path.expanduser('~/test library'))) d = EditNoteDialog('authors', 1, dbc(os.path.expanduser('~/test library')))
d.exec() d.exec()
del d, app del d, app
def develop_ask_image():
app = Application([])
from calibre.library import db as dbc
d = AskImage({},dbc(os.path.expanduser('~/test library')))
d.exec()
del d, app
if __name__ == '__main__':
develop_edit_note()
# develop_ask_image()

View File

@ -301,6 +301,8 @@ class ImageDropMixin: # {{{
self.set_pixmap(pmap) self.set_pixmap(pmap)
self.cover_changed.emit( self.cover_changed.emit(
pixmap_to_data(pmap, format='PNG')) pixmap_to_data(pmap, format='PNG'))
return True
return False
# }}} # }}}