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