From 6ff4b9ac4ac0d9c98230dcadd33b767c8b3d30a7 Mon Sep 17 00:00:00 2001
From: twodoorcoupe
Date: Wed, 19 Jun 2024 19:23:09 +0200
Subject: [PATCH 1/7] Add more options to image resizing
---
picard/coverart/processing/processors.py | 58 ++++++---
.../extension_points/cover_art_processors.py | 2 +-
picard/options.py | 18 ++-
.../ui/forms/ui_options_cover_processing.py | 122 +++++++++++-------
picard/ui/options/cover_processing.py | 54 +++++---
test/test_coverart_processing.py | 18 ++-
ui/options_cover_processing.ui | 96 +++++++++++---
7 files changed, 259 insertions(+), 109 deletions(-)
diff --git a/picard/coverart/processing/processors.py b/picard/coverart/processing/processors.py
index 7bb3ee819..c3b267983 100644
--- a/picard/coverart/processing/processors.py
+++ b/picard/coverart/processing/processors.py
@@ -35,31 +35,48 @@ class ResizeImage(ImageProcessor):
def save_to_file(self):
config = get_config()
- return config.setting['resize_images_saved_to_file']
+ return config.setting['cover_file_scale_down'] or config.setting['cover_file_scale_up']
def save_to_tags(self):
config = get_config()
- return config.setting['resize_images_saved_to_tags']
+ return config.setting['cover_tags_scale_down'] or config.setting['cover_tags_scale_up']
def same_processing(self):
- config = get_config()
- same_width = config.setting['cover_tags_maximum_width'] == config.setting['cover_file_maximum_width']
- same_height = config.setting['cover_tags_maximum_height'] == config.setting['cover_file_maximum_height']
- return self.save_to_tags and self.save_to_file and same_width and same_height
+ setting = get_config().setting
+ same_up = setting['cover_file_scale_up'] == setting['cover_tags_scale_up']
+ same_down = setting['cover_file_scale_down'] == setting['cover_tags_scale_down']
+ same_width = setting['cover_file_resize_use_width'] == setting['cover_tags_resize_use_width']
+ if setting['cover_file_resize_use_width'] and setting['cover_tags_resize_use_width']:
+ same_width = setting['cover_file_resize_target_width'] == setting['cover_tags_resize_target_width']
+ same_height = setting['cover_file_resize_use_height'] == setting['cover_tags_resize_use_height']
+ if setting['cover_file_resize_use_height'] and setting['cover_tags_resize_use_height']:
+ same_height = setting['cover_file_resize_target_height'] == setting['cover_tags_resize_target_height']
+ return same_up and same_down and same_width and same_height and self.save_to_file() and self.save_to_tags()
- def run(self, image, target):
- start_time = time.time()
+ def _find_target_size(self, image, target):
config = get_config()
+ target_width = image.info.width
+ target_height = image.info.height
if target == ProcessingTarget.TAGS:
- max_width = config.setting['cover_tags_maximum_width']
- max_height = config.setting['cover_tags_maximum_height']
+ if config.setting['cover_tags_resize_use_width']:
+ target_width = config.setting['cover_tags_resize_target_width']
+ if config.setting['cover_tags_resize_use_height']:
+ target_height = config.setting['cover_tags_resize_target_height']
+ scaling_up = config.setting['cover_tags_scale_up']
+ scaling_down = config.setting['cover_tags_scale_down']
else:
- max_width = config.setting['cover_file_maximum_width']
- max_height = config.setting['cover_file_maximum_height']
- if image.info.width <= max_width and image.info.height <= max_height:
- return
+ if config.setting['cover_file_resize_use_width']:
+ target_width = config.setting['cover_file_resize_target_width']
+ if config.setting['cover_file_resize_use_height']:
+ target_height = config.setting['cover_file_resize_target_height']
+ scaling_up = config.setting['cover_file_scale_up']
+ scaling_down = config.setting['cover_file_scale_down']
+ return target_width, target_height, scaling_up, scaling_down
+
+ def _resize_image(self, image, target_width, target_height, aspect_ratio):
+ start_time = time.time()
qimage = image.get_result()
- scaled_image = qimage.scaled(max_width, max_height, Qt.AspectRatioMode.KeepAspectRatio)
+ scaled_image = qimage.scaled(target_width, target_height, aspect_ratio)
log.debug(
"Resized cover art from %d x %d to %d x %d in %.2f ms",
image.info.width,
@@ -73,5 +90,16 @@ class ResizeImage(ImageProcessor):
image.info.datalen = scaled_image.sizeInBytes()
image.set_result(scaled_image)
+ def run(self, image, target):
+ target_width, target_height, scaling_up, scaling_down = self._find_target_size(image, target)
+ if scaling_down and (image.info.width > target_width or image.info.height > target_height):
+ aspect_ratio = Qt.AspectRatioMode.KeepAspectRatio
+ elif scaling_up and (image.info.width < target_width or image.info.height < target_height):
+ aspect_ratio = Qt.AspectRatioMode.KeepAspectRatioByExpanding
+ else:
+ # no resizing is needed
+ return
+ self._resize_image(image, target_width, target_height, aspect_ratio)
+
register_cover_art_processor(ResizeImage)
diff --git a/picard/extension_points/cover_art_processors.py b/picard/extension_points/cover_art_processors.py
index fa14404c2..e7456f26f 100644
--- a/picard/extension_points/cover_art_processors.py
+++ b/picard/extension_points/cover_art_processors.py
@@ -86,7 +86,7 @@ class ImageProcessor:
def same_processing(self):
return False
- def run(self, data, target):
+ def run(self, image, target):
pass
diff --git a/picard/options.py b/picard/options.py
index f2969c83c..ebe11edf5 100644
--- a/picard/options.py
+++ b/picard/options.py
@@ -175,12 +175,18 @@ BoolOption('setting', 'save_only_one_front_image', False, title=N_("Save only a
BoolOption('setting', 'filter_cover_by_size', False)
IntOption('setting', 'cover_minimum_width', DEFAULT_COVER_MIN_SIZE)
IntOption('setting', 'cover_minimum_height', DEFAULT_COVER_MIN_SIZE)
-BoolOption('setting', 'resize_images_saved_to_tags', False)
-IntOption('setting', 'cover_tags_maximum_width', DEFAULT_COVER_MAX_SIZE)
-IntOption('setting', 'cover_tags_maximum_height', DEFAULT_COVER_MAX_SIZE)
-BoolOption('setting', 'resize_images_saved_to_file', False)
-IntOption('setting', 'cover_file_maximum_width', DEFAULT_COVER_MAX_SIZE)
-IntOption('setting', 'cover_file_maximum_height', DEFAULT_COVER_MAX_SIZE)
+BoolOption('setting', 'cover_tags_scale_up', False)
+BoolOption('setting', 'cover_tags_scale_down', False)
+BoolOption('setting', 'cover_tags_resize_use_width', True)
+IntOption('setting', 'cover_tags_resize_target_width', DEFAULT_COVER_MAX_SIZE)
+BoolOption('setting', 'cover_tags_resize_use_height', True)
+IntOption('setting', 'cover_tags_resize_target_height', DEFAULT_COVER_MAX_SIZE)
+BoolOption('setting', 'cover_file_scale_up', False)
+BoolOption('setting', 'cover_file_scale_down', False)
+BoolOption('setting', 'cover_file_resize_use_width', True)
+IntOption('setting', 'cover_file_resize_target_width', DEFAULT_COVER_MAX_SIZE)
+BoolOption('setting', 'cover_file_resize_use_height', True)
+IntOption('setting', 'cover_file_resize_target_height', DEFAULT_COVER_MAX_SIZE)
# picard/ui/options/dialog.py
# Attached Profiles
diff --git a/picard/ui/forms/ui_options_cover_processing.py b/picard/ui/forms/ui_options_cover_processing.py
index c47f6eacd..6007c8af6 100644
--- a/picard/ui/forms/ui_options_cover_processing.py
+++ b/picard/ui/forms/ui_options_cover_processing.py
@@ -17,7 +17,7 @@ from picard.i18n import gettext as _
class Ui_CoverProcessingOptionsPage(object):
def setupUi(self, CoverProcessingOptionsPage):
CoverProcessingOptionsPage.setObjectName("CoverProcessingOptionsPage")
- CoverProcessingOptionsPage.resize(518, 285)
+ CoverProcessingOptionsPage.resize(478, 361)
self.verticalLayout = QtWidgets.QVBoxLayout(CoverProcessingOptionsPage)
self.verticalLayout.setObjectName("verticalLayout")
self.filtering = QtWidgets.QGroupBox(parent=CoverProcessingOptionsPage)
@@ -88,30 +88,42 @@ class Ui_CoverProcessingOptionsPage(object):
self.horizontalLayout_7 = QtWidgets.QHBoxLayout(self.resizing)
self.horizontalLayout_7.setObjectName("horizontalLayout_7")
self.save_to_tags = QtWidgets.QGroupBox(parent=self.resizing)
- self.save_to_tags.setCheckable(True)
+ self.save_to_tags.setCheckable(False)
self.save_to_tags.setChecked(False)
self.save_to_tags.setObjectName("save_to_tags")
self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.save_to_tags)
self.verticalLayout_3.setObjectName("verticalLayout_3")
+ self.tags_scale_widget = QtWidgets.QWidget(parent=self.save_to_tags)
+ self.tags_scale_widget.setObjectName("tags_scale_widget")
+ self.horizontalLayout_8 = QtWidgets.QHBoxLayout(self.tags_scale_widget)
+ self.horizontalLayout_8.setContentsMargins(0, 0, 0, 0)
+ self.horizontalLayout_8.setObjectName("horizontalLayout_8")
+ self.tags_scale_up = QtWidgets.QCheckBox(parent=self.tags_scale_widget)
+ self.tags_scale_up.setObjectName("tags_scale_up")
+ self.horizontalLayout_8.addWidget(self.tags_scale_up)
+ self.tags_scale_down = QtWidgets.QCheckBox(parent=self.tags_scale_widget)
+ self.tags_scale_down.setObjectName("tags_scale_down")
+ self.horizontalLayout_8.addWidget(self.tags_scale_down)
+ self.verticalLayout_3.addWidget(self.tags_scale_widget)
self.tags_resize_width_widget = QtWidgets.QWidget(parent=self.save_to_tags)
self.tags_resize_width_widget.setObjectName("tags_resize_width_widget")
self.horizontalLayout_5 = QtWidgets.QHBoxLayout(self.tags_resize_width_widget)
self.horizontalLayout_5.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout_5.setSpacing(4)
self.horizontalLayout_5.setObjectName("horizontalLayout_5")
- self.tags_resized_width_label = QtWidgets.QLabel(parent=self.tags_resize_width_widget)
- self.tags_resized_width_label.setObjectName("tags_resized_width_label")
- self.horizontalLayout_5.addWidget(self.tags_resized_width_label)
- self.tags_resized_width_value = QtWidgets.QSpinBox(parent=self.tags_resize_width_widget)
+ self.tags_resize_width_label = QtWidgets.QCheckBox(parent=self.tags_resize_width_widget)
+ self.tags_resize_width_label.setObjectName("tags_resize_width_label")
+ self.horizontalLayout_5.addWidget(self.tags_resize_width_label)
+ self.tags_resize_width_value = QtWidgets.QSpinBox(parent=self.tags_resize_width_widget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Maximum, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.tags_resized_width_value.sizePolicy().hasHeightForWidth())
- self.tags_resized_width_value.setSizePolicy(sizePolicy)
- self.tags_resized_width_value.setMaximum(9999)
- self.tags_resized_width_value.setProperty("value", 1000)
- self.tags_resized_width_value.setObjectName("tags_resized_width_value")
- self.horizontalLayout_5.addWidget(self.tags_resized_width_value)
+ sizePolicy.setHeightForWidth(self.tags_resize_width_value.sizePolicy().hasHeightForWidth())
+ self.tags_resize_width_value.setSizePolicy(sizePolicy)
+ self.tags_resize_width_value.setMaximum(9999)
+ self.tags_resize_width_value.setProperty("value", 1000)
+ self.tags_resize_width_value.setObjectName("tags_resize_width_value")
+ self.horizontalLayout_5.addWidget(self.tags_resize_width_value)
self.px_label5 = QtWidgets.QLabel(parent=self.tags_resize_width_widget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Maximum, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
@@ -127,19 +139,19 @@ class Ui_CoverProcessingOptionsPage(object):
self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout_3.setSpacing(4)
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
- self.tags_resized_height_label = QtWidgets.QLabel(parent=self.tags_resize_height_widget)
- self.tags_resized_height_label.setObjectName("tags_resized_height_label")
- self.horizontalLayout_3.addWidget(self.tags_resized_height_label)
- self.tags_resized_height_value = QtWidgets.QSpinBox(parent=self.tags_resize_height_widget)
+ self.tags_resize_height_label = QtWidgets.QCheckBox(parent=self.tags_resize_height_widget)
+ self.tags_resize_height_label.setObjectName("tags_resize_height_label")
+ self.horizontalLayout_3.addWidget(self.tags_resize_height_label)
+ self.tags_resize_height_value = QtWidgets.QSpinBox(parent=self.tags_resize_height_widget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Maximum, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.tags_resized_height_value.sizePolicy().hasHeightForWidth())
- self.tags_resized_height_value.setSizePolicy(sizePolicy)
- self.tags_resized_height_value.setMaximum(9999)
- self.tags_resized_height_value.setProperty("value", 1000)
- self.tags_resized_height_value.setObjectName("tags_resized_height_value")
- self.horizontalLayout_3.addWidget(self.tags_resized_height_value)
+ sizePolicy.setHeightForWidth(self.tags_resize_height_value.sizePolicy().hasHeightForWidth())
+ self.tags_resize_height_value.setSizePolicy(sizePolicy)
+ self.tags_resize_height_value.setMaximum(9999)
+ self.tags_resize_height_value.setProperty("value", 1000)
+ self.tags_resize_height_value.setObjectName("tags_resize_height_value")
+ self.horizontalLayout_3.addWidget(self.tags_resize_height_value)
self.px_label6 = QtWidgets.QLabel(parent=self.tags_resize_height_widget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Maximum, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
@@ -151,30 +163,42 @@ class Ui_CoverProcessingOptionsPage(object):
self.verticalLayout_3.addWidget(self.tags_resize_height_widget)
self.horizontalLayout_7.addWidget(self.save_to_tags)
self.save_to_file = QtWidgets.QGroupBox(parent=self.resizing)
- self.save_to_file.setCheckable(True)
+ self.save_to_file.setCheckable(False)
self.save_to_file.setChecked(False)
self.save_to_file.setObjectName("save_to_file")
self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.save_to_file)
self.verticalLayout_4.setObjectName("verticalLayout_4")
+ self.file_scale_widget = QtWidgets.QWidget(parent=self.save_to_file)
+ self.file_scale_widget.setObjectName("file_scale_widget")
+ self.horizontalLayout_9 = QtWidgets.QHBoxLayout(self.file_scale_widget)
+ self.horizontalLayout_9.setContentsMargins(0, 0, 0, 0)
+ self.horizontalLayout_9.setObjectName("horizontalLayout_9")
+ self.file_scale_up = QtWidgets.QCheckBox(parent=self.file_scale_widget)
+ self.file_scale_up.setObjectName("file_scale_up")
+ self.horizontalLayout_9.addWidget(self.file_scale_up)
+ self.file_scale_down = QtWidgets.QCheckBox(parent=self.file_scale_widget)
+ self.file_scale_down.setObjectName("file_scale_down")
+ self.horizontalLayout_9.addWidget(self.file_scale_down)
+ self.verticalLayout_4.addWidget(self.file_scale_widget)
self.file_resize_width_widget = QtWidgets.QWidget(parent=self.save_to_file)
self.file_resize_width_widget.setObjectName("file_resize_width_widget")
self.horizontalLayout_6 = QtWidgets.QHBoxLayout(self.file_resize_width_widget)
self.horizontalLayout_6.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout_6.setSpacing(4)
self.horizontalLayout_6.setObjectName("horizontalLayout_6")
- self.file_resized_width_label = QtWidgets.QLabel(parent=self.file_resize_width_widget)
- self.file_resized_width_label.setObjectName("file_resized_width_label")
- self.horizontalLayout_6.addWidget(self.file_resized_width_label)
- self.file_resized_width_value = QtWidgets.QSpinBox(parent=self.file_resize_width_widget)
+ self.file_resize_width_label = QtWidgets.QCheckBox(parent=self.file_resize_width_widget)
+ self.file_resize_width_label.setObjectName("file_resize_width_label")
+ self.horizontalLayout_6.addWidget(self.file_resize_width_label)
+ self.file_resize_width_value = QtWidgets.QSpinBox(parent=self.file_resize_width_widget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Maximum, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.file_resized_width_value.sizePolicy().hasHeightForWidth())
- self.file_resized_width_value.setSizePolicy(sizePolicy)
- self.file_resized_width_value.setMaximum(9999)
- self.file_resized_width_value.setProperty("value", 1000)
- self.file_resized_width_value.setObjectName("file_resized_width_value")
- self.horizontalLayout_6.addWidget(self.file_resized_width_value)
+ sizePolicy.setHeightForWidth(self.file_resize_width_value.sizePolicy().hasHeightForWidth())
+ self.file_resize_width_value.setSizePolicy(sizePolicy)
+ self.file_resize_width_value.setMaximum(9999)
+ self.file_resize_width_value.setProperty("value", 1000)
+ self.file_resize_width_value.setObjectName("file_resize_width_value")
+ self.horizontalLayout_6.addWidget(self.file_resize_width_value)
self.px_label3 = QtWidgets.QLabel(parent=self.file_resize_width_widget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Maximum, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
@@ -190,19 +214,19 @@ class Ui_CoverProcessingOptionsPage(object):
self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout_4.setSpacing(4)
self.horizontalLayout_4.setObjectName("horizontalLayout_4")
- self.file_resized_height_label = QtWidgets.QLabel(parent=self.file_resize_height_widget)
- self.file_resized_height_label.setObjectName("file_resized_height_label")
- self.horizontalLayout_4.addWidget(self.file_resized_height_label)
- self.file_resized_height_value = QtWidgets.QSpinBox(parent=self.file_resize_height_widget)
+ self.file_resize_height_label = QtWidgets.QCheckBox(parent=self.file_resize_height_widget)
+ self.file_resize_height_label.setObjectName("file_resize_height_label")
+ self.horizontalLayout_4.addWidget(self.file_resize_height_label)
+ self.file_resize_height_value = QtWidgets.QSpinBox(parent=self.file_resize_height_widget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Maximum, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.file_resized_height_value.sizePolicy().hasHeightForWidth())
- self.file_resized_height_value.setSizePolicy(sizePolicy)
- self.file_resized_height_value.setMaximum(9999)
- self.file_resized_height_value.setProperty("value", 1000)
- self.file_resized_height_value.setObjectName("file_resized_height_value")
- self.horizontalLayout_4.addWidget(self.file_resized_height_value)
+ sizePolicy.setHeightForWidth(self.file_resize_height_value.sizePolicy().hasHeightForWidth())
+ self.file_resize_height_value.setSizePolicy(sizePolicy)
+ self.file_resize_height_value.setMaximum(9999)
+ self.file_resize_height_value.setProperty("value", 1000)
+ self.file_resize_height_value.setObjectName("file_resize_height_value")
+ self.horizontalLayout_4.addWidget(self.file_resize_height_value)
self.px_label4 = QtWidgets.QLabel(parent=self.file_resize_height_widget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Maximum, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
@@ -229,12 +253,16 @@ class Ui_CoverProcessingOptionsPage(object):
self.px_label2.setText(_("px"))
self.resizing.setTitle(_("Resize images if above the given size"))
self.save_to_tags.setTitle(_("Resize images saved to tags "))
- self.tags_resized_width_label.setText(_("Maximum width:"))
+ self.tags_scale_up.setText(_("Scale up"))
+ self.tags_scale_down.setText(_("Scale down"))
+ self.tags_resize_width_label.setText(_("Width:"))
self.px_label5.setText(_("px"))
- self.tags_resized_height_label.setText(_("Maximum height:"))
+ self.tags_resize_height_label.setText(_("Height:"))
self.px_label6.setText(_("px"))
self.save_to_file.setTitle(_("Resize images saved to files"))
- self.file_resized_width_label.setText(_("Maximum width:"))
+ self.file_scale_up.setText(_("Scale up"))
+ self.file_scale_down.setText(_("Scale down"))
+ self.file_resize_width_label.setText(_("Width:"))
self.px_label3.setText(_("px"))
- self.file_resized_height_label.setText(_("Maximum height:"))
+ self.file_resize_height_label.setText(_("Height:"))
self.px_label4.setText(_("px"))
diff --git a/picard/ui/options/cover_processing.py b/picard/ui/options/cover_processing.py
index c4b240a31..27d33f83f 100644
--- a/picard/ui/options/cover_processing.py
+++ b/picard/ui/options/cover_processing.py
@@ -42,36 +42,54 @@ class CoverProcessingOptionsPage(OptionsPage):
self.register_setting('filter_cover_by_size')
self.register_setting('cover_minimum_width')
self.register_setting('cover_minimum_height')
- self.register_setting('resize_images_saved_to_tags')
- self.register_setting('cover_tags_maximum_width')
- self.register_setting('cover_tags_maximum_height')
- self.register_setting('resize_images_saved_to_file')
- self.register_setting('cover_file_maximum_width')
- self.register_setting('cover_file_maximum_height')
+ self.register_setting('cover_tags_scale_up')
+ self.register_setting('cover_tags_scale_down')
+ self.register_setting('cover_tags_resize_use_width')
+ self.register_setting('cover_tags_resize_target_width')
+ self.register_setting('cover_tags_resize_use_height')
+ self.register_setting('cover_tags_resize_target_height')
+ self.register_setting('cover_file_scale_up')
+ self.register_setting('cover_file_scale_down')
+ self.register_setting('cover_file_resize_use_width')
+ self.register_setting('cover_file_resize_target_width')
+ self.register_setting('cover_file_resize_use_height')
+ self.register_setting('cover_file_resize_target_height')
def load(self):
config = get_config()
self.ui.filtering.setChecked(config.setting['filter_cover_by_size'])
self.ui.filtering_width_value.setValue(config.setting['cover_minimum_width'])
self.ui.filtering_height_value.setValue(config.setting['cover_minimum_height'])
- self.ui.save_to_tags.setChecked(config.setting['resize_images_saved_to_tags'])
- self.ui.tags_resized_width_value.setValue(config.setting['cover_tags_maximum_width'])
- self.ui.tags_resized_height_value.setValue(config.setting['cover_tags_maximum_height'])
- self.ui.save_to_file.setChecked(config.setting['resize_images_saved_to_file'])
- self.ui.file_resized_width_value.setValue(config.setting['cover_file_maximum_width'])
- self.ui.file_resized_height_value.setValue(config.setting['cover_file_maximum_height'])
+ self.ui.tags_scale_up.setChecked(config.setting['cover_tags_scale_up'])
+ self.ui.tags_scale_down.setChecked(config.setting['cover_tags_scale_down'])
+ self.ui.tags_resize_width_label.setChecked(config.setting['cover_tags_resize_use_width'])
+ self.ui.tags_resize_width_value.setValue(config.setting['cover_tags_resize_target_width'])
+ self.ui.tags_resize_height_label.setChecked(config.setting['cover_tags_resize_use_height'])
+ self.ui.tags_resize_height_value.setValue(config.setting['cover_tags_resize_target_height'])
+ self.ui.file_scale_up.setChecked(config.setting['cover_file_scale_up'])
+ self.ui.file_scale_down.setChecked(config.setting['cover_file_scale_down'])
+ self.ui.file_resize_width_label.setChecked(config.setting['cover_file_resize_use_width'])
+ self.ui.file_resize_width_value.setValue(config.setting['cover_file_resize_target_width'])
+ self.ui.file_resize_height_label.setChecked(config.setting['cover_file_resize_use_height'])
+ self.ui.file_resize_height_value.setValue(config.setting['cover_file_resize_target_height'])
def save(self):
config = get_config()
config.setting['filter_cover_by_size'] = self.ui.filtering.isChecked()
config.setting['cover_minimum_width'] = self.ui.filtering_width_value.value()
config.setting['cover_minimum_height'] = self.ui.filtering_height_value.value()
- config.setting['resize_images_saved_to_tags'] = self.ui.save_to_tags.isChecked()
- config.setting['cover_tags_maximum_width'] = self.ui.tags_resized_width_value.value()
- config.setting['cover_tags_maximum_height'] = self.ui.tags_resized_height_value.value()
- config.setting['resize_images_saved_to_file'] = self.ui.save_to_file.isChecked()
- config.setting['cover_file_maximum_width'] = self.ui.file_resized_width_value.value()
- config.setting['cover_file_maximum_height'] = self.ui.file_resized_height_value.value()
+ config.setting['cover_tags_scale_up'] = self.ui.tags_scale_up.isChecked()
+ config.setting['cover_tags_scale_down'] = self.ui.tags_scale_down.isChecked()
+ config.setting['cover_tags_resize_use_width'] = self.ui.tags_resize_width_label.isChecked()
+ config.setting['cover_tags_resize_target_width'] = self.ui.tags_resize_width_value.value()
+ config.setting['cover_tags_resize_use_height'] = self.ui.tags_resize_height_label.isChecked()
+ config.setting['cover_tags_resize_target_height'] = self.ui.tags_resize_height_value.value()
+ config.setting['cover_file_scale_up'] = self.ui.file_scale_up.isChecked()
+ config.setting['cover_file_scale_down'] = self.ui.file_scale_down.isChecked()
+ config.setting['cover_file_resize_use_width'] = self.ui.file_resize_width_label.isChecked()
+ config.setting['cover_file_resize_target_width'] = self.ui.file_resize_width_value.value()
+ config.setting['cover_file_resize_use_height'] = self.ui.file_resize_height_label.isChecked()
+ config.setting['cover_file_resize_target_height'] = self.ui.file_resize_height_value.value()
register_options_page(CoverProcessingOptionsPage)
diff --git a/test/test_coverart_processing.py b/test/test_coverart_processing.py
index 22bf90f92..051d10e36 100644
--- a/test/test_coverart_processing.py
+++ b/test/test_coverart_processing.py
@@ -80,12 +80,18 @@ class ImageProcessorsTest(PicardTestCase):
super().setUp()
self.settings = {
'enabled_plugins': [],
- 'resize_images_saved_to_tags': True,
- 'cover_tags_maximum_width': 500,
- 'cover_tags_maximum_height': 500,
- 'resize_images_saved_to_file': True,
- 'cover_file_maximum_width': 750,
- 'cover_file_maximum_height': 750,
+ 'cover_tags_scale_up': False,
+ 'cover_tags_scale_down': True,
+ 'cover_tags_resize_use_width': True,
+ 'cover_tags_resize_target_width': 500,
+ 'cover_tags_resize_use_height': True,
+ 'cover_tags_resize_target_height': 500,
+ 'cover_file_scale_up': False,
+ 'cover_file_scale_down': True,
+ 'cover_file_resize_use_width': True,
+ 'cover_file_resize_target_width': 750,
+ 'cover_file_resize_use_height': True,
+ 'cover_file_resize_target_height': 750,
'save_images_to_tags': True,
'save_images_to_files': True,
}
diff --git a/ui/options_cover_processing.ui b/ui/options_cover_processing.ui
index 75d95803a..0943b8523 100644
--- a/ui/options_cover_processing.ui
+++ b/ui/options_cover_processing.ui
@@ -6,8 +6,8 @@
0
0
- 518
- 285
+ 478
+ 361
@@ -155,12 +155,44 @@
Resize images saved to tags
- true
+ false
false
+ -
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Scale up
+
+
+
+ -
+
+
+ Scale down
+
+
+
+
+
+
-
@@ -180,14 +212,14 @@
0
-
-
+
- Maximum width:
+ Width:
-
-
+
0
@@ -237,14 +269,14 @@
0
-
-
+
- Maximum height:
+ Height:
-
-
+
0
@@ -284,12 +316,44 @@
Resize images saved to files
- true
+ false
false
+
-
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Scale up
+
+
+
+ -
+
+
+ Scale down
+
+
+
+
+
+
-
@@ -309,14 +373,14 @@
0
-
-
+
- Maximum width:
+ Width:
-
-
+
0
@@ -366,14 +430,14 @@
0
-
-
+
- Maximum height:
+ Height:
-
-
+
0
From 89f5dc1243723749c6c1143e0e5d8c2f064e3f47 Mon Sep 17 00:00:00 2001
From: twodoorcoupe
Date: Thu, 20 Jun 2024 12:13:38 +0200
Subject: [PATCH 2/7] Refactor ResizeImage and improve ui
---
picard/coverart/processing/processors.py | 74 +++++++++++++-----------
picard/ui/options/cover_processing.py | 30 ++++++++++
2 files changed, 71 insertions(+), 33 deletions(-)
diff --git a/picard/coverart/processing/processors.py b/picard/coverart/processing/processors.py
index c3b267983..b19acb897 100644
--- a/picard/coverart/processing/processors.py
+++ b/picard/coverart/processing/processors.py
@@ -43,40 +43,59 @@ class ResizeImage(ImageProcessor):
def same_processing(self):
setting = get_config().setting
- same_up = setting['cover_file_scale_up'] == setting['cover_tags_scale_up']
- same_down = setting['cover_file_scale_down'] == setting['cover_tags_scale_down']
- same_width = setting['cover_file_resize_use_width'] == setting['cover_tags_resize_use_width']
- if setting['cover_file_resize_use_width'] and setting['cover_tags_resize_use_width']:
- same_width = setting['cover_file_resize_target_width'] == setting['cover_tags_resize_target_width']
- same_height = setting['cover_file_resize_use_height'] == setting['cover_tags_resize_use_height']
- if setting['cover_file_resize_use_height'] and setting['cover_tags_resize_use_height']:
- same_height = setting['cover_file_resize_target_height'] == setting['cover_tags_resize_target_height']
- return same_up and same_down and same_width and same_height and self.save_to_file() and self.save_to_tags()
+ tags_size = (
+ setting['cover_tags_resize_target_width'] if setting['cover_tags_resize_use_width'] else 0,
+ setting['cover_tags_resize_target_height'] if setting['cover_tags_resize_use_height'] else 0
+ )
+ file_size = (
+ setting['cover_file_resize_target_width'] if setting['cover_file_resize_use_width'] else 0,
+ setting['cover_file_resize_target_height'] if setting['cover_file_resize_use_height'] else 0
+ )
+ same_size = tags_size == file_size
+ tags_direction = (setting['cover_tags_scale_up'], setting['cover_tags_scale_down'])
+ file_direction = (setting['cover_file_scale_up'], setting['cover_file_scale_down'])
+ same_direction = tags_direction == file_direction and any(tags_direction)
+ return same_size and same_direction
- def _find_target_size(self, image, target):
+ def run(self, image, target):
+ start_time = time.time()
config = get_config()
target_width = image.info.width
target_height = image.info.height
if target == ProcessingTarget.TAGS:
- if config.setting['cover_tags_resize_use_width']:
+ scale_up = config.setting['cover_tags_scale_up']
+ scale_down = config.setting['cover_tags_scale_down']
+ use_width = config.setting['cover_tags_resize_use_width']
+ if use_width:
target_width = config.setting['cover_tags_resize_target_width']
- if config.setting['cover_tags_resize_use_height']:
+ use_height = config.setting['cover_tags_resize_use_height']
+ if use_height:
target_height = config.setting['cover_tags_resize_target_height']
- scaling_up = config.setting['cover_tags_scale_up']
- scaling_down = config.setting['cover_tags_scale_down']
else:
- if config.setting['cover_file_resize_use_width']:
+ scale_up = config.setting['cover_file_scale_up']
+ scale_down = config.setting['cover_file_scale_down']
+ use_width = config.setting['cover_file_resize_use_width']
+ if use_width:
target_width = config.setting['cover_file_resize_target_width']
- if config.setting['cover_file_resize_use_height']:
+ use_height = config.setting['cover_file_resize_use_height']
+ if use_height:
target_height = config.setting['cover_file_resize_target_height']
- scaling_up = config.setting['cover_file_scale_up']
- scaling_down = config.setting['cover_file_scale_down']
- return target_width, target_height, scaling_up, scaling_down
- def _resize_image(self, image, target_width, target_height, aspect_ratio):
- start_time = time.time()
+ if scale_down and (image.info.width > target_width or image.info.height > target_height):
+ aspect_ratio = Qt.AspectRatioMode.KeepAspectRatio
+ elif scale_up and (image.info.width < target_width or image.info.height < target_height):
+ aspect_ratio = Qt.AspectRatioMode.KeepAspectRatioByExpanding
+ else:
+ # no resizing needed
+ return
qimage = image.get_result()
- scaled_image = qimage.scaled(target_width, target_height, aspect_ratio)
+ if use_width and use_height:
+ scaled_image = qimage.scaled(target_width, target_height, aspect_ratio)
+ elif use_width:
+ scaled_image = qimage.scaledToWidth(target_width)
+ else:
+ scaled_image = qimage.scaledToHeight(target_height)
+
log.debug(
"Resized cover art from %d x %d to %d x %d in %.2f ms",
image.info.width,
@@ -90,16 +109,5 @@ class ResizeImage(ImageProcessor):
image.info.datalen = scaled_image.sizeInBytes()
image.set_result(scaled_image)
- def run(self, image, target):
- target_width, target_height, scaling_up, scaling_down = self._find_target_size(image, target)
- if scaling_down and (image.info.width > target_width or image.info.height > target_height):
- aspect_ratio = Qt.AspectRatioMode.KeepAspectRatio
- elif scaling_up and (image.info.width < target_width or image.info.height < target_height):
- aspect_ratio = Qt.AspectRatioMode.KeepAspectRatioByExpanding
- else:
- # no resizing is needed
- return
- self._resize_image(image, target_width, target_height, aspect_ratio)
-
register_cover_art_processor(ResizeImage)
diff --git a/picard/ui/options/cover_processing.py b/picard/ui/options/cover_processing.py
index 27d33f83f..abd177f1c 100644
--- a/picard/ui/options/cover_processing.py
+++ b/picard/ui/options/cover_processing.py
@@ -18,6 +18,8 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+from functools import partial
+
from picard.config import get_config
from picard.extension_points.options_pages import register_options_page
from picard.i18n import N_
@@ -55,6 +57,34 @@ class CoverProcessingOptionsPage(OptionsPage):
self.register_setting('cover_file_resize_use_height')
self.register_setting('cover_file_resize_target_height')
+ tags_checkboxes = (self.ui.tags_resize_width_label, self.ui.tags_resize_height_label)
+ tags_at_least_one_checked = partial(self._ensure_at_least_one_checked, tags_checkboxes)
+ for checkbox in tags_checkboxes:
+ checkbox.clicked.connect(tags_at_least_one_checked)
+ file_checkboxes = (self.ui.file_resize_width_label, self.ui.file_resize_height_label)
+ file_at_least_one_checked = partial(self._ensure_at_least_one_checked, file_checkboxes)
+ for checkbox in file_checkboxes:
+ checkbox.clicked.connect(file_at_least_one_checked)
+
+ self._spinboxes = {
+ self.ui.tags_resize_width_label: self.ui.tags_resize_width_value,
+ self.ui.tags_resize_height_label: self.ui.tags_resize_height_value,
+ self.ui.file_resize_width_label: self.ui.file_resize_width_value,
+ self.ui.file_resize_height_label: self.ui.file_resize_height_value,
+ }
+ for checkbox, spinbox in self._spinboxes.items():
+ spinbox.setEnabled(checkbox.isChecked())
+ checkbox.clicked.connect(self._update_resize_spinboxes)
+
+ def _update_resize_spinboxes(self):
+ spinbox = self._spinboxes[self.sender()]
+ spinbox.setEnabled(self.sender().isChecked())
+
+ def _ensure_at_least_one_checked(self, checkboxes, clicked):
+ if not clicked and not any(checkbox.isChecked() for checkbox in checkboxes):
+ sender = self.sender()
+ sender.setChecked(True)
+
def load(self):
config = get_config()
self.ui.filtering.setChecked(config.setting['filter_cover_by_size'])
From 9f5184a48ae23911b4d4da83ce31c5f6ebf7732e Mon Sep 17 00:00:00 2001
From: twodoorcoupe
Date: Thu, 20 Jun 2024 20:03:52 +0200
Subject: [PATCH 3/7] Add unit tests for new image resizing options
---
picard/ui/options/cover_processing.py | 2 +-
test/test_coverart_processing.py | 173 +++++++++++++++++---------
2 files changed, 115 insertions(+), 60 deletions(-)
diff --git a/picard/ui/options/cover_processing.py b/picard/ui/options/cover_processing.py
index abd177f1c..a1e1cb381 100644
--- a/picard/ui/options/cover_processing.py
+++ b/picard/ui/options/cover_processing.py
@@ -74,7 +74,7 @@ class CoverProcessingOptionsPage(OptionsPage):
}
for checkbox, spinbox in self._spinboxes.items():
spinbox.setEnabled(checkbox.isChecked())
- checkbox.clicked.connect(self._update_resize_spinboxes)
+ checkbox.stateChanged.connect(self._update_resize_spinboxes)
def _update_resize_spinboxes(self):
spinbox = self._spinboxes[self.sender()]
diff --git a/test/test_coverart_processing.py b/test/test_coverart_processing.py
index 051d10e36..b5f86f795 100644
--- a/test/test_coverart_processing.py
+++ b/test/test_coverart_processing.py
@@ -19,13 +19,13 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from copy import copy
-import itertools
from PyQt6.QtCore import QBuffer
from PyQt6.QtGui import QImage
from test.picardtestcase import PicardTestCase
+from picard import config
from picard.coverart.image import CoverArtImage
from picard.coverart.processing import run_image_processors
from picard.coverart.processing.filters import (
@@ -80,13 +80,13 @@ class ImageProcessorsTest(PicardTestCase):
super().setUp()
self.settings = {
'enabled_plugins': [],
- 'cover_tags_scale_up': False,
+ 'cover_tags_scale_up': True,
'cover_tags_scale_down': True,
'cover_tags_resize_use_width': True,
'cover_tags_resize_target_width': 500,
'cover_tags_resize_use_height': True,
'cover_tags_resize_target_height': 500,
- 'cover_file_scale_up': False,
+ 'cover_file_scale_up': True,
'cover_file_scale_down': True,
'cover_file_resize_use_width': True,
'cover_file_resize_target_width': 750,
@@ -95,65 +95,120 @@ class ImageProcessorsTest(PicardTestCase):
'save_images_to_tags': True,
'save_images_to_files': True,
}
+
+ def _check_image_processors(self, size, expected_tags_size, expected_file_size=None):
+ coverartimage = CoverArtImage()
+ image = create_fake_image(size[0], size[1], "jpg")
+ run_image_processors(image, coverartimage)
+ tags_size = (coverartimage.width, coverartimage.height)
+ if config.setting['save_images_to_tags']:
+ self.assertEqual(tags_size, expected_tags_size)
+ else:
+ self.assertEqual(tags_size, size)
+ if config.setting['save_images_to_files']:
+ external_cover = coverartimage.external_file_coverart
+ file_size = (external_cover.width, external_cover.height)
+ self.assertEqual(file_size, expected_file_size)
+ else:
+ self.assertIsNone(coverartimage.external_file_coverart)
+ extension = coverartimage.extension[1:]
+ self.assertEqual(extension, "jpg")
+
+ def test_image_processors_save_to_both(self):
+ self.set_config_values(self.settings)
+ self._check_image_processors((1000, 1000), (500, 500), (750, 750))
+ self._check_image_processors((600, 600), (500, 500), (750, 750))
+ self._check_image_processors((400, 400), (500, 500), (750, 750))
+
+ def test_image_processors_save_to_tags(self):
+ settings = copy(self.settings)
+ settings['save_images_to_files'] = False
+ self.set_config_values(settings)
+ self._check_image_processors((1000, 1000), (500, 500))
+ self._check_image_processors((600, 600), (500, 500))
+ self._check_image_processors((400, 400), (500, 500))
self.set_config_values(self.settings)
- def test_resize(self):
- sizes = [
- (500, 500),
- (1000, 500),
- (600, 1000),
- (1000, 1000),
- (400, 400)
- ]
- expected_sizes = [
- (500, 500),
- (500, 250),
- (300, 500),
- (500, 500),
- (400, 400)
- ]
- processor = ResizeImage()
- for size, expected_size in zip(sizes, expected_sizes):
- image = ProcessingImage(create_fake_image(size[0], size[1], "jpg"))
- processor.run(image, ProcessingTarget.TAGS)
- data = image.get_result("jpg")
- new_image = QImage.fromData(data)
- new_size = (new_image.width(), new_image.height())
- self.assertEqual(new_size, expected_size)
- self.assertEqual(new_size, (image.info.width, image.info.height))
-
- def test_image_processors(self):
- sizes = [
- (1000, 1000),
- (1000, 500),
- (600, 600),
- ]
- expected_sizes = [
- ((500, 500), (750, 750)),
- ((500, 250), (750, 375)),
- ((500, 500), (600, 600)),
- ]
+ def test_image_processors_save_to_file(self):
settings = copy(self.settings)
- self.target_combinations = itertools.product([True, False], repeat=2)
- for save_to_tags, save_to_file in self.target_combinations:
- settings['save_images_to_tags'] = save_to_tags
- settings['save_images_to_files'] = save_to_file
- self.set_config_values(settings)
- for size, expected_size in zip(sizes, expected_sizes):
- coverartimage = CoverArtImage()
- image = create_fake_image(size[0], size[1], "jpg")
- run_image_processors(image, coverartimage)
- tags_size = (coverartimage.width, coverartimage.height)
- expected_size_tags = expected_size[0] if save_to_tags else size
- self.assertEqual(tags_size, expected_size_tags)
- if save_to_file:
- external_cover = coverartimage.external_file_coverart
- file_size = (external_cover.width, external_cover.height)
- self.assertEqual(file_size, expected_size[1])
- else:
- self.assertIsNone(coverartimage.external_file_coverart)
- extension = coverartimage.extension[1:]
- self.assertEqual(extension, "jpg")
+ settings['save_images_to_tags'] = False
+ self.set_config_values(settings)
+ self._check_image_processors((1000, 1000), (1000, 1000), (750, 750))
+ self._check_image_processors((600, 600), (600, 600), (750, 750))
+ self._check_image_processors((400, 400), (400, 400), (750, 750))
+ self.set_config_values(self.settings)
+
+ def test_image_processors_save_to_none(self):
+ settings = copy(self.settings)
+ settings['save_images_to_tags'] = False
+ settings['save_images_to_files'] = False
+ self.set_config_values(settings)
+ self._check_image_processors((1000, 1000), (1000, 1000), (1000, 1000))
+ self.set_config_values(self.settings)
+
+ def _check_resize_image(self, size, expected_size):
+ image = ProcessingImage(create_fake_image(size[0], size[1], "jpg"))
+ processor = ResizeImage()
+ processor.run(image, ProcessingTarget.TAGS)
+ new_size = (image.get_result().width(), image.get_result().height())
+ new_info_size = (image.info.width, image.info.height)
+ self.assertEqual(new_size, expected_size)
+ self.assertEqual(new_info_size, expected_size)
+
+ def test_scale_down_both_dimensions(self):
+ self.set_config_values(self.settings)
+ self._check_resize_image((1000, 1000), (500, 500))
+ self._check_resize_image((1000, 500), (500, 250))
+ self._check_resize_image((600, 1200), (250, 500))
+
+ def test_scale_down_only_width(self):
+ settings = copy(self.settings)
+ settings["cover_tags_resize_use_height"] = False
+ self.set_config_values(settings)
+ self._check_resize_image((1000, 1000), (500, 500))
+ self._check_resize_image((1000, 500), (500, 250))
+ self._check_resize_image((600, 1200), (500, 1000))
+ self.set_config_values(self.settings)
+
+ def test_scale_down_only_height(self):
+ settings = copy(self.settings)
+ settings["cover_tags_resize_use_width"] = False
+ self.set_config_values(settings)
+ self._check_resize_image((1000, 1000), (500, 500))
+ self._check_resize_image((1000, 500), (1000, 500))
+ self._check_resize_image((600, 1200), (250, 500))
+ self.set_config_values(self.settings)
+
+ def test_scale_up_both_dimensions(self):
+ self.set_config_values(self.settings)
+ self._check_resize_image((250, 250), (500, 500))
+ self._check_resize_image((400, 500), (500, 625))
+ self._check_resize_image((500, 250), (1000, 500))
+
+ def test_scale_up_only_width(self):
+ settings = copy(self.settings)
+ settings["cover_tags_resize_use_height"] = False
+ self.set_config_values(settings)
+ self._check_resize_image((250, 250), (500, 500))
+ self._check_resize_image((400, 500), (500, 625))
+ self._check_resize_image((500, 250), (500, 250))
+ self.set_config_values(self.settings)
+
+ def test_scale_up_only_height(self):
+ settings = copy(self.settings)
+ settings["cover_tags_resize_use_width"] = False
+ self.set_config_values(settings)
+ self._check_resize_image((250, 250), (500, 500))
+ self._check_resize_image((400, 500), (400, 500))
+ self._check_resize_image((500, 250), (1000, 500))
+ self.set_config_values(self.settings)
+
+ def test_scale_priority(self):
+ settings = copy(self.settings)
+ settings["cover_tags_resize_target_width"] = 500
+ settings["cover_tags_resize_target_height"] = 1000
+ self.set_config_values(settings)
+ self._check_resize_image((750, 750), (500, 500))
self.set_config_values(self.settings)
def test_identification_error(self):
From c6ff9caa5a14ad8e60cb60b23e085a0dcff1fa93 Mon Sep 17 00:00:00 2001
From: twodoorcoupe
Date: Fri, 21 Jun 2024 12:58:06 +0200
Subject: [PATCH 4/7] Add option to stretch or crop
---
picard/coverart/processing/processors.py | 62 ++++++++-----
picard/options.py | 4 +
.../ui/forms/ui_options_cover_processing.py | 42 ++++++++-
picard/ui/options/cover_processing.py | 12 +++
test/test_coverart_processing.py | 66 ++++++++++++-
ui/options_cover_processing.ui | 92 ++++++++++++++++++-
6 files changed, 252 insertions(+), 26 deletions(-)
diff --git a/picard/coverart/processing/processors.py b/picard/coverart/processing/processors.py
index b19acb897..221503142 100644
--- a/picard/coverart/processing/processors.py
+++ b/picard/coverart/processing/processors.py
@@ -55,46 +55,64 @@ class ResizeImage(ImageProcessor):
tags_direction = (setting['cover_tags_scale_up'], setting['cover_tags_scale_down'])
file_direction = (setting['cover_file_scale_up'], setting['cover_file_scale_down'])
same_direction = tags_direction == file_direction and any(tags_direction)
- return same_size and same_direction
+ tags_resize_mode = (setting['cover_tags_stretch'], setting['cover_tags_crop'])
+ file_resize_mode = (setting['cover_file_stretch'], setting['cover_file_crop'])
+ same_resize_mode = tags_resize_mode == file_resize_mode
+ return same_size and same_direction and same_resize_mode
def run(self, image, target):
start_time = time.time()
config = get_config()
- target_width = image.info.width
- target_height = image.info.height
if target == ProcessingTarget.TAGS:
scale_up = config.setting['cover_tags_scale_up']
scale_down = config.setting['cover_tags_scale_down']
use_width = config.setting['cover_tags_resize_use_width']
- if use_width:
- target_width = config.setting['cover_tags_resize_target_width']
+ target_width = config.setting['cover_tags_resize_target_width']
use_height = config.setting['cover_tags_resize_use_height']
- if use_height:
- target_height = config.setting['cover_tags_resize_target_height']
+ target_height = config.setting['cover_tags_resize_target_height']
+ stretch = config.setting["cover_tags_stretch"]
+ crop = config.setting["cover_tags_crop"]
else:
scale_up = config.setting['cover_file_scale_up']
scale_down = config.setting['cover_file_scale_down']
use_width = config.setting['cover_file_resize_use_width']
- if use_width:
- target_width = config.setting['cover_file_resize_target_width']
+ target_width = config.setting['cover_file_resize_target_width']
use_height = config.setting['cover_file_resize_use_height']
- if use_height:
- target_height = config.setting['cover_file_resize_target_height']
+ target_height = config.setting['cover_file_resize_target_height']
+ stretch = config.setting["cover_file_stretch"]
+ crop = config.setting["cover_file_crop"]
- if scale_down and (image.info.width > target_width or image.info.height > target_height):
- aspect_ratio = Qt.AspectRatioMode.KeepAspectRatio
- elif scale_up and (image.info.width < target_width or image.info.height < target_height):
- aspect_ratio = Qt.AspectRatioMode.KeepAspectRatioByExpanding
- else:
+ width_scale_factor = 1
+ width_resize = image.info.width
+ if use_width:
+ width_scale_factor = target_width / image.info.width
+ width_resize = target_width
+ height_scale_factor = 1
+ height_resize = image.info.height
+ if use_height:
+ height_scale_factor = target_height / image.info.height
+ height_resize = target_height
+ if (width_scale_factor == 1 and height_scale_factor == 1
+ or ((width_scale_factor > 1 and height_scale_factor > 1) and not scale_up)
+ or ((width_scale_factor < 1 or height_scale_factor < 1) and not scale_down)):
# no resizing needed
return
+
qimage = image.get_result()
- if use_width and use_height:
- scaled_image = qimage.scaled(target_width, target_height, aspect_ratio)
- elif use_width:
- scaled_image = qimage.scaledToWidth(target_width)
- else:
- scaled_image = qimage.scaledToHeight(target_height)
+ if stretch:
+ scaled_image = qimage.scaled(width_resize, height_resize, Qt.AspectRatioMode.IgnoreAspectRatio)
+ elif crop:
+ scaled_image = qimage.scaled(width_resize, height_resize, Qt.AspectRatioMode.KeepAspectRatioByExpanding)
+ cutoff_width = (scaled_image.width() - width_resize) // 2
+ cutoff_height = (scaled_image.height() - height_resize) // 2
+ scaled_image = scaled_image.copy(cutoff_width, cutoff_height, width_resize, height_resize)
+ else: # keep aspect ratio
+ if use_width and use_height:
+ scaled_image = qimage.scaled(width_resize, height_resize, Qt.AspectRatioMode.KeepAspectRatio)
+ elif use_width:
+ scaled_image = qimage.scaledToWidth(width_resize)
+ else:
+ scaled_image = qimage.scaledToHeight(height_resize)
log.debug(
"Resized cover art from %d x %d to %d x %d in %.2f ms",
diff --git a/picard/options.py b/picard/options.py
index ebe11edf5..79d154870 100644
--- a/picard/options.py
+++ b/picard/options.py
@@ -181,12 +181,16 @@ BoolOption('setting', 'cover_tags_resize_use_width', True)
IntOption('setting', 'cover_tags_resize_target_width', DEFAULT_COVER_MAX_SIZE)
BoolOption('setting', 'cover_tags_resize_use_height', True)
IntOption('setting', 'cover_tags_resize_target_height', DEFAULT_COVER_MAX_SIZE)
+BoolOption('setting', 'cover_tags_stretch', False)
+BoolOption('setting', 'cover_tags_crop', False)
BoolOption('setting', 'cover_file_scale_up', False)
BoolOption('setting', 'cover_file_scale_down', False)
BoolOption('setting', 'cover_file_resize_use_width', True)
IntOption('setting', 'cover_file_resize_target_width', DEFAULT_COVER_MAX_SIZE)
BoolOption('setting', 'cover_file_resize_use_height', True)
IntOption('setting', 'cover_file_resize_target_height', DEFAULT_COVER_MAX_SIZE)
+BoolOption('setting', 'cover_file_stretch', False)
+BoolOption('setting', 'cover_file_crop', False)
# picard/ui/options/dialog.py
# Attached Profiles
diff --git a/picard/ui/forms/ui_options_cover_processing.py b/picard/ui/forms/ui_options_cover_processing.py
index 6007c8af6..45547575b 100644
--- a/picard/ui/forms/ui_options_cover_processing.py
+++ b/picard/ui/forms/ui_options_cover_processing.py
@@ -17,7 +17,7 @@ from picard.i18n import gettext as _
class Ui_CoverProcessingOptionsPage(object):
def setupUi(self, CoverProcessingOptionsPage):
CoverProcessingOptionsPage.setObjectName("CoverProcessingOptionsPage")
- CoverProcessingOptionsPage.resize(478, 361)
+ CoverProcessingOptionsPage.resize(478, 423)
self.verticalLayout = QtWidgets.QVBoxLayout(CoverProcessingOptionsPage)
self.verticalLayout.setObjectName("verticalLayout")
self.filtering = QtWidgets.QGroupBox(parent=CoverProcessingOptionsPage)
@@ -161,6 +161,23 @@ class Ui_CoverProcessingOptionsPage(object):
self.px_label6.setObjectName("px_label6")
self.horizontalLayout_3.addWidget(self.px_label6)
self.verticalLayout_3.addWidget(self.tags_resize_height_widget)
+ self.tags_resize_mode = QtWidgets.QWidget(parent=self.save_to_tags)
+ self.tags_resize_mode.setObjectName("tags_resize_mode")
+ self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.tags_resize_mode)
+ self.verticalLayout_5.setContentsMargins(0, 0, 0, 0)
+ self.verticalLayout_5.setSpacing(2)
+ self.verticalLayout_5.setObjectName("verticalLayout_5")
+ self.tags_keep = QtWidgets.QRadioButton(parent=self.tags_resize_mode)
+ self.tags_keep.setChecked(True)
+ self.tags_keep.setObjectName("tags_keep")
+ self.verticalLayout_5.addWidget(self.tags_keep)
+ self.tags_crop = QtWidgets.QRadioButton(parent=self.tags_resize_mode)
+ self.tags_crop.setObjectName("tags_crop")
+ self.verticalLayout_5.addWidget(self.tags_crop)
+ self.tags_stretch = QtWidgets.QRadioButton(parent=self.tags_resize_mode)
+ self.tags_stretch.setObjectName("tags_stretch")
+ self.verticalLayout_5.addWidget(self.tags_stretch)
+ self.verticalLayout_3.addWidget(self.tags_resize_mode)
self.horizontalLayout_7.addWidget(self.save_to_tags)
self.save_to_file = QtWidgets.QGroupBox(parent=self.resizing)
self.save_to_file.setCheckable(False)
@@ -236,6 +253,23 @@ class Ui_CoverProcessingOptionsPage(object):
self.px_label4.setObjectName("px_label4")
self.horizontalLayout_4.addWidget(self.px_label4)
self.verticalLayout_4.addWidget(self.file_resize_height_widget)
+ self.file_resize_mode = QtWidgets.QWidget(parent=self.save_to_file)
+ self.file_resize_mode.setObjectName("file_resize_mode")
+ self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.file_resize_mode)
+ self.verticalLayout_6.setContentsMargins(0, 0, 0, 0)
+ self.verticalLayout_6.setSpacing(2)
+ self.verticalLayout_6.setObjectName("verticalLayout_6")
+ self.file_keep = QtWidgets.QRadioButton(parent=self.file_resize_mode)
+ self.file_keep.setChecked(True)
+ self.file_keep.setObjectName("file_keep")
+ self.verticalLayout_6.addWidget(self.file_keep)
+ self.file_crop = QtWidgets.QRadioButton(parent=self.file_resize_mode)
+ self.file_crop.setObjectName("file_crop")
+ self.verticalLayout_6.addWidget(self.file_crop)
+ self.file_stretch = QtWidgets.QRadioButton(parent=self.file_resize_mode)
+ self.file_stretch.setObjectName("file_stretch")
+ self.verticalLayout_6.addWidget(self.file_stretch)
+ self.verticalLayout_4.addWidget(self.file_resize_mode)
self.horizontalLayout_7.addWidget(self.save_to_file)
self.verticalLayout.addWidget(self.resizing)
spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding)
@@ -259,6 +293,9 @@ class Ui_CoverProcessingOptionsPage(object):
self.px_label5.setText(_("px"))
self.tags_resize_height_label.setText(_("Height:"))
self.px_label6.setText(_("px"))
+ self.tags_keep.setText(_("Keep aspect ratio"))
+ self.tags_crop.setText(_("Crop scaled overflow"))
+ self.tags_stretch.setText(_("Stretch/shrink to target"))
self.save_to_file.setTitle(_("Resize images saved to files"))
self.file_scale_up.setText(_("Scale up"))
self.file_scale_down.setText(_("Scale down"))
@@ -266,3 +303,6 @@ class Ui_CoverProcessingOptionsPage(object):
self.px_label3.setText(_("px"))
self.file_resize_height_label.setText(_("Height:"))
self.px_label4.setText(_("px"))
+ self.file_keep.setText(_("Keep aspect ratio"))
+ self.file_crop.setText(_("Crop scaled overflow"))
+ self.file_stretch.setText(_("Stretch/shrink to target"))
diff --git a/picard/ui/options/cover_processing.py b/picard/ui/options/cover_processing.py
index a1e1cb381..7c2352dd2 100644
--- a/picard/ui/options/cover_processing.py
+++ b/picard/ui/options/cover_processing.py
@@ -50,12 +50,16 @@ class CoverProcessingOptionsPage(OptionsPage):
self.register_setting('cover_tags_resize_target_width')
self.register_setting('cover_tags_resize_use_height')
self.register_setting('cover_tags_resize_target_height')
+ self.register_setting('cover_tags_stretch')
+ self.register_setting('cover_tags_crop')
self.register_setting('cover_file_scale_up')
self.register_setting('cover_file_scale_down')
self.register_setting('cover_file_resize_use_width')
self.register_setting('cover_file_resize_target_width')
self.register_setting('cover_file_resize_use_height')
self.register_setting('cover_file_resize_target_height')
+ self.register_setting('cover_file_stretch')
+ self.register_setting('cover_file_crop')
tags_checkboxes = (self.ui.tags_resize_width_label, self.ui.tags_resize_height_label)
tags_at_least_one_checked = partial(self._ensure_at_least_one_checked, tags_checkboxes)
@@ -96,12 +100,16 @@ class CoverProcessingOptionsPage(OptionsPage):
self.ui.tags_resize_width_value.setValue(config.setting['cover_tags_resize_target_width'])
self.ui.tags_resize_height_label.setChecked(config.setting['cover_tags_resize_use_height'])
self.ui.tags_resize_height_value.setValue(config.setting['cover_tags_resize_target_height'])
+ self.ui.tags_stretch.setChecked(config.setting['cover_tags_stretch'])
+ self.ui.tags_crop.setChecked(config.setting['cover_tags_crop'])
self.ui.file_scale_up.setChecked(config.setting['cover_file_scale_up'])
self.ui.file_scale_down.setChecked(config.setting['cover_file_scale_down'])
self.ui.file_resize_width_label.setChecked(config.setting['cover_file_resize_use_width'])
self.ui.file_resize_width_value.setValue(config.setting['cover_file_resize_target_width'])
self.ui.file_resize_height_label.setChecked(config.setting['cover_file_resize_use_height'])
self.ui.file_resize_height_value.setValue(config.setting['cover_file_resize_target_height'])
+ self.ui.file_stretch.setChecked(config.setting['cover_file_stretch'])
+ self.ui.file_crop.setChecked(config.setting['cover_file_crop'])
def save(self):
config = get_config()
@@ -114,12 +122,16 @@ class CoverProcessingOptionsPage(OptionsPage):
config.setting['cover_tags_resize_target_width'] = self.ui.tags_resize_width_value.value()
config.setting['cover_tags_resize_use_height'] = self.ui.tags_resize_height_label.isChecked()
config.setting['cover_tags_resize_target_height'] = self.ui.tags_resize_height_value.value()
+ config.setting['cover_tags_stretch'] = self.ui.tags_stretch.isChecked()
+ config.setting['cover_tags_crop'] = self.ui.tags_crop.isChecked()
config.setting['cover_file_scale_up'] = self.ui.file_scale_up.isChecked()
config.setting['cover_file_scale_down'] = self.ui.file_scale_down.isChecked()
config.setting['cover_file_resize_use_width'] = self.ui.file_resize_width_label.isChecked()
config.setting['cover_file_resize_target_width'] = self.ui.file_resize_width_value.value()
config.setting['cover_file_resize_use_height'] = self.ui.file_resize_height_label.isChecked()
config.setting['cover_file_resize_target_height'] = self.ui.file_resize_height_value.value()
+ config.setting['cover_file_stretch'] = self.ui.file_stretch.isChecked()
+ config.setting['cover_file_crop'] = self.ui.file_crop.isChecked()
register_options_page(CoverProcessingOptionsPage)
diff --git a/test/test_coverart_processing.py b/test/test_coverart_processing.py
index b5f86f795..bba96c5f7 100644
--- a/test/test_coverart_processing.py
+++ b/test/test_coverart_processing.py
@@ -86,12 +86,16 @@ class ImageProcessorsTest(PicardTestCase):
'cover_tags_resize_target_width': 500,
'cover_tags_resize_use_height': True,
'cover_tags_resize_target_height': 500,
+ 'cover_tags_stretch': False,
+ 'cover_tags_crop': False,
'cover_file_scale_up': True,
'cover_file_scale_down': True,
'cover_file_resize_use_width': True,
'cover_file_resize_target_width': 750,
'cover_file_resize_use_height': True,
'cover_file_resize_target_height': 750,
+ 'cover_file_stretch': False,
+ 'cover_file_crop': False,
'save_images_to_tags': True,
'save_images_to_files': True,
}
@@ -182,8 +186,8 @@ class ImageProcessorsTest(PicardTestCase):
def test_scale_up_both_dimensions(self):
self.set_config_values(self.settings)
self._check_resize_image((250, 250), (500, 500))
- self._check_resize_image((400, 500), (500, 625))
- self._check_resize_image((500, 250), (1000, 500))
+ self._check_resize_image((400, 500), (400, 500))
+ self._check_resize_image((250, 150), (500, 300))
def test_scale_up_only_width(self):
settings = copy(self.settings)
@@ -211,6 +215,64 @@ class ImageProcessorsTest(PicardTestCase):
self._check_resize_image((750, 750), (500, 500))
self.set_config_values(self.settings)
+ def test_stretch_both_dimensions(self):
+ settings = copy(self.settings)
+ settings["cover_tags_stretch"] = True
+ self.set_config_values(settings)
+ self._check_resize_image((1000, 100), (500, 500))
+ self._check_resize_image((200, 500), (500, 500))
+ self._check_resize_image((200, 2000), (500, 500))
+ self.set_config_values(self.settings)
+
+ def test_stretch_only_width(self):
+ settings = copy(self.settings)
+ settings["cover_tags_stretch"] = True
+ settings["cover_tags_resize_use_height"] = False
+ self.set_config_values(settings)
+ self._check_resize_image((1000, 100), (500, 100))
+ self._check_resize_image((200, 500), (500, 500))
+ self._check_resize_image((200, 2000), (500, 2000))
+ self.set_config_values(self.settings)
+
+ def test_stretch_only_height(self):
+ settings = copy(self.settings)
+ settings["cover_tags_stretch"] = True
+ settings["cover_tags_resize_use_width"] = False
+ self.set_config_values(settings)
+ self._check_resize_image((1000, 100), (1000, 500))
+ self._check_resize_image((200, 500), (200, 500))
+ self._check_resize_image((200, 2000), (200, 500))
+ self.set_config_values(self.settings)
+
+ def test_crop_both_dimensions(self):
+ settings = copy(self.settings)
+ settings["cover_tags_crop"] = True
+ self.set_config_values(settings)
+ self._check_resize_image((1000, 100), (500, 500))
+ self._check_resize_image((750, 1000), (500, 500))
+ self._check_resize_image((250, 1000), (500, 500))
+ self.set_config_values(self.settings)
+
+ def test_crop_only_width(self):
+ settings = copy(self.settings)
+ settings["cover_tags_crop"] = True
+ settings["cover_tags_resize_use_height"] = False
+ self.set_config_values(settings)
+ self._check_resize_image((1000, 100), (500, 100))
+ self._check_resize_image((750, 1000), (500, 1000))
+ self._check_resize_image((250, 1000), (500, 1000))
+ self.set_config_values(self.settings)
+
+ def test_crop_only_height(self):
+ settings = copy(self.settings)
+ settings["cover_tags_crop"] = True
+ settings["cover_tags_resize_use_width"] = False
+ self.set_config_values(settings)
+ self._check_resize_image((1000, 100), (1000, 500))
+ self._check_resize_image((750, 1000), (750, 500))
+ self._check_resize_image((250, 1000), (250, 500))
+ self.set_config_values(self.settings)
+
def test_identification_error(self):
image = create_fake_image(0, 0, "jpg")
coverartimage = CoverArtImage()
diff --git a/ui/options_cover_processing.ui b/ui/options_cover_processing.ui
index 0943b8523..26f3f6db9 100644
--- a/ui/options_cover_processing.ui
+++ b/ui/options_cover_processing.ui
@@ -7,7 +7,7 @@
0
0
478
- 361
+ 423
@@ -307,6 +307,51 @@
+ -
+
+
+
+ 2
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Keep aspect ratio
+
+
+ true
+
+
+
+ -
+
+
+ Crop scaled overflow
+
+
+
+ -
+
+
+ Stretch/shrink to target
+
+
+
+
+
+
@@ -468,6 +513,51 @@
+ -
+
+
+
+ 2
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Keep aspect ratio
+
+
+ true
+
+
+
+ -
+
+
+ Crop scaled overflow
+
+
+
+ -
+
+
+ Stretch/shrink to target
+
+
+
+
+
+
From ccbd59324dd60c7bf388a4d25e6317535d7572f0 Mon Sep 17 00:00:00 2001
From: twodoorcoupe
Date: Fri, 21 Jun 2024 16:45:01 +0200
Subject: [PATCH 5/7] Add tooltips to new radio buttons
---
picard/coverart/processing/processors.py | 2 +-
picard/ui/forms/ui_options_cover_processing.py | 6 ++++++
ui/options_cover_processing.ui | 18 ++++++++++++++++++
3 files changed, 25 insertions(+), 1 deletion(-)
diff --git a/picard/coverart/processing/processors.py b/picard/coverart/processing/processors.py
index 221503142..13b69cb1c 100644
--- a/picard/coverart/processing/processors.py
+++ b/picard/coverart/processing/processors.py
@@ -93,7 +93,7 @@ class ResizeImage(ImageProcessor):
height_scale_factor = target_height / image.info.height
height_resize = target_height
if (width_scale_factor == 1 and height_scale_factor == 1
- or ((width_scale_factor > 1 and height_scale_factor > 1) and not scale_up)
+ or ((width_scale_factor > 1 or height_scale_factor > 1) and not scale_up)
or ((width_scale_factor < 1 or height_scale_factor < 1) and not scale_down)):
# no resizing needed
return
diff --git a/picard/ui/forms/ui_options_cover_processing.py b/picard/ui/forms/ui_options_cover_processing.py
index 45547575b..3fb88fc0f 100644
--- a/picard/ui/forms/ui_options_cover_processing.py
+++ b/picard/ui/forms/ui_options_cover_processing.py
@@ -293,8 +293,11 @@ class Ui_CoverProcessingOptionsPage(object):
self.px_label5.setText(_("px"))
self.tags_resize_height_label.setText(_("Height:"))
self.px_label6.setText(_("px"))
+ self.tags_keep.setToolTip(_("Keep the original aspect ratio of the image.
For example, a 2000x1000 image resized to 1000x1000 would adjust to 1000x500.
"))
self.tags_keep.setText(_("Keep aspect ratio"))
+ self.tags_crop.setToolTip(_("
Resize the image while keeping aspect ratio, then center crop any excess.
For example, a 500x1000 image resized to 1000x1000 would first scale up to 1000x2000, then the excess height would be cropped to make it 1000x1000.
"))
self.tags_crop.setText(_("Crop scaled overflow"))
+ self.tags_stretch.setToolTip(_("Stretch the image to exactly fit the specified dimensions, modifying the aspect ratio if necessary.
For example, a 2000x1000 image resized to 1000x1000 would shrink to fit, altering the original proportions.
"))
self.tags_stretch.setText(_("Stretch/shrink to target"))
self.save_to_file.setTitle(_("Resize images saved to files"))
self.file_scale_up.setText(_("Scale up"))
@@ -303,6 +306,9 @@ class Ui_CoverProcessingOptionsPage(object):
self.px_label3.setText(_("px"))
self.file_resize_height_label.setText(_("Height:"))
self.px_label4.setText(_("px"))
+ self.file_keep.setToolTip(_("Keep the original aspect ratio of the image.
For example, a 2000x1000 image resized to 1000x1000 would adjust to 1000x500.
"))
self.file_keep.setText(_("Keep aspect ratio"))
+ self.file_crop.setToolTip(_("Resize the image while keeping aspect ratio, then center crop any excess.
For example, a 500x1000 image resized to 1000x1000 would first scale up to 1000x2000, then the excess height would be cropped to make it 1000x1000.
"))
self.file_crop.setText(_("Crop scaled overflow"))
+ self.file_stretch.setToolTip(_("Stretch the image to exactly fit the specified dimensions, modifying the aspect ratio if necessary.
For example, a 2000x1000 image resized to 1000x1000 would shrink to fit, altering the original proportions.
"))
self.file_stretch.setText(_("Stretch/shrink to target"))
diff --git a/ui/options_cover_processing.ui b/ui/options_cover_processing.ui
index 26f3f6db9..9e8c4f4b5 100644
--- a/ui/options_cover_processing.ui
+++ b/ui/options_cover_processing.ui
@@ -327,6 +327,9 @@
-
+
+ <html><head/><body><p>Keep the original aspect ratio of the image.</p><p>For example, a 2000x1000 image resized to 1000x1000 would adjust to 1000x500.</p></body></html>
+
Keep aspect ratio
@@ -337,6 +340,9 @@
-
+
+ <html><head/><body><p>Resize the image while keeping aspect ratio, then center crop any excess. </p><p>For example, a 500x1000 image resized to 1000x1000 would first scale up to 1000x2000, then the excess height would be cropped to make it 1000x1000.</p></body></html>
+
Crop scaled overflow
@@ -344,6 +350,9 @@
-
+
+ <html><head/><body><p>Stretch the image to exactly fit the specified dimensions, modifying the aspect ratio if necessary.</p><p>For example, a 2000x1000 image resized to 1000x1000 would shrink to fit, altering the original proportions.</p></body></html>
+
Stretch/shrink to target
@@ -533,6 +542,9 @@
-
+
+ <html><head/><body><p>Keep the original aspect ratio of the image.</p><p>For example, a 2000x1000 image resized to 1000x1000 would adjust to 1000x500.</p></body></html>
+
Keep aspect ratio
@@ -543,6 +555,9 @@
-
+
+ <html><head/><body><p>Resize the image while keeping aspect ratio, then center crop any excess. </p><p>For example, a 500x1000 image resized to 1000x1000 would first scale up to 1000x2000, then the excess height would be cropped to make it 1000x1000.</p></body></html>
+
Crop scaled overflow
@@ -550,6 +565,9 @@
-
+
+ <html><head/><body><p>Stretch the image to exactly fit the specified dimensions, modifying the aspect ratio if necessary.</p><p>For example, a 2000x1000 image resized to 1000x1000 would shrink to fit, altering the original proportions.</p></body></html>
+
Stretch/shrink to target
From 762afe38d209f2c6630e9ff3b634789b58c6fd2b Mon Sep 17 00:00:00 2001
From: twodoorcoupe
Date: Sat, 22 Jun 2024 09:51:02 +0200
Subject: [PATCH 6/7] Fix scale factor check and improve tooltips for resize
modes
---
picard/coverart/processing/processors.py | 24 +++++-----
.../ui/forms/ui_options_cover_processing.py | 46 ++++++++-----------
picard/ui/options/cover_processing.py | 38 ++++++++++++++-
ui/options_cover_processing.ui | 34 ++++----------
4 files changed, 78 insertions(+), 64 deletions(-)
diff --git a/picard/coverart/processing/processors.py b/picard/coverart/processing/processors.py
index 13b69cb1c..6068eebb1 100644
--- a/picard/coverart/processing/processors.py
+++ b/picard/coverart/processing/processors.py
@@ -82,16 +82,18 @@ class ResizeImage(ImageProcessor):
stretch = config.setting["cover_file_stretch"]
crop = config.setting["cover_file_crop"]
- width_scale_factor = 1
- width_resize = image.info.width
- if use_width:
- width_scale_factor = target_width / image.info.width
- width_resize = target_width
- height_scale_factor = 1
- height_resize = image.info.height
- if use_height:
- height_scale_factor = target_height / image.info.height
- height_resize = target_height
+ width_resize = target_width if use_width else image.info.width
+ height_resize = target_height if use_height else image.info.height
+ width_scale_factor = width_resize / image.info.width
+ height_scale_factor = height_resize / image.info.height
+ use_both_dimensions = use_height and use_width
+ if use_both_dimensions and not stretch:
+ if crop:
+ scale_factor = max(width_scale_factor, height_scale_factor)
+ else:
+ scale_factor = min(width_scale_factor, height_scale_factor)
+ width_scale_factor = scale_factor
+ height_scale_factor = scale_factor
if (width_scale_factor == 1 and height_scale_factor == 1
or ((width_scale_factor > 1 or height_scale_factor > 1) and not scale_up)
or ((width_scale_factor < 1 or height_scale_factor < 1) and not scale_down)):
@@ -107,7 +109,7 @@ class ResizeImage(ImageProcessor):
cutoff_height = (scaled_image.height() - height_resize) // 2
scaled_image = scaled_image.copy(cutoff_width, cutoff_height, width_resize, height_resize)
else: # keep aspect ratio
- if use_width and use_height:
+ if use_both_dimensions:
scaled_image = qimage.scaled(width_resize, height_resize, Qt.AspectRatioMode.KeepAspectRatio)
elif use_width:
scaled_image = qimage.scaledToWidth(width_resize)
diff --git a/picard/ui/forms/ui_options_cover_processing.py b/picard/ui/forms/ui_options_cover_processing.py
index 3fb88fc0f..54a023fc1 100644
--- a/picard/ui/forms/ui_options_cover_processing.py
+++ b/picard/ui/forms/ui_options_cover_processing.py
@@ -163,20 +163,20 @@ class Ui_CoverProcessingOptionsPage(object):
self.verticalLayout_3.addWidget(self.tags_resize_height_widget)
self.tags_resize_mode = QtWidgets.QWidget(parent=self.save_to_tags)
self.tags_resize_mode.setObjectName("tags_resize_mode")
- self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.tags_resize_mode)
- self.verticalLayout_5.setContentsMargins(0, 0, 0, 0)
- self.verticalLayout_5.setSpacing(2)
- self.verticalLayout_5.setObjectName("verticalLayout_5")
+ self.horizontalLayout_10 = QtWidgets.QHBoxLayout(self.tags_resize_mode)
+ self.horizontalLayout_10.setContentsMargins(0, 0, 0, 0)
+ self.horizontalLayout_10.setSpacing(2)
+ self.horizontalLayout_10.setObjectName("horizontalLayout_10")
self.tags_keep = QtWidgets.QRadioButton(parent=self.tags_resize_mode)
self.tags_keep.setChecked(True)
self.tags_keep.setObjectName("tags_keep")
- self.verticalLayout_5.addWidget(self.tags_keep)
+ self.horizontalLayout_10.addWidget(self.tags_keep)
self.tags_crop = QtWidgets.QRadioButton(parent=self.tags_resize_mode)
self.tags_crop.setObjectName("tags_crop")
- self.verticalLayout_5.addWidget(self.tags_crop)
+ self.horizontalLayout_10.addWidget(self.tags_crop)
self.tags_stretch = QtWidgets.QRadioButton(parent=self.tags_resize_mode)
self.tags_stretch.setObjectName("tags_stretch")
- self.verticalLayout_5.addWidget(self.tags_stretch)
+ self.horizontalLayout_10.addWidget(self.tags_stretch)
self.verticalLayout_3.addWidget(self.tags_resize_mode)
self.horizontalLayout_7.addWidget(self.save_to_tags)
self.save_to_file = QtWidgets.QGroupBox(parent=self.resizing)
@@ -255,20 +255,20 @@ class Ui_CoverProcessingOptionsPage(object):
self.verticalLayout_4.addWidget(self.file_resize_height_widget)
self.file_resize_mode = QtWidgets.QWidget(parent=self.save_to_file)
self.file_resize_mode.setObjectName("file_resize_mode")
- self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.file_resize_mode)
- self.verticalLayout_6.setContentsMargins(0, 0, 0, 0)
- self.verticalLayout_6.setSpacing(2)
- self.verticalLayout_6.setObjectName("verticalLayout_6")
+ self.horizontalLayout_11 = QtWidgets.QHBoxLayout(self.file_resize_mode)
+ self.horizontalLayout_11.setContentsMargins(0, 0, 0, 0)
+ self.horizontalLayout_11.setSpacing(2)
+ self.horizontalLayout_11.setObjectName("horizontalLayout_11")
self.file_keep = QtWidgets.QRadioButton(parent=self.file_resize_mode)
self.file_keep.setChecked(True)
self.file_keep.setObjectName("file_keep")
- self.verticalLayout_6.addWidget(self.file_keep)
+ self.horizontalLayout_11.addWidget(self.file_keep)
self.file_crop = QtWidgets.QRadioButton(parent=self.file_resize_mode)
self.file_crop.setObjectName("file_crop")
- self.verticalLayout_6.addWidget(self.file_crop)
+ self.horizontalLayout_11.addWidget(self.file_crop)
self.file_stretch = QtWidgets.QRadioButton(parent=self.file_resize_mode)
self.file_stretch.setObjectName("file_stretch")
- self.verticalLayout_6.addWidget(self.file_stretch)
+ self.horizontalLayout_11.addWidget(self.file_stretch)
self.verticalLayout_4.addWidget(self.file_resize_mode)
self.horizontalLayout_7.addWidget(self.save_to_file)
self.verticalLayout.addWidget(self.resizing)
@@ -293,12 +293,9 @@ class Ui_CoverProcessingOptionsPage(object):
self.px_label5.setText(_("px"))
self.tags_resize_height_label.setText(_("Height:"))
self.px_label6.setText(_("px"))
- self.tags_keep.setToolTip(_("
Keep the original aspect ratio of the image.
For example, a 2000x1000 image resized to 1000x1000 would adjust to 1000x500.
"))
- self.tags_keep.setText(_("Keep aspect ratio"))
- self.tags_crop.setToolTip(_("Resize the image while keeping aspect ratio, then center crop any excess.
For example, a 500x1000 image resized to 1000x1000 would first scale up to 1000x2000, then the excess height would be cropped to make it 1000x1000.
"))
- self.tags_crop.setText(_("Crop scaled overflow"))
- self.tags_stretch.setToolTip(_(" Stretch the image to exactly fit the specified dimensions, modifying the aspect ratio if necessary.
For example, a 2000x1000 image resized to 1000x1000 would shrink to fit, altering the original proportions.
"))
- self.tags_stretch.setText(_("Stretch/shrink to target"))
+ self.tags_keep.setText(_("Fit"))
+ self.tags_crop.setText(_("Fill"))
+ self.tags_stretch.setText(_("Stretch"))
self.save_to_file.setTitle(_("Resize images saved to files"))
self.file_scale_up.setText(_("Scale up"))
self.file_scale_down.setText(_("Scale down"))
@@ -306,9 +303,6 @@ class Ui_CoverProcessingOptionsPage(object):
self.px_label3.setText(_("px"))
self.file_resize_height_label.setText(_("Height:"))
self.px_label4.setText(_("px"))
- self.file_keep.setToolTip(_("Keep the original aspect ratio of the image.
For example, a 2000x1000 image resized to 1000x1000 would adjust to 1000x500.
"))
- self.file_keep.setText(_("Keep aspect ratio"))
- self.file_crop.setToolTip(_(" Resize the image while keeping aspect ratio, then center crop any excess.
For example, a 500x1000 image resized to 1000x1000 would first scale up to 1000x2000, then the excess height would be cropped to make it 1000x1000.
"))
- self.file_crop.setText(_("Crop scaled overflow"))
- self.file_stretch.setToolTip(_("Stretch the image to exactly fit the specified dimensions, modifying the aspect ratio if necessary.
For example, a 2000x1000 image resized to 1000x1000 would shrink to fit, altering the original proportions.
"))
- self.file_stretch.setText(_("Stretch/shrink to target"))
+ self.file_keep.setText(_("Fit"))
+ self.file_crop.setText(_("Fill"))
+ self.file_stretch.setText(_("Stretch"))
diff --git a/picard/ui/options/cover_processing.py b/picard/ui/options/cover_processing.py
index 7c2352dd2..91f53ea8c 100644
--- a/picard/ui/options/cover_processing.py
+++ b/picard/ui/options/cover_processing.py
@@ -22,7 +22,10 @@ from functools import partial
from picard.config import get_config
from picard.extension_points.options_pages import register_options_page
-from picard.i18n import N_
+from picard.i18n import (
+ N_,
+ _,
+)
from picard.ui.forms.ui_options_cover_processing import (
Ui_CoverProcessingOptionsPage,
@@ -61,6 +64,36 @@ class CoverProcessingOptionsPage(OptionsPage):
self.register_setting('cover_file_stretch')
self.register_setting('cover_file_crop')
+ tooltip_keep = N_(
+ "Scale the source image so that it fits within the target dimensions. One "
+ "of the final image dimensions may be less than the target dimension if "
+ "the source image and target dimensions have different aspect ratios. "
+ "For example, a 2000x1000 image resized to target dimensions of "
+ "1000x1000 would result in a final image size of 1000x500."
+ )
+ self.ui.tags_keep.setToolTip(self._add_rich_text(_(tooltip_keep)))
+ self.ui.file_keep.setToolTip(self._add_rich_text(_(tooltip_keep)))
+ tooltip_crop = N_(
+ "Scale the source image so that it completely fills the target dimensions "
+ "in both directions. If the source image and target dimensions have "
+ "different aspect ratios, then there will be overflow in one direction which "
+ "will be (center) cropped. "
+ "For example, a 500x1000 image resized to target dimensions of "
+ "1000x1000 would first scale up to 1000x2000, then the excess height "
+ "would be center cropped resulting in the final image size of 1000x1000."
+ )
+ self.ui.tags_crop.setToolTip(self._add_rich_text(_(tooltip_crop)))
+ self.ui.file_crop.setToolTip(self._add_rich_text(_(tooltip_crop)))
+ tooltip_stretch = N_(
+ "Stretch the image to exactly fit the specified dimensions, "
+ "distorting it if necessary. "
+ "For example, a 500x1000 image with target dimension of 1000x1000 "
+ "would be stretched horizontally resulting in the final image "
+ "size of 1000x1000."
+ )
+ self.ui.tags_stretch.setToolTip(self._add_rich_text(_(tooltip_stretch)))
+ self.ui.file_stretch.setToolTip(self._add_rich_text(_(tooltip_stretch)))
+
tags_checkboxes = (self.ui.tags_resize_width_label, self.ui.tags_resize_height_label)
tags_at_least_one_checked = partial(self._ensure_at_least_one_checked, tags_checkboxes)
for checkbox in tags_checkboxes:
@@ -80,6 +113,9 @@ class CoverProcessingOptionsPage(OptionsPage):
spinbox.setEnabled(checkbox.isChecked())
checkbox.stateChanged.connect(self._update_resize_spinboxes)
+ def _add_rich_text(self, text):
+ return "" + text + ""
+
def _update_resize_spinboxes(self):
spinbox = self._spinboxes[self.sender()]
spinbox.setEnabled(self.sender().isChecked())
diff --git a/ui/options_cover_processing.ui b/ui/options_cover_processing.ui
index 9e8c4f4b5..a706a61a8 100644
--- a/ui/options_cover_processing.ui
+++ b/ui/options_cover_processing.ui
@@ -309,7 +309,7 @@
-
-
+
2
@@ -327,11 +327,8 @@
-
-
- <html><head/><body><p>Keep the original aspect ratio of the image.</p><p>For example, a 2000x1000 image resized to 1000x1000 would adjust to 1000x500.</p></body></html>
-
- Keep aspect ratio
+ Fit
true
@@ -340,21 +337,15 @@
-
-
- <html><head/><body><p>Resize the image while keeping aspect ratio, then center crop any excess. </p><p>For example, a 500x1000 image resized to 1000x1000 would first scale up to 1000x2000, then the excess height would be cropped to make it 1000x1000.</p></body></html>
-
- Crop scaled overflow
+ Fill
-
-
- <html><head/><body><p>Stretch the image to exactly fit the specified dimensions, modifying the aspect ratio if necessary.</p><p>For example, a 2000x1000 image resized to 1000x1000 would shrink to fit, altering the original proportions.</p></body></html>
-
- Stretch/shrink to target
+ Stretch
@@ -524,7 +515,7 @@
-
-
+
2
@@ -542,11 +533,8 @@
-
-
- <html><head/><body><p>Keep the original aspect ratio of the image.</p><p>For example, a 2000x1000 image resized to 1000x1000 would adjust to 1000x500.</p></body></html>
-
- Keep aspect ratio
+ Fit
true
@@ -555,21 +543,15 @@
-
-
- <html><head/><body><p>Resize the image while keeping aspect ratio, then center crop any excess. </p><p>For example, a 500x1000 image resized to 1000x1000 would first scale up to 1000x2000, then the excess height would be cropped to make it 1000x1000.</p></body></html>
-
- Crop scaled overflow
+ Fill
-
-
- <html><head/><body><p>Stretch the image to exactly fit the specified dimensions, modifying the aspect ratio if necessary.</p><p>For example, a 2000x1000 image resized to 1000x1000 would shrink to fit, altering the original proportions.</p></body></html>
-
- Stretch/shrink to target
+ Stretch
From ccf716fbe1ab4304e0b22d6ae9fea62f3c41fa7d Mon Sep 17 00:00:00 2001
From: twodoorcoupe
Date: Mon, 24 Jun 2024 09:11:11 +0200
Subject: [PATCH 7/7] Improve resize modes tooltips formatting
---
picard/ui/options/cover_processing.py | 44 ++++++++++++++++-----------
1 file changed, 26 insertions(+), 18 deletions(-)
diff --git a/picard/ui/options/cover_processing.py b/picard/ui/options/cover_processing.py
index 91f53ea8c..abd090cb8 100644
--- a/picard/ui/options/cover_processing.py
+++ b/picard/ui/options/cover_processing.py
@@ -65,34 +65,45 @@ class CoverProcessingOptionsPage(OptionsPage):
self.register_setting('cover_file_crop')
tooltip_keep = N_(
- "Scale the source image so that it fits within the target dimensions. One "
- "of the final image dimensions may be less than the target dimension if "
- "the source image and target dimensions have different aspect ratios. "
+ ""
+ "Scale the source image so that it fits within the target dimensions."
+ "
"
+ "One of the final image dimensions may be less than the target dimension if "
+ "the source image and target dimensions have different aspect ratios."
+ "
"
"For example, a 2000x1000 image resized to target dimensions of "
"1000x1000 would result in a final image size of 1000x500."
+ "
"
)
- self.ui.tags_keep.setToolTip(self._add_rich_text(_(tooltip_keep)))
- self.ui.file_keep.setToolTip(self._add_rich_text(_(tooltip_keep)))
- tooltip_crop = N_(
+ self.ui.tags_keep.setToolTip(_(tooltip_keep))
+ self.ui.file_keep.setToolTip(_(tooltip_keep))
+ tooltip_crop = (
+ ""
"Scale the source image so that it completely fills the target dimensions "
- "in both directions. If the source image and target dimensions have "
- "different aspect ratios, then there will be overflow in one direction which "
- "will be (center) cropped. "
+ "in both directions."
+ "
"
+ "If the source image and target dimensions have different aspect ratios"
+ "then there will be overflow in one direction which will be (center) cropped."
+ "
"
"For example, a 500x1000 image resized to target dimensions of "
"1000x1000 would first scale up to 1000x2000, then the excess height "
"would be center cropped resulting in the final image size of 1000x1000."
+ "
"
)
- self.ui.tags_crop.setToolTip(self._add_rich_text(_(tooltip_crop)))
- self.ui.file_crop.setToolTip(self._add_rich_text(_(tooltip_crop)))
- tooltip_stretch = N_(
+ self.ui.tags_crop.setToolTip(_(tooltip_crop))
+ self.ui.file_crop.setToolTip(_(tooltip_crop))
+ tooltip_stretch = (
+ ""
"Stretch the image to exactly fit the specified dimensions, "
- "distorting it if necessary. "
+ "distorting it if necessary."
+ "
"
"For example, a 500x1000 image with target dimension of 1000x1000 "
"would be stretched horizontally resulting in the final image "
"size of 1000x1000."
+ "
"
)
- self.ui.tags_stretch.setToolTip(self._add_rich_text(_(tooltip_stretch)))
- self.ui.file_stretch.setToolTip(self._add_rich_text(_(tooltip_stretch)))
+ self.ui.tags_stretch.setToolTip(_(tooltip_stretch))
+ self.ui.file_stretch.setToolTip(_(tooltip_stretch))
tags_checkboxes = (self.ui.tags_resize_width_label, self.ui.tags_resize_height_label)
tags_at_least_one_checked = partial(self._ensure_at_least_one_checked, tags_checkboxes)
@@ -113,9 +124,6 @@ class CoverProcessingOptionsPage(OptionsPage):
spinbox.setEnabled(checkbox.isChecked())
checkbox.stateChanged.connect(self._update_resize_spinboxes)
- def _add_rich_text(self, text):
- return "" + text + ""
-
def _update_resize_spinboxes(self):
spinbox = self._spinboxes[self.sender()]
spinbox.setEnabled(self.sender().isChecked())