Improve Caption margins and default visibility

- Add Bottom margin support to Caption JSON, properties, rendering, and tests
- Rename Caption margin property labels to "Margin: Top/Left/Bottom/Right"
- Clip Caption rendering to the configured margin rectangle
- Default Caption fade in/out to 0.0 so newly inserted captions render immediately
- Avoid divide-by-zero fade calculations when Caption fade durations are disabled
This commit is contained in:
Jonathan Thomas
2026-05-09 16:27:10 -05:00
parent ec71b7335c
commit 07e60a8a60
3 changed files with 43 additions and 11 deletions
+26 -9
View File
@@ -27,9 +27,9 @@
using namespace openshot;
/// Blank constructor, useful when using Json to load the effect properties
Caption::Caption() : color("#ffffff"), stroke("#a9a9a9"), background("#ff000000"), background_alpha(0.0), left(0.1), top(0.75), right(0.1),
Caption::Caption() : color("#ffffff"), stroke("#a9a9a9"), background("#ff000000"), background_alpha(0.0), left(0.1), top(0.75), right(0.1), bottom(0.0),
stroke_width(0.5), font_size(30.0), font_alpha(1.0), is_dirty(true), font_name("sans"), font(NULL), metrics(NULL),
fade_in(0.35), fade_out(0.35), background_corner(10.0), background_padding(20.0), line_spacing(1.0)
fade_in(0.0), fade_out(0.0), background_corner(10.0), background_padding(20.0), line_spacing(1.0)
{
// Init effect properties
init_effect_details();
@@ -37,9 +37,9 @@ Caption::Caption() : color("#ffffff"), stroke("#a9a9a9"), background("#ff000000"
// Default constructor
Caption::Caption(std::string captions) :
color("#ffffff"), stroke("#a9a9a9"), background("#ff000000"), background_alpha(0.0), left(0.1), top(0.75), right(0.1),
color("#ffffff"), stroke("#a9a9a9"), background("#ff000000"), background_alpha(0.0), left(0.1), top(0.75), right(0.1), bottom(0.0),
stroke_width(0.5), font_size(30.0), font_alpha(1.0), is_dirty(true), font_name("sans"), font(NULL), metrics(NULL),
fade_in(0.35), fade_out(0.35), background_corner(10.0), background_padding(20.0), line_spacing(1.0),
fade_in(0.0), fade_out(0.0), background_corner(10.0), background_padding(20.0), line_spacing(1.0),
caption_text(captions)
{
// Init effect properties
@@ -162,22 +162,28 @@ std::shared_ptr<openshot::Frame> Caption::GetFrame(std::shared_ptr<openshot::Fra
double fade_in_value = fade_in.GetValue(frame_number) * fps.ToDouble();
double fade_out_value = fade_out.GetValue(frame_number) * fps.ToDouble();
double right_value = right.GetValue(frame_number);
double bottom_value = bottom.GetValue(frame_number);
double background_corner_value = background_corner.GetValue(frame_number) * timeline_scale_factor;
double padding_value = background_padding.GetValue(frame_number) * timeline_scale_factor;
double stroke_width_value = stroke_width.GetValue(frame_number) * timeline_scale_factor;
double line_spacing_value = line_spacing.GetValue(frame_number);
double metrics_line_spacing = metrics.lineSpacing();
// Calculate caption area (based on left, top, and right margin)
// Calculate caption area (based on left, top, right, and bottom margin)
double left_margin_x = frame_image->width() * left_value;
double starting_y = (frame_image->height() * top_value) + metrics_line_spacing;
double top_margin_y = frame_image->height() * top_value;
double starting_y = top_margin_y + metrics_line_spacing;
double current_y = starting_y;
double bottom_y = starting_y;
double top_y = starting_y;
double max_text_width = 0.0;
double right_margin_x = frame_image->width() - (frame_image->width() * right_value);
double caption_area_width = right_margin_x - left_margin_x;
QRectF caption_area = QRectF(left_margin_x, starting_y, caption_area_width, frame_image->height());
double bottom_margin_y = frame_image->height() - (frame_image->height() * bottom_value);
double caption_area_height = std::max(bottom_margin_y - starting_y, 0.0);
QRectF caption_area = QRectF(left_margin_x, starting_y, caption_area_width, caption_area_height);
QRectF caption_clip_area = QRectF(left_margin_x, top_margin_y, caption_area_width,
std::max(bottom_margin_y - top_margin_y, 0.0));
// Keep track of all required text paths
std::vector<QPainterPath> text_paths;
@@ -220,8 +226,12 @@ std::shared_ptr<openshot::Frame> Caption::GetFrame(std::shared_ptr<openshot::Fra
!line.isEmpty() && frame_number >= start_frame && frame_number <= end_frame && line.length() > 1) {
// Calculate fade in/out ranges
fade_in_percentage = ((float) frame_number - (float) start_frame) / fade_in_value;
fade_out_percentage = 1.0 - (((float) frame_number - ((float) end_frame - fade_out_value)) / fade_out_value);
fade_in_percentage = fade_in_value > 0.0
? ((float) frame_number - (float) start_frame) / fade_in_value
: 1.0;
fade_out_percentage = fade_out_value > 0.0
? 1.0 - (((float) frame_number - ((float) end_frame - fade_out_value)) / fade_out_value)
: -1.0;
// Loop through words, and find word-wrap boundaries
QStringList words = line.split(" ");
@@ -317,6 +327,8 @@ std::shared_ptr<openshot::Frame> Caption::GetFrame(std::shared_ptr<openshot::Fra
background_brush.setStyle(Qt::SolidPattern);
painter.setBrush(background_brush);
painter.setPen(Qt::NoPen);
painter.save();
painter.setClipRect(caption_clip_area);
painter.drawRoundedRect(caption_area_with_padding, background_corner_value, background_corner_value);
// Set fill-color of text
@@ -360,6 +372,7 @@ std::shared_ptr<openshot::Frame> Caption::GetFrame(std::shared_ptr<openshot::Fra
painter.setBrush(font_brush);
painter.drawPath(path);
}
painter.restore();
// End painter
painter.end();
@@ -396,6 +409,7 @@ Json::Value Caption::JsonValue() const {
root["left"] = left.JsonValue();
root["top"] = top.JsonValue();
root["right"] = right.JsonValue();
root["bottom"] = bottom.JsonValue();
root["caption_text"] = caption_text;
root["caption_font"] = font_name;
@@ -457,6 +471,8 @@ void Caption::SetJsonValue(const Json::Value root) {
top.SetJsonValue(root["top"]);
if (!root["right"].isNull())
right.SetJsonValue(root["right"]);
if (!root["bottom"].isNull())
bottom.SetJsonValue(root["bottom"]);
if (!root["caption_text"].isNull())
caption_text = root["caption_text"].asString();
if (!root["caption_font"].isNull())
@@ -497,6 +513,7 @@ std::string Caption::PropertiesJSON(int64_t requested_frame) const {
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["bottom"] = add_property_json("Margin: Bottom", bottom.GetValue(requested_frame), "float", "", &bottom, 0.0, 1.0, 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);
+1
View File
@@ -65,6 +65,7 @@ public:
Keyframe left; ///< Size of left bar
Keyframe top; ///< Size of top bar
Keyframe right; ///< Size of right bar
Keyframe bottom; ///< Size of bottom bar
Keyframe fade_in; ///< Fade in per caption (# of seconds)
Keyframe fade_out; ///< Fade in per caption (# of seconds)
std::string font_name; ///< Font string
+16 -2
View File
@@ -82,17 +82,31 @@ TEST_CASE("caption effect", "[libopenshot][caption]") {
CHECK(c1.left.GetValue(1) == Approx(0.10f).margin(0.00001));
CHECK(c1.right.GetValue(1) == Approx(0.10f).margin(0.00001));
CHECK(c1.top.GetValue(1) == Approx(0.75).margin(0.00001));
CHECK(c1.bottom.GetValue(1) == Approx(0.0).margin(0.00001));
CHECK(c1.stroke_width.GetValue(1) == Approx(0.5f).margin(0.00001));
CHECK(c1.font_size.GetValue(1) == Approx(30.0f).margin(0.00001));
CHECK(c1.font_alpha.GetValue(1) == Approx(1.0f).margin(0.00001));
CHECK(c1.font_name == "sans");
CHECK(c1.fade_in.GetValue(1) == Approx(0.35f).margin(0.00001));
CHECK(c1.fade_out.GetValue(1) == Approx(0.35f).margin(0.00001));
CHECK(c1.fade_in.GetValue(1) == Approx(0.0f).margin(0.00001));
CHECK(c1.fade_out.GetValue(1) == Approx(0.0f).margin(0.00001));
CHECK(c1.background_corner.GetValue(1) == Approx(10.0f).margin(0.00001));
CHECK(c1.background_padding.GetValue(1) == Approx(20.0f).margin(0.00001));
CHECK(c1.line_spacing.GetValue(1) == Approx(1.0f).margin(0.00001));
CHECK(c1.CaptionText() == "00:00:00:000 --> 00:10:00:000\nEdit this caption with our caption editor");
Json::Value caption_json = c1.JsonValue();
REQUIRE(caption_json["bottom"].isObject());
caption_json["bottom"] = openshot::Keyframe(0.25).JsonValue();
openshot::Caption c2;
c2.SetJsonValue(caption_json);
CHECK(c2.bottom.GetValue(1) == Approx(0.25).margin(0.00001));
Json::Value properties = openshot::stringToJson(c1.PropertiesJSON(1));
CHECK(properties["top"]["name"].asString() == "Margin: Top");
CHECK(properties["left"]["name"].asString() == "Margin: Left");
CHECK(properties["bottom"]["name"].asString() == "Margin: Bottom");
CHECK(properties["right"]["name"].asString() == "Margin: Right");
// Load clip with video
std::stringstream path;
path << TEST_MEDIA_PATH << "sintel_trailer-720p.mp4";