diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e14f734..6328a18 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -25,6 +25,8 @@ add_executable(tomato ./image_loader_stb.cpp ./image_loader_webp.hpp ./image_loader_webp.cpp + ./image_loader_qoi.hpp + ./image_loader_qoi.cpp ./texture_uploader.hpp ./sdlrenderer_texture_uploader.hpp @@ -90,5 +92,6 @@ target_link_libraries(tomato PUBLIC stb_image_write webpdemux libwebpmux # the f why (needed for anim encode) + qoi ) diff --git a/src/chat_gui4.cpp b/src/chat_gui4.cpp index 6f2e2d1..f91e345 100644 --- a/src/chat_gui4.cpp +++ b/src/chat_gui4.cpp @@ -613,6 +613,7 @@ float ChatGui4::render(float time_delta) { "image/gif", "image/jpeg", "image/bmp", + "image/qoi", }; for (const char* mime_type : image_mime_types) { diff --git a/src/image_loader_qoi.cpp b/src/image_loader_qoi.cpp new file mode 100644 index 0000000..bed0fd6 --- /dev/null +++ b/src/image_loader_qoi.cpp @@ -0,0 +1,91 @@ +#include "./image_loader_qoi.hpp" + +#include +#include + +#include + +ImageLoaderQOI::ImageInfo ImageLoaderQOI::loadInfoFromMemory(const uint8_t* data, uint64_t data_size) { + ImageInfo res; + + qoi_desc desc; + // TODO: only read the header + auto* ret = qoi_decode(data, data_size, &desc, 4); + if (ret == nullptr) { + return res; + } + free(ret); + + res.width = desc.width; + res.height = desc.height; + //desc.colorspace; + + return res; +} + +ImageLoaderQOI::ImageResult ImageLoaderQOI::loadFromMemoryRGBA(const uint8_t* data, uint64_t data_size) { + ImageResult res; + + qoi_desc desc; + + uint8_t* img_data = static_cast( + qoi_decode(data, data_size, &desc, 4) + ); + if (img_data == nullptr) { + // not readable + return res; + } + + res.width = desc.width; + res.height = desc.height; + + auto& new_frame = res.frames.emplace_back(); + new_frame.ms = 0; + new_frame.data.insert(new_frame.data.cbegin(), img_data, img_data+(desc.width*desc.height*4)); + + free(img_data); + return res; +} + +std::vector ImageEncoderQOI::encodeToMemoryRGBA(const ImageResult& input_image, const std::map&) { + if (input_image.frames.empty()) { + std::cerr << "IEQOI error: empty image\n"; + return {}; + } + + if (input_image.frames.size() > 1) { + std::cerr << "IEQOI warning: image with animation, only first frame will be encoded!\n"; + return {}; + } + + // TODO: look into RDO (eg https://github.com/richgel999/rdopng) + //int png_compression_level = 8; + //if (extra_options.count("png_compression_level")) { + //png_compression_level = extra_options.at("png_compression_level"); + //} + + qoi_desc desc; + desc.width = input_image.width; + desc.height = input_image.height; + desc.channels = 4; + desc.colorspace = QOI_SRGB; // TODO: decide + + int out_len {0}; + uint8_t* enc_data = static_cast(qoi_encode( + input_image.frames.front().data.data(), + &desc, + &out_len + )); + + if (enc_data == nullptr) { + std::cerr << "IEQOI error: qoi_encode failed!\n"; + return {}; + } + + std::vector new_data(enc_data, enc_data+out_len); + + free(enc_data); // TODO: a streaming encoder would be better + + return new_data; +} + diff --git a/src/image_loader_qoi.hpp b/src/image_loader_qoi.hpp new file mode 100644 index 0000000..73dd147 --- /dev/null +++ b/src/image_loader_qoi.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "./image_loader.hpp" + +struct ImageLoaderQOI : public ImageLoaderI { + ImageInfo loadInfoFromMemory(const uint8_t* data, uint64_t data_size) override; + ImageResult loadFromMemoryRGBA(const uint8_t* data, uint64_t data_size) override; +}; + +struct ImageEncoderQOI : public ImageEncoderI { + std::vector encodeToMemoryRGBA(const ImageResult& input_image, const std::map& extra_options = {}) override; +}; + diff --git a/src/media_meta_info_loader.cpp b/src/media_meta_info_loader.cpp index d156fe7..ab4a796 100644 --- a/src/media_meta_info_loader.cpp +++ b/src/media_meta_info_loader.cpp @@ -2,6 +2,7 @@ #include "./image_loader_webp.hpp" #include "./image_loader_sdl_bmp.hpp" +#include "./image_loader_qoi.hpp" #include "./image_loader_stb.hpp" #include @@ -77,6 +78,7 @@ MediaMetaInfoLoader::MediaMetaInfoLoader(RegistryMessageModel& rmm) : _rmm(rmm) // HACK: make them be added externally? _image_loaders.push_back(std::make_unique()); _image_loaders.push_back(std::make_unique()); + _image_loaders.push_back(std::make_unique()); _image_loaders.push_back(std::make_unique()); _rmm.subscribe(this, RegistryMessageModel_Event::message_construct); diff --git a/src/message_image_loader.cpp b/src/message_image_loader.cpp index ecd3281..6abccfc 100644 --- a/src/message_image_loader.cpp +++ b/src/message_image_loader.cpp @@ -1,6 +1,7 @@ #include "./message_image_loader.hpp" #include "./image_loader_sdl_bmp.hpp" +#include "./image_loader_qoi.hpp" #include "./image_loader_stb.hpp" #include "./image_loader_webp.hpp" #include "./media_meta_info_loader.hpp" @@ -19,6 +20,7 @@ uint64_t getTimeMS(void); MessageImageLoader::MessageImageLoader(void) { _image_loaders.push_back(std::make_unique()); + _image_loaders.push_back(std::make_unique()); _image_loaders.push_back(std::make_unique()); _image_loaders.push_back(std::make_unique()); } diff --git a/src/send_image_popup.cpp b/src/send_image_popup.cpp index 722a951..ea145fe 100644 --- a/src/send_image_popup.cpp +++ b/src/send_image_popup.cpp @@ -3,6 +3,7 @@ #include "./image_loader_sdl_bmp.hpp" #include "./image_loader_stb.hpp" #include "./image_loader_webp.hpp" +#include "./image_loader_qoi.hpp" #include @@ -13,6 +14,7 @@ uint64_t getTimeMS(void); SendImagePopup::SendImagePopup(TextureUploaderI& tu) : _tu(tu) { _image_loaders.push_back(std::make_unique()); + _image_loaders.push_back(std::make_unique()); _image_loaders.push_back(std::make_unique()); _image_loaders.push_back(std::make_unique()); } @@ -421,7 +423,7 @@ void SendImagePopup::render(float time_delta) { if (compress) { ImGui::SameLine(); - ImGui::Combo("##compression_type", ¤t_compressor, "webp\0jpeg\0png\n"); + ImGui::Combo("##compression_type", ¤t_compressor, "webp\0jpeg\0png\0qoi\0"); ImGui::Indent(); // combo "webp""webp-lossless""png""jpg?" @@ -486,6 +488,11 @@ void SendImagePopup::render(float time_delta) { if (!new_data.empty()) { _on_send(new_data, ".png"); } + } else if (current_compressor == 3) { + new_data = ImageEncoderQOI{}.encodeToMemoryRGBA(tmp_img, {});; + if (!new_data.empty()) { + _on_send(new_data, ".qoi"); + } } // error diff --git a/src/tox_avatar_loader.cpp b/src/tox_avatar_loader.cpp index 3aa7a8c..f741a5e 100644 --- a/src/tox_avatar_loader.cpp +++ b/src/tox_avatar_loader.cpp @@ -1,6 +1,7 @@ #include "./tox_avatar_loader.hpp" #include "./image_loader_sdl_bmp.hpp" +#include "./image_loader_qoi.hpp" #include "./image_loader_stb.hpp" #include "./image_loader_webp.hpp" @@ -21,6 +22,7 @@ uint64_t getTimeMS(void); ToxAvatarLoader::ToxAvatarLoader(Contact3Registry& cr) : _cr(cr) { _image_loaders.push_back(std::make_unique()); + _image_loaders.push_back(std::make_unique()); _image_loaders.push_back(std::make_unique()); _image_loaders.push_back(std::make_unique()); }