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