Adding composite/blend modes to libopenshot:

-Normal
-Darken
-Multiply
-Color Burn
-Lighten
-Screen
-Color Dodge
-Add
-Overlay
-Soft Light
-Hard Light
-Difference
-Exclusion
This commit is contained in:
Jonathan Thomas
2025-09-12 17:47:41 -05:00
parent b94dcac3b4
commit 523ef17aa4
4 changed files with 79 additions and 7 deletions

View File

@@ -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<openshot::Frame> frame, std::shared_
QPainter painter(background_canvas.get());
// Composite a new layer onto the image
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
painter.setCompositionMode(static_cast<QPainter::CompositionMode>(composite));
painter.drawImage(0, 0, *frame->GetImage());
painter.end();
@@ -1260,7 +1297,7 @@ void Clip::apply_keyframes(std::shared_ptr<Frame> frame, QSize timeline_size) {
painter.setTransform(transform);
// Composite a new layer onto the image
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
painter.setCompositionMode(static_cast<QPainter::CompositionMode>(composite));
// Apply opacity via painter instead of per-pixel alpha manipulation
const float alpha_value = alpha.GetValue(frame->number);

View File

@@ -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;

View File

@@ -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

View File

@@ -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);
}
}