Bump libdcp for subtitle Z-position fixes (#2356).
[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                         content()->get_font(subtitle.font().get_value_or("")),
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                                 0,
259                                 dcp::Direction::LTR,
260                                 escape_text(block.text),
261                                 dcp::Effect::NONE,
262                                 block.effect_colour.get_value_or(sub::Colour(0, 0, 0)).dcp(),
263                                 /* Hack: we should use subtitle.fade_up and subtitle.fade_down here
264                                    but the times of these often don't have a frame rate associated
265                                    with them so the sub::Time won't convert them to milliseconds without
266                                    throwing an exception.  Since only DCP subs fill those in (and we don't
267                                    use libsub for DCP subs) we can cheat by just putting 0 in here.
268                                 */
269                                 dcp::Time (),
270                                 dcp::Time (),
271                                 0
272                                 );
273
274                         auto string_text = StringText(
275                                 dcp_subtitle,
276                                 content()->outline_width(),
277                                 content()->get_font(block.font.get_value_or("")),
278                                 dcp::Standard::SMPTE
279                                 );
280                         set_forced_appearance(content(), string_text);
281                         string_texts.push_back(string_text);
282                 }
283         }
284
285         PlainStart(ContentStringText(from, string_texts));
286         maybe_set_position(from);
287 }
288
289
290 void
291 TextDecoder::emit_stop (ContentTime to)
292 {
293         Stop (to);
294 }
295
296
297 void
298 TextDecoder::emit_plain (ContentTimePeriod period, vector<dcp::SubtitleString> subtitles, dcp::Standard valign_standard)
299 {
300         emit_plain_start (period.from, subtitles, valign_standard);
301         emit_stop (period.to);
302 }
303
304
305 void
306 TextDecoder::emit_plain (ContentTimePeriod period, sub::Subtitle const& subtitles)
307 {
308         emit_plain_start (period.from, subtitles);
309         emit_stop (period.to);
310 }
311
312
313 /*  @param rect Area expressed as a fraction of the video frame that this subtitle
314  *  is for (e.g. a width of 0.5 means the width of the subtitle is half the width
315  *  of the video frame)
316  */
317 void
318 TextDecoder::emit_bitmap (ContentTimePeriod period, shared_ptr<const Image> image, dcpomatic::Rect<double> rect)
319 {
320         emit_bitmap_start ({ period.from, image, rect });
321         emit_stop (period.to);
322 }
323
324
325 void
326 TextDecoder::seek ()
327 {
328         _position = ContentTime ();
329 }
330
331
332 void
333 TextDecoder::maybe_set_position (dcpomatic::ContentTime position)
334 {
335         if (!_position || position > *_position) {
336                 _position = position;
337         }
338 }
339