Missing finalize() in dcpdecryptmxf.
[libdcp.git] / src / subtitle_asset.cc
1 /*
2     Copyright (C) 2012-2021 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
35 /** @file  src/subtitle_asset.cc
36  *  @brief SubtitleAsset class
37  */
38
39
40 #include "raw_convert.h"
41 #include "compose.hpp"
42 #include "subtitle_asset.h"
43 #include "subtitle_asset_internal.h"
44 #include "util.h"
45 #include "xml.h"
46 #include "subtitle_string.h"
47 #include "subtitle_image.h"
48 #include "dcp_assert.h"
49 #include "load_font_node.h"
50 #include "reel_asset.h"
51 #include <asdcp/AS_DCP.h>
52 #include <asdcp/KM_util.h>
53 #include <libxml++/nodes/element.h>
54 #include <boost/algorithm/string.hpp>
55 #include <boost/lexical_cast.hpp>
56 #include <boost/shared_array.hpp>
57
58
59 using std::dynamic_pointer_cast;
60 using std::string;
61 using std::cout;
62 using std::cerr;
63 using std::map;
64 using std::shared_ptr;
65 using std::vector;
66 using std::make_shared;
67 using boost::shared_array;
68 using boost::optional;
69 using boost::lexical_cast;
70 using namespace dcp;
71
72
73 SubtitleAsset::SubtitleAsset ()
74 {
75
76 }
77
78
79 SubtitleAsset::SubtitleAsset (boost::filesystem::path file)
80         : Asset (file)
81 {
82
83 }
84
85
86 string
87 string_attribute (xmlpp::Element const * node, string name)
88 {
89         auto a = node->get_attribute (name);
90         if (!a) {
91                 throw XMLError (String::compose ("missing attribute %1", name));
92         }
93         return string (a->get_value ());
94 }
95
96
97 optional<string>
98 optional_string_attribute (xmlpp::Element const * node, string name)
99 {
100         auto a = node->get_attribute (name);
101         if (!a) {
102                 return {};
103         }
104         return string (a->get_value ());
105 }
106
107
108 optional<bool>
109 optional_bool_attribute (xmlpp::Element const * node, string name)
110 {
111         auto s = optional_string_attribute (node, name);
112         if (!s) {
113                 return {};
114         }
115
116         return (s.get() == "1" || s.get() == "yes");
117 }
118
119
120 template <class T>
121 optional<T>
122 optional_number_attribute (xmlpp::Element const * node, string name)
123 {
124         auto s = optional_string_attribute (node, name);
125         if (!s) {
126                 return boost::optional<T> ();
127         }
128
129         std::string t = s.get ();
130         boost::erase_all (t, " ");
131         return raw_convert<T> (t);
132 }
133
134
135 SubtitleAsset::ParseState
136 SubtitleAsset::font_node_state (xmlpp::Element const * node, Standard standard) const
137 {
138         ParseState ps;
139
140         if (standard == Standard::INTEROP) {
141                 ps.font_id = optional_string_attribute (node, "Id");
142         } else {
143                 ps.font_id = optional_string_attribute (node, "ID");
144         }
145         ps.size = optional_number_attribute<int64_t> (node, "Size");
146         ps.aspect_adjust = optional_number_attribute<float> (node, "AspectAdjust");
147         ps.italic = optional_bool_attribute (node, "Italic");
148         ps.bold = optional_string_attribute(node, "Weight").get_value_or("normal") == "bold";
149         if (standard == Standard::INTEROP) {
150                 ps.underline = optional_bool_attribute (node, "Underlined");
151         } else {
152                 ps.underline = optional_bool_attribute (node, "Underline");
153         }
154         auto c = optional_string_attribute (node, "Color");
155         if (c) {
156                 ps.colour = Colour (c.get ());
157         }
158         auto const e = optional_string_attribute (node, "Effect");
159         if (e) {
160                 ps.effect = string_to_effect (e.get ());
161         }
162         c = optional_string_attribute (node, "EffectColor");
163         if (c) {
164                 ps.effect_colour = Colour (c.get ());
165         }
166
167         return ps;
168 }
169
170 void
171 SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, xmlpp::Element const * node) const
172 {
173         auto hp = optional_number_attribute<float> (node, "HPosition");
174         if (!hp) {
175                 hp = optional_number_attribute<float> (node, "Hposition");
176         }
177         if (hp) {
178                 ps.h_position = hp.get () / 100;
179         }
180
181         auto ha = optional_string_attribute (node, "HAlign");
182         if (!ha) {
183                 ha = optional_string_attribute (node, "Halign");
184         }
185         if (ha) {
186                 ps.h_align = string_to_halign (ha.get ());
187         }
188
189         auto vp = optional_number_attribute<float> (node, "VPosition");
190         if (!vp) {
191                 vp = optional_number_attribute<float> (node, "Vposition");
192         }
193         if (vp) {
194                 ps.v_position = vp.get () / 100;
195         }
196
197         auto va = optional_string_attribute (node, "VAlign");
198         if (!va) {
199                 va = optional_string_attribute (node, "Valign");
200         }
201         if (va) {
202                 ps.v_align = string_to_valign (va.get ());
203         }
204
205 }
206
207
208 SubtitleAsset::ParseState
209 SubtitleAsset::text_node_state (xmlpp::Element const * node) const
210 {
211         ParseState ps;
212
213         position_align (ps, node);
214
215         auto d = optional_string_attribute (node, "Direction");
216         if (d) {
217                 ps.direction = string_to_direction (d.get ());
218         }
219
220         ps.type = ParseState::Type::TEXT;
221
222         return ps;
223 }
224
225
226 SubtitleAsset::ParseState
227 SubtitleAsset::image_node_state (xmlpp::Element const * node) const
228 {
229         ParseState ps;
230
231         position_align (ps, node);
232
233         ps.type = ParseState::Type::IMAGE;
234
235         return ps;
236 }
237
238
239 SubtitleAsset::ParseState
240 SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional<int> tcr) const
241 {
242         ParseState ps;
243         ps.in = Time (string_attribute(node, "TimeIn"), tcr);
244         ps.out = Time (string_attribute(node, "TimeOut"), tcr);
245         ps.fade_up_time = fade_time (node, "FadeUpTime", tcr);
246         ps.fade_down_time = fade_time (node, "FadeDownTime", tcr);
247         return ps;
248 }
249
250
251 Time
252 SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional<int> tcr) const
253 {
254         auto const u = optional_string_attribute(node, name).get_value_or ("");
255         Time t;
256
257         if (u.empty ()) {
258                 t = Time (0, 0, 0, 20, 250);
259         } else if (u.find (":") != string::npos) {
260                 t = Time (u, tcr);
261         } else {
262                 t = Time (0, 0, 0, lexical_cast<int> (u), tcr.get_value_or(250));
263         }
264
265         if (t > Time (0, 0, 8, 0, 250)) {
266                 t = Time (0, 0, 8, 0, 250);
267         }
268
269         return t;
270 }
271
272
273 void
274 SubtitleAsset::parse_subtitles (xmlpp::Element const * node, vector<ParseState>& state, optional<int> tcr, Standard standard)
275 {
276         if (node->get_name() == "Font") {
277                 state.push_back (font_node_state (node, standard));
278         } else if (node->get_name() == "Subtitle") {
279                 state.push_back (subtitle_node_state (node, tcr));
280         } else if (node->get_name() == "Text") {
281                 state.push_back (text_node_state (node));
282         } else if (node->get_name() == "SubtitleList") {
283                 state.push_back (ParseState ());
284         } else if (node->get_name() == "Image") {
285                 state.push_back (image_node_state (node));
286         } else {
287                 throw XMLError ("unexpected node " + node->get_name());
288         }
289
290         for (auto i: node->get_children()) {
291                 auto const v = dynamic_cast<xmlpp::ContentNode const *>(i);
292                 if (v) {
293                         maybe_add_subtitle (v->get_content(), state, standard);
294                 }
295                 auto const e = dynamic_cast<xmlpp::Element const *>(i);
296                 if (e) {
297                         parse_subtitles (e, state, tcr, standard);
298                 }
299         }
300
301         state.pop_back ();
302 }
303
304
305 void
306 SubtitleAsset::maybe_add_subtitle (string text, vector<ParseState> const & parse_state, Standard standard)
307 {
308         if (empty_or_white_space (text)) {
309                 return;
310         }
311
312         ParseState ps;
313         for (auto const& i: parse_state) {
314                 if (i.font_id) {
315                         ps.font_id = i.font_id.get();
316                 }
317                 if (i.size) {
318                         ps.size = i.size.get();
319                 }
320                 if (i.aspect_adjust) {
321                         ps.aspect_adjust = i.aspect_adjust.get();
322                 }
323                 if (i.italic) {
324                         ps.italic = i.italic.get();
325                 }
326                 if (i.bold) {
327                         ps.bold = i.bold.get();
328                 }
329                 if (i.underline) {
330                         ps.underline = i.underline.get();
331                 }
332                 if (i.colour) {
333                         ps.colour = i.colour.get();
334                 }
335                 if (i.effect) {
336                         ps.effect = i.effect.get();
337                 }
338                 if (i.effect_colour) {
339                         ps.effect_colour = i.effect_colour.get();
340                 }
341                 if (i.h_position) {
342                         ps.h_position = i.h_position.get();
343                 }
344                 if (i.h_align) {
345                         ps.h_align = i.h_align.get();
346                 }
347                 if (i.v_position) {
348                         ps.v_position = i.v_position.get();
349                 }
350                 if (i.v_align) {
351                         ps.v_align = i.v_align.get();
352                 }
353                 if (i.direction) {
354                         ps.direction = i.direction.get();
355                 }
356                 if (i.in) {
357                         ps.in = i.in.get();
358                 }
359                 if (i.out) {
360                         ps.out = i.out.get();
361                 }
362                 if (i.fade_up_time) {
363                         ps.fade_up_time = i.fade_up_time.get();
364                 }
365                 if (i.fade_down_time) {
366                         ps.fade_down_time = i.fade_down_time.get();
367                 }
368                 if (i.type) {
369                         ps.type = i.type.get();
370                 }
371         }
372
373         if (!ps.in || !ps.out) {
374                 /* We're not in a <Subtitle> node; just ignore this content */
375                 return;
376         }
377
378         DCP_ASSERT (ps.type);
379
380         switch (ps.type.get()) {
381         case ParseState::Type::TEXT:
382                 _subtitles.push_back (
383                         make_shared<SubtitleString>(
384                                 ps.font_id,
385                                 ps.italic.get_value_or (false),
386                                 ps.bold.get_value_or (false),
387                                 ps.underline.get_value_or (false),
388                                 ps.colour.get_value_or (dcp::Colour (255, 255, 255)),
389                                 ps.size.get_value_or (42),
390                                 ps.aspect_adjust.get_value_or (1.0),
391                                 ps.in.get(),
392                                 ps.out.get(),
393                                 ps.h_position.get_value_or(0),
394                                 ps.h_align.get_value_or(HAlign::CENTER),
395                                 ps.v_position.get_value_or(0),
396                                 ps.v_align.get_value_or(VAlign::CENTER),
397                                 ps.direction.get_value_or (Direction::LTR),
398                                 text,
399                                 ps.effect.get_value_or (Effect::NONE),
400                                 ps.effect_colour.get_value_or (dcp::Colour (0, 0, 0)),
401                                 ps.fade_up_time.get_value_or(Time()),
402                                 ps.fade_down_time.get_value_or(Time())
403                                 )
404                         );
405                 break;
406         case ParseState::Type::IMAGE:
407         {
408                 switch (standard) {
409                 case Standard::INTEROP:
410                         if (text.size() >= 4) {
411                                 /* Remove file extension */
412                                 text = text.substr(0, text.size() - 4);
413                         }
414                         break;
415                 case Standard::SMPTE:
416                         /* It looks like this urn:uuid: is required, but DoM wasn't expecting it (and not writing it)
417                          * until around 2.15.140 so I guess either:
418                          *   a) it is not (always) used in the field, or
419                          *   b) nobody noticed / complained.
420                          */
421                         if (text.substr(0, 9) == "urn:uuid:") {
422                                 text = text.substr(9);
423                         }
424                         break;
425                 }
426
427                 /* Add a subtitle with no image data and we'll fill that in later */
428                 _subtitles.push_back (
429                         make_shared<SubtitleImage>(
430                                 ArrayData(),
431                                 text,
432                                 ps.in.get(),
433                                 ps.out.get(),
434                                 ps.h_position.get_value_or(0),
435                                 ps.h_align.get_value_or(HAlign::CENTER),
436                                 ps.v_position.get_value_or(0),
437                                 ps.v_align.get_value_or(VAlign::CENTER),
438                                 ps.fade_up_time.get_value_or(Time()),
439                                 ps.fade_down_time.get_value_or(Time())
440                                 )
441                         );
442                 break;
443         }
444         }
445 }
446
447
448 vector<shared_ptr<const Subtitle>>
449 SubtitleAsset::subtitles () const
450 {
451         vector<shared_ptr<const Subtitle>> s;
452         for (auto i: _subtitles) {
453                 s.push_back (i);
454         }
455         return s;
456 }
457
458
459 vector<shared_ptr<const Subtitle>>
460 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
461 {
462         vector<shared_ptr<const Subtitle>> s;
463         for (auto i: _subtitles) {
464                 if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
465                         s.push_back (i);
466                 }
467         }
468
469         return s;
470 }
471
472
473 /* XXX: this needs a test */
474 vector<shared_ptr<const Subtitle>>
475 SubtitleAsset::subtitles_in_reel (shared_ptr<const dcp::ReelAsset> asset) const
476 {
477         auto frame_rate = asset->edit_rate().as_float();
478         auto start = dcp::Time(asset->entry_point().get_value_or(0), frame_rate, time_code_rate());
479         auto during = subtitles_during (start, start + dcp::Time(asset->intrinsic_duration(), frame_rate, time_code_rate()), false);
480
481         vector<shared_ptr<const dcp::Subtitle>> corrected;
482         for (auto i: during) {
483                 auto c = make_shared<dcp::Subtitle>(*i);
484                 c->set_in (c->in() - start);
485                 c->set_out (c->out() - start);
486                 corrected.push_back (c);
487         }
488
489         return corrected;
490 }
491
492
493 void
494 SubtitleAsset::add (shared_ptr<Subtitle> s)
495 {
496         _subtitles.push_back (s);
497 }
498
499
500 Time
501 SubtitleAsset::latest_subtitle_out () const
502 {
503         Time t;
504         for (auto i: _subtitles) {
505                 if (i->out() > t) {
506                         t = i->out ();
507                 }
508         }
509
510         return t;
511 }
512
513
514 bool
515 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
516 {
517         if (!Asset::equals (other_asset, options, note)) {
518                 return false;
519         }
520
521         auto other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
522         if (!other) {
523                 return false;
524         }
525
526         if (_subtitles.size() != other->_subtitles.size()) {
527                 note (NoteType::ERROR, String::compose("different number of subtitles: %1 vs %2", _subtitles.size(), other->_subtitles.size()));
528                 return false;
529         }
530
531         auto i = _subtitles.begin();
532         auto j = other->_subtitles.begin();
533
534         while (i != _subtitles.end()) {
535                 auto string_i = dynamic_pointer_cast<SubtitleString> (*i);
536                 auto string_j = dynamic_pointer_cast<SubtitleString> (*j);
537                 auto image_i = dynamic_pointer_cast<SubtitleImage> (*i);
538                 auto image_j = dynamic_pointer_cast<SubtitleImage> (*j);
539
540                 if ((string_i && !string_j) || (image_i && !image_j)) {
541                         note (NoteType::ERROR, "subtitles differ: string vs. image");
542                         return false;
543                 }
544
545                 if (string_i && *string_i != *string_j) {
546                         note (NoteType::ERROR, String::compose("subtitles differ in text or metadata: %1 vs %2", string_i->text(), string_j->text()));
547                         return false;
548                 }
549
550                 if (image_i && !image_i->equals(image_j, options, note)) {
551                         return false;
552                 }
553
554                 ++i;
555                 ++j;
556         }
557
558         return true;
559 }
560
561
562 struct SubtitleSorter
563 {
564         bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
565                 if (a->in() != b->in()) {
566                         return a->in() < b->in();
567                 }
568                 return a->v_position() < b->v_position();
569         }
570 };
571
572
573 void
574 SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
575 {
576         if (part->children.empty ()) {
577                 return;
578         }
579
580         /* Pull up from children */
581         for (auto i: part->children) {
582                 pull_fonts (i);
583         }
584
585         if (part->parent) {
586                 /* Establish the common font features that each of part's children have;
587                    these features go into part's font.
588                 */
589                 part->font = part->children.front()->font;
590                 for (auto i: part->children) {
591                         part->font.take_intersection (i->font);
592                 }
593
594                 /* Remove common values from part's children's fonts */
595                 for (auto i: part->children) {
596                         i->font.take_difference (part->font);
597                 }
598         }
599
600         /* Merge adjacent children with the same font */
601         auto i = part->children.begin();
602         vector<shared_ptr<order::Part>> merged;
603
604         while (i != part->children.end()) {
605
606                 if ((*i)->font.empty ()) {
607                         merged.push_back (*i);
608                         ++i;
609                 } else {
610                         auto j = i;
611                         ++j;
612                         while (j != part->children.end() && (*i)->font == (*j)->font) {
613                                 ++j;
614                         }
615                         if (std::distance (i, j) == 1) {
616                                 merged.push_back (*i);
617                                 ++i;
618                         } else {
619                                 shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
620                                 for (auto k = i; k != j; ++k) {
621                                         (*k)->font.clear ();
622                                         group->children.push_back (*k);
623                                 }
624                                 merged.push_back (group);
625                                 i = j;
626                         }
627                 }
628         }
629
630         part->children = merged;
631 }
632
633
634 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
635  *  class because the differences between the two are fairly subtle.
636  */
637 void
638 SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, Standard standard) const
639 {
640         auto sorted = _subtitles;
641         std::stable_sort(sorted.begin(), sorted.end(), SubtitleSorter());
642
643         /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
644            font information into the bottom level (String) objects.
645         */
646
647         auto root = make_shared<order::Part>(shared_ptr<order::Part>());
648         shared_ptr<order::Subtitle> subtitle;
649         shared_ptr<order::Text> text;
650
651         Time last_in;
652         Time last_out;
653         Time last_fade_up_time;
654         Time last_fade_down_time;
655         HAlign last_h_align;
656         float last_h_position;
657         VAlign last_v_align;
658         float last_v_position;
659         Direction last_direction;
660
661         for (auto i: sorted) {
662                 if (!subtitle ||
663                     (last_in != i->in() ||
664                      last_out != i->out() ||
665                      last_fade_up_time != i->fade_up_time() ||
666                      last_fade_down_time != i->fade_down_time())
667                         ) {
668
669                         subtitle = make_shared<order::Subtitle>(root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time());
670                         root->children.push_back (subtitle);
671
672                         last_in = i->in ();
673                         last_out = i->out ();
674                         last_fade_up_time = i->fade_up_time ();
675                         last_fade_down_time = i->fade_down_time ();
676                         text.reset ();
677                 }
678
679                 auto is = dynamic_pointer_cast<SubtitleString>(i);
680                 if (is) {
681                         if (!text ||
682                             last_h_align != is->h_align() ||
683                             fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
684                             last_v_align != is->v_align() ||
685                             fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
686                             last_direction != is->direction()
687                                 ) {
688                                 text.reset (new order::Text (subtitle, is->h_align(), is->h_position(), is->v_align(), is->v_position(), is->direction()));
689                                 subtitle->children.push_back (text);
690
691                                 last_h_align = is->h_align ();
692                                 last_h_position = is->h_position ();
693                                 last_v_align = is->v_align ();
694                                 last_v_position = is->v_position ();
695                                 last_direction = is->direction ();
696                         }
697
698                         text->children.push_back (make_shared<order::String>(text, order::Font (is, standard), is->text()));
699                 }
700
701                 auto ii = dynamic_pointer_cast<SubtitleImage>(i);
702                 if (ii) {
703                         text.reset ();
704                         subtitle->children.push_back (
705                                 make_shared<order::Image>(subtitle, ii->id(), ii->png_image(), ii->h_align(), ii->h_position(), ii->v_align(), ii->v_position())
706                                 );
707                 }
708         }
709
710         /* Pull font changes as high up the hierarchy as we can */
711
712         pull_fonts (root);
713
714         /* Write XML */
715
716         order::Context context;
717         context.time_code_rate = time_code_rate;
718         context.standard = standard;
719         context.spot_number = 1;
720
721         root->write_xml (xml_root, context);
722 }
723
724
725 map<string, ArrayData>
726 SubtitleAsset::font_data () const
727 {
728         map<string, ArrayData> out;
729         for (auto const& i: _fonts) {
730                 out[i.load_id] = i.data;
731         }
732         return out;
733 }
734
735
736 map<string, boost::filesystem::path>
737 SubtitleAsset::font_filenames () const
738 {
739         map<string, boost::filesystem::path> out;
740         for (auto const& i: _fonts) {
741                 if (i.file) {
742                         out[i.load_id] = *i.file;
743                 }
744         }
745         return out;
746 }
747
748
749 /** Replace empty IDs in any <LoadFontId> and <Font> tags with
750  *  a dummy string.  Some systems give errors with empty font IDs
751  *  (see DCP-o-matic bug #1689).
752  */
753 void
754 SubtitleAsset::fix_empty_font_ids ()
755 {
756         bool have_empty = false;
757         vector<string> ids;
758         for (auto i: load_font_nodes()) {
759                 if (i->id == "") {
760                         have_empty = true;
761                 } else {
762                         ids.push_back (i->id);
763                 }
764         }
765
766         if (!have_empty) {
767                 return;
768         }
769
770         string const empty_id = unique_string (ids, "font");
771
772         for (auto i: load_font_nodes()) {
773                 if (i->id == "") {
774                         i->id = empty_id;
775                 }
776         }
777
778         for (auto i: _subtitles) {
779                 auto j = dynamic_pointer_cast<SubtitleString> (i);
780                 if (j && j->font() && j->font().get() == "") {
781                         j->set_font (empty_id);
782                 }
783         }
784 }