Fix crashes when using templates in some cases (#2491).
[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 void
70 set_forced_appearance(shared_ptr<const TextContent> content, StringText& subtitle)
71 {
72         if (content->colour()) {
73                 subtitle.set_colour(*content->colour());
74         }
75         if (content->effect_colour()) {
76                 subtitle.set_effect_colour(*content->effect_colour());
77         }
78         if (content->effect()) {
79                 subtitle.set_effect(*content->effect());
80         }
81         if (content->fade_in()) {
82                 subtitle.set_fade_up_time(dcp::Time(content->fade_in()->seconds(), 1000));
83         }
84         if (content->fade_out()) {
85                 subtitle.set_fade_down_time (dcp::Time(content->fade_out()->seconds(), 1000));
86         }
87 }
88
89
90 string
91 TextDecoder::remove_invalid_characters_for_xml(string text)
92 {
93         string output;
94
95         /* https://www.w3.org/TR/REC-xml/#charsets says that XML may only contain 0x9, 0xa, 0xd below 0x32.
96          * Not sure if we should be doing direct UTF-8 manipulation here.
97          */
98         for (size_t i = 0; i < text.length(); ++i) {
99                 auto const c = text[i];
100                 if ((c & 0xe0) == 0xc0) {
101                         // start of 2-byte code point
102                         output += c;
103                         output += text[i + 1];
104                         ++i;
105                 } else if ((c & 0xf0) == 0xe0) {
106                         // start of 3-byte code point
107                         output += c;
108                         output += text[i + 1];
109                         output += text[i + 2];
110                         i += 2;
111                 } else if ((c & 0xf8) == 0xf0) {
112                         // start of 4-byte code point
113                         output += c;
114                         output += text[i + 1];
115                         output += text[i + 2];
116                         output += text[i + 3];
117                         i += 3;
118                 } else {
119                         if (c >= 0x20 || c == 0x9 || c == 0xa || c == 0xd) {
120                                 output += c;
121                         }
122                 }
123         }
124
125         return output;
126 }
127
128
129 void
130 TextDecoder::emit_plain_start(ContentTime from, vector<dcp::SubtitleString> subtitles, dcp::SubtitleStandard valign_standard)
131 {
132         vector<StringText> string_texts;
133
134         for (auto& subtitle: subtitles) {
135                 auto string_text = StringText(
136                         subtitle,
137                         content()->outline_width(),
138                         content()->get_font(subtitle.font().get_value_or("")),
139                         valign_standard
140                         );
141                 string_text.set_text(remove_invalid_characters_for_xml(string_text.text()));
142                 set_forced_appearance(content(), string_text);
143                 string_texts.push_back(string_text);
144         }
145
146         PlainStart(ContentStringText(from, string_texts));
147         maybe_set_position(from);
148 }
149
150
151 void
152 TextDecoder::emit_plain_start (ContentTime from, sub::Subtitle const & sub_subtitle)
153 {
154         /* See if our next subtitle needs to be vertically placed on screen by us */
155         bool needs_placement = false;
156         optional<int> bottom_line;
157         for (auto line: sub_subtitle.lines) {
158                 if (!line.vertical_position.reference || (line.vertical_position.line && !line.vertical_position.lines) || line.vertical_position.reference.get() == sub::TOP_OF_SUBTITLE) {
159                         needs_placement = true;
160                         if (!bottom_line || bottom_line.get() < line.vertical_position.line.get()) {
161                                 bottom_line = line.vertical_position.line.get();
162                         }
163                 }
164         }
165
166         /* Find the lowest proportional position */
167         optional<float> lowest_proportional;
168         for (auto line: sub_subtitle.lines) {
169                 if (line.vertical_position.proportional) {
170                         if (!lowest_proportional) {
171                                 lowest_proportional = line.vertical_position.proportional;
172                         } else {
173                                 lowest_proportional = min(lowest_proportional.get(), line.vertical_position.proportional.get());
174                         }
175                 }
176         }
177
178         vector<StringText> string_texts;
179         for (auto line: sub_subtitle.lines) {
180                 for (auto block: line.blocks) {
181
182                         if (!block.font_size.specified()) {
183                                 /* Fallback default font size if no other has been specified */
184                                 block.font_size.set_points (48);
185                         }
186
187                         float v_position;
188                         dcp::VAlign v_align;
189                         if (needs_placement) {
190                                 DCPOMATIC_ASSERT (line.vertical_position.line);
191                                 double const multiplier = 1.2 * content()->line_spacing() * content()->y_scale() * block.font_size.proportional (72 * 11);
192                                 switch (line.vertical_position.reference.get_value_or(sub::BOTTOM_OF_SCREEN)) {
193                                 case sub::BOTTOM_OF_SCREEN:
194                                 case sub::TOP_OF_SUBTITLE:
195                                         /* This 0.1 is an arbitrary value to lift the bottom sub off the bottom
196                                            of the screen a bit to a pleasing degree.
197                                            */
198                                         v_position = 0.1 +
199                                                 (1 + bottom_line.get() - line.vertical_position.line.get()) * multiplier;
200
201                                         /* Align our subtitles to the bottom of the screen, because if we are making a SMPTE
202                                          * DCP and the projection system uses the wrong standard to interpret vertical position,
203                                          * a bottom-aligned subtitle will be less wrong than a top-aligned one.  This is because
204                                          * in the top-aligned case the difference will be the distance between bbox top an
205                                          * baseline, but in the bottom-aligned case the difference will be between bbox bottom
206                                          * and baseline (which is shorter).
207                                          */
208                                         v_align = dcp::VAlign::BOTTOM;
209                                         break;
210                                 case sub::TOP_OF_SCREEN:
211                                         /* This 0.1 is another fudge factor to bring the top line away from the top of the screen a little */
212                                         v_position = 0.12 + line.vertical_position.line.get() * multiplier;
213                                         v_align = dcp::VAlign::TOP;
214                                         break;
215                                 case sub::VERTICAL_CENTRE_OF_SCREEN:
216                                         v_position = line.vertical_position.line.get() * multiplier;
217                                         v_align = dcp::VAlign::CENTER;
218                                         break;
219                                 }
220                         } else {
221                                 DCPOMATIC_ASSERT (line.vertical_position.reference);
222                                 if (line.vertical_position.proportional) {
223                                         v_position = line.vertical_position.proportional.get();
224                                 } else {
225                                         DCPOMATIC_ASSERT (line.vertical_position.line);
226                                         DCPOMATIC_ASSERT (line.vertical_position.lines);
227                                         v_position = float(*line.vertical_position.line) / *line.vertical_position.lines;
228                                 }
229
230                                 if (lowest_proportional) {
231                                         /* Adjust line spacing */
232                                         v_position = ((v_position - lowest_proportional.get()) * content()->line_spacing()) + lowest_proportional.get();
233                                 }
234
235                                 switch (line.vertical_position.reference.get()) {
236                                 case sub::TOP_OF_SCREEN:
237                                         v_align = dcp::VAlign::TOP;
238                                         break;
239                                 case sub::VERTICAL_CENTRE_OF_SCREEN:
240                                         v_align = dcp::VAlign::CENTER;
241                                         break;
242                                 case sub::BOTTOM_OF_SCREEN:
243                                         v_align = dcp::VAlign::BOTTOM;
244                                         break;
245                                 default:
246                                         v_align = dcp::VAlign::TOP;
247                                         break;
248                                 }
249                         }
250
251                         dcp::HAlign h_align;
252                         float h_position = line.horizontal_position.proportional;
253                         switch (line.horizontal_position.reference) {
254                         case sub::LEFT_OF_SCREEN:
255                                 h_align = dcp::HAlign::LEFT;
256                                 h_position = max(h_position, 0.05f);
257                                 break;
258                         case sub::HORIZONTAL_CENTRE_OF_SCREEN:
259                                 h_align = dcp::HAlign::CENTER;
260                                 break;
261                         case sub::RIGHT_OF_SCREEN:
262                                 h_align = dcp::HAlign::RIGHT;
263                                 h_position = max(h_position, 0.05f);
264                                 break;
265                         default:
266                                 h_align = dcp::HAlign::CENTER;
267                                 break;
268                         }
269
270                         /* The idea here (rightly or wrongly) is that we set the appearance based on the
271                            values in the libsub objects, and these are overridden with values from the
272                            content by the other emit_plain_start() above.
273                         */
274
275                         auto dcp_colour = [](sub::Colour const& c) {
276                                 return dcp::Colour(lrintf(c.r * 255), lrintf(c.g * 255), lrintf(c.b * 255));
277                                 };
278
279                         auto dcp_subtitle = dcp::SubtitleString(
280                                 optional<string>(),
281                                 block.italic,
282                                 block.bold,
283                                 block.underline,
284                                 dcp_colour(block.colour),
285                                 block.font_size.points (72 * 11),
286                                 1.0,
287                                 dcp::Time (from.seconds(), 1000),
288                                 /* XXX: hmm; this is a bit ugly (we don't know the to time yet) */
289                                 dcp::Time (),
290                                 h_position,
291                                 h_align,
292                                 v_position,
293                                 v_align,
294                                 0,
295                                 dcp::Direction::LTR,
296                                 remove_invalid_characters_for_xml(block.text),
297                                 dcp::Effect::NONE,
298                                 dcp_colour(block.effect_colour.get_value_or(sub::Colour(0, 0, 0))),
299                                 /* Hack: we should use subtitle.fade_up and subtitle.fade_down here
300                                    but the times of these often don't have a frame rate associated
301                                    with them so the sub::Time won't convert them to milliseconds without
302                                    throwing an exception.  Since only DCP subs fill those in (and we don't
303                                    use libsub for DCP subs) we can cheat by just putting 0 in here.
304                                 */
305                                 dcp::Time (),
306                                 dcp::Time (),
307                                 0
308                                 );
309
310                         auto string_text = StringText(
311                                 dcp_subtitle,
312                                 content()->outline_width(),
313                                 content()->get_font(block.font.get_value_or("")),
314                                 dcp::SubtitleStandard::SMPTE_2014
315                                 );
316                         set_forced_appearance(content(), string_text);
317                         string_texts.push_back(string_text);
318                 }
319         }
320
321         PlainStart(ContentStringText(from, string_texts));
322         maybe_set_position(from);
323 }
324
325
326 void
327 TextDecoder::emit_stop (ContentTime to)
328 {
329         Stop (to);
330 }
331
332
333 void
334 TextDecoder::emit_plain(ContentTimePeriod period, vector<dcp::SubtitleString> subtitles, dcp::SubtitleStandard valign_standard)
335 {
336         emit_plain_start (period.from, subtitles, valign_standard);
337         emit_stop (period.to);
338 }
339
340
341 void
342 TextDecoder::emit_plain (ContentTimePeriod period, sub::Subtitle const& subtitles)
343 {
344         emit_plain_start (period.from, subtitles);
345         emit_stop (period.to);
346 }
347
348
349 /*  @param rect Area expressed as a fraction of the video frame that this subtitle
350  *  is for (e.g. a width of 0.5 means the width of the subtitle is half the width
351  *  of the video frame)
352  */
353 void
354 TextDecoder::emit_bitmap (ContentTimePeriod period, shared_ptr<const Image> image, dcpomatic::Rect<double> rect)
355 {
356         emit_bitmap_start ({ period.from, image, rect });
357         emit_stop (period.to);
358 }
359
360
361 void
362 TextDecoder::seek ()
363 {
364         _position = ContentTime ();
365 }
366
367
368 void
369 TextDecoder::maybe_set_position (dcpomatic::ContentTime position)
370 {
371         if (!_position || position > *_position) {
372                 _position = position;
373         }
374 }
375