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