c6563a260fe238656a2c65889c13f8cdb06d1298
[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         ContentTime first
46         )
47         : DecoderPart (parent)
48         , _content (content)
49         , _position (first)
50 {
51
52 }
53
54
55 /** Called by subclasses when an image subtitle is starting.
56  *  @param from From time of the subtitle.
57  *  @param image Subtitle image.
58  *  @param rect Area expressed as a fraction of the video frame that this subtitle
59  *  is for (e.g. a width of 0.5 means the width of the subtitle is half the width
60  *  of the video frame)
61  */
62 void
63 TextDecoder::emit_bitmap_start (ContentBitmapText const& bitmap)
64 {
65         BitmapStart (bitmap);
66         _position = bitmap.from();
67 }
68
69
70 static
71 string
72 escape_text (string text)
73 {
74         /* We must escape some things, otherwise they might confuse our subtitle
75            renderer (which uses entities and some HTML-esque markup to do bold/italic etc.)
76         */
77         boost::algorithm::replace_all(text, "&", "&amp;");
78         boost::algorithm::replace_all(text, "<", "&lt;");
79         boost::algorithm::replace_all(text, ">", "&gt;");
80         return text;
81 }
82
83
84 void
85 TextDecoder::emit_plain_start (ContentTime from, vector<dcp::SubtitleString> subtitles)
86 {
87         for (auto& subtitle: subtitles) {
88                 subtitle.set_text(escape_text(subtitle.text()));
89
90                 /* Set any forced appearance */
91                 if (content()->colour()) {
92                         subtitle.set_colour(*content()->colour());
93                 }
94                 if (content()->effect_colour()) {
95                         subtitle.set_effect_colour(*content()->effect_colour());
96                 }
97                 if (content()->effect()) {
98                         subtitle.set_effect(*content()->effect());
99                 }
100                 if (content()->fade_in()) {
101                         subtitle.set_fade_up_time(dcp::Time(content()->fade_in()->seconds(), 1000));
102                 }
103                 if (content()->fade_out()) {
104                         subtitle.set_fade_down_time (dcp::Time(content()->fade_out()->seconds(), 1000));
105                 }
106         }
107
108         PlainStart(ContentStringText(from, subtitles));
109         _position = from;
110 }
111
112
113 void
114 TextDecoder::emit_plain_start (ContentTime from, sub::Subtitle const & subtitle)
115 {
116         /* See if our next subtitle needs to be vertically placed on screen by us */
117         bool needs_placement = false;
118         optional<int> bottom_line;
119         for (auto line: subtitle.lines) {
120                 if (!line.vertical_position.reference || (line.vertical_position.line && !line.vertical_position.lines) || line.vertical_position.reference.get() == sub::TOP_OF_SUBTITLE) {
121                         needs_placement = true;
122                         if (!bottom_line || bottom_line.get() < line.vertical_position.line.get()) {
123                                 bottom_line = line.vertical_position.line.get();
124                         }
125                 }
126         }
127
128         /* Find the lowest proportional position */
129         optional<float> lowest_proportional;
130         for (auto line: subtitle.lines) {
131                 if (line.vertical_position.proportional) {
132                         if (!lowest_proportional) {
133                                 lowest_proportional = line.vertical_position.proportional;
134                         } else {
135                                 lowest_proportional = min(lowest_proportional.get(), line.vertical_position.proportional.get());
136                         }
137                 }
138         }
139
140         vector<dcp::SubtitleString> out;
141         for (auto line: subtitle.lines) {
142                 for (auto block: line.blocks) {
143
144                         if (!block.font_size.specified()) {
145                                 /* Fallback default font size if no other has been specified */
146                                 block.font_size.set_points (48);
147                         }
148
149                         float v_position;
150                         dcp::VAlign v_align;
151                         if (needs_placement) {
152                                 DCPOMATIC_ASSERT (line.vertical_position.line);
153                                 double const multiplier = 1.2 * content()->line_spacing() * content()->y_scale() * block.font_size.proportional (72 * 11);
154                                 switch (line.vertical_position.reference.get_value_or(sub::BOTTOM_OF_SCREEN)) {
155                                 case sub::BOTTOM_OF_SCREEN:
156                                 case sub::TOP_OF_SUBTITLE:
157                                         /* This 1.015 is an arbitrary value to lift the bottom sub off the bottom
158                                            of the screen a bit to a pleasing degree.
159                                            */
160                                         v_position = 1.015 -
161                                                 (1 + bottom_line.get() - line.vertical_position.line.get()) * multiplier;
162
163                                         v_align = dcp::VAlign::TOP;
164                                         break;
165                                 case sub::TOP_OF_SCREEN:
166                                         /* This 0.1 is another fudge factor to bring the top line away from the top of the screen a little */
167                                         v_position = 0.12 + line.vertical_position.line.get() * multiplier;
168                                         v_align = dcp::VAlign::TOP;
169                                         break;
170                                 case sub::VERTICAL_CENTRE_OF_SCREEN:
171                                         v_position = line.vertical_position.line.get() * multiplier;
172                                         v_align = dcp::VAlign::CENTER;
173                                         break;
174                                 }
175                         } else {
176                                 DCPOMATIC_ASSERT (line.vertical_position.reference);
177                                 if (line.vertical_position.proportional) {
178                                         v_position = line.vertical_position.proportional.get();
179                                 } else {
180                                         DCPOMATIC_ASSERT (line.vertical_position.line);
181                                         DCPOMATIC_ASSERT (line.vertical_position.lines);
182                                         v_position = float(*line.vertical_position.line) / *line.vertical_position.lines;
183                                 }
184
185                                 if (lowest_proportional) {
186                                         /* Adjust line spacing */
187                                         v_position = ((v_position - lowest_proportional.get()) * content()->line_spacing()) + lowest_proportional.get();
188                                 }
189
190                                 switch (line.vertical_position.reference.get()) {
191                                 case sub::TOP_OF_SCREEN:
192                                         v_align = dcp::VAlign::TOP;
193                                         break;
194                                 case sub::VERTICAL_CENTRE_OF_SCREEN:
195                                         v_align = dcp::VAlign::CENTER;
196                                         break;
197                                 case sub::BOTTOM_OF_SCREEN:
198                                         v_align = dcp::VAlign::BOTTOM;
199                                         break;
200                                 default:
201                                         v_align = dcp::VAlign::TOP;
202                                         break;
203                                 }
204                         }
205
206                         dcp::HAlign h_align;
207                         float h_position = line.horizontal_position.proportional;
208                         switch (line.horizontal_position.reference) {
209                         case sub::LEFT_OF_SCREEN:
210                                 h_align = dcp::HAlign::LEFT;
211                                 h_position = max(h_position, 0.05f);
212                                 break;
213                         case sub::HORIZONTAL_CENTRE_OF_SCREEN:
214                                 h_align = dcp::HAlign::CENTER;
215                                 break;
216                         case sub::RIGHT_OF_SCREEN:
217                                 h_align = dcp::HAlign::RIGHT;
218                                 h_position = max(h_position, 0.05f);
219                                 break;
220                         default:
221                                 h_align = dcp::HAlign::CENTER;
222                                 break;
223                         }
224
225                         /* The idea here (rightly or wrongly) is that we set the appearance based on the
226                            values in the libsub objects, and these are overridden with values from the
227                            content by the other emit_plain_start() above.
228                         */
229
230                         out.push_back (
231                                 dcp::SubtitleString (
232                                         string(TEXT_FONT_ID),
233                                         block.italic,
234                                         block.bold,
235                                         block.underline,
236                                         block.colour.dcp(),
237                                         block.font_size.points (72 * 11),
238                                         1.0,
239                                         dcp::Time (from.seconds(), 1000),
240                                         /* XXX: hmm; this is a bit ugly (we don't know the to time yet) */
241                                         dcp::Time (),
242                                         h_position,
243                                         h_align,
244                                         v_position,
245                                         v_align,
246                                         dcp::Direction::LTR,
247                                         block.text,
248                                         dcp::Effect::NONE,
249                                         block.effect_colour.get_value_or(sub::Colour(0, 0, 0)).dcp(),
250                                         /* Hack: we should use subtitle.fade_up and subtitle.fade_down here
251                                            but the times of these often don't have a frame rate associated
252                                            with them so the sub::Time won't convert them to milliseconds without
253                                            throwing an exception.  Since only DCP subs fill those in (and we don't
254                                            use libsub for DCP subs) we can cheat by just putting 0 in here.
255                                         */
256                                         dcp::Time (),
257                                         dcp::Time (),
258                                         0
259                                         )
260                                 );
261                 }
262         }
263
264         /* Pass these subs through the other emit_plain_start so that they get their forced settings applied */
265         emit_plain_start (from, out);
266 }
267
268
269 void
270 TextDecoder::emit_stop (ContentTime to)
271 {
272         Stop (to);
273 }
274
275
276 void
277 TextDecoder::emit_plain (ContentTimePeriod period, vector<dcp::SubtitleString> subtitles)
278 {
279         emit_plain_start (period.from, subtitles);
280         emit_stop (period.to);
281 }
282
283
284 void
285 TextDecoder::emit_plain (ContentTimePeriod period, sub::Subtitle const& subtitles)
286 {
287         emit_plain_start (period.from, subtitles);
288         emit_stop (period.to);
289 }
290
291
292 /*  @param rect Area expressed as a fraction of the video frame that this subtitle
293  *  is for (e.g. a width of 0.5 means the width of the subtitle is half the width
294  *  of the video frame)
295  */
296 void
297 TextDecoder::emit_bitmap (ContentTimePeriod period, shared_ptr<const Image> image, dcpomatic::Rect<double> rect)
298 {
299         emit_bitmap_start ({ period.from, image, rect });
300         emit_stop (period.to);
301 }
302
303
304 void
305 TextDecoder::seek ()
306 {
307         _position = ContentTime ();
308 }