From c6ff9caa5a14ad8e60cb60b23e085a0dcff1fa93 Mon Sep 17 00:00:00 2001 From: twodoorcoupe Date: Fri, 21 Jun 2024 12:58:06 +0200 Subject: [PATCH] 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 + + + + + +