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, dcp::SubtitleString& 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 for (auto& subtitle: subtitles) {
110 subtitle.set_text(escape_text(subtitle.text()));
111 set_forced_appearance(content(), subtitle);
114 PlainStart(ContentStringText(from, subtitles));
120 TextDecoder::emit_plain_start (ContentTime from, sub::Subtitle const & sub_subtitle)
122 /* See if our next subtitle needs to be vertically placed on screen by us */
123 bool needs_placement = false;
124 optional<int> bottom_line;
125 for (auto line: sub_subtitle.lines) {
126 if (!line.vertical_position.reference || (line.vertical_position.line && !line.vertical_position.lines) || line.vertical_position.reference.get() == sub::TOP_OF_SUBTITLE) {
127 needs_placement = true;
128 if (!bottom_line || bottom_line.get() < line.vertical_position.line.get()) {
129 bottom_line = line.vertical_position.line.get();
134 /* Find the lowest proportional position */
135 optional<float> lowest_proportional;
136 for (auto line: sub_subtitle.lines) {
137 if (line.vertical_position.proportional) {
138 if (!lowest_proportional) {
139 lowest_proportional = line.vertical_position.proportional;
141 lowest_proportional = min(lowest_proportional.get(), line.vertical_position.proportional.get());
146 vector<dcp::SubtitleString> dcp_subtitles;
147 for (auto line: sub_subtitle.lines) {
148 for (auto block: line.blocks) {
150 if (!block.font_size.specified()) {
151 /* Fallback default font size if no other has been specified */
152 block.font_size.set_points (48);
157 if (needs_placement) {
158 DCPOMATIC_ASSERT (line.vertical_position.line);
159 double const multiplier = 1.2 * content()->line_spacing() * content()->y_scale() * block.font_size.proportional (72 * 11);
160 switch (line.vertical_position.reference.get_value_or(sub::BOTTOM_OF_SCREEN)) {
161 case sub::BOTTOM_OF_SCREEN:
162 case sub::TOP_OF_SUBTITLE:
163 /* This 1.015 is an arbitrary value to lift the bottom sub off the bottom
164 of the screen a bit to a pleasing degree.
167 (1 + bottom_line.get() - line.vertical_position.line.get()) * multiplier;
169 v_align = dcp::VAlign::TOP;
171 case sub::TOP_OF_SCREEN:
172 /* This 0.1 is another fudge factor to bring the top line away from the top of the screen a little */
173 v_position = 0.12 + line.vertical_position.line.get() * multiplier;
174 v_align = dcp::VAlign::TOP;
176 case sub::VERTICAL_CENTRE_OF_SCREEN:
177 v_position = line.vertical_position.line.get() * multiplier;
178 v_align = dcp::VAlign::CENTER;
182 DCPOMATIC_ASSERT (line.vertical_position.reference);
183 if (line.vertical_position.proportional) {
184 v_position = line.vertical_position.proportional.get();
186 DCPOMATIC_ASSERT (line.vertical_position.line);
187 DCPOMATIC_ASSERT (line.vertical_position.lines);
188 v_position = float(*line.vertical_position.line) / *line.vertical_position.lines;
191 if (lowest_proportional) {
192 /* Adjust line spacing */
193 v_position = ((v_position - lowest_proportional.get()) * content()->line_spacing()) + lowest_proportional.get();
196 switch (line.vertical_position.reference.get()) {
197 case sub::TOP_OF_SCREEN:
198 v_align = dcp::VAlign::TOP;
200 case sub::VERTICAL_CENTRE_OF_SCREEN:
201 v_align = dcp::VAlign::CENTER;
203 case sub::BOTTOM_OF_SCREEN:
204 v_align = dcp::VAlign::BOTTOM;
207 v_align = dcp::VAlign::TOP;
213 float h_position = line.horizontal_position.proportional;
214 switch (line.horizontal_position.reference) {
215 case sub::LEFT_OF_SCREEN:
216 h_align = dcp::HAlign::LEFT;
217 h_position = max(h_position, 0.05f);
219 case sub::HORIZONTAL_CENTRE_OF_SCREEN:
220 h_align = dcp::HAlign::CENTER;
222 case sub::RIGHT_OF_SCREEN:
223 h_align = dcp::HAlign::RIGHT;
224 h_position = max(h_position, 0.05f);
227 h_align = dcp::HAlign::CENTER;
231 /* The idea here (rightly or wrongly) is that we set the appearance based on the
232 values in the libsub objects, and these are overridden with values from the
233 content by the other emit_plain_start() above.
236 dcp_subtitles.push_back(
237 dcp::SubtitleString (
238 string(TEXT_FONT_ID),
243 block.font_size.points (72 * 11),
245 dcp::Time (from.seconds(), 1000),
246 /* XXX: hmm; this is a bit ugly (we don't know the to time yet) */
255 block.effect_colour.get_value_or(sub::Colour(0, 0, 0)).dcp(),
256 /* Hack: we should use subtitle.fade_up and subtitle.fade_down here
257 but the times of these often don't have a frame rate associated
258 with them so the sub::Time won't convert them to milliseconds without
259 throwing an exception. Since only DCP subs fill those in (and we don't
260 use libsub for DCP subs) we can cheat by just putting 0 in here.
270 for (auto& subtitle: dcp_subtitles) {
271 subtitle.set_text(escape_text(subtitle.text()));
272 set_forced_appearance(content(), subtitle);
275 PlainStart(ContentStringText(from, dcp_subtitles));
281 TextDecoder::emit_stop (ContentTime to)
288 TextDecoder::emit_plain (ContentTimePeriod period, vector<dcp::SubtitleString> subtitles)
290 emit_plain_start (period.from, subtitles);
291 emit_stop (period.to);
296 TextDecoder::emit_plain (ContentTimePeriod period, sub::Subtitle const& subtitles)
298 emit_plain_start (period.from, subtitles);
299 emit_stop (period.to);
303 /* @param rect Area expressed as a fraction of the video frame that this subtitle
304 * is for (e.g. a width of 0.5 means the width of the subtitle is half the width
305 * of the video frame)
308 TextDecoder::emit_bitmap (ContentTimePeriod period, shared_ptr<const Image> image, dcpomatic::Rect<double> rect)
310 emit_bitmap_start ({ period.from, image, rect });
311 emit_stop (period.to);
318 _position = ContentTime ();