16#include "../Timeline.h"
18#include <QGuiApplication>
25#include <QPainterPath>
30Caption::Caption() : color(
"#ffffff"), stroke(
"#a9a9a9"), background(
"#ff000000"), background_alpha(0.0), left(0.1), top(0.75), right(0.1),
31 stroke_width(0.5), font_size(30.0), font_alpha(1.0), is_dirty(true), font_name(
"sans"), font(NULL), metrics(NULL),
32 fade_in(0.35), fade_out(0.35), background_corner(10.0), background_padding(20.0), line_spacing(1.0)
35 init_effect_details();
40 color(
"#ffffff"), stroke(
"#a9a9a9"), background(
"#ff000000"), background_alpha(0.0), left(0.1), top(0.75), right(0.1),
41 stroke_width(0.5), font_size(30.0), font_alpha(1.0), is_dirty(true), font_name(
"sans"), font(NULL), metrics(NULL),
42 fade_in(0.35), fade_out(0.35), background_corner(10.0), background_padding(20.0), line_spacing(1.0),
43 caption_text(captions)
46 init_effect_details();
50void Caption::init_effect_details()
63 if (caption_text.length() == 0) {
64 caption_text =
"00:00:00:000 --> 00:10:00:000\nEdit this caption with our caption editor";
75 caption_text = new_caption_text;
80void Caption::process_regex() {
85 matchedCaptions.clear();
87 QString caption_prepared = QString(caption_text.c_str());
88 if (caption_prepared.endsWith(
"\n\n") ==
false) {
90 caption_prepared.append(
"\n\n");
94 QRegularExpression allPathsRegex(QStringLiteral(
"(\\d{2})?:*(\\d{2}):(\\d{2}).(\\d{2,3})\\s*-->\\s*(\\d{2})?:*(\\d{2}):(\\d{2}).(\\d{2,3})([\\s\\S]*?)(.*?)(?=\\d{2}.\\d{2,3}|\\Z)"), QRegularExpression::MultilineOption);
95 QRegularExpressionMatchIterator i = allPathsRegex.globalMatch(caption_prepared);
97 QRegularExpressionMatch match = i.next();
98 if (match.hasMatch()) {
100 matchedCaptions.push_back(match);
108std::shared_ptr<openshot::Frame>
Caption::GetFrame(std::shared_ptr<openshot::Frame> frame, int64_t frame_number)
117 QSize image_size(1, 1);
126 if (timeline != NULL) {
129 }
else if (
clip != NULL &&
clip->Reader() != NULL) {
130 fps =
clip->Reader()->info.fps;
131 image_size = QSize(
clip->Reader()->info.width,
clip->Reader()->info.height);
134 if (!frame->has_image_data) {
136 frame->AddColor(image_size.width(), image_size.height(),
"#000000");
140 std::shared_ptr<QImage> frame_image = frame->GetImage();
144 double timeline_scale_factor = frame_image->width() / 600.0;
147 QPainter painter(frame_image.get());
148 painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing,
true);
151 painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
154 double font_size_value =
font_size.
GetValue(frame_number) * timeline_scale_factor;
155 QFont font(QString(
font_name.c_str()),
int(font_size_value));
156 font.setPixelSize(std::max(font_size_value, 1.0));
157 QFontMetricsF metrics = QFontMetricsF(font);
169 double metrics_line_spacing = metrics.lineSpacing();
172 double left_margin_x = frame_image->width() * left_value;
173 double starting_y = (frame_image->height() * top_value) + metrics_line_spacing;
174 double current_y = starting_y;
175 double bottom_y = starting_y;
176 double top_y = starting_y;
177 double max_text_width = 0.0;
178 double right_margin_x = frame_image->width() - (frame_image->width() * right_value);
179 double caption_area_width = right_margin_x - left_margin_x;
180 QRectF caption_area = QRectF(left_margin_x, starting_y, caption_area_width, frame_image->height());
183 std::vector<QPainterPath> text_paths;
184 double fade_in_percentage = 0.0;
185 double fade_out_percentage = 0.0;
186 double line_height = metrics_line_spacing * line_spacing_value;
189 for (
auto match = matchedCaptions.begin(); match != matchedCaptions.end(); match++) {
192 int64_t start_frame = ((match->captured(1).toFloat() * 60.0 * 60.0 ) + (match->captured(2).toFloat() * 60.0 ) +
193 match->captured(3).toFloat() + (match->captured(4).toFloat() / 1000.0)) * fps.
ToFloat();
194 int64_t end_frame = ((match->captured(5).toFloat() * 60.0 * 60.0 ) + (match->captured(6).toFloat() * 60.0 ) +
195 match->captured(7).toFloat() + (match->captured(8).toFloat() / 1000.0)) * fps.
ToFloat();
198 QStringList lines = match->captured(9).split(
"\n");
199 for(
int index = 0; index < lines.length(); index++) {
201 QString line = lines[index];
203 if (!line.startsWith(QStringLiteral(
"NOTE")) &&
204 !line.isEmpty() && frame_number >= start_frame && frame_number <= end_frame && line.length() > 1) {
207 fade_in_percentage = ((float) frame_number - (
float) start_frame) / fade_in_value;
208 fade_out_percentage = 1.0 - (((float) frame_number - ((
float) end_frame - fade_out_value)) / fade_out_value);
211 QStringList words = line.split(
" ");
214 bool use_spaces =
true;
215 if (line.length() > 20 && words.length() == 1) {
216 words = line.split(
"");
219 int words_remaining = words.length();
220 while (words_remaining > 0) {
221 bool words_displayed =
false;
222 for(
int word_index = words.length(); word_index > 0; word_index--) {
224 QString fitting_line = words.mid(0, word_index).join(
" ");
227 QRectF textRect = metrics.boundingRect(caption_area, Qt::TextSingleLine, fitting_line);
228 if (textRect.width() <= caption_area.width()) {
230 QPoint p(left_margin_x, current_y);
234 QString fitting_line;
236 fitting_line = words.mid(0, word_index).join(
" ");
238 fitting_line = words.mid(0, word_index).join(
"");
240 path1.addText(p, font, fitting_line);
241 text_paths.push_back(path1);
244 words = words.mid(word_index, words.length());
245 words_remaining = words.length();
246 words_displayed =
true;
249 current_y += line_height;
252 if (path1.boundingRect().width() > max_text_width) {
253 max_text_width = path1.boundingRect().width();
256 if (path1.boundingRect().top() < top_y) {
257 top_y = path1.boundingRect().top();
260 if (path1.boundingRect().bottom() > bottom_y) {
261 bottom_y = path1.boundingRect().bottom();
267 if (!words_displayed) {
278 QRectF caption_area_with_padding = QRectF(left_margin_x - (padding_value / 2.0),
279 top_y - (padding_value / 2.0),
280 max_text_width + padding_value,
281 (bottom_y - top_y) + padding_value);
284 double alignment_offset = std::max((caption_area_width - max_text_width) / 2.0, 0.0);
287 QBrush background_brush;
290 caption_area_with_padding.translate(alignment_offset, 0.0);
291 if (fade_in_percentage < 1.0) {
294 }
else if (fade_out_percentage >= 0.0 && fade_out_percentage <= 1.0) {
300 background_brush.setColor(background_qcolor);
301 background_brush.setStyle(Qt::SolidPattern);
302 painter.setBrush(background_brush);
303 painter.setPen(Qt::NoPen);
304 painter.drawRoundedRect(caption_area_with_padding, background_corner_value, background_corner_value);
308 QColor font_qcolor = QColor(QString(
color.
GetColorHex(frame_number).c_str()));
310 font_brush.setStyle(Qt::SolidPattern);
314 QColor stroke_qcolor;
317 pen.setColor(stroke_qcolor);
318 pen.setWidthF(std::max(stroke_width_value, 0.0));
322 for(QPainterPath path : text_paths) {
324 path.translate(alignment_offset, 0.0);
325 if (fade_in_percentage < 1.0) {
329 }
else if (fade_out_percentage >= 0.0 && fade_out_percentage <= 1.0) {
334 pen.setColor(stroke_qcolor);
335 font_brush.setColor(font_qcolor);
338 if (stroke_width_value <= 0.0) {
339 painter.setPen(Qt::NoPen);
344 painter.setBrush(font_brush);
345 painter.drawPath(path);
383 root[
"caption_text"] = caption_text;
400 catch (
const std::exception& e)
403 throw InvalidJSON(
"JSON is invalid (missing keys or invalid data types)");
414 if (!root[
"color"].isNull())
416 if (!root[
"stroke"].isNull())
418 if (!root[
"background"].isNull())
420 if (!root[
"background_alpha"].isNull())
422 if (!root[
"background_corner"].isNull())
424 if (!root[
"background_padding"].isNull())
426 if (!root[
"stroke_width"].isNull())
428 if (!root[
"font_size"].isNull())
430 if (!root[
"font_alpha"].isNull())
432 if (!root[
"fade_in"].isNull())
434 if (!root[
"fade_out"].isNull())
436 if (!root[
"line_spacing"].isNull())
438 if (!root[
"left"].isNull())
440 if (!root[
"top"].isNull())
442 if (!root[
"right"].isNull())
444 if (!root[
"caption_text"].isNull())
445 caption_text = root[
"caption_text"].asString();
446 if (!root[
"caption_font"].isNull())
447 font_name = root[
"caption_font"].asString();
458 root[
"id"] =
add_property_json(
"ID", 0.0,
"string",
Id(), NULL, -1, -1,
true, requested_frame);
459 root[
"position"] =
add_property_json(
"Position",
Position(),
"float",
"", NULL, 0, 1000 * 60 * 30,
false, requested_frame);
461 root[
"start"] =
add_property_json(
"Start",
Start(),
"float",
"", NULL, 0, 1000 * 60 * 30,
false, requested_frame);
462 root[
"end"] =
add_property_json(
"End",
End(),
"float",
"", NULL, 0, 1000 * 60 * 30,
false, requested_frame);
463 root[
"duration"] =
add_property_json(
"Duration",
Duration(),
"float",
"", NULL, 0, 1000 * 60 * 30,
true, requested_frame);
490 root[
"caption_text"] =
add_property_json(
"Captions", 0.0,
"caption", caption_text, NULL, -1, -1,
false, requested_frame);
497 return root.toStyledString();
Header file for Caption effect class.
Header file for all Exception classes.
std::string PropertiesJSON(int64_t requested_frame) const override
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...
Keyframe background_padding
Background padding.
Caption()
Blank constructor, useful when using Json to load the effect properties.
Keyframe stroke_width
Width of text border / stroke.
Json::Value JsonValue() const override
Generate Json::Value for this object.
void SetJsonValue(const Json::Value root) override
Load Json::Value into this object.
std::string Json() const override
Generate JSON string of this object.
void SetJson(const std::string value) override
Load JSON string into this object.
std::string font_name
Font string.
Color background
Color of caption area background.
Keyframe font_size
Font size in points.
Keyframe font_alpha
Font color alpha.
Keyframe background_alpha
Background color alpha.
Keyframe fade_out
Fade in per caption (# of seconds)
Keyframe background_corner
Background cornder radius.
Keyframe fade_in
Fade in per caption (# of seconds)
Keyframe line_spacing
Distance between lines (1.0 default / 100%)
Keyframe top
Size of top bar.
Color stroke
Color of text border / stroke.
std::string CaptionText()
Set the caption string to use (see VTT format)
Keyframe right
Size of right bar.
Color color
Color of caption text.
Keyframe left
Size of left bar.
float Start() const
Get start position (in seconds) of clip (trim start of video)
float Duration() const
Get the length of this clip (in seconds)
virtual float End() const
Get end position (in seconds) of clip (trim end of video)
std::string Id() const
Get the Id of this clip object.
int Layer() const
Get layer of clip on timeline (lower number is covered by higher numbers)
openshot::TimelineBase * timeline
Pointer to the parent timeline instance (if any)
float Position() const
Get position on timeline (in seconds)
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)
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.
This class represents a fraction.
float ToFloat()
Return this fraction as a float (i.e. 1/2 = 0.5)
double ToDouble() const
Return this fraction as a double (i.e. 1/2 = 0.5)
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.
This namespace is the default namespace for all code in the openshot library.
const Json::Value stringToJson(const std::string value)
bool has_video
Determines if this effect manipulates the image of a frame.
std::string parent_effect_id
Id of the parent effect (if there is one)
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.