tedit/llm.md
2025-04-07 20:08:16 -04:00

85 KiB

// 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 "imgui.h"
#include "imgui_impl_sdl2.h"
#include "imgui_impl_opengl3.h"
#include <stdio.h>
#include <SDL.h>
#if defined(IMGUI_IMPL_OPENGL_ES2)
#include <SDL_opengles2.h>
#else
#include <SDL_opengl.h>

#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"


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; 

// 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

    // 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);

    AppImage g_loadedImage; // Your loaded image data
    bool g_imageIsLoaded = false;

    std::optional<AppImage> imgOpt = loadImage("testraw.arw");
    if (imgOpt)
    {
        g_loadedImage = std::move(*imgOpt); // Store the loaded image data
        if (loadImageTexture(g_loadedImage))
        {
            // Load the image into the viewer
            g_imageIsLoaded = true;
            std::cout << "Image loaded into viewer successfully." << std::endl;
        }
        else
        {
            g_imageIsLoaded = false;
            std::cerr << "Failed to load image into viewer." << std::endl;
        }
    }
    else
    {
        g_imageIsLoaded = false;
    }

    ImGuiTexInspect::ImplOpenGL3_Init(); // Or DirectX 11 equivalent (check your chosen backend header file)
    ImGuiTexInspect::Init();
    ImGuiTexInspect::CreateContext();

    // 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();

        if (ImGui::BeginMainMenuBar()) {
            if (ImGui::BeginMenu("File")) {
                if (ImGui::MenuItem("Open...", "Ctrl+O")) { /* Do something */ }
                if (ImGui::MenuItem("Save", "Ctrl+S")) { /* Do something */ }
                if (ImGui::MenuItem("Save As...", "Ctrl+Shift+S")) { /* Do something */ }
                ImGui::Separator();
                if (ImGui::MenuItem("Export...", "Ctrl+E")) { /* Do something */ }
                ImGui::Separator();
                if (ImGui::MenuItem("Exit")) {
                    // Need access to the window pointer to close it cleanly
                    // glfwSetWindowShouldClose(window_ptr, true); // How to get window_ptr here? Pass it or use a global/singleton.
                    // For simplicity now, just print.
                     std::cout << "Exit selected (implement window closing)" << std::endl;
                }
                ImGui::EndMenu();
            }
            if (ImGui::BeginMenu("Edit")) {
                if (ImGui::MenuItem("Undo", "Ctrl+Z")) { /* Do something */ }
                if (ImGui::MenuItem("Redo", "Ctrl+Y")) { /* Do something */ }
                ImGui::Separator();
                 if (ImGui::MenuItem("Copy Settings")) { /* Do something */ }
                 if (ImGui::MenuItem("Paste Settings")) { /* Do something */ }
                ImGui::EndMenu();
            }
             if (ImGui::BeginMenu("View")) {
                if (ImGui::MenuItem("Zoom In", "+")) { /* Do something */ }
                if (ImGui::MenuItem("Zoom Out", "-")) { /* Do something */ }
                if (ImGui::MenuItem("Fit to Screen", "0")) { /* Do something */ }
                ImGui::EndMenu();
            }
             if (ImGui::BeginMenu("Help")) {
                if (ImGui::MenuItem("About")) { /* Do something */ }
                ImGui::EndMenu();
            }
            ImGui::EndMainMenuBar();
        }

        static bool use_dockspace = true;
        if (use_dockspace)
        {
            ImGuiViewport *viewport = ImGui::GetMainViewport();
            ImGui::SetNextWindowPos(viewport->WorkPos);
            ImGui::SetNextWindowSize(viewport->WorkSize);
            ImGui::SetNextWindowViewport(viewport->ID);
            ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
            ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
            ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse |
                                            ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
            window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus;
            window_flags |= ImGuiWindowFlags_NoBackground; // Let DockSpace handle background

            ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
            ImGui::Begin("MainDockspaceWindow", &use_dockspace, window_flags);
            ImGui::PopStyleVar(3); // WindowPadding, WindowBorderSize, WindowRounding

            ImGuiID dockspace_id = ImGui::GetID("MyDockSpace");
            ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_None);

            // Setup a docking layout: Left = "Image Exif", Right = "Edit Image", Center = "Image View"
            if (!ImGui::DockBuilderGetNode(dockspace_id))
            {
                ImGui::DockBuilderRemoveNode(dockspace_id);
                ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
                ImGui::DockBuilderSetNodeSize(dockspace_id, viewport->Size);
                ImGuiID dock_main_id = dockspace_id;
                ImGuiID dock_left_id, dock_right_id, dock_center_id;
                // Split off 20% on the left for Image Exif.
                ImGui::DockBuilderSplitNode(dock_main_id, ImGuiDir_Left, 0.2f, &dock_left_id, &dock_main_id);
                // Split off 20% on the right for Edit Image.
                ImGui::DockBuilderSplitNode(dock_main_id, ImGuiDir_Right, 0.2f, &dock_right_id, &dock_center_id);
                // Dock the windows into their panels.
                ImGui::DockBuilderDockWindow("Image Exif", dock_left_id);
                ImGui::DockBuilderDockWindow("Edit Image", dock_right_id);
                ImGui::DockBuilderDockWindow("Image View", dock_center_id);
                ImGui::DockBuilderFinish(dockspace_id);
            }

            // "Image View" window in the center.
            ImGui::Begin("Image View");
            ImVec2 contentSize = ImGui::GetContentRegionAvail();
            ImGuiTexInspect::BeginInspectorPanel("Image Inspector", g_loadedImage.m_textureId,
                                                   ImVec2(g_loadedImage.m_width, g_loadedImage.m_height),
                                                   ImGuiTexInspect::InspectorFlags_NoTooltip |
                                                   ImGuiTexInspect::InspectorFlags_NoGrid |
                                                   ImGuiTexInspect::InspectorFlags_NoForceFilterNearest,
                                                   ImGuiTexInspect::SizeIncludingBorder(contentSize));
            ImGuiTexInspect::EndInspectorPanel();
            ImGui::End();

            // "Image Exif" window docked at the left.
            ImGui::Begin("Image Exif");
            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::Text("Image Metadata: ");
            for (const auto &entry : g_loadedImage.m_metadata)
            {
                ImGui::Text("%s: %s", entry.first.c_str(), entry.second.c_str());
            }
            ImGui::End();

            // "Edit Image" window docked at the right.
            ImGui::Begin("Edit Image");
            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);
                ImGui::SliderFloat("Contrast", &contrast, -100.0f, 100.0f);
                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::End();

            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
    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplSDL2_Shutdown();
    ImGui::DestroyContext();

    SDL_GL_DeleteContext(gl_context);
    SDL_DestroyWindow(window);
    SDL_Quit();

    return 0;
}
#ifndef APP_IMAGE_H
#define APP_IMAGE_H

