Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Correct image scaling on GTK #2119

Merged
merged 1 commit into from
Sep 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions android/tests_backend/widgets/imageview.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ class ImageViewProbe(SimpleProbe):
@property
def preserve_aspect_ratio(self):
return self.native.getScaleType() == ImageView.ScaleType.FIT_CENTER

def assert_image_size(self, width, height):
# Android internally scales the image to the container,
# so there's no image size check required.
pass
1 change: 1 addition & 0 deletions changes/2119.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
An issue with imageview scaling on GTK was resolved.
5 changes: 5 additions & 0 deletions cocoa/tests_backend/widgets/imageview.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ class ImageViewProbe(SimpleProbe):
@property
def preserve_aspect_ratio(self):
return self.native.imageScaling == NSImageScaleProportionallyUpOrDown

def assert_image_size(self, width, height):
# Cocoa internally scales the image to the container,
# so there's no image size check required.
pass
70 changes: 36 additions & 34 deletions gtk/src/toga_gtk/widgets/imageview.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,49 +7,51 @@
class ImageView(Widget):
def create(self):
self.native = Gtk.Image()
self.native.connect("size-allocate", self.gtk_size_allocate)
self._aspect_ratio = None

def set_image(self, image):
if image:
self.native.set_from_pixbuf(image._impl.native)
self.set_scaled_pixbuf(image._impl.native, self.native.get_allocation())
else:
self.native.set_from_pixbuf(None)

def set_bounds(self, x, y, width, height):
super().set_bounds(x, y, width, height)

# GTK doesn't have any native image resizing; we need to manually
# scale the native pixbuf to the preferred size as a result of
# resizing the image.
def gtk_size_allocate(self, widget, allocation):
# GTK doesn't have any native image resizing; so, when the Gtk.Image
# has a new size allocated, we need to manually scale the native pixbuf
# to the preferred size as a result of resizing the image.
if self.interface.image:
if self._aspect_ratio is None:
# Don't preserve aspect ratio; image fits the available space.
image_width = width
image_height = height
self.set_scaled_pixbuf(self.interface.image._impl.native, allocation)

def set_scaled_pixbuf(self, image, allocation):
if self._aspect_ratio is None:
# Don't preserve aspect ratio; image fits the available space.
image_width = allocation.width
image_height = allocation.height
else:
# Determine what the width/height of the image would be
# preserving the aspect ratio. If the scaled size exceeds
# the allocated size, then that isn't the dimension
# being preserved.
candidate_width = int(allocation.height * self._aspect_ratio)
candidate_height = int(allocation.width / self._aspect_ratio)
if candidate_width > allocation.width:
image_width = allocation.width
image_height = candidate_height
else:
# Determine what the width/height of the image would be
# preserving the aspect ratio. If the scaled size exceeds
# the allocated size, then that isn't the dimension
# being preserved.
candidate_width = int(height * self._aspect_ratio)
candidate_height = int(width / self._aspect_ratio)
if candidate_width > width:
image_width = width
image_height = candidate_height
else:
image_width = candidate_width
image_height = height

# Minimum image size is 1x1
image_width = max(1, image_width)
image_height = max(1, image_height)

# Scale the pixbuf to fit the provided space.
scaled = self.interface.image._impl.native.scale_simple(
image_width, image_height, GdkPixbuf.InterpType.BILINEAR
)

self.native.set_from_pixbuf(scaled)
image_width = candidate_width
image_height = allocation.height

# Minimum image size is 1x1
image_width = max(1, image_width)
image_height = max(1, image_height)

# Scale the pixbuf to fit the provided space.
scaled = self.interface.image._impl.native.scale_simple(
image_width, image_height, GdkPixbuf.InterpType.BILINEAR
)

self.native.set_from_pixbuf(scaled)

