16 #include "../Timeline.h"
18 #include <QGuiApplication>
25 #include <QPainterPath>
30 Caption::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();
50 void 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;
80 void 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);
108 std::shared_ptr<openshot::Frame>
Caption::GetFrame(std::shared_ptr<openshot::Frame> frame, int64_t frame_number)
125 if (timeline != NULL) {
127 }
else if (
clip != NULL &&
clip->Reader() != NULL) {
128 fps =
clip->Reader()->info.fps;
132 std::shared_ptr<QImage> frame_image = frame->GetImage();
136 double timeline_scale_factor = frame->GetImage()->width() / 600.0;
139 QPainter painter(frame_image.get());
140 painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing,
true);
143 painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
146 double font_size_value =
font_size.
GetValue(frame_number) * timeline_scale_factor;
147 QFont font(QString(
font_name.c_str()),
int(font_size_value));
148 font.setPixelSize(std::max(font_size_value, 1.0));
149 QFontMetricsF metrics = QFontMetricsF(font);
161 double metrics_line_spacing = metrics.lineSpacing();
164 double left_margin_x = frame_image->width() * left_value;
165 double starting_y = (frame_image->height() * top_value) + metrics_line_spacing;
166 double current_y = starting_y;
167 double bottom_y = starting_y;
168 double top_y = starting_y;
169 double max_text_width = 0.0;
170 double right_margin_x = frame_image->width() - (frame_image->width() * right_value);
171 double caption_area_width = right_margin_x - left_margin_x;
172 QRectF caption_area = QRectF(left_margin_x, starting_y, caption_area_width, frame_image->height());
175 std::vector<QPainterPath> text_paths;
176 double fade_in_percentage = 0.0;
177 double fade_out_percentage = 0.0;
178 double line_height = metrics_line_spacing * line_spacing_value;
181 for (
auto match = matchedCaptions.begin(); match != matchedCaptions.end(); match++) {
184 int64_t start_frame = ((match->captured(1).toFloat() * 60.0 * 60.0 ) + (match->captured(2).toFloat() * 60.0 ) +
185 match->captured(3).toFloat() + (match->captured(4).toFloat() / 1000.0)) * fps.
ToFloat();
186 int64_t end_frame = ((match->captured(5).toFloat() * 60.0 * 60.0 ) + (match->captured(6).toFloat() * 60.0 ) +
187 match->captured(7).toFloat() + (match->captured(8).toFloat() / 1000.0)) * fps.
ToFloat();
190 QStringList lines = match->captured(9).split(
"\n");
191 for(
int index = 0; index < lines.length(); index++) {
193 QString line = lines[index];
195 if (!line.startsWith(QStringLiteral(
"NOTE")) &&
196 !line.isEmpty() && frame_number >= start_frame && frame_number <= end_frame && line.length() > 1) {
199 fade_in_percentage = ((float) frame_number - (
float) start_frame) / fade_in_value;
200 fade_out_percentage = 1.0 - (((float) frame_number - ((
float) end_frame - fade_out_value)) / fade_out_value);
203 QStringList words = line.split(
" ");
206 bool use_spaces =
true;
207 if (line.length() > 20 && words.length() == 1) {
208 words = line.split(
"");
211 int words_remaining = words.length();
212 while (words_remaining > 0) {
213 bool words_displayed =
false;
214 for(
int word_index = words.length(); word_index > 0; word_index--) {
216 QString fitting_line = words.mid(0, word_index).join(
" ");
219 QRectF textRect = metrics.boundingRect(caption_area, Qt::TextSingleLine, fitting_line);
220 if (textRect.width() <= caption_area.width()) {
222 QPoint p(left_margin_x, current_y);
226 QString fitting_line;
228 fitting_line = words.mid(0, word_index).join(
" ");
230 fitting_line = words.mid(0, word_index).join(
"");
232 path1.addText(p, font, fitting_line);
233 text_paths.push_back(path1);
236 words = words.mid(word_index, words.length());
237 words_remaining = words.length();
238 words_displayed =
true;
241 current_y += line_height;
244 if (path1.boundingRect().width() > max_text_width) {
245 max_text_width = path1.boundingRect().width();
248 if (path1.boundingRect().top() < top_y) {
249 top_y = path1.boundingRect().top();
252 if (path1.boundingRect().bottom() > bottom_y) {
253 bottom_y = path1.boundingRect().bottom();
259 if (!words_displayed) {
270 QRectF caption_area_with_padding = QRectF(left_margin_x - (padding_value / 2.0),
271 top_y - (padding_value / 2.0),
272 max_text_width + padding_value,
273 (bottom_y - top_y) + padding_value);
276 double alignment_offset = std::max((caption_area_width - max_text_width) / 2.0, 0.0);
279 QBrush background_brush;
282 caption_area_with_padding.translate(alignment_offset, 0.0);
283 if (fade_in_percentage < 1.0) {
286 }
else if (fade_out_percentage >= 0.0 && fade_out_percentage <= 1.0) {
292 background_brush.setColor(background_qcolor);
293 background_brush.setStyle(Qt::SolidPattern);
294 painter.setBrush(background_brush);
295 painter.setPen(Qt::NoPen);
296 painter.drawRoundedRect(caption_area_with_padding, background_corner_value, background_corner_value);
300 QColor font_qcolor = QColor(QString(
color.
GetColorHex(frame_number).c_str()));
302 font_brush.setStyle(Qt::SolidPattern);
306 QColor stroke_qcolor;
309 pen.setColor(stroke_qcolor);
310 pen.setWidthF(std::max(stroke_width_value, 0.0));
314 for(QPainterPath
path : text_paths) {
316 path.translate(alignment_offset, 0.0);
317 if (fade_in_percentage < 1.0) {
321 }
else if (fade_out_percentage >= 0.0 && fade_out_percentage <= 1.0) {
326 pen.setColor(stroke_qcolor);
327 font_brush.setColor(font_qcolor);
330 if (stroke_width_value <= 0.0) {
331 painter.setPen(Qt::NoPen);
336 painter.setBrush(font_brush);
337 painter.drawPath(
path);
375 root[
"caption_text"] = caption_text;
392 catch (
const std::exception& e)
395 throw InvalidJSON(
"JSON is invalid (missing keys or invalid data types)");
406 if (!root[
"color"].isNull())
408 if (!root[
"stroke"].isNull())
410 if (!root[
"background"].isNull())
412 if (!root[
"background_alpha"].isNull())
414 if (!root[
"background_corner"].isNull())
416 if (!root[
"background_padding"].isNull())
418 if (!root[
"stroke_width"].isNull())
420 if (!root[
"font_size"].isNull())
422 if (!root[
"font_alpha"].isNull())
424 if (!root[
"fade_in"].isNull())
426 if (!root[
"fade_out"].isNull())
428 if (!root[
"line_spacing"].isNull())
430 if (!root[
"left"].isNull())
432 if (!root[
"top"].isNull())
434 if (!root[
"right"].isNull())
436 if (!root[
"caption_text"].isNull())
437 caption_text = root[
"caption_text"].asString();
438 if (!root[
"caption_font"].isNull())
439 font_name = root[
"caption_font"].asString();
450 root[
"id"] =
add_property_json(
"ID", 0.0,
"string",
Id(), NULL, -1, -1,
true, requested_frame);
451 root[
"position"] =
add_property_json(
"Position",
Position(),
"float",
"", NULL, 0, 1000 * 60 * 30,
false, requested_frame);
453 root[
"start"] =
add_property_json(
"Start",
Start(),
"float",
"", NULL, 0, 1000 * 60 * 30,
false, requested_frame);
454 root[
"end"] =
add_property_json(
"End",
End(),
"float",
"", NULL, 0, 1000 * 60 * 30,
false, requested_frame);
455 root[
"duration"] =
add_property_json(
"Duration",
Duration(),
"float",
"", NULL, 0, 1000 * 60 * 30,
true, requested_frame);
482 root[
"caption_text"] =
add_property_json(
"Captions", 0.0,
"caption", caption_text, NULL, -1, -1,
false, requested_frame);
489 return root.toStyledString();
Header file for Caption effect class.
Header file for all Exception classes.
std::string PropertiesJSON(int64_t requested_frame) const override
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.
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...
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.