You've already forked libopenshot
mirror of
https://github.com/OpenShot/libopenshot.git
synced 2026-06-08 22:17:28 -07:00
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:
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -54,12 +54,14 @@ set(OPENSHOT_TESTS
|
||||
ChromaKey
|
||||
Crop
|
||||
Displace
|
||||
Glow
|
||||
LensFlare
|
||||
AnalogTape
|
||||
BenchmarkArgs
|
||||
EffectMask
|
||||
Mask
|
||||
Sharpen
|
||||
Shadow
|
||||
SphericalEffect
|
||||
WaveEffect
|
||||
)
|
||||
|
||||
+115
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user