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