From 5b53a49ba1f55e9359f8926ce3caea878c8d5ad5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 13 Mar 2024 21:33:49 +0530 Subject: [PATCH] Trim image: Allow specifying the size of the trim rectangle using numbers. Fixes #2056116 [Trim Cover: Trim to specified size/ratio](https://bugs.launchpad.net/calibre/+bug/2056116) --- src/calibre/gui2/dialogs/trim_image.py | 67 +++++++++++++++++++- src/calibre/gui2/tweak_book/editor/canvas.py | 37 +++++++---- 2 files changed, 91 insertions(+), 13 deletions(-) diff --git a/src/calibre/gui2/dialogs/trim_image.py b/src/calibre/gui2/dialogs/trim_image.py index 034af0e064..7639c59b39 100644 --- a/src/calibre/gui2/dialogs/trim_image.py +++ b/src/calibre/gui2/dialogs/trim_image.py @@ -7,14 +7,68 @@ __copyright__ = '2013, Kovid Goyal ' import os import sys from qt.core import ( - QDialog, QDialogButtonBox, QHBoxLayout, QIcon, QKeySequence, - QLabel, QSize, Qt, QToolBar, QVBoxLayout + QCheckBox, QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout, QIcon, QKeySequence, + QLabel, QSize, QSpinBox, Qt, QToolBar, QVBoxLayout, ) from calibre.gui2 import gprefs from calibre.gui2.tweak_book.editor.canvas import Canvas +class Region(QDialog): + + ignore_value_changes = False + + def __init__(self, parent, width, height, max_width, max_height): + super().__init__(parent) + self.setWindowTitle(_('Set size of selected area')) + self.l = l = QFormLayout(self) + l.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) + self.width_input = w = QSpinBox(self) + w.setRange(20, max_width), w.setSuffix(' px'), w.setValue(width) + w.valueChanged.connect(self.value_changed) + l.addRow(_('&Width:'), w) + self.height_input = h = QSpinBox(self) + h.setRange(20, max_height), h.setSuffix(' px'), h.setValue(height) + h.valueChanged.connect(self.value_changed) + l.addRow(_('&Height:'), h) + self.const_aspect = ca = QCheckBox(_('Keep the ratio of width to height fixed')) + ca.toggled.connect(self.const_aspect_toggled) + l.addRow(ca) + k = QKeySequence('alt+1', QKeySequence.SequenceFormat.PortableText) + la = QLabel('

'+_('Note that holding down the {} key while dragging the selection handles' + ' will resize the selection while preserving its aspect ratio.').format( + k.toString(QKeySequence.SequenceFormat.NativeText))) + la.setWordWrap(True) + la.setMinimumWidth(400) + l.addRow(la) + self.bb = bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, self) + bb.accepted.connect(self.accept) + bb.rejected.connect(self.reject) + l.addRow(bb) + self.resize(self.sizeHint()) + self.current_aspect = width / height + + def const_aspect_toggled(self): + if self.const_aspect.isChecked(): + self.current_aspect = self.width_input.value() / self.height_input.value() + + def value_changed(self): + if self.ignore_value_changes or not self.const_aspect.isChecked(): + return + src = self.sender() + self.ignore_value_changes = True + if src is self.height_input: + self.width_input.setValue(int(self.current_aspect * self.height_input.value())) + else: + self.height_input.setValue(int(self.width_input.value() / self.current_aspect)) + self.ignore_value_changes = False + + @property + def selection_size(self): + return self.width_input.value(), self.height_input.value() + + class TrimImage(QDialog): def __init__(self, img_data, parent=None): @@ -44,6 +98,8 @@ class TrimImage(QDialog): ac.setToolTip('{} [{}]'.format(_('Trim image by removing borders outside the selected region'), ac.shortcut().toString(QKeySequence.SequenceFormat.NativeText))) ac.setEnabled(False) + self.size_selection = ac = self.bar.addAction(QIcon.ic('resize.png'), _('&Region'), self.do_region) + ac.setToolTip(_('Specify a selection size using numbers to allow for precise control')) c.selection_state_changed.connect(self.selection_changed) c.selection_area_changed.connect(self.selection_area_changed) l.addWidget(c) @@ -73,6 +129,13 @@ class TrimImage(QDialog): def sizeHint(self): return QSize(900, 600) + def do_region(self): + rect = self.canvas.selection_rect_in_image_coords + d = Region(self, int(rect.width()), int(rect.height()), self.canvas.current_image.width(), self.canvas.current_image.height()) + if d.exec() == QDialog.DialogCode.Accepted: + width, height = d.selection_size + self.canvas.set_selection_size_in_image_coords(width, height) + def do_trim(self): self.canvas.trim_image() self.selection_changed(False) diff --git a/src/calibre/gui2/tweak_book/editor/canvas.py b/src/calibre/gui2/tweak_book/editor/canvas.py index e4c265550e..84ba1b20d5 100644 --- a/src/calibre/gui2/tweak_book/editor/canvas.py +++ b/src/calibre/gui2/tweak_book/editor/canvas.py @@ -4,25 +4,26 @@ __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' -import sys, weakref +import sys +import weakref from functools import wraps from io import BytesIO - from qt.core import ( - QWidget, QPainter, QColor, QApplication, Qt, QPixmap, QRectF, QTransform, - QPointF, QPen, pyqtSignal, QUndoCommand, QUndoStack, QIcon, QImage, - QImageWriter) + QApplication, QColor, QIcon, QImage, QImageWriter, QPainter, QPen, QPixmap, QPointF, + QRect, QRectF, Qt, QTransform, QUndoCommand, QUndoStack, QWidget, pyqtSignal, +) from calibre import fit_image from calibre.gui2 import error_dialog, pixmap_to_data from calibre.gui2.dnd import ( - image_extensions, dnd_has_extension, dnd_has_image, dnd_get_image, DownloadDialog) -from calibre.gui2.tweak_book import capitalize -from calibre.utils.imghdr import identify -from calibre.utils.img import ( - remove_borders_from_image, gaussian_sharpen_image, gaussian_blur_image, image_to_data, despeckle_image, - normalize_image, oil_paint_image + DownloadDialog, dnd_get_image, dnd_has_extension, dnd_has_image, image_extensions, ) +from calibre.gui2.tweak_book import capitalize +from calibre.utils.img import ( + despeckle_image, gaussian_blur_image, gaussian_sharpen_image, image_to_data, + normalize_image, oil_paint_image, remove_borders_from_image, +) +from calibre.utils.imghdr import identify def painter(func): @@ -575,6 +576,20 @@ class Canvas(QWidget): self.selection_state.current_mode = 'select' self.selection_state.rect = None self.selection_state_changed.emit(False) + @property + def selection_rect_in_image_coords(self): + if self.selection_state.current_mode == 'selected': + left, top, width, height = self.rect_for_trim() + return QRect(0, 0, int(width), int(height)) + return self.current_image.rect() + + def set_selection_size_in_image_coords(self, width, height): + self.selection_state.reset() + i = self.current_image + self.selection_state.rect = QRectF(self.target.left(), self.target.top(), + width * self.target.width() / i.width(), height * self.target.height() / i.height()) + self.selection_state.current_mode = 'selected' + self.update() def mouseMoveEvent(self, ev): changed = False