From e7eac2deda2a84bae058f84437381a98a49af3bb Mon Sep 17 00:00:00 2001 From: Spencer Magnusson Date: Thu, 17 Jul 2025 07:03:32 -0700 Subject: [PATCH 1/2] Item-based Color Signed-off-by: Spencer Magnusson --- .../otio-serialized-schema-only-fields.md | 6 + docs/tutorials/otio-serialized-schema.md | 6 + src/opentimelineio/CMakeLists.txt | 4 +- src/opentimelineio/clip.cpp | 5 +- src/opentimelineio/clip.h | 3 +- src/opentimelineio/color.cpp | 164 ++++++++++++++++++ src/opentimelineio/color.h | 90 ++++++++++ src/opentimelineio/composition.cpp | 5 +- src/opentimelineio/composition.h | 3 +- src/opentimelineio/deserialization.cpp | 27 +++ src/opentimelineio/item.cpp | 6 +- src/opentimelineio/item.h | 16 +- src/opentimelineio/safely_typed_any.cpp | 12 ++ src/opentimelineio/safely_typed_any.h | 3 + src/opentimelineio/serializableObject.h | 5 + src/opentimelineio/serialization.cpp | 59 +++++++ src/opentimelineio/track.cpp | 5 +- src/opentimelineio/track.h | 3 +- .../opentimelineio-bindings/otio_bindings.cpp | 1 + .../otio_serializableObjects.cpp | 61 ++++++- .../opentimelineio-bindings/otio_utils.cpp | 1 + .../opentimelineio/core/__init__.py | 3 + .../opentimelineio/core/color.py | 35 ++++ .../opentimelineio/core/composition.py | 2 + .../opentimelineio/schema/clip.py | 2 + tests/baselines/empty_clip.json | 1 + tests/baselines/empty_color.json | 8 + tests/baselines/empty_gap.json | 1 + tests/baselines/empty_stack.json | 1 + tests/baselines/empty_track.json | 1 + tests/test_clip.py | 2 + tests/test_color.py | 109 ++++++++++++ tests/test_composition.py | 2 + tests/test_serialization.cpp | 3 + 34 files changed, 635 insertions(+), 20 deletions(-) create mode 100644 src/opentimelineio/color.cpp create mode 100644 src/opentimelineio/color.h create mode 100644 src/py-opentimelineio/opentimelineio/core/color.py create mode 100644 tests/baselines/empty_color.json create mode 100644 tests/test_color.py diff --git a/docs/tutorials/otio-serialized-schema-only-fields.md b/docs/tutorials/otio-serialized-schema-only-fields.md index b9496d9d99..814b5ae1de 100644 --- a/docs/tutorials/otio-serialized-schema-only-fields.md +++ b/docs/tutorials/otio-serialized-schema-only-fields.md @@ -40,6 +40,7 @@ parameters: ### Composition.1 parameters: +- *color* - *effects* - *enabled* - *markers* @@ -50,6 +51,7 @@ parameters: ### Item.1 parameters: +- *color* - *effects* - *enabled* - *markers* @@ -132,6 +134,7 @@ parameters: parameters: - *active_media_reference_key* +- *color* - *effects* - *enabled* - *markers* @@ -169,6 +172,7 @@ parameters: ### Gap.1 parameters: +- *color* - *effects* - *enabled* - *markers* @@ -237,6 +241,7 @@ parameters: ### Stack.1 parameters: +- *color* - *effects* - *enabled* - *markers* @@ -263,6 +268,7 @@ parameters: ### Track.1 parameters: +- *color* - *effects* - *enabled* - *kind* diff --git a/docs/tutorials/otio-serialized-schema.md b/docs/tutorials/otio-serialized-schema.md index 90706b3633..8627c436c4 100644 --- a/docs/tutorials/otio-serialized-schema.md +++ b/docs/tutorials/otio-serialized-schema.md @@ -88,6 +88,7 @@ Should be subclassed (for example by :class:`.Track` and :class:`.Stack`), not u ``` parameters: +- *color*: - *effects*: - *enabled*: If true, an Item contributes to compositions. For example, when an audio/video clip is ``enabled=false`` the clip is muted/hidden. - *markers*: @@ -106,6 +107,7 @@ None ``` parameters: +- *color*: - *effects*: - *enabled*: If true, an Item contributes to compositions. For example, when an audio/video clip is ``enabled=false`` the clip is muted/hidden. - *markers*: @@ -290,6 +292,7 @@ Contains a :class:`.MediaReference` and a trim on that media reference. parameters: - *active_media_reference_key*: +- *color*: - *effects*: - *enabled*: If true, an Item contributes to compositions. For example, when an audio/video clip is ``enabled=false`` the clip is muted/hidden. - *markers*: @@ -363,6 +366,7 @@ None ``` parameters: +- *color*: - *effects*: - *enabled*: If true, an Item contributes to compositions. For example, when an audio/video clip is ``enabled=false`` the clip is muted/hidden. - *markers*: @@ -591,6 +595,7 @@ None ``` parameters: +- *color*: - *effects*: - *enabled*: If true, an Item contributes to compositions. For example, when an audio/video clip is ``enabled=false`` the clip is muted/hidden. - *markers*: @@ -641,6 +646,7 @@ None ``` parameters: +- *color*: - *effects*: - *enabled*: If true, an Item contributes to compositions. For example, when an audio/video clip is ``enabled=false`` the clip is muted/hidden. - *kind*: diff --git a/src/opentimelineio/CMakeLists.txt b/src/opentimelineio/CMakeLists.txt index cf5190c57a..f5258b0569 100644 --- a/src/opentimelineio/CMakeLists.txt +++ b/src/opentimelineio/CMakeLists.txt @@ -4,6 +4,7 @@ set(OPENTIMELINEIO_HEADER_FILES anyDictionary.h anyVector.h + color.h clip.h composable.h composition.h @@ -38,7 +39,8 @@ set(OPENTIMELINEIO_HEADER_FILES vectorIndexing.h version.h) -add_library(opentimelineio ${OTIO_SHARED_OR_STATIC_LIB} +add_library(opentimelineio ${OTIO_SHARED_OR_STATIC_LIB} + color.cpp clip.cpp composable.cpp composition.cpp diff --git a/src/opentimelineio/clip.cpp b/src/opentimelineio/clip.cpp index b4a6ec8ec6..12e67de563 100644 --- a/src/opentimelineio/clip.cpp +++ b/src/opentimelineio/clip.cpp @@ -15,8 +15,9 @@ Clip::Clip( AnyDictionary const& metadata, std::vector const& effects, std::vector const& markers, - std::string const& active_media_reference_key) - : Parent{ name, source_range, metadata, effects, markers } + std::string const& active_media_reference_key, + std::optional const& color) + : Parent{ name, source_range, metadata, effects, markers, /*enabled*/ true, color } , _active_media_reference_key(active_media_reference_key) { set_media_reference(media_reference); diff --git a/src/opentimelineio/clip.h b/src/opentimelineio/clip.h index f4b0e8d857..d148569c0b 100644 --- a/src/opentimelineio/clip.h +++ b/src/opentimelineio/clip.h @@ -46,7 +46,8 @@ class Clip : public Item AnyDictionary const& metadata = AnyDictionary(), std::vector const& effects = std::vector(), std::vector const& markers = std::vector(), - std::string const& active_media_reference_key = default_media_key); + std::string const& active_media_reference_key = default_media_key, + std::optional const& color = std::nullopt); /// @name Media References ///@{ diff --git a/src/opentimelineio/color.cpp b/src/opentimelineio/color.cpp new file mode 100644 index 0000000000..4f09d92fe7 --- /dev/null +++ b/src/opentimelineio/color.cpp @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#include +#include + +#include "opentimelineio/color.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { + +Color::Color( + double const r, + double const g, + double const b, + double const a, + std::string const& name) + : _name(name), + _r(r), + _g(g), + _b(b), + _a(a) {} + +Color::Color(Color const& other) : _name(other.name()), + _r(other.r()), + _g(other.g()), + _b(other.b()), + _a(other.a()) {} + +const Color Color::pink(1.0, 0.0, 1.0, 1.0, "Pink"); +const Color Color::red(1.0, 0.0, 0.0, 1.0, "Red"); +const Color Color::orange(1.0, 0.5, 0.0, 1.0, "Orange"); +const Color Color::yellow(1.0, 1.0, 0.0, 1.0, "Yellow"); +const Color Color::green(0.0, 1.0, 0.0, 1.0, "Green"); +const Color Color::cyan(0.0, 1.0, 1.0, 1.0, "Cyan"); +const Color Color::blue(0.0, 0.0, 1.0, 1.0, "Blue"); +const Color Color::purple(0.5, 0.0, 0.5, 1.0, "Purple"); +const Color Color::magenta(1.0, 0.0, 1.0, 1.0, "Magenta"); +const Color Color::black(0.0, 0.0, 0.0, 1.0, "Black"); +const Color Color::white(1.0, 1.0, 1.0, 1.0, "White"); + +Color* +Color::from_hex(std::string const& color) +{ + std::string temp = color; + if (temp[0] == '#') + { + temp = temp.substr(1); + } + else if (temp[0] == '0' && (temp[1] == 'x' || temp[1] == 'X')) + { + temp = temp.substr(2); + } + + double _r, _g, _b, _a; + // 0xFFFFFFFF (rgba, 255) + int BASE_16 = 16; + double BASE_16_DIV = 255.0; + double BASE_8_DIV = 15.0; + if (temp.length() == 8) + { + _r = std::stoi(temp.substr(0, 2), nullptr, BASE_16) / BASE_16_DIV; + _g = std::stoi(temp.substr(2, 2), nullptr, BASE_16) / BASE_16_DIV; + _b = std::stoi(temp.substr(4, 2), nullptr, BASE_16) / BASE_16_DIV; + _a = std::stoi(temp.substr(6, 2), nullptr, BASE_16) / BASE_16_DIV; + } + // 0xFFFFFF (rgb, 255) + else if (temp.length() == 6) + { + _r = std::stoi(temp.substr(0, 2), nullptr, BASE_16) / BASE_16_DIV; + _g = std::stoi(temp.substr(2, 2), nullptr, BASE_16) / BASE_16_DIV; + _b = std::stoi(temp.substr(4, 2), nullptr, BASE_16) / BASE_16_DIV; + _a = 1.0; + } + // 0xFFF (rgb, 16) + else if (temp.length() == 3) + { + _r = std::stoi(temp.substr(0, 1), nullptr, BASE_16) / BASE_8_DIV; + _g = std::stoi(temp.substr(1, 1), nullptr, BASE_16) / BASE_8_DIV; + _b = std::stoi(temp.substr(2, 1), nullptr, BASE_16) / BASE_8_DIV; + _a = 1.0; + } + // 0xFFFF (rgba, 16) + else if (temp.length() == 4) + { + _r = std::stoi(temp.substr(0, 1), nullptr, BASE_16) / BASE_8_DIV; + _g = std::stoi(temp.substr(1, 1), nullptr, BASE_16) / BASE_8_DIV; + _b = std::stoi(temp.substr(2, 1), nullptr, BASE_16) / BASE_8_DIV; + _a = std::stoi(temp.substr(3, 1), nullptr, BASE_16) / BASE_8_DIV; + } + else { + throw std::invalid_argument("Invalid hex format"); + } + return new Color(_r, _g, _b, _a); +} + +Color* +Color::from_int_list(std::vector const& color, int bit_depth) +{ + double depth = pow(2, bit_depth) - 1.0; // e.g. 8 = 255.0 + if (color.size() == 3) + return new Color(color[0] / depth, color[1] / depth, color[2] / depth, 1.0); + else if (color.size() == 4) + return new Color(color[0] / depth, color[1] / depth, color[2] / depth, color[3] / depth); + + throw std::invalid_argument("List must have exactly 3 or 4 elements"); +} + +Color* +Color::from_agbr_int(unsigned int agbr) noexcept +{ + auto conv_r = (agbr & 0xFF) / 255.0; + auto conv_g = ((agbr >> 16) & 0xFF) / 255.0; + auto conv_b = ((agbr >> 8) & 0xFF) / 255.0; + auto conv_a = ((agbr >> 24) & 0xFF) / 255.0; + return new Color(conv_r, conv_g, conv_b, conv_a); +} + +Color* +Color::from_float_list(std::vector const& color) +{ + if (color.size() == 3) + return new Color(color[0], color[1], color[2], 1.0); + else if (color.size() == 4) + return new Color(color[0], color[1], color[2], color[3]); + + throw std::invalid_argument("List must have exactly 3 or 4 elements"); +} + +std::string +Color::to_hex() +{ + auto rgba = to_rgba_int_list(8); + std::stringstream ss; + ss << "#"; + + ss << std::hex << std::setfill('0'); + ss << std::setw(2) << rgba[0]; + ss << std::setw(2) << rgba[1]; + ss << std::setw(2) << rgba[2]; + ss << std::setw(2) << rgba[3]; + return ss.str(); +} + +std::vector +Color::to_rgba_int_list(int base) +{ + double math_base = pow(2, base) - 1.0; + return {int(_r * math_base), int(_g * math_base), int(_b * math_base), int(_a * math_base)}; +} + +unsigned int +Color::to_agbr_integer() +{ + auto rgba = to_rgba_int_list(8); + return (rgba[3] << 24) + (rgba[2] << 16) + (rgba[1] << 8) + rgba[0]; +} + +std::vector +Color::to_rgba_float_list() +{ + return {_r, _g, _b, _a}; +} + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION \ No newline at end of file diff --git a/src/opentimelineio/color.h b/src/opentimelineio/color.h new file mode 100644 index 0000000000..147c56b9a8 --- /dev/null +++ b/src/opentimelineio/color.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#pragma once + +#include +#include + +#include "opentimelineio/version.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { + + +/// @brief Color consists of red, green, blue, +/// and alpha double floating point values, +/// allowing conversion between different formats. +/// To be considered interoperable, +/// the sRGB transfer function encoded values, +/// ranging between zero and one, are expected to be accurate +/// to within 1/255 of the intended value. +/// Round-trip conversions may not be guaranteed outside that. +/// This class is meant for use in user interface elements, +// like marker or clip coloring, NOT for image pixel content. +class Color +{ +public: + struct Schema + { + static auto constexpr name = "Color"; + static int constexpr version = 1; + }; + + Color( + double const r = 1.f, + double const g = 1.f, + double const b = 1.f, + double const a = 1.f, + std::string const& name = ""); + + Color(Color const& other); + + static const Color pink; + static const Color red; + static const Color orange; + static const Color yellow; + static const Color green; + static const Color cyan; + static const Color blue; + static const Color purple; + static const Color magenta; + static const Color black; + static const Color white; + + static Color* from_hex(std::string const& color); + static Color* from_int_list(std::vector const& color, int bit_depth); + static Color* from_agbr_int(unsigned int agbr) noexcept; + static Color* from_float_list(std::vector const& color); + + friend bool + operator==(Color lhs, Color rhs) noexcept + { + return lhs.to_hex() == rhs.to_hex() && lhs.to_agbr_integer() == rhs.to_agbr_integer(); + } + + std::string to_hex(); + std::vector to_rgba_int_list(int base); + unsigned int to_agbr_integer(); + std::vector to_rgba_float_list(); + + double r() const { return _r; } + double g() const { return _g; } + double b() const { return _b; } + double a() const { return _a; } + std::string name() const { return _name; } + + void set_r(double r) { _r = r; } + void set_g(double g) { _g = g; } + void set_b(double b) { _b = b; } + void set_a(double a) { _a = a; } + void set_name(std::string const& name) { _name = name; } + +private: + double _r; + double _g; + double _b; + double _a; + std::string _name; +}; + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION \ No newline at end of file diff --git a/src/opentimelineio/composition.cpp b/src/opentimelineio/composition.cpp index e6915ac2d9..8de379960d 100644 --- a/src/opentimelineio/composition.cpp +++ b/src/opentimelineio/composition.cpp @@ -15,8 +15,9 @@ Composition::Composition( std::optional const& source_range, AnyDictionary const& metadata, std::vector const& effects, - std::vector const& markers) - : Parent(name, source_range, metadata, effects, markers) + std::vector const& markers, + std::optional const& color) + : Parent(name, source_range, metadata, effects, markers, /*enabled*/ true, color) {} Composition::~Composition() diff --git a/src/opentimelineio/composition.h b/src/opentimelineio/composition.h index 6b6177dee8..9c8828a745 100644 --- a/src/opentimelineio/composition.h +++ b/src/opentimelineio/composition.h @@ -40,7 +40,8 @@ class Composition : public Item std::optional const& source_range = std::nullopt, AnyDictionary const& metadata = AnyDictionary(), std::vector const& effects = std::vector(), - std::vector const& markers = std::vector()); + std::vector const& markers = std::vector(), + std::optional const& color = std::nullopt); /// @brief Return the kind of composition. virtual std::string composition_kind() const; diff --git a/src/opentimelineio/deserialization.cpp b/src/opentimelineio/deserialization.cpp index 9090e23ade..0e99e8af6e 100644 --- a/src/opentimelineio/deserialization.cpp +++ b/src/opentimelineio/deserialization.cpp @@ -4,6 +4,7 @@ #include "opentime/rationalTime.h" #include "opentime/timeRange.h" #include "opentime/timeTransform.h" +#include "opentimelineio/color.h" #include "opentimelineio/serializableObject.h" #include "opentimelineio/serializableObjectWithMetadata.h" #include "stringUtils.h" @@ -587,6 +588,18 @@ SerializableObject::Reader::_decode(_Resolver& resolver) ? std::any(TimeRange(start_time, duration)) : std::any(); } + else if (schema_name_and_version == "Color.1") + { + float r, g, b, a; + std::string name; + return _fetch("r", &r) + && _fetch("g", &g) + && _fetch("b", &b) + && _fetch("a", &a) + && _fetch("name", &name) + ? std::any(Color(r, g, b, a, name)) + : std::any(); + } else if (schema_name_and_version == "TimeTransform.1") { RationalTime offset; @@ -726,6 +739,12 @@ SerializableObject::Reader::read(std::string const& key, TimeRange* value) return _fetch(key, value); } +bool +SerializableObject::Reader::read(std::string const& key, Color* value) +{ + return _fetch(key, value); +} + bool SerializableObject::Reader::read(std::string const& key, TimeTransform* value) { @@ -816,6 +835,14 @@ SerializableObject::Reader::read( return _read_optional(key, value); } +bool +SerializableObject::Reader::read( + std::string const& key, + std::optional* value) +{ + return _read_optional(key, value); +} + bool SerializableObject::Reader::read( std::string const& key, diff --git a/src/opentimelineio/item.cpp b/src/opentimelineio/item.cpp index 2ed16f38b2..ea22858f1e 100644 --- a/src/opentimelineio/item.cpp +++ b/src/opentimelineio/item.cpp @@ -16,12 +16,14 @@ Item::Item( AnyDictionary const& metadata, std::vector const& effects, std::vector const& markers, - bool enabled) + bool enabled, + std::optional const& color) : Parent(name, metadata) , _source_range(source_range) , _effects(effects.begin(), effects.end()) , _markers(markers.begin(), markers.end()) , _enabled(enabled) + , _color(color) {} Item::~Item() @@ -176,6 +178,7 @@ Item::read_from(Reader& reader) && reader.read_if_present("effects", &_effects) && reader.read_if_present("markers", &_markers) && reader.read_if_present("enabled", &_enabled) + && reader.read_if_present("color", &_color) && Parent::read_from(reader); } @@ -187,6 +190,7 @@ Item::write_to(Writer& writer) const writer.write("effects", _effects); writer.write("markers", _markers); writer.write("enabled", _enabled); + writer.write("color", _color); } }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/item.h b/src/opentimelineio/item.h index 80d341e110..6448bc6362 100644 --- a/src/opentimelineio/item.h +++ b/src/opentimelineio/item.h @@ -4,6 +4,7 @@ #pragma once #include "opentime/timeRange.h" +#include "opentimelineio/color.h" #include "opentimelineio/composable.h" #include "opentimelineio/errorStatus.h" #include "opentimelineio/version.h" @@ -42,7 +43,8 @@ class Item : public Composable AnyDictionary const& metadata = AnyDictionary(), std::vector const& effects = std::vector(), std::vector const& markers = std::vector(), - bool enabled = true); + bool enabled = true, + std::optional const& color = std::nullopt); bool visible() const override; bool overlapping() const override; @@ -117,6 +119,17 @@ class Item : public Composable Item const* to_item, ErrorStatus* error_status = nullptr) const; + std::optional color() const noexcept + { + return _color; + } + + /// @brief Set the color of the item. + void set_color(std::optional const& color) + { + _color = color; + } + protected: virtual ~Item(); @@ -127,6 +140,7 @@ class Item : public Composable std::optional _source_range; std::vector> _effects; std::vector> _markers; + std::optional _color; bool _enabled; }; diff --git a/src/opentimelineio/safely_typed_any.cpp b/src/opentimelineio/safely_typed_any.cpp index d549b5ac5d..3643d22303 100644 --- a/src/opentimelineio/safely_typed_any.cpp +++ b/src/opentimelineio/safely_typed_any.cpp @@ -59,6 +59,12 @@ create_safely_typed_any(TimeTransform&& value) return std::any(value); } +std::any +create_safely_typed_any(Color&& value) +{ + return std::any(value); +} + std::any create_safely_typed_any(IMATH_NAMESPACE::V2d&& value) { @@ -143,6 +149,12 @@ safely_cast_time_transform_any(std::any const& a) return std::any_cast(a); } +Color +safely_cast_color_any(std::any const& a) +{ + return std::any_cast(a); +} + IMATH_NAMESPACE::V2d safely_cast_point_any(std::any const& a) { diff --git a/src/opentimelineio/safely_typed_any.h b/src/opentimelineio/safely_typed_any.h index 2fef66da6c..22398a7e8b 100644 --- a/src/opentimelineio/safely_typed_any.h +++ b/src/opentimelineio/safely_typed_any.h @@ -22,6 +22,7 @@ #include "opentime/rationalTime.h" #include "opentime/timeRange.h" #include "opentime/timeTransform.h" +#include "opentimelineio/color.h" #include "opentimelineio/serializableObject.h" #include "opentimelineio/version.h" @@ -38,6 +39,7 @@ std::any create_safely_typed_any(double&&); std::any create_safely_typed_any(std::string&&); std::any create_safely_typed_any(RationalTime&&); std::any create_safely_typed_any(TimeRange&&); +std::any create_safely_typed_any(Color&&); std::any create_safely_typed_any(TimeTransform&&); std::any create_safely_typed_any(IMATH_NAMESPACE::V2d&&); std::any create_safely_typed_any(IMATH_NAMESPACE::Box2d&&); @@ -59,6 +61,7 @@ std::string safely_cast_string_any(std::any const& a); RationalTime safely_cast_rational_time_any(std::any const& a); TimeRange safely_cast_time_range_any(std::any const& a); TimeTransform safely_cast_time_transform_any(std::any const& a); +Color safely_cast_color_any(std::any const& a); IMATH_NAMESPACE::V2d safely_cast_point_any(std::any const& a); IMATH_NAMESPACE::Box2d safely_cast_box_any(std::any const& a); diff --git a/src/opentimelineio/serializableObject.h b/src/opentimelineio/serializableObject.h index 63b096a038..44f1be8d2f 100644 --- a/src/opentimelineio/serializableObject.h +++ b/src/opentimelineio/serializableObject.h @@ -8,6 +8,7 @@ #include "opentime/timeTransform.h" #include "opentimelineio/anyDictionary.h" #include "opentimelineio/anyVector.h" +#include "opentimelineio/color.h" #include "opentimelineio/errorStatus.h" #include "opentimelineio/typeRegistry.h" #include "opentimelineio/version.h" @@ -125,6 +126,7 @@ class SerializableObject bool read(std::string const& key, RationalTime* dest); bool read(std::string const& key, TimeRange* dest); bool read(std::string const& key, class TimeTransform* dest); + bool read(std::string const& key, Color* dest); bool read(std::string const& key, IMATH_NAMESPACE::V2d* value); bool read(std::string const& key, IMATH_NAMESPACE::Box2d* value); bool read(std::string const& key, AnyVector* dest); @@ -137,6 +139,7 @@ class SerializableObject bool read(std::string const& key, std::optional* dest); bool read(std::string const& key, std::optional* dest); bool read(std::string const& key, std::optional* dest); + bool read(std::string const& key, std::optional* dest); bool read( std::string const& key, std::optional* value); @@ -449,12 +452,14 @@ class SerializableObject void write(std::string const& key, TimeRange value); void write(std::string const& key, IMATH_NAMESPACE::V2d value); void write(std::string const& key, IMATH_NAMESPACE::Box2d value); + void write(std::string const& key, std::optional value); void write(std::string const& key, std::optional value); void write(std::string const& key, std::optional value); void write( std::string const& key, std::optional value); void write(std::string const& key, class TimeTransform value); + void write(std::string const& key, Color value); void write(std::string const& key, SerializableObject const* value); void write(std::string const& key, SerializableObject* value) { diff --git a/src/opentimelineio/serialization.cpp b/src/opentimelineio/serialization.cpp index 0b9b2de2ba..632b406ba2 100644 --- a/src/opentimelineio/serialization.cpp +++ b/src/opentimelineio/serialization.cpp @@ -4,6 +4,7 @@ #include "opentimelineio/serialization.h" #include "errorStatus.h" #include "opentimelineio/anyDictionary.h" +#include "opentimelineio/color.h" #include "opentimelineio/serializableObject.h" #include "opentimelineio/unknownSchema.h" #include "stringUtils.h" @@ -75,6 +76,7 @@ class Encoder virtual void write_value(class RationalTime const& value) = 0; virtual void write_value(class TimeRange const& value) = 0; virtual void write_value(class TimeTransform const& value) = 0; + virtual void write_value(class Color const& value) = 0; virtual void write_value(struct SerializableObject::ReferenceId) = 0; virtual void write_value(IMATH_NAMESPACE::Box2d const&) = 0; virtual void write_value(IMATH_NAMESPACE::V2d const&) = 0; @@ -253,6 +255,25 @@ class CloningEncoder : public Encoder _store(std::any(value)); } } + void write_value(Color const& value) override + { + if (_result_object_policy == ResultObjectPolicy::OnlyAnyDictionary) + { + AnyDictionary result{ + { "OTIO_SCHEMA", "Color.1" }, + { "r", value.r() }, + { "g", value.g() }, + { "b", value.b() }, + { "a", value.a() }, + { "name", value.name() }, + }; + _store(std::any(std::move(result))); + } + else + { + _store(std::any(value)); + } + } void write_value(SerializableObject::ReferenceId value) override { if (_result_object_policy == ResultObjectPolicy::OnlyAnyDictionary) @@ -582,6 +603,31 @@ class JSONEncoder : public Encoder _writer.EndObject(); } + void write_value(Color const& value) + { + _writer.StartObject(); + + _writer.Key("OTIO_SCHEMA"); + _writer.String("Color.1"); + + _writer.Key("r"); + _writer.Double(value.r()); + + _writer.Key("g"); + _writer.Double(value.g()); + + _writer.Key("b"); + _writer.Double(value.b()); + + _writer.Key("a"); + _writer.Double(value.a()); + + _writer.Key("name"); + _writer.String(value.name().c_str()); + + _writer.EndObject(); + } + void write_value(SerializableObject::ReferenceId value) { _writer.StartObject(); @@ -697,6 +743,9 @@ SerializableObject::Writer::_build_dispatch_tables() wt[&typeid(TimeTransform)] = [this](std::any const& value) { _encoder.write_value(std::any_cast(value)); }; + wt[&typeid(Color)] = [this](std::any const& value) { + _encoder.write_value(std::any_cast(value)); + }; wt[&typeid(IMATH_NAMESPACE::V2d)] = [this](std::any const& value) { _encoder.write_value(std::any_cast(value)); }; @@ -742,6 +791,7 @@ SerializableObject::Writer::_build_dispatch_tables() et[&typeid(RationalTime)] = &_simple_any_comparison; et[&typeid(TimeRange)] = &_simple_any_comparison; et[&typeid(TimeTransform)] = &_simple_any_comparison; + et[&typeid(Color)] = &_simple_any_comparison; et[&typeid(SerializableObject::ReferenceId)] = &_simple_any_comparison; et[&typeid(IMATH_NAMESPACE::V2d)] = @@ -931,6 +981,15 @@ SerializableObject::Writer::write(std::string const& key, TimeTransform value) _encoder.write_value(value); } +void +SerializableObject::Writer::write( + std::string const& key, + std::optional value) +{ + _encoder_write_key(key); + value ? _encoder.write_value(*value) : _encoder.write_null_value(); +} + void SerializableObject::Writer::write( std::string const& key, diff --git a/src/opentimelineio/track.cpp b/src/opentimelineio/track.cpp index c0dcba1a9d..44d372b229 100644 --- a/src/opentimelineio/track.cpp +++ b/src/opentimelineio/track.cpp @@ -13,8 +13,9 @@ Track::Track( std::string const& name, std::optional const& source_range, std::string const& kind, - AnyDictionary const& metadata) - : Parent(name, source_range, metadata) + AnyDictionary const& metadata, + std::optional const& color) + : Parent(name, source_range, metadata, std::vector(), std::vector(), color) , _kind(kind) {} diff --git a/src/opentimelineio/track.h b/src/opentimelineio/track.h index cb7a01d2cc..8a58b23b8d 100644 --- a/src/opentimelineio/track.h +++ b/src/opentimelineio/track.h @@ -47,7 +47,8 @@ class Track : public Composition std::string const& name = std::string(), std::optional const& source_range = std::nullopt, std::string const& kind = Kind::video, - AnyDictionary const& metadata = AnyDictionary()); + AnyDictionary const& metadata = AnyDictionary(), + std::optional const& color = std::nullopt); /// @brief Return this kind of track. std::string kind() const noexcept { return _kind; } diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_bindings.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_bindings.cpp index 3dfa67f3b3..c714888b4e 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_bindings.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_bindings.cpp @@ -272,6 +272,7 @@ PYBIND11_MODULE(_otio, m) { .def(py::init([](RationalTime rt) { return new PyAny(rt); })) .def(py::init([](TimeRange tr) { return new PyAny(tr); })) .def(py::init([](TimeTransform tt) { return new PyAny(tt); })) + .def(py::init([](Color c) { return new PyAny(c); })) .def(py::init([](IMATH_NAMESPACE::V2d v2d) { return new PyAny(v2d); })) .def(py::init([](IMATH_NAMESPACE::Box2d box2d) { return new PyAny(box2d); })) .def(py::init([](AnyVectorProxy* p) { return new PyAny(p->fetch_any_vector()); })) diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp index 873690baa0..1c6c2f0f50 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp @@ -7,6 +7,7 @@ #include "otio_errorStatusHandler.h" #include "opentimelineio/clip.h" +#include "opentimelineio/color.h" #include "opentimelineio/composable.h" #include "opentimelineio/composition.h" #include "opentimelineio/effect.h" @@ -204,6 +205,39 @@ static void define_bases2(py::module m) { MarkerVectorProxy::define_py_class(m, "MarkerVector"); EffectVectorProxy::define_py_class(m, "EffectVector"); + py::class_(m, "Color", py::dynamic_attr(), R"docstring(:class:`Color` is a definition of red, green, blue, and alpha double floating point values, allowing conversion between different formats. To be considered interoperable, the sRGB transfer function encoded values, ranging between zero and one, are expected to be accurate to within 1/255 of the intended value. Round-trip conversions may not be guaranteed outside that. This Color class is meant for use in user interface elements, like marker or clip coloring, NOT for image pixel content.)docstring") + .def(py::init([](double r, double g, double b, double a, std::string name) { + return new Color(r, g, b, a, name); + }), "r"_a = 1.0, "g"_a = 1.0, "b"_a = 1.0, "a"_a = 1.0, + py::arg_v("name"_a = std::string())) + .def_property("r", &Color::r, &Color::set_r) + .def_property("g", &Color::g, &Color::set_g) + .def_property("b", &Color::b, &Color::set_b) + .def_property("a", &Color::a, &Color::set_a) + .def_property("name", &Color::name, &Color::set_name) + + .def("to_hex", &Color::to_hex) + .def("to_rgba_int_list", &Color::to_rgba_int_list, py::arg("base") = 8) + .def("to_agbr_integer", &Color::to_agbr_integer) + .def("to_rgba_float_list", &Color::to_rgba_float_list) + + .def_static("from_hex", Color::from_hex) + .def_static("from_float_list", &Color::from_float_list) + .def_static("from_int_list", &Color::from_int_list) + .def_static("from_agbr_int", &Color::from_agbr_int) + + .def_property_readonly_static("PINK", [](py::object) { return new Color(Color::pink); }, py::return_value_policy::take_ownership) + .def_property_readonly_static("RED", [](py::object) { return new Color(Color::red); }, py::return_value_policy::take_ownership) + .def_property_readonly_static("ORANGE", [](py::object) { return new Color(Color::orange); }, py::return_value_policy::take_ownership) + .def_property_readonly_static("YELLOW", [](py::object) { return new Color(Color::yellow); }, py::return_value_policy::take_ownership) + .def_property_readonly_static("GREEN", [](py::object) { return new Color(Color::green); }, py::return_value_policy::take_ownership) + .def_property_readonly_static("CYAN", [](py::object) { return new Color(Color::cyan); }, py::return_value_policy::take_ownership) + .def_property_readonly_static("BLUE", [](py::object) { return new Color(Color::blue); }, py::return_value_policy::take_ownership) + .def_property_readonly_static("PURPLE", [](py::object) { return new Color(Color::purple); }, py::return_value_policy::take_ownership) + .def_property_readonly_static("MAGENTA", [](py::object) { return new Color(Color::magenta); }, py::return_value_policy::take_ownership) + .def_property_readonly_static("BLACK", [](py::object) { return new Color(Color::black); }, py::return_value_policy::take_ownership) + .def_property_readonly_static("WHITE", [](py::object) { return new Color(Color::white); }, py::return_value_policy::take_ownership); + auto marker_class = py::class_>(m, "Marker", py::dynamic_attr(), R"docstring( A marker indicates a marked range of time on an item in a timeline, usually with a name, color or other metadata. @@ -311,20 +345,23 @@ An object that can be composed within a :class:`~Composition` (such as :class:`~ py::class_>(m, "Item", py::dynamic_attr()) .def(py::init([](std::string name, std::optional source_range, - std::optional> effects, std::optional> markers, py::bool_ enabled, py::object metadata) { + std::optional> effects, std::optional> markers, py::bool_ enabled, std::optional color, py::object metadata) { return new Item(name, source_range, py_to_any_dictionary(metadata), vector_or_default(effects), vector_or_default(markers), - enabled); }), + enabled, + color); }), py::arg_v("name"_a = std::string()), "source_range"_a = std::nullopt, "effects"_a = py::none(), "markers"_a = py::none(), "enabled"_a = true, + "color"_a = std::nullopt, py::arg_v("metadata"_a = py::none())) .def_property("enabled", &Item::enabled, &Item::set_enabled, "If true, an Item contributes to compositions. For example, when an audio/video clip is ``enabled=false`` the clip is muted/hidden.") .def_property("source_range", &Item::source_range, &Item::set_source_range) + .def_property("color", &Item::color, &Item::set_color) .def("available_range", [](Item* item) { return item->available_range(ErrorStatusHandler()); }) @@ -427,11 +464,13 @@ Contains a :class:`.MediaReference` and a trim on that media reference. std::optional source_range, py::object metadata, std::optional> effects, std::optional> markers, - const std::string& active_media_reference) { + const std::string& active_media_reference, + std::optional color) { return new Clip(name, media_reference, source_range, py_to_any_dictionary(metadata), vector_or_default(effects), vector_or_default(markers), - active_media_reference); + active_media_reference, + color); }), py::arg_v("name"_a = std::string()), "media_reference"_a = nullptr, @@ -439,7 +478,8 @@ Contains a :class:`.MediaReference` and a trim on that media reference. py::arg_v("metadata"_a = py::none()), "effects"_a = py::none(), "markers"_a = py::none(), - "active_media_reference"_a = std::string(Clip::default_media_key)) + "active_media_reference"_a = std::string(Clip::default_media_key), + "color"_a = std::nullopt) .def_property_readonly_static("DEFAULT_MEDIA_KEY",[](py::object /* self */) { return Clip::default_media_key; }) @@ -447,6 +487,7 @@ Contains a :class:`.MediaReference` and a trim on that media reference. .def_property("active_media_reference_key", &Clip::active_media_reference_key, [](Clip* clip, std::string const& new_active_key) { clip->set_active_media_reference_key(new_active_key, ErrorStatusHandler()); }) + .def_property("color", &Clip::color, &Clip::set_color) .def("media_references", &Clip::media_references) .def("set_media_references", [](Clip* clip, Clip::MediaReferences const& media_references, std::string const& new_active_key) { clip->set_media_references(media_references, new_active_key, ErrorStatusHandler()); @@ -568,13 +609,15 @@ Should be subclassed (for example by :class:`.Track` and :class:`.Stack`), not u track_class .def(py::init([](std::string name, std::optional> children, std::optional const& source_range, - std::string const& kind, py::object metadata) { + std::string const& kind, py::object metadata, + std::optional color) { auto composable_children = vector_or_default(children); Track* t = new Track( name, source_range, kind, - py_to_any_dictionary(metadata) + py_to_any_dictionary(metadata), + color ); if (!composable_children.empty()) t->set_children(composable_children, ErrorStatusHandler()); @@ -584,8 +627,10 @@ Should be subclassed (for example by :class:`.Track` and :class:`.Stack`), not u "children"_a = py::none(), "source_range"_a = std::nullopt, "kind"_a = std::string(Track::Kind::video), - py::arg_v("metadata"_a = py::none())) + py::arg_v("metadata"_a = py::none()), + "color"_a = std::nullopt) .def_property("kind", &Track::kind, &Track::set_kind) + .def_property("color", &Track::color, &Track::set_color) .def("neighbors_of", [](Track* t, Composable* item, Track::NeighborGapPolicy policy) { auto result = t->neighbors_of(item, ErrorStatusHandler(), policy); return py::make_tuple(py::cast(result.first.take_value()), py::cast(result.second.take_value())); diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_utils.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_utils.cpp index 2b1d5229c3..0403461e1d 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_utils.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_utils.cpp @@ -62,6 +62,7 @@ void _build_any_to_py_dispatch_table() { t[&typeid(RationalTime)] = [](std::any const& a, bool) { return py::cast(safely_cast_rational_time_any(a)); }; t[&typeid(TimeRange)] = [](std::any const& a, bool) { return py::cast(safely_cast_time_range_any(a)); }; t[&typeid(TimeTransform)] = [](std::any const& a, bool) { return py::cast(safely_cast_time_transform_any(a)); }; + t[&typeid(Color)] = [](std::any const& a, bool) { return py::cast(safely_cast_color_any(a)); }; t[&typeid(IMATH_NAMESPACE::V2d)] = [](std::any const& a, bool) { return py::cast(safely_cast_point_any(a)); }; t[&typeid(IMATH_NAMESPACE::Box2d)] = [](std::any const& a, bool) { return py::cast(safely_cast_box_any(a)); }; t[&typeid(SerializableObject::Retainer<>)] = [](std::any const& a, bool) { diff --git a/src/py-opentimelineio/opentimelineio/core/__init__.py b/src/py-opentimelineio/opentimelineio/core/__init__.py index 03c4f9f5c9..784ab11523 100644 --- a/src/py-opentimelineio/opentimelineio/core/__init__.py +++ b/src/py-opentimelineio/opentimelineio/core/__init__.py @@ -8,6 +8,7 @@ CannotComputeAvailableRangeError, # classes + Color, Composable, Composition, Item, @@ -40,12 +41,14 @@ ) from . import ( # noqa mediaReference, + color, composition, composable, item, ) __all__ = [ + 'Color', 'Composable', 'Composition', 'Item', diff --git a/src/py-opentimelineio/opentimelineio/core/color.py b/src/py-opentimelineio/opentimelineio/core/color.py new file mode 100644 index 0000000000..dca5d8f78b --- /dev/null +++ b/src/py-opentimelineio/opentimelineio/core/color.py @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +from .. core._core_utils import add_method +from .. import _otio + + +@add_method(_otio.Color) +def __str__(self): + return 'Color({}, ({}, {}, {}, {}))'.format( + repr(self.name), + self.r, + self.g, + self.b, + self.a + ) + + +@add_method(_otio.Color) +def __repr__(self): + return ( + 'otio.core.Color(' + 'name={}, ' + 'r={}, ' + 'g={}, ' + 'b={}, ' + 'a={}' + ')'.format( + repr(self.name), + repr(self.r), + repr(self.g), + repr(self.b), + repr(self.a) + ) + ) diff --git a/src/py-opentimelineio/opentimelineio/core/composition.py b/src/py-opentimelineio/opentimelineio/core/composition.py index 3388f2216b..b21357c418 100644 --- a/src/py-opentimelineio/opentimelineio/core/composition.py +++ b/src/py-opentimelineio/opentimelineio/core/composition.py @@ -23,6 +23,7 @@ def __repr__(self): "name={}, " "children={}, " "source_range={}, " + "color={}, " "metadata={}" ")".format( "core" if self.__class__ is _otio.Composition else "schema", @@ -30,6 +31,7 @@ def __repr__(self): repr(self.name), repr(list(self)), repr(self.source_range), + repr(self.color), repr(self.metadata) ) ) diff --git a/src/py-opentimelineio/opentimelineio/schema/clip.py b/src/py-opentimelineio/opentimelineio/schema/clip.py index cfa31a67cd..756d37d560 100644 --- a/src/py-opentimelineio/opentimelineio/schema/clip.py +++ b/src/py-opentimelineio/opentimelineio/schema/clip.py @@ -24,6 +24,7 @@ def __repr__(self): 'name={}, ' 'media_reference={}, ' 'source_range={}, ' + 'color={}, ' 'metadata={}, ' 'effects={}, ' 'markers={}' @@ -31,6 +32,7 @@ def __repr__(self): repr(self.name), repr(self.media_reference), repr(self.source_range), + repr(self.color), repr(self.metadata), repr(self.effects), repr(self.markers) diff --git a/tests/baselines/empty_clip.json b/tests/baselines/empty_clip.json index b362a5d33a..e8fa31532c 100644 --- a/tests/baselines/empty_clip.json +++ b/tests/baselines/empty_clip.json @@ -7,6 +7,7 @@ "enabled" : true, "effects" : [], "active_media_reference_key": "DEFAULT_MEDIA", + "color": null, "media_references" : { "DEFAULT_MEDIA" : { "FROM_TEST_FILE" : "empty_missingreference.json" diff --git a/tests/baselines/empty_color.json b/tests/baselines/empty_color.json new file mode 100644 index 0000000000..4d8900ac7b --- /dev/null +++ b/tests/baselines/empty_color.json @@ -0,0 +1,8 @@ +{ + "OTIO_SCHEMA": "Color.1", + "r": 1.0, + "g": 1.0, + "b": 1.0, + "a": 1.0, + "name": "" +} \ No newline at end of file diff --git a/tests/baselines/empty_gap.json b/tests/baselines/empty_gap.json index 66119f48fa..2c9a208449 100644 --- a/tests/baselines/empty_gap.json +++ b/tests/baselines/empty_gap.json @@ -4,6 +4,7 @@ "name" : "", "markers" : [], "enabled" : true, + "color": null, "effects" : [], "source_range" : { "FROM_TEST_FILE" : "empty_timerange.json" diff --git a/tests/baselines/empty_stack.json b/tests/baselines/empty_stack.json index d8c4d7d82e..b451afee53 100644 --- a/tests/baselines/empty_stack.json +++ b/tests/baselines/empty_stack.json @@ -9,5 +9,6 @@ "children" : [], "markers" : [], "enabled" : true, + "color": null, "effects" : [] } diff --git a/tests/baselines/empty_track.json b/tests/baselines/empty_track.json index a814f5553d..440cdf9670 100644 --- a/tests/baselines/empty_track.json +++ b/tests/baselines/empty_track.json @@ -9,6 +9,7 @@ "children" : [], "markers" : [], "enabled" : true, + "color": null, "effects" : [], "kind" : "Video" } diff --git a/tests/test_clip.py b/tests/test_clip.py index f1712020ba..83ada7b4ed 100644 --- a/tests/test_clip.py +++ b/tests/test_clip.py @@ -72,6 +72,7 @@ def test_str(self): "name='test_clip', " 'media_reference={}, ' 'source_range=None, ' + 'color=None, ' 'metadata={{}}, ' 'effects=[], ' 'markers=[]' @@ -102,6 +103,7 @@ def test_str_with_filepath(self): "target_url='/var/tmp/foo.mov'" "), " 'source_range=None, ' + 'color=None, ' 'metadata={}, ' 'effects=[], ' 'markers=[]' diff --git a/tests/test_color.py b/tests/test_color.py new file mode 100644 index 0000000000..f287433184 --- /dev/null +++ b/tests/test_color.py @@ -0,0 +1,109 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +import unittest + +import opentimelineio as otio +import opentimelineio.test_utils as otio_test_utils + + +class ColorTests(unittest.TestCase, otio_test_utils.OTIOAssertions): + + def test_convert_to(self): + white = otio.core.Color.WHITE + self.assertEqual(white.r, 1.0) + self.assertEqual(white.g, 1.0) + self.assertEqual(white.b, 1.0) + self.assertEqual(white.a, 1.0) + self.assertEqual(white.to_hex(), "#ffffffff") + self.assertEqual(white.to_rgba_int_list(8), [255, 255, 255, 255]) + self.assertEqual(white.to_agbr_integer(), 4294967295) + self.assertEqual(white.to_rgba_float_list(), [1.0, 1.0, 1.0, 1.0]) + + black = otio.core.Color.BLACK + self.assertEqual(black.r, 0.0) + self.assertEqual(black.g, 0.0) + self.assertEqual(black.b, 0.0) + self.assertEqual(black.a, 1.0) + self.assertEqual(black.to_hex(), "#000000ff") + self.assertEqual(black.to_rgba_int_list(8), [0, 0, 0, 255]) + self.assertEqual(black.to_agbr_integer(), 4278190080) + self.assertEqual(black.to_rgba_float_list(), [0.0, 0.0, 0.0, 1.0]) + + def test_from_hex(self): + all_reds = [ + "f00", # 3 digits + "f00f", # 4 digits + "ff0000", # 6 digits + "ff0000ff", # 8 digits + "0xff0000", # prefix + "#ff0000", # prefix + ] + for red_hex in all_reds + [s.upper() for s in all_reds]: + red = otio.core.Color.from_hex(red_hex) + self.assertEqual(red.r, 1.0) + self.assertEqual(red.g, 0.0) + self.assertEqual(red.b, 0.0) + self.assertEqual(red.a, 1.0) + + def test_from_int_list(self): + colors = ( + [255, 255, 255], + [0, 0, 0], + [255, 0, 0, 0], + ) + + for c in colors: + actual = otio.core.Color.from_int_list(c, 8).to_rgba_int_list(8) + if len(c) == 4: + self.assertEqual(c, actual) + elif len(c) == 3: + self.assertEqual(c[0], actual[0]) + self.assertEqual(c[1], actual[1]) + self.assertEqual(c[2], actual[2]) + self.assertEqual(255, actual[3]) + + def test_from_float_list(self): + colors = ( + [1.0, 1.0, 1.0], + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0], + ) + + for c in colors: + actual = otio.core.Color.from_float_list(c).to_rgba_float_list() + if len(c) == 4: + self.assertEqual(c, actual) + elif len(c) == 3: + self.assertEqual(c[0], actual[0]) + self.assertEqual(c[1], actual[1]) + self.assertEqual(c[2], actual[2]) + self.assertEqual(1.0, actual[3]) + + def test_from_agbr_int(self): + self.assertEqual( + otio.core.Color.from_agbr_int(4281740498).to_hex(), + '#d2362cff' + ) + + def test_repr(self): + cl = otio.core.Color.ORANGE + self.assertMultiLineEqual( + repr(cl), + 'otio.core.Color(' + 'name={}, ' + 'r={}, ' + 'g={}, ' + 'b={}, ' + 'a={})'.format( + repr(cl.name), + repr(cl.r), + repr(cl.g), + repr(cl.b), + repr(cl.a), + ) + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_composition.py b/tests/test_composition.py index 40d4e9f4be..a3e56feb3f 100755 --- a/tests/test_composition.py +++ b/tests/test_composition.py @@ -432,6 +432,7 @@ def test_repr(self): "name=" + repr(st.name) + ", " + "children=" + repr(list(st)) + ", " + "source_range=" + repr(st.source_range) + ", " + + "color=None, " "metadata=" + repr(st.metadata) + ")" ) @@ -943,6 +944,7 @@ def test_repr(self): "name=" + repr(sq.name) + ", " + "children=" + repr(list(sq)) + ", " + "source_range=" + repr(sq.source_range) + ", " + + "color=None, " + "metadata=" + repr(sq.metadata) + ")" ) diff --git a/tests/test_serialization.cpp b/tests/test_serialization.cpp index 53c352b2ca..e67ea09a6b 100644 --- a/tests/test_serialization.cpp +++ b/tests/test_serialization.cpp @@ -49,6 +49,7 @@ main(int argc, char** argv) "effects": [], "markers": [], "enabled": true, + "color": null, "children": [ { "OTIO_SCHEMA": "Track.1", @@ -58,6 +59,7 @@ main(int argc, char** argv) "effects": [], "markers": [], "enabled": true, + "color": null, "children": [ { "OTIO_SCHEMA": "Clip.2", @@ -67,6 +69,7 @@ main(int argc, char** argv) "effects": [], "markers": [], "enabled": true, + "color": null, "media_references": { "DEFAULT_MEDIA": { "OTIO_SCHEMA": "MissingReference.1", From b8cefa1d8764f927ec475b1995f1c046c8874435 Mon Sep 17 00:00:00 2001 From: Spencer Magnusson Date: Tue, 29 Jul 2025 14:22:46 -0700 Subject: [PATCH 2/2] Add transparent color constant Signed-off-by: Spencer Magnusson --- src/opentimelineio/color.cpp | 1 + src/opentimelineio/color.h | 1 + .../opentimelineio-bindings/otio_serializableObjects.cpp | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/opentimelineio/color.cpp b/src/opentimelineio/color.cpp index 4f09d92fe7..c827e090b5 100644 --- a/src/opentimelineio/color.cpp +++ b/src/opentimelineio/color.cpp @@ -37,6 +37,7 @@ const Color Color::purple(0.5, 0.0, 0.5, 1.0, "Purple"); const Color Color::magenta(1.0, 0.0, 1.0, 1.0, "Magenta"); const Color Color::black(0.0, 0.0, 0.0, 1.0, "Black"); const Color Color::white(1.0, 1.0, 1.0, 1.0, "White"); +const Color Color::transparent(1.0, 1.0, 1.0, 0.0, "Transparent"); Color* Color::from_hex(std::string const& color) diff --git a/src/opentimelineio/color.h b/src/opentimelineio/color.h index 147c56b9a8..1d3018b5c6 100644 --- a/src/opentimelineio/color.h +++ b/src/opentimelineio/color.h @@ -50,6 +50,7 @@ class Color static const Color magenta; static const Color black; static const Color white; + static const Color transparent; static Color* from_hex(std::string const& color); static Color* from_int_list(std::vector const& color, int bit_depth); diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp index 1c6c2f0f50..e928336ad1 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp @@ -236,7 +236,8 @@ static void define_bases2(py::module m) { .def_property_readonly_static("PURPLE", [](py::object) { return new Color(Color::purple); }, py::return_value_policy::take_ownership) .def_property_readonly_static("MAGENTA", [](py::object) { return new Color(Color::magenta); }, py::return_value_policy::take_ownership) .def_property_readonly_static("BLACK", [](py::object) { return new Color(Color::black); }, py::return_value_policy::take_ownership) - .def_property_readonly_static("WHITE", [](py::object) { return new Color(Color::white); }, py::return_value_policy::take_ownership); + .def_property_readonly_static("WHITE", [](py::object) { return new Color(Color::white); }, py::return_value_policy::take_ownership) + .def_property_readonly_static("TRANSPARENT", [](py::object) { return new Color(Color::transparent); }, py::return_value_policy::take_ownership); auto marker_class = py::class_>(m, "Marker", py::dynamic_attr(), R"docstring(