/**************************************************************************/ /* test_image.h */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /**************************************************************************/ /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ /* "Software"), to deal in the Software without restriction, including */ /* without limitation the rights to use, copy, modify, merge, publish, */ /* distribute, sublicense, and/or sell copies of the Software, and to */ /* permit persons to whom the Software is furnished to do so, subject to */ /* the following conditions: */ /* */ /* The above copyright notice and this permission notice shall be */ /* included in all copies or substantial portions of the Software. */ /* */ /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ #ifndef TEST_IMAGE_H #define TEST_IMAGE_H #include "core/io/image.h" #include "core/os/os.h" #include "tests/test_utils.h" #include "thirdparty/doctest/doctest.h" namespace TestImage { TEST_CASE("[Image] Instantiation") { Ref image = memnew(Image(8, 4, false, Image::FORMAT_RGBA8)); CHECK_MESSAGE( !image->is_empty(), "An image created with specified size and format should not be empty at first."); CHECK_MESSAGE( image->is_invisible(), "A newly created image should be invisible."); CHECK_MESSAGE( !image->is_compressed(), "A newly created image should not be compressed."); CHECK(!image->has_mipmaps()); PackedByteArray image_data = image->get_data(); for (int i = 0; i < image_data.size(); i++) { CHECK_MESSAGE( image_data[i] == 0, "An image created without data specified should have its data zeroed out."); } Ref image_copy = memnew(Image()); CHECK_MESSAGE( image_copy->is_empty(), "An image created without any specified size and format be empty at first."); image_copy->copy_internals_from(image); CHECK_MESSAGE( image->get_data() == image_copy->get_data(), "Duplicated images should have the same data."); image_data = image->get_data(); Ref image_from_data = memnew(Image(8, 4, false, Image::FORMAT_RGBA8, image_data)); CHECK_MESSAGE( image->get_data() == image_from_data->get_data(), "An image created from data of another image should have the same data of the original image."); } TEST_CASE("[Image] Saving and loading") { Ref image = memnew(Image(4, 4, false, Image::FORMAT_RGBA8)); const String save_path_png = TestUtils::get_temp_path("image.png"); const String save_path_exr = TestUtils::get_temp_path("image.exr"); // Save PNG Error err; err = image->save_png(save_path_png); CHECK_MESSAGE( err == OK, "The image should be saved successfully as a .png file."); // Only available on editor builds. #ifdef TOOLS_ENABLED // Save EXR err = image->save_exr(save_path_exr, false); CHECK_MESSAGE( err == OK, "The image should be saved successfully as an .exr file."); #endif // TOOLS_ENABLED // Load using load() Ref image_load = memnew(Image()); err = image_load->load(save_path_png); CHECK_MESSAGE( err == OK, "The image should load successfully using load()."); CHECK_MESSAGE( image->get_data() == image_load->get_data(), "The loaded image should have the same data as the one that got saved."); // Load BMP Ref image_bmp = memnew(Image()); Ref f_bmp = FileAccess::open(TestUtils::get_data_path("images/icon.bmp"), FileAccess::READ, &err); REQUIRE(!f_bmp.is_null()); PackedByteArray data_bmp; data_bmp.resize(f_bmp->get_length() + 1); f_bmp->get_buffer(data_bmp.ptrw(), f_bmp->get_length()); CHECK_MESSAGE( image_bmp->load_bmp_from_buffer(data_bmp) == OK, "The BMP image should load successfully."); // Load JPG Ref image_jpg = memnew(Image()); Ref f_jpg = FileAccess::open(TestUtils::get_data_path("images/icon.jpg"), FileAccess::READ, &err); REQUIRE(!f_jpg.is_null()); PackedByteArray data_jpg; data_jpg.resize(f_jpg->get_length() + 1); f_jpg->get_buffer(data_jpg.ptrw(), f_jpg->get_length()); CHECK_MESSAGE( image_jpg->load_jpg_from_buffer(data_jpg) == OK, "The JPG image should load successfully."); // Load WebP Ref image_webp = memnew(Image()); Ref f_webp = FileAccess::open(TestUtils::get_data_path("images/icon.webp"), FileAccess::READ, &err); REQUIRE(!f_webp.is_null()); PackedByteArray data_webp; data_webp.resize(f_webp->get_length() + 1); f_webp->get_buffer(data_webp.ptrw(), f_webp->get_length()); CHECK_MESSAGE( image_webp->load_webp_from_buffer(data_webp) == OK, "The WebP image should load successfully."); // Load PNG Ref image_png = memnew(Image()); Ref f_png = FileAccess::open(TestUtils::get_data_path("images/icon.png"), FileAccess::READ, &err); REQUIRE(!f_png.is_null()); PackedByteArray data_png; data_png.resize(f_png->get_length() + 1); f_png->get_buffer(data_png.ptrw(), f_png->get_length()); CHECK_MESSAGE( image_png->load_png_from_buffer(data_png) == OK, "The PNG image should load successfully."); // Load TGA Ref image_tga = memnew(Image()); Ref f_tga = FileAccess::open(TestUtils::get_data_path("images/icon.tga"), FileAccess::READ, &err); REQUIRE(!f_tga.is_null()); PackedByteArray data_tga; data_tga.resize(f_tga->get_length() + 1); f_tga->get_buffer(data_tga.ptrw(), f_tga->get_length()); CHECK_MESSAGE( image_tga->load_tga_from_buffer(data_tga) == OK, "The TGA image should load successfully."); } TEST_CASE("[Image] Basic getters") { Ref image = memnew(Image(8, 4, false, Image::FORMAT_LA8)); CHECK(image->get_width() == 8); CHECK(image->get_height() == 4); CHECK(image->get_size() == Vector2(8, 4)); CHECK(image->get_format() == Image::FORMAT_LA8); CHECK(image->get_used_rect() == Rect2i(0, 0, 0, 0)); Ref image_get_rect = image->get_region(Rect2i(0, 0, 2, 1)); CHECK(image_get_rect->get_size() == Vector2(2, 1)); } TEST_CASE("[Image] Resizing") { Ref image = memnew(Image(8, 8, false, Image::FORMAT_RGBA8)); // Crop image->crop(4, 4); CHECK_MESSAGE( image->get_size() == Vector2(4, 4), "get_size() should return the correct size after cropping."); image->set_pixel(0, 0, Color(1, 1, 1, 1)); // Resize for (int i = 0; i < 5; i++) { Ref image_resized = memnew(Image()); image_resized->copy_internals_from(image); Image::Interpolation interpolation = static_cast(i); image_resized->resize(8, 8, interpolation); CHECK_MESSAGE( image_resized->get_size() == Vector2(8, 8), "get_size() should return the correct size after resizing."); CHECK_MESSAGE( image_resized->get_pixel(1, 1).a > 0, "Resizing an image should also affect its content."); } // shrink_x2() image->shrink_x2(); CHECK_MESSAGE( image->get_size() == Vector2(2, 2), "get_size() should return the correct size after shrink_x2()."); // resize_to_po2() Ref image_po_2 = memnew(Image(14, 28, false, Image::FORMAT_RGBA8)); image_po_2->resize_to_po2(); CHECK_MESSAGE( image_po_2->get_size() == Vector2(16, 32), "get_size() should return the correct size after resize_to_po2()."); } TEST_CASE("[Image] Modifying pixels of an image") { Ref image = memnew(Image(3, 3, false, Image::FORMAT_RGBA8)); image->set_pixel(0, 0, Color(1, 1, 1, 1)); CHECK_MESSAGE( !image->is_invisible(), "Image should not be invisible after drawing on it."); CHECK_MESSAGE( image->get_pixelv(Vector2(0, 0)).is_equal_approx(Color(1, 1, 1, 1)), "Image's get_pixel() should return the same color value as the one being set with set_pixel() in the same position."); CHECK_MESSAGE( image->get_used_rect() == Rect2i(0, 0, 1, 1), "Image's get_used_rect should return the expected value, larger than Rect2i(0, 0, 0, 0) if it's visible."); image->set_pixelv(Vector2(0, 0), Color(0.5, 0.5, 0.5, 0.5)); Ref image2 = memnew(Image(3, 3, false, Image::FORMAT_RGBA8)); // Fill image with color image2->fill(Color(0.5, 0.5, 0.5, 0.5)); for (int y = 0; y < image2->get_height(); y++) { for (int x = 0; x < image2->get_width(); x++) { CHECK_MESSAGE( image2->get_pixel(x, y).r > 0.49, "fill() should colorize all pixels of the image."); } } // Fill rect with color { const int img_width = 3; const int img_height = 3; Vector rects; rects.push_back(Rect2i()); rects.push_back(Rect2i(-5, -5, 3, 3)); rects.push_back(Rect2i(img_width, 0, 12, 12)); rects.push_back(Rect2i(0, img_height, 12, 12)); rects.push_back(Rect2i(img_width + 1, img_height + 2, 12, 12)); rects.push_back(Rect2i(1, 1, 1, 1)); rects.push_back(Rect2i(0, 1, 2, 3)); rects.push_back(Rect2i(-5, 0, img_width + 10, 2)); rects.push_back(Rect2i(0, -5, 2, img_height + 10)); rects.push_back(Rect2i(-1, -1, img_width + 1, img_height + 1)); for (const Rect2i &rect : rects) { Ref img = memnew(Image(img_width, img_height, false, Image::FORMAT_RGBA8)); img->fill_rect(rect, Color(1, 1, 1, 1)); for (int y = 0; y < img->get_height(); y++) { for (int x = 0; x < img->get_width(); x++) { if (rect.abs().has_point(Point2(x, y))) { CHECK_MESSAGE( img->get_pixel(x, y).is_equal_approx(Color(1, 1, 1, 1)), "fill_rect() should colorize all image pixels within rect bounds."); } else { CHECK_MESSAGE( !img->get_pixel(x, y).is_equal_approx(Color(1, 1, 1, 1)), "fill_rect() shouldn't colorize any image pixel out of rect bounds."); } } } } } // Blend two images together image->blend_rect(image2, Rect2i(Vector2i(0, 0), image2->get_size()), Vector2i(0, 0)); CHECK_MESSAGE( image->get_pixel(0, 0).a > 0.7, "blend_rect() should blend the alpha values of the two images."); CHECK_MESSAGE( image->get_used_rect().size == image->get_size(), "get_used_rect() should return the expected value, its Rect size should be the same as get_size() if there are no transparent pixels."); Ref image3 = memnew(Image(2, 2, false, Image::FORMAT_RGBA8)); image3->set_pixel(0, 0, Color(0, 1, 0, 1)); //blit_rect() two images together image->blit_rect(image3, Rect2i(Vector2i(0, 0), image3->get_size()), Vector2i(0, 0)); CHECK_MESSAGE( image->get_pixel(0, 0).is_equal_approx(Color(0, 1, 0, 1)), "blit_rect() should replace old colors and not blend them."); CHECK_MESSAGE( !image->get_pixel(2, 2).is_equal_approx(Color(0, 1, 0, 1)), "blit_rect() should not affect the area of the image that is outside src_rect."); // Flip image image3->flip_x(); CHECK(image3->get_pixel(1, 0).is_equal_approx(Color(0, 1, 0, 1))); CHECK_MESSAGE( image3->get_pixel(0, 0).is_equal_approx(Color(0, 0, 0, 0)), "flip_x() should not leave old pixels behind."); image3->flip_y(); CHECK(image3->get_pixel(1, 1).is_equal_approx(Color(0, 1, 0, 1))); CHECK_MESSAGE( image3->get_pixel(1, 0).is_equal_approx(Color(0, 0, 0, 0)), "flip_y() should not leave old pixels behind."); // Pre-multiply Alpha then Convert from RGBA to L8, checking alpha { Ref gray_image = memnew(Image(3, 3, false, Image::FORMAT_RGBA8)); gray_image->fill_rect(Rect2i(0, 0, 3, 3), Color(1, 1, 1, 0)); gray_image->set_pixel(1, 1, Color(1, 1, 1, 1)); gray_image->set_pixel(1, 2, Color(0.5, 0.5, 0.5, 0.5)); gray_image->set_pixel(2, 1, Color(0.25, 0.05, 0.5, 1.0)); gray_image->set_pixel(2, 2, Color(0.5, 0.25, 0.95, 0.75)); gray_image->premultiply_alpha(); gray_image->convert(Image::FORMAT_L8); CHECK_MESSAGE(gray_image->get_pixel(0, 0).is_equal_approx(Color(0, 0, 0, 1)), "convert() RGBA to L8 should be black."); CHECK_MESSAGE(gray_image->get_pixel(0, 1).is_equal_approx(Color(0, 0, 0, 1)), "convert() RGBA to L8 should be black."); CHECK_MESSAGE(gray_image->get_pixel(0, 2).is_equal_approx(Color(0, 0, 0, 1)), "convert() RGBA to L8 should be black."); CHECK_MESSAGE(gray_image->get_pixel(1, 0).is_equal_approx(Color(0, 0, 0, 1)), "convert() RGBA to L8 should be black."); CHECK_MESSAGE(gray_image->get_pixel(1, 1).is_equal_approx(Color(1, 1, 1, 1)), "convert() RGBA to L8 should be white."); CHECK_MESSAGE(gray_image->get_pixel(1, 2).is_equal_approx(Color(0.250980407, 0.250980407, 0.250980407, 1)), "convert() RGBA to L8 should be around 0.250980407 (64)."); CHECK_MESSAGE(gray_image->get_pixel(2, 0).is_equal_approx(Color(0, 0, 0, 1)), "convert() RGBA to L8 should be black."); CHECK_MESSAGE(gray_image->get_pixel(2, 1).is_equal_approx(Color(0.121568628, 0.121568628, 0.121568628, 1)), "convert() RGBA to L8 should be around 0.121568628 (31)."); CHECK_MESSAGE(gray_image->get_pixel(2, 2).is_equal_approx(Color(0.266666681, 0.266666681, 0.266666681, 1)), "convert() RGBA to L8 should be around 0.266666681 (68)."); } } TEST_CASE("[Image] Custom mipmaps") { Ref image = memnew(Image(100, 100, false, Image::FORMAT_RGBA8)); REQUIRE(!image->has_mipmaps()); image->generate_mipmaps(); REQUIRE(image->has_mipmaps()); const int mipmaps = image->get_mipmap_count() + 1; REQUIRE(mipmaps == 7); // Initialize reference mipmap data. // Each byte is given value "mipmap_index * 5". { PackedByteArray data = image->get_data(); uint8_t *data_ptr = data.ptrw(); for (int mip = 0; mip < mipmaps; mip++) { int mip_offset = 0; int mip_size = 0; image->get_mipmap_offset_and_size(mip, mip_offset, mip_size); for (int i = 0; i < mip_size; i++) { data_ptr[mip_offset + i] = mip * 5; } } image->set_data(image->get_width(), image->get_height(), image->has_mipmaps(), image->get_format(), data); } // Byte format conversion. for (int format = Image::FORMAT_L8; format <= Image::FORMAT_RGBA8; format++) { Ref image_bytes = memnew(Image()); image_bytes->copy_internals_from(image); image_bytes->convert((Image::Format)format); REQUIRE(image_bytes->has_mipmaps()); PackedByteArray data = image_bytes->get_data(); const uint8_t *data_ptr = data.ptr(); for (int mip = 0; mip < mipmaps; mip++) { int mip_offset = 0; int mip_size = 0; image_bytes->get_mipmap_offset_and_size(mip, mip_offset, mip_size); for (int i = 0; i < mip_size; i++) { if (data_ptr[mip_offset + i] != mip * 5) { REQUIRE_MESSAGE(false, "Byte format conversion error."); } } } } // Floating point format conversion. for (int format = Image::FORMAT_RF; format <= Image::FORMAT_RGBAF; format++) { Ref image_rgbaf = memnew(Image()); image_rgbaf->copy_internals_from(image); image_rgbaf->convert((Image::Format)format); REQUIRE(image_rgbaf->has_mipmaps()); PackedByteArray data = image_rgbaf->get_data(); const uint8_t *data_ptr = data.ptr(); for (int mip = 0; mip < mipmaps; mip++) { int mip_offset = 0; int mip_size = 0; image_rgbaf->get_mipmap_offset_and_size(mip, mip_offset, mip_size); for (int i = 0; i < mip_size; i += 4) { float value = *(float *)(data_ptr + mip_offset + i); if (!Math::is_equal_approx(value * 255.0f, mip * 5)) { REQUIRE_MESSAGE(false, "Floating point conversion error."); } } } } } TEST_CASE("[Image] Convert image") { for (int format = Image::FORMAT_RF; format < Image::FORMAT_RGBE9995; format++) { for (int new_format = Image::FORMAT_RF; new_format < Image::FORMAT_RGBE9995; new_format++) { Ref image = memnew(Image(4, 4, false, (Image::Format)format)); image->convert((Image::Format)new_format); String format_string = Image::format_names[(Image::Format)format]; String new_format_string = Image::format_names[(Image::Format)new_format]; format_string = "Error converting from " + format_string + " to " + new_format_string + "."; CHECK_MESSAGE(image->get_format() == new_format, format_string); } } Ref image = memnew(Image(4, 4, false, Image::FORMAT_RGBA8)); PackedByteArray image_data = image->get_data(); ERR_PRINT_OFF; image->convert((Image::Format)-1); ERR_PRINT_ON; CHECK_MESSAGE(image->get_data() == image_data, "Image conversion to invalid type (-1) should not alter image."); Ref image2 = memnew(Image(4, 4, false, Image::FORMAT_RGBA8)); image_data = image2->get_data(); ERR_PRINT_OFF; image2->convert((Image::Format)(Image::FORMAT_MAX + 1)); ERR_PRINT_ON; CHECK_MESSAGE(image2->get_data() == image_data, "Image conversion to invalid type (Image::FORMAT_MAX + 1) should not alter image."); } } // namespace TestImage #endif // TEST_IMAGE_H