diff --git a/.gitignore b/.gitignore index 9a97bef2a..f55c0a2d1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ python/src/xstudio/version.py .vs/ .DS_Store /build/ +xstudio_install/ +**/qml/*_qml_export.h diff --git a/cmake/macros.cmake b/cmake/macros.cmake index beecfebbe..bc38746cb 100644 --- a/cmake/macros.cmake +++ b/cmake/macros.cmake @@ -101,10 +101,12 @@ macro(default_options_local name) $ ) if (APPLE) - set_target_properties(${name} - PROPERTIES - LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Frameworks" - ) + set_target_properties(${name} + PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Frameworks" + INSTALL_RPATH "@executable_path/../Frameworks" + INSTALL_RPATH_USE_LINK_PATH TRUE + ) else() set_target_properties(${name} PROPERTIES @@ -204,7 +206,7 @@ macro(default_plugin_options name) COMMAND ${CMAKE_COMMAND} -E copy "$" "${CMAKE_CURRENT_BINARY_DIR}/plugin" ) endif() - + endmacro() macro(add_plugin_qml name _dir) @@ -366,7 +368,7 @@ macro(add_python_plugin NAME) copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/${NAME} ${CMAKE_BINARY_DIR}/bin/plugin-python/${NAME}) endif() - + endmacro() macro(create_plugin NAME VERSION DEPS) diff --git a/src/global/src/CMakeLists.txt b/src/global/src/CMakeLists.txt index ebb7dd934..28218db67 100644 --- a/src/global/src/CMakeLists.txt +++ b/src/global/src/CMakeLists.txt @@ -57,4 +57,12 @@ if(UNIX AND NOT APPLE) target_link_libraries(${PROJECT_NAME} PRIVATE asound) # Link against asound on Linux endif() -set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) +if(APPLE) + set_target_properties(${PROJECT_NAME} PROPERTIES + LINK_DEPENDS_NO_SHARED true + INSTALL_RPATH "@executable_path/../Frameworks" + BUILD_WITH_INSTALL_RPATH TRUE + ) +else() + set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) +endif() diff --git a/src/launch/xstudio/src/CMakeLists.txt b/src/launch/xstudio/src/CMakeLists.txt index fdc660382..137e0c7b7 100644 --- a/src/launch/xstudio/src/CMakeLists.txt +++ b/src/launch/xstudio/src/CMakeLists.txt @@ -12,12 +12,12 @@ set(SOURCES xstudio.cpp xstudio_win_resource.rc ../../../../ui/qml/xstudio/qml.qrc - ) + ) else() set(SOURCES xstudio.cpp ../../../../ui/qml/xstudio/qml.qrc - ) + ) endif() if(WIN32) # Add the /bigobj option for xstudio.cpp @@ -134,6 +134,12 @@ elseif(APPLE) configure_file(macdeploy.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/macdeploy.cmake @ONLY) install(SCRIPT ${CMAKE_CURRENT_BINARY_DIR}/macdeploy.cmake) + # Add custom command to fix library paths after build + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -DAPP_BUNDLE_DIR=${CMAKE_BINARY_DIR}/xSTUDIO.app + -P ${CMAKE_CURRENT_SOURCE_DIR}/fixup_macos_bundle.cmake + ) + else() set_target_properties(${PROJECT_NAME} diff --git a/src/launch/xstudio/src/fixup_macos_bundle.cmake b/src/launch/xstudio/src/fixup_macos_bundle.cmake new file mode 100644 index 000000000..4766332a6 --- /dev/null +++ b/src/launch/xstudio/src/fixup_macos_bundle.cmake @@ -0,0 +1,15 @@ +# Fix library paths in the app bundle to ensure they're relative +file(GLOB_RECURSE LIBRARIES "${APP_BUNDLE_DIR}/Contents/Frameworks/*.dylib") +foreach(LIB ${LIBRARIES}) + get_filename_component(LIB_NAME ${LIB} NAME) + execute_process(COMMAND install_name_tool -id "@rpath/${LIB_NAME}" ${LIB}) +endforeach() + +# Fix executable references to libraries +execute_process( + COMMAND + install_name_tool + -add_rpath + "@executable_path/../Frameworks" + ${APP_BUNDLE_DIR}/Contents/MacOS/xstudio.bin +) diff --git a/src/launch/xstudio/src/macdeploy.cmake.in b/src/launch/xstudio/src/macdeploy.cmake.in index 45d0c270b..be0c1e2bf 100644 --- a/src/launch/xstudio/src/macdeploy.cmake.in +++ b/src/launch/xstudio/src/macdeploy.cmake.in @@ -1,3 +1,19 @@ message("Running @macdeployqt_exe@ with args: ${CMAKE_BINARY_DIR}/xSTUDIO.app -qmldir=@CMAKE_SOURCE_DIR@/ui") execute_process(COMMAND "@macdeployqt_exe@" ${CMAKE_BINARY_DIR}/xSTUDIO.app -qmldir=@CMAKE_SOURCE_DIR@/ui - WORKING_DIRECTORY "${CMAKE_INSTALL_PREFIX}") \ No newline at end of file + WORKING_DIRECTORY "${CMAKE_INSTALL_PREFIX}") + +# Fix library paths to ensure they're relative to the bundle +execute_process( + COMMAND + install_name_tool -id + "@rpath/libglobal.dylib" + ${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Frameworks/libglobal.dylib +) +execute_process( + COMMAND + install_name_tool + -change + "@rpath/libglobal.dylib" + "@executable_path/../Frameworks/libglobal.dylib" + ${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/MacOS/xstudio.bin +) diff --git a/src/plugin/media_metadata/CMakeLists.txt b/src/plugin/media_metadata/CMakeLists.txt index 13eb8d447..6fe875574 100644 --- a/src/plugin/media_metadata/CMakeLists.txt +++ b/src/plugin/media_metadata/CMakeLists.txt @@ -1,4 +1,5 @@ add_src_and_test(ffprobe) add_src_and_test(openexr) +add_src_and_test(openimageio) build_studio_plugins("${STUDIO_PLUGINS}") diff --git a/src/plugin/media_metadata/openimageio/src/CMakeLists.txt b/src/plugin/media_metadata/openimageio/src/CMakeLists.txt new file mode 100644 index 000000000..1e915e30d --- /dev/null +++ b/src/plugin/media_metadata/openimageio/src/CMakeLists.txt @@ -0,0 +1,8 @@ +find_package(OpenImageIO) + +SET(LINK_DEPS + xstudio::media_metadata + OpenImageIO::OpenImageIO +) + +create_plugin_with_alias(media_metadata_openimageio xstudio::media_metadata::openimageio ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/plugin/media_metadata/openimageio/src/openimageio_metadata.cpp b/src/plugin/media_metadata/openimageio/src/openimageio_metadata.cpp new file mode 100644 index 000000000..b07861a45 --- /dev/null +++ b/src/plugin/media_metadata/openimageio/src/openimageio_metadata.cpp @@ -0,0 +1,177 @@ +#include "openimageio_metadata.hpp" + +#include +#include + +#include +#include + +#include "xstudio/utility/helpers.hpp" + +namespace fs = std::filesystem; + +using namespace xstudio::media_metadata; +using namespace xstudio::utility; +using namespace xstudio; + +OpenImageIOMediaMetadata::OpenImageIOMediaMetadata() : MediaMetadata("OpenImageIO") {} + +MMCertainty OpenImageIOMediaMetadata::supported( + const caf::uri &uri, const std::array &signature) { + // Step 1: List of supported extensions by OIIO + static const std::unordered_set supported_extensions = { + "JPG", + "JPEG", + "PNG", + "TIF", + "TIFF", + "TGA", + "BMP", + "PSD", + "HDR", + "DPX", + "ACES", + "JP2", + "J2K", + "WEBP", + "EXR", + }; + + // Step 2: Convert the URI to a POSIX path string and fs::path + std::string path = uri_to_posix_path(uri); + fs::path p(path); + + // Step 3: Check if the file exists and is a regular file + // Return not supported if the file does not exist or is not a regular file + if (!fs::exists(p) || !fs::is_regular_file(p)) { + return MMC_NO; + } + + // Step 4: Get the upper-case extension (handling platform differences) +#ifdef _WIN32 + std::string ext = ltrim_char(to_upper_path(p.extension()), '.'); +#else + std::string ext = ltrim_char(to_upper(p.extension().string()), '.'); +#endif + + // Step 5: Check if the extension is in the supported list + // Return fully supported if the extension is in the supported list + if (supported_extensions.count(ext)) { + return MMC_FULLY; + } + + // Step 6: Try to detect via OIIO if the extension is supported + // Return maybe supported if the extension is supported by OIIO + auto in = OIIO::ImageInput::open(path); + if (in) { + in->close(); + return MMC_MAYBE; + } + + // Step 7: Return not supported if all checks fail + return MMC_NO; +} + +nlohmann::json OpenImageIOMediaMetadata::read_metadata(const caf::uri &uri) { + std::string path = uri_to_posix_path(uri); + + try { + // Step 1: Open the image using OpenImageIO + auto in = OIIO::ImageInput::open(path); + if (!in) { + throw std::runtime_error("Cannot open: " + OIIO::geterror()); + } + + // Step 2: Get the image specification + const OIIO::ImageSpec &spec = in->spec(); + + // Step 3: Initialize a JSON object for metadata + nlohmann::json metadata; + + // Step 4: Populate basic image properties (width, height, resolution) + metadata["width"] = spec.width; + metadata["height"] = spec.height; + metadata["resolution"] = fmt::format("{} x {}", spec.width, spec.height); + + // Step 5: Extract and format the file extension (format) + fs::path p(path); +#ifdef _WIN32 + std::string ext = ltrim_char(to_upper_path(p.extension()), '.'); +#else + std::string ext = ltrim_char(to_upper(p.extension().string()), '.'); +#endif + metadata["format"] = ext; + + // Step 6: Determine bit depth and add to metadata + if (spec.format == OIIO::TypeDesc::UINT8) { + metadata["bit_depth"] = "8 bits"; + } else if (spec.format == OIIO::TypeDesc::UINT16) { + metadata["bit_depth"] = "16 bits"; + } else if (spec.format == OIIO::TypeDesc::HALF) { + metadata["bit_depth"] = "16 bits float"; + } else if (spec.format == OIIO::TypeDesc::FLOAT) { + metadata["bit_depth"] = "32 bits float"; + } else { + metadata["bit_depth"] = "unknown"; + } + + // Step 7: Read pixel aspect ratio and aspect ratio + metadata["pixel_aspect"] = spec.get_float_attribute("PixelAspectRatio", 1.0f); + metadata["aspect_ratio"] = spec.get_float_attribute("XResolution", spec.width) + / spec.get_float_attribute("YResolution", spec.height); + + // Step 8: Add extra attributes to metadata + for (const auto ¶m : spec.extra_attribs) { + metadata[param.name().string()] = param.get_string(); + } + + // Step 9: Return metadata + return metadata; + } catch (const std::exception &e) { + spdlog::error("Failed to read metadata from {}: {}", path, e.what()); + return nlohmann::json::object(); + } +} + +std::optional +OpenImageIOMediaMetadata::fill_standard_fields(const nlohmann::json &metadata) { + // Step 1: Initialize StandardFields and set default format + StandardFields fields; + fields.format_ = "OpenImageIO"; + + // Step 2: Fill in the resolution if present in metadata + if (metadata.contains("resolution")) { + fields.resolution_ = metadata["resolution"].get(); + } + + // Step 3: Fill in the image extension (format) if present in metadata + if (metadata.contains("format")) { + fields.format_ = metadata["format"].get(); + } + + // Step 4: Fill in the bit depth if present in metadata + if (metadata.contains("bit_depth")) { + fields.bit_depth_ = metadata["bit_depth"].get(); + } + + // Step 5: Fill in the pixel aspect ratio if present in metadata + if (metadata.contains("pixel_aspect")) { + fields.pixel_aspect_ = metadata["pixel_aspect"].get(); + } + + return std::make_optional(fields); +} + +// Point d'entrĂ©e du plugin +extern "C" { +plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { + return new plugin_manager::PluginFactoryCollection( + std::vector>({std::make_shared< + MediaMetadataPlugin>>( + Uuid("8f3c4a7e-9b2d-4e1f-a5c8-3d6f7e8a9b1c"), + "OpenImageIO", + "xStudio", + "OpenImageIO Media Metadata Reader", + semver::version("1.0.0"))})); +} +} diff --git a/src/plugin/media_metadata/openimageio/src/openimageio_metadata.hpp b/src/plugin/media_metadata/openimageio/src/openimageio_metadata.hpp new file mode 100644 index 000000000..1086009d2 --- /dev/null +++ b/src/plugin/media_metadata/openimageio/src/openimageio_metadata.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include + +#include "xstudio/media_metadata/media_metadata.hpp" + +namespace xstudio { +namespace media_metadata { + + class OpenImageIOMediaMetadata : public MediaMetadata { + public: + OpenImageIOMediaMetadata(); + ~OpenImageIOMediaMetadata() override = default; + + MMCertainty + supported(const caf::uri &uri, const std::array &signature) override; + + protected: + nlohmann::json read_metadata(const caf::uri &uri) override; + std::optional + fill_standard_fields(const nlohmann::json &metadata) override; + }; + +} // namespace media_metadata +} // namespace xstudio diff --git a/src/plugin/media_metadata/openimageio/test/CMakeLists.txt b/src/plugin/media_metadata/openimageio/test/CMakeLists.txt new file mode 100644 index 000000000..66caa1970 --- /dev/null +++ b/src/plugin/media_metadata/openimageio/test/CMakeLists.txt @@ -0,0 +1,7 @@ +include(CTest) + +SET(LINK_DEPS + CAF::core +) + +create_tests("${LINK_DEPS}") diff --git a/src/plugin/media_reader/CMakeLists.txt b/src/plugin/media_reader/CMakeLists.txt index 130849d92..ad8a96b2f 100644 --- a/src/plugin/media_reader/CMakeLists.txt +++ b/src/plugin/media_reader/CMakeLists.txt @@ -2,5 +2,6 @@ add_src_and_test(ffmpeg) add_src_and_test(openexr) add_src_and_test(ppm) add_src_and_test(blank) +add_src_and_test(openimageio) build_studio_plugins("${STUDIO_PLUGINS}") diff --git a/src/plugin/media_reader/openimageio/src/CMakeLists.txt b/src/plugin/media_reader/openimageio/src/CMakeLists.txt new file mode 100644 index 000000000..0c343eee0 --- /dev/null +++ b/src/plugin/media_reader/openimageio/src/CMakeLists.txt @@ -0,0 +1,16 @@ +find_package(OpenImageIO REQUIRED) + +SET(LINK_DEPS + xstudio::media_reader + OpenImageIO::OpenImageIO +) + +create_plugin_with_alias( + media_reader_oiio + xstudio::media_reader::oiio + ${XSTUDIO_GLOBAL_VERSION} + "${LINK_DEPS}" +) + +target_include_directories(media_reader_oiio PRIVATE ${OPENIMAGEIO_INCLUDE_DIR}) + diff --git a/src/plugin/media_reader/openimageio/src/openimageio.cpp b/src/plugin/media_reader/openimageio/src/openimageio.cpp new file mode 100644 index 000000000..036dfad24 --- /dev/null +++ b/src/plugin/media_reader/openimageio/src/openimageio.cpp @@ -0,0 +1,966 @@ +#include +#include +#include +#include +#include +#include + +#include "openimageio.hpp" +#include "xstudio/media_reader/media_reader.hpp" +#include "xstudio/utility/helpers.hpp" +#include "xstudio/ui/opengl/shader_program_base.hpp" + +#include +#include +#include + +namespace fs = std::filesystem; + +using namespace xstudio::media_reader; +using namespace xstudio::utility; +using namespace xstudio; + +namespace { +// Unique UUID for this plugin +static Uuid s_plugin_uuid{"a1b2c3d4-e5f6-7890-abcd-ef1234567890"}; + +// UUID for the shader +static Uuid myshader_uuid{"b1c2d3e4-f5a6-7b98-cdef-123456789abc"}; + +// Channel name definitions +static const std::set RED_CHANNEL_NAMES = {"r", "red"}; +static const std::set GREEN_CHANNEL_NAMES = {"g", "green"}; +static const std::set BLUE_CHANNEL_NAMES = {"b", "blue"}; +static const std::set ALPHA_CHANNEL_NAMES = {"a", "alpha"}; +static const std::set LUMINANCE_CHANNEL_NAMES = { + "y", "luminance", "l", "gray", "grey", "mono"}; + +/** + * @brief Specifies the type of image based on its channel composition. + * + * - IMAGE_GRAYSCALE: An image containing a single luminance (Y) channel. + * - IMAGE_GRAYSCALE_ALPHA: An image containing a luminance (Y) channel plus an alpha channel + * (Y+A). + * - IMAGE_RGB: An image containing three color channels: Red, Green, and Blue. + * - IMAGE_RGBA: An image containing three color channels (Red, Green, Blue) plus an + * alpha channel (RGBA). + */ +enum ImageType { + IMAGE_GRAYSCALE, ///< 1 channel - luminance (Y) only + IMAGE_GRAYSCALE_ALPHA, ///< 2 channels - luminance (Y) and alpha (A) + IMAGE_RGB, ///< 3 channels - red, green, blue (RGB) + IMAGE_RGBA, ///< 4 channels - red, green, blue, alpha (RGBA) +}; + +/** + * @brief Channel identifiers for planar image layout. + */ +enum class Channel { RED, GREEN, BLUE, ALPHA }; + +// Supports: 8-bit, 16-bit UINT, 16-bit HALF, float32 +static std::string oiio_shader_code{R"( +#version 410 core +uniform int width; +uniform int height; +uniform int bytes_per_channel; // 1=8bit, 2=16bit, 4=float +uniform int is_half_float; // 0=UINT16, 1=HALF float +uniform int has_alpha; // 0=no alpha, 1=has alpha +uniform int channel_r_start; +uniform int channel_g_start; +uniform int channel_b_start; +uniform int channel_a_start; + +// Forward declarations +int get_image_data_1byte(int byte_address); +int get_image_data_2bytes(int byte_address); +vec2 get_image_data_2floats(int byte_address); +float get_image_data_float32(int byte_address); + +vec4 fetch_rgba_pixel(ivec2 image_coord) +{ + float R = 0.0f, G = 0.0f, B = 0.0f, A = 1.0f; + + int r_coord = (image_coord.x + image_coord.y * width) * bytes_per_channel + channel_r_start; + int g_coord = (image_coord.x + image_coord.y * width) * bytes_per_channel + channel_g_start; + int b_coord = (image_coord.x + image_coord.y * width) * bytes_per_channel + channel_b_start; + int a_coord = (image_coord.x + image_coord.y * width) * bytes_per_channel + channel_a_start; + + if (bytes_per_channel == 1) { + R = get_image_data_1byte(r_coord) / 255.0; + G = get_image_data_1byte(g_coord) / 255.0; + B = get_image_data_1byte(b_coord) / 255.0; + if (has_alpha == 1) A = get_image_data_1byte(a_coord) / 255.0; + } + else if (bytes_per_channel == 2) { + if (is_half_float == 1) { + R = get_image_data_2floats(r_coord).x; + G = get_image_data_2floats(g_coord).x; + B = get_image_data_2floats(b_coord).x; + if (has_alpha == 1) A = get_image_data_2floats(a_coord).x; + } + else { + R = get_image_data_2bytes(r_coord) / 65535.0; + G = get_image_data_2bytes(g_coord) / 65535.0; + B = get_image_data_2bytes(b_coord) / 65535.0; + if (has_alpha == 1) A = get_image_data_2bytes(a_coord) / 65535.0; + } + } + else if (bytes_per_channel == 4) { + R = get_image_data_float32(r_coord); + G = get_image_data_float32(g_coord); + B = get_image_data_float32(b_coord); + if (has_alpha == 1) A = get_image_data_float32(a_coord); + } + + return vec4(R, G, B, A); +} +)"}; + +static ui::viewport::GPUShaderPtr + oiio_shader(new ui::opengl::OpenGLShader(myshader_uuid, oiio_shader_code)); + +} // namespace + +OIIOMediaReader::OIIOMediaReader(const utility::JsonStore &prefs) : MediaReader("OIIO", prefs) { + update_preferences(prefs); +} + +void OIIOMediaReader::update_preferences(const utility::JsonStore &prefs) { + try { + // Set OIIO threading + int threads = + global_store::preference_value(prefs, "/plugin/media_reader/OIIO/threads"); + OIIO::attribute("threads", threads); + } catch (const std::exception &e) { + } + + try { + // Set OIIO EXR-specific threading + int exr_threads = + global_store::preference_value(prefs, "/plugin/media_reader/OIIO/exr_threads"); + OIIO::attribute("exr_threads", exr_threads); + } catch (const std::exception &e) { + } + + try { + // Enable or disable TBB in OIIO + int use_tbb = + global_store::preference_value(prefs, "/plugin/media_reader/OIIO/use_tbb"); + OIIO::attribute("use_tbb", use_tbb); + } catch (const std::exception &e) { + } + + try { + // Set OIIO log times preference + // When the "log_times" attribute is nonzero, ImageBufAlgo functions are instrumented to + // record the number of times they were called and the total amount of time spent + // executing them. + int log_times = + global_store::preference_value(prefs, "/plugin/media_reader/OIIO/log_times"); + OIIO::attribute("log_times", log_times); + } catch (const std::exception &e) { + } +} + +utility::Uuid OIIOMediaReader::plugin_uuid() const { return s_plugin_uuid; } + +/** + * @brief Detects the image type (grayscale, RGB, etc.) from the given OIIO::ImageSpec. + * + * This function inspects the channel names of the provided ImageSpec to determine the + * image type. It checks for recognized channel names corresponding to RGB, alpha, + * and grayscale channels. If channel names are ambiguous, it falls back to the number + * of channels to decide the type. + * + * @param spec The OpenImageIO ImageSpec describing the image's channel layout. + * @return ImageType Enum indicating the detected image type. + * Possible values: IMAGE_GRAYSCALE, IMAGE_GRAYSCALE_ALPHA, IMAGE_RGB, IMAGE_RGBA + */ +ImageType detect_image_type(const OIIO::ImageSpec &spec) { + static const std::set rgb_names = []() { + std::set merged = RED_CHANNEL_NAMES; + merged.insert(GREEN_CHANNEL_NAMES.begin(), GREEN_CHANNEL_NAMES.end()); + merged.insert(BLUE_CHANNEL_NAMES.begin(), BLUE_CHANNEL_NAMES.end()); + return merged; + }(); + static const std::set alpha_names = ALPHA_CHANNEL_NAMES; + static const std::set gray_names = LUMINANCE_CHANNEL_NAMES; + + int rgb_count = 0; + int alpha_count = 0; + int gray_count = 0; + + // Iterate over each channel and classify its type according to the channel name. + for (int i = 0; i < spec.nchannels; ++i) { + std::string name = spec.channelnames[i]; + std::transform(name.begin(), name.end(), name.begin(), ::tolower); + + if (rgb_names.count(name)) { + rgb_count++; + } else if (alpha_names.count(name)) { + alpha_count++; + } else if (gray_names.count(name)) { + gray_count++; + } + } + + // Prioritize RGB detection: if at least three RGB channels exist, it's RGB or RGBA. + if (rgb_count >= 3) { + return alpha_count > 0 ? IMAGE_RGBA : IMAGE_RGB; + } + + // Handle grayscale images with or without alpha. + if (gray_count == 1) { + return alpha_count > 0 ? IMAGE_GRAYSCALE_ALPHA : IMAGE_GRAYSCALE; + } + + // Fallback: deduce based purely on channel count. + if (spec.nchannels == 1) + return IMAGE_GRAYSCALE; + if (spec.nchannels == 2) + return IMAGE_GRAYSCALE_ALPHA; + if (spec.nchannels == 3) + return IMAGE_RGB; + if (spec.nchannels == 4) + return IMAGE_RGBA; + + // Default case: treat as RGB(A) depending on whether any alpha was found. + return alpha_count > 0 ? IMAGE_RGBA : IMAGE_RGB; +} + +/** + * @brief Generic function to find a channel index by matching a set of possible names. + * + * Iterates through the channel names in the given ImageSpec and returns the index + * of the first channel that matches any name in the provided set (case-insensitive). + * + * @param spec The OpenImageIO ImageSpec describing the image. + * @param channel_names Set of possible channel names to match. + * @param default_value Default value to return if no match is found. + * @return The index of the matching channel, or default_value if not found. + */ +int get_channel_index_by_names( + const OIIO::ImageSpec &spec, + const std::set &channel_names, + int default_value = -1) { + + for (int i = 0; i < spec.nchannels; ++i) { + std::string name = spec.channelnames[i]; + std::transform(name.begin(), name.end(), name.begin(), ::tolower); + + if (channel_names.count(name)) { + return i; + } + } + + return default_value; +} + +/** + * @brief Get the index of the red channel in the image specification. + * @param spec The OpenImageIO ImageSpec describing the image. + * @return The index of the red channel, or -1 if not found. + */ +inline int get_red_channel_index(const OIIO::ImageSpec &spec) { + return get_channel_index_by_names(spec, RED_CHANNEL_NAMES); +} + +/** + * @brief Get the index of the green channel in the image specification. + * @param spec The OpenImageIO ImageSpec describing the image. + * @return The index of the green channel, or -1 if not found. + */ +inline int get_green_channel_index(const OIIO::ImageSpec &spec) { + return get_channel_index_by_names(spec, GREEN_CHANNEL_NAMES); +} + +/** + * @brief Get the index of the blue channel in the image specification. + * @param spec The OpenImageIO ImageSpec describing the image. + * @return The index of the blue channel, or -1 if not found. + */ +inline int get_blue_channel_index(const OIIO::ImageSpec &spec) { + return get_channel_index_by_names(spec, BLUE_CHANNEL_NAMES); +} + +/** + * @brief Get the index of the alpha channel in the image specification. + * @param spec The OpenImageIO ImageSpec describing the image. + * @return The index of the alpha channel, or spec.alpha_channel if not found. + */ +inline int get_alpha_channel_index(const OIIO::ImageSpec &spec) { + return get_channel_index_by_names(spec, ALPHA_CHANNEL_NAMES, spec.alpha_channel); +} + +/** + * @brief Get the index of the luminance (grayscale) channel in the image specification. + * @param spec The OpenImageIO ImageSpec describing the image. + * @return The index of the luminance channel, or -1 if not found. + */ +inline int get_luminance_channel_index(const OIIO::ImageSpec &spec) { + return get_channel_index_by_names(spec, LUMINANCE_CHANNEL_NAMES); +} + +/** + * @brief Get the indices of the channels in the image specification. + * + * This function returns a map of channel names to their indices in the image specification. + * The channel names are represented by a single character: 'R' for red, 'G' for green, 'B' for + * blue, 'A' for alpha, 'Y' for luminance. The indices are the positions of the channels in the + * image specification. If a channel is not found, the index is set to -1. + * + * @param spec The OpenImageIO ImageSpec describing the image. + * @param image_type The type of the image. + * @return A map of channel names to their indices. + */ +std::map +get_channel_indices(const OIIO::ImageSpec &spec, const ImageType image_type) { + if (image_type == IMAGE_RGBA) { + int red_index = get_red_channel_index(spec); + int green_index = get_green_channel_index(spec); + int blue_index = get_blue_channel_index(spec); + int alpha_index = get_alpha_channel_index(spec); + + return { + {'R', red_index != -1 ? red_index : 0}, + {'G', green_index != -1 ? green_index : 1}, + {'B', blue_index != -1 ? blue_index : 2}, + {'A', alpha_index != -1 ? alpha_index : 3}}; + } else if (image_type == IMAGE_RGB) { + int red_index = get_red_channel_index(spec); + int green_index = get_green_channel_index(spec); + int blue_index = get_blue_channel_index(spec); + + return { + {'R', red_index != -1 ? red_index : 0}, + {'G', green_index != -1 ? green_index : 1}, + {'B', blue_index != -1 ? blue_index : 2}}; + } else if (image_type == IMAGE_GRAYSCALE) { + int luminance_index = get_luminance_channel_index(spec); + + return {{'Y', luminance_index != -1 ? luminance_index : 0}}; + } else if (image_type == IMAGE_GRAYSCALE_ALPHA) { + int luminance_index = get_luminance_channel_index(spec); + int alpha_index = get_alpha_channel_index(spec); + + return { + {'Y', luminance_index != -1 ? luminance_index : 0}, + {'A', alpha_index != -1 ? alpha_index : 1}}; + } else { + throw media_unreadable_error("Unsupported image type: " + std::to_string(image_type)); + } +} + +/** + * @brief Get the number of channels in the image. + * + * Returns the number of channels in the image based on the image type. + * The number of channels is 1 for grayscale images, 2 for grayscale+alpha images, 3 for RGB + * images, and 4 for RGBA images. If the image type is not supported, throws an error. + * + * @param image_type The type of the image. + * @return The number of channels in the image. + */ +size_t get_rendered_image_channel_count(const ImageType image_type) { + if (image_type == IMAGE_RGBA) { + return 4; + } else if (image_type == IMAGE_RGB) { + return 3; + } else if (image_type == IMAGE_GRAYSCALE_ALPHA) { + return 2; + } else if (image_type == IMAGE_GRAYSCALE) { + return 1; + } else { + throw media_unreadable_error("Unsupported image type: " + std::to_string(image_type)); + } +} + +/** + * @brief Get the number of bytes per channel in the image. + * + * Returns the number of bytes per channel in the image based on the format. + * The number of bytes per channel is 1 for UINT8, 2 for UINT16, 4 for FLOAT, and 2 for HALF. + * If the format is not supported, throws an error. + * + * @param format The format of the image. + * @return The number of bytes per channel in the image. + */ +int get_rendered_image_bytes_per_channel(const OIIO::TypeDesc format) { + if (format == OIIO::TypeDesc::UINT8) { + return 1; + } else if (format == OIIO::TypeDesc::UINT16) { + return 2; + } else if (format == OIIO::TypeDesc::HALF) { + return 2; + } else if (format == OIIO::TypeDesc::FLOAT) { + return 4; + } else { + throw media_unreadable_error("Unsupported format: " + std::string(format.c_str())); + } +} + +/** + * @brief Get whether the image is half float. + * + * Returns whether the image is half float based on the format. + * The image is half float if the format is HALF. + * If the format is not supported, returns 0. + * + * @param format The format of the image. + * @return Whether the image is half float. + */ +int get_is_half_float(const OIIO::TypeDesc format) { + if (format == OIIO::TypeDesc::HALF) { + return 1; + } else { + return 0; + } +} + +/** + * @brief Get the rendered format of the image. + * + * Returns the rendered format of the image based on the format. + * The rendered format is the format of the image that will be used to render the image. + * The rendered format is UINT8 for UINT8, UINT16 for UINT16, HALF for HALF, and FLOAT for + * FLOAT. If the format is not supported, returns UINT8. + * + * @param format The format of the image. + * @return The rendered format of the image. + */ +OIIO::TypeDesc get_rendered_image_format(const OIIO::TypeDesc format) { + if (format == OIIO::TypeDesc::UINT8) { + return OIIO::TypeDesc::UINT8; + } else if (format == OIIO::TypeDesc::UINT16) { + return OIIO::TypeDesc::UINT16; + } else if (format == OIIO::TypeDesc::HALF) { + return OIIO::TypeDesc::HALF; + } else if (format == OIIO::TypeDesc::FLOAT) { + return OIIO::TypeDesc::FLOAT; + } else { + return OIIO::TypeDesc::UINT8; + } +} + +/** + * @brief Generic function to get the start offset of a channel in a planar image buffer. + * + * For RGB/RGBA images, the layout is: [R plane][G plane][B plane][A plane] + * For grayscale images, R/G/B all point to plane 0 (Y), and A points to plane 1. + * + * @param channel The channel to get the offset for. + * @param width Image width in pixels. + * @param height Image height in pixels. + * @param bytes_per_channel Bytes per channel (1, 2, or 4). + * @param image_type The type of the image. + * @return The byte offset from the start of the buffer for this channel. + */ +size_t get_rendered_image_channel_start( + Channel channel, + const size_t width, + const size_t height, + const int bytes_per_channel, + const ImageType image_type) { + + const size_t plane_size = width * height * bytes_per_channel; + + // For grayscale images, R/G/B all map to Y (plane 0) + if (image_type == IMAGE_GRAYSCALE || image_type == IMAGE_GRAYSCALE_ALPHA) { + if (channel == Channel::ALPHA) { + return plane_size; // Alpha is in plane 1 for grayscale+alpha + } + return 0; // R, G, B all point to Y channel (plane 0) + } + + // For RGB/RGBA images + if (image_type == IMAGE_RGBA || image_type == IMAGE_RGB) { + switch (channel) { + case Channel::RED: + return 0; + case Channel::GREEN: + return plane_size; + case Channel::BLUE: + return plane_size * 2; + case Channel::ALPHA: + return plane_size * 3; + } + } + + throw media_unreadable_error("Unsupported image type: " + std::to_string(image_type)); +} + +/** + * @brief Returns the starting byte offset in the planar image buffer for the Red channel. + * + * This is a convenience wrapper around get_rendered_image_channel_start for improved + * readability and backward compatibility. + * + * @param width Width of the image in pixels. + * @param height Height of the image in pixels. + * @param bytes_per_channel Number of bytes used to represent each channel (e.g., 1 for uint8, + * 2 for uint16, 4 for float). + * @param image_type The detected type of the image (e.g., IMAGE_RGB, IMAGE_RGBA, + * IMAGE_GRAYSCALE, etc.). + * @return Byte offset for the start of the Red channel plane in the packed + * buffer. + */ +inline size_t get_rendered_image_channel_red_start( + const size_t width, + const size_t height, + const int bytes_per_channel, + const ImageType image_type) { + return get_rendered_image_channel_start( + Channel::RED, width, height, bytes_per_channel, image_type); +} + +/** + * @brief Returns the starting byte offset in the planar image buffer for the Green channel. + * + * This is a convenience wrapper around get_rendered_image_channel_start for improved + * readability and backward compatibility. + * + * @param width Width of the image in pixels. + * @param height Height of the image in pixels. + * @param bytes_per_channel Number of bytes used to represent each channel (e.g., 1 for uint8, + * 2 for uint16, 4 for float). + * @param image_type The detected type of the image (e.g., IMAGE_RGB, IMAGE_RGBA, + * IMAGE_GRAYSCALE, etc.). + * @return Byte offset for the start of the Green channel plane in the packed + * buffer. + */ +inline size_t get_rendered_image_channel_green_start( + const size_t width, + const size_t height, + const int bytes_per_channel, + const ImageType image_type) { + return get_rendered_image_channel_start( + Channel::GREEN, width, height, bytes_per_channel, image_type); +} + +/** + * @brief Returns the starting byte offset in the planar image buffer for the Blue channel. + * + * This is a convenience wrapper around get_rendered_image_channel_start for improved + * readability and backward compatibility. + * + * @param width Width of the image in pixels. + * @param height Height of the image in pixels. + * @param bytes_per_channel Number of bytes used to represent each channel (e.g., 1 for uint8, + * 2 for uint16, 4 for float). + * @param image_type The detected type of the image (e.g., IMAGE_RGB, IMAGE_RGBA, + * IMAGE_GRAYSCALE, etc.). + * @return Byte offset for the start of the Blue channel plane in the packed + * buffer. + */ +inline size_t get_rendered_image_channel_blue_start( + const size_t width, + const size_t height, + const int bytes_per_channel, + const ImageType image_type) { + return get_rendered_image_channel_start( + Channel::BLUE, width, height, bytes_per_channel, image_type); +} + +/** + * @brief Returns the starting byte offset in the planar image buffer for the Alpha channel. + * + * This is a convenience wrapper around get_rendered_image_channel_start for improved + * readability and backward compatibility. + * + * @param width Width of the image in pixels. + * @param height Height of the image in pixels. + * @param bytes_per_channel Number of bytes used to represent each channel (e.g., 1 for uint8, + * 2 for uint16, 4 for float). + * @param image_type The detected type of the image (e.g., IMAGE_RGB, IMAGE_RGBA, + * IMAGE_GRAYSCALE, etc.). + * @return Byte offset for the start of the Alpha channel plane in the packed + * buffer. + */ +inline size_t get_rendered_image_channel_alpha_start( + const size_t width, + const size_t height, + const int bytes_per_channel, + const ImageType image_type) { + return get_rendered_image_channel_start( + Channel::ALPHA, width, height, bytes_per_channel, image_type); +} + +/** + * @brief Reads image channels from the given OIIO::ImageInput and fills the output pixel + * buffer. + * + * This function copies image channel data in planar format into the target buffer. It supports + * images of type RGBA, RGB, GRAYSCALE, and GRAYSCALE+ALPHA. The function uses the provided + * channel indices map to determine the correct input channel numbers for each color component. + * Data is read in the provided rendered format and written into the output buffer, offset for + * each channel as appropriate. + * + * @param image Pointer to an open OIIO::ImageInput used to read channel data. + * @param image_type The detected type of the image (RGBA, RGB, GRAYSCALE, etc). + * @param channel_indices Map of channel letters ('R', 'G', 'B', 'A', 'Y') to their + * corresponding channel indices. + * @param rendered_format The desired OIIO::TypeDesc of the output data (usually matches + * buffer format). + * @param buffer Output pointer to the buffer to be filled with planar channel data. + * Layout is assumed to be [R][G][B][A], channel-major. + * Each channel plane is of size (spec.width * spec.height * + * bytes_per_channel). For grayscale, the 'Y' channel is filled in the R component's plane. For + * RGB and RGBA: interleaved in planar major order: R plane, then G, B, (then A). + */ +void fill_rendered_image( + OIIO::ImageInput *image, + const ImageType image_type, + const std::map &channel_indices, + const OIIO::TypeDesc &rendered_format, + byte *buffer, + size_t width, + size_t height, + int bytes_per_channel) { + size_t plane_size = width * height * bytes_per_channel; + + // RGB or RGBA (3 or 4-channel) images: fill R, G, B + if (image_type == IMAGE_RGBA || image_type == IMAGE_RGB) { + // Read R plane + image->read_image( + 0, + 0, + channel_indices.at('R'), + channel_indices.at('R') + 1, + rendered_format, + &buffer[plane_size * 0]); + // Read G plane + image->read_image( + 0, + 0, + channel_indices.at('G'), + channel_indices.at('G') + 1, + rendered_format, + &buffer[plane_size * 1]); + // Read B plane + image->read_image( + 0, + 0, + channel_indices.at('B'), + channel_indices.at('B') + 1, + rendered_format, + &buffer[plane_size * 2]); + } + // Grayscale or Grayscale+Alpha images: fill Y + else if (image_type == IMAGE_GRAYSCALE || image_type == IMAGE_GRAYSCALE_ALPHA) { + image->read_image( + 0, + 0, + channel_indices.at('Y'), + channel_indices.at('Y') + 1, + rendered_format, + &buffer[plane_size * 0]); + } + + // If Grayscale+Alpha, images: fill A + if (image_type == IMAGE_GRAYSCALE_ALPHA) { + image->read_image( + 0, + 0, + channel_indices.at('A'), + channel_indices.at('A') + 1, + rendered_format, + &buffer[plane_size * 1]); + } + // If RGBA, images: fill A + else if (image_type == IMAGE_RGBA) { + image->read_image( + 0, + 0, + channel_indices.at('A'), + channel_indices.at('A') + 1, + rendered_format, + &buffer[plane_size * 3]); + } +} + +ImageBufPtr OIIOMediaReader::image(const media::AVFrameID &mptr) { + ImageBufPtr buf; + + try { + // Step 1: Convert the URI to a POSIX path + std::string path = uri_to_posix_path(mptr.uri()); + + // Step 2: Open the image using OpenImageIO + auto image = OIIO::ImageInput::open(path); + if (!image) { + throw media_corrupt_error("OIIO error: " + OIIO::geterror()); + } + + // Step 3: Retrieve the image specification + const OIIO::ImageSpec &spec = image->spec(); + + // Step 4: Extract image dimensions + size_t width = spec.width; + size_t height = spec.height; + + // Step 5: Detect the image type (RGB, RGBA, Grayscale, Grayscale+Alpha) + ImageType image_type = detect_image_type(spec); + + // Step 6: Get channel indices map (e.g., 'R', 'G', 'B', 'A', or 'Y') + std::map channel_indices = get_channel_indices(spec, image_type); + + // Step 7: Determine channel and pixel information for buffer allocation + size_t num_channels = get_rendered_image_channel_count(image_type); + OIIO::TypeDesc rendered_format = get_rendered_image_format(spec.format); + int bytes_per_channel = get_rendered_image_bytes_per_channel(rendered_format); + int is_half_float = get_is_half_float(spec.format); + + // Step 8: Compute the channel offsets for the shader + size_t channel_r_start = + get_rendered_image_channel_red_start(width, height, bytes_per_channel, image_type); + size_t channel_g_start = get_rendered_image_channel_green_start( + width, height, bytes_per_channel, image_type); + size_t channel_b_start = + get_rendered_image_channel_blue_start(width, height, bytes_per_channel, image_type); + size_t channel_a_start = get_rendered_image_channel_alpha_start( + width, height, bytes_per_channel, image_type); + + // Step 9: Calculate total number of pixels and bytes per pixel + size_t pixel_count = spec.width * spec.height; + int bytes_per_pixel = bytes_per_channel * num_channels; + + // Step 10: Prepare JSON input parameters for the shader program + JsonStore jsn; + jsn["width"] = width; + jsn["height"] = height; + jsn["bytes_per_channel"] = bytes_per_channel; + jsn["is_half_float"] = is_half_float; + jsn["has_alpha"] = image_type == IMAGE_RGBA || image_type == IMAGE_GRAYSCALE_ALPHA; + jsn["channel_r_start"] = channel_r_start; + jsn["channel_g_start"] = channel_g_start; + jsn["channel_b_start"] = channel_b_start; + jsn["channel_a_start"] = channel_a_start; + + // Step 11: Allocate and configure the image buffer + buf.reset(new ImageBuffer(myshader_uuid, jsn)); + buf->allocate(pixel_count * bytes_per_pixel); + buf->set_shader(oiio_shader); + buf->set_image_dimensions(Imath::V2i(width, height)); + + // Step 12: Read the image and fill the buffer in planar format + fill_rendered_image( + image.get(), + image_type, + channel_indices, + rendered_format, + buf->buffer(), + width, + height, + bytes_per_channel); + + // Step 13: Close the image file + image->close(); + + } catch (const std::exception &e) { + throw media_unreadable_error( + "Unable to read with OIIO: " + std::string(e.what()) + " (" + + to_string(mptr.uri()) + ")"); + } + + return buf; +} + +thumbnail::ThumbnailBufferPtr +OIIOMediaReader::thumbnail(const media::AVFrameID &mpr, const size_t thumb_size) { + + try { + + // Step 1: Convert uri to POSIX file path. + std::string path = uri_to_posix_path(mpr.uri()); + + // Step 2: Open the image buffer. + OIIO::ImageBuf imagebuf(path); + if (imagebuf.has_error()) { + throw media_corrupt_error("OIIO error: " + imagebuf.geterror()); + } + + // Step 3: Query image spec and dimensions. + const OIIO::ImageSpec &spec = imagebuf.spec(); + int width = spec.width; + int height = spec.height; + + // Step 4: Compute target dimensions for thumbnail, preserving aspect ratio. + float aspect = static_cast(width) / static_cast(height); + int thumb_width = thumb_size; + int thumb_height = thumb_size; + + if (aspect > 1.0f) { + // Wider than tall, constrain height. + thumb_height = static_cast(thumb_size / aspect); + } else { + // Taller than wide, constrain width. + thumb_width = static_cast(thumb_size * aspect); + } + + // Step 5: Resize the image to thumbnail dimensions. + OIIO::ImageBuf resized = OIIO::ImageBufAlgo::resize( + imagebuf, "", 0, OIIO::ROI(0, thumb_width, 0, thumb_height)); + if (resized.has_error()) { + throw std::runtime_error("OIIO resize error: " + resized.geterror()); + } + + // Step 6: Detect image type and get channel indices. + ImageType image_type = detect_image_type(spec); + std::map channel_indices = get_channel_indices(spec, image_type); + + // Step 7: Convert the image to a 3-channel RGB image buffer. + OIIO::ImageBuf rgb_buf; + + if (image_type == IMAGE_RGBA || image_type == IMAGE_RGB) { + int red_channel_index = + get_red_channel_index(spec) != -1 ? get_red_channel_index(spec) : 0; + int green_channel_index = + get_green_channel_index(spec) != -1 ? get_green_channel_index(spec) : 1; + int blue_channel_index = + get_blue_channel_index(spec) != -1 ? get_blue_channel_index(spec) : 2; + + rgb_buf = OIIO::ImageBufAlgo::channels( + resized, 3, {red_channel_index, green_channel_index, blue_channel_index}); + } else if (image_type == IMAGE_GRAYSCALE || image_type == IMAGE_GRAYSCALE_ALPHA) { + int luminance_channel_index = + get_luminance_channel_index(spec) != -1 ? get_luminance_channel_index(spec) : 0; + + rgb_buf = OIIO::ImageBufAlgo::channels( + resized, + 3, + {luminance_channel_index, luminance_channel_index, luminance_channel_index}); + } else { + throw media_unreadable_error( + "Unsupported image type: " + std::to_string(image_type)); + } + + // Step 8: Allocate the thumbnail buffer. + auto thumb = std::make_shared( + thumb_width, thumb_height, thumbnail::TF_RGB24); + + // Step 9: Copy RGB data from buffer to the thumbnail. + OIIO::ROI roi(0, thumb_width, 0, thumb_height, 0, 1, 0, 3); + rgb_buf.get_pixels(roi, OIIO::TypeDesc::UINT8, &(thumb->data()[0])); + if (rgb_buf.has_error()) { + throw std::runtime_error("OIIO pixel copy error: " + rgb_buf.geterror()); + } + + // Step 10: Return the requested thumbnail. + return thumb; + + } catch (const std::exception &e) { + // Return a black thumbnail on error + auto thumb = std::make_shared( + thumb_size, thumb_size, thumbnail::TF_RGB24); + std::memset(&(thumb->data()[0]), 0, thumb->size()); + return thumb; + } +} + +media::MediaDetail OIIOMediaReader::detail(const caf::uri &uri) const { + media::MediaDetail detail; + detail.reader_ = name(); + + try { + // Step 1: Convert the URI to a POSIX path string + std::string path = uri_to_posix_path(uri); + + // Step 2: Open the image input using OpenImageIO + auto in = OIIO::ImageInput::open(path); + if (!in) { + throw std::runtime_error("Cannot open: " + OIIO::geterror()); + } + + // Step 3: Retrieve the image specification from the opened image + const OIIO::ImageSpec &spec = in->spec(); + + // Step 4: Populate a StreamDetail structure with metadata + media::StreamDetail stream; + stream.name_ = "image"; + stream.media_type_ = media::MediaType::MT_IMAGE; + stream.resolution_ = Imath::V2i(spec.width, spec.height); + stream.pixel_aspect_ = spec.get_float_attribute("PixelAspectRatio", 1.0f); + + // Step 5: Add the stream details to the MediaDetail object + detail.streams_.push_back(stream); + + // Step 6: Close the image input + in->close(); + + } catch (const std::exception &e) { + spdlog::warn("OIIO detail error: {}", e.what()); + } + + return detail; +} + +MRCertainty +OIIOMediaReader::supported(const caf::uri &uri, const std::array &signature) { + + // Step 1: List of supported extensions by OIIO + static const std::set supported_extensions = { + "JPG", + "JPEG", + "PNG", + "TIF", + "TIFF", + "TGA", + "BMP", + "PSD", + "HDR", + "DPX", + "ACES", + "JP2", + "J2K", + "WEBP", + "EXR", + }; + + // Step 2: Convert the URI to a POSIX path string and fs::path + std::string path = uri_to_posix_path(uri); + fs::path p(path); + + // Step 3: Check if the file exists and is a regular file + // Return not supported if the file does not exist or is not a regular file + if (!fs::exists(p) || !fs::is_regular_file(p)) { + return MRC_NO; + } + + // Step 4: Get the upper-case extension (handling platform differences) +#ifdef _WIN32 + std::string ext = ltrim_char(to_upper_path(p.extension()), '.'); +#else + std::string ext = ltrim_char(to_upper(p.extension().string()), '.'); +#endif + + // Step 5: Check if the extension is in the supported list + // Return fully supported if the extension is in the supported list + if (supported_extensions.count(ext)) { + return MRC_FULLY; + } + + // Step 6: Try to detect via OIIO if the extension is supported + // Return maybe supported if the extension is supported by OIIO + auto in = OIIO::ImageInput::open(path); + if (in) { + in->close(); + return MRC_MAYBE; + } + + // Step 7: Return not supported if all checks fail + return MRC_NO; +} + +// Plugin entry point +extern "C" { +plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { + return new plugin_manager::PluginFactoryCollection( + std::vector>( + {std::make_shared>>( + s_plugin_uuid, + "OpenImageIO", + "xStudio", + "OpenImageIO Media Reader", + semver::version("1.0.0"))})); +} +} diff --git a/src/plugin/media_reader/openimageio/src/openimageio.hpp b/src/plugin/media_reader/openimageio/src/openimageio.hpp new file mode 100644 index 000000000..952c7029a --- /dev/null +++ b/src/plugin/media_reader/openimageio/src/openimageio.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include "xstudio/media_reader/media_reader.hpp" +#include "xstudio/thumbnail/thumbnail.hpp" + +namespace xstudio { +namespace media_reader { + class OIIOMediaReader : public MediaReader { + public: + OIIOMediaReader(const utility::JsonStore &prefs = utility::JsonStore()); + ~OIIOMediaReader() override = default; + + void update_preferences(const utility::JsonStore &prefs) override; + + ImageBufPtr image(const media::AVFrameID &mptr) override; + + MRCertainty + supported(const caf::uri &uri, const std::array &signature) override; + + thumbnail::ThumbnailBufferPtr + thumbnail(const media::AVFrameID &mpr, const size_t thumb_size) override; + + media::MediaDetail detail(const caf::uri &uri) const override; + + [[nodiscard]] utility::Uuid plugin_uuid() const override; + }; +} // namespace media_reader +} // namespace xstudio + diff --git a/src/plugin/media_reader/openimageio/test/CMakeLists.txt b/src/plugin/media_reader/openimageio/test/CMakeLists.txt new file mode 100644 index 000000000..ea9eac542 --- /dev/null +++ b/src/plugin/media_reader/openimageio/test/CMakeLists.txt @@ -0,0 +1,7 @@ +SET(LINK_DEPS + xstudio::media_reader + OpenImageIO::OpenImageIO +) + +create_tests("${LINK_DEPS}") + diff --git a/src/plugin/media_reader/openimageio/test/openimageio_test.cpp b/src/plugin/media_reader/openimageio/test/openimageio_test.cpp new file mode 100644 index 000000000..e867684bc --- /dev/null +++ b/src/plugin/media_reader/openimageio/test/openimageio_test.cpp @@ -0,0 +1,15 @@ +#include +#include +#include + +#include + +#include "xstudio/utility/helpers.hpp" + +using namespace xstudio::utility; + +GTEST_TEST(OIIOMediaReaderTest, Test) { + // Simple test to verify that the plugin compiles + EXPECT_TRUE(true); +} + diff --git a/src/plugin/viewport_overlay/annotations/src/CMakeLists.txt b/src/plugin/viewport_overlay/annotations/src/CMakeLists.txt index 3273703b4..2e0b21db7 100644 --- a/src/plugin/viewport_overlay/annotations/src/CMakeLists.txt +++ b/src/plugin/viewport_overlay/annotations/src/CMakeLists.txt @@ -3,7 +3,7 @@ project(annotations_tool VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) find_package(Imath) find_package(Qt6 COMPONENTS Core Quick Gui REQUIRED) -set(ANNO_SYNC_EXTENSIONS On) +set(ANNO_SYNC_EXTENSIONS Off) set(SOURCES annotations_tool.cpp @@ -42,4 +42,4 @@ target_link_libraries(${PROJECT_NAME} set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) -add_plugin_qml(${PROJECT_NAME} qml) \ No newline at end of file +add_plugin_qml(${PROJECT_NAME} qml) diff --git a/ui/qml/xstudio/views/timeline/XsTimeline.qml b/ui/qml/xstudio/views/timeline/XsTimeline.qml index 39b5c946b..44d041ba3 100644 --- a/ui/qml/xstudio/views/timeline/XsTimeline.qml +++ b/ui/qml/xstudio/views/timeline/XsTimeline.qml @@ -1508,10 +1508,16 @@ Rectangle { onWheel: { // maintain position as we zoom.. if(wheel.modifiers == Qt.ShiftModifier) { - if(wheel.angleDelta.y > 1) { - scaleY += 0.2 + // wheel.angleDelta.y always return 0 on MacOS laptops + // when SHIFT is pressed and a mouse wheel is used, but in + // that case the x component is updating and usable. + let deltaY = wheel.angleDelta.y == 0 ? wheel.angleDelta.x : wheel.angleDelta.y + // Limit the scale to keep it within a usable range and + // avoid a negative scaleY value. + if(deltaY > 1) { + scaleY = Math.min(2.0, scaleY + 0.2) } else { - scaleY -= 0.2 + scaleY = Math.max(0.6, scaleY - 0.2) } wheel.accepted = true } else if(wheel.modifiers == Qt.ControlModifier) { diff --git a/vcpkg.json b/vcpkg.json index dcb3e197a..3a3d2ca8d 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -15,6 +15,7 @@ "lcms", "caf", "opencolorio", + "openimageio", "openexr", "imath", { @@ -56,6 +57,10 @@ "name": "opencolorio", "version": "2.2.1#1" }, + { + "name": "openimageio", + "version": "2.5.19.1" + }, { "name": "caf", "version": "1.0.2#0"