3ecd16a79d165966101ea868aff187b9ca9983a0
[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
378 Time
379 SubtitleAsset::latest_subtitle_out () const
380 {
381         Time t;
382         BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
383                 if (i->out() > t) {
384                         t = i->out ();
385                 }
386         }
387
388         return t;
389 }
390
391 bool
392 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
393 {
394         if (!Asset::equals (other_asset, options, note)) {
395                 return false;
396         }
397
398         shared_ptr<const SubtitleAsset> other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
399         if (!other) {
400                 return false;
401         }
402
403         if (_subtitles.size() != other->_subtitles.size()) {
404                 note (DCP_ERROR, "subtitles differ");
405                 return false;
406         }
407
408         list<shared_ptr<Subtitle> >::const_iterator i = _subtitles.begin ();
409         list<shared_ptr<Subtitle> >::const_iterator j = other->_subtitles.begin ();
410
411         while (i != _subtitles.end()) {
412                 shared_ptr<SubtitleString> string_i = dynamic_pointer_cast<SubtitleString> (*i);
413                 shared_ptr<SubtitleString> string_j = dynamic_pointer_cast<SubtitleString> (*j);
414                 shared_ptr<SubtitleImage> image_i = dynamic_pointer_cast<SubtitleImage> (*i);
415                 shared_ptr<SubtitleImage> image_j = dynamic_pointer_cast<SubtitleImage> (*j);
416
417                 if ((string_i && !string_j) || (image_i && !image_j)) {
418                         note (DCP_ERROR, "subtitles differ");
419                         return false;
420                 }
421
422                 if (string_i && *string_i != *string_j) {
423                         note (DCP_ERROR, "subtitles differ");
424                         return false;
425                 }
426
427                 if (image_i && *image_i != *image_j) {
428                         note (DCP_ERROR, "subtitles differ");
429                         return false;
430                 }
431         }
432
433         return true;
434 }
435
436 struct SubtitleSorter
437 {
438         bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
439                 if (a->in() != b->in()) {
440                         return a->in() < b->in();
441                 }
442                 return a->v_position() < b->v_position();
443         }
444 };
445
446 void
447 SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
448 {
449         if (part->children.empty ()) {
450                 return;
451         }
452
453         /* Pull up from children */
454         BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
455                 pull_fonts (i);
456         }
457
458         if (part->parent) {
459                 /* Establish the common font features that each of part's children have;
460                    these features go into part's font.
461                 */
462                 part->font = part->children.front()->font;
463                 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
464                         part->font.take_intersection (i->font);
465                 }
466
467                 /* Remove common values from part's children's fonts */
468                 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
469                         i->font.take_difference (part->font);
470                 }
471         }
472
473         /* Merge adjacent children with the same font */
474         list<shared_ptr<order::Part> >::const_iterator i = part->children.begin();
475         list<shared_ptr<order::Part> > merged;
476
477         while (i != part->children.end()) {
478
479                 if ((*i)->font.empty ()) {
480                         merged.push_back (*i);
481                         ++i;
482                 } else {
483                         list<shared_ptr<order::Part> >::const_iterator j = i;
484                         ++j;
485                         while (j != part->children.end() && (*i)->font == (*j)->font) {
486                                 ++j;
487                         }
488                         if (distance (i, j) == 1) {
489                                 merged.push_back (*i);
490                                 ++i;
491                         } else {
492                                 shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
493                                 for (list<shared_ptr<order::Part> >::const_iterator k = i; k != j; ++k) {
494                                         (*k)->font.clear ();
495                                         group->children.push_back (*k);
496                                 }
497                                 merged.push_back (group);
498                                 i = j;
499                         }
500                 }
501         }
502
503         part->children = merged;
504 }
505
506 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
507  *  class because the differences between the two are fairly subtle.
508  */
509 void
510 SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, Standard standard) const
511 {
512         list<shared_ptr<Subtitle> > sorted = _subtitles;
513         sorted.sort (SubtitleSorter ());
514
515         /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
516            font information into the bottom level (String) objects.
517         */
518
519         shared_ptr<order::Part> root (new order::Part (shared_ptr<order::Part> ()));
520         shared_ptr<order::Subtitle> subtitle;
521         shared_ptr<order::Text> text;
522
523         Time last_in;
524         Time last_out;
525         Time last_fade_up_time;
526         Time last_fade_down_time;
527         HAlign last_h_align;
528         float last_h_position;
529         VAlign last_v_align;
530         float last_v_position;
531         Direction last_direction;
532
533         BOOST_FOREACH (shared_ptr<Subtitle> i, sorted) {
534                 if (!subtitle ||
535                     (last_in != i->in() ||
536                      last_out != i->out() ||
537                      last_fade_up_time != i->fade_up_time() ||
538                      last_fade_down_time != i->fade_down_time())
539                         ) {
540
541                         subtitle.reset (new order::Subtitle (root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time()));
542                         root->children.push_back (subtitle);
543
544                         last_in = i->in ();
545                         last_out = i->out ();
546                         last_fade_up_time = i->fade_up_time ();
547                         last_fade_down_time = i->fade_down_time ();
548                         text.reset ();
549                 }
550
551                 shared_ptr<SubtitleString> is = dynamic_pointer_cast<SubtitleString>(i);
552
553                 if (is) {
554                         if (!text ||
555                             last_h_align != is->h_align() ||
556                             fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
557                             last_v_align != is->v_align() ||
558                             fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
559                             last_direction != is->direction()
560                                 ) {
561                                 text.reset (new order::Text (subtitle, is->h_align(), is->h_position(), is->v_align(), is->v_position(), is->direction()));
562                                 subtitle->children.push_back (text);
563
564                                 last_h_align = is->h_align ();
565                                 last_h_position = is->h_position ();
566                                 last_v_align = is->v_align ();
567                                 last_v_position = is->v_position ();
568                                 last_direction = is->direction ();
569                         }
570
571                         text->children.push_back (shared_ptr<order::String> (new order::String (text, order::Font (is, standard), is->text())));
572                 }
573
574                 /* XXX: image */
575         }
576
577         /* Pull font changes as high up the hierarchy as we can */
578
579         pull_fonts (root);
580
581         /* Write XML */
582
583         order::Context context;
584         context.time_code_rate = time_code_rate;
585         context.standard = standard;
586         context.spot_number = 1;
587
588         root->write_xml (xml_root, context);
589 }
590
591 map<string, Data>
592 SubtitleAsset::fonts_with_load_ids () const
593 {
594         map<string, Data> out;
595         BOOST_FOREACH (Font const & i, _fonts) {
596                 out[i.load_id] = i.data;
597         }
598         return out;
599 }