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