diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 85bc328..e582d76 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -72,5 +72,6 @@ target_link_libraries(tomato PUBLIC stb_image webpdemux + libwebpmux # the f why (needed for anim encode) ) diff --git a/src/chat_gui4.cpp b/src/chat_gui4.cpp index c533044..906f4fa 100644 --- a/src/chat_gui4.cpp +++ b/src/chat_gui4.cpp @@ -383,7 +383,7 @@ void ChatGui4::render(void) { tmp_file_name << std::put_time(std::localtime(&ctime), "%F_%H-%M-%S") << "." - << std::setfill('0') << std::setw(4) + << std::setfill('0') << std::setw(3) << std::chrono::duration_cast(now.time_since_epoch() - std::chrono::duration_cast(now.time_since_epoch())).count() << file_ext ; diff --git a/src/send_image_popup.cpp b/src/send_image_popup.cpp index 72f9ae5..a32b4ec 100644 --- a/src/send_image_popup.cpp +++ b/src/send_image_popup.cpp @@ -6,6 +6,10 @@ #include +// tmp +#include +#include + SendImagePopup::SendImagePopup(TextureUploaderI& tu) : _tu(tu) { _image_loaders.push_back(std::make_unique()); _image_loaders.push_back(std::make_unique()); @@ -39,6 +43,20 @@ bool SendImagePopup::load(void) { continue; } +#if 1 + crop_rect.x = 0; + crop_rect.y = 0; + crop_rect.w = original_image.width; + crop_rect.h = original_image.height; +#else + crop_rect.x = original_image.width * 0.1f; + crop_rect.y = original_image.height * 0.1f; + crop_rect.w = original_image.width * 0.8f; + crop_rect.h = original_image.height * 0.8f; + +#endif + crop_rect = sanitizeCrop(crop_rect, original_image.width, original_image.height); + original_file_ext = "."; if (original_image.file_ext != nullptr) { original_file_ext += original_image.file_ext; @@ -70,6 +88,128 @@ bool SendImagePopup::load(void) { return false; } +std::vector SendImagePopup::compressWebp(const ImageLoaderI::ImageResult& input_image, uint32_t quality) { + // HACK: move to own interface + + WebPAnimEncoderOptions enc_options; + if (!WebPAnimEncoderOptionsInit(&enc_options)) { + std::cerr << "SIP error: WebPAnimEncoderOptionsInit()\n"; + return {}; + } + + // Tune 'enc_options' as needed. + enc_options.minimize_size = 1; // might be slow? optimize for size, no key-frame insertion + + WebPAnimEncoder* enc = WebPAnimEncoderNew(input_image.width, input_image.height, &enc_options); + if (enc == nullptr) { + std::cerr << "SIP error: WebPAnimEncoderNew()\n"; + return {}; + } + + int prev_timestamp = 0; + for (const auto& frame : input_image.frames) { + WebPConfig config; + //WebPConfigInit(&config); + if (!WebPConfigPreset(&config, WebPPreset::WEBP_PRESET_DEFAULT, quality)) { + std::cerr << "SIP error: WebPConfigPreset()\n"; + WebPAnimEncoderDelete(enc); + return {}; + } + //WebPConfigLosslessPreset(&config, 6); // 9 for max compression + + WebPPicture frame_webp; + if (!WebPPictureInit(&frame_webp)) { + std::cerr << "SIP error: WebPPictureInit()\n"; + WebPAnimEncoderDelete(enc); + return {}; + } + frame_webp.width = input_image.width; + frame_webp.height = input_image.height; + if (!WebPPictureImportRGBA(&frame_webp, frame.data.data(), 4*input_image.width)) { + std::cerr << "SIP error: WebPPictureImportRGBA()\n"; + WebPAnimEncoderDelete(enc); + return {}; + } + + if (!WebPAnimEncoderAdd(enc, &frame_webp, prev_timestamp, &config)) { + std::cerr << "SIP error: WebPAnimEncoderAdd()\n"; + WebPPictureFree(&frame_webp); + WebPAnimEncoderDelete(enc); + return {}; + } + prev_timestamp += frame.ms; + } + if (!WebPAnimEncoderAdd(enc, NULL, prev_timestamp, NULL)) { // tell anim encoder its the end + std::cerr << "SIP error: WebPAnimEncoderAdd(NULL)\n"; + WebPAnimEncoderDelete(enc); + return {}; + } + + WebPData webp_data; + WebPDataInit(&webp_data); + if (!WebPAnimEncoderAssemble(enc, &webp_data)) { + std::cerr << "SIP error: WebPAnimEncoderAdd(NULL)\n"; + WebPAnimEncoderDelete(enc); + return {}; + } + WebPAnimEncoderDelete(enc); + + // Write the 'webp_data' to a file, or re-mux it further. + // TODO: make it not a copy + std::vector new_data{webp_data.bytes, webp_data.bytes+webp_data.size}; + + WebPDataClear(&webp_data); + + return new_data; +} + +ImageLoaderI::ImageResult SendImagePopup::crop(const ImageLoaderI::ImageResult& input_image, const Rect& crop_rect) { + // TODO: proper error handling + assert(crop_rect.x+crop_rect.w <= input_image.width); + assert(crop_rect.y+crop_rect.h <= input_image.height); + + ImageLoaderI::ImageResult new_image; + new_image.width = crop_rect.w; + new_image.height = crop_rect.h; + new_image.file_ext = input_image.file_ext; + + for (const auto& input_frame : input_image.frames) { + auto& new_frame = new_image.frames.emplace_back(); + new_frame.ms = input_frame.ms; + + // TODO: improve this, this is super inefficent + for (int64_t y = crop_rect.y; y < crop_rect.y + crop_rect.h; y++) { + for (int64_t x = crop_rect.x; x < crop_rect.x + crop_rect.w; x++) { + new_frame.data.push_back(input_frame.data.at(y*input_image.width*4+x*4+0)); + new_frame.data.push_back(input_frame.data.at(y*input_image.width*4+x*4+1)); + new_frame.data.push_back(input_frame.data.at(y*input_image.width*4+x*4+2)); + new_frame.data.push_back(input_frame.data.at(y*input_image.width*4+x*4+3)); + } + } + } + + return new_image; +} + +SendImagePopup::Rect SendImagePopup::sanitizeCrop(Rect crop_rect, uint32_t image_width, uint32_t image_height) { + // w and h min is 1 -> x/y need to be smaller so the img is atleast 1px in any dim + if (crop_rect.x >= image_width-1) { + crop_rect.x = image_width-2; + } + if (crop_rect.y >= image_height-1) { + crop_rect.y = image_height-2; + } + + if (crop_rect.w > crop_rect.x + image_width) { + crop_rect.w = crop_rect.x + image_width; + } + if (crop_rect.h > crop_rect.y + image_height) { + crop_rect.h = crop_rect.y + image_height; + } + + return crop_rect; +} + void SendImagePopup::sendMemory( const uint8_t* data, size_t data_size, std::function&, std::string_view)>&& on_send, @@ -101,14 +241,9 @@ void SendImagePopup::render(void) { if (_open_popup) { _open_popup = false; ImGui::OpenPopup("send image##SendImagePopup"); - - //const auto TEXT_BASE_WIDTH = ImGui::CalcTextSize("A").x; - //const auto TEXT_BASE_HEIGHT = ImGui::GetTextLineHeightWithSpacing(); - - //ImGui::SetNextWindowSize({TEXT_BASE_WIDTH*100, TEXT_BASE_HEIGHT*30}); } - // TODO: add cancel action + // TODO: add cancel shortcut (esc) if (ImGui::BeginPopupModal("send image##SendImagePopup", nullptr/*, ImGuiWindowFlags_NoDecoration*/)) { const auto TEXT_BASE_WIDTH = ImGui::CalcTextSize("A").x; const auto TEXT_BASE_HEIGHT = ImGui::GetTextLineHeightWithSpacing(); @@ -126,7 +261,7 @@ void SendImagePopup::render(void) { - ( ImGui::GetWindowContentRegionMin().y + TEXT_BASE_HEIGHT*(2-1) // row of buttons (-1 bc fh inclues fontsize) - + ImGui::GetFrameHeightWithSpacing() + + ImGui::GetFrameHeightWithSpacing()*4 ) ; if (height > max_height) { @@ -134,11 +269,42 @@ void SendImagePopup::render(void) { height = max_height; } + // save curser pos // TODO: propergate type ImGui::Image( preview_image.getID(), ImVec2{static_cast(width), static_cast(height)} ); + + // TODO: crop input here + } + + crop_rect = sanitizeCrop(crop_rect, original_image.width, original_image.height); + const bool cropped = crop_rect.x != 0 || crop_rect.y != 0 || crop_rect.w != original_image.width || crop_rect.h != original_image.height; + + bool recalc_size = false; + if (cropped) { + if (!compress) { + // looks like a change + recalc_size = true; + } + compress = true; + } + recalc_size |= ImGui::Checkbox("reencode", &compress); + if (compress) { + ImGui::Indent(); + // combo "webp""webp-lossless""png""jpg?" + // if lossy quality slider (1-100) default 80 + const uint32_t qmin = 1; + const uint32_t qmax = 100; + recalc_size |= ImGui::SliderScalar("quality", ImGuiDataType_U32, &quality, &qmin, &qmax); + + if (recalc_size) { + // compress and save temp? cooldown? async? save size only? + // print size where? + } + + ImGui::Unindent(); } if (ImGui::Button("X cancel", {ImGui::GetWindowContentRegionWidth()/2.f, TEXT_BASE_HEIGHT*2})) { @@ -148,16 +314,24 @@ void SendImagePopup::render(void) { } ImGui::SameLine(); if (ImGui::Button("send ->", {-FLT_MIN, TEXT_BASE_HEIGHT*2})) { - //if (_is_valid(_current_file_path)) { + if (compress || cropped) { + std::vector new_data; + if (cropped) { + std::cout << "SIP: CROP!!!!!\n"; + new_data = compressWebp(crop(original_image, crop_rect), quality); + } else { + new_data = compressWebp(original_image, quality); + } - // if modified - //_on_send(data); - // else + if (!new_data.empty()) { + _on_send(new_data, ".webp"); + } + } else { _on_send(original_data, original_file_ext); + } - ImGui::CloseCurrentPopup(); - reset(); - //} + ImGui::CloseCurrentPopup(); + reset(); } ImGui::EndPopup(); diff --git a/src/send_image_popup.hpp b/src/send_image_popup.hpp index c95e897..25e33dc 100644 --- a/src/send_image_popup.hpp +++ b/src/send_image_popup.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "./image_loader.hpp" #include "./texture_cache.hpp" @@ -32,6 +33,9 @@ struct SendImagePopup { // texture to render (orig img) TextureEntry preview_image; + bool compress {false}; + uint32_t quality {80u}; + bool _open_popup {false}; std::function&, std::string_view)> _on_send = [](const auto&, auto){}; @@ -44,6 +48,10 @@ struct SendImagePopup { // returns if loaded successfully bool load(void); + static std::vector compressWebp(const ImageLoaderI::ImageResult& input_image, uint32_t quality = 80u); + static ImageLoaderI::ImageResult crop(const ImageLoaderI::ImageResult& input_image, const Rect& crop_rect); + static Rect sanitizeCrop(Rect crop_rect, uint32_t image_width, uint32_t image_height); + public: SendImagePopup(TextureUploaderI& tu);