From fff297a32c880c00b9a51eac8a1d8c03925bb013 Mon Sep 17 00:00:00 2001 From: Chong Kai Xiong Date: Tue, 24 Jan 2023 02:06:19 +0800 Subject: [PATCH 01/10] Core (Build): Require zlib 1.2 and libpng 1.4. --- libvisual/CMakeLists.txt | 5 ++++- libvisual/libvisual/CMakeLists.txt | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/libvisual/CMakeLists.txt b/libvisual/CMakeLists.txt index 4409400c..c1706c96 100644 --- a/libvisual/CMakeLists.txt +++ b/libvisual/CMakeLists.txt @@ -82,8 +82,11 @@ IF(NOT HAVE_STDC_MATH) MESSAGE(FATAL_ERROR "Libvisual requires libm to build") ENDIF() +# Check for Zlib +FIND_PACKAGE(ZLIB 1.2 REQUIRED) + # Check for libpng -FIND_PACKAGE(PNG REQUIRED) +FIND_PACKAGE(PNG 1.4 REQUIRED) # Internationalization SET(GETTEXT_PACKAGE "libvisual-${LV_VERSION_SUFFIX}") diff --git a/libvisual/libvisual/CMakeLists.txt b/libvisual/libvisual/CMakeLists.txt index c784cf71..cf089d28 100644 --- a/libvisual/libvisual/CMakeLists.txt +++ b/libvisual/libvisual/CMakeLists.txt @@ -176,6 +176,7 @@ TARGET_LINK_LIBRARIES(libvisual PkgConfig::ORC PNG::PNG Threads::Threads + ZLIB::ZLIB ) IF(WIN32) From eaa1d8e19ddafc35baeeb96d2ee82d4b20579c72 Mon Sep 17 00:00:00 2001 From: Chong Kai Xiong Date: Sat, 1 Apr 2023 07:32:00 +0800 Subject: [PATCH 02/10] Core (LV::Video): Implement saving to PNG files. --- libvisual/libvisual/lv_video.cpp | 19 +++ libvisual/libvisual/lv_video.h | 11 ++ libvisual/libvisual/lv_video_c.cpp | 8 + libvisual/libvisual/private/lv_video_png.cpp | 146 ++++++++++++++++++- libvisual/libvisual/private/lv_video_png.hpp | 6 +- 5 files changed, 184 insertions(+), 6 deletions(-) diff --git a/libvisual/libvisual/lv_video.cpp b/libvisual/libvisual/lv_video.cpp index 9bb5f1b1..50a37232 100644 --- a/libvisual/libvisual/lv_video.cpp +++ b/libvisual/libvisual/lv_video.cpp @@ -37,10 +37,14 @@ #include "private/lv_video_bmp.hpp" #include "private/lv_video_png.hpp" #include +#include #include +#include namespace LV { + namespace fs = std::filesystem; + namespace { bool is_valid_scale_method (VisVideoScaleMethod scale_method) @@ -260,6 +264,21 @@ namespace LV { return m_impl->buffer->is_allocated (); } + bool Video::save_to_file (std::string const& path) const + { + auto extension = fs::path {path}.extension (); + + std::ofstream file {path}; + + if (extension == ".png") { + return bitmap_save_png (*this, file); + } + + visual_log (VISUAL_LOG_ERROR, "Unsupported format with extension '%s'.", extension.c_str ()); + + return false; + } + void Video::copy_attrs (VideoConstPtr const& src) { set_depth (src->m_impl->depth); diff --git a/libvisual/libvisual/lv_video.h b/libvisual/libvisual/lv_video.h index 793375d0..77386cc1 100644 --- a/libvisual/libvisual/lv_video.h +++ b/libvisual/libvisual/lv_video.h @@ -259,6 +259,15 @@ namespace LV { */ BufferPtr get_buffer () const; + /** + * Saves contents to a file. + * + * @param path Path name of file to save to. Specify the file format by using the appropriate extension. + * + * @return true if file was successfully saved, false otherwise. + */ + bool save_to_file (std::string const& path) const; + /** * Sets all attributes. * @@ -513,6 +522,8 @@ LV_NODISCARD LV_API VisVideo *visual_video_new_with_buffer (int width, int heigh LV_NODISCARD LV_API VisVideo *visual_video_new_wrap_buffer (void *buffer, int owner, int width, int height, VisVideoDepth depth, int pitch); LV_NODISCARD LV_API VisVideo *visual_video_load_from_file (const char *path); +LV_API int visual_video_save_to_file (VisVideo *video); + LV_API void visual_video_ref (VisVideo *video); LV_API void visual_video_unref (VisVideo *video); diff --git a/libvisual/libvisual/lv_video_c.cpp b/libvisual/libvisual/lv_video_c.cpp index 3c423695..66619301 100644 --- a/libvisual/libvisual/lv_video_c.cpp +++ b/libvisual/libvisual/lv_video_c.cpp @@ -68,6 +68,14 @@ VisVideo *visual_video_load_from_file (const char *path) return self.get (); } +int visual_video_save_to_file (VisVideo *self, const char *path) +{ + visual_return_val_if_fail (self != nullptr, FALSE); + visual_return_val_if_fail (path != nullptr, FALSE); + + return self->save_to_file (path); +} + int visual_video_has_allocated_buffer (VisVideo *self) { visual_return_val_if_fail (self != nullptr, FALSE); diff --git a/libvisual/libvisual/private/lv_video_png.cpp b/libvisual/libvisual/private/lv_video_png.cpp index 1bea1107..242a7e77 100644 --- a/libvisual/libvisual/private/lv_video_png.cpp +++ b/libvisual/libvisual/private/lv_video_png.cpp @@ -22,9 +22,11 @@ #include "config.h" #include "lv_video_png.hpp" #include "lv_common.h" -#include -#include +#include +#include #include +#include +#include namespace LV { @@ -40,20 +42,52 @@ namespace LV { } } - void handle_png_warning (png_structp png_ptr, char const* message) + void handle_png_write (png_structp png_ptr, png_bytep data, png_size_t length) + { + auto io_ptr = png_get_io_ptr (png_ptr); + auto& output = *static_cast (io_ptr); + + if (!output.write(reinterpret_cast(data), length)) { + std::longjmp (png_jmpbuf (png_ptr), -1); + } + } + + void handle_png_flush (png_structp png_ptr) + { + auto io_ptr = png_get_io_ptr (png_ptr); + auto& output = *static_cast (io_ptr); + + output.flush (); + } + + void handle_png_load_warning (png_structp png_ptr, char const* message) { (void)png_ptr; visual_log (VISUAL_LOG_WARNING, "PNG load error: %s", message); } - void handle_png_error (png_structp png_ptr, char const* message) + void handle_png_load_error (png_structp png_ptr, char const* message) { (void)png_ptr; visual_log (VISUAL_LOG_ERROR, "PNG load error: %s", message); } + void handle_png_save_warning (png_structp png_ptr, char const* message) + { + (void)png_ptr; + + visual_log (VISUAL_LOG_WARNING, "PNG save error: %s", message); + } + + void handle_png_save_error (png_structp png_ptr, char const* message) + { + (void)png_ptr; + + visual_log (VISUAL_LOG_ERROR, "PNG save error: %s", message); + } + } // anonymous VideoPtr bitmap_load_png (std::istream& input) @@ -80,7 +114,10 @@ namespace LV { return nullptr; } - auto png_ptr = png_create_read_struct (PNG_LIBPNG_VER_STRING, nullptr, handle_png_error, handle_png_warning); + auto png_ptr = png_create_read_struct (PNG_LIBPNG_VER_STRING, + nullptr, + handle_png_load_error, + handle_png_load_warning); if (!png_ptr) { return nullptr; } @@ -212,4 +249,103 @@ namespace LV { return video; } + bool bitmap_save_png(Video const& bitmap, std::ostream &output) + { + auto saved_stream_pos = output.tellp (); + + auto png_ptr = png_create_write_struct (PNG_LIBPNG_VER_STRING, + nullptr, + handle_png_save_error, + handle_png_save_warning); + if (!png_ptr) { + visual_log (VISUAL_LOG_ERROR, "Failed to create PNG write struct."); + return false; + } + + auto info_ptr = png_create_info_struct (png_ptr); + if (!info_ptr) { + visual_log (VISUAL_LOG_ERROR, "Failed to create PNG info struct."); + png_destroy_write_struct (&png_ptr, nullptr); + return false; + } + + auto bitmap_width = bitmap.get_width (); + auto bitmap_height = bitmap.get_height (); + + std::vector pixel_row_ptrs; + pixel_row_ptrs.reserve (bitmap_height); + + for (auto y = 0; y < bitmap_height; y++) { + pixel_row_ptrs.push_back (static_cast (bitmap.get_pixel_ptr (0, y))); + } + + if (setjmp (png_jmpbuf (png_ptr))) { + visual_log (VISUAL_LOG_ERROR, "Some error occurred."); + output.seekp (saved_stream_pos); + png_destroy_write_struct (&png_ptr, &info_ptr); + } + + int bit_depth = 0; + int color_type = 0; + + switch (bitmap.get_depth ()) { + case VISUAL_VIDEO_DEPTH_8BIT: { + bit_depth = 8; + color_type = PNG_COLOR_TYPE_PALETTE; + break; + } + case VISUAL_VIDEO_DEPTH_24BIT: { + bit_depth = 8; + color_type = PNG_COLOR_TYPE_RGB; + break; + } + case VISUAL_VIDEO_DEPTH_32BIT: { + bit_depth = 8; + color_type = PNG_COLOR_TYPE_RGB_ALPHA; + break; + } + default: { + visual_log (VISUAL_LOG_ERROR, "Invalid depth or depth not supported."); + return false; + } + } + +#if VISUAL_LITTLE_ENDIAN + png_set_bgr (png_ptr); +#endif + + png_set_filter (png_ptr, 0, PNG_FILTER_NONE); + + png_set_compression_level (png_ptr, Z_BEST_COMPRESSION); + + png_set_IHDR (png_ptr, info_ptr, bitmap_width, bitmap_height, + bit_depth, color_type, PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + + if (color_type == PNG_COLOR_TYPE_PALETTE) { + auto const& colors = bitmap.get_palette ().colors; + + std::vector out_palette; + out_palette.reserve (colors.size ()); + + for (auto const& color : colors) { + out_palette.push_back ({color.r, color.g, color.b}); + } + + png_set_PLTE (png_ptr, info_ptr, out_palette.data (), out_palette.size ()); + } + + png_set_sRGB (png_ptr, info_ptr, PNG_sRGB_INTENT_PERCEPTUAL); + + png_set_write_fn (png_ptr, &output, handle_png_write, handle_png_flush); + + png_write_info (png_ptr, info_ptr); + png_write_rows (png_ptr, pixel_row_ptrs.data (), bitmap_height); + png_write_end (png_ptr, info_ptr); + + png_destroy_write_struct (&png_ptr, &info_ptr); + + return true; + } + } // LV namespace diff --git a/libvisual/libvisual/private/lv_video_png.hpp b/libvisual/libvisual/private/lv_video_png.hpp index 9019fd9c..247dc797 100644 --- a/libvisual/libvisual/private/lv_video_png.hpp +++ b/libvisual/libvisual/private/lv_video_png.hpp @@ -26,7 +26,11 @@ #include namespace LV { + VideoPtr bitmap_load_png (std::istream& input); -} + + bool bitmap_save_png (Video const& bitmap, std::ostream& output); + +} // LV namespace #endif // _LV_VIDEO_BMP_HPP From 935f97924ea6c57ed573e6b074493c99b38c97c1 Mon Sep 17 00:00:00 2001 From: Chong Kai Xiong Date: Wed, 29 Jan 2025 05:54:54 +0800 Subject: [PATCH 03/10] Core (LV::Video): Apply modern C++-isms to PNG writer. --- libvisual/libvisual/private/lv_video_png.cpp | 83 ++++++++++---------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/libvisual/libvisual/private/lv_video_png.cpp b/libvisual/libvisual/private/lv_video_png.cpp index 242a7e77..eadd8f5f 100644 --- a/libvisual/libvisual/private/lv_video_png.cpp +++ b/libvisual/libvisual/private/lv_video_png.cpp @@ -23,6 +23,7 @@ #include "lv_video_png.hpp" #include "lv_common.h" #include +#include #include #include #include @@ -34,8 +35,8 @@ namespace LV { void handle_png_read (png_structp png_ptr, png_bytep data, png_size_t length) { - auto io_ptr = png_get_io_ptr (png_ptr); - auto& input = *static_cast (io_ptr); + auto io_ptr {png_get_io_ptr (png_ptr)}; + auto& input {*static_cast (io_ptr)}; if (!input.read (reinterpret_cast (data), length)) { std::longjmp (png_jmpbuf (png_ptr), -1); @@ -44,8 +45,8 @@ namespace LV { void handle_png_write (png_structp png_ptr, png_bytep data, png_size_t length) { - auto io_ptr = png_get_io_ptr (png_ptr); - auto& output = *static_cast (io_ptr); + auto io_ptr {png_get_io_ptr (png_ptr)}; + auto& output {*static_cast (io_ptr)}; if (!output.write(reinterpret_cast(data), length)) { std::longjmp (png_jmpbuf (png_ptr), -1); @@ -54,8 +55,8 @@ namespace LV { void handle_png_flush (png_structp png_ptr) { - auto io_ptr = png_get_io_ptr (png_ptr); - auto& output = *static_cast (io_ptr); + auto io_ptr {png_get_io_ptr (png_ptr)}; + auto& output {*static_cast (io_ptr)}; output.flush (); } @@ -103,7 +104,7 @@ namespace LV { return nullptr; } - bool is_png = !png_sig_cmp (signature, 0, sizeof (signature)); + auto is_png {!png_sig_cmp (signature, 0, sizeof (signature))}; // Clean up test by rewinding to the beginning, like we have read nothing. if (!input.seekg (start_stream_pos)) { @@ -114,21 +115,21 @@ namespace LV { return nullptr; } - auto png_ptr = png_create_read_struct (PNG_LIBPNG_VER_STRING, - nullptr, - handle_png_load_error, - handle_png_load_warning); + auto png_ptr {png_create_read_struct (PNG_LIBPNG_VER_STRING, + nullptr, + handle_png_load_error, + handle_png_load_warning)}; if (!png_ptr) { return nullptr; } - auto info_ptr = png_create_info_struct (png_ptr); + auto info_ptr {png_create_info_struct (png_ptr)}; if (!info_ptr) { png_destroy_read_struct (&png_ptr, nullptr, nullptr); return nullptr; } - auto end_info = png_create_info_struct (png_ptr); + auto end_info {png_create_info_struct (png_ptr)}; if (!end_info) { png_destroy_read_struct (&png_ptr, &info_ptr, nullptr); return nullptr; @@ -161,8 +162,8 @@ namespace LV { png_read_info (png_ptr, info_ptr); - auto color_type = png_get_color_type (png_ptr, info_ptr); - auto bit_depth = png_get_bit_depth (png_ptr, info_ptr); + auto color_type {png_get_color_type (png_ptr, info_ptr)}; + auto bit_depth {png_get_bit_depth (png_ptr, info_ptr)}; if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) { png_set_expand_gray_1_2_4_to_8 (png_ptr); @@ -185,13 +186,13 @@ namespace LV { png_set_gray_to_rgb (png_ptr); } -#if VISUAL_LITTLE_ENDIAN - png_set_bgr (png_ptr); -#endif + if constexpr (std::endian::native == std::endian::little) { + png_set_bgr (png_ptr); + } png_read_update_info (png_ptr, info_ptr); - VisVideoDepth depth = VISUAL_VIDEO_DEPTH_NONE; + auto depth {VISUAL_VIDEO_DEPTH_NONE}; switch (png_get_color_type (png_ptr, info_ptr)) { case PNG_COLOR_TYPE_PALETTE: @@ -207,9 +208,9 @@ namespace LV { std::longjmp (png_jmpbuf (png_ptr), -1); } - auto width = png_get_image_width (png_ptr, info_ptr); - auto height = png_get_image_height (png_ptr, info_ptr); - auto row_stride = png_get_rowbytes (png_ptr, info_ptr); + auto width {png_get_image_width (png_ptr, info_ptr)}; + auto height {png_get_image_height (png_ptr, info_ptr)}; + auto row_stride {png_get_rowbytes (png_ptr, info_ptr)}; // NOTE: We have to use visual_mem_malloc() here as LV::Video // will free the buffer with visual_mem_free() @@ -217,7 +218,7 @@ namespace LV { pixel_row_ptrs = new uint8_t*[height]; - for (unsigned int y = 0; y < height; y++) { + for (unsigned int y {0}; y < height; y++) { pixel_row_ptrs[y] = pixels + y * row_stride; } @@ -249,34 +250,34 @@ namespace LV { return video; } - bool bitmap_save_png(Video const& bitmap, std::ostream &output) + bool bitmap_save_png(Video const& bitmap, std::ostream& output) { - auto saved_stream_pos = output.tellp (); + auto saved_stream_pos {output.tellp ()}; - auto png_ptr = png_create_write_struct (PNG_LIBPNG_VER_STRING, - nullptr, - handle_png_save_error, - handle_png_save_warning); + auto png_ptr {png_create_write_struct (PNG_LIBPNG_VER_STRING, + nullptr, + handle_png_save_error, + handle_png_save_warning)}; if (!png_ptr) { visual_log (VISUAL_LOG_ERROR, "Failed to create PNG write struct."); return false; } - auto info_ptr = png_create_info_struct (png_ptr); + auto info_ptr {png_create_info_struct (png_ptr)}; if (!info_ptr) { visual_log (VISUAL_LOG_ERROR, "Failed to create PNG info struct."); png_destroy_write_struct (&png_ptr, nullptr); return false; } - auto bitmap_width = bitmap.get_width (); - auto bitmap_height = bitmap.get_height (); + auto bitmap_width {bitmap.get_width ()}; + auto bitmap_height {bitmap.get_height ()}; std::vector pixel_row_ptrs; pixel_row_ptrs.reserve (bitmap_height); - for (auto y = 0; y < bitmap_height; y++) { - pixel_row_ptrs.push_back (static_cast (bitmap.get_pixel_ptr (0, y))); + for (auto y {0}; y < bitmap_height; y++) { + pixel_row_ptrs.push_back (static_cast (bitmap.get_pixel_ptr (0, y))); } if (setjmp (png_jmpbuf (png_ptr))) { @@ -285,8 +286,8 @@ namespace LV { png_destroy_write_struct (&png_ptr, &info_ptr); } - int bit_depth = 0; - int color_type = 0; + auto bit_depth {0}; + auto color_type {0}; switch (bitmap.get_depth ()) { case VISUAL_VIDEO_DEPTH_8BIT: { @@ -310,9 +311,9 @@ namespace LV { } } -#if VISUAL_LITTLE_ENDIAN - png_set_bgr (png_ptr); -#endif + if constexpr (std::endian::native == std::endian::little) { + png_set_bgr (png_ptr); + } png_set_filter (png_ptr, 0, PNG_FILTER_NONE); @@ -323,13 +324,13 @@ namespace LV { PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); if (color_type == PNG_COLOR_TYPE_PALETTE) { - auto const& colors = bitmap.get_palette ().colors; + auto const& colors {bitmap.get_palette ().colors}; std::vector out_palette; out_palette.reserve (colors.size ()); for (auto const& color : colors) { - out_palette.push_back ({color.r, color.g, color.b}); + out_palette.emplace_back (color.r, color.g, color.b); } png_set_PLTE (png_ptr, info_ptr, out_palette.data (), out_palette.size ()); From e12321a1d61c2f543a7be5a1bb5c3239d9da4925 Mon Sep 17 00:00:00 2001 From: Chong Kai Xiong Date: Wed, 29 Jan 2025 05:56:44 +0800 Subject: [PATCH 04/10] Core (LV::Video): Rename bit_depth to bits_per_channel to avoid confusion. --- libvisual/libvisual/private/lv_video_png.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libvisual/libvisual/private/lv_video_png.cpp b/libvisual/libvisual/private/lv_video_png.cpp index eadd8f5f..5bbb887b 100644 --- a/libvisual/libvisual/private/lv_video_png.cpp +++ b/libvisual/libvisual/private/lv_video_png.cpp @@ -286,22 +286,22 @@ namespace LV { png_destroy_write_struct (&png_ptr, &info_ptr); } - auto bit_depth {0}; + auto bits_per_channel {0}; auto color_type {0}; switch (bitmap.get_depth ()) { case VISUAL_VIDEO_DEPTH_8BIT: { - bit_depth = 8; + bits_per_channel = 8; color_type = PNG_COLOR_TYPE_PALETTE; break; } case VISUAL_VIDEO_DEPTH_24BIT: { - bit_depth = 8; + bits_per_channel = 8; color_type = PNG_COLOR_TYPE_RGB; break; } case VISUAL_VIDEO_DEPTH_32BIT: { - bit_depth = 8; + bits_per_channel = 8; color_type = PNG_COLOR_TYPE_RGB_ALPHA; break; } @@ -320,7 +320,7 @@ namespace LV { png_set_compression_level (png_ptr, Z_BEST_COMPRESSION); png_set_IHDR (png_ptr, info_ptr, bitmap_width, bitmap_height, - bit_depth, color_type, PNG_INTERLACE_NONE, + bits_per_channel, color_type, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); if (color_type == PNG_COLOR_TYPE_PALETTE) { From 4c372e1c63ab0df072df287f1d930c5f0e9b9674 Mon Sep 17 00:00:00 2001 From: Chong Kai Xiong Date: Fri, 24 Jan 2025 18:52:25 +0800 Subject: [PATCH 05/10] Core (LV::Video): Refactor image loading code. --- libvisual/libvisual/lv_video.cpp | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/libvisual/libvisual/lv_video.cpp b/libvisual/libvisual/lv_video.cpp index 50a37232..6d9d722e 100644 --- a/libvisual/libvisual/lv_video.cpp +++ b/libvisual/libvisual/lv_video.cpp @@ -37,9 +37,10 @@ #include "private/lv_video_bmp.hpp" #include "private/lv_video_png.hpp" #include -#include -#include #include +#include +#include +#include namespace LV { @@ -47,6 +48,13 @@ namespace LV { namespace { + using BitmapLoad = std::function; + std::unordered_map const bitmap_load_map = + { + { "bmp", bitmap_load_bmp }, + { "png", bitmap_load_png } + }; + bool is_valid_scale_method (VisVideoScaleMethod scale_method) { return scale_method == VISUAL_VIDEO_SCALE_NEAREST @@ -195,17 +203,14 @@ namespace LV { VideoPtr Video::create_from_stream (std::istream& input) { - auto image = bitmap_load_bmp (input); - if (image) { - return image; - } - - image = bitmap_load_png (input); - if (image) { - return image; + for (auto entry : bitmap_load_map) { + auto image {entry.second (input)}; + if (image) { + return image; + } } - return {}; + return nullptr; } VideoPtr Video::create_scale_depth (VideoConstPtr const& src, From 45c2e298987332a05b162eafabd5f006e625bf4a Mon Sep 17 00:00:00 2001 From: Chong Kai Xiong Date: Fri, 24 Jan 2025 18:54:00 +0800 Subject: [PATCH 06/10] Core (LV::Video): Add save_to_stream() for saving images to a stream. --- libvisual/libvisual/lv_video.cpp | 52 ++++++++++++++++++++++++++++---- libvisual/libvisual/lv_video.h | 9 ++++++ 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/libvisual/libvisual/lv_video.cpp b/libvisual/libvisual/lv_video.cpp index 6d9d722e..2fd79579 100644 --- a/libvisual/libvisual/lv_video.cpp +++ b/libvisual/libvisual/lv_video.cpp @@ -36,6 +36,8 @@ #include "private/lv_video_transform.hpp" #include "private/lv_video_bmp.hpp" #include "private/lv_video_png.hpp" +#include +#include #include #include #include @@ -49,18 +51,40 @@ namespace LV { namespace { using BitmapLoad = std::function; + using BitmapSave = std::function; + std::unordered_map const bitmap_load_map = { { "bmp", bitmap_load_bmp }, { "png", bitmap_load_png } }; + std::unordered_map const bitmap_save_map = + { + { "png", bitmap_save_png } + }; + + std::unordered_map const bitmap_format_extension_map = + { + { ".png", "png" }, + { ".bmp", "bmp" } + }; + bool is_valid_scale_method (VisVideoScaleMethod scale_method) { return scale_method == VISUAL_VIDEO_SCALE_NEAREST || scale_method == VISUAL_VIDEO_SCALE_BILINEAR; } + // TODO: Factor this out into a string utility library + std::string to_lower_ascii (std::string const& s) + { + std::string result {s}; + std::transform (result.begin (), result.end (), result.begin (), + [=] (unsigned char c) { return std::tolower (c); }); + return result; + } + } // anonymous namespace @@ -271,17 +295,33 @@ namespace LV { bool Video::save_to_file (std::string const& path) const { - auto extension = fs::path {path}.extension (); + std::string extension {to_lower_ascii (fs::path {path}.extension ())}; - std::ofstream file {path}; + auto entry {bitmap_format_extension_map.find (extension)}; + if (entry == bitmap_format_extension_map.end ()) { + visual_log (VISUAL_LOG_ERROR, "Could not deduce format from filename (%s)", path.c_str ()); + return false; + } - if (extension == ".png") { - return bitmap_save_png (*this, file); + std::ofstream output {path}; + if (!output) { + visual_log (VISUAL_LOG_ERROR, "Could not create file '%s'", path.c_str ()); + return false; } - visual_log (VISUAL_LOG_ERROR, "Unsupported format with extension '%s'.", extension.c_str ()); + return save_to_stream (output, entry->second); + } + + bool Video::save_to_stream (std::ostream& output, std::string const& format) const + { + auto entry {bitmap_save_map.find (format)}; + if (entry == bitmap_save_map.end ()) { + std::string format_str {format}; + visual_log (VISUAL_LOG_ERROR, "Saving to %s format is not supported", format_str.c_str ()); + return false; + } - return false; + return entry->second (*this, output); } void Video::copy_attrs (VideoConstPtr const& src) diff --git a/libvisual/libvisual/lv_video.h b/libvisual/libvisual/lv_video.h index 77386cc1..1c4589ff 100644 --- a/libvisual/libvisual/lv_video.h +++ b/libvisual/libvisual/lv_video.h @@ -268,6 +268,15 @@ namespace LV { */ bool save_to_file (std::string const& path) const; + /** + * Saves contents to a stream. + * + * @param output stream + * + * @return true if file was successfully saved, false otherwise. + */ + bool save_to_stream (std::ostream& output, std::string const& format) const; + /** * Sets all attributes. * From 276a302ef1597dce2caa15af5c3e8c940be36fec Mon Sep 17 00:00:00 2001 From: Chong Kai Xiong Date: Fri, 24 Jan 2025 18:56:47 +0800 Subject: [PATCH 07/10] Core (Tests): Factor out raw image and raw palette loading functions into a common library. --- libvisual/tests/video_test/CMakeLists.txt | 6 ++ libvisual/tests/video_test/common.cpp | 87 +++++++++++++++++++ libvisual/tests/video_test/common.hpp | 17 ++++ .../tests/video_test/video_load_test.cpp | 86 +----------------- 4 files changed, 111 insertions(+), 85 deletions(-) create mode 100644 libvisual/tests/video_test/common.cpp create mode 100644 libvisual/tests/video_test/common.hpp diff --git a/libvisual/tests/video_test/CMakeLists.txt b/libvisual/tests/video_test/CMakeLists.txt index 4a8bad4c..35b47699 100644 --- a/libvisual/tests/video_test/CMakeLists.txt +++ b/libvisual/tests/video_test/CMakeLists.txt @@ -1,15 +1,21 @@ LV_BUILD_TEST(video_check_test SOURCES video_check_test.cpp + LINK_LIBS video_test_common ) LV_BUILD_TEST(video_blit_test SOURCES video_blit_test.cpp + LINK_LIBS video_test_common ) LV_BUILD_TEST(video_load_test SOURCES video_load_test.cpp + LINK_LIBS video_test_common ) +ADD_LIBRARY(video_test_common STATIC common.cpp) +TARGET_LINK_LIBRARIES(video_test_common PUBLIC libvisual) + IF(HAVE_SDL) LV_BUILD_TEST(video_scale_test SOURCES video_scale_test.cpp diff --git a/libvisual/tests/video_test/common.cpp b/libvisual/tests/video_test/common.cpp new file mode 100644 index 00000000..39ce8fc4 --- /dev/null +++ b/libvisual/tests/video_test/common.cpp @@ -0,0 +1,87 @@ +#include "common.hpp" +#include +#include +#include +#include + +namespace fs = std::filesystem; + +LV::VideoPtr load_raw_image (fs::path const& path, int width, int height, VisVideoDepth depth) +{ + auto image {LV::Video::create (width, height, depth)}; + + std::size_t const content_bytes_per_row = image->get_width () * image->get_bpp (); + + { + std::ifstream input {path, std::ios::binary}; + if (!input) { + return nullptr; + } + + for (int y = 0; y < image->get_height (); y++) { + auto pixel_row_ptr = static_cast (image->get_pixel_ptr (0, y)); + if (!input.read (pixel_row_ptr, content_bytes_per_row)) { + return nullptr; + } + } + } + + if constexpr (std::endian::native == std::endian::little) { + auto byteswapped_image {LV::Video::create (width, height, depth)}; + byteswapped_image->flip_pixel_bytes (image); + return byteswapped_image; + } else { + return image; + } +} + +std::optional load_raw_palette (fs::path const& path) +{ + std::vector palette_buffer (256 * 3); + { + std::ifstream input {path, std::ios::binary}; + if (!input.read (reinterpret_cast (palette_buffer.data ()), palette_buffer.size ())) { + return std::nullopt; + } + } + + LV::Palette palette; + palette.colors.reserve (256); + for (unsigned int i = 0; i < 256; i++) { + palette.colors.emplace_back (palette_buffer[i*3], palette_buffer[i*3+1], palette_buffer[i*3+2]); + } + + return palette; +} + +LV::VideoPtr load_raw_indexed_image (fs::path const& image_path, + fs::path const& palette_path, + int width, + int height) +{ + auto image {LV::Video::create (width, height, VISUAL_VIDEO_DEPTH_8BIT)}; + + std::size_t const content_bytes_per_row = image->get_width () * image->get_bpp (); + + { + std::ifstream input {image_path, std::ios::binary}; + if (!input) { + return nullptr; + } + + for (int y = 0; y < image->get_height (); y++) { + auto pixel_row_ptr = static_cast (image->get_pixel_ptr (0, y)); + if (!input.read (pixel_row_ptr, content_bytes_per_row)) { + return nullptr; + } + } + } + + auto palette {load_raw_palette (palette_path)}; + if (!palette.has_value ()) { + return nullptr; + } + image->set_palette (palette.value ()); + + return image; +} diff --git a/libvisual/tests/video_test/common.hpp b/libvisual/tests/video_test/common.hpp new file mode 100644 index 00000000..1488730b --- /dev/null +++ b/libvisual/tests/video_test/common.hpp @@ -0,0 +1,17 @@ +#ifndef LV_VIDEO_TEST_COMMON_HPP +#define LV_VIDEO_TEST_COMMON_HPP + +#include +#include +#include + +LV::VideoPtr load_raw_image (std::filesystem::path const& path, int width, int height, VisVideoDepth depth); + +std::optional load_raw_palette (std::filesystem::path const& path); + +LV::VideoPtr load_raw_indexed_image (std::filesystem::path const& image_path, + std::filesystem::path const& palette_path, + int width, + int height); + +#endif // LV_VIDEO_TEST_COMMON_HPP diff --git a/libvisual/tests/video_test/video_load_test.cpp b/libvisual/tests/video_test/video_load_test.cpp index 720d2d50..4e18112d 100644 --- a/libvisual/tests/video_test/video_load_test.cpp +++ b/libvisual/tests/video_test/video_load_test.cpp @@ -1,93 +1,9 @@ #include "test.h" +#include "video_test/common.hpp" #include -#include -#include -#include -#include namespace { - namespace fs = std::filesystem; - - LV::VideoPtr load_raw_image (fs::path const& path, int width, int height, VisVideoDepth depth) - { - auto image {LV::Video::create (width, height, depth)}; - - std::size_t const content_bytes_per_row = image->get_width () * image->get_bpp (); - - { - std::ifstream input {path, std::ios::binary}; - if (!input) { - return nullptr; - } - - for (int y = 0; y < image->get_height (); y++) { - auto pixel_row_ptr = static_cast (image->get_pixel_ptr (0, y)); - if (!input.read (pixel_row_ptr, content_bytes_per_row)) { - return nullptr; - } - } - } - - if constexpr (std::endian::native == std::endian::little) { - auto byteswapped_image {LV::Video::create (width, height, depth)}; - byteswapped_image->flip_pixel_bytes (image); - return byteswapped_image; - } else { - return image; - } - } - - std::optional load_raw_palette (fs::path const& path) - { - std::vector palette_buffer (256 * 3); - { - std::ifstream input {path, std::ios::binary}; - if (!input.read (reinterpret_cast (palette_buffer.data ()), palette_buffer.size ())) { - return std::nullopt; - } - } - - LV::Palette palette; - palette.colors.reserve (256); - for (unsigned int i = 0; i < 256; i++) { - palette.colors.emplace_back (palette_buffer[i*3], palette_buffer[i*3+1], palette_buffer[i*3+2]); - } - - return palette; - } - - LV::VideoPtr load_raw_indexed_image (fs::path const& image_path, - fs::path const& palette_path, - int width, - int height) - { - auto image {LV::Video::create (width, height, VISUAL_VIDEO_DEPTH_8BIT)}; - - std::size_t const content_bytes_per_row = image->get_width () * image->get_bpp (); - - { - std::ifstream input {image_path, std::ios::binary}; - if (!input) { - return nullptr; - } - - for (int y = 0; y < image->get_height (); y++) { - auto pixel_row_ptr = static_cast (image->get_pixel_ptr (0, y)); - if (!input.read (pixel_row_ptr, content_bytes_per_row)) { - return nullptr; - } - } - } - - auto palette {load_raw_palette (palette_path)}; - if (!palette.has_value ()) { - return nullptr; - } - image->set_palette (palette.value ()); - - return image; - } void test_load_indexed8 () { From d6b53744dcba6e1e852b0b10840ae0bac4db89ca Mon Sep 17 00:00:00 2001 From: Chong Kai Xiong Date: Fri, 24 Jan 2025 19:09:08 +0800 Subject: [PATCH 08/10] Core (Tests): Add unit test for image saving. --- libvisual/tests/video_test/CMakeLists.txt | 5 ++ .../tests/video_test/video_save_test.cpp | 53 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 libvisual/tests/video_test/video_save_test.cpp diff --git a/libvisual/tests/video_test/CMakeLists.txt b/libvisual/tests/video_test/CMakeLists.txt index 35b47699..beef643a 100644 --- a/libvisual/tests/video_test/CMakeLists.txt +++ b/libvisual/tests/video_test/CMakeLists.txt @@ -13,6 +13,11 @@ LV_BUILD_TEST(video_load_test LINK_LIBS video_test_common ) +LV_BUILD_TEST(video_save_test + SOURCES video_save_test.cpp + LINK_LIBS video_test_common +) + ADD_LIBRARY(video_test_common STATIC common.cpp) TARGET_LINK_LIBRARIES(video_test_common PUBLIC libvisual) diff --git a/libvisual/tests/video_test/video_save_test.cpp b/libvisual/tests/video_test/video_save_test.cpp new file mode 100644 index 00000000..449644e6 --- /dev/null +++ b/libvisual/tests/video_test/video_save_test.cpp @@ -0,0 +1,53 @@ +#include "test.h" +#include +#include + +void test_save_indexed8 () +{ + auto png_image {LV::Video::create_from_file ("../images/additive-colors-indexed8.png")}; + LV_TEST_ASSERT (png_image); + + std::stringstream buffer; + LV_TEST_ASSERT (png_image->save_to_stream (buffer, "png")); + + auto png_saved_image {LV::Video::create_from_stream (buffer)}; + LV_TEST_ASSERT (png_saved_image); + LV_TEST_ASSERT (png_saved_image->has_same_content (png_image)); +} + +void test_save_rgb24 () +{ + auto png_image {LV::Video::create_from_file ("../images/additive-colors-rgb24.png")}; + LV_TEST_ASSERT (png_image); + + std::stringstream buffer; + LV_TEST_ASSERT (png_image->save_to_stream (buffer, "png")); + + auto png_saved_image {LV::Video::create_from_stream (buffer)}; + LV_TEST_ASSERT (png_saved_image); + LV_TEST_ASSERT (png_saved_image->has_same_content (png_image)); +} + +void test_save_argb32 () +{ + auto png_image {LV::Video::create_from_file ("../images/additive-colors-argb32.png")}; + LV_TEST_ASSERT (png_image); + + std::stringstream buffer; + LV_TEST_ASSERT (png_image->save_to_stream (buffer, "png")); + + auto png_saved_image {LV::Video::create_from_stream (buffer)}; + LV_TEST_ASSERT (png_saved_image); + LV_TEST_ASSERT (png_saved_image->has_same_content (png_image)); +} + +int main (int argc, char* argv[]) +{ + LV::System::init (argc, argv); + + test_save_indexed8 (); + test_save_rgb24 (); + test_save_argb32 (); + + LV::System::destroy (); +} From dfbbc07176a78c42cd180c8761cc23a1c08aff02 Mon Sep 17 00:00:00 2001 From: Chong Kai Xiong Date: Fri, 24 Jan 2025 20:29:09 +0800 Subject: [PATCH 09/10] Core (LV::Video): Account for wide-character paths on Windows. --- libvisual/libvisual/lv_video.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/libvisual/libvisual/lv_video.cpp b/libvisual/libvisual/lv_video.cpp index 2fd79579..cbb8fac6 100644 --- a/libvisual/libvisual/lv_video.cpp +++ b/libvisual/libvisual/lv_video.cpp @@ -79,9 +79,9 @@ namespace LV { // TODO: Factor this out into a string utility library std::string to_lower_ascii (std::string const& s) { - std::string result {s}; - std::transform (result.begin (), result.end (), result.begin (), - [=] (unsigned char c) { return std::tolower (c); }); + auto result {s}; + for (auto& c : result) + c = std::tolower (c); return result; } @@ -295,9 +295,10 @@ namespace LV { bool Video::save_to_file (std::string const& path) const { - std::string extension {to_lower_ascii (fs::path {path}.extension ())}; + auto extension {fs::path {path}.extension ()}; + auto extension_lower (to_lower_ascii (extension.string ())); - auto entry {bitmap_format_extension_map.find (extension)}; + auto entry {bitmap_format_extension_map.find (extension_lower)}; if (entry == bitmap_format_extension_map.end ()) { visual_log (VISUAL_LOG_ERROR, "Could not deduce format from filename (%s)", path.c_str ()); return false; From da70a77b443338c796f3e1606b5667908f93f8b3 Mon Sep 17 00:00:00 2001 From: Chong Kai Xiong Date: Wed, 29 Jan 2025 19:38:32 +0800 Subject: [PATCH 10/10] Core (LV::Video): Introduce LV::ImageFormat enum for identifying image formats. --- libvisual/libvisual/lv_video.cpp | 80 +++++++++++++------ libvisual/libvisual/lv_video.h | 22 ++++- .../tests/video_test/video_save_test.cpp | 6 +- 3 files changed, 79 insertions(+), 29 deletions(-) diff --git a/libvisual/libvisual/lv_video.cpp b/libvisual/libvisual/lv_video.cpp index cbb8fac6..b97af6a7 100644 --- a/libvisual/libvisual/lv_video.cpp +++ b/libvisual/libvisual/lv_video.cpp @@ -29,6 +29,7 @@ #include "lv_color.h" #include "lv_common.h" #include "lv_cpu.h" +#include "private/lv_string_hash.hpp" #include "private/lv_video_private.hpp" #include "private/lv_video_blit.hpp" #include "private/lv_video_convert.hpp" @@ -42,7 +43,10 @@ #include #include #include +#include +#include #include +#include namespace LV { @@ -50,25 +54,46 @@ namespace LV { namespace { - using BitmapLoad = std::function; - using BitmapSave = std::function; + using BitmapReader = std::function; + using BitmapWriter = std::function; - std::unordered_map const bitmap_load_map = + struct ImageFormatInfo { - { "bmp", bitmap_load_bmp }, - { "png", bitmap_load_png } + std::string name; + BitmapReader reader; + BitmapWriter writer; + std::vector extensions; }; - std::unordered_map const bitmap_save_map = + using ImageFormatExtensionLookup = std::unordered_map< + std::string, ImageFormat, StringHash, std::equal_to<>>; + + std::unordered_map const supported_image_formats { - { "png", bitmap_save_png } + {ImageFormat::BMP, {"BMP", bitmap_load_bmp, nullptr, {".bmp"}}}, + {ImageFormat::PNG, {"PNG", bitmap_load_png, bitmap_save_png, {".png"}}} }; - std::unordered_map const bitmap_format_extension_map = + ImageFormatExtensionLookup build_image_format_extension_lookup () { - { ".png", "png" }, - { ".bmp", "bmp" } - }; + ImageFormatExtensionLookup map; + + for (auto const& entry : supported_image_formats) + for (auto const& extension : entry.second.extensions) + map.emplace (extension, entry.first); + + return map; + } + + std::optional find_image_format_by_extension (std::string_view extension) + { + static auto const lookup {build_image_format_extension_lookup ()}; + + auto match {lookup.find (extension)}; + if (match == lookup.end ()) + return std::nullopt; + return match->second; + } bool is_valid_scale_method (VisVideoScaleMethod scale_method) { @@ -227,10 +252,12 @@ namespace LV { VideoPtr Video::create_from_stream (std::istream& input) { - for (auto entry : bitmap_load_map) { - auto image {entry.second (input)}; - if (image) { - return image; + for (auto entry : supported_image_formats) { + if (entry.second.reader) { + auto image {entry.second.reader (input)}; + if (image) { + return image; + } } } @@ -298,8 +325,8 @@ namespace LV { auto extension {fs::path {path}.extension ()}; auto extension_lower (to_lower_ascii (extension.string ())); - auto entry {bitmap_format_extension_map.find (extension_lower)}; - if (entry == bitmap_format_extension_map.end ()) { + auto format {find_image_format_by_extension (extension_lower)}; + if (!format.has_value ()) { visual_log (VISUAL_LOG_ERROR, "Could not deduce format from filename (%s)", path.c_str ()); return false; } @@ -310,19 +337,24 @@ namespace LV { return false; } - return save_to_stream (output, entry->second); + return save_to_stream (output, format.value ()); } - bool Video::save_to_stream (std::ostream& output, std::string const& format) const + bool Video::save_to_stream (std::ostream& output, ImageFormat format) const { - auto entry {bitmap_save_map.find (format)}; - if (entry == bitmap_save_map.end ()) { - std::string format_str {format}; - visual_log (VISUAL_LOG_ERROR, "Saving to %s format is not supported", format_str.c_str ()); + auto entry {supported_image_formats.find (format)}; + + if (entry == supported_image_formats.end ()) { + visual_log (VISUAL_LOG_ERROR, "Saving to unknown format (%d).", static_cast (format)); + return false; + } + + if (!entry->second.writer) { + visual_log (VISUAL_LOG_ERROR, "Saving to %s is unsupported.", entry->second.name.c_str ()); return false; } - return entry->second (*this, output); + return entry->second.writer (*this, output); } void Video::copy_attrs (VideoConstPtr const& src) diff --git a/libvisual/libvisual/lv_video.h b/libvisual/libvisual/lv_video.h index 1c4589ff..3c1f3fec 100644 --- a/libvisual/libvisual/lv_video.h +++ b/libvisual/libvisual/lv_video.h @@ -96,6 +96,14 @@ typedef enum { VISUAL_VIDEO_COMPOSE_TYPE_CUSTOM /**< Custom compose function (looks up on the source VisVideo. */ } VisVideoComposeType; +/** + * The set of known and supported image formats. + */ +typedef enum { + VISUAL_IMAGE_FORMAT_BMP = 0, + VISUAL_IMAGE_FORMAT_PNG +} VisImageFormat; + typedef struct _VisVideoAttrOptions VisVideoAttrOptions; #ifdef __cplusplus @@ -125,6 +133,15 @@ struct _VisVideoAttrOptions { namespace LV { + /** + * The set of known and supported image formats. + */ + enum class ImageFormat + { + BMP = VISUAL_IMAGE_FORMAT_BMP, + PNG = VISUAL_IMAGE_FORMAT_PNG + }; + class Video; typedef IntrusivePtr