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
46 : DecoderPart (parent)
53 /** Called by subclasses when an image subtitle is starting.
54 * @param from From time of the subtitle.
55 * @param image Subtitle image.
56 * @param rect Area expressed as a fraction of the video frame that this subtitle
57 * is for (e.g. a width of 0.5 means the width of the subtitle is half the width
61 TextDecoder::emit_bitmap_start (ContentBitmapText const& bitmap)
64 maybe_set_position(bitmap.from());
70 escape_text (string text)
72 /* We must escape some things, otherwise they might confuse our subtitle
73 renderer (which uses entities and some HTML-esque markup to do bold/italic etc.)
75 boost::algorithm::replace_all(text, "&", "&");
76 boost::algorithm::replace_all(text, "<", "<");
77 boost::algorithm::replace_all(text, ">", ">");
84 set_forced_appearance(shared_ptr<const TextContent> content, StringText& subtitle)
86 if (content->colour()) {
87 subtitle.set_colour(*content->colour());
89 if (content->effect_colour()) {
90 subtitle.set_effect_colour(*content->effect_colour());
92 if (content->effect()) {
93 subtitle.set_effect(*content->effect());
95 if (content->fade_in()) {
96 subtitle.set_fade_up_time(dcp::Time(content->fade_in()->seconds(), 1000));
98 if (content->fade_out()) {
99 subtitle.set_fade_down_time (dcp::Time(content->fade_out()->seconds(), 1000));
105 TextDecoder::emit_plain_start (ContentTime from, vector<dcp::SubtitleString> subtitles, dcp::Standard valign_standard)
107 vector<StringText> string_texts;
109 for (auto& subtitle: subtitles) {
110 auto string_text = StringText(
112 content()->outline_width(),
113 content()->get_font(subtitle.font().get_value_or("")),
116 string_text.set_text(escape_text(string_text.text()));
117 set_forced_appearance(content(), string_text);
118 string_texts.push_back(string_text);
121 PlainStart(ContentStringText(from, string_texts));
122 maybe_set_position(from);
127 TextDecoder::emit_plain_start (ContentTime from, sub::Subtitle const & sub_subtitle)
129 /* See if our next subtitle needs to be vertically placed on screen by us */
130 bool needs_placement = false;
131 optional<int> bottom_line;
132 for (auto line: sub_subtitle.lines) {
133 if (!line.vertical_position.reference || (line.vertical_position.line && !line.vertical_position.lines) || line.vertical_position.reference.get() == sub::TOP_OF_SUBTITLE) {
134 needs_placement = true;
135 if (!bottom_line || bottom_line.get() < line.vertical_position.line.get()) {
136 bottom_line = line.vertical_position.line.get();
141 /* Find the lowest proportional position */
142 optional<float> lowest_proportional;
143 for (auto line: sub_subtitle.lines) {
144 if (line.vertical_position.proportional) {
145 if (!lowest_proportional) {
146 lowest_proportional = line.vertical_position.proportional;
148 lowest_proportional = min(lowest_proportional.get(), line.vertical_position.proportional.get());
153 vector<StringText> string_texts;
154 for (auto line: sub_subtitle.lines) {
155 for (auto block: line.blocks) {
157 if (!block.font_size.specified()) {
158 /* Fallback default font size if no other has been specified */
159 block.font_size.set_points (48);
164 if (needs_placement) {
165 DCPOMATIC_ASSERT (line.vertical_position.line);
166 double const multiplier = 1.2 * content()->line_spacing() * content()->y_scale() * block.font_size.proportional (72 * 11);
167 switch (line.vertical_position.reference.get_value_or(sub::BOTTOM_OF_SCREEN)) {
168 case sub::BOTTOM_OF_SCREEN:
169 case sub::TOP_OF_SUBTITLE:
170 /* This 0.9 is an arbitrary value to lift the bottom sub off the bottom
171 of the screen a bit to a pleasing degree.
174 (1 + bottom_line.get() - line.vertical_position.line.get()) * multiplier;
176 v_align = dcp::VAlign::TOP;
178 case sub::TOP_OF_SCREEN:
179 /* This 0.1 is another fudge factor to bring the top line away from the top of the screen a little */
180 v_position = 0.12 + line.vertical_position.line.get() * multiplier;
181 v_align = dcp::VAlign::TOP;
183 case sub::VERTICAL_CENTRE_OF_SCREEN:
184 v_position = line.vertical_position.line.get() * multiplier;
185 v_align = dcp::VAlign::CENTER;
189 DCPOMATIC_ASSERT (line.vertical_position.reference);
190 if (line.vertical_position.proportional) {
191 v_position = line.vertical_position.proportional.get();
193 DCPOMATIC_ASSERT (line.vertical_position.line);
194 DCPOMATIC_ASSERT (line.vertical_position.lines);
195 v_position = float(*line.vertical_position.line) / *line.vertical_position.lines;
198 if (lowest_proportional) {
199 /* Adjust line spacing */
200 v_position = ((v_position - lowest_proportional.get()) * content()->line_spacing()) + lowest_proportional.get();
203 switch (line.vertical_position.reference.get()) {
204 case sub::TOP_OF_SCREEN:
205 v_align = dcp::VAlign::TOP;
207 case sub::VERTICAL_CENTRE_OF_SCREEN:
208 v_align = dcp::VAlign::CENTER;
210 case sub::BOTTOM_OF_SCREEN:
211 v_align = dcp::VAlign::BOTTOM;
214 v_align = dcp::VAlign::TOP;
220 float h_position = line.horizontal_position.proportional;
221 switch (line.horizontal_position.reference) {
222 case sub::LEFT_OF_SCREEN:
223 h_align = dcp::HAlign::LEFT;
224 h_position = max(h_position, 0.05f);
226 case sub::HORIZONTAL_CENTRE_OF_SCREEN:
227 h_align = dcp::HAlign::CENTER;
229 case sub::RIGHT_OF_SCREEN:
230 h_align = dcp::HAlign::RIGHT;
231 h_position = max(h_position, 0.05f);
234 h_align = dcp::HAlign::CENTER;
238 /* The idea here (rightly or wrongly) is that we set the appearance based on the
239 values in the libsub objects, and these are overridden with values from the
240 content by the other emit_plain_start() above.
243 auto dcp_subtitle = dcp::SubtitleString(
249 block.font_size.points (72 * 11),
251 dcp::Time (from.seconds(), 1000),
252 /* XXX: hmm; this is a bit ugly (we don't know the to time yet) */
260 escape_text(block.text),
262 block.effect_colour.get_value_or(sub::Colour(0, 0, 0)).dcp(),
263 /* Hack: we should use subtitle.fade_up and subtitle.fade_down here
264 but the times of these often don't have a frame rate associated
265 with them so the sub::Time won't convert them to milliseconds without
266 throwing an exception. Since only DCP subs fill those in (and we don't
267 use libsub for DCP subs) we can cheat by just putting 0 in here.
274 auto string_text = StringText(
276 content()->outline_width(),
277 content()->get_font(block.font.get_value_or("")),
280 set_forced_appearance(content(), string_text);
281 string_texts.push_back(string_text);
285 PlainStart(ContentStringText(from, string_texts));
286 maybe_set_position(from);
291 TextDecoder::emit_stop (ContentTime to)
298 TextDecoder::emit_plain (ContentTimePeriod period, vector<dcp::SubtitleString> subtitles, dcp::Standard valign_standard)
300 emit_plain_start (period.from, subtitles, valign_standard);
301 emit_stop (period.to);
306 TextDecoder::emit_plain (ContentTimePeriod period, sub::Subtitle const& subtitles)
308 emit_plain_start (period.from, subtitles);
309 emit_stop (period.to);
313 /* @param rect Area expressed as a fraction of the video frame that this subtitle
314 * is for (e.g. a width of 0.5 means the width of the subtitle is half the width
315 * of the video frame)
318 TextDecoder::emit_bitmap (ContentTimePeriod period, shared_ptr<const Image> image, dcpomatic::Rect<double> rect)
320 emit_bitmap_start ({ period.from, image, rect });
321 emit_stop (period.to);
328 _position = ContentTime ();
333 TextDecoder::maybe_set_position (dcpomatic::ContentTime position)
335 if (!_position || position > *_position) {
336 _position = position;