Fix build with newer boost.
[libdcp.git] / src / subtitle_asset.cc
1 /*
2     Copyright (C) 2012-2018 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 "subtitle_image.h"
42 #include "dcp_assert.h"
43 #include <asdcp/AS_DCP.h>
44 #include <asdcp/KM_util.h>
45 #include <libxml++/nodes/element.h>
46 #include <boost/algorithm/string.hpp>
47 #include <boost/lexical_cast.hpp>
48 #include <boost/shared_array.hpp>
49 #include <boost/foreach.hpp>
50
51 using std::string;
52 using std::list;
53 using std::cout;
54 using std::cerr;
55 using std::map;
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 void
155 SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, xmlpp::Element const * node) const
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 }
190
191 SubtitleAsset::ParseState
192 SubtitleAsset::text_node_state (xmlpp::Element const * node) const
193 {
194         ParseState ps;
195
196         position_align (ps, node);
197
198         optional<string> d = optional_string_attribute (node, "Direction");
199         if (d) {
200                 ps.direction = string_to_direction (d.get ());
201         }
202
203         ps.type = ParseState::TEXT;
204
205         return ps;
206 }
207
208 SubtitleAsset::ParseState
209 SubtitleAsset::image_node_state (xmlpp::Element const * node) const
210 {
211         ParseState ps;
212
213         position_align (ps, node);
214
215         ps.type = ParseState::IMAGE;
216
217         return ps;
218 }
219
220 SubtitleAsset::ParseState
221 SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional<int> tcr) const
222 {
223         ParseState ps;
224         ps.in = Time (string_attribute(node, "TimeIn"), tcr);
225         ps.out = Time (string_attribute(node, "TimeOut"), tcr);
226         ps.fade_up_time = fade_time (node, "FadeUpTime", tcr);
227         ps.fade_down_time = fade_time (node, "FadeDownTime", tcr);
228         return ps;
229 }
230
231 Time
232 SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional<int> tcr) const
233 {
234         string const u = optional_string_attribute(node, name).get_value_or ("");
235         Time t;
236
237         if (u.empty ()) {
238                 t = Time (0, 0, 0, 20, 250);
239         } else if (u.find (":") != string::npos) {
240                 t = Time (u, tcr);
241         } else {
242                 t = Time (0, 0, 0, lexical_cast<int> (u), tcr.get_value_or(250));
243         }
244
245         if (t > Time (0, 0, 8, 0, 250)) {
246                 t = Time (0, 0, 8, 0, 250);
247         }
248
249         return t;
250 }
251
252 void
253 SubtitleAsset::parse_subtitles (xmlpp::Element const * node, list<ParseState>& state, optional<int> tcr, Standard standard)
254 {
255         if (node->get_name() == "Font") {
256                 state.push_back (font_node_state (node, standard));
257         } else if (node->get_name() == "Subtitle") {
258                 state.push_back (subtitle_node_state (node, tcr));
259         } else if (node->get_name() == "Text") {
260                 state.push_back (text_node_state (node));
261         } else if (node->get_name() == "SubtitleList") {
262                 state.push_back (ParseState ());
263         } else if (node->get_name() == "Image") {
264                 state.push_back (image_node_state (node));
265         } else {
266                 throw XMLError ("unexpected node " + node->get_name());
267         }
268
269         xmlpp::Node::NodeList c = node->get_children ();
270         for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
271                 xmlpp::ContentNode const * v = dynamic_cast<xmlpp::ContentNode const *> (*i);
272                 if (v) {
273                         maybe_add_subtitle (v->get_content(), state, standard);
274                 }
275                 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
276                 if (e) {
277                         parse_subtitles (e, state, tcr, standard);
278                 }
279         }
280
281         state.pop_back ();
282 }
283
284 void
285 SubtitleAsset::maybe_add_subtitle (string text, list<ParseState> const & parse_state, Standard standard)
286 {
287         if (empty_or_white_space (text)) {
288                 return;
289         }
290
291         ParseState ps;
292         BOOST_FOREACH (ParseState const & i, parse_state) {
293                 if (i.font_id) {
294                         ps.font_id = i.font_id.get();
295                 }
296                 if (i.size) {
297                         ps.size = i.size.get();
298                 }
299                 if (i.aspect_adjust) {
300                         ps.aspect_adjust = i.aspect_adjust.get();
301                 }
302                 if (i.italic) {
303                         ps.italic = i.italic.get();
304                 }
305                 if (i.bold) {
306                         ps.bold = i.bold.get();
307                 }
308                 if (i.underline) {
309                         ps.underline = i.underline.get();
310                 }
311                 if (i.colour) {
312                         ps.colour = i.colour.get();
313                 }
314                 if (i.effect) {
315                         ps.effect = i.effect.get();
316                 }
317                 if (i.effect_colour) {
318                         ps.effect_colour = i.effect_colour.get();
319                 }
320                 if (i.h_position) {
321                         ps.h_position = i.h_position.get();
322                 }
323                 if (i.h_align) {
324                         ps.h_align = i.h_align.get();
325                 }
326                 if (i.v_position) {
327                         ps.v_position = i.v_position.get();
328                 }
329                 if (i.v_align) {
330                         ps.v_align = i.v_align.get();
331                 }
332                 if (i.direction) {
333                         ps.direction = i.direction.get();
334                 }
335                 if (i.in) {
336                         ps.in = i.in.get();
337                 }
338                 if (i.out) {
339                         ps.out = i.out.get();
340                 }
341                 if (i.fade_up_time) {
342                         ps.fade_up_time = i.fade_up_time.get();
343                 }
344                 if (i.fade_down_time) {
345                         ps.fade_down_time = i.fade_down_time.get();
346                 }
347                 if (i.type) {
348                         ps.type = i.type.get();
349                 }
350         }
351
352         if (!ps.in || !ps.out) {
353                 /* We're not in a <Subtitle> node; just ignore this content */
354                 return;
355         }
356
357         DCP_ASSERT (ps.type);
358
359         switch (ps.type.get()) {
360         case ParseState::TEXT:
361                 _subtitles.push_back (
362                         shared_ptr<Subtitle> (
363                                 new SubtitleString (
364                                         ps.font_id,
365                                         ps.italic.get_value_or (false),
366                                         ps.bold.get_value_or (false),
367                                         ps.underline.get_value_or (false),
368                                         ps.colour.get_value_or (dcp::Colour (255, 255, 255)),
369                                         ps.size.get_value_or (42),
370                                         ps.aspect_adjust.get_value_or (1.0),
371                                         ps.in.get(),
372                                         ps.out.get(),
373                                         ps.h_position.get_value_or(0),
374                                         ps.h_align.get_value_or(HALIGN_CENTER),
375                                         ps.v_position.get_value_or(0),
376                                         ps.v_align.get_value_or(VALIGN_CENTER),
377                                         ps.direction.get_value_or (DIRECTION_LTR),
378                                         text,
379                                         ps.effect.get_value_or (NONE),
380                                         ps.effect_colour.get_value_or (dcp::Colour (0, 0, 0)),
381                                         ps.fade_up_time.get_value_or(Time()),
382                                         ps.fade_down_time.get_value_or(Time())
383                                         )
384                                 )
385                         );
386                 break;
387         case ParseState::IMAGE:
388                 /* Add a subtitle with no image data and we'll fill that in later */
389                 _subtitles.push_back (
390                         shared_ptr<Subtitle> (
391                                 new SubtitleImage (
392                                         Data (),
393                                         standard == INTEROP ? text.substr(0, text.size() - 4) : text,
394                                         ps.in.get(),
395                                         ps.out.get(),
396                                         ps.h_position.get_value_or(0),
397                                         ps.h_align.get_value_or(HALIGN_CENTER),
398                                         ps.v_position.get_value_or(0),
399                                         ps.v_align.get_value_or(VALIGN_CENTER),
400                                         ps.fade_up_time.get_value_or(Time()),
401                                         ps.fade_down_time.get_value_or(Time())
402                                         )
403                                 )
404                         );
405                 break;
406         }
407 }
408
409 list<shared_ptr<Subtitle> >
410 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
411 {
412         list<shared_ptr<Subtitle> > s;
413         BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
414                 if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
415                         s.push_back (i);
416                 }
417         }
418
419         return s;
420 }
421
422 void
423 SubtitleAsset::add (shared_ptr<Subtitle> s)
424 {
425         _subtitles.push_back (s);
426 }
427
428 Time
429 SubtitleAsset::latest_subtitle_out () const
430 {
431         Time t;
432         BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
433                 if (i->out() > t) {
434                         t = i->out ();
435                 }
436         }
437
438         return t;
439 }
440
441 bool
442 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
443 {
444         if (!Asset::equals (other_asset, options, note)) {
445                 return false;
446         }
447
448         shared_ptr<const SubtitleAsset> other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
449         if (!other) {
450                 return false;
451         }
452
453         if (_subtitles.size() != other->_subtitles.size()) {
454                 note (DCP_ERROR, "subtitles differ");
455                 return false;
456         }
457
458         list<shared_ptr<Subtitle> >::const_iterator i = _subtitles.begin ();
459         list<shared_ptr<Subtitle> >::const_iterator j = other->_subtitles.begin ();
460
461         while (i != _subtitles.end()) {
462                 shared_ptr<SubtitleString> string_i = dynamic_pointer_cast<SubtitleString> (*i);
463                 shared_ptr<SubtitleString> string_j = dynamic_pointer_cast<SubtitleString> (*j);
464                 shared_ptr<SubtitleImage> image_i = dynamic_pointer_cast<SubtitleImage> (*i);
465                 shared_ptr<SubtitleImage> image_j = dynamic_pointer_cast<SubtitleImage> (*j);
466
467                 if ((string_i && !string_j) || (image_i && !image_j)) {
468                         note (DCP_ERROR, "subtitles differ");
469                         return false;
470                 }
471
472                 if (string_i && *string_i != *string_j) {
473                         note (DCP_ERROR, "subtitles differ");
474                         return false;
475                 }
476
477                 if (image_i && *image_i != *image_j) {
478                         note (DCP_ERROR, "subtitles differ");
479                         return false;
480                 }
481
482                 ++i;
483                 ++j;
484         }
485
486         return true;
487 }
488
489 struct SubtitleSorter
490 {
491         bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
492                 if (a->in() != b->in()) {
493                         return a->in() < b->in();
494                 }
495                 return a->v_position() < b->v_position();
496         }
497 };
498
499 void
500 SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
501 {
502         if (part->children.empty ()) {
503                 return;
504         }
505
506         /* Pull up from children */
507         BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
508                 pull_fonts (i);
509         }
510
511         if (part->parent) {
512                 /* Establish the common font features that each of part's children have;
513                    these features go into part's font.
514                 */
515                 part->font = part->children.front()->font;
516                 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
517                         part->font.take_intersection (i->font);
518                 }
519
520                 /* Remove common values from part's children's fonts */
521                 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
522                         i->font.take_difference (part->font);
523                 }
524         }
525
526         /* Merge adjacent children with the same font */
527         list<shared_ptr<order::Part> >::const_iterator i = part->children.begin();
528         list<shared_ptr<order::Part> > merged;
529
530         while (i != part->children.end()) {
531
532                 if ((*i)->font.empty ()) {
533                         merged.push_back (*i);
534                         ++i;
535                 } else {
536                         list<shared_ptr<order::Part> >::const_iterator j = i;
537                         ++j;
538                         while (j != part->children.end() && (*i)->font == (*j)->font) {
539                                 ++j;
540                         }
541                         if (std::distance (i, j) == 1) {
542                                 merged.push_back (*i);
543                                 ++i;
544                         } else {
545                                 shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
546                                 for (list<shared_ptr<order::Part> >::const_iterator k = i; k != j; ++k) {
547                                         (*k)->font.clear ();
548                                         group->children.push_back (*k);
549                                 }
550                                 merged.push_back (group);
551                                 i = j;
552                         }
553                 }
554         }
555
556         part->children = merged;
557 }
558
559 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
560  *  class because the differences between the two are fairly subtle.
561  */
562 void
563 SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, Standard standard) const
564 {
565         list<shared_ptr<Subtitle> > sorted = _subtitles;
566         sorted.sort (SubtitleSorter ());
567
568         /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
569            font information into the bottom level (String) objects.
570         */
571
572         shared_ptr<order::Part> root (new order::Part (shared_ptr<order::Part> ()));
573         shared_ptr<order::Subtitle> subtitle;
574         shared_ptr<order::Text> text;
575
576         Time last_in;
577         Time last_out;
578         Time last_fade_up_time;
579         Time last_fade_down_time;
580         HAlign last_h_align;
581         float last_h_position;
582         VAlign last_v_align;
583         float last_v_position;
584         Direction last_direction;
585
586         BOOST_FOREACH (shared_ptr<Subtitle> i, sorted) {
587                 if (!subtitle ||
588                     (last_in != i->in() ||
589                      last_out != i->out() ||
590                      last_fade_up_time != i->fade_up_time() ||
591                      last_fade_down_time != i->fade_down_time())
592                         ) {
593
594                         subtitle.reset (new order::Subtitle (root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time()));
595                         root->children.push_back (subtitle);
596
597                         last_in = i->in ();
598                         last_out = i->out ();
599                         last_fade_up_time = i->fade_up_time ();
600                         last_fade_down_time = i->fade_down_time ();
601                         text.reset ();
602                 }
603
604                 shared_ptr<SubtitleString> is = dynamic_pointer_cast<SubtitleString>(i);
605                 if (is) {
606                         if (!text ||
607                             last_h_align != is->h_align() ||
608                             fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
609                             last_v_align != is->v_align() ||
610                             fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
611                             last_direction != is->direction()
612                                 ) {
613                                 text.reset (new order::Text (subtitle, is->h_align(), is->h_position(), is->v_align(), is->v_position(), is->direction()));
614                                 subtitle->children.push_back (text);
615
616                                 last_h_align = is->h_align ();
617                                 last_h_position = is->h_position ();
618                                 last_v_align = is->v_align ();
619                                 last_v_position = is->v_position ();
620                                 last_direction = is->direction ();
621                         }
622
623                         text->children.push_back (shared_ptr<order::String> (new order::String (text, order::Font (is, standard), is->text())));
624                 }
625
626                 shared_ptr<SubtitleImage> ii = dynamic_pointer_cast<SubtitleImage>(i);
627                 if (ii) {
628                         text.reset ();
629                         subtitle->children.push_back (
630                                 shared_ptr<order::Image> (new order::Image (subtitle, ii->id(), ii->png_image(), ii->h_align(), ii->h_position(), ii->v_align(), ii->v_position()))
631                                 );
632                 }
633         }
634
635         /* Pull font changes as high up the hierarchy as we can */
636
637         pull_fonts (root);
638
639         /* Write XML */
640
641         order::Context context;
642         context.time_code_rate = time_code_rate;
643         context.standard = standard;
644         context.spot_number = 1;
645
646         root->write_xml (xml_root, context);
647 }
648
649 map<string, Data>
650 SubtitleAsset::fonts_with_load_ids () const
651 {
652         map<string, Data> out;
653         BOOST_FOREACH (Font const & i, _fonts) {
654                 out[i.load_id] = i.data;
655         }
656         return out;
657 }