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
+
+
+
+
+
+