#include <vector>
#include <string>
#include <map>
#include <optional> // Requires C++17
#include <memory>
#include <cstdint>
#include <cmath>
#include <fstream>
#include <stdexcept>
#include <algorithm>
#include <iostream> // For errors/warnings
#include <cstring>  // For strcmp, memcpy, etc.
#include <setjmp.h> // 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<std::string, std::string>;

// --- 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<float> &getPixelVector() { return m_pixelData; }
    const std::vector<float> &getPixelVector() const { return m_pixelData; }

    // --- Metadata ---
    ImageMetadata &getMetadata() { return m_metadata; }
    const ImageMetadata &getMetadata() const { return m_metadata; }

    // --- Color Information ---
    std::vector<uint8_t> &getIccProfile() { return m_iccProfile; }
    const std::vector<uint8_t> &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<float> m_pixelData;
    ImageMetadata m_metadata;
    std::vector<uint8_t> 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<AppImage> containing the loaded image on success, std::nullopt on failure.
 */
std::optional<AppImage> 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 <libraw/libraw.h>
#include <jpeglib.h>
#include <png.h>
#include <tiffio.h>

// 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<char *>(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<unsigned char> buffer(size);
        if (!file.read(reinterpret_cast<char *>(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<AppImage> 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<size_t>(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<uint16_t *>(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<float>(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<JpegErrorManager *>(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<AppImage> 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<size_t>(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<unsigned char> 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<float>(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<jmp_buf *>(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<AppImage> 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<png_voidp>(&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<size_t>(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<unsigned char> 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<float>(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<uint16_t *>(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<float>(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<AppImage> 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<size_t>(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<unsigned char> 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<float>(buf_ptr[i]) / max_val;
                    }
                }
                else
                { // bitsPerSample == 16
                    uint16_t *buf_ptr = reinterpret_cast<uint16_t *>(scanline_buffer.data());
                    for (size_t i = 0; i < w * samplesPerPixel; ++i)
                    {
                        *app_data_ptr++ = static_cast<float>(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<float>(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<uint16_t *>(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<float>(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<size_t>(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<size_t>(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<AppImage> 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<unsigned char> 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<size_t>(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<int>(std::round(srgb_val * 255.0f));
                buffer_ptr[i] = static_cast<unsigned char>(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<png_voidp>(&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<png_bytep> row_pointers(image.getHeight());
        size_t values_per_row = static_cast<size_t>(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<unsigned char> 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<size_t>(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<int>(std::round(srgb_val * 255.0f));
                    uchar_ptr[i] = static_cast<unsigned char>(std::max(0, std::min(255, int_val)));
                }
            }
            else
            { // 16-bit
                uint16_t *ushort_ptr = reinterpret_cast<uint16_t *>(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<int>(std::round(linear_val * 65535.0f));
                    uint16_t val16 = static_cast<uint16_t>(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<uint16_t>(image.getChannels()));
        TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, static_cast<uint16_t>(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<size_t>(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<unsigned char> 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<size_t>(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<int>(std::round(srgb_val * 255.0f));
                    uchar_ptr[i] = static_cast<unsigned char>(std::max(0, std::min(255, int_val)));
                }
            }
            else
            { // 16-bit
                uint16_t *ushort_ptr = reinterpret_cast<uint16_t *>(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<int>(std::round(linear_val * 65535.0f));
                    ushort_ptr[i] = static_cast<uint16_t>(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)
{
    // --- Basic Error Checking ---
    if (appImage.isEmpty() || appImage.getWidth() == 0 || appImage.getHeight() == 0)
    {
        return false;
    }
    if (!appImage.isLinear())
    {
        AppImageUtil::LogWarning("Image is not in linear color space.");
    }
    const int width = static_cast<int>(appImage.getWidth());
    const int height = static_cast<int>(appImage.getHeight());
    const int channels = static_cast<int>(appImage.getChannels());
    if (channels != 1 && channels != 3 && channels != 4)
    {
        return false;
    }
    const float *linearData = appImage.getData();

    // --- Prepare 8-bit sRGB data for OpenGL (RGBA format) ---
    GLenum internalFormat = GL_RGBA8;
    GLenum dataFormat = GL_RGBA;
    int outputChannels = 4;
    std::vector<unsigned char> textureData(static_cast<size_t>(width) * height * outputChannels);
    unsigned char *outPtr = textureData.data();
    const float *inPtr = linearData;
    for (int y = 0; y < height; ++y)
    {
        for (int x = 0; x < width; ++x)
        {
            float r = 0, g = 0, b = 0, a = 1;
            if (channels == 1)
            {
                r = g = b = *inPtr++;
            }
            else if (channels == 3)
            {
                r = *inPtr++;
                g = *inPtr++;
                b = *inPtr++;
            }
            else
            {
                r = *inPtr++;
                g = *inPtr++;
                b = *inPtr++;
                a = *inPtr++;
            }
            float sr = ImGuiImageViewerUtil::linear_to_srgb_approx(r), sg = ImGuiImageViewerUtil::linear_to_srgb_approx(g), sb = ImGuiImageViewerUtil::linear_to_srgb_approx(b);
            a = std::fmax(0.f, std::fmin(1.f, a));
            *outPtr++ = static_cast<unsigned char>(std::max(0, std::min(255, static_cast<int>(ImGuiImageViewerUtil::Round(sr * 255.f)))));
            *outPtr++ = static_cast<unsigned char>(std::max(0, std::min(255, static_cast<int>(ImGuiImageViewerUtil::Round(sg * 255.f)))));
            *outPtr++ = static_cast<unsigned char>(std::max(0, std::min(255, static_cast<int>(ImGuiImageViewerUtil::Round(sb * 255.f)))));
            *outPtr++ = static_cast<unsigned char>(std::max(0, std::min(255, static_cast<int>(ImGuiImageViewerUtil::Round(a * 255.f)))));
        }
    }

    // --- Upload to OpenGL Texture ---
    GLint lastTexture;
    glGetIntegerv(GL_TEXTURE_BINDING_2D, &lastTexture);
    if (appImage.m_textureId == 0)
    {
        glGenTextures(1, &appImage.m_textureId);
    }
    glBindTexture(GL_TEXTURE_2D, appImage.m_textureId);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // No filtering
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); // No filtering
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
    glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, dataFormat, GL_UNSIGNED_BYTE, textureData.data());
    glBindTexture(GL_TEXTURE_2D, lastTexture);

    appImage.m_textureWidth = width;
    appImage.m_textureHeight = height;
    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

I've attached some relevant code for a image editor I am writing. What I would like to do next is implement a shader pipeline for applying the editing controls specified

I would like for each operation to be a individual shader. I would like the order of the shaders to be configurable (ideally in a Imgui window on the let side) and I would like for the operations to be done in the best color space possible.

Finally, add two controls to the top and bottom of the edit side where the user can specify the input color space, and the output color space