Add OpenSSL licence exception.
[libdcp.git] / src / subtitle_asset.cc
1 /*
2     Copyright (C) 2012-2015 Carl Hetherington <cth@carlh.net>
3
4     This file is part of libdcp.
5
6     libdcp 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     libdcp 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 libdcp.  If not, see <http://www.gnu.org/licenses/>.
18
19     In addition, as a special exception, the copyright holders give
20     permission to link the code of portions of this program with the
21     OpenSSL library under certain conditions as described in each
22     individual source file, and distribute linked combinations
23     including the two.
24
25     You must obey the GNU General Public License in all respects
26     for all of the code used other than OpenSSL.  If you modify
27     file(s) with this exception, you may extend this exception to your
28     version of the file(s), but you are not obligated to do so.  If you
29     do not wish to do so, delete this exception statement from your
30     version.  If you delete this exception statement from all source
31     files in the program, then also delete it here.
32 */
33
34 #include "raw_convert.h"
35 #include "subtitle_asset.h"
36 #include "util.h"
37 #include "xml.h"
38 #include "font_node.h"
39 #include "text_node.h"
40 #include "subtitle_string.h"
41 #include "dcp_assert.h"
42 #include "AS_DCP.h"
43 #include "KM_util.h"
44 #include <libxml++/nodes/element.h>
45 #include <boost/algorithm/string.hpp>
46 #include <boost/shared_array.hpp>
47 #include <boost/foreach.hpp>
48 #include <fstream>
49
50 using std::string;
51 using std::list;
52 using std::ostream;
53 using std::ofstream;
54 using std::stringstream;
55 using std::cout;
56 using std::cerr;
57 using std::map;
58 using boost::shared_ptr;
59 using boost::shared_array;
60 using boost::optional;
61 using boost::dynamic_pointer_cast;
62 using namespace dcp;
63
64 SubtitleAsset::SubtitleAsset ()
65 {
66
67 }
68
69 SubtitleAsset::SubtitleAsset (boost::filesystem::path file)
70         : Asset (file)
71 {
72
73 }
74
75 void
76 SubtitleAsset::parse_subtitles (
77         shared_ptr<cxml::Document> xml,
78         list<shared_ptr<dcp::FontNode> > font_nodes,
79         list<shared_ptr<dcp::SubtitleNode> > subtitle_nodes
80         )
81 {
82         /* Make Subtitle objects to represent the raw XML nodes in a sane way */
83         ParseState parse_state;
84         examine_nodes (xml, font_nodes, parse_state);
85         examine_nodes (xml, subtitle_nodes, parse_state);
86 }
87
88 void
89 SubtitleAsset::examine_nodes (
90         shared_ptr<const cxml::Node> xml,
91         list<shared_ptr<dcp::SubtitleNode> > const & subtitle_nodes,
92         ParseState& parse_state
93         )
94 {
95         BOOST_FOREACH (shared_ptr<dcp::SubtitleNode> i, subtitle_nodes) {
96                 parse_state.subtitle_nodes.push_back (i);
97                 examine_nodes (xml, i->text_nodes, parse_state);
98                 examine_nodes (xml, i->font_nodes, parse_state);
99                 parse_state.subtitle_nodes.pop_back ();
100         }
101 }
102
103 void
104 SubtitleAsset::examine_nodes (
105         shared_ptr<const cxml::Node> xml,
106         list<shared_ptr<dcp::FontNode> > const & font_nodes,
107         ParseState& parse_state
108         )
109 {
110         BOOST_FOREACH (shared_ptr<dcp::FontNode> i, font_nodes) {
111
112                 parse_state.font_nodes.push_back (i);
113                 maybe_add_subtitle (i->text, parse_state);
114
115                 examine_nodes (xml, i->subtitle_nodes, parse_state);
116                 examine_nodes (xml, i->font_nodes, parse_state);
117                 examine_nodes (xml, i->text_nodes, parse_state);
118
119                 parse_state.font_nodes.pop_back ();
120         }
121 }
122
123 void
124 SubtitleAsset::examine_nodes (
125         shared_ptr<const cxml::Node> xml,
126         list<shared_ptr<dcp::TextNode> > const & text_nodes,
127         ParseState& parse_state
128         )
129 {
130         BOOST_FOREACH (shared_ptr<dcp::TextNode> i, text_nodes) {
131                 parse_state.text_nodes.push_back (i);
132                 maybe_add_subtitle (i->text, parse_state);
133                 examine_nodes (xml, i->font_nodes, parse_state);
134                 parse_state.text_nodes.pop_back ();
135         }
136 }
137
138 void
139 SubtitleAsset::maybe_add_subtitle (string text, ParseState const & parse_state)
140 {
141         if (empty_or_white_space (text)) {
142                 return;
143         }
144
145         if (parse_state.text_nodes.empty() || parse_state.subtitle_nodes.empty ()) {
146                 return;
147         }
148
149         DCP_ASSERT (!parse_state.text_nodes.empty ());
150         DCP_ASSERT (!parse_state.subtitle_nodes.empty ());
151
152         dcp::FontNode effective_font (parse_state.font_nodes);
153         dcp::TextNode effective_text (*parse_state.text_nodes.back ());
154         dcp::SubtitleNode effective_subtitle (*parse_state.subtitle_nodes.back ());
155
156         _subtitles.push_back (
157                 SubtitleString (
158                         effective_font.id,
159                         effective_font.italic.get_value_or (false),
160                         effective_font.bold.get_value_or (false),
161                         effective_font.colour.get_value_or (dcp::Colour (255, 255, 255)),
162                         effective_font.size,
163                         effective_font.aspect_adjust.get_value_or (1.0),
164                         effective_subtitle.in,
165                         effective_subtitle.out,
166                         effective_text.h_position,
167                         effective_text.h_align,
168                         effective_text.v_position,
169                         effective_text.v_align,
170                         effective_text.direction,
171                         text,
172                         effective_font.effect.get_value_or (NONE),
173                         effective_font.effect_colour.get_value_or (dcp::Colour (0, 0, 0)),
174                         effective_subtitle.fade_up_time,
175                         effective_subtitle.fade_down_time
176                         )
177                 );
178 }
179
180 list<SubtitleString>
181 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
182 {
183         list<SubtitleString> s;
184         BOOST_FOREACH (SubtitleString const & i, _subtitles) {
185                 if ((starting && from <= i.in() && i.in() < to) || (!starting && i.out() >= from && i.in() <= to)) {
186                         s.push_back (i);
187                 }
188         }
189
190         return s;
191 }
192
193 void
194 SubtitleAsset::add (SubtitleString s)
195 {
196         _subtitles.push_back (s);
197 }
198
199 Time
200 SubtitleAsset::latest_subtitle_out () const
201 {
202         Time t;
203         BOOST_FOREACH (SubtitleString const & i, _subtitles) {
204                 if (i.out() > t) {
205                         t = i.out ();
206                 }
207         }
208
209         return t;
210 }
211
212 bool
213 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
214 {
215         if (!Asset::equals (other_asset, options, note)) {
216                 return false;
217         }
218
219         shared_ptr<const SubtitleAsset> other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
220         if (!other) {
221                 return false;
222         }
223
224         if (_subtitles != other->_subtitles) {
225                 note (DCP_ERROR, "subtitles differ");
226                 return false;
227         }
228
229         return true;
230 }
231
232 struct SubtitleSorter {
233         bool operator() (SubtitleString const & a, SubtitleString const & b) {
234                 if (a.in() != b.in()) {
235                         return a.in() < b.in();
236                 }
237                 return a.v_position() < b.v_position();
238         }
239 };
240
241 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
242  *  class because the differences between the two are fairly subtle.
243  */
244 void
245 SubtitleAsset::subtitles_as_xml (xmlpp::Element* root, int time_code_rate, Standard standard) const
246 {
247         list<SubtitleString> sorted = _subtitles;
248         sorted.sort (SubtitleSorter ());
249
250         string const xmlns = standard == SMPTE ? "dcst" : "";
251
252         /* XXX: script, underlined not supported */
253
254         optional<string> font;
255         bool italic = false;
256         bool bold = false;
257         Colour colour;
258         int size = 0;
259         float aspect_adjust = 1.0;
260         Effect effect = NONE;
261         Colour effect_colour;
262         int spot_number = 1;
263         Time last_in;
264         Time last_out;
265         Time last_fade_up_time;
266         Time last_fade_down_time;
267
268         xmlpp::Element* font_element = 0;
269         xmlpp::Element* subtitle_element = 0;
270
271         BOOST_FOREACH (SubtitleString const & i, sorted) {
272
273                 /* We will start a new <Font>...</Font> whenever some font property changes.
274                    I suppose we should really make an optimal hierarchy of <Font> tags, but
275                    that seems hard.
276                 */
277
278                 bool const font_changed =
279                         font          != i.font()          ||
280                         italic        != i.italic()        ||
281                         bold          != i.bold()          ||
282                         colour        != i.colour()        ||
283                         size          != i.size()          ||
284                         fabs (aspect_adjust - i.aspect_adjust()) > ASPECT_ADJUST_EPSILON ||
285                         effect        != i.effect()        ||
286                         effect_colour != i.effect_colour();
287
288                 if (font_changed) {
289                         font = i.font ();
290                         italic = i.italic ();
291                         bold = i.bold ();
292                         colour = i.colour ();
293                         size = i.size ();
294                         aspect_adjust = i.aspect_adjust ();
295                         effect = i.effect ();
296                         effect_colour = i.effect_colour ();
297                 }
298
299                 if (!font_element || font_changed) {
300                         font_element = root->add_child ("Font", xmlns);
301                         if (font) {
302                                 if (standard == SMPTE) {
303                                         font_element->set_attribute ("ID", font.get ());
304                                 } else {
305                                         font_element->set_attribute ("Id", font.get ());
306                                 }
307                         }
308                         font_element->set_attribute ("Italic", italic ? "yes" : "no");
309                         font_element->set_attribute ("Color", colour.to_argb_string());
310                         font_element->set_attribute ("Size", raw_convert<string> (size));
311                         if (fabs (aspect_adjust - 1.0) > ASPECT_ADJUST_EPSILON) {
312                                 font_element->set_attribute ("AspectAdjust", raw_convert<string> (aspect_adjust));
313                         }
314                         font_element->set_attribute ("Effect", effect_to_string (effect));
315                         font_element->set_attribute ("EffectColor", effect_colour.to_argb_string());
316                         font_element->set_attribute ("Script", "normal");
317                         if (standard == SMPTE) {
318                                 font_element->set_attribute ("Underline", "no");
319                         } else {
320                                 font_element->set_attribute ("Underlined", "no");
321                         }
322                         font_element->set_attribute ("Weight", bold ? "bold" : "normal");
323                 }
324
325                 if (!subtitle_element || font_changed ||
326                     (last_in != i.in() ||
327                      last_out != i.out() ||
328                      last_fade_up_time != i.fade_up_time() ||
329                      last_fade_down_time != i.fade_down_time()
330                             )) {
331
332                         subtitle_element = font_element->add_child ("Subtitle", xmlns);
333                         subtitle_element->set_attribute ("SpotNumber", raw_convert<string> (spot_number++));
334                         subtitle_element->set_attribute ("TimeIn", i.in().rebase(time_code_rate).as_string(standard));
335                         subtitle_element->set_attribute ("TimeOut", i.out().rebase(time_code_rate).as_string(standard));
336                         if (standard == SMPTE) {
337                                 subtitle_element->set_attribute ("FadeUpTime", i.fade_up_time().rebase(time_code_rate).as_string(standard));
338                                 subtitle_element->set_attribute ("FadeDownTime", i.fade_down_time().rebase(time_code_rate).as_string(standard));
339                         } else {
340                                 subtitle_element->set_attribute ("FadeUpTime", raw_convert<string> (i.fade_up_time().as_editable_units(time_code_rate)));
341                                 subtitle_element->set_attribute ("FadeDownTime", raw_convert<string> (i.fade_down_time().as_editable_units(time_code_rate)));
342                         }
343
344                         last_in = i.in ();
345                         last_out = i.out ();
346                         last_fade_up_time = i.fade_up_time ();
347                         last_fade_down_time = i.fade_down_time ();
348                 }
349
350                 xmlpp::Element* text = subtitle_element->add_child ("Text", xmlns);
351
352                 if (i.h_align() != HALIGN_CENTER) {
353                         if (standard == SMPTE) {
354                                 text->set_attribute ("Halign", halign_to_string (i.h_align ()));
355                         } else {
356                                 text->set_attribute ("HAlign", halign_to_string (i.h_align ()));
357                         }
358                 }
359
360                 if (i.h_position() > ALIGN_EPSILON) {
361                         if (standard == SMPTE) {
362                                 text->set_attribute ("Hposition", raw_convert<string> (i.h_position() * 100, 6));
363                         } else {
364                                 text->set_attribute ("HPosition", raw_convert<string> (i.h_position() * 100, 6));
365                         }
366                 }
367
368                 if (standard == SMPTE) {
369                         text->set_attribute ("Valign", valign_to_string (i.v_align()));
370                 } else {
371                         text->set_attribute ("VAlign", valign_to_string (i.v_align()));
372                 }
373
374                 if (i.v_position() > ALIGN_EPSILON) {
375                         if (standard == SMPTE) {
376                                 text->set_attribute ("Vposition", raw_convert<string> (i.v_position() * 100, 6));
377                         } else {
378                                 text->set_attribute ("VPosition", raw_convert<string> (i.v_position() * 100, 6));
379                         }
380                 } else {
381                         if (standard == SMPTE) {
382                                 text->set_attribute ("Vposition", "0");
383                         } else {
384                                 text->set_attribute ("VPosition", "0");
385                         }
386                 }
387
388                 /* Interop only supports "horizontal" or "vertical" for direction, so only write this
389                    for SMPTE.
390                 */
391                 if (i.direction() != DIRECTION_LTR && standard == SMPTE) {
392                         text->set_attribute ("Direction", direction_to_string (i.direction ()));
393                 }
394
395                 text->add_child_text (i.text());
396         }
397 }
398
399 map<string, Data>
400 SubtitleAsset::fonts_with_load_ids () const
401 {
402         map<string, Data> out;
403         BOOST_FOREACH (Font const & i, _fonts) {
404                 out[i.load_id] = i.data;
405         }
406         return out;
407 }