2 Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
4 This file is part of DCP-o-matic.
6 DCP-o-matic is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
11 DCP-o-matic is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
22 #include "compose.hpp"
24 #include "text_content.h"
25 #include "text_decoder.h"
27 #include <sub/subtitle.h>
28 #include <boost/algorithm/string.hpp>
35 using std::shared_ptr;
38 using boost::optional;
39 using namespace dcpomatic;
42 TextDecoder::TextDecoder (
44 shared_ptr<const TextContent> content,
47 : DecoderPart (parent)
55 /** Called by subclasses when an image subtitle is starting.
56 * @param from From time of the subtitle.
57 * @param image Subtitle image.
58 * @param rect Area expressed as a fraction of the video frame that this subtitle
59 * is for (e.g. a width of 0.5 means the width of the subtitle is half the width
63 TextDecoder::emit_bitmap_start (ContentBitmapText const& bitmap)
66 _position = bitmap.from();
72 escape_text (string text)
74 /* We must escape some things, otherwise they might confuse our subtitle
75 renderer (which uses entities and some HTML-esque markup to do bold/italic etc.)
77 boost::algorithm::replace_all(text, "&", "&");
78 boost::algorithm::replace_all(text, "<", "<");
79 boost::algorithm::replace_all(text, ">", ">");
86 set_forced_appearance(shared_ptr<const TextContent> content, StringText& subtitle)
88 if (content->colour()) {
89 subtitle.set_colour(*content->colour());
91 if (content->effect_colour()) {
92 subtitle.set_effect_colour(*content->effect_colour());
94 if (content->effect()) {
95 subtitle.set_effect(*content->effect());
97 if (content->fade_in()) {
98 subtitle.set_fade_up_time(dcp::Time(content->fade_in()->seconds(), 1000));
100 if (content->fade_out()) {
101 subtitle.set_fade_down_time (dcp::Time(content->fade_out()->seconds(), 1000));
107 TextDecoder::emit_plain_start (ContentTime from, vector<dcp::SubtitleString> subtitles)
109 vector<StringText> string_texts;
111 for (auto& subtitle: subtitles) {
112 auto string_text = StringText(subtitle, content()->outline_width(), subtitle.font() ? content()->get_font(*subtitle.font()) : shared_ptr<Font>());
113 string_text.set_text(escape_text(string_text.text()));
114 set_forced_appearance(content(), string_text);
115 string_texts.push_back(string_text);
118 PlainStart(ContentStringText(from, string_texts));
124 TextDecoder::emit_plain_start (ContentTime from, sub::Subtitle const & sub_subtitle)
126 /* See if our next subtitle needs to be vertically placed on screen by us */
127 bool needs_placement = false;
128 optional<int> bottom_line;
129 for (auto line: sub_subtitle.lines) {
130 if (!line.vertical_position.reference || (line.vertical_position.line && !line.vertical_position.lines) || line.vertical_position.reference.get() == sub::TOP_OF_SUBTITLE) {
131 needs_placement = true;
132 if (!bottom_line || bottom_line.get() < line.vertical_position.line.get()) {
133 bottom_line = line.vertical_position.line.get();
138 /* Find the lowest proportional position */
139 optional<float> lowest_proportional;
140 for (auto line: sub_subtitle.lines) {
141 if (line.vertical_position.proportional) {
142 if (!lowest_proportional) {
143 lowest_proportional = line.vertical_position.proportional;
145 lowest_proportional = min(lowest_proportional.get(), line.vertical_position.proportional.get());
150 vector<StringText> string_texts;
151 for (auto line: sub_subtitle.lines) {
152 for (auto block: line.blocks) {
154 if (!block.font_size.specified()) {
155 /* Fallback default font size if no other has been specified */
156 block.font_size.set_points (48);
161 if (needs_placement) {
162 DCPOMATIC_ASSERT (line.vertical_position.line);
163 double const multiplier = 1.2 * content()->line_spacing() * content()->y_scale() * block.font_size.proportional (72 * 11);
164 switch (line.vertical_position.reference.get_value_or(sub::BOTTOM_OF_SCREEN)) {
165 case sub::BOTTOM_OF_SCREEN:
166 case sub::TOP_OF_SUBTITLE:
167 /* This 1.015 is an arbitrary value to lift the bottom sub off the bottom
168 of the screen a bit to a pleasing degree.
171 (1 + bottom_line.get() - line.vertical_position.line.get()) * multiplier;
173 v_align = dcp::VAlign::TOP;
175 case sub::TOP_OF_SCREEN:
176 /* This 0.1 is another fudge factor to bring the top line away from the top of the screen a little */
177 v_position = 0.12 + line.vertical_position.line.get() * multiplier;
178 v_align = dcp::VAlign::TOP;
180 case sub::VERTICAL_CENTRE_OF_SCREEN:
181 v_position = line.vertical_position.line.get() * multiplier;
182 v_align = dcp::VAlign::CENTER;
186 DCPOMATIC_ASSERT (line.vertical_position.reference);
187 if (line.vertical_position.proportional) {
188 v_position = line.vertical_position.proportional.get();
190 DCPOMATIC_ASSERT (line.vertical_position.line);
191 DCPOMATIC_ASSERT (line.vertical_position.lines);
192 v_position = float(*line.vertical_position.line) / *line.vertical_position.lines;
195 if (lowest_proportional) {
196 /* Adjust line spacing */
197 v_position = ((v_position - lowest_proportional.get()) * content()->line_spacing()) + lowest_proportional.get();
200 switch (line.vertical_position.reference.get()) {
201 case sub::TOP_OF_SCREEN:
202 v_align = dcp::VAlign::TOP;
204 case sub::VERTICAL_CENTRE_OF_SCREEN:
205 v_align = dcp::VAlign::CENTER;
207 case sub::BOTTOM_OF_SCREEN:
208 v_align = dcp::VAlign::BOTTOM;
211 v_align = dcp::VAlign::TOP;
217 float h_position = line.horizontal_position.proportional;
218 switch (line.horizontal_position.reference) {
219 case sub::LEFT_OF_SCREEN:
220 h_align = dcp::HAlign::LEFT;
221 h_position = max(h_position, 0.05f);
223 case sub::HORIZONTAL_CENTRE_OF_SCREEN:
224 h_align = dcp::HAlign::CENTER;
226 case sub::RIGHT_OF_SCREEN:
227 h_align = dcp::HAlign::RIGHT;
228 h_position = max(h_position, 0.05f);
231 h_align = dcp::HAlign::CENTER;
235 /* The idea here (rightly or wrongly) is that we set the appearance based on the
236 values in the libsub objects, and these are overridden with values from the
237 content by the other emit_plain_start() above.
240 auto dcp_subtitle = dcp::SubtitleString(
246 block.font_size.points (72 * 11),
248 dcp::Time (from.seconds(), 1000),
249 /* XXX: hmm; this is a bit ugly (we don't know the to time yet) */
256 escape_text(block.text),
258 block.effect_colour.get_value_or(sub::Colour(0, 0, 0)).dcp(),
259 /* Hack: we should use subtitle.fade_up and subtitle.fade_down here
260 but the times of these often don't have a frame rate associated
261 with them so the sub::Time won't convert them to milliseconds without
262 throwing an exception. Since only DCP subs fill those in (and we don't
263 use libsub for DCP subs) we can cheat by just putting 0 in here.
270 auto string_text = StringText(dcp_subtitle, content()->outline_width(), content()->get_font(block.font.get_value_or("")));
271 set_forced_appearance(content(), string_text);
272 string_texts.push_back(string_text);
276 PlainStart(ContentStringText(from, string_texts));
282 TextDecoder::emit_stop (ContentTime to)
289 TextDecoder::emit_plain (ContentTimePeriod period, vector<dcp::SubtitleString> subtitles)
291 emit_plain_start (period.from, subtitles);
292 emit_stop (period.to);
297 TextDecoder::emit_plain (ContentTimePeriod period, sub::Subtitle const& subtitles)
299 emit_plain_start (period.from, subtitles);
300 emit_stop (period.to);
304 /* @param rect Area expressed as a fraction of the video frame that this subtitle
305 * is for (e.g. a width of 0.5 means the width of the subtitle is half the width
306 * of the video frame)
309 TextDecoder::emit_bitmap (ContentTimePeriod period, shared_ptr<const Image> image, dcpomatic::Rect<double> rect)
311 emit_bitmap_start ({ period.from, image, rect });
312 emit_stop (period.to);
319 _position = ContentTime ();