Compare commits

..

2 Commits

Author SHA1 Message Date
Green Sky
1547999ec0 add image scaling to send image popup
Some checks failed
ContinuousDelivery / linux-ubuntu (push) Has been cancelled
ContinuousDelivery / android (map[ndk_abi:arm64-v8a vcpkg_toolkit:arm64-android-23]) (push) Has been cancelled
ContinuousDelivery / android (map[ndk_abi:armeabi-v7a vcpkg_toolkit:arm-neon-android-23]) (push) Has been cancelled
ContinuousDelivery / android (map[ndk_abi:x86_64 vcpkg_toolkit:x64-android-23]) (push) Has been cancelled
ContinuousDelivery / windows (windows-2022, ) (push) Has been cancelled
ContinuousDelivery / windows (windows-2022, asan) (push) Has been cancelled
ContinuousDelivery / dumpsyms (push) Has been cancelled
ContinuousDelivery / release (push) Has been cancelled
ContinuousIntegration / on ubuntu-24.04-arm (push) Has been cancelled
ContinuousIntegration / asan on ubuntu-24.04-arm (push) Has been cancelled
ContinuousIntegration / on ubuntu-latest (push) Has been cancelled
ContinuousIntegration / asan on ubuntu-latest (push) Has been cancelled
ContinuousIntegration / android (map[ndk_abi:arm64-v8a vcpkg_toolkit:arm64-android-23]) (push) Has been cancelled
ContinuousIntegration / android (map[ndk_abi:armeabi-v7a vcpkg_toolkit:arm-neon-android-23]) (push) Has been cancelled
ContinuousIntegration / android (map[ndk_abi:x86_64 vcpkg_toolkit:x64-android-23]) (push) Has been cancelled
ContinuousIntegration / macos (push) Has been cancelled
ContinuousIntegration / windows (push) Has been cancelled
2025-11-05 18:53:07 +01:00
Green Sky
e8a15a58dd add image scaler (box sampling) 2025-11-05 00:01:42 +01:00
7 changed files with 275 additions and 22 deletions

View File

