From ec71b7335c29f659e64a37bef6443e0fda472bfa Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Sat, 9 May 2026 14:50:36 -0500 Subject: [PATCH] Added top/bottom/left/right margins to Blur effect, similar to Pixelate one (for consistency). Renamed the user-facing JsonProperty labels across all effects with similar keyframes to: - Margin: Left - Margin: Top - Margin: Right - Margin: Bottom --- src/effects/Bars.cpp | 8 ++-- src/effects/Blur.cpp | 92 ++++++++++++++++++++++++++++++++++++---- src/effects/Blur.h | 17 ++++++++ src/effects/Caption.cpp | 6 +-- src/effects/Crop.cpp | 8 ++-- src/effects/Pixelate.cpp | 8 ++-- tests/Blur.cpp | 60 ++++++++++++++++++++++++++ tests/CMakeLists.txt | 1 + 8 files changed, 177 insertions(+), 23 deletions(-) create mode 100644 tests/Blur.cpp diff --git a/src/effects/Bars.cpp b/src/effects/Bars.cpp index da5b7036..00f5a8ce 100644 --- a/src/effects/Bars.cpp +++ b/src/effects/Bars.cpp @@ -167,10 +167,10 @@ std::string Bars::PropertiesJSON(int64_t requested_frame) const { root["color"]["red"] = add_property_json("Red", color.red.GetValue(requested_frame), "float", "", &color.red, 0, 255, false, requested_frame); root["color"]["blue"] = add_property_json("Blue", color.blue.GetValue(requested_frame), "float", "", &color.blue, 0, 255, false, requested_frame); root["color"]["green"] = add_property_json("Green", color.green.GetValue(requested_frame), "float", "", &color.green, 0, 255, false, requested_frame); - root["left"] = add_property_json("Left Size", left.GetValue(requested_frame), "float", "", &left, 0.0, 0.5, false, requested_frame); - root["top"] = add_property_json("Top Size", top.GetValue(requested_frame), "float", "", &top, 0.0, 0.5, false, requested_frame); - root["right"] = add_property_json("Right Size", right.GetValue(requested_frame), "float", "", &right, 0.0, 0.5, false, requested_frame); - root["bottom"] = add_property_json("Bottom Size", bottom.GetValue(requested_frame), "float", "", &bottom, 0.0, 0.5, false, requested_frame); + root["left"] = add_property_json("Margin: Left", left.GetValue(requested_frame), "float", "", &left, 0.0, 0.5, false, requested_frame); + root["top"] = add_property_json("Margin: Top", top.GetValue(requested_frame), "float", "", &top, 0.0, 0.5, false, requested_frame); + root["right"] = add_property_json("Margin: Right", right.GetValue(requested_frame), "float", "", &right, 0.0, 0.5, false, requested_frame); + root["bottom"] = add_property_json("Margin: Bottom", bottom.GetValue(requested_frame), "float", "", &bottom, 0.0, 0.5, false, requested_frame); // Return formatted string return root.toStyledString(); diff --git a/src/effects/Blur.cpp b/src/effects/Blur.cpp index 5d058c1a..65c53f56 100644 --- a/src/effects/Blur.cpp +++ b/src/effects/Blur.cpp @@ -13,10 +13,28 @@ #include "Blur.h" #include "Exceptions.h" +#include +#include +#include +#include + +#include + using namespace openshot; +namespace { +double clamp_margin(double value) { + if (value < 0.0) + return 0.0; + if (value > 1.0) + return 1.0; + return value; +} +} + /// Blank constructor, useful when using Json to load the effect properties Blur::Blur() : horizontal_radius(6.0), vertical_radius(6.0), sigma(3.0), iterations(3.0), + left(0.0), top(0.0), right(0.0), bottom(0.0), mask_mode(BLUR_MASK_POST_BLEND) { // Init effect properties init_effect_details(); @@ -25,7 +43,21 @@ Blur::Blur() : horizontal_radius(6.0), vertical_radius(6.0), sigma(3.0), iterati // Default constructor Blur::Blur(Keyframe new_horizontal_radius, Keyframe new_vertical_radius, Keyframe new_sigma, Keyframe new_iterations) : horizontal_radius(new_horizontal_radius), vertical_radius(new_vertical_radius), - sigma(new_sigma), iterations(new_iterations), mask_mode(BLUR_MASK_POST_BLEND) + sigma(new_sigma), iterations(new_iterations), + left(0.0), top(0.0), right(0.0), bottom(0.0), + mask_mode(BLUR_MASK_POST_BLEND) +{ + // Init effect properties + init_effect_details(); +} + +// Default constructor +Blur::Blur(Keyframe new_horizontal_radius, Keyframe new_vertical_radius, Keyframe new_sigma, Keyframe new_iterations, + Keyframe new_left, Keyframe new_top, Keyframe new_right, Keyframe new_bottom) : + horizontal_radius(new_horizontal_radius), vertical_radius(new_vertical_radius), + sigma(new_sigma), iterations(new_iterations), + left(new_left), top(new_top), right(new_right), bottom(new_bottom), + mask_mode(BLUR_MASK_POST_BLEND) { // Init effect properties init_effect_details(); @@ -61,33 +93,61 @@ std::shared_ptr Blur::GetFrame(std::shared_ptr int w = frame_image->width(); int h = frame_image->height(); + if (w <= 0 || h <= 0 || iteration_value <= 0) + return frame; + + // Define area we're working on in terms of a QRect with QMargins applied. + QRect area(QPoint(0, 0), frame_image->size()); + area = area.marginsRemoved({ + int(clamp_margin(left.GetValue(frame_number)) * w), + int(clamp_margin(top.GetValue(frame_number)) * h), + int(clamp_margin(right.GetValue(frame_number)) * w), + int(clamp_margin(bottom.GetValue(frame_number)) * h) + }); + area = area.intersected(QRect(QPoint(0, 0), frame_image->size())); + if (area.isEmpty()) + return frame; // Grab two copies of the image pixel data - QImage image_copy = frame_image->copy(); + QImage image_copy = frame_image->copy(area); + std::shared_ptr blur_image = std::make_shared(image_copy); std::shared_ptr frame_image_2 = std::make_shared(image_copy); + const int area_w = blur_image->width(); + const int area_h = blur_image->height(); + const int horizontal_area_radius = std::min(std::max(0, horizontal_radius_value), std::max(0, area_w - 1)); + const int vertical_area_radius = std::min(std::max(0, vertical_radius_value), std::max(0, area_h - 1)); + bool blurred = false; // Loop through each iteration for (int iteration = 0; iteration < iteration_value; ++iteration) { // HORIZONTAL BLUR (if any) - if (horizontal_radius_value > 0.0) { + if (horizontal_area_radius > 0.0) { // Apply horizontal blur to target RGBA channels - boxBlurH(frame_image->bits(), frame_image_2->bits(), w, h, horizontal_radius_value); + boxBlurH(blur_image->bits(), frame_image_2->bits(), area_w, area_h, horizontal_area_radius); // Swap output image back to input - frame_image.swap(frame_image_2); + blur_image.swap(frame_image_2); + blurred = true; } // VERTICAL BLUR (if any) - if (vertical_radius_value > 0.0) { + if (vertical_area_radius > 0.0) { // Apply vertical blur to target RGBA channels - boxBlurT(frame_image->bits(), frame_image_2->bits(), w, h, vertical_radius_value); + boxBlurT(blur_image->bits(), frame_image_2->bits(), area_w, area_h, vertical_area_radius); // Swap output image back to input - frame_image.swap(frame_image_2); + blur_image.swap(frame_image_2); + blurred = true; } } + if (blurred) { + QPainter painter(frame_image.get()); + painter.drawImage(area, *blur_image); + painter.end(); + } + // return the modified frame return frame; } @@ -249,6 +309,10 @@ Json::Value Blur::JsonValue() const { root["vertical_radius"] = vertical_radius.JsonValue(); root["sigma"] = sigma.JsonValue(); root["iterations"] = iterations.JsonValue(); + root["left"] = left.JsonValue(); + root["top"] = top.JsonValue(); + root["right"] = right.JsonValue(); + root["bottom"] = bottom.JsonValue(); root["mask_mode"] = mask_mode; // return JsonValue @@ -287,6 +351,14 @@ void Blur::SetJsonValue(const Json::Value root) { sigma.SetJsonValue(root["sigma"]); if (!root["iterations"].isNull()) iterations.SetJsonValue(root["iterations"]); + if (!root["left"].isNull()) + left.SetJsonValue(root["left"]); + if (!root["top"].isNull()) + top.SetJsonValue(root["top"]); + if (!root["right"].isNull()) + right.SetJsonValue(root["right"]); + if (!root["bottom"].isNull()) + bottom.SetJsonValue(root["bottom"]); if (!root["mask_mode"].isNull()) mask_mode = root["mask_mode"].asInt(); } @@ -302,6 +374,10 @@ std::string Blur::PropertiesJSON(int64_t requested_frame) const { root["vertical_radius"] = add_property_json("Vertical Radius", vertical_radius.GetValue(requested_frame), "float", "", &vertical_radius, 0, 100, false, requested_frame); root["sigma"] = add_property_json("Sigma", sigma.GetValue(requested_frame), "float", "", &sigma, 0, 100, false, requested_frame); root["iterations"] = add_property_json("Iterations", iterations.GetValue(requested_frame), "float", "", &iterations, 0, 100, false, requested_frame); + root["left"] = add_property_json("Margin: Left", left.GetValue(requested_frame), "float", "", &left, 0.0, 1.0, false, requested_frame); + root["top"] = add_property_json("Margin: Top", top.GetValue(requested_frame), "float", "", &top, 0.0, 1.0, false, requested_frame); + root["right"] = add_property_json("Margin: Right", right.GetValue(requested_frame), "float", "", &right, 0.0, 1.0, false, requested_frame); + root["bottom"] = add_property_json("Margin: Bottom", bottom.GetValue(requested_frame), "float", "", &bottom, 0.0, 1.0, false, requested_frame); root["mask_mode"] = add_property_json("Mask Mode", mask_mode, "int", "", NULL, 0, 1, false, requested_frame); root["mask_mode"]["choices"].append(add_property_choice_json("Limit to Mask", BLUR_MASK_POST_BLEND, mask_mode)); root["mask_mode"]["choices"].append(add_property_choice_json("Vary Strength", BLUR_MASK_DRIVE_AMOUNT, mask_mode)); diff --git a/src/effects/Blur.h b/src/effects/Blur.h index 7cf80bfc..d9ac15b5 100644 --- a/src/effects/Blur.h +++ b/src/effects/Blur.h @@ -57,6 +57,10 @@ namespace openshot Keyframe vertical_radius; ///< Vertical blur radius keyframe. The size of the vertical blur operation in pixels. Keyframe sigma; ///< Sigma keyframe. The amount of spread in the blur operation. Should be larger than radius. Keyframe iterations; ///< Iterations keyframe. The # of blur iterations per pixel. 3 iterations = Gaussian. + Keyframe left; ///< Size of left margin + Keyframe top; ///< Size of top margin + Keyframe right; ///< Size of right margin + Keyframe bottom; ///< Size of bottom margin int mask_mode; ///< How to apply common masks to blur (post-blend or drive-amount). /// Blank constructor, useful when using Json to load the effect properties @@ -71,6 +75,19 @@ namespace openshot /// @param new_iterations The curve to adjust the # of iterations (between 1 and 100) Blur(Keyframe new_horizontal_radius, Keyframe new_vertical_radius, Keyframe new_sigma, Keyframe new_iterations); + /// Default constructor, which takes blur curves and an affected area. + /// + /// @param new_horizontal_radius The curve to adjust the horizontal blur radius (between 0 and 100, rounded to int) + /// @param new_vertical_radius The curve to adjust the vertical blur radius (between 0 and 100, rounded to int) + /// @param new_sigma The curve to adjust the sigma amount (the size of the blur brush (between 0 and 100), float values accepted) + /// @param new_iterations The curve to adjust the # of iterations (between 1 and 100) + /// @param new_left The curve to adjust the left margin size (between 0 and 1) + /// @param new_top The curve to adjust the top margin size (between 0 and 1) + /// @param new_right The curve to adjust the right margin size (between 0 and 1) + /// @param new_bottom The curve to adjust the bottom margin size (between 0 and 1) + Blur(Keyframe new_horizontal_radius, Keyframe new_vertical_radius, Keyframe new_sigma, Keyframe new_iterations, + Keyframe new_left, Keyframe new_top, Keyframe new_right, Keyframe new_bottom); + /// @brief This method is required for all derived classes of ClipBase, and returns a /// new openshot::Frame object. All Clip keyframes and effects are resolved into /// pixels. diff --git a/src/effects/Caption.cpp b/src/effects/Caption.cpp index 3a5ef583..2d38b0e8 100644 --- a/src/effects/Caption.cpp +++ b/src/effects/Caption.cpp @@ -494,9 +494,9 @@ std::string Caption::PropertiesJSON(int64_t requested_frame) const { root["fade_in"] = add_property_json("Fade In (Seconds)", fade_in.GetValue(requested_frame), "float", "", &fade_in, 0.0, 3.0, false, requested_frame); root["fade_out"] = add_property_json("Fade Out (Seconds)", fade_out.GetValue(requested_frame), "float", "", &fade_out, 0.0, 3.0, false, requested_frame); root["line_spacing"] = add_property_json("Line Spacing", line_spacing.GetValue(requested_frame), "float", "", &line_spacing, 0.0, 5.0, false, requested_frame); - root["left"] = add_property_json("Left Size", left.GetValue(requested_frame), "float", "", &left, 0.0, 0.5, false, requested_frame); - root["top"] = add_property_json("Top Size", top.GetValue(requested_frame), "float", "", &top, 0.0, 1.0, false, requested_frame); - root["right"] = add_property_json("Right Size", right.GetValue(requested_frame), "float", "", &right, 0.0, 0.5, false, requested_frame); + root["left"] = add_property_json("Margin: Left", left.GetValue(requested_frame), "float", "", &left, 0.0, 0.5, false, requested_frame); + root["top"] = add_property_json("Margin: Top", top.GetValue(requested_frame), "float", "", &top, 0.0, 1.0, false, requested_frame); + root["right"] = add_property_json("Margin: Right", right.GetValue(requested_frame), "float", "", &right, 0.0, 0.5, false, requested_frame); root["caption_text"] = add_property_json("Captions", 0.0, "caption", caption_text, NULL, -1, -1, false, requested_frame); root["caption_font"] = add_property_json("Font", 0.0, "font", font_name, NULL, -1, -1, false, requested_frame); diff --git a/src/effects/Crop.cpp b/src/effects/Crop.cpp index aada4968..cb5e1f10 100644 --- a/src/effects/Crop.cpp +++ b/src/effects/Crop.cpp @@ -187,10 +187,10 @@ std::string Crop::PropertiesJSON(int64_t requested_frame) const { Json::Value root = BasePropertiesJSON(requested_frame); // Keyframes - root["left"] = add_property_json("Left Size", left.GetValue(requested_frame), "float", "", &left, 0.0, 1.0, false, requested_frame); - root["top"] = add_property_json("Top Size", top.GetValue(requested_frame), "float", "", &top, 0.0, 1.0, false, requested_frame); - root["right"] = add_property_json("Right Size", right.GetValue(requested_frame), "float", "", &right, 0.0, 1.0, false, requested_frame); - root["bottom"] = add_property_json("Bottom Size", bottom.GetValue(requested_frame), "float", "", &bottom, 0.0, 1.0, false, requested_frame); + root["left"] = add_property_json("Margin: Left", left.GetValue(requested_frame), "float", "", &left, 0.0, 1.0, false, requested_frame); + root["top"] = add_property_json("Margin: Top", top.GetValue(requested_frame), "float", "", &top, 0.0, 1.0, false, requested_frame); + root["right"] = add_property_json("Margin: Right", right.GetValue(requested_frame), "float", "", &right, 0.0, 1.0, false, requested_frame); + root["bottom"] = add_property_json("Margin: Bottom", bottom.GetValue(requested_frame), "float", "", &bottom, 0.0, 1.0, false, requested_frame); root["x"] = add_property_json("X Offset", x.GetValue(requested_frame), "float", "", &x, -1.0, 1.0, false, requested_frame); root["y"] = add_property_json("Y Offset", y.GetValue(requested_frame), "float", "", &y, -1.0, 1.0, false, requested_frame); diff --git a/src/effects/Pixelate.cpp b/src/effects/Pixelate.cpp index 9c3cf9cc..f145ccd9 100644 --- a/src/effects/Pixelate.cpp +++ b/src/effects/Pixelate.cpp @@ -198,10 +198,10 @@ std::string Pixelate::PropertiesJSON(int64_t requested_frame) const { // Keyframes root["pixelization"] = add_property_json("Pixelization", pixelization.GetValue(requested_frame), "float", "", &pixelization, 0.0, 0.9999, false, requested_frame); - root["left"] = add_property_json("Left Margin", left.GetValue(requested_frame), "float", "", &left, 0.0, 1.0, false, requested_frame); - root["top"] = add_property_json("Top Margin", top.GetValue(requested_frame), "float", "", &top, 0.0, 1.0, false, requested_frame); - root["right"] = add_property_json("Right Margin", right.GetValue(requested_frame), "float", "", &right, 0.0, 1.0, false, requested_frame); - root["bottom"] = add_property_json("Bottom Margin", bottom.GetValue(requested_frame), "float", "", &bottom, 0.0, 1.0, false, requested_frame); + root["left"] = add_property_json("Margin: Left", left.GetValue(requested_frame), "float", "", &left, 0.0, 1.0, false, requested_frame); + root["top"] = add_property_json("Margin: Top", top.GetValue(requested_frame), "float", "", &top, 0.0, 1.0, false, requested_frame); + root["right"] = add_property_json("Margin: Right", right.GetValue(requested_frame), "float", "", &right, 0.0, 1.0, false, requested_frame); + root["bottom"] = add_property_json("Margin: Bottom", bottom.GetValue(requested_frame), "float", "", &bottom, 0.0, 1.0, false, requested_frame); root["mask_mode"] = add_property_json("Mask Mode", mask_mode, "int", "", NULL, 0, 1, false, requested_frame); root["mask_mode"]["choices"].append(add_property_choice_json("Limit to Mask", PIXELATE_MASK_LIMIT_TO_AREA, mask_mode)); root["mask_mode"]["choices"].append(add_property_choice_json("Vary Strength", PIXELATE_MASK_VARY_STRENGTH, mask_mode)); diff --git a/tests/Blur.cpp b/tests/Blur.cpp new file mode 100644 index 00000000..3edf708c --- /dev/null +++ b/tests/Blur.cpp @@ -0,0 +1,60 @@ +/** + * @file + * @brief Unit tests for Blur effect + * + * @ref License + */ + +// Copyright (c) 2008-2026 OpenShot Studios, LLC +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "openshot_catch.h" + +#include "Frame.h" +#include "effects/Blur.h" + +#include + +using namespace openshot; + +TEST_CASE("Blur margins limit affected area", "[effect][blur]") { + auto frame = std::make_shared(1, 8, 1, "#000000"); + auto image = frame->GetImage(); + + for (int x = 0; x < image->width(); ++x) + image->setPixelColor(x, 0, (x % 2 == 0) ? QColor(255, 255, 255, 255) : QColor(0, 0, 0, 255)); + + Blur effect(Keyframe(1.0), Keyframe(0.0), Keyframe(3.0), Keyframe(1.0), + Keyframe(0.5), Keyframe(0.0), Keyframe(0.0), Keyframe(0.0)); + auto out = effect.GetFrame(frame, 1); + auto out_image = out->GetImage(); + + CHECK(out_image->pixelColor(0, 0) == QColor(255, 255, 255, 255)); + CHECK(out_image->pixelColor(1, 0) == QColor(0, 0, 0, 255)); + CHECK(out_image->pixelColor(2, 0) == QColor(255, 255, 255, 255)); + CHECK(out_image->pixelColor(3, 0) == QColor(0, 0, 0, 255)); + + CHECK(out_image->pixelColor(4, 0).red() < 255); + CHECK(out_image->pixelColor(4, 0).red() > 0); + CHECK(out_image->pixelColor(5, 0).red() > 0); +} + +TEST_CASE("Blur margin properties serialize", "[effect][blur][json]") { + Blur effect(Keyframe(6.0), Keyframe(6.0), Keyframe(3.0), Keyframe(3.0), + Keyframe(0.1), Keyframe(0.2), Keyframe(0.3), Keyframe(0.4)); + + Json::Value json = effect.JsonValue(); + REQUIRE(json["left"].isObject()); + REQUIRE(json["top"].isObject()); + REQUIRE(json["right"].isObject()); + REQUIRE(json["bottom"].isObject()); + + Blur copy; + copy.SetJsonValue(json); + + CHECK(copy.left.GetValue(1) == Approx(0.1)); + CHECK(copy.top.GetValue(1) == Approx(0.2)); + CHECK(copy.right.GetValue(1) == Approx(0.3)); + CHECK(copy.bottom.GetValue(1) == Approx(0.4)); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9edfb66c..51f5d795 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -54,6 +54,7 @@ set(OPENSHOT_TESTS # Effects AudioVisualization BeatSync + Blur ColorGrade ColorMap ChromaKey