From c1ca6616bddbaa353c6863de844aef6a38426a54 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 27 Oct 2023 18:44:17 +0530 Subject: [PATCH] Notes editor: Allow specifying image sizes --- src/calibre/gui2/comments_editor.py | 62 +++++++++++++++++-- .../gui2/dialogs/edit_category_notes.py | 57 +++++++++++++---- 2 files changed, 104 insertions(+), 15 deletions(-) diff --git a/src/calibre/gui2/comments_editor.py b/src/calibre/gui2/comments_editor.py index 734e785ca9..b5d7691345 100644 --- a/src/calibre/gui2/comments_editor.py +++ b/src/calibre/gui2/comments_editor.py @@ -4,6 +4,7 @@ import os import re +import sys import weakref from collections import defaultdict from contextlib import contextmanager @@ -14,13 +15,13 @@ from qt.core import ( QAction, QApplication, QBrush, QByteArray, QCheckBox, QColor, QColorDialog, QDialog, QDialogButtonBox, QFont, QFontInfo, QFontMetrics, QFormLayout, QHBoxLayout, QIcon, QKeySequence, QLabel, QLineEdit, QMenu, QPalette, QPlainTextEdit, QPointF, - QPushButton, QSize, QSyntaxHighlighter, Qt, QTabWidget, QTextBlockFormat, + QPushButton, QSize, QSpinBox, QSyntaxHighlighter, Qt, QTabWidget, QTextBlockFormat, QTextCharFormat, QTextCursor, QTextDocument, QTextEdit, QTextFormat, - QTextFrameFormat, QTextListFormat, QTimer, QToolButton, QUrl, QVBoxLayout, QWidget, - pyqtSignal, pyqtSlot, + QTextFrameFormat, QTextImageFormat, QTextListFormat, QTimer, QToolButton, QUrl, + QVBoxLayout, QWidget, pyqtSignal, pyqtSlot, ) -from calibre import xml_replace_entities +from calibre import 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 ( @@ -914,6 +915,56 @@ class EditorWidget(QTextEdit, LineEditECM): # {{{ if fmt.isImageFormat(): c.deletePreviousChar() + def resize_image_at(self, cursor_pos): + c = self.textCursor() + c.clearSelection() + c.setPosition(cursor_pos) + c.movePosition(QTextCursor.MoveOperation.PreviousCharacter, QTextCursor.MoveMode.KeepAnchor) + fmt = c.charFormat() + if not fmt.isImageFormat(): + return + fmt = fmt.toImageFormat() + from calibre.utils.img import image_from_data + img = image_from_data(self.loadResource(QTextDocument.ResourceType.ImageResource, QUrl(fmt.name()))) + w, h = int(fmt.width()), int(fmt.height()) + d = QDialog(self) + l = QVBoxLayout(d) + la = QLabel(_('Shrink image to fit within:')) + h = QHBoxLayout() + l.addLayout(h) + la = QLabel(_('&Width:')) + h.addWidget(la) + d.width = w = QSpinBox(self) + w.setRange(0, 10000), w.setSuffix(' px') + w.setValue(int(fmt.width())) + h.addWidget(w), la.setBuddy(w) + w.setSpecialValueText(' ') + la = QLabel(_('&Height:')) + h.addWidget(la) + d.height = w = QSpinBox(self) + w.setRange(0, 10000), w.setSuffix(' px') + w.setValue(int(fmt.height())) + h.addWidget(w), la.setBuddy(w) + w.setSpecialValueText(' ') + h.addStretch(10) + bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) + bb.accepted.connect(d.accept), bb.rejected.connect(d.reject) + d.setWindowTitle(_('Enter new size for image')) + l.addWidget(bb) + d.resize(d.sizeHint()) + + if d.exec() == QDialog.DialogCode.Accepted: + page_width, page_height = (d.width.value() or sys.maxsize), (d.height.value() or sys.maxsize) + w, h = int(img.width()), int(img.height()) + resized, nw, nh = fit_image(w, h, page_width, page_height) + if resized: + fmt.setWidth(nw), fmt.setHeight(nh) + else: + f = QTextImageFormat() + f.setName(fmt.name()) + fmt = f + c.setCharFormat(fmt) + def align_image_at(self, cursor_pos, alignment): c = self.textCursor() c.clearSelection() @@ -971,6 +1022,9 @@ class EditorWidget(QTextEdit, LineEditECM): # {{{ ac.triggered.connect(partial(self.align_image_at, c.position(), epos)) if pos == epos: ac.setChecked(True) + cs = align_menu.addAction(QIcon.ic('resize.png'), _('Change size')) + cs.triggered.connect(partial(self.resize_image_at, c.position())) + align_menu.addSeparator() a(_('Float to the left'), QTextFrameFormat.Position.FloatLeft) a(_('Inline with text'), QTextFrameFormat.Position.InFlow) a(_('Float to the right'), QTextFrameFormat.Position.FloatRight) diff --git a/src/calibre/gui2/dialogs/edit_category_notes.py b/src/calibre/gui2/dialogs/edit_category_notes.py index f0f3bcd6aa..6da7771188 100644 --- a/src/calibre/gui2/dialogs/edit_category_notes.py +++ b/src/calibre/gui2/dialogs/edit_category_notes.py @@ -2,15 +2,16 @@ # License: GPLv3 Copyright: 2023, Kovid Goyal import os +import sys from qt.core import ( QButtonGroup, QByteArray, QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout, - QIcon, QLabel, QLineEdit, QPixmap, QPushButton, QRadioButton, QSize, Qt, + QIcon, QLabel, QLineEdit, QPixmap, QPushButton, QRadioButton, QSize, QSpinBox, Qt, QTextDocument, QTextFrameFormat, QTextImageFormat, QUrl, QVBoxLayout, QWidget, pyqtSlot, ) from typing import NamedTuple -from calibre import sanitize_file_name +from calibre import sanitize_file_name, fit_image from calibre.db.constants import RESOURCE_URL_SCHEME from calibre.db.notes.connect import hash_data from calibre.db.notes.exim import export_note, import_note @@ -83,6 +84,7 @@ class AskImage(Dialog): 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) @@ -100,6 +102,15 @@ class AskImage(Dialog): ne.setPlaceholderText(_('Filename for the image')) vr.addWidget(ne) + 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.paste_button = b = QPushButton(QIcon.ic('edit-paste.png'), _('&Paste from clipboard'), self) + b.clicked.connect(self.paste_image) + hb.addWidget(b) + self.la2 = la = QLabel(_('Place image:')) vr.addWidget(la) self.hr = hr = QHBoxLayout() @@ -113,14 +124,23 @@ class AskImage(Dialog): 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.paste_button = b = QPushButton(QIcon.ic('edit-paste.png'), _('&Paste from clipboard'), self) - b.clicked.connect(self.paste_image) - hb.addWidget(b) + self.la2 = la = QLabel(_('Shrink image to fit within:')) + vr.addWidget(la) + self.hr2 = h = QHBoxLayout() + vr.addLayout(h) + la = QLabel(_('&Width:')) + h.addWidget(la) + self.width = w = QSpinBox(self) + w.setRange(0, 10000), w.setSuffix(' px') + h.addWidget(w), la.setBuddy(w) + w.setSpecialValueText(' ') + la = QLabel(_('&Height:')) + h.addWidget(la) + self.height = w = QSpinBox(self) + w.setRange(0, 10000), w.setSuffix(' px') + h.addWidget(w), la.setBuddy(w) + w.setSpecialValueText(' ') + h.addStretch(10) vr.addStretch(10) self.add_file_button.setFocus(Qt.FocusReason.OtherFocusReason) @@ -154,7 +174,7 @@ class AskImage(Dialog): 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) + 'No image is present in the system clipboard'), show=True) @property def image_layout(self) -> 'QTextFrameFormat.Position': @@ -164,6 +184,15 @@ class AskImage(Dialog): if b is self.float_left: return QTextFrameFormat.Position.FloatLeft return QTextFrameFormat.Position.FloatRight + + @property + def image_size(self) -> tuple[int, int]: + s = self.image_preview.pixmap().size() + return s.width(), s.height() + + @property + def bounding_size(self) -> tuple[int, int]: + return (self.width.value() or sys.maxsize), (self.height.value() or sys.maxsize) # }}} @@ -238,6 +267,12 @@ class NoteEditorWidget(EditorWidget): fmt = QTextImageFormat() alg, digest = ir.digest.split(':', 1) fmt.setName(RESOURCE_URL_SCHEME + f'://{alg}/{digest}?placement={uuid4()}') + page_width, page_height = d.bounding_size + w, h = d.image_size + resized, nw, nh = fit_image(w, h, page_width, page_height) + if resized: + fmt.setWidth(nw) + fmt.setHeight(nh) c.insertImage(fmt, d.image_layout)