diff --git a/src/Clip.cpp b/src/Clip.cpp index 16e1b052..9c3e62c6 100644 --- a/src/Clip.cpp +++ b/src/Clip.cpp @@ -32,6 +32,34 @@ using namespace openshot; +namespace { + struct CompositeChoice { const char* name; CompositeType value; }; + const CompositeChoice composite_choices[] = { + {"Normal", COMPOSITE_SOURCE_OVER}, + + // Darken group + {"Darken", COMPOSITE_DARKEN}, + {"Multiply", COMPOSITE_MULTIPLY}, + {"Color Burn", COMPOSITE_COLOR_BURN}, + + // Lighten group + {"Lighten", COMPOSITE_LIGHTEN}, + {"Screen", COMPOSITE_SCREEN}, + {"Color Dodge", COMPOSITE_COLOR_DODGE}, + {"Add", COMPOSITE_PLUS}, + + // Contrast group + {"Overlay", COMPOSITE_OVERLAY}, + {"Soft Light", COMPOSITE_SOFT_LIGHT}, + {"Hard Light", COMPOSITE_HARD_LIGHT}, + + // Compare + {"Difference", COMPOSITE_DIFFERENCE}, + {"Exclusion", COMPOSITE_EXCLUSION}, + }; + const int composite_choices_count = sizeof(composite_choices)/sizeof(CompositeChoice); +} + // Init default settings for a clip void Clip::init_settings() { @@ -45,6 +73,7 @@ void Clip::init_settings() anchor = ANCHOR_CANVAS; display = FRAME_DISPLAY_NONE; mixing = VOLUME_MIX_NONE; + composite = COMPOSITE_SOURCE_OVER; waveform = false; previous_properties = ""; parentObjectId = ""; @@ -766,6 +795,7 @@ std::string Clip::PropertiesJSON(int64_t requested_frame) const { root["scale"] = add_property_json("Scale", scale, "int", "", NULL, 0, 3, false, requested_frame); root["display"] = add_property_json("Frame Number", display, "int", "", NULL, 0, 3, false, requested_frame); root["mixing"] = add_property_json("Volume Mixing", mixing, "int", "", NULL, 0, 2, false, requested_frame); + root["composite"] = add_property_json("Composite", composite, "int", "", NULL, 0, composite_choices_count - 1, false, requested_frame); root["waveform"] = add_property_json("Waveform", waveform, "int", "", NULL, 0, 1, false, requested_frame); root["parentObjectId"] = add_property_json("Parent", 0.0, "string", parentObjectId, NULL, -1, -1, false, requested_frame); @@ -797,6 +827,10 @@ std::string Clip::PropertiesJSON(int64_t requested_frame) const { root["mixing"]["choices"].append(add_property_choice_json("Average", VOLUME_MIX_AVERAGE, mixing)); root["mixing"]["choices"].append(add_property_choice_json("Reduce", VOLUME_MIX_REDUCE, mixing)); + // Add composite choices (dropdown style) + for (int i = 0; i < composite_choices_count; ++i) + root["composite"]["choices"].append(add_property_choice_json(composite_choices[i].name, composite_choices[i].value, composite)); + // Add waveform choices (dropdown style) root["waveform"]["choices"].append(add_property_choice_json("Yes", true, waveform)); root["waveform"]["choices"].append(add_property_choice_json("No", false, waveform)); @@ -879,6 +913,7 @@ Json::Value Clip::JsonValue() const { root["anchor"] = anchor; root["display"] = display; root["mixing"] = mixing; + root["composite"] = composite; root["waveform"] = waveform; root["scale_x"] = scale_x.JsonValue(); root["scale_y"] = scale_y.JsonValue(); @@ -967,6 +1002,8 @@ void Clip::SetJsonValue(const Json::Value root) { display = (FrameDisplayType) root["display"].asInt(); if (!root["mixing"].isNull()) mixing = (VolumeMixType) root["mixing"].asInt(); + if (!root["composite"].isNull()) + composite = (CompositeType) root["composite"].asInt(); if (!root["waveform"].isNull()) waveform = root["waveform"].asBool(); if (!root["scale_x"].isNull()) @@ -1197,7 +1234,7 @@ void Clip::apply_background(std::shared_ptr frame, std::shared_ QPainter painter(background_canvas.get()); // Composite a new layer onto the image - painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + painter.setCompositionMode(static_cast(composite)); painter.drawImage(0, 0, *frame->GetImage()); painter.end(); @@ -1260,7 +1297,7 @@ void Clip::apply_keyframes(std::shared_ptr frame, QSize timeline_size) { painter.setTransform(transform); // Composite a new layer onto the image - painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + painter.setCompositionMode(static_cast(composite)); // Apply opacity via painter instead of per-pixel alpha manipulation const float alpha_value = alpha.GetValue(frame->number); diff --git a/src/Clip.h b/src/Clip.h index caeabd57..98da5401 100644 --- a/src/Clip.h +++ b/src/Clip.h @@ -169,6 +169,7 @@ namespace openshot { openshot::AnchorType anchor; ///< The anchor determines what parent a clip should snap to openshot::FrameDisplayType display; ///< The format to display the frame number (if any) openshot::VolumeMixType mixing; ///< What strategy should be followed when mixing audio with other clips + openshot::CompositeType composite; ///< How this clip is composited onto lower layers #ifdef USE_OPENCV bool COMPILED_WITH_CV = true; diff --git a/src/Enums.h b/src/Enums.h index 14b69316..e3029c1f 100644 --- a/src/Enums.h +++ b/src/Enums.h @@ -64,6 +64,37 @@ enum VolumeMixType VOLUME_MIX_REDUCE ///< Reduce volume by about %25, and then mix (louder, but could cause pops if the sum exceeds 100%) }; +/// This enumeration determines how clips are composited onto lower layers. +enum CompositeType { + COMPOSITE_SOURCE_OVER, + COMPOSITE_DESTINATION_OVER, + COMPOSITE_CLEAR, + COMPOSITE_SOURCE, + COMPOSITE_DESTINATION, + COMPOSITE_SOURCE_IN, + COMPOSITE_DESTINATION_IN, + COMPOSITE_SOURCE_OUT, + COMPOSITE_DESTINATION_OUT, + COMPOSITE_SOURCE_ATOP, + COMPOSITE_DESTINATION_ATOP, + COMPOSITE_XOR, + + // svg 1.2 blend modes + COMPOSITE_PLUS, + COMPOSITE_MULTIPLY, + COMPOSITE_SCREEN, + COMPOSITE_OVERLAY, + COMPOSITE_DARKEN, + COMPOSITE_LIGHTEN, + COMPOSITE_COLOR_DODGE, + COMPOSITE_COLOR_BURN, + COMPOSITE_HARD_LIGHT, + COMPOSITE_SOFT_LIGHT, + COMPOSITE_DIFFERENCE, + COMPOSITE_EXCLUSION, + + COMPOSITE_LAST = COMPOSITE_EXCLUSION +}; /// This enumeration determines the distortion type of Distortion Effect. enum DistortionType diff --git a/tests/Clip.cpp b/tests/Clip.cpp index e37e2315..ffd5c832 100644 --- a/tests/Clip.cpp +++ b/tests/Clip.cpp @@ -42,6 +42,7 @@ TEST_CASE( "default constructor", "[libopenshot][clip]" ) CHECK(c1.anchor == ANCHOR_CANVAS); CHECK(c1.gravity == GRAVITY_CENTER); CHECK(c1.scale == SCALE_FIT); + CHECK(c1.composite == COMPOSITE_SOURCE_OVER); CHECK(c1.Layer() == 0); CHECK(c1.Position() == Approx(0.0f).margin(0.00001)); CHECK(c1.Start() == Approx(0.0f).margin(0.00001)); @@ -60,6 +61,7 @@ TEST_CASE( "path string constructor", "[libopenshot][clip]" ) CHECK(c1.anchor == ANCHOR_CANVAS); CHECK(c1.gravity == GRAVITY_CENTER); CHECK(c1.scale == SCALE_FIT); + CHECK(c1.composite == COMPOSITE_SOURCE_OVER); CHECK(c1.Layer() == 0); CHECK(c1.Position() == Approx(0.0f).margin(0.00001)); CHECK(c1.Start() == Approx(0.0f).margin(0.00001)); @@ -76,6 +78,7 @@ TEST_CASE( "basic getters and setters", "[libopenshot][clip]" ) CHECK(c1.anchor == ANCHOR_CANVAS); CHECK(c1.gravity == GRAVITY_CENTER); CHECK(c1.scale == SCALE_FIT); + CHECK(c1.composite == COMPOSITE_SOURCE_OVER); CHECK(c1.Layer() == 0); CHECK(c1.Position() == Approx(0.0f).margin(0.00001)); CHECK(c1.Start() == Approx(0.0f).margin(0.00001)); @@ -541,9 +544,9 @@ TEST_CASE( "painter_opacity_applied_no_per_pixel_mutation", "[libopenshot][clip] // In Qt, pixelColor() returns unpremultiplied values, so expect alpha ≈ 127 and red ≈ 255. QColor p = img->pixelColor(70, 50); CHECK(p.alpha() == Approx(127).margin(10)); - CHECK(p.red() == Approx(255).margin(2)); + CHECK(p.red() == Approx(255).margin(2)); CHECK(p.green() == Approx(0).margin(2)); - CHECK(p.blue() == Approx(0).margin(2)); + CHECK(p.blue() == Approx(0).margin(2)); } TEST_CASE( "composite_over_opaque_background_blend", "[libopenshot][clip][pr]" ) @@ -652,9 +655,9 @@ TEST_CASE( "transform_path_identity_vs_scaled", "[libopenshot][clip][pr]" ) REQUIRE(img); // Pick a mid pixel that is white in the grid (multiple of 4) QColor c = img->pixelColor(20, 20); - CHECK(c.red() >= 240); + CHECK(c.red() >= 240); CHECK(c.green() >= 240); - CHECK(c.blue() >= 240); + CHECK(c.blue() >= 240); } // Case B: Downscale (trigger transform path). Clear the clip cache so we don't @@ -684,7 +687,7 @@ TEST_CASE( "transform_path_identity_vs_scaled", "[libopenshot][clip][pr]" ) // Optional diagnostic: scaled typically yields <= number of pure whites vs identity. int white_id = count_white(*out_identity->GetImage(), x0, y0, x1, y1); - int white_sc = count_white(*img_scaled, x0, y0, x1, y1); + int white_sc = count_white(*img_scaled, x0, y0, x1, y1); CHECK(white_sc <= white_id); } }