main.cpp ``` // Dear ImGui: standalone example application for SDL2 + OpenGL // (SDL is a cross-platform general purpose library for handling windows, inputs, OpenGL/Vulkan/Metal graphics context creation, etc.) // Learn about Dear ImGui: // - FAQ https://dearimgui.com/faq // - Getting Started https://dearimgui.com/getting-started // - Documentation https://dearimgui.com/docs (same as your local docs/ folder). // - Introduction, links and more at the top of imgui.cpp #define IMGUI_DEFINE_MATH_OPERATORS #include #include "imgui.h" #include "imgui_impl_sdl2.h" #include "imgui_impl_opengl3.h" #include #include #if defined(IMGUI_IMPL_OPENGL_ES2) #include #else #include #include #endif // This example can also compile and run with Emscripten! See 'Makefile.emscripten' for details. #ifdef __EMSCRIPTEN__ #include "../libs/emscripten/emscripten_mainloop_stub.h" #endif #include "exif.h" #define APP_IMAGE_IMPLEMENTATION #define IMGUI_IMAGE_VIEWER_IMPLEMENTATION #include "app_image.h" #include "tex_inspect_opengl.h" #include "imgui_tex_inspect.h" #include "shaderutils.h" static float exposure = 0.0f; static float contrast = 0.0f; static float highlights = 0.0f; static float shadows = 0.0f; static float whites = 0.0f; static float blacks = 0.0f; static float temperature = 6500.0f; // Example starting point (Kelvin) static float tint = 0.0f; static float vibrance = 0.0f; static float saturation = 0.0f; static float clarity = 0.0f; static float texture = 0.0f; static float dehaze = 0.0f; #include #include #include #include // For std::function #include // For unique_ptr #include "imfilebrowser.h" // <<< Add this #include // <<< Add for path manipulation (C++17) struct ShaderUniform { std::string name; GLint location = -1; // Add type info if needed for different glUniform calls, or handle in setter }; struct PipelineOperation { std::string name; GLuint shaderProgram = 0; bool enabled = true; std::map uniforms; // Map uniform name to its info // Function to update uniforms based on global slider values etc. std::function updateUniformsCallback; // Store the actual slider variable pointers for direct modification in ImGui // This avoids needing complex callbacks for simple sliders float *exposureVal = nullptr; float *contrastVal = nullptr; float *highlightsVal = nullptr; float *shadowsVal = nullptr; float *whitesVal = nullptr; float *blacksVal = nullptr; float *temperatureVal = nullptr; float *tintVal = nullptr; float *vibranceVal = nullptr; float *saturationVal = nullptr; float *clarityVal = nullptr; float *textureVal = nullptr; float *dehazeVal = nullptr; // ... add pointers for other controls as needed PipelineOperation(std::string n) : name(std::move(n)) {} void FindUniformLocations() { if (!shaderProgram) return; for (auto &pair : uniforms) { pair.second.location = glGetUniformLocation(shaderProgram, pair.second.name.c_str()); if (pair.second.location == -1 && name != "Passthrough" && name != "LinearToSRGB" && name != "SRGBToLinear") { // Ignore for simple shaders // Don't treat missing texture samplers as errors here, they are set explicitly if (pair.second.name != "InputTexture") { fprintf(stderr, "Warning: Uniform '%s' not found in shader '%s'\n", pair.second.name.c_str(), name.c_str()); } } } } }; // Enum for Color Spaces (expand later) enum class ColorSpace { LINEAR_SRGB, // Linear Rec.709/sRGB primaries SRGB // Non-linear sRGB (display) // Add AdobeRGB, ProPhoto etc. later }; const char *ColorSpaceToString(ColorSpace cs) { switch (cs) { case ColorSpace::LINEAR_SRGB: return "Linear sRGB"; case ColorSpace::SRGB: return "sRGB"; default: return "Unknown"; } } bool ReadTextureToAppImage(GLuint textureId, int width, int height, AppImage &outImage) { if (textureId == 0 || width <= 0 || height <= 0) { fprintf(stderr, "ReadTextureToAppImage: Invalid parameters.\n"); return false; } // We assume the texture 'textureId' holds LINEAR RGBA FLOAT data (e.g., GL_RGBA16F) // Resize AppImage to hold the data outImage.resize(width, height, 4); // Expecting 4 channels (RGBA) from pipeline texture outImage.m_isLinear = true; // Data we read back should be linear outImage.m_colorSpaceName = "Linear sRGB"; // Assuming pipeline used sRGB primaries std::vector &pixelData = outImage.getPixelVector(); if (pixelData.empty()) { fprintf(stderr, "ReadTextureToAppImage: Failed to allocate AppImage buffer.\n"); return false; } // Bind the texture GLint lastTexture; glGetIntegerv(GL_TEXTURE_BINDING_2D, &lastTexture); glBindTexture(GL_TEXTURE_2D, textureId); // Set alignment (good practice) glPixelStorei(GL_PACK_ALIGNMENT, 1); // Read the pixels // We request GL_RGBA and GL_FLOAT as that's our assumed linear working format on GPU glGetTexImage(GL_TEXTURE_2D, 0, // Mipmap level 0 GL_RGBA, // Request RGBA format GL_FLOAT, // Request float data type pixelData.data()); // Pointer to destination buffer GLenum err = glGetError(); glBindTexture(GL_TEXTURE_2D, lastTexture); // Restore previous binding if (err != GL_NO_ERROR) { fprintf(stderr, "ReadTextureToAppImage: OpenGL Error during glGetTexImage: %u\n", err); outImage.clear_image(); // Clear invalid data return false; } printf("ReadTextureToAppImage: Successfully read %dx%d texture.\n", width, height); return true; } class ImageProcessingPipeline { private: GLuint m_fbo[2] = {0, 0}; GLuint m_tex[2] = {0, 0}; // Ping-pong textures GLuint m_vao = 0; GLuint m_vbo = 0; int m_texWidth = 0; int m_texHeight = 0; GLuint m_passthroughShader = 0; GLuint m_linearToSrgbShader = 0; GLuint m_srgbToLinearShader = 0; void CreateFullscreenQuad() { // Simple quad covering -1 to 1 in x,y and 0 to 1 in u,v float vertices[] = { // positions // texCoords -1.0f, 1.0f, 0.0f, 1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f}; printf("Matrix ready.\n"); glGenVertexArrays(1, &m_vao); printf("Fullscreen quad VAO created.\n"); glGenBuffers(1, &m_vbo); printf("Fullscreen quad VBO created.\n"); glBindVertexArray(m_vao); glBindBuffer(GL_ARRAY_BUFFER, m_vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); printf("Fullscreen quad VBO created.\n"); // Position attribute glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)0); glEnableVertexAttribArray(0); // Texture coordinate attribute glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)(2 * sizeof(float))); glEnableVertexAttribArray(1); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); printf("Fullscreen quad VAO/VBO created.\n"); } void CreateOrResizeFBOs(int width, int height) { if (width == m_texWidth && height == m_texHeight && m_fbo[0] != 0) { return; // Already correct size } if (width <= 0 || height <= 0) return; // Invalid dimensions // Cleanup existing DestroyFBOs(); m_texWidth = width; m_texHeight = height; glGenFramebuffers(2, m_fbo); glGenTextures(2, m_tex); GLint lastTexture; glGetIntegerv(GL_TEXTURE_BINDING_2D, &lastTexture); GLint lastFBO; glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &lastFBO); // Or GL_FRAMEBUFFER_BINDING for (int i = 0; i < 2; ++i) { glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[i]); glBindTexture(GL_TEXTURE_2D, m_tex[i]); // Create floating point texture glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // Use NEAREST for processing steps glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // Attach texture to FBO glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_tex[i], 0); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { fprintf(stderr, "ERROR::FRAMEBUFFER:: Framebuffer %d is not complete!\n", i); DestroyFBOs(); // Clean up partial setup glBindTexture(GL_TEXTURE_2D, lastTexture); glBindFramebuffer(GL_FRAMEBUFFER, lastFBO); return; } else { printf("FBO %d (Texture %d) created successfully (%dx%d).\n", m_fbo[i], m_tex[i], width, height); } } glBindTexture(GL_TEXTURE_2D, lastTexture); glBindFramebuffer(GL_FRAMEBUFFER, lastFBO); } void DestroyFBOs() { if (m_fbo[0]) glDeleteFramebuffers(2, m_fbo); if (m_tex[0]) glDeleteTextures(2, m_tex); m_fbo[0] = m_fbo[1] = 0; m_tex[0] = m_tex[1] = 0; m_texWidth = m_texHeight = 0; printf("Destroyed FBOs and textures.\n"); } public: // The ordered list of operations the user has configured std::vector activeOperations; ColorSpace inputColorSpace = ColorSpace::LINEAR_SRGB; // Default based on AppImage goal ColorSpace outputColorSpace = ColorSpace::SRGB; // Default for display ImageProcessingPipeline() = default; ~ImageProcessingPipeline() { DestroyFBOs(); if (m_vao) glDeleteVertexArrays(1, &m_vao); if (m_vbo) glDeleteBuffers(1, &m_vbo); // Shaders owned by PipelineOperation structs should be deleted externally or via smart pointers if (m_passthroughShader) glDeleteProgram(m_passthroughShader); if (m_linearToSrgbShader) glDeleteProgram(m_linearToSrgbShader); if (m_srgbToLinearShader) glDeleteProgram(m_srgbToLinearShader); printf("ImageProcessingPipeline destroyed.\n"); } void Init(const std::string &shaderBasePath) { printf("Initializing ImageProcessingPipeline...\n"); CreateFullscreenQuad(); printf("Fullscreen quad created.\n"); // Load essential shaders std::string vsPath = shaderBasePath + "passthrough.vert"; printf("Loading shaders from: %s\n", vsPath.c_str()); m_passthroughShader = LoadShaderProgramFromFiles(vsPath, shaderBasePath + "passthrough.frag"); m_linearToSrgbShader = LoadShaderProgramFromFiles(vsPath, shaderBasePath + "linear_to_srgb.frag"); m_srgbToLinearShader = LoadShaderProgramFromFiles(vsPath, shaderBasePath + "srgb_to_linear.frag"); printf("Loaded shaders: %s, %s, %s\n", vsPath.c_str(), (shaderBasePath + "linear_to_srgb.frag").c_str(), (shaderBasePath + "srgb_to_linear.frag").c_str()); if (!m_passthroughShader || !m_linearToSrgbShader || !m_srgbToLinearShader) { fprintf(stderr, "Failed to load essential pipeline shaders!\n"); } else { printf("Essential pipeline shaders loaded.\n"); } } void ResetResources() { printf("Pipeline: Resetting FBOs and Textures.\n"); DestroyFBOs(); // Call the existing cleanup method } // Call this each frame to process the image // Returns the Texture ID of the final processed image GLuint ProcessImage(GLuint inputTextureId, int width, int height, bool applyOutputConversion = true) { if (inputTextureId == 0 || width <= 0 || height <= 0) { return 0; // No input or invalid size } CreateOrResizeFBOs(width, height); if (m_fbo[0] == 0) { fprintf(stderr, "FBOs not ready, cannot process image.\n"); return 0; // FBOs not ready } // Store original viewport and FBO to restore later GLint viewport[4]; glGetIntegerv(GL_VIEWPORT, viewport); GLint lastFBO; glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &lastFBO); glViewport(0, 0, m_texWidth, m_texHeight); glBindVertexArray(m_vao); // Bind the quad VAO once int currentSourceTexIndex = 0; // Start with texture m_tex[0] as the first *write* target GLuint currentReadTexId = inputTextureId; // Initially read from the original image // --- Input Color Space Conversion --- bool inputConversionDone = false; if (inputColorSpace == ColorSpace::SRGB) { printf("Pipeline: Applying sRGB -> Linear conversion.\n"); glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentSourceTexIndex]); glUseProgram(m_srgbToLinearShader); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_1D, currentReadTexId); glUniform1i(glGetUniformLocation(m_srgbToLinearShader, "InputTexture"), 0); glDrawArrays(GL_TRIANGLES, 0, 6); currentReadTexId = m_tex[currentSourceTexIndex]; // Next read is from the texture we just wrote to currentSourceTexIndex = 1 - currentSourceTexIndex; // Swap target FBO/texture inputConversionDone = true; } else { printf("Pipeline: Input is Linear, no conversion needed.\n"); // If input is already linear, we might need to copy it to the first FBO texture // if there are actual processing steps, otherwise the first step reads the original. // This copy ensures the ping-pong works correctly even if the first *user* step is disabled. // However, if NO user steps are enabled, we want to display the original (potentially with output conversion). bool anyUserOpsEnabled = false; for (const auto &op : activeOperations) { if (op.enabled && op.shaderProgram && op.name != "Passthrough") { // Check it's a real operation anyUserOpsEnabled = true; break; } } if (anyUserOpsEnabled) { // Need to copy original linear input into the pipeline's texture space printf("Pipeline: Copying linear input to FBO texture for processing.\n"); glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentSourceTexIndex]); glUseProgram(m_passthroughShader); // Use simple passthrough glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, currentReadTexId); glUniform1i(glGetUniformLocation(m_passthroughShader, "InputTexture"), 0); glDrawArrays(GL_TRIANGLES, 0, 6); currentReadTexId = m_tex[currentSourceTexIndex]; currentSourceTexIndex = 1 - currentSourceTexIndex; inputConversionDone = true; } else { // No user ops, keep reading directly from original inputTextureId inputConversionDone = false; // Treat as if no initial step happened yet printf("Pipeline: No enabled user operations, skipping initial copy.\n"); } } // --- Apply Editing Operations --- int appliedOps = 0; for (const auto &op : activeOperations) { if (op.enabled && op.shaderProgram) { printf("Pipeline: Applying operation: %s\n", op.name.c_str()); glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentSourceTexIndex]); glUseProgram(op.shaderProgram); // Set Input Texture Sampler glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, currentReadTexId); GLint loc = glGetUniformLocation(op.shaderProgram, "InputTexture"); if (loc != -1) glUniform1i(loc, 0); else if (op.name != "Passthrough") fprintf(stderr, "Warning: InputTexture uniform not found in shader %s\n", op.name.c_str()); // Set operation-specific uniforms if (op.updateUniformsCallback) { op.updateUniformsCallback(op.shaderProgram); } else { // Alternative: Set uniforms directly based on stored pointers if (op.exposureVal && op.uniforms.count("exposureValue")) { glUniform1f(op.uniforms.at("exposureValue").location, *op.exposureVal); } if (op.contrastVal && op.uniforms.count("contrastValue")) { glUniform1f(op.uniforms.at("contrastValue").location, *op.contrastVal); } if (op.clarityVal && op.uniforms.count("clarityValue")) { glUniform1f(op.uniforms.at("clarityValue").location, *op.clarityVal); } if (op.highlightsVal && op.uniforms.count("highlightsValue")) { glUniform1f(op.uniforms.at("highlightsValue").location, *op.highlightsVal); } if (op.shadowsVal && op.uniforms.count("shadowsValue")) { glUniform1f(op.uniforms.at("shadowsValue").location, *op.shadowsVal); } if (op.whitesVal && op.uniforms.count("whitesValue")) { glUniform1f(op.uniforms.at("whitesValue").location, *op.whitesVal); } if (op.blacksVal && op.uniforms.count("blacksValue")) { glUniform1f(op.uniforms.at("blacksValue").location, *op.blacksVal); } if (op.textureVal && op.uniforms.count("textureValue")) { glUniform1f(op.uniforms.at("textureValue").location, *op.textureVal); } if (op.dehazeVal && op.uniforms.count("dehazeValue")) { glUniform1f(op.uniforms.at("dehazeValue").location, *op.dehazeVal); } if (op.saturationVal && op.uniforms.count("saturationValue")) { glUniform1f(op.uniforms.at("saturationValue").location, *op.saturationVal); } if (op.vibranceVal && op.uniforms.count("vibranceValue")) { glUniform1f(op.uniforms.at("vibranceValue").location, *op.vibranceVal); } if (op.temperatureVal && op.uniforms.count("temperatureValue")) { glUniform1f(op.uniforms.at("temperatureValue").location, *op.temperatureVal); } if (op.tintVal && op.uniforms.count("tintValue")) { glUniform1f(op.uniforms.at("tintValue").location, *op.tintVal); } } glDrawArrays(GL_TRIANGLES, 0, 6); // Prepare for next pass currentReadTexId = m_tex[currentSourceTexIndex]; // Next pass reads from the texture we just wrote currentSourceTexIndex = 1 - currentSourceTexIndex; // Swap FBO target appliedOps++; } } // If no user ops were applied AND no input conversion happened, // currentReadTexId is still the original inputTextureId. if (appliedOps == 0 && !inputConversionDone) { printf("Pipeline: No operations applied, output = input (%d).\n", currentReadTexId); // Proceed to output conversion using original inputTextureId } else if (appliedOps > 0 || inputConversionDone) { printf("Pipeline: %d operations applied, final intermediate texture ID: %d\n", appliedOps, currentReadTexId); // currentReadTexId now holds the result of the last applied operation (or the input conversion) } else { // This case should ideally not be reached if logic above is correct printf("Pipeline: Inconsistent state after processing loop.\n"); } // --- Output Color Space Conversion --- GLuint finalTextureId = currentReadTexId; // Assume this is the final one unless converted if (applyOutputConversion) { if (outputColorSpace == ColorSpace::SRGB) { // Check if the last written data (currentReadTexId) is already sRGB. // In this simple setup, it's always linear *unless* no ops applied and input was sRGB. // More robustly: Track the color space through the pipeline. // For now, assume currentReadTexId holds linear data if any op or input conversion happened. bool needsLinearToSrgb = (appliedOps > 0 || inputConversionDone); if (!needsLinearToSrgb && inputColorSpace == ColorSpace::SRGB) { printf("Pipeline: Output is sRGB, and input was sRGB with no ops, no final conversion needed.\n"); // Input was sRGB, no ops applied, output should be sRGB. currentReadTexId is original sRGB input. finalTextureId = currentReadTexId; } else if (needsLinearToSrgb) { printf("Pipeline: Applying Linear -> sRGB conversion for output.\n"); glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentSourceTexIndex]); // Use the *next* FBO for the final write glUseProgram(m_linearToSrgbShader); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, currentReadTexId); // Read the last result glUniform1i(glGetUniformLocation(m_linearToSrgbShader, "InputTexture"), 0); glDrawArrays(GL_TRIANGLES, 0, 6); finalTextureId = m_tex[currentSourceTexIndex]; // The final result is in this texture } else { // Input was linear, no ops, output requires sRGB. printf("Pipeline: Input Linear, no ops, applying Linear -> sRGB conversion for output.\n"); glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentSourceTexIndex]); glUseProgram(m_linearToSrgbShader); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, currentReadTexId); // Read original linear input glUniform1i(glGetUniformLocation(m_linearToSrgbShader, "InputTexture"), 0); glDrawArrays(GL_TRIANGLES, 0, 6); finalTextureId = m_tex[currentSourceTexIndex]; } } else { printf("Pipeline: Output is Linear, no final conversion needed.\n"); // If output should be linear, finalTextureId is already correct (it's currentReadTexId) finalTextureId = currentReadTexId; } } else { printf("Pipeline: Skipped output conversion. Final (linear) ID: %d\n", finalTextureId); } // --- Cleanup --- glBindVertexArray(0); glBindFramebuffer(GL_FRAMEBUFFER, lastFBO); // Restore original framebuffer binding glViewport(viewport[0], viewport[1], viewport[2], viewport[3]); // Restore viewport glUseProgram(0); // Unbind shader program printf("Pipeline: ProcessImage returning final texture ID: %d\n", finalTextureId); return finalTextureId; } }; static ImageProcessingPipeline g_pipeline; // <<< Global pipeline manager instance static std::vector> g_allOperations; // Store all possible operations static GLuint g_processedTextureId = 0; // Texture ID after pipeline processing static ColorSpace g_inputColorSpace = ColorSpace::LINEAR_SRGB; // Connect to pipeline's setting static ColorSpace g_outputColorSpace = ColorSpace::SRGB; // Connect to pipeline's setting // File Dialogs static ImGui::FileBrowser g_openFileDialog; // Add flags for save dialog: Allow new filename, allow creating directories static ImGui::FileBrowser g_exportSaveFileDialog(ImGuiFileBrowserFlags_EnterNewFilename | ImGuiFileBrowserFlags_CreateNewDir); // Export Dialog State static bool g_showExportWindow = false; static ImageSaveFormat g_exportFormat = ImageSaveFormat::JPEG; // Default format static int g_exportQuality = 90; // Default JPEG quality static std::string g_exportErrorMsg = ""; // To display errors in the export dialog // Current loaded file path (useful for default export name) static std::string g_currentFilePath = ""; // Crop State static bool g_cropActive = false; static ImVec4 g_cropRectNorm = ImVec4(0.0f, 0.0f, 1.0f, 1.0f); // (MinX, MinY, MaxX, MaxY) normalized 0-1 static ImVec4 g_cropRectNormInitial = g_cropRectNorm; // Store initial state for cancel/dragging base static float g_cropAspectRatio = 0.0f; // 0.0f = Freeform, > 0.0f = constrained (Width / Height) static int g_selectedAspectRatioIndex = 0; // Index for the dropdown static GLuint g_histogramComputeShader = 0; static GLuint g_histogramSSBO = 0; const int NUM_HISTOGRAM_BINS = 256; const int HISTOGRAM_BUFFER_SIZE = NUM_HISTOGRAM_BINS * 3; // R, G, B static std::vector g_histogramDataCPU(HISTOGRAM_BUFFER_SIZE, 0); static unsigned int g_histogramMaxCount = 255; // Max count found, for scaling (init to 1 to avoid div by zero) static bool g_histogramResourcesInitialized = false; // Interaction state enum class CropHandle { NONE, TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT, TOP, BOTTOM, LEFT, RIGHT, INSIDE }; static CropHandle g_activeCropHandle = CropHandle::NONE; static bool g_isDraggingCrop = false; static ImVec2 g_dragStartMousePos = ImVec2(0, 0); // Screen coords bool InitHistogramResources(const std::string& shaderBasePath) { printf("Initializing Histogram Resources...\n"); // Load Compute Shader // We need a way to load compute shaders, modify shader_utils or add here std::string compSource = ReadFile(shaderBasePath + "histogram.comp"); // Assuming ReadFile exists if (compSource.empty()) { fprintf(stderr, "ERROR: Failed to read histogram.comp\n"); return false; } // Simple Compute Shader Compilation/Linking (add error checking!) GLuint computeShaderObj = glCreateShader(GL_COMPUTE_SHADER); const char* src = compSource.c_str(); glShaderSource(computeShaderObj, 1, &src, nullptr); glCompileShader(computeShaderObj); // --- Add GLint success; glGetShaderiv; glGetShaderInfoLog checks --- GLint success; glGetShaderiv(computeShaderObj, GL_COMPILE_STATUS, &success); if (!success) { GLint logLength; glGetShaderiv(computeShaderObj, GL_INFO_LOG_LENGTH, &logLength); std::vector log(logLength); glGetShaderInfoLog(computeShaderObj, logLength, nullptr, log.data()); fprintf(stderr, "ERROR::SHADER::HISTOGRAM::COMPILATION_FAILED\n%s\n", log.data()); glDeleteShader(computeShaderObj); return false; } g_histogramComputeShader = glCreateProgram(); glAttachShader(g_histogramComputeShader, computeShaderObj); glLinkProgram(g_histogramComputeShader); // --- Add GLint success; glGetProgramiv; glGetProgramInfoLog checks --- glGetProgramiv(g_histogramComputeShader, GL_LINK_STATUS, &success); if (!success) { GLint logLength; glGetProgramiv(g_histogramComputeShader, GL_INFO_LOG_LENGTH, &logLength); std::vector log(logLength); glGetProgramInfoLog(g_histogramComputeShader, logLength, nullptr, log.data()); fprintf(stderr, "ERROR::PROGRAM::HISTOGRAM::LINKING_FAILED\n%s\n", log.data()); glDeleteProgram(g_histogramComputeShader); g_histogramComputeShader = 0; glDeleteShader(computeShaderObj); // Delete shader obj even on link failure return false; } glDeleteShader(computeShaderObj); // Delete shader object after linking printf("Histogram compute shader loaded and linked successfully (Program ID: %u).\n", g_histogramComputeShader); // Create Shader Storage Buffer Object (SSBO) glGenBuffers(1, &g_histogramSSBO); glBindBuffer(GL_SHADER_STORAGE_BUFFER, g_histogramSSBO); // Allocate buffer size: 3 channels * 256 bins * size of uint glBufferData(GL_SHADER_STORAGE_BUFFER, HISTOGRAM_BUFFER_SIZE * sizeof(unsigned int), NULL, GL_DYNAMIC_READ); // Data will be written by GPU, read by CPU glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); // Unbind GLenum err = glGetError(); if (err != GL_NO_ERROR || g_histogramSSBO == 0) { fprintf(stderr, "ERROR: Failed to create histogram SSBO. OpenGL Error: %u\n", err); if (g_histogramComputeShader) glDeleteProgram(g_histogramComputeShader); g_histogramComputeShader = 0; return false; } else { printf("Histogram SSBO created successfully (Buffer ID: %u, Size: %d bytes).\n", g_histogramSSBO, HISTOGRAM_BUFFER_SIZE * sizeof(unsigned int)); } g_histogramResourcesInitialized = true; return true; } // Aspect Ratio Options struct AspectRatioOption { const char *name; float ratio; // W/H }; static std::vector g_aspectRatios = { {"Freeform", 0.0f}, {"Original", 0.0f}, // Will be calculated dynamically {"1:1", 1.0f}, {"16:9", 16.0f / 9.0f}, {"9:16", 9.0f / 16.0f}, {"4:3", 4.0f / 3.0f}, {"3:4", 3.0f / 4.0f}, // Add more as needed }; void UpdateCropRect(ImVec4& rectNorm, CropHandle handle, ImVec2 deltaNorm, float aspectRatio) { ImVec2 minXY = ImVec2(rectNorm.x, rectNorm.y); ImVec2 maxXY = ImVec2(rectNorm.z, rectNorm.w); // Apply delta based on handle switch (handle) { case CropHandle::TOP_LEFT: minXY += deltaNorm; break; case CropHandle::TOP_RIGHT: minXY.y += deltaNorm.y; maxXY.x += deltaNorm.x; break; case CropHandle::BOTTOM_LEFT: minXY.x += deltaNorm.x; maxXY.y += deltaNorm.y; break; case CropHandle::BOTTOM_RIGHT: maxXY += deltaNorm; break; case CropHandle::TOP: minXY.y += deltaNorm.y; break; case CropHandle::BOTTOM: maxXY.y += deltaNorm.y; break; case CropHandle::LEFT: minXY.x += deltaNorm.x; break; case CropHandle::RIGHT: maxXY.x += deltaNorm.x; break; case CropHandle::INSIDE: minXY += deltaNorm; maxXY += deltaNorm; break; case CropHandle::NONE: return; // No change } // Ensure min < max temporarily before aspect constraint if (minXY.x > maxXY.x) ImSwap(minXY.x, maxXY.x); if (minXY.y > maxXY.y) ImSwap(minXY.y, maxXY.y); // Apply Aspect Ratio Constraint (if aspectRatio > 0) if (aspectRatio > 0.0f && handle != CropHandle::INSIDE && handle != CropHandle::NONE) { float currentW = maxXY.x - minXY.x; float currentH = maxXY.y - minXY.y; if (currentW < 1e-5f) currentW = 1e-5f; // Avoid division by zero if (currentH < 1e-5f) currentH = 1e-5f; float currentAspect = currentW / currentH; float targetAspect = aspectRatio; // Determine which dimension to adjust based on which handle was moved and aspect delta // Simplified approach: Adjust height based on width, unless moving top/bottom handles primarily bool adjustHeight = true; if (handle == CropHandle::TOP || handle == CropHandle::BOTTOM) { adjustHeight = false; // Primarily adjust width based on height change } if (adjustHeight) { // Adjust height based on width float targetH = currentW / targetAspect; float deltaH = targetH - currentH; // Distribute height change based on handle if (handle == CropHandle::TOP_LEFT || handle == CropHandle::TOP_RIGHT || handle == CropHandle::TOP) { minXY.y -= deltaH; // Adjust top edge } else { maxXY.y += deltaH; // Adjust bottom edge (or split for side handles?) // For LEFT/RIGHT handles, could split deltaH: minXY.y -= deltaH*0.5; maxXY.y += deltaH*0.5; } } else { // Adjust width based on height float targetW = currentH * targetAspect; float deltaW = targetW - currentW; // Distribute width change based on handle if (handle == CropHandle::TOP_LEFT || handle == CropHandle::BOTTOM_LEFT || handle == CropHandle::LEFT) { minXY.x -= deltaW; // Adjust left edge } else { maxXY.x += deltaW; // Adjust right edge // For TOP/BOTTOM handles, could split deltaW: minXY.x -= deltaW*0.5; maxXY.x += deltaW*0.5; } } } // End aspect ratio constraint // Update the output rectNorm rectNorm = ImVec4(minXY.x, minXY.y, maxXY.x, maxXY.y); } // Helper function to crop AppImage data bool ApplyCropToImage(AppImage& image, const ImVec4 cropRectNorm) { if (image.isEmpty()) { fprintf(stderr, "ApplyCropToImage: Input image is empty.\n"); return false; } if (cropRectNorm.x >= cropRectNorm.z || cropRectNorm.y >= cropRectNorm.w) { fprintf(stderr, "ApplyCropToImage: Invalid crop rectangle (zero or negative size).\n"); return false; // Invalid crop rect } // Clamp rect just in case ImVec4 clampedRect = cropRectNorm; clampedRect.x = ImClamp(clampedRect.x, 0.0f, 1.0f); clampedRect.y = ImClamp(clampedRect.y, 0.0f, 1.0f); clampedRect.z = ImClamp(clampedRect.z, 0.0f, 1.0f); clampedRect.w = ImClamp(clampedRect.w, 0.0f, 1.0f); // Calculate pixel coordinates int srcW = image.getWidth(); int srcH = image.getHeight(); int channels = image.getChannels(); int cropX_px = static_cast(round(clampedRect.x * srcW)); int cropY_px = static_cast(round(clampedRect.y * srcH)); int cropMaxX_px = static_cast(round(clampedRect.z * srcW)); int cropMaxY_px = static_cast(round(clampedRect.w * srcH)); int cropW_px = cropMaxX_px - cropX_px; int cropH_px = cropMaxY_px - cropY_px; if (cropW_px <= 0 || cropH_px <= 0) { fprintf(stderr, "ApplyCropToImage: Resulting crop size is zero or negative (%dx%d).\n", cropW_px, cropH_px); return false; } printf("Applying crop: Start=(%d,%d), Size=(%dx%d)\n", cropX_px, cropY_px, cropW_px, cropH_px); // Create new image for cropped data AppImage croppedImage(cropW_px, cropH_px, channels); if (croppedImage.isEmpty()) { fprintf(stderr, "ApplyCropToImage: Failed to allocate memory for cropped image.\n"); return false; } croppedImage.m_isLinear = image.isLinear(); // Preserve flags croppedImage.m_colorSpaceName = image.getColorSpaceName(); // TODO: Copy metadata/ICC profile if needed? Cropping usually invalidates some metadata. const float* srcData = image.getData(); float* dstData = croppedImage.getData(); // Copy pixel data row by row, channel by channel for (int y_dst = 0; y_dst < cropH_px; ++y_dst) { int y_src = cropY_px + y_dst; // Ensure source Y is valid (should be due to clamping/checks, but be safe) if (y_src < 0 || y_src >= srcH) continue; // Calculate start pointers for source and destination rows const float* srcRowStart = srcData + (static_cast(y_src) * srcW + cropX_px) * channels; float* dstRowStart = dstData + (static_cast(y_dst) * cropW_px) * channels; // Copy the entire row (width * channels floats) std::memcpy(dstRowStart, srcRowStart, static_cast(cropW_px) * channels * sizeof(float)); } // Replace the original image data with the cropped data // Use std::move if AppImage supports move assignment for efficiency image = std::move(croppedImage); printf("Cropped image created successfully (%dx%d).\n", image.getWidth(), image.getHeight()); return true; } void InitShaderOperations(const std::string &shaderBasePath) { // Clear existing (if any) g_allOperations.clear(); g_pipeline.activeOperations.clear(); // Also clear the active list in the pipeline // --- Define Operations --- // Use unique_ptr for automatic memory management // Match uniform names to the GLSL shaders auto whiteBalanceOp = std::make_unique("White Balance"); whiteBalanceOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "white_balance.frag"); if (whiteBalanceOp->shaderProgram) { whiteBalanceOp->uniforms["temperatureValue"] = {"temperature"}; whiteBalanceOp->uniforms["tintValue"] = {"tint"}; whiteBalanceOp->temperatureVal = &temperature; whiteBalanceOp->tintVal = ∭ whiteBalanceOp->FindUniformLocations(); g_allOperations.push_back(std::move(whiteBalanceOp)); printf(" + Loaded White Balance\n"); } else printf(" - FAILED White Balance\n"); auto exposureOp = std::make_unique("Exposure"); exposureOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "exposure.frag"); exposureOp->uniforms["exposureValue"] = {"exposureValue"}; exposureOp->exposureVal = &exposure; // Link to global slider variable exposureOp->FindUniformLocations(); g_allOperations.push_back(std::move(exposureOp)); auto contrastOp = std::make_unique("Contrast"); contrastOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "contrast.frag"); if (contrastOp->shaderProgram) { contrastOp->uniforms["contrastValue"] = {"contrastValue"}; contrastOp->contrastVal = &contrast; contrastOp->FindUniformLocations(); g_allOperations.push_back(std::move(contrastOp)); printf(" + Loaded Contrast\n"); } else printf(" - FAILED Contrast\n"); auto highlightsShadowsOp = std::make_unique("Highlights/Shadows"); highlightsShadowsOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "highlights_shadows.frag"); if (highlightsShadowsOp->shaderProgram) { highlightsShadowsOp->uniforms["highlightsValue"] = {"highlightsValue"}; highlightsShadowsOp->uniforms["shadowsValue"] = {"shadowsValue"}; highlightsShadowsOp->highlightsVal = &highlights; highlightsShadowsOp->shadowsVal = &shadows; highlightsShadowsOp->FindUniformLocations(); g_allOperations.push_back(std::move(highlightsShadowsOp)); printf(" + Loaded Highlights/Shadows\n"); } else printf(" - FAILED Highlights/Shadows\n"); auto whiteBlackOp = std::make_unique("Whites/Blacks"); whiteBlackOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "whites_blacks.frag"); if (whiteBlackOp->shaderProgram) { whiteBlackOp->uniforms["whitesValue"] = {"whitesValue"}; whiteBlackOp->uniforms["blacksValue"] = {"blacksValue"}; whiteBlackOp->whitesVal = &whites; whiteBlackOp->blacksVal = &blacks; whiteBlackOp->FindUniformLocations(); g_allOperations.push_back(std::move(whiteBlackOp)); printf(" + Loaded Whites/Blacks\n"); } else printf(" - FAILED Whites/Blacks\n"); auto textureOp = std::make_unique("Texture"); textureOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "texture.frag"); if (textureOp->shaderProgram) { textureOp->uniforms["textureValue"] = {"textureValue"}; textureOp->textureVal = &texture; textureOp->FindUniformLocations(); g_allOperations.push_back(std::move(textureOp)); printf(" + Loaded Texture\n"); } else printf(" - FAILED Texture\n"); auto clarityOp = std::make_unique("Clarity"); clarityOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "clarity.frag"); if (clarityOp->shaderProgram) { clarityOp->uniforms["clarityValue"] = {"clarityValue"}; clarityOp->clarityVal = &clarity; clarityOp->FindUniformLocations(); g_allOperations.push_back(std::move(clarityOp)); printf(" + Loaded Clarity\n"); } else printf(" - FAILED Clarity\n"); auto dehazeOp = std::make_unique("Dehaze"); dehazeOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "dehaze.frag"); if (dehazeOp->shaderProgram) { dehazeOp->uniforms["dehazeValue"] = {"dehazeValue"}; dehazeOp->dehazeVal = &dehaze; dehazeOp->FindUniformLocations(); g_allOperations.push_back(std::move(dehazeOp)); printf(" + Loaded Dehaze\n"); } else printf(" - FAILED Dehaze\n"); auto saturationOp = std::make_unique("Saturation"); saturationOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "saturation.frag"); if (saturationOp->shaderProgram) { saturationOp->uniforms["saturationValue"] = {"saturationValue"}; saturationOp->saturationVal = &saturation; saturationOp->FindUniformLocations(); g_allOperations.push_back(std::move(saturationOp)); printf(" + Loaded Saturation\n"); } else printf(" - FAILED Saturation\n"); auto vibranceOp = std::make_unique("Vibrance"); vibranceOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "vibrance.frag"); if (vibranceOp->shaderProgram) { vibranceOp->uniforms["vibranceValue"] = {"vibranceValue"}; vibranceOp->vibranceVal = &vibrance; vibranceOp->FindUniformLocations(); g_allOperations.push_back(std::move(vibranceOp)); printf(" + Loaded Vibrance\n"); } else printf(" - FAILED Vibrance\n"); g_pipeline.activeOperations.clear(); for (const auto &op_ptr : g_allOperations) { if (op_ptr) { // Make sure pointer is valid g_pipeline.activeOperations.push_back(*op_ptr); // Add a *copy* to the active list // Re-find locations for the copy (or ensure copy constructor handles it) g_pipeline.activeOperations.back().FindUniformLocations(); // Copy the pointers to the actual slider variables g_pipeline.activeOperations.back().exposureVal = op_ptr->exposureVal; g_pipeline.activeOperations.back().contrastVal = op_ptr->contrastVal; g_pipeline.activeOperations.back().clarityVal = op_ptr->clarityVal; g_pipeline.activeOperations.back().highlightsVal = op_ptr->highlightsVal; g_pipeline.activeOperations.back().shadowsVal = op_ptr->shadowsVal; g_pipeline.activeOperations.back().whitesVal = op_ptr->whitesVal; g_pipeline.activeOperations.back().blacksVal = op_ptr->blacksVal; g_pipeline.activeOperations.back().textureVal = op_ptr->textureVal; g_pipeline.activeOperations.back().dehazeVal = op_ptr->dehazeVal; g_pipeline.activeOperations.back().saturationVal = op_ptr->saturationVal; g_pipeline.activeOperations.back().vibranceVal = op_ptr->vibranceVal; g_pipeline.activeOperations.back().temperatureVal = op_ptr->temperatureVal; g_pipeline.activeOperations.back().tintVal = op_ptr->tintVal; // Set initial enabled state if needed (e.g., all enabled by default) g_pipeline.activeOperations.back().enabled = true; } } printf("Initialized %zu possible operations. %zu added to default active pipeline.\n", g_allOperations.size(), g_pipeline.activeOperations.size()); } // Add this function somewhere accessible, e.g., before main() void ComputeHistogramGPU(GLuint inputTextureID, int width, int height) { if (!g_histogramResourcesInitialized || inputTextureID == 0 || width <= 0 || height <= 0) { // Clear CPU data if not computed std::fill(g_histogramDataCPU.begin(), g_histogramDataCPU.end(), 0); g_histogramMaxCount = 1; printf("Histogram resources not initialized or invalid input. Skipping computation.\n"); return; } // 1. Clear the SSBO buffer data to zeros glBindBuffer(GL_SHADER_STORAGE_BUFFER, g_histogramSSBO); // Using glBufferSubData might be marginally faster than glClearBufferData if driver optimizes zeroing // static std::vector zeros(HISTOGRAM_BUFFER_SIZE, 0); // Create once // glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, HISTOGRAM_BUFFER_SIZE * sizeof(unsigned int), zeros.data()); // Or use glClearBufferData (often recommended) GLuint zero = 0; glClearBufferData(GL_SHADER_STORAGE_BUFFER, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT, &zero); glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); // Unbind // 2. Bind resources and dispatch compute shader glUseProgram(g_histogramComputeShader); // Bind input texture as image unit 0 (read-only) // IMPORTANT: Ensure the format matches the compute shader layout qualifier (e.g., rgba8) // If textureToDisplay is RGBA16F, you'd use layout(rgba16f) in shader glBindImageTexture(0, inputTextureID, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA16); // Assuming display texture is RGBA8 // Bind SSBO to binding point 1 glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, g_histogramSSBO); // Calculate number of work groups GLuint workGroupSizeX = 16; // Must match layout in shader GLuint workGroupSizeY = 16; GLuint numGroupsX = (width + workGroupSizeX - 1) / workGroupSizeX; GLuint numGroupsY = (height + workGroupSizeY - 1) / workGroupSizeY; // Dispatch the compute shader glDispatchCompute(numGroupsX, numGroupsY, 1); // 3. Synchronization: Ensure compute shader writes finish before CPU reads buffer // Use a memory barrier on the SSBO writes glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); // Unbind resources (optional here, but good practice) glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0); glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA16); glUseProgram(0); // 4. Read histogram data back from SSBO to CPU vector glBindBuffer(GL_SHADER_STORAGE_BUFFER, g_histogramSSBO); glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, HISTOGRAM_BUFFER_SIZE * sizeof(unsigned int), g_histogramDataCPU.data()); glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); // Unbind // 5. Find the maximum count for scaling the plot (optional, can be capped) g_histogramMaxCount = 255; // Reset to 255 (prevents div by zero) for (unsigned int count : g_histogramDataCPU) { if (count > g_histogramMaxCount) { g_histogramMaxCount = count; } } // Optional: Cap max count to prevent extreme peaks from flattening the rest // unsigned int capThreshold = (width * height) / 50; // e.g., cap at 2% of pixels // g_histogramMaxCount = std::min(g_histogramMaxCount, capThreshold); // if (g_histogramMaxCount == 0) g_histogramMaxCount = 1; // Ensure not zero after capping GLenum err = glGetError(); if (err != GL_NO_ERROR) { fprintf(stderr, "OpenGL Error during histogram computation/readback: %u\n", err); // Optionally clear CPU data on error std::fill(g_histogramDataCPU.begin(), g_histogramDataCPU.end(), 0); g_histogramMaxCount = 1; printf("Histogram computation failed. Data cleared.\n"); } else { printf("Histogram computed. Max count: %u\n", g_histogramMaxCount); } } // Add this function somewhere accessible, e.g., before main() void DrawHistogramWidget(const char* widgetId, ImVec2 graphSize) { if (g_histogramDataCPU.empty() || g_histogramMaxCount <= 1) { // Check if data is valid if (g_histogramDataCPU.empty()) { ImGui::Text("Histogram data not initialized."); } else { ImGui::Text("Histogram data is empty or invalid."); } if (g_histogramMaxCount <= 1) { ImGui::Text("Histogram max count is invalid."); } ImGui::Text("Histogram data not available."); return; } ImGui::PushID(widgetId); // Isolate widget IDs ImDrawList* drawList = ImGui::GetWindowDrawList(); const ImVec2 widgetPos = ImGui::GetCursorScreenPos(); // Determine actual graph size (negative values mean use available space) if (graphSize.x <= 0.0f) graphSize.x = ImGui::GetContentRegionAvail().x; if (graphSize.y <= 0.0f) graphSize.y = 100.0f; // Default height // Draw background for the histogram area (optional) drawList->AddRectFilled(widgetPos, widgetPos + graphSize, IM_COL32(30, 30, 30, 200)); // Calculate scaling factors float barWidth = graphSize.x / float(NUM_HISTOGRAM_BINS); float scaleY = graphSize.y / float(g_histogramMaxCount); // Scale based on max count // Define colors (with some transparency for overlap visibility) const ImU32 colR = IM_COL32(255, 0, 0, 180); const ImU32 colG = IM_COL32(0, 255, 0, 180); const ImU32 colB = IM_COL32(0, 0, 255, 180); // Draw the histogram bars (R, G, B) for (int i = 0; i < NUM_HISTOGRAM_BINS; ++i) { // Get heights (clamped to graph size) float hR = ImMin(float(g_histogramDataCPU[i]) * scaleY, graphSize.y); float hG = ImMin(float(g_histogramDataCPU[i + NUM_HISTOGRAM_BINS]) * scaleY, graphSize.y); float hB = ImMin(float(g_histogramDataCPU[i + NUM_HISTOGRAM_BINS * 2]) * scaleY, graphSize.y); // Calculate bar positions float x0 = widgetPos.x + float(i) * barWidth; float x1 = x0 + barWidth; // Use lines if bars are too thin, or thin rects float yBase = widgetPos.y + graphSize.y; // Bottom of the graph // Draw lines or thin rectangles (lines are often better for dense histograms) // Overlap/Blend: Draw B, then G, then R so Red is most prominent? Or use alpha blending. if (hB > 0) drawList->AddLine(ImVec2(x0 + barWidth * 0.5f, yBase), ImVec2(x0 + barWidth * 0.5f, yBase - hB), colB, 1.0f); if (hG > 0) drawList->AddLine(ImVec2(x0 + barWidth * 0.5f, yBase), ImVec2(x0 + barWidth * 0.5f, yBase - hG), colG, 1.0f); if (hR > 0) drawList->AddLine(ImVec2(x0 + barWidth * 0.5f, yBase), ImVec2(x0 + barWidth * 0.5f, yBase - hR), colR, 1.0f); // --- Alternative: Rectangles (might overlap heavily) --- // if (hB > 0) drawList->AddRectFilled(ImVec2(x0, yBase - hB), ImVec2(x1, yBase), colB); // if (hG > 0) drawList->AddRectFilled(ImVec2(x0, yBase - hG), ImVec2(x1, yBase), colG); // if (hR > 0) drawList->AddRectFilled(ImVec2(x0, yBase - hR), ImVec2(x1, yBase), colR); } // Draw border around the histogram area (optional) drawList->AddRect(widgetPos, widgetPos + graphSize, IM_COL32(150, 150, 150, 255)); // Advance cursor past the histogram widget area ImGui::Dummy(graphSize); ImGui::PopID(); // Restore ID stack } // Main code int main(int, char **) { // Setup SDL if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_GAMECONTROLLER) != 0) { printf("Error: %s\n", SDL_GetError()); return -1; } // Decide GL+GLSL versions #if defined(IMGUI_IMPL_OPENGL_ES2) // GL ES 2.0 + GLSL 100 (WebGL 1.0) const char *glsl_version = "#version 100"; SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); #elif defined(IMGUI_IMPL_OPENGL_ES3) // GL ES 3.0 + GLSL 300 es (WebGL 2.0) const char *glsl_version = "#version 300 es"; SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); #elif defined(__APPLE__) // GL 3.2 Core + GLSL 150 const char *glsl_version = "#version 150"; SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); // Always required on Mac SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); #else // GL 3.0 + GLSL 130 const char *glsl_version = "#version 130"; SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); #endif // From 2.0.18: Enable native IME. #ifdef SDL_HINT_IME_SHOW_UI SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1"); #endif // Create window with graphics context SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); SDL_Window *window = SDL_CreateWindow("tedit", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1280, 720, window_flags); if (window == nullptr) { printf("Error: SDL_CreateWindow(): %s\n", SDL_GetError()); return -1; } SDL_GLContext gl_context = SDL_GL_CreateContext(window); if (gl_context == nullptr) { printf("Error: SDL_GL_CreateContext(): %s\n", SDL_GetError()); return -1; } SDL_GL_MakeCurrent(window, gl_context); SDL_GL_SetSwapInterval(1); // Enable vsync glewExperimental = GL_TRUE; // Needed for core profile GLenum err = glewInit(); if (err != GLEW_OK) { fprintf(stderr, "Error: %s\n", glewGetErrorString(err)); return -1; } // Setup Dear ImGui context IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO &io = ImGui::GetIO(); (void)io; io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // Enable Docking // io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // Enable Multi-Viewport / Platform Windows // io.ConfigViewportsNoAutoMerge = true; // io.ConfigViewportsNoTaskBarIcon = true; // Setup Dear ImGui style ImGui::StyleColorsDark(); // ImGui::StyleColorsLight(); // When viewports are enabled we tweak WindowRounding/WindowBg so platform windows can look identical to regular ones. ImGuiStyle &style = ImGui::GetStyle(); if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { style.WindowRounding = 0.0f; style.Colors[ImGuiCol_WindowBg].w = 1.0f; } // Setup Platform/Renderer backends ImGui_ImplSDL2_InitForOpenGL(window, gl_context); ImGui_ImplOpenGL3_Init(glsl_version); // Our state ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); g_openFileDialog.SetTitle("Open Image File"); // Add common image formats and typical RAW formats g_openFileDialog.SetTypeFilters({ ".jpg", ".jpeg", ".png", ".tif", ".tiff", // Standard formats ".arw", ".cr2", ".cr3", ".nef", ".dng", ".orf", ".raf", ".rw2", // Common RAW ".*" // Allow any file as fallback }); g_exportSaveFileDialog.SetTitle("Export Image As"); // Type filters for saving are less critical as we force the extension later, // but can be helpful for user navigation. Let's set a default. g_exportSaveFileDialog.SetTypeFilters({".jpg", ".png", ".tif"}); AppImage g_loadedImage; // Your loaded image data bool g_imageIsLoaded = false; g_processedTextureId = 0; // Initialize processed texture ID printf("Initializing image processing pipeline...\n"); g_pipeline.Init("shaders/"); // Assuming shaders are in shaders/ subdir ImGuiTexInspect::ImplOpenGL3_Init(); // Or DirectX 11 equivalent (check your chosen backend header file) ImGuiTexInspect::Init(); ImGuiTexInspect::CreateContext(); InitShaderOperations("shaders/"); // Initialize shader operations if (!InitHistogramResources("shaders/")) { // Handle error - maybe disable histogram feature fprintf(stderr, "Histogram initialization failed, feature disabled.\n"); } // Main loop bool done = false; #ifdef __EMSCRIPTEN__ // For an Emscripten build we are disabling file-system access, so let's not attempt to do a fopen() of the imgui.ini file. // You may manually call LoadIniSettingsFromMemory() to load settings from your own storage. io.IniFilename = nullptr; EMSCRIPTEN_MAINLOOP_BEGIN #else while (!done) #endif { // Poll and handle events (inputs, window resize, etc.) // You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs. // - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or clear/overwrite your copy of the mouse data. // - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your copy of the keyboard data. // Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags. SDL_Event event; while (SDL_PollEvent(&event)) { ImGui_ImplSDL2_ProcessEvent(&event); if (event.type == SDL_QUIT) done = true; if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_CLOSE && event.window.windowID == SDL_GetWindowID(window)) done = true; } if (SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED) { SDL_Delay(10); continue; } // Start the Dear ImGui frame ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplSDL2_NewFrame(); ImGui::NewFrame(); GLuint textureToDisplay = 0; // Use a local var for clarity GLuint textureToSave = 0; // Texture ID holding final linear data for saving if (g_imageIsLoaded && g_loadedImage.m_textureId != 0) { g_pipeline.inputColorSpace = g_inputColorSpace; g_pipeline.outputColorSpace = g_outputColorSpace; // Modify pipeline processing slightly to get both display and save textures // Add a flag or method to control output conversion for saving textureToSave = g_pipeline.ProcessImage( g_loadedImage.m_textureId, g_loadedImage.getWidth(), g_loadedImage.getHeight(), false // <-- Add argument: bool applyOutputConversion = true ); textureToDisplay = g_pipeline.ProcessImage( g_loadedImage.m_textureId, g_loadedImage.getWidth(), g_loadedImage.getHeight(), true // Apply conversion for display ); // If the pipeline wasn't modified, textureToSave might need extra work } else { textureToDisplay = 0; textureToSave = 0; } // --- Menu Bar --- if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("File")) { if (ImGui::MenuItem("Open...", "Ctrl+O")) { g_openFileDialog.Open(); } // Disable Export if no image is loaded if (ImGui::MenuItem("Export...", "Ctrl+E", false, g_imageIsLoaded)) { g_exportErrorMsg = ""; // Clear previous errors g_showExportWindow = true; // <<< Set the flag to show the window } ImGui::Separator(); if (ImGui::MenuItem("Exit")) { done = true; // Simple exit for now } ImGui::EndMenu(); } // ... other menus ... ImGui::EndMainMenuBar(); } // --- File Dialog Display & Handling --- g_openFileDialog.Display(); g_exportSaveFileDialog.Display(); if (g_openFileDialog.HasSelected()) { std::string selectedPath = g_openFileDialog.GetSelected().string(); g_openFileDialog.ClearSelected(); printf("Opening file: %s\n", selectedPath.c_str()); // --- Load the selected image --- std::optional imgOpt = loadImage(selectedPath); if (imgOpt) { // If an image was already loaded, clean up its texture first if (g_loadedImage.m_textureId != 0) { glDeleteTextures(1, &g_loadedImage.m_textureId); g_loadedImage.m_textureId = 0; } // Clean up pipeline resources (FBOs/Textures) before loading new texture g_pipeline.ResetResources(); // <<< NEED TO ADD THIS METHOD g_loadedImage = std::move(*imgOpt); printf("Image loaded (%dx%d, %d channels, Linear:%s)\n", g_loadedImage.getWidth(), g_loadedImage.getHeight(), g_loadedImage.getChannels(), g_loadedImage.isLinear() ? "Yes" : "No"); if (loadImageTexture(g_loadedImage)) { g_imageIsLoaded = true; g_currentFilePath = selectedPath; // Store path printf("Float texture created successfully (ID: %u).\n", g_loadedImage.m_textureId); // Maybe reset sliders/pipeline state? Optional. } else { g_imageIsLoaded = false; g_currentFilePath = ""; fprintf(stderr, "Failed to load image into GL texture.\n"); // TODO: Show error to user (e.g., modal popup) } } else { g_imageIsLoaded = false; g_currentFilePath = ""; fprintf(stderr, "Failed to load image file: %s\n", selectedPath.c_str()); // TODO: Show error to user } } if (g_showExportWindow) // <<< Only attempt to draw if flag is true { // Optional: Center the window the first time it appears ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Appearing); // Auto-height ImVec2 center = ImGui::GetMainViewport()->GetCenter(); ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); // Begin a standard window. Pass &g_showExportWindow to enable the 'X' button. if (ImGui::Begin("Export Settings", &g_showExportWindow, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text("Choose Export Format and Settings:"); ImGui::Separator(); // --- Format Selection --- ImGui::Text("Format:"); ImGui::SameLine(); // ... (Combo box logic for g_exportFormat remains the same) ... const char *formats[] = {"JPEG", "PNG (8-bit)", "PNG (16-bit)", "TIFF (8-bit)", "TIFF (16-bit)"}; int currentFormatIndex = 0; switch (g_exportFormat) { /* ... map g_exportFormat to index ... */ } if (ImGui::Combo("##ExportFormat", ¤tFormatIndex, formats, IM_ARRAYSIZE(formats))) { switch (currentFormatIndex) { /* ... map index back to g_exportFormat ... */ } g_exportErrorMsg = ""; } // --- Format Specific Options --- if (g_exportFormat == ImageSaveFormat::JPEG) { ImGui::SliderInt("Quality", &g_exportQuality, 1, 100); } else { ImGui::Dummy(ImVec2(0.0f, ImGui::GetFrameHeightWithSpacing())); // Keep consistent height } ImGui::Separator(); // --- Display Error Messages --- if (!g_exportErrorMsg.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.2f, 0.2f, 1.0f)); ImGui::TextWrapped("Error: %s", g_exportErrorMsg.c_str()); ImGui::PopStyleColor(); ImGui::Separator(); } // --- Action Buttons --- if (ImGui::Button("Save As...", ImVec2(120, 0))) { // ... (Logic to set default name/path and call g_exportSaveFileDialog.Open() remains the same) ... std::filesystem::path currentPath(g_currentFilePath); std::string defaultName = currentPath.stem().string() + "_edited"; g_exportSaveFileDialog.SetPwd(currentPath.parent_path()); // g_exportSaveFileDialog.SetInputName(defaultName); // If supported g_exportSaveFileDialog.Open(); } ImGui::SameLine(); // No need for an explicit Cancel button if the 'X' works, but can keep it: if (ImGui::Button("Cancel", ImVec2(120, 0))) { g_showExportWindow = false; // Close the window by setting the flag } } // Matches ImGui::Begin("Export Settings",...) ImGui::End(); // IMPORTANT: Always call End() for Begin() } // End of if(g_showExportWindow) // --- Handle Export Save Dialog Selection --- if (g_exportSaveFileDialog.HasSelected()) { // ... (Your existing logic to get path, correct extension) ... std::filesystem::path savePathFs = g_exportSaveFileDialog.GetSelected(); g_exportSaveFileDialog.ClearSelected(); std::string savePath = savePathFs.string(); // ... (Ensure/correct extension logic) ... // --- Get Processed Image Data & Save --- printf("Attempting to save to: %s\n", savePath.c_str()); g_exportErrorMsg = ""; if (textureToSave != 0) { AppImage exportImageRGBA; // Name it clearly - it holds RGBA data printf("Reading back texture ID %u for saving...\n", textureToSave); if (ReadTextureToAppImage(textureToSave, g_loadedImage.getWidth(), g_loadedImage.getHeight(), exportImageRGBA)) { printf("Texture readback successful, saving...\n"); // <<< --- ADD CONVERSION LOGIC HERE --- >>> bool saveResult = false; if (g_exportFormat == ImageSaveFormat::JPEG) { // JPEG cannot handle 4 channels, convert to 3 (RGB) if (exportImageRGBA.getChannels() == 4) { printf("JPEG selected: Converting 4-channel RGBA to 3-channel RGB...\n"); AppImage exportImageRGB(exportImageRGBA.getWidth(), exportImageRGBA.getHeight(), 3); // Check allocation success? (Should be fine if RGBA worked) const float *rgbaData = exportImageRGBA.getData(); float *rgbData = exportImageRGB.getData(); size_t numPixels = exportImageRGBA.getWidth() * exportImageRGBA.getHeight(); for (size_t i = 0; i < numPixels; ++i) { // Copy R, G, B; discard A rgbData[i * 3 + 0] = rgbaData[i * 4 + 0]; // R rgbData[i * 3 + 1] = rgbaData[i * 4 + 1]; // G rgbData[i * 3 + 2] = rgbaData[i * 4 + 2]; // B } exportImageRGB.m_isLinear = exportImageRGBA.isLinear(); // Preserve linearity flag exportImageRGB.m_colorSpaceName = exportImageRGBA.getColorSpaceName(); // Preserve colorspace info printf("Conversion complete, saving RGB data...\n"); saveResult = saveImage(exportImageRGB, savePath, g_exportFormat, g_exportQuality); } else { // Source wasn't 4 channels? Unexpected, but save it directly. printf("Warning: Expected 4 channels for JPEG conversion, got %d. Saving directly...\n", exportImageRGBA.getChannels()); saveResult = saveImage(exportImageRGBA, savePath, g_exportFormat, g_exportQuality); } } else { // Format is PNG or TIFF, which should handle 4 channels (or 1/3) printf("Saving image with original channels (%d) for PNG/TIFF...\n", exportImageRGBA.getChannels()); saveResult = saveImage(exportImageRGBA, savePath, g_exportFormat, g_exportQuality); } // <<< --- END CONVERSION LOGIC --- >>> if (saveResult) { printf("Image saved successfully!\n"); g_showExportWindow = false; // <<< Close the settings window on success } else { fprintf(stderr, "Failed to save image.\n"); g_exportErrorMsg = "Failed to save image data to file."; } } else { fprintf(stderr, "Failed to read back texture data from GPU.\n"); g_exportErrorMsg = "Failed to read processed image data from GPU."; } } else { fprintf(stderr, "Cannot save: Invalid processed texture ID.\n"); g_exportErrorMsg = "No valid processed image data available to save."; } } static bool use_dockspace = true; if (use_dockspace) { ImGuiViewport *viewport = ImGui::GetMainViewport(); ImGuiID dockspace_id = ImGui::GetID("MyDockSpace"); // Use DockSpaceOverViewport instead of creating a manual window // Set the viewport size for the dockspace node. This is important. ImGui::SetNextWindowPos(viewport->WorkPos); ImGui::SetNextWindowSize(viewport->WorkSize); ImGui::SetNextWindowViewport(viewport->ID); // Use PassthruCentralNode to make the central node background transparent // so the ImGui default background shows until a window is docked there. ImGuiDockNodeFlags dockspace_flags = ImGuiDockNodeFlags_PassthruCentralNode; // We wrap the DockSpace call in a window that doesn't really draw anything itself, // but is required by the DockBuilder mechanism to target the space. // Make it borderless, no title, etc. ImGuiWindowFlags host_window_flags = 0; host_window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; host_window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus; host_window_flags |= ImGuiWindowFlags_NoBackground; // Make the host window transparent ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); ImGui::Begin("DockSpaceWindowHost", nullptr, host_window_flags); // No bool* needed ImGui::PopStyleVar(3); // Create the actual dockspace area. ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), dockspace_flags); ImGui::End(); // End the transparent host window // --- DockBuilder setup (runs once) --- // This logic remains the same, targeting the dockspace_id // Use DockBuilderGetNode()->IsEmpty() as a robust check for first time setup or reset. ImGuiDockNode *centralNode = ImGui::DockBuilderGetNode(dockspace_id); if (centralNode == nullptr || centralNode->IsEmpty()) { printf("DockBuilder: Setting up initial layout for DockID %u\n", dockspace_id); ImGui::DockBuilderRemoveNode(dockspace_id); // Clear out any previous state ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace); ImGui::DockBuilderSetNodeSize(dockspace_id, viewport->Size); // Set the size for the root node ImGuiID dock_main_id = dockspace_id; // This is the ID of the node just added ImGuiID dock_right_id, dock_left_id, dock_center_id; // Split right first (Edit Panel) ImGui::DockBuilderSplitNode(dock_main_id, ImGuiDir_Right, 0.25f, &dock_right_id, &dock_main_id); // Then split left from the remaining main area (Exif Panel) ImGui::DockBuilderSplitNode(dock_main_id, ImGuiDir_Left, 0.25f, &dock_left_id, &dock_center_id); // dock_center_id is the final remaining central node // Dock the windows into the nodes ImGui::DockBuilderDockWindow("Image Exif", dock_left_id); ImGui::DockBuilderDockWindow("Edit Image", dock_right_id); ImGui::DockBuilderDockWindow("Image View", dock_center_id); // Dock image view into the center ImGui::DockBuilderFinish(dockspace_id); printf("DockBuilder: Layout finished.\n"); } // --- End DockBuilder setup --- // --- Now Begin the actual windows that get docked --- // These calls are now *outside* any manual container window. // They will find their place in the dockspace based on the DockBuilder setup or user interaction. // "Image View" window ImGui::Begin("Image View"); // Display the texture that HAS the output conversion applied ImVec2 imageWidgetTopLeftScreen = ImGui::GetCursorScreenPos(); // Position BEFORE the inspector panel ImVec2 availableContentSize = ImGui::GetContentRegionAvail(); // Size available FOR the inspector panel GLuint displayTexId = textureToDisplay; // Use the display texture ID if (displayTexId != 0) { ComputeHistogramGPU(textureToDisplay, g_loadedImage.getWidth(), g_loadedImage.getHeight()); // Assume ImGuiTexInspect fills available space. This might need adjustment. ImVec2 displaySize = availableContentSize; float displayAspect = displaySize.x / displaySize.y; float imageAspect = float(g_loadedImage.getWidth()) / float(g_loadedImage.getHeight()); ImVec2 imageDisplaySize; // Actual size the image occupies on screen (letterboxed/pillarboxed) ImVec2 imageDisplayOffset = ImVec2(0, 0); // Offset within the widget area due to letterboxing if (displayAspect > imageAspect) { // Display is wider than image -> letterbox (bars top/bottom) imageDisplaySize.y = displaySize.y; imageDisplaySize.x = imageDisplaySize.y * imageAspect; imageDisplayOffset.x = (displaySize.x - imageDisplaySize.x) * 0.5f; } else { // Display is taller than image (or same aspect) -> pillarbox (bars left/right) imageDisplaySize.x = displaySize.x; imageDisplaySize.y = imageDisplaySize.x / imageAspect; imageDisplayOffset.y = (displaySize.y - imageDisplaySize.y) * 0.5f; } ImVec2 imageTopLeftScreen = imageWidgetTopLeftScreen + imageDisplayOffset; ImVec2 imageBottomRightScreen = imageTopLeftScreen + imageDisplaySize; // Use textureToDisplay here ImGuiTexInspect::BeginInspectorPanel("Image Inspector", (ImTextureID)(intptr_t)displayTexId, ImVec2(g_loadedImage.m_width, g_loadedImage.m_height), ImGuiTexInspect::InspectorFlags_NoTooltip | ImGuiTexInspect::InspectorFlags_NoGrid | ImGuiTexInspect::InspectorFlags_NoForceFilterNearest, ImGuiTexInspect::SizeIncludingBorder(availableContentSize)); ImGuiTexInspect::EndInspectorPanel(); // --- Draw Crop Overlay If Active --- if (g_cropActive && g_imageIsLoaded) { ImDrawList *drawList = ImGui::GetForegroundDrawList(); ImGuiIO &io = ImGui::GetIO(); ImVec2 mousePos = io.MousePos; // Calculate screen coords of the current crop rectangle ImVec2 cropMinScreen = imageTopLeftScreen + ImVec2(g_cropRectNorm.x, g_cropRectNorm.y) * imageDisplaySize; ImVec2 cropMaxScreen = imageTopLeftScreen + ImVec2(g_cropRectNorm.z, g_cropRectNorm.w) * imageDisplaySize; ImVec2 cropSizeScreen = cropMaxScreen - cropMinScreen; // Define handle size and interaction margin float handleScreenSize = 8.0f; float handleInteractionMargin = handleScreenSize * 1.5f; // Larger click area ImU32 colRect = IM_COL32(255, 255, 255, 200); // White rectangle ImU32 colHandle = IM_COL32(255, 255, 255, 255); // Solid white handle ImU32 colGrid = IM_COL32(200, 200, 200, 100); // Faint grid lines ImU32 colHover = IM_COL32(255, 255, 0, 255); // Yellow highlight // --- Define Handle Positions (screen coordinates) --- // Corners ImVec2 tl = cropMinScreen; ImVec2 tr = ImVec2(cropMaxScreen.x, cropMinScreen.y); ImVec2 bl = ImVec2(cropMinScreen.x, cropMaxScreen.y); ImVec2 br = cropMaxScreen; // Mid-edges ImVec2 tm = ImVec2((tl.x + tr.x) * 0.5f, tl.y); ImVec2 bm = ImVec2((bl.x + br.x) * 0.5f, bl.y); ImVec2 lm = ImVec2(tl.x, (tl.y + bl.y) * 0.5f); ImVec2 rm = ImVec2(tr.x, (tr.y + br.y) * 0.5f); // Handle definitions for hit testing and drawing struct HandleDef { CropHandle id; ImVec2 pos; }; HandleDef handles[] = { {CropHandle::TOP_LEFT, tl}, {CropHandle::TOP_RIGHT, tr}, {CropHandle::BOTTOM_LEFT, bl}, {CropHandle::BOTTOM_RIGHT, br}, {CropHandle::TOP, tm}, {CropHandle::BOTTOM, bm}, {CropHandle::LEFT, lm}, {CropHandle::RIGHT, rm}}; // --- Interaction Handling --- bool isHoveringAnyHandle = false; CropHandle hoveredHandle = CropHandle::NONE; // Only interact if window is hovered if (ImGui::IsWindowHovered()) // ImGuiHoveredFlags_AllowWhenBlockedByActiveItem might also be needed { // Check handles first (higher priority than inside rect) for (const auto &h : handles) { ImRect handleRect(h.pos - ImVec2(handleInteractionMargin, handleInteractionMargin), h.pos + ImVec2(handleInteractionMargin, handleInteractionMargin)); if (handleRect.Contains(mousePos)) { hoveredHandle = h.id; isHoveringAnyHandle = true; break; } } // Check inside rect if no handle hovered ImRect insideRect(cropMinScreen, cropMaxScreen); if (!isHoveringAnyHandle && insideRect.Contains(mousePos)) { hoveredHandle = CropHandle::INSIDE; } // Mouse Down: Start dragging if (hoveredHandle != CropHandle::NONE && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { g_activeCropHandle = hoveredHandle; g_isDraggingCrop = true; g_dragStartMousePos = mousePos; g_cropRectNormInitial = g_cropRectNorm; // Store state at drag start printf("Started dragging handle: %d\n", (int)g_activeCropHandle); } } // End IsWindowHovered check // Mouse Drag: Update crop rectangle if (g_isDraggingCrop && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { ImVec2 mouseDeltaScreen = mousePos - g_dragStartMousePos; // Convert delta to normalized image coordinates ImVec2 mouseDeltaNorm = ImVec2(0, 0); if (imageDisplaySize.x > 1e-3 && imageDisplaySize.y > 1e-3) { // Avoid division by zero mouseDeltaNorm = mouseDeltaScreen / imageDisplaySize; } // Update g_cropRectNorm based on handle and delta // Store temporary rect to apply constraints later ImVec4 tempRect = g_cropRectNormInitial; // Work from initial state + delta // --- Update Logic (Needs Aspect Ratio Constraint Integration) --- // [This part is complex - Simplified version below] UpdateCropRect(tempRect, g_activeCropHandle, mouseDeltaNorm, g_cropAspectRatio); // Clamp final rect to 0-1 range and ensure min < max tempRect.x = ImClamp(tempRect.x, 0.0f, 1.0f); tempRect.y = ImClamp(tempRect.y, 0.0f, 1.0f); tempRect.z = ImClamp(tempRect.z, 0.0f, 1.0f); tempRect.w = ImClamp(tempRect.w, 0.0f, 1.0f); if (tempRect.x > tempRect.z) ImSwap(tempRect.x, tempRect.z); if (tempRect.y > tempRect.w) ImSwap(tempRect.y, tempRect.w); // Prevent zero size rect? (Optional) // float minSizeNorm = 0.01f; // e.g., 1% minimum size // if (tempRect.z - tempRect.x < minSizeNorm) tempRect.z = tempRect.x + minSizeNorm; // if (tempRect.w - tempRect.y < minSizeNorm) tempRect.w = tempRect.y + minSizeNorm; g_cropRectNorm = tempRect; // Update the actual state } else if (g_isDraggingCrop && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { // Mouse Release: Stop dragging g_isDraggingCrop = false; g_activeCropHandle = CropHandle::NONE; printf("Stopped dragging crop.\n"); } // --- Drawing --- // Dimming overlay (optional) - Draw 4 rects outside the crop area drawList->AddRectFilled(imageTopLeftScreen, ImVec2(cropMinScreen.x, imageBottomRightScreen.y), IM_COL32(0,0,0,100)); // Left drawList->AddRectFilled(ImVec2(cropMaxScreen.x, imageTopLeftScreen.y), imageBottomRightScreen, IM_COL32(0,0,0,100)); // Right drawList->AddRectFilled(ImVec2(cropMinScreen.x, imageTopLeftScreen.y), ImVec2(cropMaxScreen.x, cropMinScreen.y), IM_COL32(0,0,0,100)); // Top drawList->AddRectFilled(ImVec2(cropMinScreen.x, cropMaxScreen.y), ImVec2(cropMaxScreen.x, imageBottomRightScreen.y), IM_COL32(0,0,0,100)); // Bottom // Draw crop rectangle outline drawList->AddRect(cropMinScreen, cropMaxScreen, colRect, 0.0f, 0, 1.5f); // Draw grid lines (simple 3x3 grid) float thirdW = cropSizeScreen.x / 3.0f; float thirdH = cropSizeScreen.y / 3.0f; drawList->AddLine(ImVec2(cropMinScreen.x + thirdW, cropMinScreen.y), ImVec2(cropMinScreen.x + thirdW, cropMaxScreen.y), colGrid, 1.0f); drawList->AddLine(ImVec2(cropMinScreen.x + thirdW * 2, cropMinScreen.y), ImVec2(cropMinScreen.x + thirdW * 2, cropMaxScreen.y), colGrid, 1.0f); drawList->AddLine(ImVec2(cropMinScreen.x, cropMinScreen.y + thirdH), ImVec2(cropMaxScreen.x, cropMinScreen.y + thirdH), colGrid, 1.0f); drawList->AddLine(ImVec2(cropMinScreen.x, cropMinScreen.y + thirdH * 2), ImVec2(cropMaxScreen.x, cropMinScreen.y + thirdH * 2), colGrid, 1.0f); // Draw handles for (const auto &h : handles) { bool isHovered = (h.id == hoveredHandle); bool isActive = (h.id == g_activeCropHandle); drawList->AddRectFilled(h.pos - ImVec2(handleScreenSize / 2, handleScreenSize / 2), h.pos + ImVec2(handleScreenSize / 2, handleScreenSize / 2), (isHovered || isActive) ? colHover : colHandle); } } // End if(g_cropActive) } else { // Show placeholder text if no image is loaded ImVec2 winSize = ImGui::GetWindowSize(); ImVec2 textSize = ImGui::CalcTextSize("No Image Loaded"); ImGui::SetCursorPos(ImVec2((winSize.x - textSize.x) * 0.5f, (winSize.y - textSize.y) * 0.5f)); ImGui::Text("No Image Loaded. File -> Open... to load an image"); std::fill(g_histogramDataCPU.begin(), g_histogramDataCPU.end(), 0); g_histogramMaxCount = 1; // Or maybe: "File -> Open... to load an image" } ImGui::End(); // End Image View // "Image Exif" window ImGui::Begin("Image Exif"); if (g_imageIsLoaded) { ImGui::Text("Image Width: %d", g_loadedImage.m_width); ImGui::Text("Image Height: %d", g_loadedImage.m_height); ImGui::Text("Image Loaded: %s", g_imageIsLoaded ? "Yes" : "No"); ImGui::Text("Image Channels: %d", g_loadedImage.m_channels); ImGui::Text("Image Color Space: %s", g_loadedImage.m_colorSpaceName.c_str()); ImGui::Text("Image ICC Profile Size: %zu bytes", g_loadedImage.m_iccProfile.size()); ImGui::Text("Image Metadata Size: %zu bytes", g_loadedImage.m_metadata.size()); ImGui::Separator(); ImGui::Text("Image Metadata: "); for (const auto &entry : g_loadedImage.m_metadata) { ImGui::Text("%s: %s", entry.first.c_str(), entry.second.c_str()); } } // Closing the if statement for g_imageIsLoaded ImGui::End(); // End Image Exif // "Edit Image" window ImGui::Begin("Edit Image"); if (ImGui::CollapsingHeader("Histogram", ImGuiTreeNodeFlags_DefaultOpen)) { DrawHistogramWidget("ExifHistogram", ImVec2(-1, 256)); } // --- Edit Image (Right) --- ImGui::Begin("Edit Image"); // --- Pipeline Configuration --- ImGui::SeparatorText("Processing Pipeline"); // Input Color Space Selector ImGui::Text("Input Color Space:"); ImGui::SameLine(); if (ImGui::BeginCombo("##InputCS", ColorSpaceToString(g_inputColorSpace))) { if (ImGui::Selectable(ColorSpaceToString(ColorSpace::LINEAR_SRGB), g_inputColorSpace == ColorSpace::LINEAR_SRGB)) { g_inputColorSpace = ColorSpace::LINEAR_SRGB; } if (ImGui::Selectable(ColorSpaceToString(ColorSpace::SRGB), g_inputColorSpace == ColorSpace::SRGB)) { g_inputColorSpace = ColorSpace::SRGB; } // Add other spaces later ImGui::EndCombo(); } // Output Color Space Selector ImGui::Text("Output Color Space:"); ImGui::SameLine(); if (ImGui::BeginCombo("##OutputCS", ColorSpaceToString(g_outputColorSpace))) { if (ImGui::Selectable(ColorSpaceToString(ColorSpace::LINEAR_SRGB), g_outputColorSpace == ColorSpace::LINEAR_SRGB)) { g_outputColorSpace = ColorSpace::LINEAR_SRGB; } if (ImGui::Selectable(ColorSpaceToString(ColorSpace::SRGB), g_outputColorSpace == ColorSpace::SRGB)) { g_outputColorSpace = ColorSpace::SRGB; } // Add other spaces later ImGui::EndCombo(); } ImGui::Separator(); ImGui::Text("Operation Order:"); // Drag-and-Drop Reordering List // Store indices or pointers to allow reordering `g_pipeline.activeOperations` int move_from = -1, move_to = -1; for (int i = 0; i < g_pipeline.activeOperations.size(); ++i) { PipelineOperation &op = g_pipeline.activeOperations[i]; ImGui::PushID(i); // Ensure unique IDs for controls within the loop // Checkbox to enable/disable ImGui::Checkbox("", &op.enabled); ImGui::SameLine(); // Simple Up/Down Buttons (alternative or complementary to DND) if (ImGui::ArrowButton("##up", ImGuiDir_Up) && i > 0) { move_from = i; move_to = i - 1; } ImGui::SameLine(); if (ImGui::ArrowButton("##down", ImGuiDir_Down) && i < g_pipeline.activeOperations.size() - 1) { move_from = i; move_to = i + 1; } ImGui::SameLine(); // Selectable for drag/drop source/target ImGui::Selectable(op.name.c_str(), false, 0, ImVec2(ImGui::GetContentRegionAvail().x - 30, 0)); // Leave space for buttons // Simple Drag Drop implementation if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { ImGui::SetDragDropPayload("PIPELINE_OP_DND", &i, sizeof(int)); ImGui::Text("Move %s", op.name.c_str()); ImGui::EndDragDropSource(); } if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload *payload = ImGui::AcceptDragDropPayload("PIPELINE_OP_DND")) { IM_ASSERT(payload->DataSize == sizeof(int)); move_from = *(const int *)payload->Data; move_to = i; } ImGui::EndDragDropTarget(); } ImGui::PopID(); } // Process move if detected if (move_from != -1 && move_to != -1 && move_from != move_to) { PipelineOperation temp = g_pipeline.activeOperations[move_from]; g_pipeline.activeOperations.erase(g_pipeline.activeOperations.begin() + move_from); g_pipeline.activeOperations.insert(g_pipeline.activeOperations.begin() + move_to, temp); printf("Moved operation %d to %d\n", move_from, move_to); } ImGui::SeparatorText("Adjustments"); // --- Adjustment Controls --- // Group sliders under collapsing headers as before // The slider variables (exposure, contrast, etc.) are now directly // linked to the PipelineOperation structs via pointers. if (ImGui::CollapsingHeader("White Balance", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::SliderFloat("Temperature", &temperature, 1000.0f, 20000.0f); ImGui::SliderFloat("Tint", &tint, -100.0f, 100.0f); } ImGui::Separator(); if (ImGui::CollapsingHeader("Tone", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::SliderFloat("Exposure", &exposure, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_Logarithmic); ImGui::SliderFloat("Contrast", &contrast, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_Logarithmic); ImGui::Separator(); ImGui::SliderFloat("Highlights", &highlights, -100.0f, 100.0f); ImGui::SliderFloat("Shadows", &shadows, -100.0f, 100.0f); ImGui::SliderFloat("Whites", &whites, -100.0f, 100.0f); ImGui::SliderFloat("Blacks", &blacks, -100.0f, 100.0f); } ImGui::Separator(); if (ImGui::CollapsingHeader("Presence", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::SliderFloat("Texture", &texture, -100.0f, 100.0f); ImGui::SliderFloat("Clarity", &clarity, -100.0f, 100.0f); ImGui::SliderFloat("Dehaze", &dehaze, -100.0f, 100.0f); ImGui::Separator(); ImGui::SliderFloat("Vibrance", &vibrance, -100.0f, 100.0f); ImGui::SliderFloat("Saturation", &saturation, -100.0f, 100.0f); } ImGui::Separator(); ImGui::SeparatorText("Transform"); if (!g_cropActive) { if (ImGui::Button("Crop & Straighten")) { // Combine visually for now g_cropActive = true; g_cropRectNorm = ImVec4(0.0f, 0.0f, 1.0f, 1.0f); // Reset crop on activation g_cropRectNormInitial = g_cropRectNorm; // Store initial state g_activeCropHandle = CropHandle::NONE; g_isDraggingCrop = false; // Update Original aspect ratio if needed if (g_loadedImage.getHeight() > 0) { for (auto &opt : g_aspectRatios) { if (strcmp(opt.name, "Original") == 0) { opt.ratio = float(g_loadedImage.getWidth()) / float(g_loadedImage.getHeight()); break; } } } // If current selection is 'Original', update g_cropAspectRatio if (g_selectedAspectRatioIndex >= 0 && g_selectedAspectRatioIndex < g_aspectRatios.size() && strcmp(g_aspectRatios[g_selectedAspectRatioIndex].name, "Original") == 0) { g_cropAspectRatio = g_aspectRatios[g_selectedAspectRatioIndex].ratio; } printf("Crop tool activated.\n"); } } else { ImGui::Text("Crop Active"); // Aspect Ratio Selector if (ImGui::BeginCombo("Aspect Ratio", g_aspectRatios[g_selectedAspectRatioIndex].name)) { for (int i = 0; i < g_aspectRatios.size(); ++i) { bool is_selected = (g_selectedAspectRatioIndex == i); if (ImGui::Selectable(g_aspectRatios[i].name, is_selected)) { g_selectedAspectRatioIndex = i; g_cropAspectRatio = g_aspectRatios[i].ratio; // Optional: Reset crop rectangle slightly or adjust existing one // to the new ratio if transitioning from freeform? Or just let user resize. printf("Selected aspect ratio: %s (%.2f)\n", g_aspectRatios[i].name, g_cropAspectRatio); } if (is_selected) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); } // Apply/Cancel Buttons if (ImGui::Button("Apply Crop")) { printf("Apply Crop button clicked.\n"); // <<< --- CALL FUNCTION TO APPLY CROP --- >>> if (ApplyCropToImage(g_loadedImage, g_cropRectNorm)) { printf("Crop applied successfully. Reloading texture and resetting pipeline.\n"); // Reload texture with cropped data if (!loadImageTexture(g_loadedImage)) { fprintf(stderr, "Error reloading texture after crop!\n"); g_imageIsLoaded = false; // Mark as not usable } // Reset pipeline FBOs/Textures due to size change g_pipeline.ResetResources(); } else { fprintf(stderr, "Failed to apply crop to image data.\n"); // Optionally show error to user } // Reset state after applying g_cropActive = false; g_cropRectNorm = ImVec4(0.0f, 0.0f, 1.0f, 1.0f); g_activeCropHandle = CropHandle::NONE; g_isDraggingCrop = false; } ImGui::SameLine(); if (ImGui::Button("Cancel Crop")) { printf("Crop cancelled.\n"); g_cropActive = false; g_cropRectNorm = ImVec4(0.0f, 0.0f, 1.0f, 1.0f); // Reset to full image g_activeCropHandle = CropHandle::NONE; g_isDraggingCrop = false; } } ImGui::End(); // End Edit Image ImGui::End(); // End MainDockspaceWindow } else { // Option 2: Simple full-screen window (no docking) ImGuiViewport *viewport = ImGui::GetMainViewport(); ImGui::SetNextWindowPos(viewport->WorkPos); ImGui::SetNextWindowSize(viewport->WorkSize); ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoBringToFrontOnFocus; ImGui::Begin("FullImageViewer", nullptr, window_flags); ImGui::Text("Image Viewer"); ImGuiTexInspect::BeginInspectorPanel("Image Inspector", g_loadedImage.m_textureId, ImVec2(g_loadedImage.m_width, g_loadedImage.m_height), ImGuiTexInspect::InspectorFlags_NoTooltip); ImGuiTexInspect::EndInspectorPanel(); ImGui::End(); } // Rendering ImGui::Render(); glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y); glClearColor(clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w); glClear(GL_COLOR_BUFFER_BIT); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); // Update and Render additional Platform Windows // (Platform functions may change the current OpenGL context, so we save/restore it to make it easier to paste this code elsewhere. // For this specific demo app we could also call SDL_GL_MakeCurrent(window, gl_context) directly) if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { SDL_Window *backup_current_window = SDL_GL_GetCurrentWindow(); SDL_GLContext backup_current_context = SDL_GL_GetCurrentContext(); ImGui::UpdatePlatformWindows(); ImGui::RenderPlatformWindowsDefault(); SDL_GL_MakeCurrent(backup_current_window, backup_current_context); } SDL_GL_SwapWindow(window); } #ifdef __EMSCRIPTEN__ EMSCRIPTEN_MAINLOOP_END; #endif // Cleanup // --- Cleanup --- // Destroy operations which will delete shader programs g_allOperations.clear(); // Deletes PipelineOperation objects and their shaders g_pipeline.activeOperations.clear(); // Clear the list in pipeline (doesn't own shaders) // Pipeline destructor handles FBOs/VAO etc. // Delete the originally loaded texture if (g_loadedImage.m_textureId != 0) { glDeleteTextures(1, &g_loadedImage.m_textureId); g_loadedImage.m_textureId = 0; } if (g_histogramResourcesInitialized) { if (g_histogramSSBO) glDeleteBuffers(1, &g_histogramSSBO); if (g_histogramComputeShader) glDeleteProgram(g_histogramComputeShader); printf("Cleaned up histogram resources.\n"); } ImGuiTexInspect::Shutdown(); ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplSDL2_Shutdown(); ImGui::DestroyContext(); SDL_GL_DeleteContext(gl_context); SDL_DestroyWindow(window); SDL_Quit(); return 0; } ``` app_image.h ``` #ifndef APP_IMAGE_H #define APP_IMAGE_H #include #include #include #include // Requires C++17 #include #include #include #include #include #include #include // For errors/warnings #include // For strcmp, memcpy, etc. #include // For libjpeg/libpng error handling #define IMGUI_DEFINE_MATH_OPERATORS // Allows ImVec2 operators #include "imgui_internal.h" // Need ImFloorSigned, ImClamp, ImMax, ImMin, ImAbs // --- User Instructions --- // 1. Place easyexif.h in your include path. // 2. Ensure development libraries for LibRaw, libjpeg-turbo, libpng, and libtiff are installed. // 3. In EXACTLY ONE .cpp file in your project, before including this header, define: // #define APP_IMAGE_IMPLEMENTATION // 4. When compiling, LINK against the necessary libraries, e.g., using CMake or directly: // g++ your_app.cpp -o your_app -std=c++17 -lraw -ljpeg -lpng -ltiff -lm (order might matter) // --- Forward declarations of external library types (optional, mostly for clarity) --- // struct jpeg_decompress_struct; // struct jpeg_compress_struct; // struct jpeg_error_mgr; // struct png_struct_def; // struct png_info_def; // typedef struct tiff TIFF; // From tiffio.h // class LibRaw; // From libraw/libraw.h // Include easyexif here as it's header-only anyway #include "exif.h" // Enum for specifying save formats enum class ImageSaveFormat { JPEG, // Quality setting applies (1-100), saves as 8-bit sRGB. PNG_8, // 8-bit PNG (sRGB assumption). PNG_16, // 16-bit PNG (Linear or sRGB depends on future implementation details, currently linear). TIFF_8, // 8-bit TIFF (Uncompressed, RGB). TIFF_16, // 16-bit TIFF (Uncompressed, RGB, Linear). // TIFF_LZW_16 // Example for compressed TIFF UNKNOWN }; // Basic structure for image metadata (can hold EXIF tags) using ImageMetadata = std::map; // --- App Internal Image Representation --- class AppImage { public: // --- Constructors --- AppImage() = default; AppImage(uint32_t width, uint32_t height, uint32_t channels = 3); // --- Accessors --- uint32_t getWidth() const { return m_width; } uint32_t getHeight() const { return m_height; } uint32_t getChannels() const { return m_channels; } bool isEmpty() const { return m_pixelData.empty(); } // Pixel data: Linear floating point [0.0, 1.0+], interleaved RGB/RGBA/Gray. float *getData() { return m_pixelData.data(); } const float *getData() const { return m_pixelData.data(); } size_t getDataSize() const { return m_pixelData.size() * sizeof(float); } size_t getTotalFloats() const { return m_pixelData.size(); } std::vector &getPixelVector() { return m_pixelData; } const std::vector &getPixelVector() const { return m_pixelData; } // --- Metadata --- ImageMetadata &getMetadata() { return m_metadata; } const ImageMetadata &getMetadata() const { return m_metadata; } // --- Color Information --- std::vector &getIccProfile() { return m_iccProfile; } const std::vector &getIccProfile() const { return m_iccProfile; } std::string &getColorSpaceName() { return m_colorSpaceName; } const std::string &getColorSpaceName() const { return m_colorSpaceName; } bool isLinear() const { return m_isLinear; } // --- Modifiers --- void resize(uint32_t newWidth, uint32_t newHeight, uint32_t newChannels = 0); void clear_image(); // --- Data members --- // Making them public for easier access in the implementation section below, // alternatively make loadImage/saveImage friends or add internal setters. // public: uint32_t m_width = 0; uint32_t m_height = 0; uint32_t m_channels = 0; // 1=Gray, 3=RGB, 4=RGBA std::vector m_pixelData; ImageMetadata m_metadata; std::vector m_iccProfile; std::string m_colorSpaceName = "Unknown"; bool m_isLinear = true; // Default assumption for internal format GLuint m_textureId = 0; int m_textureWidth = 0; int m_textureHeight = 0; }; // --- API Function Declarations --- /** * @brief Loads an image file, attempting type detection (RAW, JPEG, PNG, TIFF). * Uses LibRaw, libjpeg-turbo, libpng, libtiff. * Uses EasyExif for EXIF metadata from JPEGs (only). * Converts loaded pixel data to internal linear float format. * Extracts ICC profile if available (primarily from RAW). * * @param filePath Path to the image file. * @return std::optional containing the loaded image on success, std::nullopt on failure. */ std::optional loadImage(const std::string &filePath); /** * @brief Saves the AppImage to a file (JPEG, PNG, TIFF). * Uses libjpeg-turbo, libpng, libtiff. * Converts internal linear float data to target format (e.g., 8-bit sRGB for JPEG). * NOTE: Does NOT currently save EXIF or ICC metadata. This requires more complex handling * (e.g., using Exiv2 library or manual file manipulation after saving pixels). * * @param image The AppImage to save. Assumed to be in linear float format. * @param filePath Path to save the image file. * @param format The desired output format. * @param quality JPEG quality (1-100), ignored otherwise. * @return True on success, false on failure. */ bool saveImage(const AppImage &image, const std::string &filePath, ImageSaveFormat format, int quality = 90); bool loadImageTexture(const AppImage &appImage); // ============================================================================ // =================== IMPLEMENTATION SECTION ================================= // ============================================================================ // Define APP_IMAGE_IMPLEMENTATION in exactly one .cpp file before including this header #ifdef APP_IMAGE_IMPLEMENTATION #include #include #include #include // Internal helper namespace namespace AppImageUtil { // --- Error Handling --- // Basic error reporting (prints to stderr) inline void LogError(const std::string &msg) { std::cerr << "AppImage Error: " << msg << std::endl; } inline void LogWarning(const std::string &msg) { std::cerr << "AppImage Warning: " << msg << std::endl; } // --- Color Conversion Helpers (Approximate sRGB) --- // For critical work, use a color management library (LittleCMS) and proper piecewise functions inline float srgb_to_linear_approx(float srgbVal) { if (srgbVal <= 0.0f) return 0.0f; if (srgbVal <= 0.04045f) { return srgbVal / 12.92f; } else { return std::pow((srgbVal + 0.055f) / 1.055f, 2.4f); } } inline float linear_to_srgb_approx(float linearVal) { if (linearVal <= 0.0f) return 0.0f; // Simple clamp for typical display output linearVal = std::fmax(0.0f, std::fmin(1.0f, linearVal)); if (linearVal <= 0.0031308f) { return linearVal * 12.92f; } else { return 1.055f * std::pow(linearVal, 1.0f / 2.4f) - 0.055f; } } // --- File Type Detection --- enum class DetectedFileType { RAW, JPEG, PNG, TIFF, UNKNOWN }; inline DetectedFileType detectFileType(const std::string &filePath) { std::ifstream file(filePath, std::ios::binary); if (!file) return DetectedFileType::UNKNOWN; unsigned char magic[12]; // Read enough bytes for common signatures file.read(reinterpret_cast(magic), sizeof(magic)); if (!file) return DetectedFileType::UNKNOWN; // Check common signatures if (magic[0] == 0xFF && magic[1] == 0xD8 && magic[2] == 0xFF) return DetectedFileType::JPEG; if (magic[0] == 0x89 && magic[1] == 'P' && magic[2] == 'N' && magic[3] == 'G') return DetectedFileType::PNG; if ((magic[0] == 'I' && magic[1] == 'I' && magic[2] == 0x2A && magic[3] == 0x00) || // Little-endian TIFF (magic[0] == 'M' && magic[1] == 'M' && magic[2] == 0x00 && magic[3] == 0x2A)) // Big-endian TIFF { size_t dotPos = filePath.rfind('.'); if (dotPos != std::string::npos) { std::string ext = filePath.substr(dotPos); std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); // Common RAW formats that use TIFF structure const char *rawTiffExtensions[] = { ".nef", // Nikon ".cr2", // Canon ".dng", // Adobe/Various ".arw", // Sony ".srw", // Samsung ".orf", // Olympus ".pef", // Pentax ".raf", // Fuji ".rw2" // Panasonic }; for (const char *rawExt : rawTiffExtensions) { if (ext == rawExt) return DetectedFileType::RAW; } } return DetectedFileType::TIFF; } // If no standard signature matches, check extension for RAW as a fallback // (LibRaw handles many internal variations) size_t dotPos = filePath.rfind('.'); if (dotPos != std::string::npos) { std::string ext = filePath.substr(dotPos); std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); const char *rawExtensions[] = { ".3fr", ".ari", ".arw", ".bay", ".braw", ".crw", ".cr2", ".cr3", ".cap", ".data", ".dcs", ".dcr", ".dng", ".drf", ".eip", ".erf", ".fff", ".gpr", ".iiq", ".k25", ".kdc", ".mdc", ".mef", ".mos", ".mrw", ".nef", ".nrw", ".obm", ".orf", ".pef", ".ptx", ".pxn", ".r3d", ".raf", ".raw", ".rwl", ".rw2", ".rwz", ".sr2", ".srf", ".srw", ".tif", ".x3f" // Note: .tif can be RAW or regular TIFF }; for (const char *rawExt : rawExtensions) { if (ext == rawExt) return DetectedFileType::RAW; } // Special case: Leica .dng can also be loaded by LibRaw if (ext == ".dng") return DetectedFileType::RAW; } return DetectedFileType::UNKNOWN; } // --- EXIF Loading Helper (using EasyExif) --- inline void loadExifData(const std::string &filePath, ImageMetadata &metadata) { std::ifstream file(filePath, std::ios::binary | std::ios::ate); if (!file) return; std::streamsize size = file.tellg(); file.seekg(0, std::ios::beg); std::vector buffer(size); if (!file.read(reinterpret_cast(buffer.data()), size)) return; easyexif::EXIFInfo exifInfo; int code = exifInfo.parseFrom(buffer.data(), buffer.size()); if (code == 0) { // Helper lambda to add if not empty auto addMeta = [&](const std::string &key, const std::string &value) { if (!value.empty()) metadata[key] = value; }; auto addMetaInt = [&](const std::string &key, int value) { if (value > 0) metadata[key] = std::to_string(value); }; auto addMetaDouble = [&](const std::string &key, double value) { if (value > 0) metadata[key] = std::to_string(value); }; addMeta("Exif.Image.Make", exifInfo.Make); addMeta("Exif.Image.Model", exifInfo.Model); addMeta("Exif.Image.Software", exifInfo.Software); addMetaInt("Exif.Image.Orientation", exifInfo.Orientation); addMeta("Exif.Image.DateTime", exifInfo.DateTime); addMeta("Exif.Photo.DateTimeOriginal", exifInfo.DateTimeOriginal); addMeta("Exif.Photo.DateTimeDigitized", exifInfo.DateTimeDigitized); addMeta("Exif.Image.SubSecTimeOriginal", exifInfo.SubSecTimeOriginal); // Often empty addMeta("Exif.Image.Copyright", exifInfo.Copyright); addMetaDouble("Exif.Photo.ExposureTime", exifInfo.ExposureTime); addMetaDouble("Exif.Photo.FNumber", exifInfo.FNumber); addMetaInt("Exif.Photo.ISOSpeedRatings", exifInfo.ISOSpeedRatings); addMetaDouble("Exif.Photo.ShutterSpeedValue", exifInfo.ShutterSpeedValue); // APEX addMetaDouble("Exif.Photo.ApertureValue", exifInfo.FNumber); // APEX addMetaDouble("Exif.Photo.ExposureBiasValue", exifInfo.ExposureBiasValue); addMetaDouble("Exif.Photo.FocalLength", exifInfo.FocalLength); addMeta("Exif.Photo.LensModel", exifInfo.LensInfo.Model); // GeoLocation if (exifInfo.GeoLocation.Latitude != 0 || exifInfo.GeoLocation.Longitude != 0) { metadata["Exif.GPSInfo.Latitude"] = std::to_string(exifInfo.GeoLocation.Latitude); metadata["Exif.GPSInfo.Longitude"] = std::to_string(exifInfo.GeoLocation.Longitude); metadata["Exif.GPSInfo.Altitude"] = std::to_string(exifInfo.GeoLocation.Altitude); metadata["Exif.GPSInfo.LatitudeRef"] = exifInfo.GeoLocation.LatComponents.direction; metadata["Exif.GPSInfo.LongitudeRef"] = exifInfo.GeoLocation.LonComponents.direction; } } else { // LogWarning("Could not parse EXIF data (Code " + std::to_string(code) + ") from " + filePath); } } // --- LibRaw Loading --- inline std::optional loadRaw(const std::string &filePath) { LibRaw rawProcessor; AppImage image; // Set parameters for desired output // Output 16-bit data rawProcessor.imgdata.params.output_bps = 16; // Disable automatic brightness adjustment (we want linear) rawProcessor.imgdata.params.no_auto_bright = 1; // Set output color space (e.g., 1 = sRGB, 3 = ProPhoto, 4 = AdobeRGB) // ProPhoto (3) or AdobeRGB (4) are good wide-gamut choices if editor supports them. // sRGB (1) is safest if unsure. We'll assume Linear sRGB for now. rawProcessor.imgdata.params.output_color = 1; // 1 = sRGB primaries // Set gamma (1.0 for linear) - use {1.0, 1.0} for linear output rawProcessor.imgdata.params.gamm[0] = 1.0; // Linear gamma rawProcessor.imgdata.params.gamm[1] = 1.0; // Use camera white balance if available, otherwise auto rawProcessor.imgdata.params.use_camera_wb = 1; rawProcessor.imgdata.params.use_auto_wb = (rawProcessor.imgdata.params.use_camera_wb == 0); // Consider other params: demosaic algorithm, highlight recovery, etc. int ret; if ((ret = rawProcessor.open_file(filePath.c_str())) != LIBRAW_SUCCESS) { LogError("LibRaw: Cannot open file " + filePath + " - " + libraw_strerror(ret)); return std::nullopt; } if ((ret = rawProcessor.unpack()) != LIBRAW_SUCCESS) { LogError("LibRaw: Cannot unpack file " + filePath + " - " + libraw_strerror(ret)); return std::nullopt; } // Process the image (demosaic, color conversion, etc.) if ((ret = rawProcessor.dcraw_process()) != LIBRAW_SUCCESS) { LogError("LibRaw: Cannot process file " + filePath + " - " + libraw_strerror(ret)); // Try fallback processing if dcraw_process fails (might be non-RAW TIFF/JPEG) if (ret == LIBRAW_UNSUPPORTED_THUMBNAIL || ret == LIBRAW_REQUEST_FOR_NONEXISTENT_IMAGE) { LogWarning("LibRaw: File " + filePath + " might be non-RAW or only has thumbnail. Attempting fallback."); // You could try loading with libjpeg/libtiff here, but for simplicity we fail } return std::nullopt; } // Get the processed image data libraw_processed_image_t *processed_image = rawProcessor.dcraw_make_mem_image(&ret); if (!processed_image) { LogError("LibRaw: Cannot make memory image for " + filePath + " - " + libraw_strerror(ret)); return std::nullopt; } // Copy data to AppImage format if (processed_image->type == LIBRAW_IMAGE_BITMAP && processed_image->bits == 16) { image.m_width = processed_image->width; image.m_height = processed_image->height; image.m_channels = processed_image->colors; // Should be 3 (RGB) image.m_isLinear = true; // We requested linear gamma if (image.m_channels != 3) { LogWarning("LibRaw: Expected 3 channels, got " + std::to_string(image.m_channels)); // Handle grayscale or other cases if needed, for now assume RGB image.m_channels = 3; } size_t num_pixels = static_cast(image.m_width) * image.m_height; size_t total_floats = num_pixels * image.m_channels; image.m_pixelData.resize(total_floats); uint16_t *raw_data = reinterpret_cast(processed_image->data); float *app_data = image.m_pixelData.data(); // Convert 16-bit unsigned short [0, 65535] to float [0.0, 1.0+] for (size_t i = 0; i < total_floats; ++i) { app_data[i] = static_cast(raw_data[i]) / 65535.0f; } // Get color space name based on output_color param switch (rawProcessor.imgdata.params.output_color) { case 1: image.m_colorSpaceName = "Linear sRGB"; break; case 2: image.m_colorSpaceName = "Linear Adobe RGB (1998)"; break; // Check LibRaw docs if this is correct mapping case 3: image.m_colorSpaceName = "Linear ProPhoto RGB"; break; case 4: image.m_colorSpaceName = "Linear XYZ"; break; // Check LibRaw docs default: image.m_colorSpaceName = "Linear Unknown"; break; } // Extract Metadata (Example - add more fields as needed) image.m_metadata["LibRaw.Camera.Make"] = rawProcessor.imgdata.idata.make; image.m_metadata["LibRaw.Camera.Model"] = rawProcessor.imgdata.idata.model; image.m_metadata["LibRaw.Image.Timestamp"] = std::to_string(rawProcessor.imgdata.other.timestamp); image.m_metadata["LibRaw.Image.ShotOrder"] = std::to_string(rawProcessor.imgdata.other.shot_order); image.m_metadata["LibRaw.Photo.ExposureTime"] = std::to_string(rawProcessor.imgdata.other.shutter); image.m_metadata["LibRaw.Photo.Aperture"] = std::to_string(rawProcessor.imgdata.other.aperture); image.m_metadata["LibRaw.Photo.ISOSpeed"] = std::to_string(rawProcessor.imgdata.other.iso_speed); image.m_metadata["LibRaw.Photo.FocalLength"] = std::to_string(rawProcessor.imgdata.other.focal_len); // Copy EasyExif compatible fields if possible for consistency image.m_metadata["Exif.Image.Make"] = rawProcessor.imgdata.idata.make; image.m_metadata["Exif.Image.Model"] = rawProcessor.imgdata.idata.model; image.m_metadata["Exif.Photo.ExposureTime"] = std::to_string(rawProcessor.imgdata.other.shutter); image.m_metadata["Exif.Photo.FNumber"] = std::to_string(rawProcessor.imgdata.other.aperture); // Aperture == FNumber image.m_metadata["Exif.Photo.ISOSpeedRatings"] = std::to_string(rawProcessor.imgdata.other.iso_speed); image.m_metadata["Exif.Photo.FocalLength"] = std::to_string(rawProcessor.imgdata.other.focal_len); // LibRaw often provides DateTimeOriginal via timestamp // Convert timestamp to string if needed: // time_t ts = rawProcessor.imgdata.other.timestamp; // char buf[30]; // strftime(buf, sizeof(buf), "%Y:%m:%d %H:%M:%S", localtime(&ts)); // image.m_metadata["Exif.Photo.DateTimeOriginal"] = buf; // Extract ICC Profile unsigned int icc_size = 0; const void *icc_profile_ptr = nullptr; if (icc_profile_ptr && icc_size > 0) { image.m_iccProfile.resize(icc_size); std::memcpy(image.m_iccProfile.data(), icc_profile_ptr, icc_size); LogWarning("LibRaw: Successfully extracted ICC profile (" + std::to_string(icc_size) + " bytes)."); // We could potentially parse the ICC profile name here, but it's complex. if (image.m_colorSpaceName == "Linear Unknown") image.m_colorSpaceName = "Linear (Embedded ICC)"; } else { LogWarning("LibRaw: No ICC profile found or extracted."); } } else { LogError("LibRaw: Processed image is not 16-bit bitmap (type=" + std::to_string(processed_image->type) + " bits=" + std::to_string(processed_image->bits) + ")"); LibRaw::dcraw_clear_mem(processed_image); return std::nullopt; } // Clean up LibRaw resources LibRaw::dcraw_clear_mem(processed_image); // rawProcessor is automatically cleaned up by its destructor return image; } // --- libjpeg Loading --- // Custom error handler for libjpeg struct JpegErrorManager { jpeg_error_mgr pub; jmp_buf setjmp_buffer; // For returning control on error }; void jpegErrorExit(j_common_ptr cinfo) { JpegErrorManager *myerr = reinterpret_cast(cinfo->err); // Format the error message char buffer[JMSG_LENGTH_MAX]; (*cinfo->err->format_message)(cinfo, buffer); LogError("libjpeg: " + std::string(buffer)); // Return control to setjmp point longjmp(myerr->setjmp_buffer, 1); } inline std::optional loadJpeg(const std::string &filePath) { FILE *infile = fopen(filePath.c_str(), "rb"); if (!infile) { LogError("Cannot open JPEG file: " + filePath); return std::nullopt; } AppImage image; jpeg_decompress_struct cinfo; JpegErrorManager jerr; // Custom error handler // Setup error handling cinfo.err = jpeg_std_error(&jerr.pub); jerr.pub.error_exit = jpegErrorExit; if (setjmp(jerr.setjmp_buffer)) { // If we get here, a fatal error occurred jpeg_destroy_decompress(&cinfo); fclose(infile); return std::nullopt; } // Initialize decompression object jpeg_create_decompress(&cinfo); jpeg_stdio_src(&cinfo, infile); // Read header jpeg_read_header(&cinfo, TRUE); // Start decompressor - this guesses output parameters like color space // We usually get JCS_RGB for color JPEGs cinfo.out_color_space = JCS_RGB; // Request RGB output jpeg_start_decompress(&cinfo); image.m_width = cinfo.output_width; image.m_height = cinfo.output_height; image.m_channels = cinfo.output_components; // Should be 3 for JCS_RGB if (image.m_channels != 1 && image.m_channels != 3) { LogError("libjpeg: Unsupported number of channels: " + std::to_string(image.m_channels)); jpeg_finish_decompress(&cinfo); jpeg_destroy_decompress(&cinfo); fclose(infile); return std::nullopt; } size_t num_pixels = static_cast(image.m_width) * image.m_height; size_t total_floats = num_pixels * image.m_channels; image.m_pixelData.resize(total_floats); image.m_isLinear = true; // We will convert to linear image.m_colorSpaceName = "Linear sRGB"; // Standard JPEG assumption // Allocate temporary buffer for one scanline int row_stride = cinfo.output_width * cinfo.output_components; std::vector scanline_buffer(row_stride); JSAMPROW row_pointer[1]; row_pointer[0] = scanline_buffer.data(); float *app_data_ptr = image.m_pixelData.data(); // Read scanlines while (cinfo.output_scanline < cinfo.output_height) { jpeg_read_scanlines(&cinfo, row_pointer, 1); // Convert scanline from 8-bit sRGB to linear float for (int i = 0; i < row_stride; ++i) { *app_data_ptr++ = srgb_to_linear_approx(static_cast(scanline_buffer[i]) / 255.0f); } } // Finish decompression and clean up jpeg_finish_decompress(&cinfo); jpeg_destroy_decompress(&cinfo); fclose(infile); // Load EXIF data separately loadExifData(filePath, image.m_metadata); return image; } // --- libpng Loading --- // Custom error handler for libpng void pngErrorFunc(png_structp png_ptr, png_const_charp error_msg) { LogError("libpng: " + std::string(error_msg)); jmp_buf *jmp_ptr = reinterpret_cast(png_get_error_ptr(png_ptr)); if (jmp_ptr) { longjmp(*jmp_ptr, 1); } // If no jmp_buf, just exit (shouldn't happen if setup correctly) exit(EXIT_FAILURE); } void pngWarningFunc(png_structp png_ptr, png_const_charp warning_msg) { LogWarning("libpng: " + std::string(warning_msg)); // Don't longjmp on warnings } inline std::optional loadPng(const std::string &filePath) { FILE *fp = fopen(filePath.c_str(), "rb"); if (!fp) { LogError("Cannot open PNG file: " + filePath); return std::nullopt; } // Check PNG signature unsigned char header[8]; fread(header, 1, 8, fp); if (png_sig_cmp(header, 0, 8)) { LogError("File is not a valid PNG: " + filePath); fclose(fp); return std::nullopt; } png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, pngErrorFunc, pngWarningFunc); if (!png_ptr) { LogError("libpng: png_create_read_struct failed"); fclose(fp); return std::nullopt; } png_infop info_ptr = png_create_info_struct(png_ptr); if (!info_ptr) { LogError("libpng: png_create_info_struct failed"); png_destroy_read_struct(&png_ptr, nullptr, nullptr); fclose(fp); return std::nullopt; } // Setup jump buffer for error handling jmp_buf jmpbuf; if (setjmp(jmpbuf)) { LogError("libpng: Error during read"); png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); fclose(fp); return std::nullopt; } // Assign jump buffer to png error pointer // Note: The cast from jmp_buf* to png_voidp* might feel odd, but it's standard practice png_set_error_fn(png_ptr, reinterpret_cast(&jmpbuf), pngErrorFunc, pngWarningFunc); png_init_io(png_ptr, fp); png_set_sig_bytes(png_ptr, 8); // We already read the 8 signature bytes // Read file info png_read_info(png_ptr, info_ptr); AppImage image; png_uint_32 png_width, png_height; int bit_depth, color_type, interlace_method, compression_method, filter_method; png_get_IHDR(png_ptr, info_ptr, &png_width, &png_height, &bit_depth, &color_type, &interlace_method, &compression_method, &filter_method); image.m_width = png_width; image.m_height = png_height; // --- Transformations --- // We want linear float RGB or RGBA output // Handle palette -> RGB if (color_type == PNG_COLOR_TYPE_PALETTE) { png_set_palette_to_rgb(png_ptr); } // Handle low bit depth grayscale -> 8 bit if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) { png_set_expand_gray_1_2_4_to_8(png_ptr); bit_depth = 8; // Update bit depth after expansion } // Handle transparency chunk -> Alpha channel if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) { png_set_tRNS_to_alpha(png_ptr); } // Convert 16-bit -> 8-bit if needed (we handle 16 bit below, so maybe don't strip) // if (bit_depth == 16) { // png_set_strip_16(png_ptr); // bit_depth = 8; // } // Convert grayscale -> RGB if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { png_set_gray_to_rgb(png_ptr); } // Add alpha channel if missing but requested (we might always want RGBA internally) // if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY) { // png_set_add_alpha(png_ptr, 0xFF, PNG_FILLER_AFTER); // Add opaque alpha // } // --- Gamma Handling --- double file_gamma = 0.0; bool is_srgb = (png_get_sRGB(png_ptr, info_ptr, nullptr) != 0); if (is_srgb) { // If sRGB chunk is present, libpng can convert to linear for us png_set_gamma(png_ptr, 1.0, 0.45455); // Request linear output (screen gamma 2.2) image.m_isLinear = true; image.m_colorSpaceName = "Linear sRGB"; } else if (png_get_gAMA(png_ptr, info_ptr, &file_gamma)) { // If gAMA chunk is present, convert to linear png_set_gamma(png_ptr, 1.0, file_gamma); image.m_isLinear = true; image.m_colorSpaceName = "Linear Unknown (Gamma Corrected)"; } else { // No gamma info, assume sRGB and convert manually later image.m_isLinear = false; // Data read will be sRGB image.m_colorSpaceName = "sRGB (Assumed)"; } // Apply transformations png_read_update_info(png_ptr, info_ptr); // Get updated info after transformations image.m_channels = png_get_channels(png_ptr, info_ptr); bit_depth = png_get_bit_depth(png_ptr, info_ptr); // Update bit_depth after transforms if (image.m_channels < 3) { LogWarning("libpng: Resulting image has < 3 channels after transforms. Handling as RGB."); // Force RGB if needed? Be careful here. For simplicity, assume RGB/RGBA works. } // Allocate memory for the image data size_t num_pixels = static_cast(image.m_width) * image.m_height; size_t total_floats = num_pixels * image.m_channels; image.m_pixelData.resize(total_floats); float *app_data_ptr = image.m_pixelData.data(); // Allocate row pointers png_bytep *row_pointers = new png_bytep[image.m_height]; size_t row_bytes = png_get_rowbytes(png_ptr, info_ptr); std::vector image_buffer(row_bytes * image.m_height); // Read whole image at once for (png_uint_32 i = 0; i < image.m_height; ++i) { row_pointers[i] = image_buffer.data() + i * row_bytes; } // Read the entire image png_read_image(png_ptr, row_pointers); // Convert the read data to linear float unsigned char *buffer_ptr = image_buffer.data(); if (bit_depth == 8) { for (size_t i = 0; i < total_floats; ++i) { float val = static_cast(buffer_ptr[i]) / 255.0f; // Convert to linear if libpng didn't do it (i.e., no sRGB/gAMA chunk found) app_data_ptr[i] = image.m_isLinear ? val : srgb_to_linear_approx(val); } } else if (bit_depth == 16) { uint16_t *buffer_ptr16 = reinterpret_cast(buffer_ptr); // PNG 16-bit uses network byte order (big-endian) bool needs_swap = (png_get_uint_16((png_bytep) "\x01\x02") != 0x0102); // Check system endianness for (size_t i = 0; i < total_floats; ++i) { uint16_t raw_val = buffer_ptr16[i]; if (needs_swap) { // Swap bytes if system is little-endian raw_val = (raw_val >> 8) | (raw_val << 8); } float val = static_cast(raw_val) / 65535.0f; // Convert to linear if libpng didn't do it app_data_ptr[i] = image.m_isLinear ? val : srgb_to_linear_approx(val); } } else { LogError("libpng: Unsupported bit depth after transforms: " + std::to_string(bit_depth)); delete[] row_pointers; png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); fclose(fp); return std::nullopt; } // If we assumed sRGB and converted manually, update state if (!image.m_isLinear) { image.m_isLinear = true; image.m_colorSpaceName = "Linear sRGB (Assumed)"; } // Clean up delete[] row_pointers; png_read_end(png_ptr, nullptr); // Finish reading remaining chunks png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); fclose(fp); // Note: PNG typically doesn't store EXIF in the same way as JPEG/TIFF. // It can have text chunks (tEXt, zTXt, iTXt) which might hold metadata. // Reading these requires additional libpng calls (png_get_text). Not implemented here. return image; } // --- libtiff Loading --- // Suppress libtiff warnings/errors (optional, can be noisy) void tiffErrorHandler(const char *module, const char *fmt, va_list ap) { /* Do nothing */ } void tiffWarningHandler(const char *module, const char *fmt, va_list ap) { /* Do nothing */ } inline std::optional loadTiff(const std::string &filePath) { // Set custom handlers to suppress console output from libtiff // TIFFSetErrorHandler(tiffErrorHandler); // TIFFSetWarningHandler(tiffWarningHandler); TIFF *tif = TIFFOpen(filePath.c_str(), "r"); if (!tif) { LogError("Cannot open TIFF file: " + filePath); return std::nullopt; } AppImage image; uint32_t w, h; uint16_t bitsPerSample, samplesPerPixel, photometric, planarConfig; TIFFGetFieldDefaulted(tif, TIFFTAG_IMAGEWIDTH, &w); TIFFGetFieldDefaulted(tif, TIFFTAG_IMAGELENGTH, &h); TIFFGetFieldDefaulted(tif, TIFFTAG_BITSPERSAMPLE, &bitsPerSample); TIFFGetFieldDefaulted(tif, TIFFTAG_SAMPLESPERPIXEL, &samplesPerPixel); TIFFGetFieldDefaulted(tif, TIFFTAG_PHOTOMETRIC, &photometric); TIFFGetFieldDefaulted(tif, TIFFTAG_PLANARCONFIG, &planarConfig); image.m_width = w; image.m_height = h; image.m_channels = samplesPerPixel; // Usually 1 (Gray) or 3 (RGB) or 4 (RGBA) // --- Sanity Checks --- if (w == 0 || h == 0 || samplesPerPixel == 0) { LogError("libtiff: Invalid dimensions or samples per pixel."); TIFFClose(tif); return std::nullopt; } if (bitsPerSample != 8 && bitsPerSample != 16 && bitsPerSample != 32) { // Note: 32-bit float TIFFs exist but require different handling LogError("libtiff: Unsupported bits per sample: " + std::to_string(bitsPerSample) + ". Only 8/16 supported currently."); TIFFClose(tif); return std::nullopt; } if (photometric != PHOTOMETRIC_MINISBLACK && photometric != PHOTOMETRIC_MINISWHITE && photometric != PHOTOMETRIC_RGB && photometric != PHOTOMETRIC_PALETTE && photometric != PHOTOMETRIC_MASK && photometric != PHOTOMETRIC_SEPARATED /*CMYK?*/ && photometric != PHOTOMETRIC_LOGL && photometric != PHOTOMETRIC_LOGLUV) { LogWarning("libtiff: Unhandled photometric interpretation: " + std::to_string(photometric)); // We will try to read as RGB/Gray anyway... might be wrong. } // --- Data Reading --- // Use TIFFReadRGBAImage for simplicity - converts many formats to RGBA uint32 internally // Advantage: Handles various photometric interpretations, planar configs, palettes etc. // Disadvantage: Always gives 8-bit RGBA, loses 16-bit precision. Less control. // Alternative: Read scanlines manually (more complex, preserves bit depth) // Let's try the manual scanline approach to preserve bit depth size_t num_pixels = static_cast(w) * h; size_t total_values = num_pixels * samplesPerPixel; // Total uint8/uint16 values image.m_pixelData.resize(total_values); // Resize for float output image.m_isLinear = true; // Assume linear, correct later if gamma info found image.m_colorSpaceName = "Linear Unknown (TIFF)"; // Default assumption tmsize_t scanline_size = TIFFScanlineSize(tif); std::vector scanline_buffer(scanline_size); float *app_data_ptr = image.m_pixelData.data(); float max_val = (bitsPerSample == 8) ? 255.0f : 65535.0f; // Normalization factor if (planarConfig == PLANARCONFIG_CONTIG) { for (uint32_t row = 0; row < h; ++row) { if (TIFFReadScanline(tif, scanline_buffer.data(), row) < 0) { LogError("libtiff: Error reading scanline " + std::to_string(row)); TIFFClose(tif); return std::nullopt; } // Process the contiguous scanline if (bitsPerSample == 8) { unsigned char *buf_ptr = scanline_buffer.data(); for (size_t i = 0; i < w * samplesPerPixel; ++i) { *app_data_ptr++ = static_cast(buf_ptr[i]) / max_val; } } else { // bitsPerSample == 16 uint16_t *buf_ptr = reinterpret_cast(scanline_buffer.data()); for (size_t i = 0; i < w * samplesPerPixel; ++i) { *app_data_ptr++ = static_cast(buf_ptr[i]) / max_val; } } } } else if (planarConfig == PLANARCONFIG_SEPARATE) { // Read plane by plane - more complex, needs buffer per plane LogWarning("libtiff: Planar configuration PLANARCONFIG_SEPARATE reading not fully implemented, data might be incorrect."); // Basic attempt: Read all scanlines for each plane sequentially into the final buffer size_t plane_stride = w * h; for (uint16_t plane = 0; plane < samplesPerPixel; ++plane) { float *plane_start_ptr = image.m_pixelData.data() + plane; // Start at the channel offset for (uint32_t row = 0; row < h; ++row) { if (TIFFReadScanline(tif, scanline_buffer.data(), row, plane) < 0) { LogError("libtiff: Error reading scanline " + std::to_string(row) + " plane " + std::to_string(plane)); TIFFClose(tif); return std::nullopt; } // Process the separate scanline for this plane if (bitsPerSample == 8) { unsigned char *buf_ptr = scanline_buffer.data(); float *current_pixel_in_plane = plane_start_ptr + row * w * samplesPerPixel; for (uint32_t col = 0; col < w; ++col) { *current_pixel_in_plane = static_cast(buf_ptr[col]) / max_val; current_pixel_in_plane += samplesPerPixel; // Jump to next pixel's spot for this channel } } else { // 16 bit uint16_t *buf_ptr = reinterpret_cast(scanline_buffer.data()); float *current_pixel_in_plane = plane_start_ptr + row * w * samplesPerPixel; for (uint32_t col = 0; col < w; ++col) { *current_pixel_in_plane = static_cast(buf_ptr[col]) / max_val; current_pixel_in_plane += samplesPerPixel; } } } } } else { LogError("libtiff: Unknown planar configuration: " + std::to_string(planarConfig)); TIFFClose(tif); return std::nullopt; } // --- Post-processing based on Photometric interpretation --- // Handle grayscale inversion if (photometric == PHOTOMETRIC_MINISWHITE) { LogWarning("libtiff: Inverting MINISWHITE image."); for (float &val : image.m_pixelData) { val = 1.0f - val; // Simple inversion } } // TODO: Handle Palette -> RGB (needs reading the colormap tag) if (photometric == PHOTOMETRIC_PALETTE) { LogWarning("libtiff: PHOTOMETRIC_PALETTE not fully handled. Image loaded as indexed."); // Requires reading TIFFTAG_COLORMAP and expanding pixels } // TODO: Check for gamma tags or ICC profile tag // uint16_t* icc_profile_count = nullptr; // void* icc_profile_data = nullptr; // if (TIFFGetField(tif, TIFFTAG_ICCPROFILE, &icc_profile_count, &icc_profile_data) && icc_profile_count && icc_profile_data) { // image.m_iccProfile.resize(*icc_profile_count); // std::memcpy(image.m_iccProfile.data(), icc_profile_data, *icc_profile_count); // image.m_colorSpaceName = "Linear (Embedded ICC)"; // Or just "(Embedded ICC)" // } else { // // Check for gamma? Not standard. Assume sRGB/linear for now. // } // If no specific color info found, assume sRGB and convert to linear // For TIFF, it's often safer to assume linear if 16-bit, sRGB if 8-bit without other info. if (bitsPerSample == 8) { LogWarning("libtiff: Assuming 8-bit TIFF is sRGB. Converting to linear."); for (float &val : image.m_pixelData) { val = srgb_to_linear_approx(val); } image.m_isLinear = true; image.m_colorSpaceName = "Linear sRGB (Assumed)"; } else { LogWarning("libtiff: Assuming 16-bit TIFF is already linear."); image.m_isLinear = true; image.m_colorSpaceName = "Linear Unknown (TIFF)"; } TIFFClose(tif); // Try loading EXIF using LibTiff directory reading or Exiv2 (not EasyExif) // This basic example doesn't load EXIF from TIFFs. // You could use Exiv2 here if integrated. LogWarning("EXIF loading from TIFF not implemented in this example."); return image; } } // namespace AppImageUtil // --- AppImage Constructor Implementation --- AppImage::AppImage(uint32_t width, uint32_t height, uint32_t channels) : m_width(width), m_height(height), m_channels(channels), m_isLinear(true) { if (width > 0 && height > 0 && channels > 0) { try { m_pixelData.resize(static_cast(width) * height * channels); } catch (const std::bad_alloc &e) { AppImageUtil::LogError("Failed to allocate memory for image: " + std::string(e.what())); clear_image(); // Reset to empty state throw; // Re-throw exception } } // Default assumption is linear data in our internal format m_colorSpaceName = "Linear Generic"; } void AppImage::resize(uint32_t newWidth, uint32_t newHeight, uint32_t newChannels) { if (newChannels == 0) newChannels = m_channels; if (newChannels == 0) newChannels = 3; // Default if was empty m_width = newWidth; m_height = newHeight; m_channels = newChannels; if (newWidth == 0 || newHeight == 0 || newChannels == 0) { m_pixelData.clear(); // Keep metadata? Optional. } else { try { m_pixelData.resize(static_cast(newWidth) * newHeight * newChannels); // Note: Resizing doesn't preserve pixel content intelligently. // Consider adding different resize modes (clear, copy existing, etc.) } catch (const std::bad_alloc &e) { AppImageUtil::LogError("Failed to allocate memory during resize: " + std::string(e.what())); clear_image(); throw; } } } void AppImage::clear_image() { m_width = 0; m_height = 0; m_channels = 0; m_pixelData.clear(); m_metadata.clear(); m_iccProfile.clear(); m_colorSpaceName = "Unknown"; m_isLinear = true; } // --- loadImage Implementation --- std::optional loadImage(const std::string &filePath) { using namespace AppImageUtil; DetectedFileType type = detectFileType(filePath); try { switch (type) { case DetectedFileType::RAW: LogWarning("Detected type: RAW (using LibRaw)"); return loadRaw(filePath); case DetectedFileType::JPEG: LogWarning("Detected type: JPEG (using libjpeg)"); return loadJpeg(filePath); case DetectedFileType::PNG: LogWarning("Detected type: PNG (using libpng)"); return loadPng(filePath); case DetectedFileType::TIFF: LogWarning("Detected type: TIFF (using libtiff)"); // LibRaw can sometimes open TIFFs that contain RAW data. Try it first? // For now, directly use libtiff. return loadTiff(filePath); case DetectedFileType::UNKNOWN: default: LogError("Unknown or unsupported file type: " + filePath); return std::nullopt; } } catch (const std::exception &e) { LogError("Exception caught during image loading: " + std::string(e.what())); return std::nullopt; } catch (...) { LogError("Unknown exception caught during image loading."); return std::nullopt; } } // --- saveImage Implementation --- namespace AppImageUtil { // --- libjpeg Saving --- inline bool saveJpeg(const AppImage &image, const std::string &filePath, int quality) { if (image.getChannels() != 1 && image.getChannels() != 3) { LogError("libjpeg save: Can only save 1 (Grayscale) or 3 (RGB) channels. Image has " + std::to_string(image.getChannels())); return false; } FILE *outfile = fopen(filePath.c_str(), "wb"); if (!outfile) { LogError("Cannot open file for JPEG writing: " + filePath); return false; } jpeg_compress_struct cinfo; JpegErrorManager jerr; // Use the same error manager as loading // Setup error handling cinfo.err = jpeg_std_error(&jerr.pub); jerr.pub.error_exit = jpegErrorExit; // Use the same exit function if (setjmp(jerr.setjmp_buffer)) { // Error occurred during compression jpeg_destroy_compress(&cinfo); fclose(outfile); return false; } // Initialize compression object jpeg_create_compress(&cinfo); jpeg_stdio_dest(&cinfo, outfile); // Set parameters cinfo.image_width = image.getWidth(); cinfo.image_height = image.getHeight(); cinfo.input_components = image.getChannels(); cinfo.in_color_space = (image.getChannels() == 1) ? JCS_GRAYSCALE : JCS_RGB; jpeg_set_defaults(&cinfo); jpeg_set_quality(&cinfo, std::max(1, std::min(100, quality)), TRUE /* limit to baseline-JPEG */); // Could set density, comments, etc. here if needed using jpeg_set_... functions // Start compressor jpeg_start_compress(&cinfo, TRUE); // Prepare 8-bit sRGB scanline buffer int row_stride = cinfo.image_width * cinfo.input_components; std::vector scanline_buffer(row_stride); const float *app_data = image.getData(); // Process scanlines while (cinfo.next_scanline < cinfo.image_height) { unsigned char *buffer_ptr = scanline_buffer.data(); size_t row_start_index = static_cast(cinfo.next_scanline) * cinfo.image_width * cinfo.input_components; // Convert one row from linear float to 8-bit sRGB uchar for (int i = 0; i < row_stride; ++i) { float linear_val = app_data[row_start_index + i]; float srgb_val = linear_to_srgb_approx(linear_val); int int_val = static_cast(std::round(srgb_val * 255.0f)); buffer_ptr[i] = static_cast(std::max(0, std::min(255, int_val))); } JSAMPROW row_pointer[1]; row_pointer[0] = scanline_buffer.data(); jpeg_write_scanlines(&cinfo, row_pointer, 1); } // Finish compression and clean up jpeg_finish_compress(&cinfo); jpeg_destroy_compress(&cinfo); fclose(outfile); // --- Metadata Saving --- LogWarning("JPEG EXIF/ICC Metadata saving is NOT implemented."); // Saving metadata would typically involve: // 1. Using Exiv2 library. // 2. Opening the file *after* libjpeg saves the pixels. // 3. Writing the metadata from image.m_metadata and image.m_iccProfile into the file structure. return true; } // --- libpng Saving --- inline bool savePng(const AppImage &image, const std::string &filePath, int bit_depth_out) { if (bit_depth_out != 8 && bit_depth_out != 16) { LogError("libpng save: Only 8 or 16 bit output supported."); return false; } if (image.getChannels() < 1 || image.getChannels() > 4 || image.getChannels() == 2) { LogError("libpng save: Can only save 1 (Gray), 3 (RGB), or 4 (RGBA) channels. Image has " + std::to_string(image.getChannels())); return false; } FILE *fp = fopen(filePath.c_str(), "wb"); if (!fp) { LogError("Cannot open file for PNG writing: " + filePath); return false; } png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, pngErrorFunc, pngWarningFunc); if (!png_ptr) { LogError("libpng: png_create_write_struct failed"); fclose(fp); return false; } png_infop info_ptr = png_create_info_struct(png_ptr); if (!info_ptr) { LogError("libpng: png_create_info_struct failed"); png_destroy_write_struct(&png_ptr, nullptr); fclose(fp); return false; } // Setup jump buffer for error handling jmp_buf jmpbuf; if (setjmp(jmpbuf)) { LogError("libpng: Error during write"); png_destroy_write_struct(&png_ptr, &info_ptr); fclose(fp); return false; } png_set_error_fn(png_ptr, reinterpret_cast(&jmpbuf), pngErrorFunc, pngWarningFunc); png_init_io(png_ptr, fp); // Determine PNG color type int color_type; switch (image.getChannels()) { case 1: color_type = PNG_COLOR_TYPE_GRAY; break; case 3: color_type = PNG_COLOR_TYPE_RGB; break; case 4: color_type = PNG_COLOR_TYPE_RGB_ALPHA; break; default: /* Should have been caught earlier */ return false; } // Set IHDR chunk png_set_IHDR(png_ptr, info_ptr, image.getWidth(), image.getHeight(), bit_depth_out, color_type, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); // Set Gamma/sRGB info bool save_as_srgb = (bit_depth_out == 8); // Convention: Save 8-bit as sRGB, 16-bit as linear if (save_as_srgb) { png_set_sRGB_gAMA_and_cHRM(png_ptr, info_ptr, PNG_sRGB_INTENT_PERCEPTUAL); LogWarning("libpng save: Saving 8-bit PNG with sRGB chunk."); } else { // 16-bit linear png_set_gAMA(png_ptr, info_ptr, 1.0); // Explicitly linear gamma LogWarning("libpng save: Saving 16-bit PNG with gamma 1.0 (linear)."); } // Write header info png_write_info(png_ptr, info_ptr); // --- Prepare Data --- std::vector row_pointers(image.getHeight()); size_t values_per_row = static_cast(image.getWidth()) * image.getChannels(); size_t bytes_per_value = (bit_depth_out == 8) ? 1 : 2; size_t row_bytes = values_per_row * bytes_per_value; std::vector output_buffer(row_bytes * image.getHeight()); const float *app_data = image.getData(); bool needs_swap = (bit_depth_out == 16 && (png_get_uint_16((png_bytep) "\x01\x02") != 0x0102)); // Check endianness only for 16-bit // Convert internal float data to target format row by row for (uint32_t y = 0; y < image.getHeight(); ++y) { unsigned char *row_buf_ptr = output_buffer.data() + y * row_bytes; row_pointers[y] = row_buf_ptr; size_t row_start_index = static_cast(y) * values_per_row; if (bit_depth_out == 8) { unsigned char *uchar_ptr = row_buf_ptr; for (size_t i = 0; i < values_per_row; ++i) { float linear_val = app_data[row_start_index + i]; float srgb_val = linear_to_srgb_approx(linear_val); // Convert to sRGB for 8-bit output int int_val = static_cast(std::round(srgb_val * 255.0f)); uchar_ptr[i] = static_cast(std::max(0, std::min(255, int_val))); } } else { // 16-bit uint16_t *ushort_ptr = reinterpret_cast(row_buf_ptr); for (size_t i = 0; i < values_per_row; ++i) { float linear_val = app_data[row_start_index + i]; // Clamp linear value before scaling for 16-bit output (0.0 to 1.0 range typical for linear PNG) linear_val = std::fmax(0.0f, std::fmin(1.0f, linear_val)); int int_val = static_cast(std::round(linear_val * 65535.0f)); uint16_t val16 = static_cast(std::max(0, std::min(65535, int_val))); if (needs_swap) { // Swap bytes for big-endian PNG format ushort_ptr[i] = (val16 >> 8) | (val16 << 8); } else { ushort_ptr[i] = val16; } } } } // Write image data png_write_image(png_ptr, row_pointers.data()); // End writing png_write_end(png_ptr, nullptr); // Clean up png_destroy_write_struct(&png_ptr, &info_ptr); fclose(fp); LogWarning("PNG Metadata saving (text chunks, ICC) is NOT implemented."); return true; } // --- libtiff Saving --- inline bool saveTiff(const AppImage &image, const std::string &filePath, int bit_depth_out) { if (bit_depth_out != 8 && bit_depth_out != 16) { LogError("libtiff save: Only 8 or 16 bit output supported."); return false; } if (image.getChannels() < 1 || image.getChannels() > 4 || image.getChannels() == 2) { LogError("libtiff save: Can only save 1 (Gray), 3 (RGB), or 4 (RGBA) channels. Image has " + std::to_string(image.getChannels())); return false; } TIFF *tif = TIFFOpen(filePath.c_str(), "w"); if (!tif) { LogError("Cannot open file for TIFF writing: " + filePath); return false; } // --- Set Core TIFF Tags --- TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, image.getWidth()); TIFFSetField(tif, TIFFTAG_IMAGELENGTH, image.getHeight()); TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, static_cast(image.getChannels())); TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, static_cast(bit_depth_out)); TIFFSetField(tif, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT); TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); // Interleaved is simpler // Set Photometric Interpretation uint16_t photometric; if (image.getChannels() == 1) { photometric = PHOTOMETRIC_MINISBLACK; // Grayscale } else if (image.getChannels() >= 3) { photometric = PHOTOMETRIC_RGB; // RGB or RGBA if (image.getChannels() == 4) { // Need to specify that the extra channel is Alpha uint16_t extra_samples = 1; uint16_t sample_info[] = {EXTRASAMPLE_ASSOCALPHA}; // Associated alpha TIFFSetField(tif, TIFFTAG_EXTRASAMPLES, extra_samples, sample_info); } } else { LogError("libtiff save: Unexpected channel count: " + std::to_string(image.getChannels())); TIFFClose(tif); return false; } TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, photometric); // Compression (optional, default is none) TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE); // Examples: COMPRESSION_LZW, COMPRESSION_ADOBE_DEFLATE // Rows per strip (can affect performance/compatibility) // A sensible default is often related to scanline buffer size. TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, TIFFDefaultStripSize(tif, (uint32_t)-1)); // Software Tag (optional) TIFFSetField(tif, TIFFTAG_SOFTWARE, "AppImage Saver"); // --- Prepare and Write Data --- size_t values_per_row = static_cast(image.getWidth()) * image.getChannels(); size_t bytes_per_value = (bit_depth_out == 8) ? 1 : 2; tmsize_t row_bytes = values_per_row * bytes_per_value; std::vector output_buffer(row_bytes); // Buffer for one row const float *app_data = image.getData(); bool save_as_srgb = (bit_depth_out == 8); // Convention: 8-bit=sRGB, 16-bit=Linear for (uint32_t y = 0; y < image.getHeight(); ++y) { unsigned char *row_buf_ptr = output_buffer.data(); size_t row_start_index = static_cast(y) * values_per_row; if (bit_depth_out == 8) { unsigned char *uchar_ptr = row_buf_ptr; for (size_t i = 0; i < values_per_row; ++i) { float linear_val = app_data[row_start_index + i]; float srgb_val = linear_to_srgb_approx(linear_val); // Convert to sRGB int int_val = static_cast(std::round(srgb_val * 255.0f)); uchar_ptr[i] = static_cast(std::max(0, std::min(255, int_val))); } } else { // 16-bit uint16_t *ushort_ptr = reinterpret_cast(row_buf_ptr); for (size_t i = 0; i < values_per_row; ++i) { float linear_val = app_data[row_start_index + i]; // Clamp linear [0,1] before scaling linear_val = std::fmax(0.0f, std::fmin(1.0f, linear_val)); int int_val = static_cast(std::round(linear_val * 65535.0f)); ushort_ptr[i] = static_cast(std::max(0, std::min(65535, int_val))); // Note: TIFF uses native byte order by default, no swapping needed usually. } } // Write the scanline if (TIFFWriteScanline(tif, row_buf_ptr, y, 0) < 0) { LogError("libtiff save: Error writing scanline " + std::to_string(y)); TIFFClose(tif); return false; } } // Clean up TIFFClose(tif); LogWarning("TIFF EXIF/ICC Metadata saving is NOT implemented."); // Saving metadata requires: // 1. Using Exiv2 or LibTiff's directory writing functions *before* closing the file. // 2. For ICC: TIFFSetField(tif, TIFFTAG_ICCPROFILE, count, data_ptr); return true; } } // namespace AppImageUtil 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 bool loadImageTexture(AppImage &appImage) { if (appImage.isEmpty() || appImage.getWidth() == 0 || appImage.getHeight() == 0) { AppImageUtil::LogError("loadImageTexture: Image is empty."); return false; } if (!appImage.isLinear()) { // This shouldn't happen if loadImage converts correctly, but good practice to check. AppImageUtil::LogWarning("loadImageTexture: Warning - Image data is not linear. Pipeline expects linear input."); // Ideally, convert to linear here if not already done. For now, proceed with caution. } const int width = static_cast(appImage.getWidth()); const int height = static_cast(appImage.getHeight()); const int channels = static_cast(appImage.getChannels()); const float *linearData = appImage.getData(); size_t numFloats = static_cast(width) * height * channels; if (!linearData || numFloats == 0) { AppImageUtil::LogError("loadImageTexture: Image data pointer is null or size is zero."); return false; } // --- Determine OpenGL texture format --- GLenum internalFormat; GLenum dataFormat; GLenum dataType = GL_FLOAT; std::vector textureDataBuffer; // Temporary buffer if we need to convert format (e.g., RGB -> RGBA) const float* dataPtr = linearData; if (channels == 1) { internalFormat = GL_R16F; // Single channel, 16-bit float dataFormat = GL_RED; // Expand Grayscale to RGBA for easier shader handling (optional, shaders could handle GL_RED) // Example: Expand to RGBA float buffer textureDataBuffer.resize(static_cast(width) * height * 4); float* outPtr = textureDataBuffer.data(); for(int i = 0; i < width * height; ++i) { float val = linearData[i]; *outPtr++ = val; *outPtr++ = val; *outPtr++ = val; *outPtr++ = 1.0f; // Alpha } internalFormat = GL_RGBA16F; // Use RGBA16F if expanding dataFormat = GL_RGBA; dataPtr = textureDataBuffer.data(); // Point to the new buffer AppImageUtil::LogWarning("loadImageTexture: Expanding 1-channel to RGBA16F for texture."); } else if (channels == 3) { internalFormat = GL_RGBA16F; // Store as RGBA, easier for FBOs/blending dataFormat = GL_RGBA; // Need to convert RGB float -> RGBA float textureDataBuffer.resize(static_cast(width) * height * 4); float* outPtr = textureDataBuffer.data(); const float* inPtr = linearData; for(int i = 0; i < width * height; ++i) { *outPtr++ = *inPtr++; // R *outPtr++ = *inPtr++; // G *outPtr++ = *inPtr++; // B *outPtr++ = 1.0f; // A } dataPtr = textureDataBuffer.data(); // Point to the new buffer AppImageUtil::LogWarning("loadImageTexture: Expanding 3-channel RGB to RGBA16F for texture."); } else if (channels == 4) { internalFormat = GL_RGBA16F; // Native RGBA dataFormat = GL_RGBA; dataPtr = linearData; // Use original data directly } else { AppImageUtil::LogError("loadImageTexture: Unsupported number of channels: " + std::to_string(channels)); return false; } // --- Upload to OpenGL Texture --- GLint lastTexture; glGetIntegerv(GL_TEXTURE_BINDING_2D, &lastTexture); if (appImage.m_textureId == 0) { glGenTextures(1, &appImage.m_textureId); AppImageUtil::LogWarning("loadImageTexture: Generated new texture ID: " + std::to_string(appImage.m_textureId)); } else { AppImageUtil::LogWarning("loadImageTexture: Reusing texture ID: " + std::to_string(appImage.m_textureId)); } glBindTexture(GL_TEXTURE_2D, appImage.m_textureId); // Use GL_LINEAR for smoother results when zooming/scaling in the viewer, even if processing is nearest neighbor. // The processing pipeline itself uses FBOs, textures don't need mipmaps typically. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 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_ALIGNMENT, 1); // Ensure correct alignment, especially for RGB data glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); // Data is contiguous // Check if texture dimensions/format need updating bool needsTexImage = true; if (appImage.m_textureWidth == width && appImage.m_textureHeight == height) { // Could potentially use glTexSubImage2D if format matches, but glTexImage2D is safer // if the internal format might change or if it's the first load. // For simplicity, we'll just recreate with glTexImage2D. AppImageUtil::LogWarning("loadImageTexture: Texture dimensions match, overwriting with glTexImage2D."); } else { AppImageUtil::LogWarning("loadImageTexture: Texture dimensions or format mismatch, recreating with glTexImage2D."); } glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, dataFormat, dataType, dataPtr); GLenum err = glGetError(); if (err != GL_NO_ERROR) { AppImageUtil::LogError("loadImageTexture: OpenGL Error after glTexImage2D: " + std::to_string(err)); glBindTexture(GL_TEXTURE_2D, lastTexture); // Restore previous binding // Consider deleting the texture ID if creation failed badly? if (appImage.m_textureId != 0) { glDeleteTextures(1, &appImage.m_textureId); appImage.m_textureId = 0; } return false; } else { AppImageUtil::LogWarning("loadImageTexture: glTexImage2D successful."); } // Optional: Generate mipmaps if you want smoother downscaling *in the final view* // glGenerateMipmap(GL_TEXTURE_2D); // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glBindTexture(GL_TEXTURE_2D, lastTexture); // Restore previous binding appImage.m_textureWidth = width; appImage.m_textureHeight = height; AppImageUtil::LogWarning("loadImageTexture: Successfully loaded linear data into texture ID " + std::to_string(appImage.m_textureId)); return true; } // --- saveImage Implementation --- bool saveImage(const AppImage &image, const std::string &filePath, ImageSaveFormat format, int quality) { using namespace AppImageUtil; if (image.isEmpty()) { LogError("Cannot save an empty image."); return false; } // Ensure internal data is linear before saving (or handle conversion if needed) if (!image.isLinear()) { LogWarning("Attempting to save non-linear internal data. Results may be incorrect if conversion to target space isn't handled properly."); // Ideally, convert to linear here if required by the saving functions. // For this implementation, we assume the saving functions expect linear input // and perform the linear -> target space conversion (e.g., linear -> sRGB). } try { switch (format) { case ImageSaveFormat::JPEG: return saveJpeg(image, filePath, quality); case ImageSaveFormat::PNG_8: return savePng(image, filePath, 8); case ImageSaveFormat::PNG_16: return savePng(image, filePath, 16); case ImageSaveFormat::TIFF_8: return saveTiff(image, filePath, 8); case ImageSaveFormat::TIFF_16: return saveTiff(image, filePath, 16); case ImageSaveFormat::UNKNOWN: default: LogError("Unknown or unsupported save format specified."); return false; } } catch (const std::exception &e) { LogError("Exception caught during image saving: " + std::string(e.what())); return false; } catch (...) { LogError("Unknown exception caught during image saving."); return false; } } #endif // APP_IMAGE_IMPLEMENTATION #endif // APP_IMAGE_H ```