tedit/imageviewer.h
2025-04-07 20:08:16 -04:00

302 lines
13 KiB
C++

#ifndef IMGUI_IMAGE_VIEWER_H
#define IMGUI_IMAGE_VIEWER_H
#include "app_image.h" // Needs the AppImage class definition
#include "imgui.h"
#define IMGUI_DEFINE_MATH_OPERATORS // Allows ImVec2 operators
#include "imgui_internal.h" // Need ImFloorSigned, ImClamp, ImMax, ImMin, ImAbs
#include <cmath> // For pow, round, floor
#include <vector>
#include <cstdint>
#include <algorithm> // For std::max, std::min
#define IMGUI_DEFINE_MATH_OPERATORS // Allows ImVec2 operators
// --- Graphics API ---
#if defined(IMGUI_IMPL_OPENGL_ES2)
#include <SDL_opengles2.h>
#else
#include <SDL_opengl.h>
#endif
// --- User Instructions ---
// (Same as before: Include after dependencies, define IMPLEMENTATION once)
class ImGuiImageViewer {
public:
ImGuiImageViewer();
~ImGuiImageViewer();
ImGuiImageViewer(const ImGuiImageViewer&) = delete;
ImGuiImageViewer& operator=(const ImGuiImageViewer&) = delete;
bool LoadImage(const AppImage& appImage);
void UnloadImage();
// Renders within the available space, maintaining aspect ratio.
void Render(const ImVec2& availableSize);
void ResetView(); // Resets zoom to 100%, centers view
// --- Getters ---
float GetZoom() const { return m_zoom; }
// Pan offset returns the center of the view in UV coordinates (0.0-1.0)
ImVec2 GetPanOffsetUV() const { return m_panOffsetUV; }
bool IsImageLoaded() const { return m_textureId != 0; }
// Get the raw OpenGL texture ID for external use
GLuint GetTextureId() const { return m_textureId; }
private:
GLuint m_textureId = 0;
int m_textureWidth = 0;
int m_textureHeight = 0;
float m_zoom = 1.0f; // Scale factor (1.0 = 100%)
// Pan offset: Center of the view in UV coordinates (0.0 to 1.0)
ImVec2 m_panOffsetUV = ImVec2(0.5f, 0.5f);
// Interaction state
bool m_isDragging = false;
ImVec2 m_dragStartMousePos = ImVec2(0, 0);
ImVec2 m_dragStartPanOffsetUV = ImVec2(0, 0);
// Configuration
const float m_minZoom = 0.01f; // 1%
const float m_maxZoom = 50.0f; // 5000%
const float m_zoomSpeed = 1.1f;
// Clamps pan offset (UV) based on the container size and zoom.
void ClampPanOffset(const ImVec2& containerSize);
};
// ============================================================================
// =================== IMPLEMENTATION SECTION =================================
// ============================================================================
#ifdef IMGUI_IMAGE_VIEWER_IMPLEMENTATION
// --- Helper namespace ---
namespace ImGuiImageViewerUtil {
// Linear float [0,1+] -> sRGB approx [0,1]
inline float linear_to_srgb_approx(float linearVal) {
if (linearVal <= 0.0f) return 0.0f;
linearVal = std::fmax(0.0f, std::fmin(1.0f, linearVal)); // Clamp for display
if (linearVal <= 0.0031308f) { return linearVal * 12.92f; }
else { return 1.055f * std::pow(linearVal, 1.0f / 2.4f) - 0.055f; }
}
// Round float to nearest integer
inline float Round(float f) { return ImFloor(f + 0.5f); }
} // namespace ImGuiImageViewerUtil
ImGuiImageViewer::ImGuiImageViewer() {}
ImGuiImageViewer::~ImGuiImageViewer() { UnloadImage(); }
void ImGuiImageViewer::UnloadImage() {
if (m_textureId != 0) {
glDeleteTextures(1, &m_textureId);
m_textureId = 0;
m_textureWidth = 0;
m_textureHeight = 0;
ResetView();
}
}
bool ImGuiImageViewer::LoadImage(const AppImage& appImage) {
// --- Basic Error Checking ---
if (appImage.isEmpty() || appImage.getWidth() == 0 || appImage.getHeight() == 0) {
UnloadImage(); return false;
}
if (!appImage.isLinear()) { /* Log warning */ }
const int width = static_cast<int>(appImage.getWidth());
const int height = static_cast<int>(appImage.getHeight());
const int channels = static_cast<int>(appImage.getChannels());
if (channels != 1 && channels != 3 && channels != 4) { UnloadImage(); return false; }
const float* linearData = appImage.getData();
// --- Prepare 8-bit sRGB data for OpenGL (RGBA format) ---
GLenum internalFormat = GL_RGBA8; GLenum dataFormat = GL_RGBA; int outputChannels = 4;
std::vector<unsigned char> textureData(static_cast<size_t>(width) * height * outputChannels);
unsigned char* outPtr = textureData.data(); const float* inPtr = linearData;
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
float r=0,g=0,b=0,a=1; if(channels==1){r=g=b=*inPtr++;} else if(channels==3){r=*inPtr++;g=*inPtr++;b=*inPtr++;} else{r=*inPtr++;g=*inPtr++;b=*inPtr++;a=*inPtr++;}
float sr=ImGuiImageViewerUtil::linear_to_srgb_approx(r), sg=ImGuiImageViewerUtil::linear_to_srgb_approx(g), sb=ImGuiImageViewerUtil::linear_to_srgb_approx(b); a=std::fmax(0.f,std::fmin(1.f,a));
*outPtr++=static_cast<unsigned char>(std::max(0, std::min(255, static_cast<int>(ImGuiImageViewerUtil::Round(sr*255.f)))));
*outPtr++=static_cast<unsigned char>(std::max(0, std::min(255, static_cast<int>(ImGuiImageViewerUtil::Round(sg*255.f)))));
*outPtr++=static_cast<unsigned char>(std::max(0, std::min(255, static_cast<int>(ImGuiImageViewerUtil::Round(sb*255.f)))));
*outPtr++=static_cast<unsigned char>(std::max(0, std::min(255, static_cast<int>(ImGuiImageViewerUtil::Round(a*255.f)))));
}
}
// --- Upload to OpenGL Texture ---
GLint lastTexture; glGetIntegerv(GL_TEXTURE_BINDING_2D, &lastTexture);
if (m_textureId == 0) { glGenTextures(1, &m_textureId); }
glBindTexture(GL_TEXTURE_2D, m_textureId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // No filtering
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); // No filtering
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, dataFormat, GL_UNSIGNED_BYTE, textureData.data());
glBindTexture(GL_TEXTURE_2D, lastTexture);
m_textureWidth = width; m_textureHeight = height;
ResetView(); // Reset view when loading a new image
return true;
}
void ImGuiImageViewer::ResetView() {
m_zoom = 1.0f;
m_panOffsetUV = ImVec2(0.5f, 0.5f); // Center view
m_isDragging = false;
}
// Clamp pan offset (UV coordinates)
void ImGuiImageViewer::ClampPanOffset(const ImVec2& containerSize) {
if (m_textureWidth <= 0 || m_textureHeight <= 0 || m_zoom <= 0) return;
// Size of the image content projected onto the screen at current zoom
ImVec2 zoomedImageScreenSize = ImVec2(m_textureWidth * m_zoom, m_textureHeight * m_zoom);
// Calculate the size of the visible area in UV coordinates
ImVec2 viewSizeUV = containerSize / zoomedImageScreenSize; // If zoom=1, size=texSize, this is containerSize/texSize
viewSizeUV.x = ImMin(viewSizeUV.x, 1.0f); // Cannot see more than 1.0 UV width
viewSizeUV.y = ImMin(viewSizeUV.y, 1.0f); // Cannot see more than 1.0 UV height
// Calculate min/max allowed pan values (center of view in UV space)
ImVec2 minPanUV, maxPanUV;
// If the zoomed image is smaller than the container, center it.
// The pan offset (center of view) should be locked to 0.5 UV.
if (zoomedImageScreenSize.x <= containerSize.x) {
minPanUV.x = maxPanUV.x = 0.5f;
} else {
// Image is wider: Allow panning.
// Min pan: Center of view is half the view-width (in UV) from the left edge (UV=0)
minPanUV.x = viewSizeUV.x * 0.5f;
// Max pan: Center of view is half the view-width (in UV) from the right edge (UV=1)
maxPanUV.x = 1.0f - viewSizeUV.x * 0.5f;
}
if (zoomedImageScreenSize.y <= containerSize.y) {
// Image is shorter: Center it.
minPanUV.y = maxPanUV.y = 0.5f;
} else {
// Image is taller: Allow panning.
minPanUV.y = viewSizeUV.y * 0.5f;
maxPanUV.y = 1.0f - viewSizeUV.y * 0.5f;
}
// Apply clamping
m_panOffsetUV.x = ImClamp(m_panOffsetUV.x, minPanUV.x, maxPanUV.x);
m_panOffsetUV.y = ImClamp(m_panOffsetUV.y, minPanUV.y, maxPanUV.y);
}
void ImGuiImageViewer::Render(const ImVec2& availableSize) {
if (m_textureId == 0 || m_textureWidth <= 0 || m_textureHeight <= 0) {
ImGui::Dummy(availableSize); return;
}
ImGuiIO& io = ImGui::GetIO();
ImGuiStyle& style = ImGui::GetStyle();
// Begin a child window to establish a canvas with a background
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.1f, 0.1f, 0.1f, 1.0f)); // Dark background for contrast
ImGui::BeginChild("ImageViewerCanvas", availableSize, false, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration);
ImVec2 containerSize = ImGui::GetContentRegionAvail();
if (containerSize.x < 1.0f) containerSize.x = 1.0f;
if (containerSize.y < 1.0f) containerSize.y = 1.0f;
ImVec2 containerTopLeftScreen = ImGui::GetCursorScreenPos();
// Calculate aspect-correct display size and padding within the container
float imageAspect = static_cast<float>(m_textureWidth) / static_cast<float>(m_textureHeight);
float containerAspect = containerSize.x / containerSize.y;
ImVec2 displaySize = containerSize; // Final screen size of the image content
ImVec2 displayPadding = ImVec2(0, 0); // Padding for letter/pillarboxing
if (containerAspect > imageAspect) { // Letterbox: Container wider than image
displaySize.x = containerSize.y * imageAspect;
displayPadding.x = (containerSize.x - displaySize.x) * 0.5f;
} else { // Pillarbox: Container taller than image (or same aspect)
displaySize.y = containerSize.x / imageAspect;
displayPadding.y = (containerSize.y - displaySize.y) * 0.5f;
}
// Round padding to avoid pixel jitter
displayPadding.x = ImFloor(displayPadding.x);
displayPadding.y = ImFloor(displayPadding.y);
displaySize = containerSize - displayPadding * 2.0f; // Recalculate based on rounded padding
// Screen position where the image content rendering starts
ImVec2 displayTopLeftScreen = containerTopLeftScreen + displayPadding;
// Invisible button covering the *entire container* for capturing inputs
ImGui::SetCursorScreenPos(containerTopLeftScreen);
ImGui::InvisibleButton("##canvas_interaction", containerSize);
bool isHoveredOnCanvas = ImGui::IsItemHovered();
bool isActiveOnCanvas = ImGui::IsItemActive();
// Calculate mouse position relative to the *display area* top-left corner
ImVec2 mousePosDisplay = io.MousePos - displayTopLeftScreen;
// Check if the mouse is actually over the image content, not the padding
bool isMouseInDisplayArea = (mousePosDisplay.x >= 0.0f && mousePosDisplay.x < displaySize.x &&
mousePosDisplay.y >= 0.0f && mousePosDisplay.y < displaySize.y);
// 1. Zooming (centered on mouse, only when hovering the image content)
if (isHoveredOnCanvas && isMouseInDisplayArea && io.MouseWheel != 0.0f) {
// Mouse position relative to display center, in screen pixels
ImVec2 mouseRelCenter = mousePosDisplay - displaySize * 0.5f;
// Convert mouse position to UV coordinates *before* zoom change
ImVec2 mouseUV = m_panOffsetUV + mouseRelCenter / (ImVec2((float)m_textureWidth, (float)m_textureHeight) * m_zoom);
float oldZoom = m_zoom;
m_zoom *= std::pow(m_zoomSpeed, io.MouseWheel);
m_zoom = ImClamp(m_zoom, m_minZoom, m_maxZoom); // Clamp zoom
// Keep the same UV coordinate under the mouse cursor after zoom
// NewPan = MouseUV - (MouseScreenPosRelCenter / NewZoomedImageScreenSize)
m_panOffsetUV = mouseUV - mouseRelCenter / (ImVec2((float)m_textureWidth, (float)m_textureHeight) * m_zoom);
ClampPanOffset(containerSize); // Re-clamp after zoom
}
// 2. Panning (allow dragging anywhere on the canvas)
if (isActiveOnCanvas && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
if (!m_isDragging) {
m_isDragging = true;
m_dragStartMousePos = io.MousePos;
m_dragStartPanOffsetUV = m_panOffsetUV;
}
ImVec2 mouseDelta = io.MousePos - m_dragStartMousePos;
// Convert screen pixel delta to UV delta
ImVec2 uvDelta = mouseDelta / (ImVec2((float)m_textureWidth, (float)m_textureHeight) * m_zoom);
m_panOffsetUV = m_dragStartPanOffsetUV - uvDelta; // Subtract because dragging right moves content left (reduces UV offset)
ClampPanOffset(containerSize); // Clamp during drag
} else if (m_isDragging) {
m_isDragging = false;
}
// --- UV Calculation ---
// Calculate the size of the viewable rectangle in UV coordinates
ImVec2 viewSizeUV = displaySize / (ImVec2((float)m_textureWidth, (float)m_textureHeight) * m_zoom);
// Calculate top-left (uv0) and bottom-right (uv1) UV coordinates based on the centered pan offset
ImVec2 uv0 = m_panOffsetUV - viewSizeUV * 0.5f;
ImVec2 uv1 = m_panOffsetUV + viewSizeUV * 0.5f;
// --- Draw the Image ---
ImGui::SetCursorScreenPos(displayTopLeftScreen); // Position cursor for image drawing
ImGui::Image((ImTextureID)(intptr_t)m_textureId, displaySize, uv0, uv1);
ImGui::EndChild();
ImGui::PopStyleColor(); // Pop ChildBg color
}
#endif // IMGUI_IMAGE_VIEWER_IMPLEMENTATION
#endif // IMGUI_IMAGE_VIEWER_H