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)
107 vector<StringText> string_texts;
109 for (auto& subtitle: subtitles) {
110 auto string_text = StringText(subtitle, content()->outline_width(), subtitle.font() ? content()->get_font(*subtitle.font()) : shared_ptr<Font>());
111 string_text.set_text(escape_text(string_text.text()));
112 set_forced_appearance(content(), string_text);
113 string_texts.push_back(string_text);
116 PlainStart(ContentStringText(from, string_texts));
117 maybe_set_position(from);
122 TextDecoder::emit_plain_start (ContentTime from, sub::Subtitle const & sub_subtitle)
124 /* See if our next subtitle needs to be vertically placed on screen by us */
125 bool needs_placement = false;
126 optional<int> bottom_line;
127 for (auto line: sub_subtitle.lines) {
128 if (!line.vertical_position.reference || (line.vertical_position.line && !line.vertical_position.lines) || line.vertical_position.reference.get() == sub::TOP_OF_SUBTITLE) {
129 needs_placement = true;
130 if (!bottom_line || bottom_line.get() < line.vertical_position.line.get()) {
131 bottom_line = line.vertical_position.line.get();
136 /* Find the lowest proportional position */
137 optional<float> lowest_proportional;
138 for (auto line: sub_subtitle.lines) {
139 if (line.vertical_position.proportional) {
140 if (!lowest_proportional) {
141 lowest_proportional = line.vertical_position.proportional;
143 lowest_proportional = min(lowest_proportional.get(), line.vertical_position.proportional.get());
148 vector<StringText> string_texts;
149 for (auto line: sub_subtitle.lines) {
150 for (auto block: line.blocks) {
152 if (!block.font_size.specified()) {
153 /* Fallback default font size if no other has been specified */
154 block.font_size.set_points (48);
159 if (needs_placement) {
160 DCPOMATIC_ASSERT (line.vertical_position.line);
161 double const multiplier = 1.2 * content()->line_spacing() * content()->y_scale() * block.font_size.proportional (72 * 11);
162 switch (line.vertical_position.reference.get_value_or(sub::BOTTOM_OF_SCREEN)) {
163 case sub::BOTTOM_OF_SCREEN:
164 case sub::TOP_OF_SUBTITLE:
165 /* This 1.015 is an arbitrary value to lift the bottom sub off the bottom
166 of the screen a bit to a pleasing degree.
169 (1 + bottom_line.get() - line.vertical_position.line.get()) * multiplier;
171 v_align = dcp::VAlign::TOP;
173 case sub::TOP_OF_SCREEN:
174 /* This 0.1 is another fudge factor to bring the top line away from the top of the screen a little */
175 v_position = 0.12 + line.vertical_position.line.get() * multiplier;
176 v_align = dcp::VAlign::TOP;
178 case sub::VERTICAL_CENTRE_OF_SCREEN:
179 v_position = line.vertical_position.line.get() * multiplier;
180 v_align = dcp::VAlign::CENTER;
184 DCPOMATIC_ASSERT (line.vertical_position.reference);
185 if (line.vertical_position.proportional) {
186 v_position = line.vertical_position.proportional.get();
188 DCPOMATIC_ASSERT (line.vertical_position.line);
189 DCPOMATIC_ASSERT (line.vertical_position.lines);
190 v_position = float(*line.vertical_position.line) / *line.vertical_position.lines;
193 if (lowest_proportional) {
194 /* Adjust line spacing */
195 v_position = ((v_position - lowest_proportional.get()) * content()->line_spacing()) + lowest_proportional.get();
198 switch (line.vertical_position.reference.get()) {
199 case sub::TOP_OF_SCREEN:
200 v_align = dcp::VAlign::TOP;
202 case sub::VERTICAL_CENTRE_OF_SCREEN:
203 v_align = dcp::VAlign::CENTER;
205 case sub::BOTTOM_OF_SCREEN:
206 v_align = dcp::VAlign::BOTTOM;
209 v_align = dcp::VAlign::TOP;
215 float h_position = line.horizontal_position.proportional;
216 switch (line.horizontal_position.reference) {
217 case sub::LEFT_OF_SCREEN:
218 h_align = dcp::HAlign::LEFT;
219 h_position = max(h_position, 0.05f);
221 case sub::HORIZONTAL_CENTRE_OF_SCREEN:
222 h_align = dcp::HAlign::CENTER;
224 case sub::RIGHT_OF_SCREEN:
225 h_align = dcp::HAlign::RIGHT;
226 h_position = max(h_position, 0.05f);
229 h_align = dcp::HAlign::CENTER;
233 /* The idea here (rightly or wrongly) is that we set the appearance based on the
234 values in the libsub objects, and these are overridden with values from the
235 content by the other emit_plain_start() above.
238 auto dcp_subtitle = dcp::SubtitleString(
244 block.font_size.points (72 * 11),
246 dcp::Time (from.seconds(), 1000),
247 /* XXX: hmm; this is a bit ugly (we don't know the to time yet) */
254 escape_text(block.text),
256 block.effect_colour.get_value_or(sub::Colour(0, 0, 0)).dcp(),
257 /* Hack: we should use subtitle.fade_up and subtitle.fade_down here
258 but the times of these often don't have a frame rate associated
259 with them so the sub::Time won't convert them to milliseconds without
260 throwing an exception. Since only DCP subs fill those in (and we don't
261 use libsub for DCP subs) we can cheat by just putting 0 in here.
268 auto string_text = StringText(dcp_subtitle, content()->outline_width(), content()->get_font(block.font.get_value_or("")));
269 set_forced_appearance(content(), string_text);
270 string_texts.push_back(string_text);
274 PlainStart(ContentStringText(from, string_texts));
275 maybe_set_position(from);
280 TextDecoder::emit_stop (ContentTime to)
287 TextDecoder::emit_plain (ContentTimePeriod period, vector<dcp::SubtitleString> subtitles)
289 emit_plain_start (period.from, subtitles);
290 emit_stop (period.to);
295 TextDecoder::emit_plain (ContentTimePeriod period, sub::Subtitle const& subtitles)
297 emit_plain_start (period.from, subtitles);
298 emit_stop (period.to);
302 /* @param rect Area expressed as a fraction of the video frame that this subtitle
303 * is for (e.g. a width of 0.5 means the width of the subtitle is half the width
304 * of the video frame)
307 TextDecoder::emit_bitmap (ContentTimePeriod period, shared_ptr<const Image> image, dcpomatic::Rect<double> rect)
309 emit_bitmap_start ({ period.from, image, rect });
310 emit_stop (period.to);
317 _position = ContentTime ();
322 TextDecoder::maybe_set_position (dcpomatic::ContentTime position)
324 if (!_position || position > *_position) {
325 _position = position;