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