diff --git a/docs/Image formats.md b/docs/Image formats.md index 46e324b8..f1173bd1 100644 --- a/docs/Image formats.md +++ b/docs/Image formats.md @@ -1,7 +1,7 @@ \page imageFormats Image formats To work with costumes, libscratchcpp has to read image files from Scratch projects. -Only JPEG is currently supported, but support for PNG is coming soon. +Only JPEG and PNG are currently supported, but SVG is going to be supported in the future. # Implementing a custom image format To implement a custom image format that libscratchcpp doesn't support, diff --git a/src/imageformats/CMakeLists.txt b/src/imageformats/CMakeLists.txt index 2aee075d..878bb0c4 100644 --- a/src/imageformats/CMakeLists.txt +++ b/src/imageformats/CMakeLists.txt @@ -1,4 +1,5 @@ option(LIBSCRATCHCPP_JPEG_SUPPORT "JPEG image support" ON) +option(LIBSCRATCHCPP_PNG_SUPPORT "PNG image support" ON) add_subdirectory(stub) @@ -7,6 +8,11 @@ if (LIBSCRATCHCPP_JPEG_SUPPORT) add_subdirectory(jpeg) endif() +if (LIBSCRATCHCPP_PNG_SUPPORT) + target_compile_definitions(scratchcpp PRIVATE PNG_SUPPORT) + add_subdirectory(png) +endif() + target_sources(scratchcpp PRIVATE defaultimageformats.cpp diff --git a/src/imageformats/defaultimageformats.cpp b/src/imageformats/defaultimageformats.cpp index 775aae2a..94fdbfe6 100644 --- a/src/imageformats/defaultimageformats.cpp +++ b/src/imageformats/defaultimageformats.cpp @@ -7,6 +7,10 @@ #include "jpeg/jpegimageformatfactory.h" #endif +#ifdef PNG_SUPPORT +#include "png/pngimageformatfactory.h" +#endif + namespace libscratchcpp { @@ -18,6 +22,10 @@ class DefaultImageFormats #ifdef JPEG_SUPPORT ScratchConfiguration::registerImageFormat("jpg", std::make_shared()); #endif + +#ifdef PNG_SUPPORT + ScratchConfiguration::registerImageFormat("png", std::make_shared()); +#endif } }; diff --git a/src/imageformats/png/CMakeLists.txt b/src/imageformats/png/CMakeLists.txt new file mode 100644 index 00000000..da9b48ec --- /dev/null +++ b/src/imageformats/png/CMakeLists.txt @@ -0,0 +1,9 @@ +target_sources(scratchcpp + PRIVATE + pngimageformat.cpp + pngimageformat.h + pngimageformatfactory.cpp + pngimageformatfactory.h +) + +target_link_libraries(scratchcpp PRIVATE gd) diff --git a/src/imageformats/png/pngimageformat.cpp b/src/imageformats/png/pngimageformat.cpp new file mode 100644 index 00000000..63a86966 --- /dev/null +++ b/src/imageformats/png/pngimageformat.cpp @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "pngimageformat.h" + +using namespace libscratchcpp; + +PngImageFormat::PngImageFormat() +{ +} + +PngImageFormat::~PngImageFormat() +{ + if (m_img) + gdImageDestroy(m_img); +} + +void PngImageFormat::setData(unsigned int size, void *data) +{ + if (m_img) + gdImageDestroy(m_img); + + m_img = gdImageCreateFromPngPtr(size, data); +} + +unsigned int PngImageFormat::width() const +{ + return m_img ? gdImageSX(m_img) : 0; +} + +unsigned int PngImageFormat::height() const +{ + return m_img ? gdImageSY(m_img) : 0; +} + +Rgb PngImageFormat::colorAt(unsigned int x, unsigned int y, double scale) const +{ + if (!m_img) + return 0; + + int color = gdImageGetPixel(m_img, x / scale, y / scale); + int alpha = 127 - gdImageAlpha(m_img, color); // gdImageAlpha() returns values from 0 to 127 + + if (alpha == 127) // 127 should be the max (255) + alpha = 255; + else + alpha *= 2; + + return rgba(gdImageRed(m_img, color), gdImageGreen(m_img, color), gdImageBlue(m_img, color), alpha); +} diff --git a/src/imageformats/png/pngimageformat.h b/src/imageformats/png/pngimageformat.h new file mode 100644 index 00000000..7abad264 --- /dev/null +++ b/src/imageformats/png/pngimageformat.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include + +namespace libscratchcpp +{ + +class PngImageFormat : public IImageFormat +{ + public: + PngImageFormat(); + ~PngImageFormat(); + + void setData(unsigned int size, void *data) override; + + unsigned int width() const override; + unsigned int height() const override; + + Rgb colorAt(unsigned int x, unsigned int y, double scale) const override; + + private: + gdImagePtr m_img = nullptr; +}; + +} // namespace libscratchcpp diff --git a/src/imageformats/png/pngimageformatfactory.cpp b/src/imageformats/png/pngimageformatfactory.cpp new file mode 100644 index 00000000..279ad355 --- /dev/null +++ b/src/imageformats/png/pngimageformatfactory.cpp @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "pngimageformatfactory.h" +#include "pngimageformat.h" + +using namespace libscratchcpp; + +PngImageFormatFactory::PngImageFormatFactory() +{ +} + +std::shared_ptr PngImageFormatFactory::createInstance() const +{ + return std::make_shared(); +} diff --git a/src/imageformats/png/pngimageformatfactory.h b/src/imageformats/png/pngimageformatfactory.h new file mode 100644 index 00000000..d8aef203 --- /dev/null +++ b/src/imageformats/png/pngimageformatfactory.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +namespace libscratchcpp +{ + +class PngImageFormatFactory : public IImageFormatFactory +{ + public: + PngImageFormatFactory(); + + std::shared_ptr createInstance() const override; +}; + +} // namespace libscratchcpp diff --git a/test/image1.png b/test/image1.png new file mode 100644 index 00000000..9f5477d0 Binary files /dev/null and b/test/image1.png differ diff --git a/test/image2.png b/test/image2.png new file mode 100644 index 00000000..6be36ef8 Binary files /dev/null and b/test/image2.png differ diff --git a/test/imageformats/CMakeLists.txt b/test/imageformats/CMakeLists.txt index 12e6487c..b92341ea 100644 --- a/test/imageformats/CMakeLists.txt +++ b/test/imageformats/CMakeLists.txt @@ -25,3 +25,17 @@ target_link_libraries( ) gtest_discover_tests(jpeg_test) + +# png +add_executable( + png_test + png_test.cpp +) + +target_link_libraries( + png_test + GTest::gtest_main + scratchcpp +) + +gtest_discover_tests(png_test) diff --git a/test/imageformats/png_test.cpp b/test/imageformats/png_test.cpp new file mode 100644 index 00000000..e73fb38b --- /dev/null +++ b/test/imageformats/png_test.cpp @@ -0,0 +1,107 @@ +#include +#include + +#include "../common.h" + +using namespace libscratchcpp; + +class PngTest : public testing::Test +{ + public: + void SetUp() override + { + img1 = ScratchConfiguration::createImageFormat("png"); + img2 = ScratchConfiguration::createImageFormat("png"); + + struct stat stat_buf; + + FILE *f1 = fopen("image1.png", "r"); + ASSERT_TRUE(f1); + ASSERT_EQ(fstat(fileno(f1), &stat_buf), 0); + file1 = malloc(stat_buf.st_size); + ASSERT_TRUE(file1); + ASSERT_EQ(fread(file1, 1, stat_buf.st_size, f1), stat_buf.st_size); + img1->setData(stat_buf.st_size, file1); + + FILE *f2 = fopen("image2.png", "r"); + ASSERT_TRUE(f2); + ASSERT_EQ(fstat(fileno(f2), &stat_buf), 0); + file2 = malloc(stat_buf.st_size); + ASSERT_TRUE(file2); + ASSERT_EQ(fread(file2, 1, stat_buf.st_size, f2), stat_buf.st_size); + img2->setData(stat_buf.st_size, file2); + } + + void TearDown() override + { + if (file1) + free(file1); + + if (file2) + free(file2); + } + + std::shared_ptr img1; + std::shared_ptr img2; + void *file1 = nullptr; + void *file2 = nullptr; +}; + +TEST_F(PngTest, Width) +{ + ASSERT_EQ(img1->width(), 6); + ASSERT_EQ(img2->width(), 4); +} + +TEST_F(PngTest, Height) +{ + ASSERT_EQ(img1->height(), 3); + ASSERT_EQ(img2->height(), 6); +} + +TEST_F(PngTest, ColorAt) +{ + // image1 - without scaling + ASSERT_EQ(img1->colorAt(0, 0, 1), rgb(192, 192, 192)); + ASSERT_EQ(img1->colorAt(3, 0, 1), rgb(255, 128, 0)); + ASSERT_EQ(img1->colorAt(5, 0, 1), rgb(0, 255, 0)); + + ASSERT_EQ(img1->colorAt(0, 1, 1), rgb(0, 255, 255)); + ASSERT_EQ(img1->colorAt(3, 1, 1), rgb(255, 128, 128)); + ASSERT_EQ(img1->colorAt(5, 1, 1), rgb(255, 255, 255)); + + ASSERT_EQ(img1->colorAt(0, 2, 1), rgb(0, 0, 0)); + ASSERT_EQ(img1->colorAt(3, 2, 1), rgb(128, 128, 0)); + ASSERT_EQ(img1->colorAt(5, 2, 1), rgb(0, 128, 128)); + + // image1 - with scaling + ASSERT_EQ(img1->colorAt(0, 0, 2.5), rgb(192, 192, 192)); + ASSERT_EQ(img1->colorAt(8, 0, 2.5), rgb(255, 128, 0)); + ASSERT_EQ(img1->colorAt(13, 0, 2.5), rgb(0, 255, 0)); + + ASSERT_EQ(img1->colorAt(0, 3, 2.5), rgb(0, 255, 255)); + ASSERT_EQ(img1->colorAt(8, 3, 2.5), rgb(255, 128, 128)); + ASSERT_EQ(img1->colorAt(13, 3, 2.5), rgb(255, 255, 255)); + + ASSERT_EQ(img1->colorAt(0, 5, 2.5), rgb(0, 0, 0)); + ASSERT_EQ(img1->colorAt(8, 5, 2.5), rgb(128, 128, 0)); + ASSERT_EQ(img1->colorAt(13, 5, 2.5), rgb(0, 128, 128)); + + // image2 - without scaling + ASSERT_EQ(img2->colorAt(1, 0, 1), rgba(0, 0, 0, 0)); + ASSERT_EQ(img2->colorAt(3, 1, 1), rgb(255, 128, 128)); + ASSERT_EQ(img2->colorAt(3, 2, 1), rgba(149, 255, 149, 148)); + ASSERT_EQ(img2->colorAt(2, 2, 1), rgba(0, 0, 0, 0)); + ASSERT_EQ(img2->colorAt(0, 3, 1), rgba(0, 0, 0, 0)); + ASSERT_EQ(img2->colorAt(2, 4, 1), rgba(0, 0, 0, 0)); + ASSERT_EQ(img2->colorAt(1, 5, 1), rgba(0, 0, 0, 0)); + + // image2 - with scaling + ASSERT_EQ(img2->colorAt(3, 0, 2.5), rgba(0, 0, 0, 0)); + ASSERT_EQ(img2->colorAt(8, 3, 2.5), rgb(255, 128, 128)); + ASSERT_EQ(img2->colorAt(8, 5, 2.5), rgba(149, 255, 149, 148)); + ASSERT_EQ(img2->colorAt(5, 5, 2.5), rgba(0, 0, 0, 0)); + ASSERT_EQ(img2->colorAt(0, 8, 2.5), rgba(0, 0, 0, 0)); + ASSERT_EQ(img2->colorAt(5, 10, 2.5), rgba(0, 0, 0, 0)); + ASSERT_EQ(img2->colorAt(3, 13, 2.5), rgba(0, 0, 0, 0)); +}