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