From c8775306deffe826cb14d0fe177e0c09aa4204fd Mon Sep 17 00:00:00 2001 From: robmeth <91134475+robmeth@users.noreply.github.com> Date: Wed, 21 Jun 2023 12:30:29 +0200 Subject: [PATCH] feat: rotate_left and rotate_right added to Image (#361) Closes #281. ### Summary of Changes Added two methods to `Image`: * `rotate_right` to rotate the image clockwise by 90 degrees * `rotate_left` to rotate the image counter-clockwise by 90 degrees. Both return a new image and leave the original unchanged. --------- Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> --- src/safeds/data/image/containers/_image.py | 38 +++++++++++++++++ .../image/snapshot_boxplot_left_rotation.png | Bin 0 -> 7049 bytes .../image/snapshot_boxplot_right_rotation.png | Bin 0 -> 6984 bytes .../data/image/containers/test_image.py | 39 ++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 tests/resources/image/snapshot_boxplot_left_rotation.png create mode 100644 tests/resources/image/snapshot_boxplot_right_rotation.png diff --git a/src/safeds/data/image/containers/_image.py b/src/safeds/data/image/containers/_image.py index ddfeec3e6..2d0d3fc17 100644 --- a/src/safeds/data/image/containers/_image.py +++ b/src/safeds/data/image/containers/_image.py @@ -12,6 +12,7 @@ from PIL.Image import open as open_image from safeds.data.image.typing import ImageFormat +from safeds.exceptions._data import WrongFileExtensionError class Image: @@ -416,3 +417,40 @@ def invert_colors(self) -> Image: new_image = Image(data, self._format) new_image._image = ImageOps.invert(new_image._image) return new_image + + def rotate_right(self) -> Image: + """ + Return the png-image clockwise rotated by 90 degrees. + + Returns + ------- + result : Image + The image clockwise rotated by 90 degrees. + """ + if self.format != ImageFormat.PNG: + raise WrongFileExtensionError("/image", ".png") + + imagecopy = copy.deepcopy(self) + imagecopy._image = imagecopy._image.rotate(270, expand=True) + return imagecopy + + def rotate_left(self) -> Image: + """ + Return the png-image counter-clockwise rotated by 90 degrees. + + Returns + ------- + result : Image + The image counter-clockwise rotated by 90 degrees. + + Raises + ------ + WrongFileExtensionError + If given a File that's not PNG. + """ + if self.format != ImageFormat.PNG: + raise WrongFileExtensionError("/image", ".png") + + imagecopy = copy.deepcopy(self) + imagecopy._image = imagecopy._image.rotate(90, expand=True) + return imagecopy diff --git a/tests/resources/image/snapshot_boxplot_left_rotation.png b/tests/resources/image/snapshot_boxplot_left_rotation.png new file mode 100644 index 0000000000000000000000000000000000000000..600de1feea5154cef230a7f91acc8d380634d368 GIT binary patch literal 7049 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 NKRTZ%|H1p(p8?UpJmLTV literal 0 HcmV?d00001 diff --git a/tests/resources/image/snapshot_boxplot_right_rotation.png b/tests/resources/image/snapshot_boxplot_right_rotation.png new file mode 100644 index 0000000000000000000000000000000000000000..f6c40de31a0296dcad72ca64d21e0de6e14c15c3 GIT binary patch literal 6984 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 ECzvL3jsO4v literal 0 HcmV?d00001 diff --git a/tests/safeds/data/image/containers/test_image.py b/tests/safeds/data/image/containers/test_image.py index 29237dcd9..34da91c5a 100644 --- a/tests/safeds/data/image/containers/test_image.py +++ b/tests/safeds/data/image/containers/test_image.py @@ -5,6 +5,7 @@ from safeds.data.image.containers import Image from safeds.data.image.typing import ImageFormat from safeds.data.tabular.containers import Table +from safeds.exceptions._data import WrongFileExtensionError from tests.helpers import resolve_resource_path @@ -383,3 +384,41 @@ def test_should_not_sharpen(self) -> None: image = Image.from_png_file(resolve_resource_path("image/sharpen/to_sharpen.png")) image2 = image.sharpen(1) assert image == image2 + + +class TestRotate: + def test_should_return_clockwise_rotated_image( + self, + ) -> None: + image = Image.from_png_file(resolve_resource_path("image/snapshot_boxplot.png")) + image = image.rotate_right() + image2 = Image.from_png_file(resolve_resource_path("image/snapshot_boxplot_right_rotation.png")) + assert image == image2 + + def test_should_return_counter_clockwise_rotated_image( + self, + ) -> None: + image = Image.from_png_file(resolve_resource_path("image/snapshot_boxplot.png")) + image = image.rotate_left() + image2 = Image.from_png_file(resolve_resource_path("image/snapshot_boxplot_left_rotation.png")) + assert image == image2 + + def test_should_raise_if_not_png_right(self) -> None: + with pytest.raises( + WrongFileExtensionError, + match=( + "The file /image has a wrong file extension. Please provide a file with the following extension\\(s\\):" + " .png" + ), + ): + Image.from_jpeg_file(resolve_resource_path("image/white_square.jpg")).rotate_right() + + def test_should_raise_if_not_png_left(self) -> None: + with pytest.raises( + WrongFileExtensionError, + match=( + "The file /image has a wrong file extension. Please provide a file with the following extension\\(s\\):" + " .png" + ), + ): + Image.from_jpeg_file(resolve_resource_path("image/white_square.jpg")).rotate_left()