16 #include "../Timeline.h"
27 #include <QFontMetricsF>
30 #include <QPainterPath>
39 int ClampGravity(
int gravity)
59 background(
"#000000"),
67 background_alpha(0.45),
68 background_padding(14.0),
69 background_corner(6.0)
71 init_effect_details();
74 void Timer::init_effect_details()
79 info.
description =
"Render a styled count up, count down, clock, timecode, or frame number overlay.";
84 double Timer::ResolveFps()
const
89 if (
clip &&
clip->ParentTimeline() != NULL) {
91 }
else if (
const_cast<Timer*
>(
this)->ParentTimeline() != NULL) {
96 return clip->Reader()->info.fps.ToDouble();
98 return timeline->info.fps.ToDouble();
99 if (
clip != NULL &&
clip->Reader() != NULL &&
clip->Reader()->info.fps.ToDouble() > 0.0)
100 return clip->Reader()->info.fps.ToDouble();
104 int64_t Timer::EffectiveFrameNumber(int64_t frame_number)
const
107 return std::max<int64_t>(1, frame_number);
110 if (
clip != NULL &&
clip->time.GetLength() > 1)
111 return std::max<int64_t>(1,
clip->time.GetLong(frame_number));
113 return std::max<int64_t>(1, frame_number);
116 double Timer::CountdownDuration(int64_t frame_number)
const
119 if (configured_duration > 0.0)
120 return configured_duration;
129 std::string Timer::FormatSeconds(
double seconds,
double fps,
bool duration_style)
const
131 if (duration_style &&
clamp && seconds < 0.0)
134 const bool negative = seconds < 0.0;
135 const int64_t total_milliseconds = std::llround(std::fabs(seconds) * 1000.0);
136 const int milliseconds =
static_cast<int>(total_milliseconds % 1000);
137 const int64_t total_seconds = total_milliseconds / 1000;
138 const int hours =
static_cast<int>(total_seconds / 3600);
139 const int minutes =
static_cast<int>((total_seconds / 60) % 60);
140 const int whole_seconds =
static_cast<int>(total_seconds % 60);
142 std::ostringstream text;
147 text << std::setfill(
'0') << std::setw(2) << hours <<
":"
148 << std::setw(2) << minutes <<
":"
149 << std::setw(2) << whole_seconds <<
"."
150 << std::setw(3) << milliseconds;
152 text << std::setfill(
'0') << std::setw(2) << hours <<
":"
153 << std::setw(2) << minutes <<
":"
154 << std::setw(2) << whole_seconds;
156 text << FormatTimecode(seconds, fps);
158 text << static_cast<int64_t>(std::llround(seconds * fps));
160 const int total_minutes = hours * 60 + minutes;
161 text << std::setfill(
'0') << std::setw(2) << total_minutes <<
":"
162 << std::setw(2) << whole_seconds;
168 std::string Timer::FormatTimecode(
double seconds,
double fps)
const
170 const bool negative = seconds < 0.0;
171 const int64_t total_frames = std::llround(std::fabs(seconds) * fps);
172 const int64_t rounded_fps = std::max<int64_t>(1, std::llround(fps));
173 const int64_t frames = total_frames % rounded_fps;
174 const int64_t total_seconds = total_frames / rounded_fps;
175 const int64_t secs = total_seconds % 60;
176 const int64_t mins = (total_seconds / 60) % 60;
177 const int64_t hours = total_seconds / 3600;
179 std::ostringstream text;
182 text << std::setfill(
'0') << std::setw(2) << hours <<
":"
183 << std::setw(2) << mins <<
":"
184 << std::setw(2) << secs <<
":"
185 << std::setw(2) << frames;
191 const double fps = ResolveFps();
192 const int64_t effective_frame = EffectiveFrameNumber(frame_number);
193 const double elapsed =
static_cast<double>(effective_frame - 1) / fps;
203 const double fps = ResolveFps();
204 const int64_t effective_frame = EffectiveFrameNumber(frame_number);
205 std::ostringstream text;
210 text << (effective_frame + frame_offset);
212 text << FormatTimecode(
TimerSeconds(frame_number), fps);
214 double seconds = std::fmod(
TimerSeconds(frame_number), 24.0 * 60.0 * 60.0);
216 seconds += 24.0 * 60.0 * 60.0;
217 text << FormatSeconds(seconds, fps,
false);
219 text << FormatSeconds(
TimerSeconds(frame_number), fps,
true);
226 std::string Timer::TimerLayoutText(int64_t frame_number)
const
231 std::string timer_template;
233 timer_template =
"88:88:88:88";
235 timer_template =
"88:88:88.888";
237 timer_template =
"88:88:88";
239 timer_template =
"88:88";
244 std::shared_ptr<openshot::Frame>
Timer::GetFrame(std::shared_ptr<openshot::Frame> frame, int64_t frame_number)
248 QSize image_size(1280, 720);
250 if (
clip &&
clip->ParentTimeline() != NULL) {
252 }
else if (this->ParentTimeline() != NULL) {
258 }
else if (
clip != NULL &&
clip->Reader() != NULL) {
259 image_size = QSize(
clip->Reader()->info.width,
clip->Reader()->info.height);
262 if (!frame->has_image_data)
263 frame->AddColor(image_size.width(), image_size.height(),
"#00000000");
265 std::shared_ptr<QImage> frame_image = frame->GetImage();
266 if (!frame_image || frame_image->isNull())
269 const double scale_factor = frame_image->width() / 600.0;
270 const double font_size_value = std::max(1.0,
font_size.
GetValue(frame_number) * scale_factor);
271 const double stroke_width_value = std::max(0.0,
stroke_width.
GetValue(frame_number) * scale_factor);
274 const QString timer_text = QString::fromStdString(
TimerText(frame_number));
275 const QString layout_text = QString::fromStdString(TimerLayoutText(frame_number));
277 QFont font(QString::fromStdString(
font_name),
int(font_size_value));
278 font.setPixelSize(font_size_value);
279 QFontMetricsF metrics(font);
280 QRectF text_bounds = metrics.tightBoundingRect(timer_text);
281 if (text_bounds.isEmpty())
282 text_bounds = metrics.boundingRect(timer_text);
283 QRectF layout_bounds = metrics.tightBoundingRect(layout_text);
284 if (layout_bounds.isEmpty())
285 layout_bounds = metrics.boundingRect(layout_text);
287 const double text_width = std::max(1.0, text_bounds.width());
288 const double text_height = std::max(1.0, text_bounds.height());
289 double digit_slot_width = 0.0;
290 for (
int digit = 0; digit <= 9; ++digit)
291 digit_slot_width = std::max(digit_slot_width, metrics.horizontalAdvance(QString::number(digit)));
293 const bool use_slot_layout = timer_text.size() == layout_text.size() &&
295 std::vector<double> slot_widths;
296 double layout_width = 0.0;
297 if (use_slot_layout) {
298 slot_widths.reserve(layout_text.size());
299 for (
int index = 0; index < layout_text.size(); ++index) {
300 const QString layout_char(layout_text[index]);
301 const double slot_width = layout_text[index].isDigit() ? digit_slot_width : metrics.horizontalAdvance(layout_char);
302 slot_widths.push_back(slot_width);
303 layout_width += slot_width;
306 layout_width = std::max(text_width, layout_bounds.width());
308 layout_width = std::max(1.0, layout_width);
309 const double rendered_box_width = layout_width + (padding_value * 2.0);
310 const double box_height = std::max(text_height, layout_bounds.height()) + (padding_value * 2.0);
312 double x = padding_value;
314 const int resolved_gravity = ClampGravity(
gravity);
316 x = (frame_image->width() - rendered_box_width) / 2.0;
318 x = frame_image->width() - rendered_box_width - padding_value;
320 switch (resolved_gravity) {
328 y = (frame_image->height() - box_height) / 2.0;
333 y = frame_image->height() - box_height;
343 double text_x = x + padding_value - text_bounds.left();
345 text_x = x + padding_value + ((layout_width - text_width) / 2.0) - text_bounds.left();
347 text_x = x + padding_value + layout_width - text_width - text_bounds.left();
348 text_x = std::round(text_x);
349 const double text_y = std::round(y + padding_value - text_bounds.top());
351 QPainter painter(frame_image.get());
352 painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing,
true);
353 painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
358 painter.setPen(Qt::NoPen);
359 painter.setBrush(QBrush(background_qcolor));
360 painter.drawRoundedRect(QRectF(x, y, rendered_box_width, box_height), corner_value, corner_value);
364 if (use_slot_layout) {
365 double cursor_x = x + padding_value;
366 for (
int index = 0; index < timer_text.size(); ++index) {
367 const QString text_char(timer_text[index]);
368 QRectF char_bounds = metrics.tightBoundingRect(text_char);
369 if (char_bounds.isEmpty())
370 char_bounds = metrics.boundingRect(text_char);
372 const double char_x = std::round(cursor_x);
373 path.addText(QPointF(char_x - char_bounds.left(), text_y), font, text_char);
374 cursor_x += slot_widths[index];
377 path.addText(QPointF(text_x, text_y), font, timer_text);
381 stroke_qcolor.setAlphaF(std::max(0.0, std::min(1.0,
font_alpha.
GetValue(frame_number))));
382 QPen pen(stroke_qcolor);
383 pen.setWidthF(stroke_width_value);
384 painter.setPen(stroke_width_value <= 0.0 ? Qt::NoPen : pen);
386 QColor font_qcolor(QString::fromStdString(
color.
GetColorHex(frame_number)));
387 font_qcolor.setAlphaF(std::max(0.0, std::min(1.0,
font_alpha.
GetValue(frame_number))));
388 painter.setBrush(QBrush(font_qcolor));
389 painter.drawPath(
path);
407 root[
"clamp"] =
clamp;
408 root[
"gravity"] = ClampGravity(
gravity);
436 catch (
const std::exception& e)
438 throw InvalidJSON(
"JSON is invalid (missing keys or invalid data types)");
446 if (!root[
"mode"].isNull())
447 mode = root[
"mode"].asInt();
448 if (!root[
"time_source"].isNull())
450 if (!root[
"format"].isNull())
451 format = root[
"format"].asInt();
452 if (!root[
"clamp"].isNull())
453 clamp = root[
"clamp"].asInt();
454 if (!root[
"gravity"].isNull())
455 gravity = ClampGravity(root[
"gravity"].asInt());
456 if (!root[
"show_background"].isNull())
458 if (!root[
"font_name"].isNull())
459 font_name = root[
"font_name"].asString();
460 if (!root[
"prefix"].isNull())
461 prefix = root[
"prefix"].asString();
462 if (!root[
"suffix"].isNull())
463 suffix = root[
"suffix"].asString();
464 if (!root[
"color"].isNull())
466 if (!root[
"stroke"].isNull())
468 if (!root[
"background"].isNull())
470 if (!root[
"start_time"].isNull())
472 if (!root[
"end_time"].isNull())
474 if (!root[
"font_size"].isNull())
476 if (!root[
"font_alpha"].isNull())
478 if (!root[
"stroke_width"].isNull())
480 if (!root[
"x_offset"].isNull())
482 if (!root[
"y_offset"].isNull())
484 if (!root[
"background_alpha"].isNull())
486 if (!root[
"background_padding"].isNull())
488 if (!root[
"background_corner"].isNull())
530 const int resolved_gravity = ClampGravity(
gravity);
554 return root.toStyledString();