Antialiasing/smoothing for displaying images

This commit is contained in:
Rebecca Breu 2023-11-26 11:15:01 +01:00
parent 8a9cab9a73
commit 608787d9a9
7 changed files with 103 additions and 51 deletions

View file

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

View file

@ -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):

View file

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

View file

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

View file

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

View file

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

View file

@ -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):