Merge branch '1.0' of git.carlh.net:git/libdcp into 1.0
[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 "compose.hpp"
36 #include "subtitle_asset.h"
37 #include "util.h"
38 #include "xml.h"
39 #include "subtitle_string.h"
40 #include "dcp_assert.h"
41 #include <asdcp/AS_DCP.h>
42 #include <asdcp/KM_util.h>
43 #include <libxml++/nodes/element.h>
44 #include <boost/algorithm/string.hpp>
45 #include <boost/lexical_cast.hpp>
46 #include <boost/shared_array.hpp>
47 #include <boost/foreach.hpp>
48
49 using std::string;
50 using std::list;
51 using std::cout;
52 using std::cerr;
53 using std::map;
54 using boost::shared_ptr;
55 using boost::shared_array;
56 using boost::optional;
57 using boost::dynamic_pointer_cast;
58 using boost::lexical_cast;
59 using namespace dcp;
60
61 SubtitleAsset::SubtitleAsset ()
62 {
63
64 }
65
66 SubtitleAsset::SubtitleAsset (boost::filesystem::path file)
67         : Asset (file)
68 {
69
70 }
71
72 string
73 string_attribute (xmlpp::Element const * node, string name)
74 {
75         xmlpp::Attribute* a = node->get_attribute (name);
76         if (!a) {
77                 throw XMLError (String::compose ("missing attribute %1", name));
78         }
79         return string (a->get_value ());
80 }
81
82 optional<string>
83 optional_string_attribute (xmlpp::Element const * node, string name)
84 {
85         xmlpp::Attribute* a = node->get_attribute (name);
86         if (!a) {
87                 return optional<string>();
88         }
89         return string (a->get_value ());
90 }
91
92 optional<bool>
93 optional_bool_attribute (xmlpp::Element const * node, string name)
94 {
95         optional<string> s = optional_string_attribute (node, name);
96         if (!s) {
97                 return optional<bool> ();
98         }
99
100         return (s.get() == "1" || s.get() == "yes");
101 }
102
103 template <class T>
104 optional<T>
105 optional_number_attribute (xmlpp::Element const * node, string name)
106 {
107         boost::optional<std::string> s = optional_string_attribute (node, name);
108         if (!s) {
109                 return boost::optional<T> ();
110         }
111
112         std::string t = s.get ();
113         boost::erase_all (t, " ");
114         locked_stringstream u;
115         u.imbue (std::locale::classic ());
116         u << t;
117         T n;
118         u >> n;
119         return n;
120 }
121
122 SubtitleAsset::ParseState
123 SubtitleAsset::font_node_state (xmlpp::Element const * node, Standard standard) const
124 {
125         ParseState ps;
126
127         if (standard == INTEROP) {
128                 ps.font_id = optional_string_attribute (node, "Id");
129         } else {
130                 ps.font_id = optional_string_attribute (node, "ID");
131         }
132         ps.size = optional_number_attribute<int64_t> (node, "Size");
133         ps.aspect_adjust = optional_number_attribute<float> (node, "AspectAdjust");
134         ps.italic = optional_bool_attribute (node, "Italic");
135         ps.bold = optional_string_attribute(node, "Weight").get_value_or("normal") == "bold";
136         if (standard == INTEROP) {
137                 ps.underline = optional_bool_attribute (node, "Underlined");
138         } else {
139                 ps.underline = optional_bool_attribute (node, "Underline");
140         }
141         optional<string> c = optional_string_attribute (node, "Color");
142         if (c) {
143                 ps.colour = Colour (c.get ());
144         }
145         optional<string> const e = optional_string_attribute (node, "Effect");
146         if (e) {
147                 ps.effect = string_to_effect (e.get ());
148         }
149         c = optional_string_attribute (node, "EffectColor");
150         if (c) {
151                 ps.effect_colour = Colour (c.get ());
152         }
153
154         return ps;
155 }
156
157 SubtitleAsset::ParseState
158 SubtitleAsset::text_node_state (xmlpp::Element const * node) const
159 {
160         ParseState ps;
161
162         optional<float> hp = optional_number_attribute<float> (node, "HPosition");
163         if (!hp) {
164                 hp = optional_number_attribute<float> (node, "Hposition");
165         }
166         if (hp) {
167                 ps.h_position = hp.get () / 100;
168         }
169
170         optional<string> ha = optional_string_attribute (node, "HAlign");
171         if (!ha) {
172                 ha = optional_string_attribute (node, "Halign");
173         }
174         if (ha) {
175                 ps.h_align = string_to_halign (ha.get ());
176         }
177
178         optional<float> vp = optional_number_attribute<float> (node, "VPosition");
179         if (!vp) {
180                 vp = optional_number_attribute<float> (node, "Vposition");
181         }
182         if (vp) {
183                 ps.v_position = vp.get () / 100;
184         }
185
186         optional<string> va = optional_string_attribute (node, "VAlign");
187         if (!va) {
188                 va = optional_string_attribute (node, "Valign");
189         }
190         if (va) {
191                 ps.v_align = string_to_valign (va.get ());
192         }
193
194         optional<string> d = optional_string_attribute (node, "Direction");
195         if (d) {
196                 ps.direction = string_to_direction (d.get ());
197         }
198
199         return ps;
200 }
201
202 SubtitleAsset::ParseState
203 SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional<int> tcr) const
204 {
205         ParseState ps;
206         ps.in = Time (string_attribute(node, "TimeIn"), tcr);
207         ps.out = Time (string_attribute(node, "TimeOut"), tcr);
208         ps.fade_up_time = fade_time (node, "FadeUpTime", tcr);
209         ps.fade_down_time = fade_time (node, "FadeDownTime", tcr);
210         return ps;
211 }
212
213 Time
214 SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional<int> tcr) const
215 {
216         string const u = optional_string_attribute(node, name).get_value_or ("");
217         Time t;
218
219         if (u.empty ()) {
220                 t = Time (0, 0, 0, 20, 250);
221         } else if (u.find (":") != string::npos) {
222                 t = Time (u, tcr);
223         } else {
224                 t = Time (0, 0, 0, lexical_cast<int> (u), tcr.get_value_or(250));
225         }
226
227         if (t > Time (0, 0, 8, 0, 250)) {
228                 t = Time (0, 0, 8, 0, 250);
229         }
230
231         return t;
232 }
233
234 void
235 SubtitleAsset::parse_subtitles (xmlpp::Element const * node, list<ParseState>& state, optional<int> tcr, Standard standard)
236 {
237         if (node->get_name() == "Font") {
238                 state.push_back (font_node_state (node, standard));
239         } else if (node->get_name() == "Subtitle") {
240                 state.push_back (subtitle_node_state (node, tcr));
241         } else if (node->get_name() == "Text") {
242                 state.push_back (text_node_state (node));
243         } else if (node->get_name() == "SubtitleList") {
244                 state.push_back (ParseState ());
245         } else {
246                 throw XMLError ("unexpected node " + node->get_name());
247         }
248
249         xmlpp::Node::NodeList c = node->get_children ();
250         for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
251                 xmlpp::ContentNode const * v = dynamic_cast<xmlpp::ContentNode const *> (*i);
252                 if (v) {
253                         maybe_add_subtitle (v->get_content(), state);
254                 }
255                 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
256                 if (e) {
257                         parse_subtitles (e, state, tcr, standard);
258                 }
259         }
260
261         state.pop_back ();
262 }
263
264 void
265 SubtitleAsset::maybe_add_subtitle (string text, list<ParseState> const & parse_state)
266 {
267         if (empty_or_white_space (text)) {
268                 return;
269         }
270
271         ParseState ps;
272         BOOST_FOREACH (ParseState const & i, parse_state) {
273                 if (i.font_id) {
274                         ps.font_id = i.font_id.get();
275                 }
276                 if (i.size) {
277                         ps.size = i.size.get();
278                 }
279                 if (i.aspect_adjust) {
280                         ps.aspect_adjust = i.aspect_adjust.get();
281                 }
282                 if (i.italic) {
283                         ps.italic = i.italic.get();
284                 }
285                 if (i.bold) {
286                         ps.bold = i.bold.get();
287                 }
288                 if (i.underline) {
289                         ps.underline = i.underline.get();
290                 }
291                 if (i.colour) {
292                         ps.colour = i.colour.get();
293                 }
294                 if (i.effect) {
295                         ps.effect = i.effect.get();
296                 }
297                 if (i.effect_colour) {
298                         ps.effect_colour = i.effect_colour.get();
299                 }
300                 if (i.h_position) {
301                         ps.h_position = i.h_position.get();
302                 }
303                 if (i.h_align) {
304                         ps.h_align = i.h_align.get();
305                 }
306                 if (i.v_position) {
307                         ps.v_position = i.v_position.get();
308                 }
309                 if (i.v_align) {
310                         ps.v_align = i.v_align.get();
311                 }
312                 if (i.direction) {
313                         ps.direction = i.direction.get();
314                 }
315                 if (i.in) {
316                         ps.in = i.in.get();
317                 }
318                 if (i.out) {
319                         ps.out = i.out.get();
320                 }
321                 if (i.fade_up_time) {
322                         ps.fade_up_time = i.fade_up_time.get();
323                 }
324                 if (i.fade_down_time) {
325                         ps.fade_down_time = i.fade_down_time.get();
326                 }
327         }
328
329         if (!ps.in || !ps.out) {
330                 /* We're not in a <Text> node; just ignore this content */
331                 return;
332         }
333
334         _subtitles.push_back (
335                 SubtitleString (
336                         ps.font_id,
337                         ps.italic.get_value_or (false),
338                         ps.bold.get_value_or (false),
339                         ps.underline.get_value_or (false),
340                         ps.colour.get_value_or (dcp::Colour (255, 255, 255)),
341                         ps.size.get_value_or (42),
342                         ps.aspect_adjust.get_value_or (1.0),
343                         ps.in.get(),
344                         ps.out.get(),
345                         ps.h_position.get_value_or(0),
346                         ps.h_align.get_value_or(HALIGN_CENTER),
347                         ps.v_position.get_value_or(0),
348                         ps.v_align.get_value_or(VALIGN_CENTER),
349                         ps.direction.get_value_or (DIRECTION_LTR),
350                         text,
351                         ps.effect.get_value_or (NONE),
352                         ps.effect_colour.get_value_or (dcp::Colour (255, 255, 255)),
353                         ps.fade_up_time.get_value_or(Time()),
354                         ps.fade_down_time.get_value_or(Time())
355                         )
356                 );
357 }
358
359 list<SubtitleString>
360 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
361 {
362         list<SubtitleString> s;
363         BOOST_FOREACH (SubtitleString const & i, _subtitles) {
364                 if ((starting && from <= i.in() && i.in() < to) || (!starting && i.out() >= from && i.in() <= to)) {
365                         s.push_back (i);
366                 }
367         }
368
369         return s;
370 }
371
372 void
373 SubtitleAsset::add (SubtitleString s)
374 {
375         _subtitles.push_back (s);
376 }
377
378 Time
379 SubtitleAsset::latest_subtitle_out () const
380 {
381         Time t;
382         BOOST_FOREACH (SubtitleString const & i, _subtitles) {
383                 if (i.out() > t) {
384                         t = i.out ();
385                 }
386         }
387
388         return t;
389 }
390
391 bool
392 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
393 {
394         if (!Asset::equals (other_asset, options, note)) {
395                 return false;
396         }
397
398         shared_ptr<const SubtitleAsset> other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
399         if (!other) {
400                 return false;
401         }
402
403         if (_subtitles != other->_subtitles) {
404                 note (DCP_ERROR, "subtitles differ");
405                 return false;
406         }
407
408         return true;
409 }
410
411 struct SubtitleSorter {
412         bool operator() (SubtitleString const & a, SubtitleString const & b) {
413                 if (a.in() != b.in()) {
414                         return a.in() < b.in();
415                 }
416                 return a.v_position() < b.v_position();
417         }
418 };
419
420 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
421  *  class because the differences between the two are fairly subtle.
422  */
423 void
424 SubtitleAsset::subtitles_as_xml (xmlpp::Element* root, int time_code_rate, Standard standard) const
425 {
426         list<SubtitleString> sorted = _subtitles;
427         sorted.sort (SubtitleSorter ());
428
429         string const xmlns = standard == SMPTE ? "dcst" : "";
430
431         /* XXX: script not supported */
432
433         optional<string> font;
434         bool italic = false;
435         bool bold = false;
436         bool underline = false;
437         Colour colour;
438         int size = 0;
439         float aspect_adjust = 1.0;
440         Effect effect = NONE;
441         Colour effect_colour;
442         int spot_number = 1;
443         Time last_in;
444         Time last_out;
445         Time last_fade_up_time;
446         Time last_fade_down_time;
447
448         xmlpp::Element* font_element = 0;
449         xmlpp::Element* subtitle_element = 0;
450
451         BOOST_FOREACH (SubtitleString const & i, sorted) {
452
453                 /* We will start a new <Font>...</Font> whenever some font property changes.
454                    I suppose we should really make an optimal hierarchy of <Font> tags, but
455                    that seems hard.
456                 */
457
458                 bool const font_changed =
459                         font          != i.font()          ||
460                         italic        != i.italic()        ||
461                         bold          != i.bold()          ||
462                         underline     != i.underline()     ||
463                         colour        != i.colour()        ||
464                         size          != i.size()          ||
465                         fabs (aspect_adjust - i.aspect_adjust()) > ASPECT_ADJUST_EPSILON ||
466                         effect        != i.effect()        ||
467                         effect_colour != i.effect_colour();
468
469                 if (font_changed) {
470                         font = i.font ();
471                         italic = i.italic ();
472                         bold = i.bold ();
473                         underline = i.underline ();
474                         colour = i.colour ();
475                         size = i.size ();
476                         aspect_adjust = i.aspect_adjust ();
477                         effect = i.effect ();
478                         effect_colour = i.effect_colour ();
479                 }
480
481                 if (!font_element || font_changed) {
482                         font_element = root->add_child ("Font", xmlns);
483                         if (font) {
484                                 if (standard == SMPTE) {
485                                         font_element->set_attribute ("ID", font.get ());
486                                 } else {
487                                         font_element->set_attribute ("Id", font.get ());
488                                 }
489                         }
490                         font_element->set_attribute ("Italic", italic ? "yes" : "no");
491                         font_element->set_attribute ("Color", colour.to_argb_string());
492                         font_element->set_attribute ("Size", raw_convert<string> (size));
493                         if (fabs (aspect_adjust - 1.0) > ASPECT_ADJUST_EPSILON) {
494                                 font_element->set_attribute ("AspectAdjust", raw_convert<string> (aspect_adjust));
495                         }
496                         font_element->set_attribute ("Effect", effect_to_string (effect));
497                         font_element->set_attribute ("EffectColor", effect_colour.to_argb_string());
498                         font_element->set_attribute ("Script", "normal");
499                         if (standard == SMPTE) {
500                                 font_element->set_attribute ("Underline", underline ? "yes" : "no");
501                         } else {
502                                 font_element->set_attribute ("Underlined", underline ? "yes" : "no");
503                         }
504                         font_element->set_attribute ("Weight", bold ? "bold" : "normal");
505                 }
506
507                 if (!subtitle_element || font_changed ||
508                     (last_in != i.in() ||
509                      last_out != i.out() ||
510                      last_fade_up_time != i.fade_up_time() ||
511                      last_fade_down_time != i.fade_down_time()
512                             )) {
513
514                         subtitle_element = font_element->add_child ("Subtitle", xmlns);
515                         subtitle_element->set_attribute ("SpotNumber", raw_convert<string> (spot_number++));
516                         subtitle_element->set_attribute ("TimeIn", i.in().rebase(time_code_rate).as_string(standard));
517                         subtitle_element->set_attribute ("TimeOut", i.out().rebase(time_code_rate).as_string(standard));
518                         if (standard == SMPTE) {
519                                 subtitle_element->set_attribute ("FadeUpTime", i.fade_up_time().rebase(time_code_rate).as_string(standard));
520                                 subtitle_element->set_attribute ("FadeDownTime", i.fade_down_time().rebase(time_code_rate).as_string(standard));
521                         } else {
522                                 subtitle_element->set_attribute ("FadeUpTime", raw_convert<string> (i.fade_up_time().as_editable_units(time_code_rate)));
523                                 subtitle_element->set_attribute ("FadeDownTime", raw_convert<string> (i.fade_down_time().as_editable_units(time_code_rate)));
524                         }
525
526                         last_in = i.in ();
527                         last_out = i.out ();
528                         last_fade_up_time = i.fade_up_time ();
529                         last_fade_down_time = i.fade_down_time ();
530                 }
531
532                 xmlpp::Element* text = subtitle_element->add_child ("Text", xmlns);
533
534                 if (i.h_align() != HALIGN_CENTER) {
535                         if (standard == SMPTE) {
536                                 text->set_attribute ("Halign", halign_to_string (i.h_align ()));
537                         } else {
538                                 text->set_attribute ("HAlign", halign_to_string (i.h_align ()));
539                         }
540                 }
541
542                 if (i.h_position() > ALIGN_EPSILON) {
543                         if (standard == SMPTE) {
544                                 text->set_attribute ("Hposition", raw_convert<string> (i.h_position() * 100, 6));
545                         } else {
546                                 text->set_attribute ("HPosition", raw_convert<string> (i.h_position() * 100, 6));
547                         }
548                 }
549
550                 if (standard == SMPTE) {
551                         text->set_attribute ("Valign", valign_to_string (i.v_align()));
552                 } else {
553                         text->set_attribute ("VAlign", valign_to_string (i.v_align()));
554                 }
555
556                 if (i.v_position() > ALIGN_EPSILON) {
557                         if (standard == SMPTE) {
558                                 text->set_attribute ("Vposition", raw_convert<string> (i.v_position() * 100, 6));
559                         } else {
560                                 text->set_attribute ("VPosition", raw_convert<string> (i.v_position() * 100, 6));
561                         }
562                 } else {
563                         if (standard == SMPTE) {
564                                 text->set_attribute ("Vposition", "0");
565                         } else {
566                                 text->set_attribute ("VPosition", "0");
567                         }
568                 }
569
570                 /* Interop only supports "horizontal" or "vertical" for direction, so only write this
571                    for SMPTE.
572                 */
573                 if (i.direction() != DIRECTION_LTR && standard == SMPTE) {
574                         text->set_attribute ("Direction", direction_to_string (i.direction ()));
575                 }
576
577                 text->add_child_text (i.text());
578         }
579 }
580
581 map<string, Data>
582 SubtitleAsset::fonts_with_load_ids () const
583 {
584         map<string, Data> out;
585         BOOST_FOREACH (Font const & i, _fonts) {
586                 out[i.load_id] = i.data;
587         }
588         return out;
589 }