From ed7ae341c4546ec32efe46e22dccc4d770126695 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Tue, 4 Apr 2023 16:03:29 +0200 Subject: [PATCH] feat: container for images (#159) Closes #158. ### Summary of Changes * New class `safeds.data.image.containers.Image` to store images * An `Image` can be created from and saved to JPEG and PNG files * Methods that create a plot for `Table` and `Column` now return an `Image` instead of plotting them directly in an interactive environment * An `Image` can be displayed in an interactive environment --------- Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> --- poetry.lock | 157 ++++++++---------- pyproject.toml | 5 +- src/safeds/data/image/__init__.py | 1 + src/safeds/data/image/containers/__init__.py | 7 + src/safeds/data/image/containers/_image.py | 153 +++++++++++++++++ src/safeds/data/image/typing/__init__.py | 7 + src/safeds/data/image/typing/_image_format.py | 8 + src/safeds/data/tabular/containers/_column.py | 66 ++++++-- src/safeds/data/tabular/containers/_row.py | 2 +- src/safeds/data/tabular/containers/_table.py | 54 +++++- .../data/tabular/containers/_tagged_table.py | 2 +- tests/resources/image/white_square.jpg | Bin 0 -> 4083 bytes tests/resources/image/white_square.png | Bin 0 -> 1798 bytes tests/safeds/data/image/__init__.py | 0 .../safeds/data/image/containers/__init__.py | 0 .../data/image/containers/test_image.py | 122 ++++++++++++++ tests/safeds/data/image/typing/__init__.py | 0 .../data/image/typing/test_image_format.py | 14 ++ 18 files changed, 485 insertions(+), 113 deletions(-) create mode 100644 src/safeds/data/image/__init__.py create mode 100644 src/safeds/data/image/containers/__init__.py create mode 100644 src/safeds/data/image/containers/_image.py create mode 100644 src/safeds/data/image/typing/__init__.py create mode 100644 src/safeds/data/image/typing/_image_format.py create mode 100644 tests/resources/image/white_square.jpg create mode 100644 tests/resources/image/white_square.png create mode 100644 tests/safeds/data/image/__init__.py create mode 100644 tests/safeds/data/image/containers/__init__.py create mode 100644 tests/safeds/data/image/containers/test_image.py create mode 100644 tests/safeds/data/image/typing/__init__.py create mode 100644 tests/safeds/data/image/typing/test_image_format.py diff --git a/poetry.lock b/poetry.lock index 202c881e4..e6e840496 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. [[package]] name = "anyio" @@ -2056,93 +2056,82 @@ files = [ [[package]] name = "pillow" -version = "9.4.0" +version = "9.5.0" description = "Python Imaging Library (Fork)" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "Pillow-9.4.0-1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1"}, - {file = "Pillow-9.4.0-1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12"}, - {file = "Pillow-9.4.0-1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd"}, - {file = "Pillow-9.4.0-1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9"}, - {file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"}, - {file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"}, - {file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"}, - {file = "Pillow-9.4.0-2-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0"}, - {file = "Pillow-9.4.0-2-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f"}, - {file = "Pillow-9.4.0-2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c"}, - {file = "Pillow-9.4.0-2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848"}, - {file = "Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1"}, - {file = "Pillow-9.4.0-2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33"}, - {file = "Pillow-9.4.0-2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9"}, - {file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"}, - {file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070"}, - {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5b2f8a31bd43e0f18172d8ac82347c8f37ef3e0b414431157718aa234991b28"}, - {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35"}, - {file = "Pillow-9.4.0-cp310-cp310-win32.whl", hash = "sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a"}, - {file = "Pillow-9.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391"}, - {file = "Pillow-9.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133"}, - {file = "Pillow-9.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eaef5d2de3c7e9b21f1e762f289d17b726c2239a42b11e25446abf82b26ac132"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6abfb51a82e919e3933eb137e17c4ae9c0475a25508ea88993bb59faf82f3b35"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6663977496d616b618b6cfa43ec86e479ee62b942e1da76a2c3daa1c75933ef4"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60e7da3a3ad1812c128750fc1bc14a7ceeb8d29f77e0a2356a8fb2aa8925287d"}, - {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8"}, - {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a"}, - {file = "Pillow-9.4.0-cp311-cp311-win32.whl", hash = "sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c"}, - {file = "Pillow-9.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee"}, - {file = "Pillow-9.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5f532a2ad4d174eb73494e7397988e22bf427f91acc8e6ebf5bb10597b49c493"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dd5a9c3091a0f414a963d427f920368e2b6a4c2f7527fdd82cde8ef0bc7a327"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:653d7fb2df65efefbcbf81ef5fe5e5be931f1ee4332c2893ca638c9b11a409c4"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5"}, - {file = "Pillow-9.4.0-cp37-cp37m-win32.whl", hash = "sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e"}, - {file = "Pillow-9.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6"}, - {file = "Pillow-9.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:0e51f608da093e5d9038c592b5b575cadc12fd748af1479b5e858045fff955a9"}, - {file = "Pillow-9.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:765cb54c0b8724a7c12c55146ae4647e0274a839fb6de7bcba841e04298e1011"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:519e14e2c49fcf7616d6d2cfc5c70adae95682ae20f0395e9280db85e8d6c4df"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d197df5489004db87d90b918033edbeee0bd6df3848a204bca3ff0a903bef837"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:e1339790c083c5a4de48f688b4841f18df839eb3c9584a770cbd818b33e26d5d"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:a96e6e23f2b79433390273eaf8cc94fec9c6370842e577ab10dabdcc7ea0a66b"}, - {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f"}, - {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d7081c084ceb58278dd3cf81f836bc818978c0ccc770cbbb202125ddabec6628"}, - {file = "Pillow-9.4.0-cp38-cp38-win32.whl", hash = "sha256:df41112ccce5d47770a0c13651479fbcd8793f34232a2dd9faeccb75eb5d0d0d"}, - {file = "Pillow-9.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a"}, - {file = "Pillow-9.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569"}, - {file = "Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb362e3b0976dc994857391b776ddaa8c13c28a16f80ac6522c23d5257156bed"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:53dcb50fbdc3fb2c55431a9b30caeb2f7027fcd2aeb501459464f0214200a503"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:e8c5cf126889a4de385c02a2c3d3aba4b00f70234bfddae82a5eaa3ee6d5e3e6"}, - {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c6b1389ed66cdd174d040105123a5a1bc91d0aa7059c7261d20e583b6d8cbd2"}, - {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0dd4c681b82214b36273c18ca7ee87065a50e013112eea7d78c7a1b89a739153"}, - {file = "Pillow-9.4.0-cp39-cp39-win32.whl", hash = "sha256:6d9dfb9959a3b0039ee06c1a1a90dc23bac3b430842dcb97908ddde05870601c"}, - {file = "Pillow-9.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:54614444887e0d3043557d9dbc697dbb16cfb5a35d672b7a0fcc1ed0cf1c600b"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3b56206244dc8711f7e8b7d6cad4663917cd5b2d950799425076681e8766286"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db74f5562c09953b2c5f8ec4b7dfd3f5421f31811e97d1dbc0a7c93d6e3a24df"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e9d7747847c53a16a729b6ee5e737cf170f7a16611c143d95aa60a109a59c336"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:575d8912dca808edd9acd6f7795199332696d3469665ef26163cd090fa1f8bfa"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c4ed2ff6760e98d262e0cc9c9a7f7b8a9f61aa4d47c58835cdaf7b0b8811bb"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e621b0246192d3b9cb1dc62c78cfa4c6f6d2ddc0ec207d43c0dedecb914f152a"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9"}, - {file = "Pillow-9.4.0.tar.gz", hash = "sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e"}, -] - -[package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] + {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, + {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, + {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, + {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, + {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, + {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, + {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, + {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, + {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, + {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, + {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, + {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, + {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, + {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, + {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, + {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, + {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] [[package]] @@ -3256,4 +3245,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "3647447c1e226370e32e2e0570658855ac56e698ef54cac498ece3b1cdd1cddb" +content-hash = "3e0926fc4e5ae5fd751663d725f617340378416fc10569f1c59d5c62bac5af1d" diff --git a/pyproject.toml b/pyproject.toml index 671bb8238..52ec8b505 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,11 +14,12 @@ packages = [ [tool.poetry.dependencies] python = "^3.10" +ipython = "^8.8.0" +matplotlib = "^3.6.3" pandas = "^1.5.3" +pillow = "^9.5.0" scikit-learn = "^1.2.0" seaborn = "^0.12.2" -ipython = "^8.8.0" -matplotlib = "^3.6.3" [tool.poetry.group.dev.dependencies] pytest = "^7.2.1" diff --git a/src/safeds/data/image/__init__.py b/src/safeds/data/image/__init__.py new file mode 100644 index 000000000..88627d20e --- /dev/null +++ b/src/safeds/data/image/__init__.py @@ -0,0 +1 @@ +"""Work with image data.""" diff --git a/src/safeds/data/image/containers/__init__.py b/src/safeds/data/image/containers/__init__.py new file mode 100644 index 000000000..b96e6c1e0 --- /dev/null +++ b/src/safeds/data/image/containers/__init__.py @@ -0,0 +1,7 @@ +"""Classes that can store image data.""" + +from ._image import Image + +__all__ = [ + "Image", +] diff --git a/src/safeds/data/image/containers/_image.py b/src/safeds/data/image/containers/_image.py new file mode 100644 index 000000000..dbe440fe5 --- /dev/null +++ b/src/safeds/data/image/containers/_image.py @@ -0,0 +1,153 @@ +from __future__ import annotations + +import io +from pathlib import Path +from typing import BinaryIO + +from PIL.Image import Image as PillowImage +from PIL.Image import open as open_image + +from safeds.data.image.typing import ImageFormat + + +class Image: + """ + A container for image data. + + Parameters + ---------- + data : BinaryIO + The image data as bytes. + """ + + @staticmethod + def from_jpeg_file(path: str) -> Image: + """ + Create an image from a JPEG file. + + Parameters + ---------- + path : str + The path to the JPEG file. + + Returns + ------- + image : Image + The image. + """ + return Image( + data=Path(path).open("rb"), + format_=ImageFormat.JPEG, + ) + + @staticmethod + def from_png_file(path: str) -> Image: + """ + Create an image from a PNG file. + + Parameters + ---------- + path : str + The path to the PNG file. + + Returns + ------- + image : Image + The image. + """ + return Image( + data=Path(path).open("rb"), + format_=ImageFormat.PNG, + ) + + def __init__(self, data: BinaryIO, format_: ImageFormat): + data.seek(0) + + self._image: PillowImage = open_image(data, formats=[format_.value]) + self._format: ImageFormat = format_ + + # ------------------------------------------------------------------------------------------------------------------ + # Properties + # ------------------------------------------------------------------------------------------------------------------ + + @property + def format(self) -> ImageFormat: + """ + Get the image format. + + Returns + ------- + format : ImageFormat + The image format. + """ + return self._format + + # ------------------------------------------------------------------------------------------------------------------ + # Conversion + # ------------------------------------------------------------------------------------------------------------------ + + def to_jpeg_file(self, path: str) -> None: + """ + Save the image as a JPEG file. + + Parameters + ---------- + path : str + The path to the JPEG file. + """ + Path(path).parent.mkdir(parents=True, exist_ok=True) + self._image.save(path, format="jpeg") + + def to_png_file(self, path: str) -> None: + """ + Save the image as a PNG file. + + Parameters + ---------- + path : str + The path to the PNG file. + """ + Path(path).parent.mkdir(parents=True, exist_ok=True) + self._image.save(path, format="png") + + # ------------------------------------------------------------------------------------------------------------------ + # IPython integration + # ------------------------------------------------------------------------------------------------------------------ + + def _repr_jpeg_(self) -> bytes | None: + """ + Return a JPEG image as bytes. + + If the image is not a JPEG, return None. + + Returns + ------- + jpeg : bytes + The image as JPEG. + """ + if self._format != ImageFormat.JPEG: + return None + + buffer = io.BytesIO() + self._image.save(buffer, format="jpeg") + buffer.seek(0) + return buffer.read() + + def _repr_png_(self) -> bytes | None: + """ + Return a PNG image as bytes. + + If the image is not a PNG, return None. + + Returns + ------- + png : bytes + The image as PNG. + """ + if self._format != ImageFormat.PNG: + return None + + buffer = io.BytesIO() + self._image.save(buffer, format="png") + buffer.seek(0) + return buffer.read() diff --git a/src/safeds/data/image/typing/__init__.py b/src/safeds/data/image/typing/__init__.py new file mode 100644 index 000000000..fbffdf0c8 --- /dev/null +++ b/src/safeds/data/image/typing/__init__.py @@ -0,0 +1,7 @@ +"""Types used to distinguish different image formats.""" + +from ._image_format import ImageFormat + +__all__ = [ + "ImageFormat", +] diff --git a/src/safeds/data/image/typing/_image_format.py b/src/safeds/data/image/typing/_image_format.py new file mode 100644 index 000000000..3b5ecd49f --- /dev/null +++ b/src/safeds/data/image/typing/_image_format.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class ImageFormat(Enum): + """Images formats supported by us.""" + + JPEG = "jpeg" + PNG = "png" diff --git a/src/safeds/data/tabular/containers/_column.py b/src/safeds/data/tabular/containers/_column.py index 03675223e..8b8cc7fab 100644 --- a/src/safeds/data/tabular/containers/_column.py +++ b/src/safeds/data/tabular/containers/_column.py @@ -1,5 +1,6 @@ from __future__ import annotations +import io from numbers import Number from typing import TYPE_CHECKING, Any @@ -9,6 +10,8 @@ import seaborn as sns from IPython.core.display_functions import DisplayHandle, display +from safeds.data.image.containers import Image +from safeds.data.image.typing import ImageFormat from safeds.data.tabular.typing import ColumnType from safeds.exceptions import ( ColumnLengthMismatchError, @@ -470,10 +473,15 @@ def variance(self) -> float: # Plotting # ------------------------------------------------------------------------------------------------------------------ - def boxplot(self) -> None: + def boxplot(self) -> Image: """ Plot this column in a boxplot. This function can only plot real numerical data. + Returns + ------- + plot: Image + The plot as an image. + Raises ------ TypeError @@ -487,13 +495,28 @@ def boxplot(self) -> None: "The column contains complex data. Boxplots cannot plot the imaginary part of complex " "data. Please provide a Column with only real numbers", ) + + fig = plt.figure() ax = sns.boxplot(data=self._data) ax.set(xlabel=self.name) plt.tight_layout() - plt.show() - def histogram(self) -> None: - """Plot a column in a histogram.""" + buffer = io.BytesIO() + fig.savefig(buffer, format="png") + plt.close() # Prevents the figure from being displayed directly + buffer.seek(0) + return Image(buffer, ImageFormat.PNG) + + def histogram(self) -> Image: + """ + Plot a column in a histogram. + + Returns + ------- + plot: Image + The plot as an image. + """ + fig = plt.figure() ax = sns.histplot(data=self._data) ax.set_xticks(ax.get_xticks()) ax.set(xlabel=self.name) @@ -503,23 +526,17 @@ def histogram(self) -> None: horizontalalignment="right", ) # rotate the labels of the x Axis to prevent the chance of overlapping of the labels plt.tight_layout() - plt.show() + + buffer = io.BytesIO() + fig.savefig(buffer, format="png") + plt.close() # Prevents the figure from being displayed directly + buffer.seek(0) + return Image(buffer, ImageFormat.PNG) # ------------------------------------------------------------------------------------------------------------------ - # Other + # IPython integration # ------------------------------------------------------------------------------------------------------------------ - def _count_missing_values(self) -> int: - """ - Return the number of null values in the column. - - Returns - ------- - count : int - The number of null values. - """ - return self._data.isna().sum() - def _ipython_display_(self) -> DisplayHandle: """ Return a display object for the column to be used in Jupyter Notebooks. @@ -534,3 +551,18 @@ def _ipython_display_(self) -> DisplayHandle: with pd.option_context("display.max_rows", tmp.shape[0], "display.max_columns", tmp.shape[1]): return display(tmp) + + # ------------------------------------------------------------------------------------------------------------------ + # Other + # ------------------------------------------------------------------------------------------------------------------ + + def _count_missing_values(self) -> int: + """ + Return the number of null values in the column. + + Returns + ------- + count : int + The number of null values. + """ + return self._data.isna().sum() diff --git a/src/safeds/data/tabular/containers/_row.py b/src/safeds/data/tabular/containers/_row.py index f913f003b..2d954dc60 100644 --- a/src/safeds/data/tabular/containers/_row.py +++ b/src/safeds/data/tabular/containers/_row.py @@ -165,7 +165,7 @@ def count(self) -> int: return len(self._data) # ------------------------------------------------------------------------------------------------------------------ - # Other + # IPython integration # ------------------------------------------------------------------------------------------------------------------ def _ipython_display_(self) -> DisplayHandle: diff --git a/src/safeds/data/tabular/containers/_table.py b/src/safeds/data/tabular/containers/_table.py index cfd093cb1..6bcf1ec3c 100644 --- a/src/safeds/data/tabular/containers/_table.py +++ b/src/safeds/data/tabular/containers/_table.py @@ -1,6 +1,7 @@ from __future__ import annotations import functools +import io from pathlib import Path from typing import TYPE_CHECKING, Any @@ -12,6 +13,8 @@ from pandas import DataFrame, Series from scipy import stats +from safeds.data.image.containers import Image +from safeds.data.image.typing import ImageFormat from safeds.data.tabular.typing import ColumnType, Schema from safeds.exceptions import ( ColumnLengthMismatchError, @@ -942,10 +945,18 @@ def transform_column(self, name: str, transformer: Callable[[Row], Any]) -> Tabl # Plotting # ------------------------------------------------------------------------------------------------------------------ - def correlation_heatmap(self) -> None: - """Plot a correlation heatmap for all numerical columns of this `Table`.""" + def correlation_heatmap(self) -> Image: + """ + Plot a correlation heatmap for all numerical columns of this `Table`. + + Returns + ------- + plot: Image + The plot as an image. + """ only_numerical = self.remove_columns_with_non_numerical_values() + fig = plt.figure() sns.heatmap( data=only_numerical._data.corr(), vmin=-1, @@ -955,9 +966,14 @@ def correlation_heatmap(self) -> None: cmap="vlag", ) plt.tight_layout() - plt.show() - def lineplot(self, x_column_name: str, y_column_name: str) -> None: + buffer = io.BytesIO() + fig.savefig(buffer, format="png") + plt.close() # Prevents the figure from being displayed directly + buffer.seek(0) + return Image(buffer, format_=ImageFormat.PNG) + + def lineplot(self, x_column_name: str, y_column_name: str) -> Image: """ Plot two columns against each other in a lineplot. @@ -971,6 +987,11 @@ def lineplot(self, x_column_name: str, y_column_name: str) -> None: y_column_name : str The column name of the column to be plotted on the y-Axis. + Returns + ------- + plot: Image + The plot as an image. + Raises ------ UnknownColumnNameError @@ -981,6 +1002,7 @@ def lineplot(self, x_column_name: str, y_column_name: str) -> None: if not self.has_column(y_column_name): raise UnknownColumnNameError([y_column_name]) + fig = plt.figure() ax = sns.lineplot( data=self._data, x=self._schema._get_column_index_by_name(x_column_name), @@ -994,9 +1016,14 @@ def lineplot(self, x_column_name: str, y_column_name: str) -> None: horizontalalignment="right", ) # rotate the labels of the x Axis to prevent the chance of overlapping of the labels plt.tight_layout() - plt.show() - def scatterplot(self, x_column_name: str, y_column_name: str) -> None: + buffer = io.BytesIO() + fig.savefig(buffer, format="png") + plt.close() # Prevents the figure from being displayed directly + buffer.seek(0) + return Image(buffer, format_=ImageFormat.PNG) + + def scatterplot(self, x_column_name: str, y_column_name: str) -> Image: """ Plot two columns against each other in a scatterplot. @@ -1007,6 +1034,11 @@ def scatterplot(self, x_column_name: str, y_column_name: str) -> None: y_column_name : str The column name of the column to be plotted on the y-Axis. + Returns + ------- + plot: Image + The plot as an image. + Raises ------ UnknownColumnNameError @@ -1017,6 +1049,7 @@ def scatterplot(self, x_column_name: str, y_column_name: str) -> None: if not self.has_column(y_column_name): raise UnknownColumnNameError([y_column_name]) + fig = plt.figure() ax = sns.scatterplot( data=self._data, x=self._schema._get_column_index_by_name(x_column_name), @@ -1030,7 +1063,12 @@ def scatterplot(self, x_column_name: str, y_column_name: str) -> None: horizontalalignment="right", ) # rotate the labels of the x Axis to prevent the chance of overlapping of the labels plt.tight_layout() - plt.show() + + buffer = io.BytesIO() + fig.savefig(buffer, format="png") + plt.close() # Prevents the figure from being displayed directly + buffer.seek(0) + return Image(buffer, format_=ImageFormat.PNG) # ------------------------------------------------------------------------------------------------------------------ # Conversion @@ -1093,7 +1131,7 @@ def to_rows(self) -> list[Row]: return [Row(series_row, self._schema) for (_, series_row) in self._data.iterrows()] # ------------------------------------------------------------------------------------------------------------------ - # Other + # IPython integration # ------------------------------------------------------------------------------------------------------------------ def _ipython_display_(self) -> DisplayHandle: diff --git a/src/safeds/data/tabular/containers/_tagged_table.py b/src/safeds/data/tabular/containers/_tagged_table.py index 8c750b8ed..959f59856 100644 --- a/src/safeds/data/tabular/containers/_tagged_table.py +++ b/src/safeds/data/tabular/containers/_tagged_table.py @@ -73,7 +73,7 @@ def target(self) -> Column: return self._target # ------------------------------------------------------------------------------------------------------------------ - # Other + # IPython integration # ------------------------------------------------------------------------------------------------------------------ def _ipython_display_(self) -> DisplayHandle: diff --git a/tests/resources/image/white_square.jpg b/tests/resources/image/white_square.jpg new file mode 100644 index 0000000000000000000000000000000000000000..16b86aae8a2e41074df1a558aa02b7e62c7d73f8 GIT binary patch literal 4083 zcmeHKU2NM_6#ksFTG=|S5qscgNR~Gcg2r~7wlvn#YE9EE(=w_wh;1Mt$8i#?iEV6m zNs0tKAngT-Ng(m_LV*W(<^djfU=q9{Au$03gcvV?1VRFVkYF}5YST5=HqRFI&2>Fh78V!y<1Mw-a2kfM@vTPP^3+yZ2!pGH87&EXXyUJ@ zg>(5;K4ZHEKP%3PvOwN$xuO=9SGvCRD=l=BY&M%>Gb!3`RZ65%DM?l&MTwI|+-qCD z){0x+(O!gQ!_!^U@lD&}SwyX5Z}@3}-rPy4;q=4qy7r!-Q?q^BtJ!;)_oqX_dQP?8`+D7w+Hb>@NlWkr@dmU+8uR@%E-vbrHlwQowBRMvd$KaKm#SD9%i7pjiE6B#&U?7Hnz zPLy|2;0JszlUa4`idm-#uW)jS&t)@niPYR&ToDs&cSCX2Q>N$JZo6AAYsTYUR5$rh zPgT>|-lqk}waXj2Q5L$qXIK2D<{BreBw|`PQK^`g>9=_*7_({_F0b&4D2p>a`)19c zC_7d(B$z}mmRN;#a*%eW7tRe;W6zvQoWcHoWq}`!kpd$HMhc7+7%4DP;Qyz<;IwU6 zbckxwSvvR$)2qD`D4!H%%rE8gV|TODJEpGQ_3ZX@czf)Jr*C=b$3K>y48Dz&>G<^6 zYT?0^+?g!3B0!{A*BjoL3^Xj?W#_4L=P&U4zN5Zk3;^?Q=vg7&pidobra$@7 z80hL%l1fN%lS{z(*Fo?r?eV_nf$Nup;Lq2B;QH&-*nI_Du?GJ37BK%C@s|hOu~&gZ z&j9ayFyKlr0`Fb|ZrgS=R}0_6y3I(1dedLt1P-1DZv7b8|42tOnq%{L<1?W61Z~wv zz{{@y_mKUeM}sd(Bi7~K%wR!o#y|88t{^r+rO8FP7$Pw)8smZwsd465@EZBfTfg(7to?Q{!n+ZNKphA_&@ zRs&27g(0p`7z)KO<*5Y%QVskxV?@W!4x$L{HKMLO8(|BZK!CP1aKO<}>hv_!dh}kj zus~%HN$4O11PT#DL53$qBP#nP;eTlxM-iDK)EZH@&5oE@4j_409!8)AD#XBfePo$= z^NcZE8PR|sgh?EaMx$6%hq0U=*XZ?noY3N0tr{}ae2ft&QO)p~l8b~7GvGZO9TsSo zK_s74H5(C(C<<|8R#_-K=bYhZqKA3FMJkMIFalpl4hL9)l-1;Pu(^)L}LNnIppHg4V-3j@3i@N9(hfZ}?<2$^y& z;Ay0Z1C#*F@G+uVLaS2~x$xuE56$4vK>@=wz|;kkw?ebF7aJLX0mf*@Re&P5Byqe!g0Z zX=JW+Z={_TLC|fcLxAC7jTzCUSjD3;`D6!dEa*OABHb%#4N-pJqP;>Od3y_B{7d$w zy9q|5;(mjn;caSVVX`OpJO+a!2zIiIHvHqIRzJUlMXPjhB!FK zo>{HF4o77Z4XYuk3KWX(-A=gS&gV@>a`Ghpw)u>PX)Qr@irOH;2+giy6^7a@F}^fdyhTZc!SdXWjoTvY*}rZcYZPJ&+@R9{xAYWSUofv@naV_!~!{`BRk=YP%~E&U-p(9&_L zb9LvT+e1Fpt4G(2zx0in!CpT7W_bp=w)sJw?$SnO%X41m1oiK|Z}D{_7oVQ|Jl>yu zvUN1OIJex=o^{ameY5xVJ+{{FwyvJQ#57y=_it@|@BVW4t(J8c#6Ojn(;E-1xIKQA zICt>;L|Vp~n#mF6{(*hlkJohX3jDPDqbtYLhnvQ2cSO5q+o-F5s6XrNdkrZk)}N|0 zU8_Fh@7Q@@Y;8YQ6-hn2^J>nN`nAI8WByMLkAtt_3u$@SQo$<}Sf=zS#iNDwaC3## iT;xpa@;sQHE>xsWrA)c+*FGh^zO2Qi<`bKCH2(*v`e$DN literal 0 HcmV?d00001 diff --git a/tests/safeds/data/image/__init__.py b/tests/safeds/data/image/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/safeds/data/image/containers/__init__.py b/tests/safeds/data/image/containers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/safeds/data/image/containers/test_image.py b/tests/safeds/data/image/containers/test_image.py new file mode 100644 index 000000000..2c0d5b622 --- /dev/null +++ b/tests/safeds/data/image/containers/test_image.py @@ -0,0 +1,122 @@ +from pathlib import Path +from tempfile import NamedTemporaryFile + +import pytest +from safeds.data.image.containers import Image +from safeds.data.image.typing import ImageFormat + +from tests.helpers import resolve_resource_path + + +class TestFromJpegFile: + @pytest.mark.parametrize( + "path", + ["image/white_square.jpg"], + ) + def test_should_load_jpeg_file(self, path: str) -> None: + Image.from_jpeg_file(resolve_resource_path(path)) + + @pytest.mark.parametrize( + "path", + ["image/missing_file.jpg"], + ) + def test_should_raise_if_file_not_found(self, path: str) -> None: + with pytest.raises(FileNotFoundError): + Image.from_jpeg_file(resolve_resource_path(path)) + + +class TestFromPngFile: + @pytest.mark.parametrize( + "path", + ["image/white_square.png"], + ) + def test_should_load_png_file(self, path: str) -> None: + Image.from_png_file(resolve_resource_path(path)) + + @pytest.mark.parametrize( + "path", + ["image/missing_file.png"], + ) + def test_should_raise_if_file_not_found(self, path: str) -> None: + with pytest.raises(FileNotFoundError): + Image.from_png_file(resolve_resource_path(path)) + + +class TestFormat: + @pytest.mark.parametrize( + ("image", "format_"), + [ + (Image.from_jpeg_file(resolve_resource_path("image/white_square.jpg")), ImageFormat.JPEG), + (Image.from_png_file(resolve_resource_path("image/white_square.png")), ImageFormat.PNG), + ], + ) + def test_should_return_correct_format(self, image: Image, format_: ImageFormat) -> None: + assert image.format == format_ + + +class TestToJpegFile: + @pytest.mark.parametrize( + "path", + ["image/white_square.jpg"], + ) + def test_should_save_jpeg_file(self, path: str) -> None: + image = Image.from_jpeg_file(resolve_resource_path(path)) + + with NamedTemporaryFile() as tmp_file: + tmp_file.close() + with Path(tmp_file.name).open("wb") as tmp_write_file: + image.to_jpeg_file(tmp_write_file.name) + with Path(tmp_file.name).open("rb") as tmp_read_file: + image_read_back = Image.from_jpeg_file(tmp_read_file.name) + + assert image._image.tobytes() == image_read_back._image.tobytes() + + +class TestToPngFile: + @pytest.mark.parametrize( + "path", + ["image/white_square.png"], + ) + def test_should_save_png_file(self, path: str) -> None: + image = Image.from_png_file(resolve_resource_path(path)) + + with NamedTemporaryFile() as tmp_file: + tmp_file.close() + with Path(tmp_file.name).open("wb") as tmp_write_file: + image.to_png_file(tmp_write_file.name) + with Path(tmp_file.name).open("rb") as tmp_read_file: + image_read_back = Image.from_png_file(tmp_read_file.name) + + assert image._image.tobytes() == image_read_back._image.tobytes() + + +class TestReprJpeg: + @pytest.mark.parametrize( + "image", + [Image.from_jpeg_file(resolve_resource_path("image/white_square.jpg"))], + ) + def test_should_return_bytes_if_image_is_jpeg(self, image: Image) -> None: + assert isinstance(image._repr_jpeg_(), bytes) + + @pytest.mark.parametrize( + "image", + [Image.from_png_file(resolve_resource_path("image/white_square.png"))], + ) + def test_should_return_none_if_image_is_not_jpeg(self, image: Image) -> None: + assert image._repr_jpeg_() is None + + +class TestReprPng: + @pytest.mark.parametrize( + "image", + [Image.from_png_file(resolve_resource_path("image/white_square.png"))], + ) + def test_should_return_bytes_if_image_is_png(self, image: Image) -> None: + assert isinstance(image._repr_png_(), bytes) + + @pytest.mark.parametrize( + "image", + [Image.from_jpeg_file(resolve_resource_path("image/white_square.jpg"))], + ) + def test_should_return_none_if_image_is_not_png(self, image: Image) -> None: + assert image._repr_png_() is None diff --git a/tests/safeds/data/image/typing/__init__.py b/tests/safeds/data/image/typing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/safeds/data/image/typing/test_image_format.py b/tests/safeds/data/image/typing/test_image_format.py new file mode 100644 index 000000000..50756746b --- /dev/null +++ b/tests/safeds/data/image/typing/test_image_format.py @@ -0,0 +1,14 @@ +import pytest +from safeds.data.image.typing import ImageFormat + + +class TestValue: + @pytest.mark.parametrize( + ("image_format", "expected_value"), + [ + (ImageFormat.JPEG, "jpeg"), + (ImageFormat.PNG, "png"), + ], + ) + def test_should_return_correct_value(self, image_format: ImageFormat, expected_value: str) -> None: + assert image_format.value == expected_value