@@ -53,6 +53,8 @@ target_sources(tomato PUBLIC
./image_loader_qoi.cpp ./image_loader_qoi.cpp
./image_loader_sdl_image.hpp ./image_loader_sdl_image.hpp
./image_loader_sdl_image.cpp ./image_loader_sdl_image.cpp
./image_scaler.hpp
./image_scaler.cpp
./texture_uploader.hpp ./texture_uploader.hpp
./sdlrenderer_texture_uploader.hpp ./sdlrenderer_texture_uploader.hpp

View File

@@ -13,6 +13,7 @@
#include <imgui.h> #include <imgui.h>
#include <cstdint>
#include <cmath> #include <cmath>
SendImagePopup::SendImagePopup(TextureUploaderI& tu) : _tu(tu) { SendImagePopup::SendImagePopup(TextureUploaderI& tu) : _tu(tu) {
@@ -245,7 +246,7 @@ void SendImagePopup::render(float time_delta) {
- ( - (
ImGui::GetWindowContentRegionMin().y ImGui::GetWindowContentRegionMin().y
+ TEXT_BASE_HEIGHT*(2-1) // row of buttons (-1 bc fh inclues fontsize) + TEXT_BASE_HEIGHT*(2-1) // row of buttons (-1 bc fh inclues fontsize)
+ ImGui::GetFrameHeightWithSpacing()*4 + ImGui::GetFrameHeightWithSpacing()*6
) )
; ;
if (height > max_height) { if (height > max_height) {
@@ -340,23 +341,22 @@ void SendImagePopup::render(float time_delta) {
{ // 4 lines delimiting the crop result { // 4 lines delimiting the crop result
ImU32 line_color = 0xffffffff; ImU32 line_color = 0xffffffff;
{ // calc color { // calc color
static constexpr auto f = [](float x) {
while (x < 0.f) {
x += 1.f;
}
x = std::fmod(x, 1.f); // fract()
if (x < 1.f/3) {
return x * 3;
} else if (x < 2.f/3) {
return (1 - (x - (1.f/3))) * 3 - 2;
} else {
return 0.f;
}
};
auto rgb = [](float x) -> ImVec4 { auto rgb = [](float x) -> ImVec4 {
auto f = [](float x) {
while (x < 0.f) {
x += 1.f;
}
x = std::fmod(x, 1.f); // fract()
if (x < 1.f/3) {
return x * 3;
} else if (x < 2.f/3) {
return (1 - (x - (1.f/3))) * 3 - 2;
} else {
return 0.f;
}
};
float red = f(x); float red = f(x);
float green = f(x - (1.f/3)); float green = f(x - (1.f/3));
float blue = f(x - (2.f/3)); float blue = f(x - (2.f/3));
@@ -434,7 +434,7 @@ void SendImagePopup::render(float time_delta) {
} }
} }
const bool cropped = crop_rect.x != 0 || crop_rect.y != 0 || crop_rect.w != original_image.width || crop_rect.h != original_image.height; const bool cropped = crop_rect.x != 0 || crop_rect.y != 0 || crop_rect.w != int64_t(original_image.width) || crop_rect.h != int64_t(original_image.height);
if (cropping) { if (cropping) {
if (ImGui::Button("done")) { if (ImGui::Button("done")) {
cropping = false; cropping = false;
@@ -445,7 +445,7 @@ void SendImagePopup::render(float time_delta) {
} }
} }
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("reset")) { if (ImGui::Button("reset##crop")) {
crop_rect.x = 0; crop_rect.x = 0;
crop_rect.y = 0; crop_rect.y = 0;
crop_rect.w = original_image.width; crop_rect.w = original_image.width;
@@ -455,6 +455,35 @@ void SendImagePopup::render(float time_delta) {
ImGui::SameLine(); ImGui::SameLine();
ImGui::Text("x:%d y:%d w:%d h:%d", crop_rect.x, crop_rect.y, crop_rect.w, crop_rect.h); ImGui::Text("x:%d y:%d w:%d h:%d", crop_rect.x, crop_rect.y, crop_rect.w, crop_rect.h);
ImGui::TextUnformatted("scale");
ImGui::SameLine();
if (ImGui::Button("reset##scale")) {
scale_x = 1.f;
scale_y = 1.f;
compress = false; // feels nice
}
ImGui::SameLine();
ImGui::SetNextItemWidth(TEXT_BASE_HEIGHT*3);
if (ImGui::DragFloat("##scale x", &scale_x, 0.001f, 0.001f, 100.f) && scale_tie) {
scale_y = scale_x;
}
ImGui::SameLine();
ImGui::TextUnformatted("X");
ImGui::SameLine();
ImGui::SetNextItemWidth(TEXT_BASE_HEIGHT*3);
if (ImGui::DragFloat("##scale y", &scale_y, 0.001f, 0.001f, 100.f) && scale_tie) {
scale_x = scale_y;
}
ImGui::SameLine();
ImGui::Checkbox("tie", &scale_tie);
if (scale_x <= 0.f) { scale_x = 0.001f; }
if (scale_y <= 0.f) { scale_y = 0.001f; }
if (scale_x > 100.f) { scale_x = 100.f; }
if (scale_y > 100.f) { scale_y = 100.f; }
ImGui::Text("final size -> w:%d h:%d", (int)std::ceil(crop_rect.w*scale_x), (int)std::ceil(crop_rect.h*scale_y));
bool recalc_size = false; bool recalc_size = false;
if (cropped) { if (cropped) {
if (!compress) { if (!compress) {
@@ -463,6 +492,13 @@ void SendImagePopup::render(float time_delta) {
} }
compress = true; compress = true;
} }
if (scale_x != 1.f || scale_y != 1.f) {
if (!compress) {
// looks like a change
recalc_size = true;
}
compress = true;
}
recalc_size |= ImGui::Checkbox("compress", &compress); recalc_size |= ImGui::Checkbox("compress", &compress);
if (cropped && ImGui::IsItemHovered()) { if (cropped && ImGui::IsItemHovered()) {
@@ -505,11 +541,10 @@ void SendImagePopup::render(float time_delta) {
} }
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("send ->", {-FLT_MIN, TEXT_BASE_HEIGHT*2})) { if (ImGui::Button("send ->", {-FLT_MIN, TEXT_BASE_HEIGHT*2})) {
if (compress || cropped) { if (compress || cropped || scale_x != 1.f || scale_y != 1.f) {
// TODO: copy bad // TODO: copy bad
ImageLoaderI::ImageResult tmp_img; ImageLoaderI::ImageResult tmp_img;
if (cropped) { if (cropped) {
std::cout << "SIP: CROP!!!!!\n";
tmp_img = original_image.crop( tmp_img = original_image.crop(
crop_rect.x, crop_rect.x,
crop_rect.y, crop_rect.y,
@@ -520,6 +555,15 @@ void SendImagePopup::render(float time_delta) {
tmp_img = original_image; tmp_img = original_image;
} }
if (scale_x != 1.f || scale_y != 1.f) {
tmp_img = tmp_img.scale(
(int)std::ceil(crop_rect.w*scale_x),
(int)std::ceil(crop_rect.h*scale_y)
);
} else {
tmp_img = original_image;
}
std::vector<uint8_t> new_data; std::vector<uint8_t> new_data;
// HACK: generic list // HACK: generic list

View File

@@ -36,6 +36,10 @@ struct SendImagePopup {
bool dragging_last_frame_ul {false}; bool dragging_last_frame_ul {false};
bool dragging_last_frame_lr {false}; bool dragging_last_frame_lr {false};
float scale_x {1.f};
float scale_y {1.f};
bool scale_tie {true};
// texture to render (orig img) // texture to render (orig img)
TextureEntry preview_image; TextureEntry preview_image;

View File

@@ -1,6 +1,9 @@
#include "./image_loader.hpp" #include "./image_loader.hpp"
#include <cassert> #include <cassert>
#include <cstdint>
#include "./image_scaler.hpp"
ImageLoaderI::ImageResult ImageLoaderI::ImageResult::crop(int32_t c_x, int32_t c_y, int32_t c_w, int32_t c_h) const { ImageLoaderI::ImageResult ImageLoaderI::ImageResult::crop(int32_t c_x, int32_t c_y, int32_t c_w, int32_t c_h) const {
// TODO: proper error handling // TODO: proper error handling
@@ -30,3 +33,23 @@ ImageLoaderI::ImageResult ImageLoaderI::ImageResult::crop(int32_t c_x, int32_t c
return new_image; return new_image;
} }
ImageLoaderI::ImageResult ImageLoaderI::ImageResult::scale(int32_t w, int32_t h) const {
assert(w > 0);
assert(h > 0);
ImageLoaderI::ImageResult new_image;
new_image.width = w;
new_image.height = h;
new_image.file_ext = file_ext;
for (const auto& input_frame : frames) {
auto& new_frame = new_image.frames.emplace_back();
new_frame.ms = input_frame.ms;
new_frame.data.resize(w*h*4);
image_scale(new_frame.data.data(), w, h, const_cast<uint8_t*>(input_frame.data.data()), width, height);
}
return new_image;
}

View File

@@ -28,7 +28,8 @@ struct ImageLoaderI {
// only positive values are valid // only positive values are valid
ImageResult crop(int32_t c_x, int32_t c_y, int32_t c_w, int32_t c_h) const; ImageResult crop(int32_t c_x, int32_t c_y, int32_t c_w, int32_t c_h) const;
// TODO: scale // only values > 0 are valid
ImageResult scale(int32_t w, int32_t h) const;
}; };
virtual ImageResult loadFromMemoryRGBA(const uint8_t* data, uint64_t data_size) = 0; virtual ImageResult loadFromMemoryRGBA(const uint8_t* data, uint64_t data_size) = 0;
}; };

170
src/image_scaler.cpp Normal file
View File

@@ -0,0 +1,170 @@
#include "./image_scaler.hpp"
#include <cmath>
#include <cassert>
// requires ColorTmp to have * and + operators
struct ColorCanvas8888;
struct ColorFloat4 {
float v[4]{};
ColorFloat4& operator*=(const float scalar) {
v[0] *= scalar;
v[1] *= scalar;
v[2] *= scalar;
v[3] *= scalar;
return *this;
}
ColorFloat4 operator*(const float scalar) const {
ColorFloat4 newcf = *this;
newcf.v[0] *= scalar;
newcf.v[1] *= scalar;
newcf.v[2] *= scalar;
newcf.v[3] *= scalar;
return newcf;
}
ColorFloat4& operator/=(const float scalar) {
v[0] /= scalar;
v[1] /= scalar;
v[2] /= scalar;
v[3] /= scalar;
return *this;
}
ColorFloat4& operator+=(const ColorFloat4& color) {
v[0] += color.v[0];
v[1] += color.v[1];
v[2] += color.v[2];
v[3] += color.v[3];
return *this;
}
};
struct ColorCanvas8888 {
uint8_t* ptr {nullptr};
ColorFloat4 operator[](size_t i) const {
return {
{
float(ptr[i*4+0])/255.f,
float(ptr[i*4+1])/255.f,
float(ptr[i*4+2])/255.f,
float(ptr[i*4+3])/255.f,
}
};
}
void set(size_t i, const ColorFloat4& color) {
ptr[i*4+0] = std::round(color.v[0]*255.f);
ptr[i*4+1] = std::round(color.v[1]*255.f);
ptr[i*4+2] = std::round(color.v[2]*255.f);
ptr[i*4+3] = std::round(color.v[3]*255.f);
}
};
template<typename ColorCanvas, typename ColorTmp>
constexpr void image_scale(ColorCanvas& dst, const int dst_w, const int dst_h, const ColorCanvas& src, const int src_w, const int src_h) {
// Box sampling - Imagine projecting the new, smaller pixels onto the larger source, covering multiple pixel.
for (int y = 0; y < dst_h; y++) {
for (int x = 0; x < dst_w; x++) {
// We perform a weighted mean.
ColorTmp color;
float weight_sum = 0.f;
// Walk from upper edge to bottom edge (vertical)
const float edge_up = ((float)y * src_h) / dst_h;
const float edge_down = ((y + 1.f) * src_h) / dst_h;
for (float frac_pos_y = edge_up; frac_pos_y < edge_down;) {
const int src_y = (int)std::floor(frac_pos_y); assert(src_y < src_h);
const float frac_y = 1.f - (frac_pos_y - src_y);
// Walk from left edge to right edge (horizontal)
const float edge_left = ((float)x * src_w) / dst_w;
const float edge_right = ((x + 1.f) * src_w) / dst_w;
for (float frac_pos_x = edge_left; frac_pos_x < edge_right;) {
const int src_x = (int)std::floor(frac_pos_x); assert(src_x < src_w);
const float frac_x = 1.f - (frac_pos_x - src_x);
const float src_pixel_weight = frac_x * frac_y;
//const ColorTmp pixel_color = ImGui::ColorConvertU32ToFloat4(src[src_y * src_w + src_x]);
const ColorTmp pixel_color = src[src_y * src_w + src_x];
color += pixel_color * src_pixel_weight;
weight_sum += src_pixel_weight;
frac_pos_x += frac_x;
}
frac_pos_y += frac_y;
}
color /= weight_sum;
dst.set(y * dst_w + x, color);
}
}
}
bool image_scale(uint8_t* dst, const int dst_w, const int dst_h, uint8_t* src, const int src_w, const int src_h) {
if (dst == nullptr || src == nullptr) {
return false;
}
if (dst_w == src_w && dst_h == src_h) {
assert(false && "fix me !");
return false;
}
ColorCanvas8888 dst_c{dst};
const ColorCanvas8888 src_c{src};
image_scale<ColorCanvas8888, ColorFloat4>(
dst_c,
dst_w, dst_h,
src_c,
src_w, src_h
);
return true;
}
bool image_scale(SDL_Surface* dst, SDL_Surface* src) {
if (dst == nullptr || src == nullptr) {
return false;
}
if (dst->format != src->format) {
return false;
}
// TODO: handle other numbers of components beside 4
if (
src->format != SDL_PIXELFORMAT_RGBA8888 &&
src->format != SDL_PIXELFORMAT_ARGB8888 &&
src->format != SDL_PIXELFORMAT_BGRA8888 &&
src->format != SDL_PIXELFORMAT_ABGR8888 &&
src->format != SDL_PIXELFORMAT_RGBX8888 &&
src->format != SDL_PIXELFORMAT_XRGB8888 &&
src->format != SDL_PIXELFORMAT_BGRX8888 &&
src->format != SDL_PIXELFORMAT_XBGR8888
) {
return false;
}
ColorCanvas8888 dst_c{reinterpret_cast<uint8_t*>(dst->pixels)};
const ColorCanvas8888 src_c{reinterpret_cast<uint8_t*>(src->pixels)};
image_scale<ColorCanvas8888, ColorFloat4>(
dst_c,
dst->w, dst->h,
src_c,
src->w, src->h
);
return true;
}

9
src/image_scaler.hpp Normal file
View File

@@ -0,0 +1,9 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
bool image_scale(uint8_t* dst, const int dst_w, const int dst_h, uint8_t* src, const int src_w, const int src_h);
bool image_scale(SDL_Surface* dst, SDL_Surface* src);