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())