Skip to content

Commit

Permalink
Merge pull request #2119 from freakboy3742/gtk-imageview-sizing
Browse files Browse the repository at this point in the history
Correct image scaling on GTK
  • Loading branch information
mhsmith authored Sep 17, 2023
2 parents 82fad50 + cf7abe2 commit 920d020
Show file tree
Hide file tree
Showing 8 changed files with 92 additions and 36 deletions.
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

0 comments on commit 920d020

Please sign in to comment.