/** * @file * @brief Unit tests for Object Mask effect * @author Jonathan Thomas * * @ref License */ // Copyright (c) 2026 OpenShot Studios, LLC // // SPDX-License-Identifier: LGPL-3.0-or-later #include "openshot_catch.h" #include "EffectInfo.h" #include "Frame.h" #include "Json.h" #include "effects/ObjectMask.h" #include #include #include #include #include #include #include using namespace openshot; static std::shared_ptr make_object_mask_frame(int64_t number, int width, int height) { auto frame = std::make_shared(number, width, height, "#000000"); frame->GetImage()->fill(QColor(64, 64, 64, 255)); return frame; } static std::string temp_object_mask_path() { char path[] = "/tmp/libopenshot_object_mask_XXXXXX"; int fd = mkstemp(path); REQUIRE(fd != -1); close(fd); std::remove(path); return std::string(path) + ".data"; } static void append_varint(std::string& output, uint64_t value) { while (value >= 0x80) { output.push_back(static_cast((value & 0x7f) | 0x80)); value >>= 7; } output.push_back(static_cast(value)); } static void append_fixed32_float(std::string& output, float value) { uint32_t bits; std::memcpy(&bits, &value, sizeof(float)); for (int i = 0; i < 4; ++i) output.push_back(static_cast((bits >> (8 * i)) & 0xff)); } static void append_length_delimited(std::string& output, uint32_t field_number, const std::string& value) { append_varint(output, (field_number << 3) | 2); append_varint(output, static_cast(value.size())); output.append(value); } static std::string create_object_mask_data() { const std::string path = temp_object_mask_path(); std::string mask; append_varint(mask, 8); append_varint(mask, 4); append_varint(mask, 16); append_varint(mask, 4); for (uint32_t count : {0u, 6u, 10u}) { append_varint(mask, 24); append_varint(mask, count); } std::string box; append_varint(box, 13); append_fixed32_float(box, 0.0f); append_varint(box, 21); append_fixed32_float(box, 0.0f); append_varint(box, 29); append_fixed32_float(box, 1.0f); append_varint(box, 37); append_fixed32_float(box, 1.0f); append_varint(box, 40); append_varint(box, 0); append_varint(box, 53); append_fixed32_float(box, 0.95f); append_varint(box, 56); append_varint(box, 1); append_length_delimited(box, 8, mask); std::string frame; append_varint(frame, 8); append_varint(frame, 1); append_length_delimited(frame, 2, box); std::string data; append_length_delimited(data, 1, frame); append_length_delimited(data, 3, "object mask"); std::ofstream output(path, std::ios::out | std::ios::binary); output.write(data.data(), static_cast(data.size())); REQUIRE(output.good()); return path; } TEST_CASE("ObjectMask effect is registered", "[effect][object_mask]") { std::unique_ptr effect(EffectInfo().CreateEffect("ObjectMask")); REQUIRE(effect != nullptr); CHECK(effect->info.name == "Object Mask"); CHECK(effect->info.has_video); CHECK(effect->info.has_tracked_object); } TEST_CASE("ObjectMask loads protobuf masks and exposes style controls", "[effect][object_mask]") { const std::string protobuf_path = create_object_mask_data(); ObjectMask effect; Json::Value config; config["protobuf_data_path"] = protobuf_path; config["mask_alpha"] = Keyframe(0.5).JsonValue(); config["stroke_width"] = Keyframe(2.0).JsonValue(); effect.SetJsonValue(config); Json::Value properties = stringToJson(effect.PropertiesJSON(1)); CHECK(properties["draw_mask"]["name"].asString() == "Draw Mask"); CHECK(properties["mask_color"]["name"].asString() == "Mask Color"); CHECK(properties["mask_alpha"]["value"].asDouble() == Approx(0.5)); CHECK(properties["stroke_color"]["name"].asString() == "Stroke Color"); CHECK(properties["stroke_alpha"]["name"].asString() == "Stroke Alpha"); CHECK(properties["stroke_width"]["value"].asDouble() == Approx(2.0)); auto generated_mask = effect.TrackedObjectMask(std::make_shared(4, 4, QImage::Format_RGBA8888_Premultiplied), 1); REQUIRE(generated_mask != nullptr); CHECK(generated_mask->pixelColor(0, 0).red() == 255); CHECK(generated_mask->pixelColor(3, 3).red() == 0); Json::Value no_stroke; no_stroke["stroke_width"] = Keyframe(0.0).JsonValue(); effect.SetJsonValue(no_stroke); auto frame = make_object_mask_frame(1, 4, 4); auto output = effect.GetFrame(frame, 1)->GetImage(); CHECK(output->pixelColor(0, 0) != QColor(64, 64, 64, 255)); CHECK(output->pixelColor(3, 3) == QColor(64, 64, 64, 255)); std::remove(protobuf_path.c_str()); }