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