16#include "../Timeline.h"
27#include <QFontMetricsF>
30#include <QPainterPath>
39 int ClampGravity(
int gravity)
46 double TextAdvance(
const QFontMetricsF& metrics,
const QString& text)
48#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
49 return metrics.horizontalAdvance(text);
51 return metrics.width(text);
68 background(
"#000000"),
76 background_alpha(0.45),
77 background_padding(14.0),
78 background_corner(6.0)
80 init_effect_details();
83void Timer::init_effect_details()
88 info.
description =
"Render a styled count up, count down, clock, timecode, or frame number overlay.";
93double Timer::ResolveFps()
const
105 return clip->Reader()->info.fps.ToDouble();
107 return timeline->info.fps.ToDouble();
108 if (
clip != NULL &&
clip->Reader() != NULL &&
clip->Reader()->info.fps.ToDouble() > 0.0)
109 return clip->Reader()->info.fps.ToDouble();
113int64_t Timer::EffectiveFrameNumber(int64_t frame_number)
const
116 return std::max<int64_t>(1, frame_number);
119 if (
clip != NULL &&
clip->time.GetLength() > 1)
120 return std::max<int64_t>(1,
clip->time.GetLong(frame_number));
122 return std::max<int64_t>(1, frame_number);
125double Timer::CountdownDuration(int64_t frame_number)
const
128 if (configured_duration > 0.0)
129 return configured_duration;
138std::string Timer::FormatSeconds(
double seconds,
double fps,
bool duration_style)
const
140 if (duration_style &&
clamp && seconds < 0.0)
143 const bool negative = seconds < 0.0;
144 const int64_t total_milliseconds = std::llround(std::fabs(seconds) * 1000.0);
145 const int milliseconds =
static_cast<int>(total_milliseconds % 1000);
146 const int64_t total_seconds = total_milliseconds / 1000;
147 const int hours =
static_cast<int>(total_seconds / 3600);
148 const int minutes =
static_cast<int>((total_seconds / 60) % 60);
149 const int whole_seconds =
static_cast<int>(total_seconds % 60);
151 std::ostringstream text;
156 text << std::setfill(
'0') << std::setw(2) << hours <<
":"
157 << std::setw(2) << minutes <<
":"
158 << std::setw(2) << whole_seconds <<
"."
159 << std::setw(3) << milliseconds;
161 text << std::setfill(
'0') << std::setw(2) << hours <<
":"
162 << std::setw(2) << minutes <<
":"
163 << std::setw(2) << whole_seconds;
165 text << FormatTimecode(seconds, fps);
167 text << static_cast<int64_t>(std::llround(seconds * fps));
169 const int total_minutes = hours * 60 + minutes;
170 text << std::setfill(
'0') << std::setw(2) << total_minutes <<
":"
171 << std::setw(2) << whole_seconds;
177std::string Timer::FormatTimecode(
double seconds,
double fps)
const
179 const bool negative = seconds < 0.0;
180 const int64_t total_frames = std::llround(std::fabs(seconds) * fps);
181 const int64_t rounded_fps = std::max<int64_t>(1, std::llround(fps));
182 const int64_t frames = total_frames % rounded_fps;
183 const int64_t total_seconds = total_frames / rounded_fps;
184 const int64_t secs = total_seconds % 60;
185 const int64_t mins = (total_seconds / 60) % 60;
186 const int64_t hours = total_seconds / 3600;
188 std::ostringstream text;
191 text << std::setfill(
'0') << std::setw(2) << hours <<
":"
192 << std::setw(2) << mins <<
":"
193 << std::setw(2) << secs <<
":"
194 << std::setw(2) << frames;
200 const double fps = ResolveFps();
201 const int64_t effective_frame = EffectiveFrameNumber(frame_number);
202 const double elapsed =
static_cast<double>(effective_frame - 1) / fps;
212 const double fps = ResolveFps();
213 const int64_t effective_frame = EffectiveFrameNumber(frame_number);
214 std::ostringstream text;
219 text << (effective_frame + frame_offset);
221 text << FormatTimecode(
TimerSeconds(frame_number), fps);
223 double seconds = std::fmod(
TimerSeconds(frame_number), 24.0 * 60.0 * 60.0);
225 seconds += 24.0 * 60.0 * 60.0;
226 text << FormatSeconds(seconds, fps,
false);
228 text << FormatSeconds(
TimerSeconds(frame_number), fps,
true);
235std::string Timer::TimerLayoutText(int64_t frame_number)
const
240 std::string timer_template;
242 timer_template =
"88:88:88:88";
244 timer_template =
"88:88:88.888";
246 timer_template =
"88:88:88";
248 timer_template =
"88:88";
253std::shared_ptr<openshot::Frame>
Timer::GetFrame(std::shared_ptr<openshot::Frame> frame, int64_t frame_number)
257 QSize image_size(1280, 720);
265 if (timeline != NULL) {
267 }
else if (
clip != NULL &&
clip->Reader() != NULL) {
268 image_size = QSize(
clip->Reader()->info.width,
clip->Reader()->info.height);
271 if (!frame->has_image_data)
272 frame->AddColor(image_size.width(), image_size.height(),
"#00000000");
274 std::shared_ptr<QImage> frame_image = frame->GetImage();
275 if (!frame_image || frame_image->isNull())
278 const double scale_factor = frame_image->width() / 600.0;
279 const double font_size_value = std::max(1.0,
font_size.
GetValue(frame_number) * scale_factor);
280 const double stroke_width_value = std::max(0.0,
stroke_width.
GetValue(frame_number) * scale_factor);
283 const QString timer_text = QString::fromStdString(
TimerText(frame_number));
284 const QString layout_text = QString::fromStdString(TimerLayoutText(frame_number));
286 QFont font(QString::fromStdString(
font_name),
int(font_size_value));
287 font.setPixelSize(font_size_value);
288 QFontMetricsF metrics(font);
289 QRectF text_bounds = metrics.tightBoundingRect(timer_text);
290 if (text_bounds.isEmpty())
291 text_bounds = metrics.boundingRect(timer_text);
292 QRectF layout_bounds = metrics.tightBoundingRect(layout_text);
293 if (layout_bounds.isEmpty())
294 layout_bounds = metrics.boundingRect(layout_text);
296 const double text_width = std::max(1.0, text_bounds.width());
297 const double text_height = std::max(1.0, text_bounds.height());
298 double digit_slot_width = 0.0;
299 for (
int digit = 0; digit <= 9; ++digit)
300 digit_slot_width = std::max(digit_slot_width, TextAdvance(metrics, QString::number(digit)));
302 const bool use_slot_layout = timer_text.size() == layout_text.size() &&
304 std::vector<double> slot_widths;
305 double layout_width = 0.0;
306 if (use_slot_layout) {
307 slot_widths.reserve(layout_text.size());
308 for (
int index = 0; index < layout_text.size(); ++index) {
309 const QString layout_char(layout_text[index]);
310 const double slot_width = layout_text[index].isDigit() ? digit_slot_width : TextAdvance(metrics, layout_char);
311 slot_widths.push_back(slot_width);
312 layout_width += slot_width;
315 layout_width = std::max(text_width, layout_bounds.width());
317 layout_width = std::max(1.0, layout_width);
318 const double rendered_box_width = layout_width + (padding_value * 2.0);
319 const double box_height = std::max(text_height, layout_bounds.height()) + (padding_value * 2.0);
321 double x = padding_value;
323 const int resolved_gravity = ClampGravity(
gravity);
325 x = (frame_image->width() - rendered_box_width) / 2.0;
327 x = frame_image->width() - rendered_box_width - padding_value;
329 switch (resolved_gravity) {
337 y = (frame_image->height() - box_height) / 2.0;
342 y = frame_image->height() - box_height;
352 double text_x = x + padding_value - text_bounds.left();
354 text_x = x + padding_value + ((layout_width - text_width) / 2.0) - text_bounds.left();
356 text_x = x + padding_value + layout_width - text_width - text_bounds.left();
357 text_x = std::round(text_x);
358 const double text_y = std::round(y + padding_value - text_bounds.top());
360 QPainter painter(frame_image.get());
361 painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing,
true);
362 painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
367 painter.setPen(Qt::NoPen);
368 painter.setBrush(QBrush(background_qcolor));
369 painter.drawRoundedRect(QRectF(x, y, rendered_box_width, box_height), corner_value, corner_value);
373 if (use_slot_layout) {
374 double cursor_x = x + padding_value;
375 for (
int index = 0; index < timer_text.size(); ++index) {
376 const QString text_char(timer_text[index]);
377 QRectF char_bounds = metrics.tightBoundingRect(text_char);
378 if (char_bounds.isEmpty())
379 char_bounds = metrics.boundingRect(text_char);
381 const double char_x = std::round(cursor_x);
382 path.addText(QPointF(char_x - char_bounds.left(), text_y), font, text_char);
383 cursor_x += slot_widths[index];
386 path.addText(QPointF(text_x, text_y), font, timer_text);
390 stroke_qcolor.setAlphaF(std::max(0.0, std::min(1.0,
font_alpha.
GetValue(frame_number))));
391 QPen pen(stroke_qcolor);
392 pen.setWidthF(stroke_width_value);
393 painter.setPen(stroke_width_value <= 0.0 ? Qt::NoPen : pen);
395 QColor font_qcolor(QString::fromStdString(
color.
GetColorHex(frame_number)));
396 font_qcolor.setAlphaF(std::max(0.0, std::min(1.0,
font_alpha.
GetValue(frame_number))));
397 painter.setBrush(QBrush(font_qcolor));
398 painter.drawPath(
path);
416 root[
"clamp"] =
clamp;
417 root[
"gravity"] = ClampGravity(
gravity);
445 catch (
const std::exception& e)
447 throw InvalidJSON(
"JSON is invalid (missing keys or invalid data types)");
455 if (!root[
"mode"].isNull())
456 mode = root[
"mode"].asInt();
457 if (!root[
"time_source"].isNull())
459 if (!root[
"format"].isNull())
460 format = root[
"format"].asInt();
461 if (!root[
"clamp"].isNull())
462 clamp = root[
"clamp"].asInt();
463 if (!root[
"gravity"].isNull())
464 gravity = ClampGravity(root[
"gravity"].asInt());
465 if (!root[
"show_background"].isNull())
467 if (!root[
"font_name"].isNull())
468 font_name = root[
"font_name"].asString();
469 if (!root[
"prefix"].isNull())
470 prefix = root[
"prefix"].asString();
471 if (!root[
"suffix"].isNull())
472 suffix = root[
"suffix"].asString();
473 if (!root[
"color"].isNull())
475 if (!root[
"stroke"].isNull())
477 if (!root[
"background"].isNull())
479 if (!root[
"start_time"].isNull())
481 if (!root[
"end_time"].isNull())
483 if (!root[
"font_size"].isNull())
485 if (!root[
"font_alpha"].isNull())
487 if (!root[
"stroke_width"].isNull())
489 if (!root[
"x_offset"].isNull())
491 if (!root[
"y_offset"].isNull())
493 if (!root[
"background_alpha"].isNull())
495 if (!root[
"background_padding"].isNull())
497 if (!root[
"background_corner"].isNull())
539 const int resolved_gravity = ClampGravity(
gravity);
563 return root.toStyledString();
Header file for all Exception classes.
Header file for Timer effect class.
float Start() const
Get start position (in seconds) of clip (trim start of video)
virtual float End() const
Get end position (in seconds) of clip (trim end of video)
Json::Value add_property_choice_json(std::string name, int value, int selected_value) const
Generate JSON choice for a property (dropdown properties)
openshot::TimelineBase * timeline
Pointer to the parent timeline instance (if any)
virtual openshot::TimelineBase * ParentTimeline()
Get the associated Timeline pointer (if any)
Json::Value add_property_json(std::string name, float value, std::string type, std::string memo, const Keyframe *keyframe, float min_value, float max_value, bool readonly, int64_t requested_frame) const
Generate JSON for a property.
This class represents a clip (used to arrange readers on the timeline)
std::string GetColorHex(int64_t frame_number)
Get the HEX value of a color at a specific frame.
openshot::Keyframe blue
Curve representing the red value (0 - 255)
openshot::Keyframe red
Curve representing the red value (0 - 255)
openshot::Keyframe green
Curve representing the green value (0 - 255)
void SetJsonValue(const Json::Value root)
Load Json::Value into this object.
Json::Value JsonValue() const
Generate Json::Value for this object.
virtual Json::Value JsonValue() const
Generate Json::Value for this object.
openshot::ClipBase * ParentClip()
Parent clip object of this effect (which can be unparented and NULL)
Json::Value BasePropertiesJSON(int64_t requested_frame) const
Generate JSON object of base properties (recommended to be used by all effects)
virtual void SetJsonValue(const Json::Value root)
Load Json::Value into this object.
openshot::ClipBase * clip
Pointer to the parent clip instance (if any)
EffectInfoStruct info
Information about the current effect.
Exception for invalid JSON.
void SetJsonValue(const Json::Value root)
Load Json::Value into this object.
double GetValue(int64_t index) const
Get the value at a specific index.
Json::Value JsonValue() const
Generate Json::Value for this object.
This class represents a timeline.
std::string TimerText(int64_t frame_number) const
Json::Value JsonValue() const override
Generate Json::Value for this object.
Keyframe background_padding
Keyframe background_alpha
double TimerSeconds(int64_t frame_number) const
void SetJson(const std::string value) override
Load JSON string into this object.
std::string Json() const override
Generate JSON string of this object.
std::string PropertiesJSON(int64_t requested_frame) const override
Keyframe background_corner
void SetJsonValue(const Json::Value root) override
Load Json::Value into this object.
std::shared_ptr< openshot::Frame > GetFrame(int64_t frame_number) override
This method is required for all derived classes of ClipBase, and returns a new openshot::Frame object...
This namespace is the default namespace for all code in the openshot library.
@ GRAVITY_TOP_LEFT
Align clip to the top left of its parent.
@ GRAVITY_LEFT
Align clip to the left of its parent (middle aligned)
@ GRAVITY_TOP_RIGHT
Align clip to the top right of its parent.
@ GRAVITY_RIGHT
Align clip to the right of its parent (middle aligned)
@ GRAVITY_BOTTOM_LEFT
Align clip to the bottom left of its parent.
@ GRAVITY_BOTTOM
Align clip to the bottom center of its parent.
@ GRAVITY_TOP
Align clip to the top center of its parent.
@ GRAVITY_BOTTOM_RIGHT
Align clip to the bottom right of its parent.
@ GRAVITY_CENTER
Align clip to the center of its parent (middle aligned)
@ TIMER_FORMAT_HH_MM_SS_MILLISECONDS
const Json::Value stringToJson(const std::string value)
@ TIMER_MODE_FRAME_NUMBER
bool has_video
Determines if this effect manipulates the image of a frame.
bool has_audio
Determines if this effect manipulates the audio of a frame.
std::string class_name
The class name of the effect.
std::string name
The name of the effect.
std::string description
The description of this effect and what it does.