Set up TextDecoder position based on the time that the next thing will
[dcpomatic.git] / src / lib / text_decoder.cc
1 /*
2     Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
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.
10
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.
15
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/>.
18
19 */
20
21
22 #include "compose.hpp"
23 #include "log.h"
24 #include "text_content.h"
25 #include "text_decoder.h"
26 #include "util.h"
27 #include <sub/subtitle.h>
28 #include <boost/algorithm/string.hpp>
29 #include <iostream>
30
31
32 using std::cout;
33 using std::max;
34 using std::min;
35 using std::shared_ptr;
36 using std::string;
37 using std::vector;
38 using boost::optional;
39 using namespace dcpomatic;
40
41
42 TextDecoder::TextDecoder (
43         Decoder* parent,
44         shared_ptr<const TextContent> content
45         )
46         : DecoderPart (parent)
47         , _content (content)
48 {
49
50 }
51
52
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
58  *  of the video frame)
59  */
60 void
61 TextDecoder::emit_bitmap_start (ContentBitmapText const& bitmap)
62 {
63         BitmapStart (bitmap);
64         maybe_set_position(bitmap.from());
65 }
66
67
68 static
69 string
70 escape_text (string text)
71 {
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.)
74         */
75         boost::algorithm::replace_all(text, "&", "&amp;");
76         boost::algorithm::replace_all(text, "<", "&lt;");
77         boost::algorithm::replace_all(text, ">", "&gt;");
78         return text;
79 }
80
81
82 static
83 void
84 set_forced_appearance(shared_ptr<const TextContent> content, StringText& subtitle)
85 {
86         if (content->colour()) {
87                 subtitle.set_colour(*content->colour());
88         }
89         if (content->effect_colour()) {
90                 subtitle.set_effect_colour(*content->effect_colour());
91         }
92         if (content->effect()) {
93                 subtitle.set_effect(*content->effect());
94         }
95         if (content->fade_in()) {
96                 subtitle.set_fade_up_time(dcp::Time(content->fade_in()->seconds(), 1000));
97         }
98         if (content->fade_out()) {
99                 subtitle.set_fade_down_time (dcp::Time(content->fade_out()->seconds(), 1000));
100         }
101 }
102
103
104 void
105 TextDecoder::emit_plain_start (ContentTime from, vector<dcp::SubtitleString> subtitles)
106 {
107         vector<StringText> string_texts;
108
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);
114         }
115
116         PlainStart(ContentStringText(from, string_texts));
117         maybe_set_position(from);
118 }
119
120
121 void
122 TextDecoder::emit_plain_start (ContentTime from, sub::Subtitle const & sub_subtitle)
123 {
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();
132                         }
133                 }
134         }
135
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;
142                         } else {
143                                 lowest_proportional = min(lowest_proportional.get(), line.vertical_position.proportional.get());
144                         }
145                 }
146         }
147
148         vector<StringText> string_texts;
149         for (auto line: sub_subtitle.lines) {
150                 for (auto block: line.blocks) {
151
152                         if (!block.font_size.specified()) {
153                                 /* Fallback default font size if no other has been specified */
154                                 block.font_size.set_points (48);
155                         }
156
157                         float v_position;
158                         dcp::VAlign v_align;
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.
167                                            */
168                                         v_position = 1.015 -
169                                                 (1 + bottom_line.get() - line.vertical_position.line.get()) * multiplier;
170
171                                         v_align = dcp::VAlign::TOP;
172                                         break;
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;
177                                         break;
178                                 case sub::VERTICAL_CENTRE_OF_SCREEN:
179                                         v_position = line.vertical_position.line.get() * multiplier;
180                                         v_align = dcp::VAlign::CENTER;
181                                         break;
182                                 }
183                         } else {
184                                 DCPOMATIC_ASSERT (line.vertical_position.reference);
185                                 if (line.vertical_position.proportional) {
186                                         v_position = line.vertical_position.proportional.get();
187                                 } else {
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;
191                                 }
192
193                                 if (lowest_proportional) {
194                                         /* Adjust line spacing */
195                                         v_position = ((v_position - lowest_proportional.get()) * content()->line_spacing()) + lowest_proportional.get();
196                                 }
197
198                                 switch (line.vertical_position.reference.get()) {
199                                 case sub::TOP_OF_SCREEN:
200                                         v_align = dcp::VAlign::TOP;
201                                         break;
202                                 case sub::VERTICAL_CENTRE_OF_SCREEN:
203                                         v_align = dcp::VAlign::CENTER;
204                                         break;
205                                 case sub::BOTTOM_OF_SCREEN:
206                                         v_align = dcp::VAlign::BOTTOM;
207                                         break;
208                                 default:
209                                         v_align = dcp::VAlign::TOP;
210                                         break;
211                                 }
212                         }
213
214                         dcp::HAlign h_align;
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);
220                                 break;
221                         case sub::HORIZONTAL_CENTRE_OF_SCREEN:
222                                 h_align = dcp::HAlign::CENTER;
223                                 break;
224                         case sub::RIGHT_OF_SCREEN:
225                                 h_align = dcp::HAlign::RIGHT;
226                                 h_position = max(h_position, 0.05f);
227                                 break;
228                         default:
229                                 h_align = dcp::HAlign::CENTER;
230                                 break;
231                         }
232
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.
236                         */
237
238                         auto dcp_subtitle = dcp::SubtitleString(
239                                 optional<string>(),
240                                 block.italic,
241                                 block.bold,
242                                 block.underline,
243                                 block.colour.dcp(),
244                                 block.font_size.points (72 * 11),
245                                 1.0,
246                                 dcp::Time (from.seconds(), 1000),
247                                 /* XXX: hmm; this is a bit ugly (we don't know the to time yet) */
248                                 dcp::Time (),
249                                 h_position,
250                                 h_align,
251                                 v_position,
252                                 v_align,
253                                 dcp::Direction::LTR,
254                                 escape_text(block.text),
255                                 dcp::Effect::NONE,
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.
262                                 */
263                                 dcp::Time (),
264                                 dcp::Time (),
265                                 0
266                                 );
267
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);
271                 }
272         }
273
274         PlainStart(ContentStringText(from, string_texts));
275         maybe_set_position(from);
276 }
277
278
279 void
280 TextDecoder::emit_stop (ContentTime to)
281 {
282         Stop (to);
283 }
284
285
286 void
287 TextDecoder::emit_plain (ContentTimePeriod period, vector<dcp::SubtitleString> subtitles)
288 {
289         emit_plain_start (period.from, subtitles);
290         emit_stop (period.to);
291 }
292
293
294 void
295 TextDecoder::emit_plain (ContentTimePeriod period, sub::Subtitle const& subtitles)
296 {
297         emit_plain_start (period.from, subtitles);
298         emit_stop (period.to);
299 }
300
301
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)
305  */
306 void
307 TextDecoder::emit_bitmap (ContentTimePeriod period, shared_ptr<const Image> image, dcpomatic::Rect<double> rect)
308 {
309         emit_bitmap_start ({ period.from, image, rect });
310         emit_stop (period.to);
311 }
312
313
314 void
315 TextDecoder::seek ()
316 {
317         _position = ContentTime ();
318 }
319
320
321 void
322 TextDecoder::maybe_set_position (dcpomatic::ContentTime position)
323 {
324         if (!_position || position > *_position) {
325                 _position = position;
326         }
327 }
328