Support more detailed horizontal positioning coming from libsub.
[dcpomatic.git] / src / lib / subtitle_decoder.cc
1 /*
2     Copyright (C) 2013-2017 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 #include "subtitle_decoder.h"
22 #include "subtitle_content.h"
23 #include "util.h"
24 #include "log.h"
25 #include "compose.hpp"
26 #include <sub/subtitle.h>
27 #include <boost/shared_ptr.hpp>
28 #include <boost/foreach.hpp>
29 #include <boost/algorithm/string.hpp>
30 #include <iostream>
31
32 using std::list;
33 using std::cout;
34 using std::string;
35 using std::min;
36 using boost::shared_ptr;
37 using boost::optional;
38 using boost::function;
39
40 SubtitleDecoder::SubtitleDecoder (
41         Decoder* parent,
42         shared_ptr<const SubtitleContent> c,
43         shared_ptr<Log> log
44         )
45         : DecoderPart (parent, log)
46         , _content (c)
47 {
48
49 }
50
51 /** Called by subclasses when an image subtitle is starting.
52  *  @param from From time of the subtitle.
53  *  @param image Subtitle image.
54  *  @param rect Area expressed as a fraction of the video frame that this subtitle
55  *  is for (e.g. a width of 0.5 means the width of the subtitle is half the width
56  *  of the video frame)
57  */
58 void
59 SubtitleDecoder::emit_image_start (ContentTime from, shared_ptr<Image> image, dcpomatic::Rect<double> rect)
60 {
61         ImageStart (ContentImageSubtitle (from, image, rect));
62 }
63
64 void
65 SubtitleDecoder::emit_text_start (ContentTime from, list<dcp::SubtitleString> s)
66 {
67         /* We must escape < and > in strings, otherwise they might confuse our subtitle
68            renderer (which uses some HTML-esque markup to do bold/italic etc.)
69         */
70         BOOST_FOREACH (dcp::SubtitleString& i, s) {
71                 string t = i.text ();
72                 boost::algorithm::replace_all (t, "<", "&lt;");
73                 boost::algorithm::replace_all (t, ">", "&gt;");
74                 i.set_text (t);
75         }
76
77         TextStart (ContentTextSubtitle (from, s));
78         _position = from;
79 }
80
81 void
82 SubtitleDecoder::emit_text_start (ContentTime from, sub::Subtitle const & subtitle)
83 {
84         /* See if our next subtitle needs to be vertically placed on screen by us */
85         bool needs_placement = false;
86         optional<int> bottom_line;
87         BOOST_FOREACH (sub::Line i, subtitle.lines) {
88                 if (!i.vertical_position.reference || i.vertical_position.reference.get() == sub::TOP_OF_SUBTITLE) {
89                         needs_placement = true;
90                         DCPOMATIC_ASSERT (i.vertical_position.line);
91                         if (!bottom_line || bottom_line.get() < i.vertical_position.line.get()) {
92                                 bottom_line = i.vertical_position.line.get();
93                         }
94                 }
95         }
96
97         /* Find the lowest proportional position */
98         optional<float> lowest_proportional;
99         BOOST_FOREACH (sub::Line i, subtitle.lines) {
100                 if (i.vertical_position.proportional) {
101                         if (!lowest_proportional) {
102                                 lowest_proportional = i.vertical_position.proportional;
103                         } else {
104                                 lowest_proportional = min (lowest_proportional.get(), i.vertical_position.proportional.get());
105                         }
106                 }
107         }
108
109         list<dcp::SubtitleString> out;
110         BOOST_FOREACH (sub::Line i, subtitle.lines) {
111                 BOOST_FOREACH (sub::Block j, i.blocks) {
112
113                         if (!j.font_size.specified()) {
114                                 /* Fallback default font size if no other has been specified */
115                                 j.font_size.set_points (48);
116                         }
117
118                         float v_position;
119                         dcp::VAlign v_align;
120                         if (needs_placement) {
121                                 DCPOMATIC_ASSERT (i.vertical_position.line);
122                                 /* This 1.015 is an arbitrary value to lift the bottom sub off the bottom
123                                    of the screen a bit to a pleasing degree.
124                                 */
125                                 v_position = 1.015 -
126                                         (1 + bottom_line.get() - i.vertical_position.line.get())
127                                         * 1.2 * content()->line_spacing() * content()->y_scale() * j.font_size.proportional (72 * 11);
128
129                                 v_align = dcp::VALIGN_TOP;
130                         } else {
131                                 DCPOMATIC_ASSERT (i.vertical_position.proportional);
132                                 DCPOMATIC_ASSERT (i.vertical_position.reference);
133                                 v_position = i.vertical_position.proportional.get();
134
135                                 if (lowest_proportional) {
136                                         /* Adjust line spacing */
137                                         v_position = ((v_position - lowest_proportional.get()) * content()->line_spacing()) + lowest_proportional.get();
138                                 }
139
140                                 switch (i.vertical_position.reference.get()) {
141                                 case sub::TOP_OF_SCREEN:
142                                         v_align = dcp::VALIGN_TOP;
143                                         break;
144                                 case sub::VERTICAL_CENTRE_OF_SCREEN:
145                                         v_align = dcp::VALIGN_CENTER;
146                                         break;
147                                 case sub::BOTTOM_OF_SCREEN:
148                                         v_align = dcp::VALIGN_BOTTOM;
149                                         break;
150                                 default:
151                                         v_align = dcp::VALIGN_TOP;
152                                         break;
153                                 }
154                         }
155
156                         dcp::Effect effect = dcp::NONE;
157                         if (content()->outline()) {
158                                 effect = dcp::BORDER;
159                         } else if (content()->shadow()) {
160                                 effect = dcp::SHADOW;
161                         }
162
163                         dcp::HAlign h_align;
164                         switch (i.horizontal_position.reference) {
165                         case sub::LEFT_OF_SCREEN:
166                                 h_align = dcp::HALIGN_LEFT;
167                                 break;
168                         case sub::HORIZONTAL_CENTRE_OF_SCREEN:
169                                 h_align = dcp::HALIGN_CENTER;
170                                 break;
171                         case sub::RIGHT_OF_SCREEN:
172                                 h_align = dcp::HALIGN_RIGHT;
173                                 break;
174                         }
175
176                         out.push_back (
177                                 dcp::SubtitleString (
178                                         string(TEXT_FONT_ID),
179                                         j.italic,
180                                         j.bold,
181                                         j.underline,
182                                         /* force the colour to whatever is configured */
183                                         content()->colour(),
184                                         j.font_size.points (72 * 11),
185                                         1.0,
186                                         dcp::Time (from.seconds(), 1000),
187                                         /* XXX: hmm; this is a bit ugly (we don't know the to time yet) */
188                                         dcp::Time (),
189                                         i.horizontal_position.proportional,
190                                         h_align,
191                                         v_position,
192                                         v_align,
193                                         dcp::DIRECTION_LTR,
194                                         j.text,
195                                         effect,
196                                         content()->effect_colour(),
197                                         dcp::Time (content()->fade_in().seconds(), 1000),
198                                         dcp::Time (content()->fade_out().seconds(), 1000)
199                                         )
200                                 );
201                 }
202         }
203
204         emit_text_start (from, out);
205 }
206
207 void
208 SubtitleDecoder::emit_stop (ContentTime to)
209 {
210         Stop (to);
211 }
212
213 void
214 SubtitleDecoder::emit_text (ContentTimePeriod period, list<dcp::SubtitleString> s)
215 {
216         emit_text_start (period.from, s);
217         emit_stop (period.to);
218 }
219
220 void
221 SubtitleDecoder::emit_text (ContentTimePeriod period, sub::Subtitle const & s)
222 {
223         emit_text_start (period.from, s);
224         emit_stop (period.to);
225 }
226
227 void
228 SubtitleDecoder::seek ()
229 {
230         _position = ContentTime ();
231 }