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 subtitle.font() ? content()->get_font(*subtitle.font()) : shared_ptr<Font>(),
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 1.015 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) */
259 escape_text(block.text),
261 block.effect_colour.get_value_or(sub::Colour(0, 0, 0)).dcp(),
262 /* Hack: we should use subtitle.fade_up and subtitle.fade_down here
263 but the times of these often don't have a frame rate associated
264 with them so the sub::Time won't convert them to milliseconds without
265 throwing an exception. Since only DCP subs fill those in (and we don't
266 use libsub for DCP subs) we can cheat by just putting 0 in here.
273 auto string_text = StringText(
275 content()->outline_width(),
276 content()->get_font(block.font.get_value_or("")),
279 set_forced_appearance(content(), string_text);
280 string_texts.push_back(string_text);
284 PlainStart(ContentStringText(from, string_texts));
285 maybe_set_position(from);
290 TextDecoder::emit_stop (ContentTime to)
297 TextDecoder::emit_plain (ContentTimePeriod period, vector<dcp::SubtitleString> subtitles, dcp::Standard valign_standard)
299 emit_plain_start (period.from, subtitles, valign_standard);
300 emit_stop (period.to);
305 TextDecoder::emit_plain (ContentTimePeriod period, sub::Subtitle const& subtitles)
307 emit_plain_start (period.from, subtitles);
308 emit_stop (period.to);
312 /* @param rect Area expressed as a fraction of the video frame that this subtitle
313 * is for (e.g. a width of 0.5 means the width of the subtitle is half the width
314 * of the video frame)
317 TextDecoder::emit_bitmap (ContentTimePeriod period, shared_ptr<const Image> image, dcpomatic::Rect<double> rect)
319 emit_bitmap_start ({ period.from, image, rect });
320 emit_stop (period.to);
327 _position = ContentTime ();
332 TextDecoder::maybe_set_position (dcpomatic::ContentTime position)
334 if (!_position || position > *_position) {
335 _position = position;