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