mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Get basic image insertion working
This commit is contained in:
parent
54c43bc7ef
commit
6a3d7388d7
@ -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:
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user