diff --git a/Sources/objc/include/opentimelineio.h b/Sources/objc/include/opentimelineio.h index 8641494..067c4fb 100644 --- a/Sources/objc/include/opentimelineio.h +++ b/Sources/objc/include/opentimelineio.h @@ -29,6 +29,7 @@ void* otio_new_external_reference(); void* otio_new_freeze_frame(); void* otio_new_gap(); void* otio_new_generator_reference(); +void* otio_new_image_sequence_reference(); void* otio_new_item(); void* otio_new_linear_time_warp(); void* otio_new_marker(); @@ -98,6 +99,29 @@ void effect_set_name(CxxRetainer* self, NSString*); NSString* external_reference_get_target_url(CxxRetainer* self); void external_reference_set_target_url(CxxRetainer* self, NSString*); +// MARK: - ImageSequenceReference +NSString* image_sequence_reference_get_target_url_base(CxxRetainer* self); +void image_sequence_reference_set_target_url_base(CxxRetainer* self, NSString*); +NSString* image_sequence_reference_get_name_prefix(CxxRetainer* self); +void image_sequence_reference_set_name_prefix(CxxRetainer* self, NSString*); +NSString* image_sequence_reference_get_name_suffix(CxxRetainer* self); +void image_sequence_reference_set_name_suffix(CxxRetainer* self, NSString*); +int image_sequence_reference_get_start_frame(CxxRetainer* self); +void image_sequence_reference_set_start_frame(CxxRetainer* self, int start_frame); +int image_sequence_reference_get_frame_step(CxxRetainer* self); +void image_sequence_reference_set_frame_step(CxxRetainer* self, int frame_step); +double image_sequence_reference_get_rate(CxxRetainer* self); +void image_sequence_reference_set_rate(CxxRetainer* self, double rate); +int image_sequence_reference_get_frame_zero_padding(CxxRetainer* self); +void image_sequence_reference_set_frame_zero_padding(CxxRetainer* self, int frame_zero_padding); +int image_sequence_reference_get_missing_frame_policy(CxxRetainer* self); +void image_sequence_reference_set_missing_frame_policy(CxxRetainer* self, int missing_frame_policy); +int image_sequence_reference_end_frame(CxxRetainer* self); +int image_sequence_reference_number_of_images_in_sequence(CxxRetainer* self); +int image_sequence_reference_frame_for_time(CxxRetainer* self, CxxRationalTime, CxxErrorStruct*); +NSString* image_sequence_reference_target_url_for_image_number(CxxRetainer* self, int image_number, CxxErrorStruct*); +CxxRationalTime image_sequence_reference_presentation_time_for_image_number(CxxRetainer* self, int image_number, CxxErrorStruct*); + // MARK: - GeneratorReference NSString* generator_reference_get_generator_kind(CxxRetainer* self); void generator_reference_set_generator_kind(CxxRetainer* self, NSString*); diff --git a/Sources/objc/opentimelineio.mm b/Sources/objc/opentimelineio.mm index ebcb141..ed4e820 100644 --- a/Sources/objc/opentimelineio.mm +++ b/Sources/objc/opentimelineio.mm @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -143,6 +144,10 @@ return new otio::GeneratorReference; } +void* otio_new_image_sequence_reference() { + return new otio::ImageSequenceReference; +} + void* otio_new_item() { return new otio::Item; } @@ -774,6 +779,94 @@ void external_reference_set_target_url(CxxRetainer* self, NSString* target_url) SO_cast(self)->set_target_url([target_url UTF8String]); } +// MARK: - ImageSequenceReference +NSString* image_sequence_reference_get_target_url_base(CxxRetainer* self) { + return make_nsstring(SO_cast(self)->target_url_base()); +} + +void image_sequence_reference_set_target_url_base(CxxRetainer* self, NSString* target_url_base) { + SO_cast(self)->set_target_url_base([target_url_base UTF8String]); +} + +NSString* image_sequence_reference_get_name_prefix(CxxRetainer* self) { + return make_nsstring(SO_cast(self)->name_prefix()); +} + +void image_sequence_reference_set_name_prefix(CxxRetainer* self, NSString* name_prefix) { + SO_cast(self)->set_name_prefix([name_prefix UTF8String]); +} + +NSString* image_sequence_reference_get_name_suffix(CxxRetainer* self) { + return make_nsstring(SO_cast(self)->name_suffix()); +} + +void image_sequence_reference_set_name_suffix(CxxRetainer* self, NSString* name_suffix) { + SO_cast(self)->set_name_suffix([name_suffix UTF8String]); +} + +int image_sequence_reference_get_start_frame(CxxRetainer* self) { + return SO_cast(self)->start_frame(); +} + +void image_sequence_reference_set_start_frame(CxxRetainer* self, int start_frame) { + SO_cast(self)->set_start_frame(start_frame); +} + +int image_sequence_reference_get_frame_step(CxxRetainer* self) { + return SO_cast(self)->frame_step(); +} + +void image_sequence_reference_set_frame_step(CxxRetainer* self, int frame_step) { + SO_cast(self)->set_frame_step(frame_step); +} + +double image_sequence_reference_get_rate(CxxRetainer* self) { + return SO_cast(self)->rate(); +} + +void image_sequence_reference_set_rate(CxxRetainer* self, double rate) { + SO_cast(self)->set_rate(rate); +} + +int image_sequence_reference_get_frame_zero_padding(CxxRetainer* self) { + return SO_cast(self)->frame_zero_padding(); +} + +void image_sequence_reference_set_frame_zero_padding(CxxRetainer* self, int frame_zero_padding) { + SO_cast(self)->set_frame_zero_padding(frame_zero_padding); +} + +int image_sequence_reference_get_missing_frame_policy(CxxRetainer* self) { + return SO_cast(self)->missing_frame_policy(); +} + +void image_sequence_reference_set_missing_frame_policy(CxxRetainer* self, int missing_frame_policy) { + SO_cast(self)->set_missing_frame_policy(otio::ImageSequenceReference::MissingFramePolicy(missing_frame_policy)); +} + +int image_sequence_reference_end_frame(CxxRetainer* self) { + return SO_cast(self)->end_frame(); +} + +int image_sequence_reference_number_of_images_in_sequence(CxxRetainer* self) { + return SO_cast(self)->number_of_images_in_sequence(); +} + +int image_sequence_reference_frame_for_time(CxxRetainer* self, CxxRationalTime time, CxxErrorStruct* cxxErr) { + _AutoErrorHandler aeh(cxxErr); + return SO_cast(self)->frame_for_time(otioRationalTime(time), &aeh.error_status); +} + +NSString* image_sequence_reference_target_url_for_image_number(CxxRetainer* self, int image_number, CxxErrorStruct* cxxErr) { + _AutoErrorHandler aeh(cxxErr); + return make_nsstring(SO_cast(self)->target_url_for_image_number(image_number, &aeh.error_status)); +} + +CxxRationalTime image_sequence_reference_presentation_time_for_image_number(CxxRetainer* self, int image_number, CxxErrorStruct* cxxErr) { + _AutoErrorHandler aeh(cxxErr); + return cxxRationalTime(SO_cast(self)->presentation_time_for_image_number(image_number, &aeh.error_status)); +} + // MARK: - GeneratorReference NSString* generator_reference_get_generator_kind(CxxRetainer* self) { return make_nsstring(SO_cast(self)->generator_kind()); diff --git a/Sources/swift/ImageSequenceReference.swift b/Sources/swift/ImageSequenceReference.swift new file mode 100644 index 0000000..7fbb269 --- /dev/null +++ b/Sources/swift/ImageSequenceReference.swift @@ -0,0 +1,130 @@ +// +// ImageSequenceReference.swift +// +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +import Foundation +import OpenTimelineIO_objc + +public class ImageSequenceReference : MediaReference { + override public init() { + super.init(otio_new_image_sequence_reference()) + } + + public enum MissingFramePolicy: Int { + case error = 0 + case hold = 1 + case black = 2 + } + + public convenience init(targetURLBase: String? = nil, + namePrefix: String? = nil, + nameSuffix: String? = nil, + startFrame: Int = 1, + frameStep: Int = 1, + rate: Double = 1, + frameZeroPadding: Int = 0, + missingFramePolicy: MissingFramePolicy = .error, + availableRange: TimeRange? = nil, + metadata: ST? = nil) where ST.Element == Metadata.Dictionary.Element { + self.init() + metadataInit(name, metadata) + if let targetURLBase = targetURLBase { + self.targetURLBase = targetURLBase + } + if let namePrefix = namePrefix { + self.namePrefix = namePrefix + } + if let nameSuffix = nameSuffix { + self.nameSuffix = nameSuffix + } + self.startFrame = startFrame + self.frameStep = frameStep + self.rate = rate + self.frameZeroPadding = frameZeroPadding + self.missingFramePolicy = missingFramePolicy + if let availableRange = availableRange { + self.availableRange = availableRange + } + } + + public convenience init(targetURLBase: String? = nil, + namePrefix: String? = nil, + nameSuffix: String? = nil, + startFrame: Int = 1, + frameStep: Int = 1, + rate: Double = 1, + frameZeroPadding: Int = 0, + missingFramePolicy: MissingFramePolicy = .error, + availableRange: TimeRange? = nil) { + self.init(targetURLBase: targetURLBase, namePrefix: namePrefix, nameSuffix: nameSuffix, + startFrame: startFrame, frameStep: frameStep, rate: rate, + frameZeroPadding: frameZeroPadding, missingFramePolicy: missingFramePolicy, + availableRange: availableRange, metadata: Metadata.Dictionary.none) + } + + public var targetURLBase: String { + get { return image_sequence_reference_get_target_url_base(self) } + set { image_sequence_reference_set_target_url_base(self, newValue) } + } + + public var namePrefix: String { + get { return image_sequence_reference_get_name_prefix(self) } + set { image_sequence_reference_set_name_prefix(self, newValue) } + } + + public var nameSuffix: String { + get { return image_sequence_reference_get_name_suffix(self) } + set { image_sequence_reference_set_name_suffix(self, newValue) } + } + + public var startFrame: Int { + get { return Int(image_sequence_reference_get_start_frame(self)) } + set { image_sequence_reference_set_start_frame(self, Int32(newValue)) } + } + + public var frameStep: Int { + get { return Int(image_sequence_reference_get_frame_step(self)) } + set { image_sequence_reference_set_frame_step(self, Int32(newValue)) } + } + + public var rate: Double { + get { return image_sequence_reference_get_rate(self) } + set { image_sequence_reference_set_rate(self, newValue) } + } + + public var frameZeroPadding: Int { + get { return Int(image_sequence_reference_get_frame_zero_padding(self)) } + set { image_sequence_reference_set_frame_zero_padding(self, Int32(newValue)) } + } + + public var missingFramePolicy: MissingFramePolicy { + get { return MissingFramePolicy(rawValue: Int(image_sequence_reference_get_missing_frame_policy(self)))! } + set { image_sequence_reference_set_missing_frame_policy(self, Int32(newValue.rawValue)) } + } + + public var endFrame: Int { + get { return Int(image_sequence_reference_end_frame(self)) } + } + + public var numberOfImagesInSequence: Int { + get { return Int(image_sequence_reference_number_of_images_in_sequence(self)) } + } + + public func frame(for time: RationalTime) throws -> Int { + return try Int(OTIOError.returnOrThrow { image_sequence_reference_frame_for_time(self, time.cxxRationalTime, &$0) }) + } + + public func targetURL(forImageNumber imageNumber: Int) throws -> String { + return try OTIOError.returnOrThrow { image_sequence_reference_target_url_for_image_number(self, Int32(imageNumber), &$0) } + } + + public func presentationTime(forImageNumber imageNumber: Int) throws -> RationalTime { + return try RationalTime(OTIOError.returnOrThrow { image_sequence_reference_presentation_time_for_image_number(self, Int32(imageNumber), &$0) }) + } + + override internal init(_ cxxPtr: CxxSerializableObjectPtr) { + super.init(cxxPtr) + } +} diff --git a/Sources/swift/SerializableObject.swift b/Sources/swift/SerializableObject.swift index 56d50f8..8c06659 100644 --- a/Sources/swift/SerializableObject.swift +++ b/Sources/swift/SerializableObject.swift @@ -99,6 +99,7 @@ public class SerializableObject: CxxRetainer { creationFunctions["FreezeFrame"] = { FreezeFrame($0) } creationFunctions["Gap"] = { Gap($0) } creationFunctions["GeneratorReference"] = { GeneratorReference($0) } + creationFunctions["ImageSequenceReference"] = { ImageSequenceReference($0) } creationFunctions["Item"] = { Item($0) } creationFunctions["LinearTimeWarp"] = { LinearTimeWarp($0) } creationFunctions["Marker"] = { Marker($0) } diff --git a/Tests/OpenTimelineIOTests/testImageSequenceReference.swift b/Tests/OpenTimelineIOTests/testImageSequenceReference.swift new file mode 100644 index 0000000..dcc9292 --- /dev/null +++ b/Tests/OpenTimelineIOTests/testImageSequenceReference.swift @@ -0,0 +1,226 @@ +// +// testImageSequenceReference.swift +// +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +import XCTest +@testable import OpenTimelineIO + +import Foundation + +class testImageSequenceReference: XCTestCase { + + func testCreate() { + let tr = TimeRange(startTime: RationalTime(value: 0, rate: 30), + duration: RationalTime(value: 60, rate: 30)) + let dict = Metadata.Dictionary(arrayLiteral: ("custom", "bar")) + + let ref = ImageSequenceReference(targetURLBase: "file:///show/seq/shot/rndr/", + namePrefix: "show_shot.", + nameSuffix: ".exr", + frameStep: 3, + rate: 30, + frameZeroPadding: 5, + missingFramePolicy: .hold, + availableRange: tr, + metadata: dict) + + XCTAssertEqual(ref.targetURLBase, "file:///show/seq/shot/rndr/") + XCTAssertEqual(ref.namePrefix, "show_shot.") + XCTAssertEqual(ref.nameSuffix, ".exr") + XCTAssertEqual(ref.frameZeroPadding, 5) + XCTAssertEqual(ref.availableRange, tr) + XCTAssertEqual(ref.frameStep, 3) + XCTAssertEqual(ref.rate, 30) + XCTAssertEqual(ref.missingFramePolicy, .hold) + XCTAssertEqual(ref.metadata["custom"] as! String, "bar") + } + + func testProperties() { + let ref = ImageSequenceReference() + ref.targetURLBase = "file:///show/seq/shot/rndr/" + ref.namePrefix = "show_shot." + ref.nameSuffix = ".exr" + ref.startFrame = 101 + ref.frameStep = 2 + ref.rate = 24 + ref.frameZeroPadding = 4 + ref.missingFramePolicy = .black + + XCTAssertEqual(ref.targetURLBase, "file:///show/seq/shot/rndr/") + XCTAssertEqual(ref.namePrefix, "show_shot.") + XCTAssertEqual(ref.nameSuffix, ".exr") + XCTAssertEqual(ref.startFrame, 101) + XCTAssertEqual(ref.frameStep, 2) + XCTAssertEqual(ref.rate, 24) + XCTAssertEqual(ref.frameZeroPadding, 4) + XCTAssertEqual(ref.missingFramePolicy, .black) + } + + func testNumberOfImagesInSequence() { + let tr = TimeRange(startTime: RationalTime(value: 0, rate: 24), + duration: RationalTime(value: 48, rate: 24)) + let ref = ImageSequenceReference(targetURLBase: "file:///show/seq/shot/rndr/", + namePrefix: "show_shot.", + nameSuffix: ".exr", + rate: 24, + availableRange: tr) + + XCTAssertEqual(ref.numberOfImagesInSequence, 48) + + ref.frameStep = 2 + XCTAssertEqual(ref.numberOfImagesInSequence, 24) + + ref.frameStep = 3 + XCTAssertEqual(ref.numberOfImagesInSequence, 16) + } + + func testEndFrame() { + let tr = TimeRange(startTime: RationalTime(value: 12, rate: 24), + duration: RationalTime(value: 48, rate: 24)) + let ref = ImageSequenceReference(targetURLBase: "file:///show/seq/shot/rndr/", + namePrefix: "show_shot.", + nameSuffix: ".exr", + startFrame: 101, + rate: 24, + frameZeroPadding: 4, + availableRange: tr) + + XCTAssertEqual(ref.endFrame, 148) + + ref.frameStep = 2 + XCTAssertEqual(ref.endFrame, 148) + } + + func testTargetURLForImageNumber() throws { + let tr = TimeRange(startTime: RationalTime(value: 0, rate: 24), + duration: RationalTime(value: 48, rate: 24)) + let ref = ImageSequenceReference(targetURLBase: "file:///show/seq/shot/rndr/", + namePrefix: "show_shot.", + nameSuffix: ".exr", + startFrame: 1, + frameStep: 1, + rate: 24, + frameZeroPadding: 4, + availableRange: tr) + + XCTAssertEqual(ref.numberOfImagesInSequence, 48) + for i in 0..