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