def rehint(self):
width, height, self._aspect_ratio = rehint_imageview(
Expand Down
5 changes: 5 additions & 0 deletions gtk/tests_backend/widgets/imageview.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ class ImageViewProbe(SimpleProbe):
@property
def preserve_aspect_ratio(self):
return self.impl._aspect_ratio is not None

def assert_image_size(self, width, height):
# Confirm the underlying pixelbuf has been scaled to the appropriate size.
pixbuf = self.native.get_pixbuf()
assert (pixbuf.get_width(), pixbuf.get_height()) == (width, height)
5 changes: 5 additions & 0 deletions iOS/tests_backend/widgets/imageview.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ class ImageViewProbe(SimpleProbe):
@property
def preserve_aspect_ratio(self):
return self.native.contentMode == UIViewContentMode.ScaleAspectFit.value

def assert_image_size(self, width, height):
# UIKit internally scales the image to the container,
# so there's no image size check required.
pass
32 changes: 30 additions & 2 deletions testbed/tests/widgets/test_imageview.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ async def test_implicit_size(widget, probe, container_probe):
assert probe.width == pytest.approx(144, abs=2)
assert probe.height == pytest.approx(72, abs=2)
assert probe.preserve_aspect_ratio
probe.assert_image_size(144, 72)

# Clear the image; it's now an explicit sized empty image.
widget.image = None
Expand All @@ -34,6 +35,7 @@ async def test_implicit_size(widget, probe, container_probe):
assert not probe.preserve_aspect_ratio

# Restore the image; Make the parent a flex row
# Image will become as wide as the container.
widget.image = "resources/sample.png"
widget.style.flex = 1
widget.parent.style.direction = ROW
Expand All @@ -42,25 +44,36 @@ async def test_implicit_size(widget, probe, container_probe):
assert probe.width == pytest.approx(container_probe.width, abs=2)
assert probe.height == pytest.approx(container_probe.height, abs=2)
assert probe.preserve_aspect_ratio
probe.assert_image_size(
pytest.approx(probe.width, abs=2),
pytest.approx(probe.width // 2, abs=2),
)

# Make the parent a flex column
# Image will try to be as tall as the container, but will be
# constrained by preserving the aspect ratio
widget.parent.style.direction = COLUMN

await probe.redraw("Image is in a column box")
assert probe.width == pytest.approx(container_probe.width, abs=2)
assert probe.height == pytest.approx(container_probe.height, abs=2)
assert probe.preserve_aspect_ratio
probe.assert_image_size(
pytest.approx(probe.width, abs=2),
pytest.approx(probe.width // 2, abs=2),
)


async def test_explicit_width(widget, probe, container_probe):
"""If the image width is explicit, the image view will resize preserving aspect ratio."""
# Explicitly set width
# Explicitly set width; height follows aspect raio
widget.style.width = 200

await probe.redraw("Image has explicit width")
assert probe.width == pytest.approx(200, abs=2)
assert probe.height == pytest.approx(100, abs=2)
assert probe.preserve_aspect_ratio
probe.assert_image_size(200, 100)

# Clear the image; it's now an explicit sized empty image.
widget.image = None
Expand All @@ -79,6 +92,8 @@ async def test_explicit_width(widget, probe, container_probe):
assert probe.width == pytest.approx(200, abs=2)
assert probe.height == pytest.approx(container_probe.height, abs=2)
assert probe.preserve_aspect_ratio
# Container has fixed width; aspect ratio is preserved, so image isn't tall
probe.assert_image_size(200, 100)

# Make the parent a flex column
widget.parent.style.direction = COLUMN
Expand All @@ -87,17 +102,20 @@ async def test_explicit_width(widget, probe, container_probe):
assert probe.width == pytest.approx(200, abs=2)
assert probe.height == pytest.approx(container_probe.height, abs=2)
assert probe.preserve_aspect_ratio
# Container has fixed width; aspect ratio is preserved, image is implicit height
probe.assert_image_size(200, 100)


async def test_explicit_height(widget, probe, container_probe):
"""If the image height is explicit, the image view will resize preserving aspect ratio."""
# Explicitly set height
# Explicitly set height; width follows aspect raio
widget.style.height = 150

await probe.redraw("Image has explicit height")
assert probe.width == pytest.approx(300, abs=2)
assert probe.height == pytest.approx(150, abs=2)
assert probe.preserve_aspect_ratio
probe.assert_image_size(300, 150)

# Clear the image; it's now an explicit sized empty image.
widget.image = None
Expand All @@ -116,6 +134,8 @@ async def test_explicit_height(widget, probe, container_probe):
assert probe.width == pytest.approx(container_probe.width, abs=2)
assert probe.height == pytest.approx(150, abs=2)
assert probe.preserve_aspect_ratio
# Container has fixed height; aspect ratio is preserved, so image isn't wide
probe.assert_image_size(300, 150)

# Make the parent a flex column
widget.parent.style.direction = COLUMN
Expand All @@ -124,6 +144,8 @@ async def test_explicit_height(widget, probe, container_probe):
assert probe.width == pytest.approx(container_probe.width, abs=2)
assert probe.height == pytest.approx(150, abs=2)
assert probe.preserve_aspect_ratio
# Container has fixed height; aspect ratio is preserved, image is implicit height
probe.assert_image_size(300, 150)


async def test_explicit_size(widget, probe):
Expand All @@ -136,6 +158,8 @@ async def test_explicit_size(widget, probe):
assert probe.width == pytest.approx(200, abs=2)
assert probe.height == pytest.approx(300, abs=2)
assert not probe.preserve_aspect_ratio
# Image is the size specified.
probe.assert_image_size(200, 300)

# Clear the image; it's now an explicit sized empty image.
widget.image = None
Expand All @@ -154,6 +178,8 @@ async def test_explicit_size(widget, probe):
assert probe.width == pytest.approx(200, abs=2)
assert probe.height == pytest.approx(300, abs=2)
assert not probe.preserve_aspect_ratio
# Image is the size specified.
probe.assert_image_size(200, 300)

# Make the parent a flex column
widget.parent.style.direction = COLUMN
Expand All @@ -162,3 +188,5 @@ async def test_explicit_size(widget, probe):
assert probe.width == pytest.approx(200, abs=2)
assert probe.height == pytest.approx(300, abs=2)
assert not probe.preserve_aspect_ratio
# Image is the size specified.
probe.assert_image_size(200, 300)
5 changes: 5 additions & 0 deletions winforms/tests_backend/widgets/imageview.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ class ImageViewProbe(SimpleProbe):
@property
def preserve_aspect_ratio(self):
return self.native.SizeMode == WinForms.PictureBoxSizeMode.Zoom

def assert_image_size(self, width, height):
# Winforms internally scales the image to the container,
# so there's no image size check required.
pass