Skip to content

Commit

Permalink
Antialiasing/smoothing for displaying images
Browse files Browse the repository at this point in the history
  • Loading branch information
rbreu committed Nov 26, 2023
1 parent 8a9cab9 commit 608787d
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 52 deletions.
13 changes: 9 additions & 4 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----
Expand Down
2 changes: 1 addition & 1 deletion beeref/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class BeeSettingsEvents(QtCore.QObject):
class BeeSettings(QtCore.QSettings):

DEFAULTS = {
'FileIO/image_storage_format': 'best',
'Items/image_storage_format': 'best',
}

def __init__(self):
Expand Down
11 changes: 9 additions & 2 deletions beeref/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,14 @@ def get_extra_save_data(self):
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'
Expand Down Expand Up @@ -300,6 +301,12 @@ def draw_crop_rect(self, painter, rect):
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)

Expand Down
51 changes: 29 additions & 22 deletions beeref/widgets/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,23 @@
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'))

def __init__(self, parent):
super().__init__('Image Storage Format:')
parent.settings_widgets.append(self)
class RadioGroup(QtWidgets.QGroupBox):
TITLE = None
HELPTEXT = None
KEY = None
OPTIONS = None

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 = {}
Expand Down Expand Up @@ -78,19 +73,31 @@ def on_restore_defaults(self):
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()
Expand Down
26 changes: 16 additions & 10 deletions tests/items/test_pixmapitem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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),
Expand All @@ -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),
Expand All @@ -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),
Expand All @@ -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),
Expand All @@ -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'
Expand Down Expand Up @@ -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(
Expand All @@ -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())
Expand Down
43 changes: 35 additions & 8 deletions tests/selection/test_selectable_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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(
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()


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


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


Expand Down
10 changes: 5 additions & 5 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit 608787d

Please sign in to comment.