Tidying.
[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                 /* Add a subtitle with no image data and we'll fill that in later */
408                 _subtitles.push_back (
409                         make_shared<SubtitleImage>(
410                                 ArrayData(),
411                                 standard == Standard::INTEROP ? text.substr(0, text.size() - 4) : text,
412                                 ps.in.get(),
413                                 ps.out.get(),
414                                 ps.h_position.get_value_or(0),
415                                 ps.h_align.get_value_or(HAlign::CENTER),
416                                 ps.v_position.get_value_or(0),
417                                 ps.v_align.get_value_or(VAlign::CENTER),
418                                 ps.fade_up_time.get_value_or(Time()),
419                                 ps.fade_down_time.get_value_or(Time())
420                                 )
421                         );
422                 break;
423         }
424 }
425
426
427 vector<shared_ptr<const Subtitle>>
428 SubtitleAsset::subtitles () const
429 {
430         vector<shared_ptr<const Subtitle>> s;
431         for (auto i: _subtitles) {
432                 s.push_back (i);
433         }
434         return s;
435 }
436
437
438 vector<shared_ptr<const Subtitle>>
439 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
440 {
441         vector<shared_ptr<const Subtitle>> s;
442         for (auto i: _subtitles) {
443                 if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
444                         s.push_back (i);
445                 }
446         }
447
448         return s;
449 }
450
451
452 /* XXX: this needs a test */
453 vector<shared_ptr<const Subtitle>>
454 SubtitleAsset::subtitles_in_reel (shared_ptr<const dcp::ReelAsset> asset) const
455 {
456         auto frame_rate = asset->edit_rate().as_float();
457         auto start = dcp::Time(asset->entry_point().get_value_or(0), frame_rate, time_code_rate());
458         auto during = subtitles_during (start, start + dcp::Time(asset->intrinsic_duration(), frame_rate, time_code_rate()), false);
459
460         vector<shared_ptr<const dcp::Subtitle>> corrected;
461         for (auto i: during) {
462                 auto c = make_shared<dcp::Subtitle>(*i);
463                 c->set_in (c->in() - start);
464                 c->set_out (c->out() - start);
465                 corrected.push_back (c);
466         }
467
468         return corrected;
469 }
470
471
472 void
473 SubtitleAsset::add (shared_ptr<Subtitle> s)
474 {
475         _subtitles.push_back (s);
476 }
477
478
479 Time
480 SubtitleAsset::latest_subtitle_out () const
481 {
482         Time t;
483         for (auto i: _subtitles) {
484                 if (i->out() > t) {
485                         t = i->out ();
486                 }
487         }
488
489         return t;
490 }
491
492
493 bool
494 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
495 {
496         if (!Asset::equals (other_asset, options, note)) {
497                 return false;
498         }
499
500         auto other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
501         if (!other) {
502                 return false;
503         }
504
505         if (_subtitles.size() != other->_subtitles.size()) {
506                 note (NoteType::ERROR, String::compose("different number of subtitles: %1 vs %2", _subtitles.size(), other->_subtitles.size()));
507                 return false;
508         }
509
510         auto i = _subtitles.begin();
511         auto j = other->_subtitles.begin();
512
513         while (i != _subtitles.end()) {
514                 auto string_i = dynamic_pointer_cast<SubtitleString> (*i);
515                 auto string_j = dynamic_pointer_cast<SubtitleString> (*j);
516                 auto image_i = dynamic_pointer_cast<SubtitleImage> (*i);
517                 auto image_j = dynamic_pointer_cast<SubtitleImage> (*j);
518
519                 if ((string_i && !string_j) || (image_i && !image_j)) {
520                         note (NoteType::ERROR, "subtitles differ: string vs. image");
521                         return false;
522                 }
523
524                 if (string_i && *string_i != *string_j) {
525                         note (NoteType::ERROR, String::compose("subtitles differ in text or metadata: %1 vs %2", string_i->text(), string_j->text()));
526                         return false;
527                 }
528
529                 if (image_i && !image_i->equals(image_j, options, note)) {
530                         return false;
531                 }
532
533                 ++i;
534                 ++j;
535         }
536
537         return true;
538 }
539
540
541 struct SubtitleSorter
542 {
543         bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
544                 if (a->in() != b->in()) {
545                         return a->in() < b->in();
546                 }
547                 return a->v_position() < b->v_position();
548         }
549 };
550
551
552 void
553 SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
554 {
555         if (part->children.empty ()) {
556                 return;
557         }
558
559         /* Pull up from children */
560         for (auto i: part->children) {
561                 pull_fonts (i);
562         }
563
564         if (part->parent) {
565                 /* Establish the common font features that each of part's children have;
566                    these features go into part's font.
567                 */
568                 part->font = part->children.front()->font;
569                 for (auto i: part->children) {
570                         part->font.take_intersection (i->font);
571                 }
572
573                 /* Remove common values from part's children's fonts */
574                 for (auto i: part->children) {
575                         i->font.take_difference (part->font);
576                 }
577         }
578
579         /* Merge adjacent children with the same font */
580         auto i = part->children.begin();
581         vector<shared_ptr<order::Part>> merged;
582
583         while (i != part->children.end()) {
584
585                 if ((*i)->font.empty ()) {
586                         merged.push_back (*i);
587                         ++i;
588                 } else {
589                         auto j = i;
590                         ++j;
591                         while (j != part->children.end() && (*i)->font == (*j)->font) {
592                                 ++j;
593                         }
594                         if (std::distance (i, j) == 1) {
595                                 merged.push_back (*i);
596                                 ++i;
597                         } else {
598                                 shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
599                                 for (auto k = i; k != j; ++k) {
600                                         (*k)->font.clear ();
601                                         group->children.push_back (*k);
602                                 }
603                                 merged.push_back (group);
604                                 i = j;
605                         }
606                 }
607         }
608
609         part->children = merged;
610 }
611
612
613 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
614  *  class because the differences between the two are fairly subtle.
615  */
616 void
617 SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, Standard standard) const
618 {
619         auto sorted = _subtitles;
620         std::stable_sort(sorted.begin(), sorted.end(), SubtitleSorter());
621
622         /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
623            font information into the bottom level (String) objects.
624         */
625
626         auto root = make_shared<order::Part>(shared_ptr<order::Part>());
627         shared_ptr<order::Subtitle> subtitle;
628         shared_ptr<order::Text> text;
629
630         Time last_in;
631         Time last_out;
632         Time last_fade_up_time;
633         Time last_fade_down_time;
634         HAlign last_h_align;
635         float last_h_position;
636         VAlign last_v_align;
637         float last_v_position;
638         Direction last_direction;
639
640         for (auto i: sorted) {
641                 if (!subtitle ||
642                     (last_in != i->in() ||
643                      last_out != i->out() ||
644                      last_fade_up_time != i->fade_up_time() ||
645                      last_fade_down_time != i->fade_down_time())
646                         ) {
647
648                         subtitle = make_shared<order::Subtitle>(root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time());
649                         root->children.push_back (subtitle);
650
651                         last_in = i->in ();
652                         last_out = i->out ();
653                         last_fade_up_time = i->fade_up_time ();
654                         last_fade_down_time = i->fade_down_time ();
655                         text.reset ();
656                 }
657
658                 auto is = dynamic_pointer_cast<SubtitleString>(i);
659                 if (is) {
660                         if (!text ||
661                             last_h_align != is->h_align() ||
662                             fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
663                             last_v_align != is->v_align() ||
664                             fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
665                             last_direction != is->direction()
666                                 ) {
667                                 text.reset (new order::Text (subtitle, is->h_align(), is->h_position(), is->v_align(), is->v_position(), is->direction()));
668                                 subtitle->children.push_back (text);
669
670                                 last_h_align = is->h_align ();
671                                 last_h_position = is->h_position ();
672                                 last_v_align = is->v_align ();
673                                 last_v_position = is->v_position ();
674                                 last_direction = is->direction ();
675                         }
676
677                         text->children.push_back (shared_ptr<order::String> (new order::String (text, order::Font (is, standard), is->text())));
678                 }
679
680                 auto ii = dynamic_pointer_cast<SubtitleImage>(i);
681                 if (ii) {
682                         text.reset ();
683                         subtitle->children.push_back (
684                                 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()))
685                                 );
686                 }
687         }
688
689         /* Pull font changes as high up the hierarchy as we can */
690
691         pull_fonts (root);
692
693         /* Write XML */
694
695         order::Context context;
696         context.time_code_rate = time_code_rate;
697         context.standard = standard;
698         context.spot_number = 1;
699
700         root->write_xml (xml_root, context);
701 }
702
703
704 map<string, ArrayData>
705 SubtitleAsset::font_data () const
706 {
707         map<string, ArrayData> out;
708         for (auto const& i: _fonts) {
709                 out[i.load_id] = i.data;
710         }
711         return out;
712 }
713
714
715 map<string, boost::filesystem::path>
716 SubtitleAsset::font_filenames () const
717 {
718         map<string, boost::filesystem::path> out;
719         for (auto const& i: _fonts) {
720                 if (i.file) {
721                         out[i.load_id] = *i.file;
722                 }
723         }
724         return out;
725 }
726
727
728 /** Replace empty IDs in any <LoadFontId> and <Font> tags with
729  *  a dummy string.  Some systems give errors with empty font IDs
730  *  (see DCP-o-matic bug #1689).
731  */
732 void
733 SubtitleAsset::fix_empty_font_ids ()
734 {
735         bool have_empty = false;
736         vector<string> ids;
737         for (auto i: load_font_nodes()) {
738                 if (i->id == "") {
739                         have_empty = true;
740                 } else {
741                         ids.push_back (i->id);
742                 }
743         }
744
745         if (!have_empty) {
746                 return;
747         }
748
749         string const empty_id = unique_string (ids, "font");
750
751         for (auto i: load_font_nodes()) {
752                 if (i->id == "") {
753                         i->id = empty_id;
754                 }
755         }
756
757         for (auto i: _subtitles) {
758                 auto j = dynamic_pointer_cast<SubtitleString> (i);
759                 if (j && j->font() && j->font().get() == "") {
760                         j->set_font (empty_id);
761                 }
762         }
763 }