Bv2.1 7.2.3: Check that subtitle <StartTime> exists and is 0.
[libdcp.git] / src / subtitle_asset.cc
1 /*
2     Copyright (C) 2012-2019 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 "load_font_node.h"
44 #include <asdcp/AS_DCP.h>
45 #include <asdcp/KM_util.h>
46 #include <libxml++/nodes/element.h>
47 #include <boost/algorithm/string.hpp>
48 #include <boost/lexical_cast.hpp>
49 #include <boost/shared_array.hpp>
50 #include <boost/foreach.hpp>
51
52 using std::string;
53 using std::list;
54 using std::cout;
55 using std::cerr;
56 using std::map;
57 using std::shared_ptr;
58 using boost::shared_array;
59 using boost::optional;
60 using std::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 void
156 SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, xmlpp::Element const * node) const
157 {
158         optional<float> hp = optional_number_attribute<float> (node, "HPosition");
159         if (!hp) {
160                 hp = optional_number_attribute<float> (node, "Hposition");
161         }
162         if (hp) {
163                 ps.h_position = hp.get () / 100;
164         }
165
166         optional<string> ha = optional_string_attribute (node, "HAlign");
167         if (!ha) {
168                 ha = optional_string_attribute (node, "Halign");
169         }
170         if (ha) {
171                 ps.h_align = string_to_halign (ha.get ());
172         }
173
174         optional<float> vp = optional_number_attribute<float> (node, "VPosition");
175         if (!vp) {
176                 vp = optional_number_attribute<float> (node, "Vposition");
177         }
178         if (vp) {
179                 ps.v_position = vp.get () / 100;
180         }
181
182         optional<string> va = optional_string_attribute (node, "VAlign");
183         if (!va) {
184                 va = optional_string_attribute (node, "Valign");
185         }
186         if (va) {
187                 ps.v_align = string_to_valign (va.get ());
188         }
189
190 }
191
192 SubtitleAsset::ParseState
193 SubtitleAsset::text_node_state (xmlpp::Element const * node) const
194 {
195         ParseState ps;
196
197         position_align (ps, node);
198
199         optional<string> d = optional_string_attribute (node, "Direction");
200         if (d) {
201                 ps.direction = string_to_direction (d.get ());
202         }
203
204         ps.type = ParseState::TEXT;
205
206         return ps;
207 }
208
209 SubtitleAsset::ParseState
210 SubtitleAsset::image_node_state (xmlpp::Element const * node) const
211 {
212         ParseState ps;
213
214         position_align (ps, node);
215
216         ps.type = ParseState::IMAGE;
217
218         return ps;
219 }
220
221 SubtitleAsset::ParseState
222 SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional<int> tcr) const
223 {
224         ParseState ps;
225         ps.in = Time (string_attribute(node, "TimeIn"), tcr);
226         ps.out = Time (string_attribute(node, "TimeOut"), tcr);
227         ps.fade_up_time = fade_time (node, "FadeUpTime", tcr);
228         ps.fade_down_time = fade_time (node, "FadeDownTime", tcr);
229         return ps;
230 }
231
232 Time
233 SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional<int> tcr) const
234 {
235         string const u = optional_string_attribute(node, name).get_value_or ("");
236         Time t;
237
238         if (u.empty ()) {
239                 t = Time (0, 0, 0, 20, 250);
240         } else if (u.find (":") != string::npos) {
241                 t = Time (u, tcr);
242         } else {
243                 t = Time (0, 0, 0, lexical_cast<int> (u), tcr.get_value_or(250));
244         }
245
246         if (t > Time (0, 0, 8, 0, 250)) {
247                 t = Time (0, 0, 8, 0, 250);
248         }
249
250         return t;
251 }
252
253 void
254 SubtitleAsset::parse_subtitles (xmlpp::Element const * node, list<ParseState>& state, optional<int> tcr, Standard standard)
255 {
256         if (node->get_name() == "Font") {
257                 state.push_back (font_node_state (node, standard));
258         } else if (node->get_name() == "Subtitle") {
259                 state.push_back (subtitle_node_state (node, tcr));
260         } else if (node->get_name() == "Text") {
261                 state.push_back (text_node_state (node));
262         } else if (node->get_name() == "SubtitleList") {
263                 state.push_back (ParseState ());
264         } else if (node->get_name() == "Image") {
265                 state.push_back (image_node_state (node));
266         } else {
267                 throw XMLError ("unexpected node " + node->get_name());
268         }
269
270         xmlpp::Node::NodeList c = node->get_children ();
271         for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
272                 xmlpp::ContentNode const * v = dynamic_cast<xmlpp::ContentNode const *> (*i);
273                 if (v) {
274                         maybe_add_subtitle (v->get_content(), state, standard);
275                 }
276                 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
277                 if (e) {
278                         parse_subtitles (e, state, tcr, standard);
279                 }
280         }
281
282         state.pop_back ();
283 }
284
285 void
286 SubtitleAsset::maybe_add_subtitle (string text, list<ParseState> const & parse_state, Standard standard)
287 {
288         if (empty_or_white_space (text)) {
289                 return;
290         }
291
292         ParseState ps;
293         BOOST_FOREACH (ParseState const & i, parse_state) {
294                 if (i.font_id) {
295                         ps.font_id = i.font_id.get();
296                 }
297                 if (i.size) {
298                         ps.size = i.size.get();
299                 }
300                 if (i.aspect_adjust) {
301                         ps.aspect_adjust = i.aspect_adjust.get();
302                 }
303                 if (i.italic) {
304                         ps.italic = i.italic.get();
305                 }
306                 if (i.bold) {
307                         ps.bold = i.bold.get();
308                 }
309                 if (i.underline) {
310                         ps.underline = i.underline.get();
311                 }
312                 if (i.colour) {
313                         ps.colour = i.colour.get();
314                 }
315                 if (i.effect) {
316                         ps.effect = i.effect.get();
317                 }
318                 if (i.effect_colour) {
319                         ps.effect_colour = i.effect_colour.get();
320                 }
321                 if (i.h_position) {
322                         ps.h_position = i.h_position.get();
323                 }
324                 if (i.h_align) {
325                         ps.h_align = i.h_align.get();
326                 }
327                 if (i.v_position) {
328                         ps.v_position = i.v_position.get();
329                 }
330                 if (i.v_align) {
331                         ps.v_align = i.v_align.get();
332                 }
333                 if (i.direction) {
334                         ps.direction = i.direction.get();
335                 }
336                 if (i.in) {
337                         ps.in = i.in.get();
338                 }
339                 if (i.out) {
340                         ps.out = i.out.get();
341                 }
342                 if (i.fade_up_time) {
343                         ps.fade_up_time = i.fade_up_time.get();
344                 }
345                 if (i.fade_down_time) {
346                         ps.fade_down_time = i.fade_down_time.get();
347                 }
348                 if (i.type) {
349                         ps.type = i.type.get();
350                 }
351         }
352
353         if (!ps.in || !ps.out) {
354                 /* We're not in a <Subtitle> node; just ignore this content */
355                 return;
356         }
357
358         DCP_ASSERT (ps.type);
359
360         switch (ps.type.get()) {
361         case ParseState::TEXT:
362                 _subtitles.push_back (
363                         shared_ptr<Subtitle> (
364                                 new SubtitleString (
365                                         ps.font_id,
366                                         ps.italic.get_value_or (false),
367                                         ps.bold.get_value_or (false),
368                                         ps.underline.get_value_or (false),
369                                         ps.colour.get_value_or (dcp::Colour (255, 255, 255)),
370                                         ps.size.get_value_or (42),
371                                         ps.aspect_adjust.get_value_or (1.0),
372                                         ps.in.get(),
373                                         ps.out.get(),
374                                         ps.h_position.get_value_or(0),
375                                         ps.h_align.get_value_or(HALIGN_CENTER),
376                                         ps.v_position.get_value_or(0),
377                                         ps.v_align.get_value_or(VALIGN_CENTER),
378                                         ps.direction.get_value_or (DIRECTION_LTR),
379                                         text,
380                                         ps.effect.get_value_or (NONE),
381                                         ps.effect_colour.get_value_or (dcp::Colour (0, 0, 0)),
382                                         ps.fade_up_time.get_value_or(Time()),
383                                         ps.fade_down_time.get_value_or(Time())
384                                         )
385                                 )
386                         );
387                 break;
388         case ParseState::IMAGE:
389                 /* Add a subtitle with no image data and we'll fill that in later */
390                 _subtitles.push_back (
391                         shared_ptr<Subtitle> (
392                                 new SubtitleImage (
393                                         ArrayData (),
394                                         standard == INTEROP ? text.substr(0, text.size() - 4) : text,
395                                         ps.in.get(),
396                                         ps.out.get(),
397                                         ps.h_position.get_value_or(0),
398                                         ps.h_align.get_value_or(HALIGN_CENTER),
399                                         ps.v_position.get_value_or(0),
400                                         ps.v_align.get_value_or(VALIGN_CENTER),
401                                         ps.fade_up_time.get_value_or(Time()),
402                                         ps.fade_down_time.get_value_or(Time())
403                                         )
404                                 )
405                         );
406                 break;
407         }
408 }
409
410 list<shared_ptr<Subtitle> >
411 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
412 {
413         list<shared_ptr<Subtitle> > s;
414         BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
415                 if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
416                         s.push_back (i);
417                 }
418         }
419
420         return s;
421 }
422
423 void
424 SubtitleAsset::add (shared_ptr<Subtitle> s)
425 {
426         _subtitles.push_back (s);
427 }
428
429 Time
430 SubtitleAsset::latest_subtitle_out () const
431 {
432         Time t;
433         BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
434                 if (i->out() > t) {
435                         t = i->out ();
436                 }
437         }
438
439         return t;
440 }
441
442 bool
443 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
444 {
445         if (!Asset::equals (other_asset, options, note)) {
446                 return false;
447         }
448
449         shared_ptr<const SubtitleAsset> other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
450         if (!other) {
451                 return false;
452         }
453
454         if (_subtitles.size() != other->_subtitles.size()) {
455                 note (DCP_ERROR, String::compose("different number of subtitles: %1 vs %2", _subtitles.size(), other->_subtitles.size()));
456                 return false;
457         }
458
459         list<shared_ptr<Subtitle> >::const_iterator i = _subtitles.begin ();
460         list<shared_ptr<Subtitle> >::const_iterator j = other->_subtitles.begin ();
461
462         while (i != _subtitles.end()) {
463                 shared_ptr<SubtitleString> string_i = dynamic_pointer_cast<SubtitleString> (*i);
464                 shared_ptr<SubtitleString> string_j = dynamic_pointer_cast<SubtitleString> (*j);
465                 shared_ptr<SubtitleImage> image_i = dynamic_pointer_cast<SubtitleImage> (*i);
466                 shared_ptr<SubtitleImage> image_j = dynamic_pointer_cast<SubtitleImage> (*j);
467
468                 if ((string_i && !string_j) || (image_i && !image_j)) {
469                         note (DCP_ERROR, "subtitles differ: string vs. image");
470                         return false;
471                 }
472
473                 if (string_i && *string_i != *string_j) {
474                         note (DCP_ERROR, String::compose("subtitles differ in text or metadata: %1 vs %2", string_i->text(), string_j->text()));
475                         return false;
476                 }
477
478                 if (image_i && !image_i->equals(image_j, options, note)) {
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, ArrayData>
650 SubtitleAsset::font_data () const
651 {
652         map<string, ArrayData> out;
653         BOOST_FOREACH (Font const & i, _fonts) {
654                 out[i.load_id] = i.data;
655         }
656         return out;
657 }
658
659
660 map<string, boost::filesystem::path>
661 SubtitleAsset::font_filenames () const
662 {
663         map<string, boost::filesystem::path> out;
664         BOOST_FOREACH (Font const& i, _fonts) {
665                 if (i.file) {
666                         out[i.load_id] = *i.file;
667                 }
668         }
669         return out;
670 }
671
672
673 /** Replace empty IDs in any <LoadFontId> and <Font> tags with
674  *  a dummy string.  Some systems give errors with empty font IDs
675  *  (see DCP-o-matic bug #1689).
676  */
677 void
678 SubtitleAsset::fix_empty_font_ids ()
679 {
680         bool have_empty = false;
681         list<string> ids;
682         BOOST_FOREACH (shared_ptr<LoadFontNode> i, load_font_nodes()) {
683                 if (i->id == "") {
684                         have_empty = true;
685                 } else {
686                         ids.push_back (i->id);
687                 }
688         }
689
690         if (!have_empty) {
691                 return;
692         }
693
694         string const empty_id = unique_string (ids, "font");
695
696         BOOST_FOREACH (shared_ptr<LoadFontNode> i, load_font_nodes()) {
697                 if (i->id == "") {
698                         i->id = empty_id;
699                 }
700         }
701
702         BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
703                 shared_ptr<SubtitleString> j = dynamic_pointer_cast<SubtitleString> (i);
704                 if (j && j->font() && j->font().get() == "") {
705                         j->set_font (empty_id);
706                 }
707         }
708 }