Add Shadow and Glow effects with unit tests

- add new Shadow effect with soft drop shadow controls for color, opacity, blur, spread, distance, and angle
- add new Glow effect with outer/inner glow modes and controls for color, opacity, blur, and spread
- add focused unit tests covering shadow rendering, glow rendering, and JSON/property round-tripping
This commit is contained in:
Jonathan Thomas
2026-04-17 16:33:56 -05:00
parent a36dcb518b
commit 7af626a963
10 changed files with 891 additions and 0 deletions
+2
View File
@@ -118,6 +118,7 @@ set(EFFECTS_SOURCES
effects/CropHelpers.cpp
effects/Deinterlace.cpp
effects/Displace.cpp
effects/Glow.cpp
effects/Hue.cpp
effects/LensFlare.cpp
effects/AnalogTape.cpp
@@ -126,6 +127,7 @@ set(EFFECTS_SOURCES
effects/Pixelate.cpp
effects/Saturation.cpp
effects/Sharpen.cpp
effects/Shadow.cpp
effects/Shift.cpp
effects/SphericalProjection.cpp
effects/Wave.cpp
+8
View File
@@ -59,6 +59,9 @@ EffectBase* EffectInfo::CreateEffect(std::string effect_type) {
else if (effect_type == "Displace")
return new Displace();
else if (effect_type == "Glow")
return new Glow();
else if (effect_type == "Hue")
return new Hue();
@@ -80,6 +83,9 @@ EffectBase* EffectInfo::CreateEffect(std::string effect_type) {
else if (effect_type == "Sharpen")
return new Sharpen();
else if (effect_type == "Shadow")
return new Shadow();
else if (effect_type == "Shift")
return new Shift();
@@ -151,6 +157,7 @@ Json::Value EffectInfo::JsonValue() {
root.append(Crop().JsonInfo());
root.append(Deinterlace().JsonInfo());
root.append(Displace().JsonInfo());
root.append(Glow().JsonInfo());
root.append(Hue().JsonInfo());
root.append(LensFlare().JsonInfo());
root.append(Mask().JsonInfo());
@@ -158,6 +165,7 @@ Json::Value EffectInfo::JsonValue() {
root.append(Pixelate().JsonInfo());
root.append(Saturation().JsonInfo());
root.append(Sharpen().JsonInfo());
root.append(Shadow().JsonInfo());
root.append(Shift().JsonInfo());
root.append(SphericalProjection().JsonInfo());
root.append(Wave().JsonInfo());
+2
View File
@@ -25,6 +25,7 @@
#include "effects/Crop.h"
#include "effects/Deinterlace.h"
#include "effects/Displace.h"
#include "effects/Glow.h"
#include "effects/Hue.h"
#include "effects/LensFlare.h"
#include "effects/Mask.h"
@@ -32,6 +33,7 @@
#include "effects/Pixelate.h"
#include "effects/Saturation.h"
#include "effects/Sharpen.h"
#include "effects/Shadow.h"
#include "effects/SphericalProjection.h"
#include "effects/Shift.h"
#include "effects/Wave.h"
+277
View File
@@ -0,0 +1,277 @@
/**
* @file
* @brief Source file for Glow effect class
* @author Jonathan Thomas <jonathan@openshot.org>
*
* @ref License
*/
// Copyright (c) 2008-2026 OpenShot Studios, LLC
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include "Glow.h"
#include "Exceptions.h"
#include <QPainter>
#include <cmath>
#include <vector>
#include <omp.h>
using namespace openshot;
namespace {
inline float glow_clampf(float value, float low, float high) {
if (value < low)
return low;
if (value > high)
return high;
return value;
}
inline void glow_blur_horizontal(const std::vector<float>& src, std::vector<float>& dst, int w, int h, int radius) {
if (radius <= 0) {
dst = src;
return;
}
const float inv = 1.0f / static_cast<float>((radius * 2) + 1);
#pragma omp parallel for schedule(static)
for (int y = 0; y < h; ++y) {
int ti = y * w;
int li = ti;
int ri = ti + radius;
float first = src[ti];
float last = src[ti + w - 1];
float value = static_cast<float>(radius + 1) * first;
for (int j = 0; j < radius; ++j)
value += src[ti + j];
for (int x = 0; x <= radius; ++x) {
value += src[ri++] - first;
dst[ti++] = value * inv;
}
for (int x = radius + 1; x < w - radius; ++x) {
value += src[ri++] - src[li++];
dst[ti++] = value * inv;
}
for (int x = w - radius; x < w; ++x) {
value += last - src[li++];
dst[ti++] = value * inv;
}
}
}
inline void glow_blur_vertical(const std::vector<float>& src, std::vector<float>& dst, int w, int h, int radius) {
if (radius <= 0) {
dst = src;
return;
}
const float inv = 1.0f / static_cast<float>((radius * 2) + 1);
#pragma omp parallel for schedule(static)
for (int x = 0; x < w; ++x) {
int ti = x;
int li = ti;
int ri = ti + (radius * w);
float first = src[ti];
float last = src[ti + (w * (h - 1))];
float value = static_cast<float>(radius + 1) * first;
for (int j = 0; j < radius; ++j)
value += src[ti + (j * w)];
for (int y = 0; y <= radius; ++y) {
value += src[ri] - first;
dst[ti] = value * inv;
ri += w;
ti += w;
}
for (int y = radius + 1; y < h - radius; ++y) {
value += src[ri] - src[li];
dst[ti] = value * inv;
li += w;
ri += w;
ti += w;
}
for (int y = h - radius; y < h; ++y) {
value += last - src[li];
dst[ti] = value * inv;
li += w;
ti += w;
}
}
}
inline void glow_apply_box_blur(std::vector<float>& mask, int w, int h, int radius, int iterations) {
if (radius <= 0 || iterations <= 0 || w <= 0 || h <= 0)
return;
std::vector<float> temp(mask.size());
for (int i = 0; i < iterations; ++i) {
glow_blur_horizontal(mask, temp, w, h, radius);
glow_blur_vertical(temp, mask, w, h, radius);
}
}
}
Glow::Glow()
: mode(GLOW_MODE_OUTER), opacity(0.45), blur_radius(20.0), spread(0.10), color("#fff4c2") {
init_effect_details();
}
Glow::Glow(Keyframe new_opacity, Keyframe new_blur_radius, Keyframe new_spread, Color new_color)
: mode(GLOW_MODE_OUTER), opacity(new_opacity), blur_radius(new_blur_radius), spread(new_spread), color(new_color) {
init_effect_details();
}
void Glow::init_effect_details()
{
InitEffectInfo();
info.class_name = "Glow";
info.name = "Glow";
info.description = "Add an outer or inner glow based on visible pixels.";
info.has_audio = false;
info.has_video = true;
}
std::shared_ptr<openshot::Frame> Glow::GetFrame(std::shared_ptr<openshot::Frame> frame, int64_t frame_number)
{
auto frame_image = frame->GetImage();
if (!frame_image || frame_image->isNull())
return frame;
const int w = frame_image->width();
const int h = frame_image->height();
if (w <= 0 || h <= 0)
return frame;
QImage source = frame_image->copy();
const unsigned char* source_pixels = reinterpret_cast<const unsigned char*>(source.constBits());
const float opacity_value = glow_clampf(static_cast<float>(opacity.GetValue(frame_number)), 0.0f, 1.0f);
const int blur_value = std::max(0, blur_radius.GetInt(frame_number));
const float spread_value = glow_clampf(static_cast<float>(spread.GetValue(frame_number)), 0.0f, 1.0f);
const std::vector<int> rgba = color.GetColorRGBA(frame_number);
const float color_r = static_cast<float>(rgba[0]);
const float color_g = static_cast<float>(rgba[1]);
const float color_b = static_cast<float>(rgba[2]);
const float color_a = static_cast<float>(rgba[3]) / 255.0f;
std::vector<float> alpha_mask(static_cast<size_t>(w * h));
const float spread_gamma = 1.0f - (spread_value * 0.85f);
#pragma omp parallel for schedule(static)
for (int i = 0; i < (w * h); ++i) {
const float alpha = static_cast<float>(source_pixels[(i * 4) + 3]) / 255.0f;
alpha_mask[i] = (alpha <= 0.0f) ? 0.0f : std::pow(alpha, std::max(0.05f, spread_gamma));
}
std::vector<float> blurred_mask = alpha_mask;
if (blur_value > 0)
glow_apply_box_blur(blurred_mask, w, h, blur_value, 3);
QImage glow_overlay(w, h, QImage::Format_RGBA8888_Premultiplied);
glow_overlay.fill(Qt::transparent);
unsigned char* glow_pixels = reinterpret_cast<unsigned char*>(glow_overlay.bits());
if (mode == GLOW_MODE_OUTER) {
#pragma omp parallel for schedule(static)
for (int i = 0; i < (w * h); ++i) {
const float overlay_alpha = glow_clampf(blurred_mask[i] * opacity_value * color_a, 0.0f, 1.0f);
const int idx = i * 4;
glow_pixels[idx + 0] = static_cast<unsigned char>(color_r * overlay_alpha);
glow_pixels[idx + 1] = static_cast<unsigned char>(color_g * overlay_alpha);
glow_pixels[idx + 2] = static_cast<unsigned char>(color_b * overlay_alpha);
glow_pixels[idx + 3] = static_cast<unsigned char>(255.0f * overlay_alpha);
}
}
else {
std::vector<float> inverse_mask(static_cast<size_t>(w * h));
#pragma omp parallel for schedule(static)
for (int i = 0; i < (w * h); ++i)
inverse_mask[i] = 1.0f - alpha_mask[i];
if (blur_value > 0)
glow_apply_box_blur(inverse_mask, w, h, blur_value, 3);
#pragma omp parallel for schedule(static)
for (int i = 0; i < (w * h); ++i) {
const float inner = glow_clampf(inverse_mask[i] * alpha_mask[i] * opacity_value * color_a, 0.0f, 1.0f);
const int idx = i * 4;
glow_pixels[idx + 0] = static_cast<unsigned char>(color_r * inner);
glow_pixels[idx + 1] = static_cast<unsigned char>(color_g * inner);
glow_pixels[idx + 2] = static_cast<unsigned char>(color_b * inner);
glow_pixels[idx + 3] = static_cast<unsigned char>(255.0f * inner);
}
}
QImage result(w, h, QImage::Format_RGBA8888_Premultiplied);
result.fill(Qt::transparent);
{
QPainter painter(&result);
painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform, true);
if (mode == GLOW_MODE_OUTER) {
painter.drawImage(0, 0, glow_overlay);
painter.drawImage(0, 0, source);
} else {
painter.drawImage(0, 0, source);
painter.drawImage(0, 0, glow_overlay);
}
}
*frame_image = result;
return frame;
}
std::string Glow::Json() const {
return JsonValue().toStyledString();
}
Json::Value Glow::JsonValue() const {
Json::Value root = EffectBase::JsonValue();
root["type"] = info.class_name;
root["mode"] = mode;
root["opacity"] = opacity.JsonValue();
root["blur_radius"] = blur_radius.JsonValue();
root["spread"] = spread.JsonValue();
root["color"] = color.JsonValue();
return root;
}
void Glow::SetJson(const std::string value) {
try {
const Json::Value root = openshot::stringToJson(value);
SetJsonValue(root);
}
catch (const std::exception& e) {
throw InvalidJSON("JSON is invalid (missing keys or invalid data types)");
}
}
void Glow::SetJsonValue(const Json::Value root) {
EffectBase::SetJsonValue(root);
if (!root["mode"].isNull())
mode = root["mode"].asInt();
if (!root["opacity"].isNull())
opacity.SetJsonValue(root["opacity"]);
if (!root["blur_radius"].isNull())
blur_radius.SetJsonValue(root["blur_radius"]);
if (!root["spread"].isNull())
spread.SetJsonValue(root["spread"]);
if (!root["color"].isNull())
color.SetJsonValue(root["color"]);
}
std::string Glow::PropertiesJSON(int64_t requested_frame) const {
Json::Value root = BasePropertiesJSON(requested_frame);
root["mode"] = add_property_json("Mode", mode, "int", "", NULL, 0, 1, false, requested_frame);
root["mode"]["choices"].append(add_property_choice_json("Outer", GLOW_MODE_OUTER, mode));
root["mode"]["choices"].append(add_property_choice_json("Inner", GLOW_MODE_INNER, mode));
root["opacity"] = add_property_json("Opacity", opacity.GetValue(requested_frame), "float", "", &opacity, 0.0, 1.0, false, requested_frame);
root["blur_radius"] = add_property_json("Blur Radius", blur_radius.GetValue(requested_frame), "float", "", &blur_radius, 0.0, 100.0, false, requested_frame);
root["spread"] = add_property_json("Spread", spread.GetValue(requested_frame), "float", "", &spread, 0.0, 1.0, false, requested_frame);
root["color"] = add_property_json("Color", 0.0, "color", "", &color.red, 0, 255, false, requested_frame);
root["color"]["red"] = add_property_json("Red", color.red.GetValue(requested_frame), "float", "", &color.red, 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["color"]["blue"] = add_property_json("Blue", color.blue.GetValue(requested_frame), "float", "", &color.blue, 0, 255, false, requested_frame);
root["color"]["alpha"] = add_property_json("Alpha", color.alpha.GetValue(requested_frame), "float", "", &color.alpha, 0, 255, false, requested_frame);
return root.toStyledString();
}
+63
View File
@@ -0,0 +1,63 @@
/**
* @file
* @brief Header file for Glow effect class
* @author Jonathan Thomas <jonathan@openshot.org>
*
* @ref License
*/
// Copyright (c) 2008-2026 OpenShot Studios, LLC
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#ifndef OPENSHOT_GLOW_EFFECT_H
#define OPENSHOT_GLOW_EFFECT_H
#include "../Color.h"
#include "../EffectBase.h"
#include "../Frame.h"
#include "../Json.h"
#include "../KeyFrame.h"
#include <memory>
#include <string>
namespace openshot
{
enum GlowMode {
GLOW_MODE_OUTER = 0,
GLOW_MODE_INNER = 1
};
class Glow : public EffectBase
{
private:
void init_effect_details();
public:
int mode; ///< Outer or inner glow mode.
Keyframe opacity; ///< Overall glow opacity.
Keyframe blur_radius; ///< Blur radius in pixels.
Keyframe spread; ///< Boosts the source alpha before blur.
Color color; ///< Glow tint color.
Glow();
Glow(Keyframe new_opacity, Keyframe new_blur_radius, Keyframe new_spread, Color new_color);
std::shared_ptr<openshot::Frame> GetFrame(int64_t frame_number) override {
return GetFrame(std::make_shared<openshot::Frame>(), frame_number);
}
std::shared_ptr<openshot::Frame> GetFrame(std::shared_ptr<openshot::Frame> frame,
int64_t frame_number) override;
std::string Json() const override;
void SetJson(const std::string value) override;
Json::Value JsonValue() const override;
void SetJsonValue(const Json::Value root) override;
std::string PropertiesJSON(int64_t requested_frame) const override;
};
}
#endif
+279
View File
@@ -0,0 +1,279 @@
/**
* @file
* @brief Source file for Shadow effect class
* @author Jonathan Thomas <jonathan@openshot.org>
*
* @ref License
*/
// Copyright (c) 2008-2026 OpenShot Studios, LLC
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include "Shadow.h"
#include "Exceptions.h"
#include <QPainter>
#include <cmath>
#include <vector>
#include <omp.h>
using namespace openshot;
namespace {
inline float shadow_clampf(float value, float low, float high) {
if (value < low)
return low;
if (value > high)
return high;
return value;
}
inline void shadow_blur_horizontal(const std::vector<float>& src, std::vector<float>& dst, int w, int h, int radius) {
if (radius <= 0) {
dst = src;
return;
}
const float inv = 1.0f / static_cast<float>((radius * 2) + 1);
#pragma omp parallel for schedule(static)
for (int y = 0; y < h; ++y) {
int ti = y * w;
int li = ti;
int ri = ti + radius;
float first = src[ti];
float last = src[ti + w - 1];
float value = static_cast<float>(radius + 1) * first;
for (int j = 0; j < radius; ++j)
value += src[ti + j];
for (int x = 0; x <= radius; ++x) {
value += src[ri++] - first;
dst[ti++] = value * inv;
}
for (int x = radius + 1; x < w - radius; ++x) {
value += src[ri++] - src[li++];
dst[ti++] = value * inv;
}
for (int x = w - radius; x < w; ++x) {
value += last - src[li++];
dst[ti++] = value * inv;
}
}
}
inline void shadow_blur_vertical(const std::vector<float>& src, std::vector<float>& dst, int w, int h, int radius) {
if (radius <= 0) {
dst = src;
return;
}
const float inv = 1.0f / static_cast<float>((radius * 2) + 1);
#pragma omp parallel for schedule(static)
for (int x = 0; x < w; ++x) {
int ti = x;
int li = ti;
int ri = ti + (radius * w);
float first = src[ti];
float last = src[ti + (w * (h - 1))];
float value = static_cast<float>(radius + 1) * first;
for (int j = 0; j < radius; ++j)
value += src[ti + (j * w)];
for (int y = 0; y <= radius; ++y) {
value += src[ri] - first;
dst[ti] = value * inv;
ri += w;
ti += w;
}
for (int y = radius + 1; y < h - radius; ++y) {
value += src[ri] - src[li];
dst[ti] = value * inv;
li += w;
ri += w;
ti += w;
}
for (int y = h - radius; y < h; ++y) {
value += last - src[li];
dst[ti] = value * inv;
li += w;
ti += w;
}
}
}
inline void shadow_apply_box_blur(std::vector<float>& mask, int w, int h, int radius, int iterations) {
if (radius <= 0 || iterations <= 0 || w <= 0 || h <= 0)
return;
std::vector<float> temp(mask.size());
for (int i = 0; i < iterations; ++i) {
shadow_blur_horizontal(mask, temp, w, h, radius);
shadow_blur_vertical(temp, mask, w, h, radius);
}
}
inline QImage make_shadow_overlay(const std::vector<float>& mask, int w, int h,
int offset_x, int offset_y,
const std::vector<int>& rgba, float opacity) {
QImage overlay(w, h, QImage::Format_RGBA8888_Premultiplied);
overlay.fill(Qt::transparent);
unsigned char* pixels = reinterpret_cast<unsigned char*>(overlay.bits());
const float base_r = static_cast<float>(rgba[0]);
const float base_g = static_cast<float>(rgba[1]);
const float base_b = static_cast<float>(rgba[2]);
const float base_a = static_cast<float>(rgba[3]) / 255.0f;
#pragma omp parallel for schedule(static)
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
const int src_x = x - offset_x;
const int src_y = y - offset_y;
if (src_x < 0 || src_x >= w || src_y < 0 || src_y >= h)
continue;
const float alpha = shadow_clampf(mask[(src_y * w) + src_x] * opacity * base_a, 0.0f, 1.0f);
if (alpha <= 0.0f)
continue;
const int idx = ((y * w) + x) * 4;
pixels[idx + 0] = static_cast<unsigned char>(base_r * alpha);
pixels[idx + 1] = static_cast<unsigned char>(base_g * alpha);
pixels[idx + 2] = static_cast<unsigned char>(base_b * alpha);
pixels[idx + 3] = static_cast<unsigned char>(255.0f * alpha);
}
}
return overlay;
}
}
Shadow::Shadow()
: opacity(0.6), blur_radius(18.0), spread(0.12), distance(10.0), angle(60.0), color("#000000") {
init_effect_details();
}
Shadow::Shadow(Keyframe new_opacity, Keyframe new_blur_radius, Keyframe new_spread,
Keyframe new_distance, Keyframe new_angle, Color new_color)
: opacity(new_opacity), blur_radius(new_blur_radius), spread(new_spread),
distance(new_distance), angle(new_angle), color(new_color) {
init_effect_details();
}
void Shadow::init_effect_details()
{
InitEffectInfo();
info.class_name = "Shadow";
info.name = "Shadow";
info.description = "Add a soft drop shadow based on visible pixels.";
info.has_audio = false;
info.has_video = true;
}
std::shared_ptr<openshot::Frame> Shadow::GetFrame(std::shared_ptr<openshot::Frame> frame, int64_t frame_number)
{
auto frame_image = frame->GetImage();
if (!frame_image || frame_image->isNull())
return frame;
const int w = frame_image->width();
const int h = frame_image->height();
if (w <= 0 || h <= 0)
return frame;
QImage source = frame_image->copy();
const unsigned char* source_pixels = reinterpret_cast<const unsigned char*>(source.constBits());
const float opacity_value = shadow_clampf(static_cast<float>(opacity.GetValue(frame_number)), 0.0f, 1.0f);
const int blur_value = std::max(0, blur_radius.GetInt(frame_number));
const float spread_value = shadow_clampf(static_cast<float>(spread.GetValue(frame_number)), 0.0f, 1.0f);
const float distance_value = static_cast<float>(distance.GetValue(frame_number));
const float angle_value = static_cast<float>(angle.GetValue(frame_number));
const std::vector<int> rgba = color.GetColorRGBA(frame_number);
std::vector<float> alpha_mask(static_cast<size_t>(w * h));
const float spread_gamma = 1.0f - (spread_value * 0.85f);
#pragma omp parallel for schedule(static)
for (int i = 0; i < (w * h); ++i) {
const float alpha = static_cast<float>(source_pixels[(i * 4) + 3]) / 255.0f;
alpha_mask[i] = (alpha <= 0.0f) ? 0.0f : std::pow(alpha, std::max(0.05f, spread_gamma));
}
if (blur_value > 0)
shadow_apply_box_blur(alpha_mask, w, h, blur_value, 3);
const float radians = angle_value * static_cast<float>(M_PI / 180.0);
const int shadow_x = static_cast<int>(std::lround(std::cos(radians) * distance_value));
const int shadow_y = static_cast<int>(std::lround(std::sin(radians) * distance_value));
QImage result(w, h, QImage::Format_RGBA8888_Premultiplied);
result.fill(Qt::transparent);
{
QPainter painter(&result);
painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform, true);
QImage overlay = make_shadow_overlay(alpha_mask, w, h, shadow_x, shadow_y, rgba, opacity_value);
painter.drawImage(0, 0, overlay);
painter.drawImage(0, 0, source);
}
*frame_image = result;
return frame;
}
std::string Shadow::Json() const {
return JsonValue().toStyledString();
}
Json::Value Shadow::JsonValue() const {
Json::Value root = EffectBase::JsonValue();
root["type"] = info.class_name;
root["opacity"] = opacity.JsonValue();
root["blur_radius"] = blur_radius.JsonValue();
root["spread"] = spread.JsonValue();
root["distance"] = distance.JsonValue();
root["angle"] = angle.JsonValue();
root["color"] = color.JsonValue();
return root;
}
void Shadow::SetJson(const std::string value) {
try {
const Json::Value root = openshot::stringToJson(value);
SetJsonValue(root);
}
catch (const std::exception& e) {
throw InvalidJSON("JSON is invalid (missing keys or invalid data types)");
}
}
void Shadow::SetJsonValue(const Json::Value root) {
EffectBase::SetJsonValue(root);
if (!root["opacity"].isNull())
opacity.SetJsonValue(root["opacity"]);
if (!root["blur_radius"].isNull())
blur_radius.SetJsonValue(root["blur_radius"]);
if (!root["spread"].isNull())
spread.SetJsonValue(root["spread"]);
if (!root["distance"].isNull())
distance.SetJsonValue(root["distance"]);
if (!root["angle"].isNull())
angle.SetJsonValue(root["angle"]);
if (!root["color"].isNull())
color.SetJsonValue(root["color"]);
}
std::string Shadow::PropertiesJSON(int64_t requested_frame) const {
Json::Value root = BasePropertiesJSON(requested_frame);
root["opacity"] = add_property_json("Opacity", opacity.GetValue(requested_frame), "float", "", &opacity, 0.0, 1.0, false, requested_frame);
root["blur_radius"] = add_property_json("Blur Radius", blur_radius.GetValue(requested_frame), "float", "", &blur_radius, 0.0, 100.0, false, requested_frame);
root["spread"] = add_property_json("Spread", spread.GetValue(requested_frame), "float", "", &spread, 0.0, 1.0, false, requested_frame);
root["distance"] = add_property_json("Distance", distance.GetValue(requested_frame), "float", "", &distance, -500.0, 500.0, false, requested_frame);
root["angle"] = add_property_json("Angle", angle.GetValue(requested_frame), "float", "", &angle, -360.0, 360.0, false, requested_frame);
root["color"] = add_property_json("Color", 0.0, "color", "", &color.red, 0, 255, false, requested_frame);
root["color"]["red"] = add_property_json("Red", color.red.GetValue(requested_frame), "float", "", &color.red, 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["color"]["blue"] = add_property_json("Blue", color.blue.GetValue(requested_frame), "float", "", &color.blue, 0, 255, false, requested_frame);
root["color"]["alpha"] = add_property_json("Alpha", color.alpha.GetValue(requested_frame), "float", "", &color.alpha, 0, 255, false, requested_frame);
return root.toStyledString();
}
+60
View File
@@ -0,0 +1,60 @@
/**
* @file
* @brief Header file for Shadow effect class
* @author Jonathan Thomas <jonathan@openshot.org>
*
* @ref License
*/
// Copyright (c) 2008-2026 OpenShot Studios, LLC
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#ifndef OPENSHOT_SHADOW_EFFECT_H
#define OPENSHOT_SHADOW_EFFECT_H
#include "../Color.h"
#include "../EffectBase.h"
#include "../Frame.h"
#include "../Json.h"
#include "../KeyFrame.h"
#include <memory>
#include <string>
namespace openshot
{
class Shadow : public EffectBase
{
private:
void init_effect_details();
public:
Keyframe opacity; ///< Overall shadow opacity.
Keyframe blur_radius; ///< Blur radius in pixels.
Keyframe spread; ///< Boosts the source alpha before blur.
Keyframe distance; ///< Shadow offset distance in pixels.
Keyframe angle; ///< Shadow angle in degrees.
Color color; ///< Shadow tint color.
Shadow();
Shadow(Keyframe new_opacity, Keyframe new_blur_radius, Keyframe new_spread,
Keyframe new_distance, Keyframe new_angle, Color new_color);
std::shared_ptr<openshot::Frame> GetFrame(int64_t frame_number) override {
return GetFrame(std::make_shared<openshot::Frame>(), frame_number);
}
std::shared_ptr<openshot::Frame> GetFrame(std::shared_ptr<openshot::Frame> frame,
int64_t frame_number) override;
std::string Json() const override;
void SetJson(const std::string value) override;
Json::Value JsonValue() const override;
void SetJsonValue(const Json::Value root) override;
std::string PropertiesJSON(int64_t requested_frame) const override;
};
}
#endif
+2
View File
@@ -54,12 +54,14 @@ set(OPENSHOT_TESTS
ChromaKey
Crop
Displace
Glow
LensFlare
AnalogTape
BenchmarkArgs
EffectMask
Mask
Sharpen
Shadow
SphericalEffect
WaveEffect
)
+115
View File
@@ -0,0 +1,115 @@
/**
* @file
* @brief Unit tests for Glow effect
*/
// Copyright (c) 2008-2026 OpenShot Studios, LLC
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include <memory>
#include <QColor>
#include <QImage>
#include "Frame.h"
#include "effects/Glow.h"
#include "openshot_catch.h"
using namespace openshot;
static std::ostream& operator<<(std::ostream& os, QColor const& c)
{
os << "QColor(" << c.red() << "," << c.green()
<< "," << c.blue() << "," << c.alpha() << ")";
return os;
}
static std::shared_ptr<Frame> makeSinglePixelFrame()
{
QImage img(5, 5, QImage::Format_ARGB32);
img.fill(QColor(0, 0, 0, 0));
img.setPixelColor(2, 2, QColor(255, 255, 255, 255));
auto frame = std::make_shared<Frame>();
*frame->GetImage() = img;
return frame;
}
static std::shared_ptr<Frame> makeSolidSquareFrame()
{
QImage img(5, 5, QImage::Format_ARGB32);
img.fill(QColor(0, 0, 0, 0));
for (int y = 1; y <= 3; ++y) {
for (int x = 1; x <= 3; ++x)
img.setPixelColor(x, y, QColor(255, 255, 255, 255));
}
auto frame = std::make_shared<Frame>();
*frame->GetImage() = img;
return frame;
}
TEST_CASE("Glow outer mode brightens transparent neighbors", "[effect][glow]")
{
Glow effect;
effect.mode = GLOW_MODE_OUTER;
effect.opacity = Keyframe(1.0);
effect.blur_radius = Keyframe(1.0);
effect.spread = Keyframe(0.0);
effect.color = Color("#ff0000");
auto frame = makeSinglePixelFrame();
const QColor before = frame->GetImage()->pixelColor(1, 2);
auto out = effect.GetFrame(frame, 1);
const QColor after = out->GetImage()->pixelColor(1, 2);
CHECK(before.alpha() == 0);
CHECK(after.alpha() > 0);
}
TEST_CASE("Glow inner mode tints inner edge without affecting outside", "[effect][glow][inner]")
{
Glow effect;
effect.mode = GLOW_MODE_INNER;
effect.opacity = Keyframe(1.0);
effect.blur_radius = Keyframe(1.0);
effect.spread = Keyframe(0.0);
effect.color = Color("#00ff00");
auto frame = makeSolidSquareFrame();
const QColor edge_before = frame->GetImage()->pixelColor(1, 2);
const QColor outside_before = frame->GetImage()->pixelColor(0, 2);
auto out = effect.GetFrame(frame, 1);
const QColor edge_after = out->GetImage()->pixelColor(1, 2);
const QColor outside_after = out->GetImage()->pixelColor(0, 2);
CHECK(edge_after != edge_before);
CHECK(outside_before.alpha() == 0);
CHECK(outside_after.alpha() == 0);
}
TEST_CASE("Glow json round-trip preserves mode and color", "[effect][glow][json]")
{
Glow effect;
effect.mode = GLOW_MODE_INNER;
effect.opacity = Keyframe(0.5);
effect.blur_radius = Keyframe(9.0);
effect.spread = Keyframe(0.25);
effect.color = Color("#204060");
const Json::Value json = effect.JsonValue();
CHECK(json["type"].asString() == "Glow");
Glow copy;
copy.SetJsonValue(json);
const Json::Value copy_json = copy.JsonValue();
CHECK(copy_json["mode"].asInt() == GLOW_MODE_INNER);
CHECK(copy_json["opacity"]["Points"][0]["co"]["Y"].asDouble() == Approx(0.5));
CHECK(copy_json["blur_radius"]["Points"][0]["co"]["Y"].asDouble() == Approx(9.0));
CHECK(copy_json["spread"]["Points"][0]["co"]["Y"].asDouble() == Approx(0.25));
CHECK(copy_json["color"]["red"]["Points"][0]["co"]["Y"].asDouble() == Approx(32.0));
CHECK(copy_json["color"]["green"]["Points"][0]["co"]["Y"].asDouble() == Approx(64.0));
CHECK(copy_json["color"]["blue"]["Points"][0]["co"]["Y"].asDouble() == Approx(96.0));
}
+83
View File
@@ -0,0 +1,83 @@
/**
* @file
* @brief Unit tests for Shadow effect
*/
// Copyright (c) 2008-2026 OpenShot Studios, LLC
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include <memory>
#include <QColor>
#include <QImage>
#include "Frame.h"
#include "effects/Shadow.h"
#include "openshot_catch.h"
using namespace openshot;
static std::ostream& operator<<(std::ostream& os, QColor const& c)
{
os << "QColor(" << c.red() << "," << c.green()
<< "," << c.blue() << "," << c.alpha() << ")";
return os;
}
static std::shared_ptr<Frame> makeSinglePixelFrame()
{
QImage img(5, 5, QImage::Format_ARGB32);
img.fill(QColor(0, 0, 0, 0));
img.setPixelColor(2, 2, QColor(255, 255, 255, 255));
auto frame = std::make_shared<Frame>();
*frame->GetImage() = img;
return frame;
}
TEST_CASE("Shadow offsets visible pixels into surrounding area", "[effect][shadow]")
{
Shadow effect;
effect.opacity = Keyframe(1.0);
effect.blur_radius = Keyframe(0.0);
effect.spread = Keyframe(0.0);
effect.distance = Keyframe(1.0);
effect.angle = Keyframe(0.0);
effect.color = Color("#000000");
auto frame = makeSinglePixelFrame();
const QColor before = frame->GetImage()->pixelColor(3, 2);
auto out = effect.GetFrame(frame, 1);
const QColor after = out->GetImage()->pixelColor(3, 2);
CHECK(before.alpha() == 0);
CHECK(after.alpha() > 0);
}
TEST_CASE("Shadow json round-trip preserves key properties", "[effect][shadow][json]")
{
Shadow effect;
effect.opacity = Keyframe(0.42);
effect.blur_radius = Keyframe(7.0);
effect.spread = Keyframe(0.33);
effect.distance = Keyframe(11.0);
effect.angle = Keyframe(60.0);
effect.color = Color("#102030");
const Json::Value json = effect.JsonValue();
CHECK(json["type"].asString() == "Shadow");
Shadow copy;
copy.SetJsonValue(json);
const Json::Value copy_json = copy.JsonValue();
CHECK(copy_json["opacity"]["Points"][0]["co"]["Y"].asDouble() == Approx(0.42));
CHECK(copy_json["blur_radius"]["Points"][0]["co"]["Y"].asDouble() == Approx(7.0));
CHECK(copy_json["spread"]["Points"][0]["co"]["Y"].asDouble() == Approx(0.33));
CHECK(copy_json["distance"]["Points"][0]["co"]["Y"].asDouble() == Approx(11.0));
CHECK(copy_json["angle"]["Points"][0]["co"]["Y"].asDouble() == Approx(60.0));
CHECK(copy_json["color"]["red"]["Points"][0]["co"]["Y"].asDouble() == Approx(16.0));
CHECK(copy_json["color"]["green"]["Points"][0]["co"]["Y"].asDouble() == Approx(32.0));
CHECK(copy_json["color"]["blue"]["Points"][0]["co"]["Y"].asDouble() == Approx(48.0));
}