From 608787d9a9b53891af989ad58d017a78c46adce5 Mon Sep 17 00:00:00 2001 From: Rebecca Breu Date: Sun, 26 Nov 2023 11:15:01 +0100 Subject: [PATCH] Antialiasing/smoothing for displaying images --- CHANGELOG.rst | 13 +++++-- beeref/config.py | 2 +- beeref/items.py | 11 +++++- beeref/widgets/settings.py | 49 ++++++++++++++---------- tests/items/test_pixmapitem.py | 26 ++++++++----- tests/selection/test_selectable_mixin.py | 43 +++++++++++++++++---- tests/test_config.py | 10 ++--- 7 files changed, 103 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8760a65..b496f7c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,10 +4,15 @@ Added ----- -* Embedded images can now be JPG or PNG. By default, small images and - images with an alpha channel will be stored as PNG, the rest as - JPG. In the newly created settings dialog, this behaviour can be - changed to always use PNG (the former behaviour) always JPG. +* Images can now be stored JPG or PNG inside the bee file. By default, + small images and images with an alpha channel will be stored as PNG, + the rest as JPG. In the newly created settings dialog, this + behaviour can be changed to always use PNG (the former behaviour) or + always JPG. +* Antialias/smoothing for displaying images. For images being + displayed at a large zoom factor, smoothing will turn off to make + sure that icons, pixel sprites etc can be viewed correctly. + Fixed ----- diff --git a/beeref/config.py b/beeref/config.py index d3c55c2..88e91b8 100644 --- a/beeref/config.py +++ b/beeref/config.py @@ -107,7 +107,7 @@ settings_events = BeeSettingsEvents() class BeeSettings(QtCore.QSettings): DEFAULTS = { - 'FileIO/image_storage_format': 'best', + 'Items/image_storage_format': 'best', } def __init__(self): diff --git a/beeref/items.py b/beeref/items.py index 0b44a95..8bba1ec 100644 --- a/beeref/items.py +++ b/beeref/items.py @@ -134,13 +134,14 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem): def get_imgformat(self, img): """Determines the format for storing this image.""" - formt = self.settings.valueOrDefault('FileIO/image_storage_format') + formt = self.settings.valueOrDefault('Items/image_storage_format') if formt not in ('png', 'jpg', 'best'): formt = 'best' if formt == 'best': + # Images with alpha channel and small images are stored as png if (img.hasAlphaChannel() - or (img.height() < 200 and img.width() < 200)): + or (img.height() < 300 and img.width() < 300)): formt = 'png' else: formt = 'jpg' @@ -300,6 +301,12 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem): painter.drawRect(rect) def paint(self, painter, option, widget): + if painter.combinedTransform().m11() < 3: + # We want image smoothing, but only for images where we + # are not zoomed in a lot. This is to ensure that for + # example icons and pixel sprites can be viewed correctly. + painter.setRenderHint(painter.RenderHint.SmoothPixmapTransform) + if self.crop_mode: self.paint_debug(painter, option, widget) diff --git a/beeref/widgets/settings.py b/beeref/widgets/settings.py index 1923881..0d096c9 100644 --- a/beeref/widgets/settings.py +++ b/beeref/widgets/settings.py @@ -25,28 +25,23 @@ from beeref.config import BeeSettings, settings_events logger = logging.getLogger(__name__) -class ImageStorageFormatWidget(QtWidgets.QGroupBox): - KEY = 'FileIO/image_storage_format' - OPTIONS = ( - ('best', 'Best Guess', - ('Small images and images with alpha channel are stored as png,' - ' everything else as jpg')), - ('png', 'Always PNG', 'Lossless, but large bee file'), - ('jpg', 'Always JPG', - 'Small bee file, but lossy and no transparency support')) +class RadioGroup(QtWidgets.QGroupBox): + TITLE = None + HELPTEXT = None + KEY = None + OPTIONS = None - def __init__(self, parent): - super().__init__('Image Storage Format:') - parent.settings_widgets.append(self) + def __init__(self): + super().__init__(self.TITLE) self.settings = BeeSettings() - settings_events.restore_defaults.connect(self.on_restore_defaults) layout = QtWidgets.QVBoxLayout() self.setLayout(layout) - helptxt = QtWidgets.QLabel( - 'How images are stored inside bee files.' - ' Changes will only take effect on newly saved images.') - helptxt.setWordWrap(True) - layout.addWidget(helptxt) + settings_events.restore_defaults.connect(self.on_restore_defaults) + + if self.HELPTEXT: + helptxt = QtWidgets.QLabel(self.HELPTEXT) + helptxt.setWordWrap(True) + layout.addWidget(helptxt) self.ignore_values_changed = True self.buttons = {} @@ -78,19 +73,31 @@ class ImageStorageFormatWidget(QtWidgets.QGroupBox): self.ignore_values_changed = False +class ImageStorageFormatWidget(RadioGroup): + TITLE = 'Image Storage Format:' + HELPTEXT = ('How images are stored inside bee files.' + ' Changes will only take effect on newly saved images.') + KEY = 'Items/image_storage_format' + OPTIONS = ( + ('best', 'Best Guess', + ('Small images and images with alpha channel are stored as png,' + ' everything else as jpg')), + ('png', 'Always PNG', 'Lossless, but large bee file'), + ('jpg', 'Always JPG', + 'Small bee file, but lossy and no transparency support')) + + class SettingsDialog(QtWidgets.QDialog): def __init__(self, parent): super().__init__(parent) self.setWindowTitle(f'{constants.APPNAME} Settings') tabs = QtWidgets.QTabWidget() - self.settings_widgets = [] - # Miscellaneous misc = QtWidgets.QWidget() misc_layout = QtWidgets.QGridLayout() misc.setLayout(misc_layout) - misc_layout.addWidget(ImageStorageFormatWidget(self), 0, 0) + misc_layout.addWidget(ImageStorageFormatWidget(), 0, 0) tabs.addTab(misc, '&Miscellaneous') layout = QtWidgets.QVBoxLayout() diff --git a/tests/items/test_pixmapitem.py b/tests/items/test_pixmapitem.py index 8578c40..3e6ff54 100644 --- a/tests/items/test_pixmapitem.py +++ b/tests/items/test_pixmapitem.py @@ -83,21 +83,21 @@ def test_get_extra_save_data(item): def test_get_imgformat_test_with_real_image( qapp, imgfilename3x3, item, settings): - settings.setValue('FileIO/image_storage_format', 'best') + settings.setValue('Items/image_storage_format', 'best') img = QtGui.QImage(imgfilename3x3) assert item.get_imgformat(img) == 'png' def test_get_imgformat_unknown_option_defaults_to_best( qapp, imgfilename3x3, item, settings): - settings.setValue('FileIO/image_storage_format', 'foo') + settings.setValue('Items/image_storage_format', 'foo') img = QtGui.QImage(imgfilename3x3) assert item.get_imgformat(img) == 'png' def test_get_imgformat_jpg_for_large_nonalpha_image_when_setting_best( qapp, settings, item): - settings.setValue('FileIO/image_storage_format', 'best') + settings.setValue('Items/image_storage_format', 'best') img = MagicMock( hasAlphaChannel=MagicMock(return_value=False), height=MagicMock(return_value=1600), @@ -107,7 +107,7 @@ def test_get_imgformat_jpg_for_large_nonalpha_image_when_setting_best( def test_get_imgformat_png_for_large_alpha_image_when_setting_best( qapp, settings, item): - settings.setValue('FileIO/image_storage_format', 'best') + settings.setValue('Items/image_storage_format', 'best') img = MagicMock( hasAlphaChannel=MagicMock(return_value=True), height=MagicMock(return_value=1600), @@ -117,7 +117,7 @@ def test_get_imgformat_png_for_large_alpha_image_when_setting_best( def test_get_imgformat_png_for_small_nonalpha_image_when_setting_best( qapp, settings, item): - settings.setValue('FileIO/image_storage_format', 'best') + settings.setValue('Items/image_storage_format', 'best') img = MagicMock( hasAlphaChannel=MagicMock(return_value=False), height=MagicMock(return_value=100), @@ -127,7 +127,7 @@ def test_get_imgformat_png_for_small_nonalpha_image_when_setting_best( def test_get_imgformat_jpg_when_setting_jpg( qapp, settings, item): - settings.setValue('FileIO/image_storage_format', 'jpg') + settings.setValue('Items/image_storage_format', 'jpg') img = MagicMock( hasAlphaChannel=MagicMock(return_value=True), height=MagicMock(return_value=100), @@ -137,7 +137,7 @@ def test_get_imgformat_jpg_when_setting_jpg( def test_get_imgformat_png_when_setting_png( qapp, settings, item): - settings.setValue('FileIO/image_storage_format', 'png') + settings.setValue('Items/image_storage_format', 'png') img = MagicMock( hasAlphaChannel=MagicMock(return_value=False), height=MagicMock(return_value=1600), @@ -153,7 +153,7 @@ def test_pixmap_to_bytes_png(qapp, imgfilename3x3): def test_pixmap_to_bytes_jpg(qapp, imgfilename3x3, settings): - settings.setValue('FileIO/image_storage_format', 'jpg') + settings.setValue('Items/image_storage_format', 'jpg') item = BeePixmapItem(QtGui.QImage(imgfilename3x3)) data, imgformat = item.pixmap_to_bytes() assert imgformat == 'jpg' @@ -338,7 +338,10 @@ def test_paint(qapp, item): item.pixmap = MagicMock() item.paint_selectable = MagicMock() item.crop = QtCore.QRectF(10, 20, 30, 40) - painter = MagicMock() + painter = MagicMock( + combinedTransform=MagicMock( + return_value=MagicMock( + m11=MagicMock(return_value=0.5)))) item.paint(painter, None, None) item.paint_selectable.assert_called_once() painter.drawPixmap.assert_called_with( @@ -353,7 +356,10 @@ def test_paint_when_crop_mode(qapp, item): item.crop = QtCore.QRectF(10, 20, 30, 40) item.crop_mode = True item.crop_temp = QtCore.QRectF(11, 22, 29, 39) - painter = MagicMock() + painter = MagicMock( + combinedTransform=MagicMock( + return_value=MagicMock( + m11=MagicMock(return_value=0.5)))) item.paint(painter, None, None) item.paint_selectable.assert_not_called() painter.drawPixmap.assert_called_with(0, 0, item.pixmap()) diff --git a/tests/selection/test_selectable_mixin.py b/tests/selection/test_selectable_mixin.py index aae2a0f..4099f1e 100644 --- a/tests/selection/test_selectable_mixin.py +++ b/tests/selection/test_selectable_mixin.py @@ -86,7 +86,10 @@ def test_select_handle_free_center(view, item): def test_draw_debug_shape_rect(view, item): view.scene.addItem(item) - painter = MagicMock() + painter = MagicMock( + combinedTransform=MagicMock( + return_value=MagicMock( + m11=MagicMock(return_value=0.5)))) item.draw_debug_shape( painter, QtCore.QRectF(5, 6, 20, 30), @@ -97,7 +100,10 @@ def test_draw_debug_shape_rect(view, item): def test_draw_debug_shape_path(view, item): view.scene.addItem(item) - painter = MagicMock() + painter = MagicMock( + combinedTransform=MagicMock( + return_value=MagicMock( + m11=MagicMock(return_value=0.5)))) path = QtGui.QPainterPath() path.addRect(QtCore.QRectF(5, 6, 20, 30)) item.draw_debug_shape( @@ -111,7 +117,10 @@ def test_draw_debug_shape_path(view, item): @patch('beeref.items.BeePixmapItem.draw_debug_shape') def test_paint_when_not_selected(debug_mock, view, item): view.scene.addItem(item) - painter = MagicMock() + painter = MagicMock( + combinedTransform=MagicMock( + return_value=MagicMock( + m11=MagicMock(return_value=0.5)))) item.setSelected(False) item.paint(painter, None, None) painter.drawPixmap.assert_called_once() @@ -122,7 +131,10 @@ def test_paint_when_not_selected(debug_mock, view, item): def test_paint_when_selected_single_selection(view, item): view.scene.addItem(item) - painter = MagicMock() + painter = MagicMock( + combinedTransform=MagicMock( + return_value=MagicMock( + m11=MagicMock(return_value=0.5)))) item.setSelected(True) item.paint(painter, None, None) painter.drawPixmap.assert_called_once() @@ -135,7 +147,10 @@ def test_paint_when_selected_multi_selection(view, item): item2 = BeePixmapItem(QtGui.QImage()) item2.setSelected(True) view.scene.addItem(item2) - painter = MagicMock() + painter = MagicMock( + combinedTransform=MagicMock( + return_value=MagicMock( + m11=MagicMock(return_value=0.5)))) item.setSelected(True) item.paint(painter, None, None) painter.drawPixmap.assert_called_once() @@ -150,7 +165,11 @@ def test_paint_when_debug_shapes(view): args_mock.debug_boundingrects = False args_mock.debug_handles = False item = BeePixmapItem(QtGui.QImage()) - item.paint(MagicMock(), None, None) + painter = MagicMock( + combinedTransform=MagicMock( + return_value=MagicMock( + m11=MagicMock(return_value=0.5)))) + item.paint(painter, None, None) m.assert_called_once() @@ -161,7 +180,11 @@ def test_paint_when_debug_boundingrects(view): args_mock.debug_boundingrects = True args_mock.debug_handles = False item = BeePixmapItem(QtGui.QImage()) - item.paint(MagicMock(), None, None) + painter = MagicMock( + combinedTransform=MagicMock( + return_value=MagicMock( + m11=MagicMock(return_value=0.5)))) + item.paint(painter, None, None) m.assert_called_once() @@ -174,7 +197,11 @@ def test_paint_when_debug_handles(view): item = BeePixmapItem(QtGui.QImage()) view.scene.addItem(item) item.setSelected(True) - item.paint(MagicMock(), None, None) + painter = MagicMock( + combinedTransform=MagicMock( + return_value=MagicMock( + m11=MagicMock(return_value=0.5)))) + item.paint(painter, None, None) m.assert_called() diff --git a/tests/test_config.py b/tests/test_config.py index 405178d..175d39b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -36,18 +36,18 @@ def test_command_line_args_get_unknown(): def test_settings_value_or_default_gets_default(settings): - assert settings.valueOrDefault('FileIO/image_storage_format') == 'best' + assert settings.valueOrDefault('Items/image_storage_format') == 'best' def test_settings_value_or_default_gets_overriden_value(settings): - settings.setValue('FileIO/image_storage_format', 'png') - assert settings.valueOrDefault('FileIO/image_storage_format') == 'png' + settings.setValue('Items/image_storage_format', 'png') + assert settings.valueOrDefault('Items/image_storage_format') == 'png' def test_restore_defaults_restores(settings): - settings.setValue('FileIO/image_storage_format', 'png') + settings.setValue('Items/image_storage_format', 'png') settings.restore_defaults() - assert settings.contains('FileIO/image_storage_format') is False + assert settings.contains('Items/image_storage_format') is False def test_restore_defaults_leaves_other_settings(settings):