Fix default placement of SRT (etc.) now that the alignment has changed
[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, dcp::Standard valign_standard)
106 {
107         vector<StringText> string_texts;
108
109         for (auto& subtitle: subtitles) {
110                 auto string_text = StringText(
111                         subtitle,
112                         content()->outline_width(),
113                         subtitle.font() ? content()->get_font(*subtitle.font()) : shared_ptr<Font>(),
114                         valign_standard
115                         );
116                 string_text.set_text(escape_text(string_text.text()));
117                 set_forced_appearance(content(), string_text);
118                 string_texts.push_back(string_text);
119         }
120
121         PlainStart(ContentStringText(from, string_texts));
122         maybe_set_position(from);
123 }
124
125
126 void
127 TextDecoder::emit_plain_start (ContentTime from, sub::Subtitle const & sub_subtitle)
128 {
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();
137                         }
138                 }
139         }
140
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;
147                         } else {
148                                 lowest_proportional = min(lowest_proportional.get(), line.vertical_position.proportional.get());
149                         }
150                 }
151         }
152
153         vector<StringText> string_texts;
154         for (auto line: sub_subtitle.lines) {
155                 for (auto block: line.blocks) {
156
157                         if (!block.font_size.specified()) {
158                                 /* Fallback default font size if no other has been specified */
159                                 block.font_size.set_points (48);
160                         }
161
162                         float v_position;
163                         dcp::VAlign v_align;
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.
172                                            */
173                                         v_position = 0.9 -
174                                                 (1 + bottom_line.get() - line.vertical_position.line.get()) * multiplier;
175
176                                         v_align = dcp::VAlign::TOP;
177                                         break;
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;
182                                         break;
183                                 case sub::VERTICAL_CENTRE_OF_SCREEN:
184                                         v_position = line.vertical_position.line.get() * multiplier;
185                                         v_align = dcp::VAlign::CENTER;
186                                         break;
187                                 }
188                         } else {
189                                 DCPOMATIC_ASSERT (line.vertical_position.reference);
190                                 if (line.vertical_position.proportional) {
191                                         v_position = line.vertical_position.proportional.get();
192                                 } else {
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;
196                                 }
197
198                                 if (lowest_proportional) {
199                                         /* Adjust line spacing */
200                                         v_position = ((v_position - lowest_proportional.get()) * content()->line_spacing()) + lowest_proportional.get();
201                                 }
202
203                                 switch (line.vertical_position.reference.get()) {
204                                 case sub::TOP_OF_SCREEN:
205                                         v_align = dcp::VAlign::TOP;
206                                         break;
207                                 case sub::VERTICAL_CENTRE_OF_SCREEN:
208                                         v_align = dcp::VAlign::CENTER;
209                                         break;
210                                 case sub::BOTTOM_OF_SCREEN:
211                                         v_align = dcp::VAlign::BOTTOM;
212                                         break;
213                                 default:
214                                         v_align = dcp::VAlign::TOP;
215                                         break;
216                                 }
217                         }
218
219                         dcp::HAlign h_align;
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);
225                                 break;
226                         case sub::HORIZONTAL_CENTRE_OF_SCREEN:
227                                 h_align = dcp::HAlign::CENTER;
228                                 break;
229                         case sub::RIGHT_OF_SCREEN:
230                                 h_align = dcp::HAlign::RIGHT;
231                                 h_position = max(h_position, 0.05f);
232                                 break;
233                         default:
234                                 h_align = dcp::HAlign::CENTER;
235                                 break;
236                         }
237
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.
241                         */
242
243                         auto dcp_subtitle = dcp::SubtitleString(
244                                 optional<string>(),
245                                 block.italic,
246                                 block.bold,
247                                 block.underline,
248                                 block.colour.dcp(),
249                                 block.font_size.points (72 * 11),
250                                 1.0,
251                                 dcp::Time (from.seconds(), 1000),
252                                 /* XXX: hmm; this is a bit ugly (we don't know the to time yet) */
253                                 dcp::Time (),
254                                 h_position,
255                                 h_align,
256                                 v_position,
257                                 v_align,
258                                 dcp::Direction::LTR,
259                                 escape_text(block.text),
260                                 dcp::Effect::NONE,
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.
267                                 */
268                                 dcp::Time (),
269                                 dcp::Time (),
270                                 0
271                                 );
272
273                         auto string_text = StringText(
274                                 dcp_subtitle,
275                                 content()->outline_width(),
276                                 content()->get_font(block.font.get_value_or("")),
277                                 dcp::Standard::SMPTE
278                                 );
279                         set_forced_appearance(content(), string_text);
280                         string_texts.push_back(string_text);
281                 }
282         }
283
284         PlainStart(ContentStringText(from, string_texts));
285         maybe_set_position(from);
286 }
287
288
289 void
290 TextDecoder::emit_stop (ContentTime to)
291 {
292         Stop (to);
293 }
294
295
296 void
297 TextDecoder::emit_plain (ContentTimePeriod period, vector<dcp::SubtitleString> subtitles, dcp::Standard valign_standard)
298 {
299         emit_plain_start (period.from, subtitles, valign_standard);
300         emit_stop (period.to);
301 }
302
303
304 void
305 TextDecoder::emit_plain (ContentTimePeriod period, sub::Subtitle const& subtitles)
306 {
307         emit_plain_start (period.from, subtitles);
308         emit_stop (period.to);
309 }
310
311
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)
315  */
316 void
317 TextDecoder::emit_bitmap (ContentTimePeriod period, shared_ptr<const Image> image, dcpomatic::Rect<double> rect)
318 {
319         emit_bitmap_start ({ period.from, image, rect });
320         emit_stop (period.to);
321 }
322
323
324 void
325 TextDecoder::seek ()
326 {
327         _position = ContentTime ();
328 }
329
330
331 void
332 TextDecoder::maybe_set_position (dcpomatic::ContentTime position)
333 {
334         if (!_position || position > *_position) {
335                 _position = position;
336         }
337 }
338