2 Copyright (C) 2012-2019 Carl Hetherington <cth@carlh.net>
4 This file is part of libdcp.
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.
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.
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/>.
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
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.
34 #include "raw_convert.h"
35 #include "compose.hpp"
36 #include "subtitle_asset.h"
37 #include "subtitle_asset_internal.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>
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;
64 SubtitleAsset::SubtitleAsset ()
69 SubtitleAsset::SubtitleAsset (boost::filesystem::path file)
76 string_attribute (xmlpp::Element const * node, string name)
78 xmlpp::Attribute* a = node->get_attribute (name);
80 throw XMLError (String::compose ("missing attribute %1", name));
82 return string (a->get_value ());
86 optional_string_attribute (xmlpp::Element const * node, string name)
88 xmlpp::Attribute* a = node->get_attribute (name);
90 return optional<string>();
92 return string (a->get_value ());
96 optional_bool_attribute (xmlpp::Element const * node, string name)
98 optional<string> s = optional_string_attribute (node, name);
100 return optional<bool> ();
103 return (s.get() == "1" || s.get() == "yes");
108 optional_number_attribute (xmlpp::Element const * node, string name)
110 boost::optional<std::string> s = optional_string_attribute (node, name);
112 return boost::optional<T> ();
115 std::string t = s.get ();
116 boost::erase_all (t, " ");
117 return raw_convert<T> (t);
120 SubtitleAsset::ParseState
121 SubtitleAsset::font_node_state (xmlpp::Element const * node, Standard standard) const
125 if (standard == INTEROP) {
126 ps.font_id = optional_string_attribute (node, "Id");
128 ps.font_id = optional_string_attribute (node, "ID");
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");
137 ps.underline = optional_bool_attribute (node, "Underline");
139 optional<string> c = optional_string_attribute (node, "Color");
141 ps.colour = Colour (c.get ());
143 optional<string> const e = optional_string_attribute (node, "Effect");
145 ps.effect = string_to_effect (e.get ());
147 c = optional_string_attribute (node, "EffectColor");
149 ps.effect_colour = Colour (c.get ());
156 SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, xmlpp::Element const * node) const
158 optional<float> hp = optional_number_attribute<float> (node, "HPosition");
160 hp = optional_number_attribute<float> (node, "Hposition");
163 ps.h_position = hp.get () / 100;
166 optional<string> ha = optional_string_attribute (node, "HAlign");
168 ha = optional_string_attribute (node, "Halign");
171 ps.h_align = string_to_halign (ha.get ());
174 optional<float> vp = optional_number_attribute<float> (node, "VPosition");
176 vp = optional_number_attribute<float> (node, "Vposition");
179 ps.v_position = vp.get () / 100;
182 optional<string> va = optional_string_attribute (node, "VAlign");
184 va = optional_string_attribute (node, "Valign");
187 ps.v_align = string_to_valign (va.get ());
192 SubtitleAsset::ParseState
193 SubtitleAsset::text_node_state (xmlpp::Element const * node) const
197 position_align (ps, node);
199 optional<string> d = optional_string_attribute (node, "Direction");
201 ps.direction = string_to_direction (d.get ());
204 ps.type = ParseState::TEXT;
209 SubtitleAsset::ParseState
210 SubtitleAsset::image_node_state (xmlpp::Element const * node) const
214 position_align (ps, node);
216 ps.type = ParseState::IMAGE;
221 SubtitleAsset::ParseState
222 SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional<int> tcr) const
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);
233 SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional<int> tcr) const
235 string const u = optional_string_attribute(node, name).get_value_or ("");
239 t = Time (0, 0, 0, 20, 250);
240 } else if (u.find (":") != string::npos) {
243 t = Time (0, 0, 0, lexical_cast<int> (u), tcr.get_value_or(250));
246 if (t > Time (0, 0, 8, 0, 250)) {
247 t = Time (0, 0, 8, 0, 250);
254 SubtitleAsset::parse_subtitles (xmlpp::Element const * node, list<ParseState>& state, optional<int> tcr, Standard standard)
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));
267 throw XMLError ("unexpected node " + node->get_name());
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);
274 maybe_add_subtitle (v->get_content(), state, standard);
276 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
278 parse_subtitles (e, state, tcr, standard);
286 SubtitleAsset::maybe_add_subtitle (string text, list<ParseState> const & parse_state, Standard standard)
288 if (empty_or_white_space (text)) {
293 BOOST_FOREACH (ParseState const & i, parse_state) {
295 ps.font_id = i.font_id.get();
298 ps.size = i.size.get();
300 if (i.aspect_adjust) {
301 ps.aspect_adjust = i.aspect_adjust.get();
304 ps.italic = i.italic.get();
307 ps.bold = i.bold.get();
310 ps.underline = i.underline.get();
313 ps.colour = i.colour.get();
316 ps.effect = i.effect.get();
318 if (i.effect_colour) {
319 ps.effect_colour = i.effect_colour.get();
322 ps.h_position = i.h_position.get();
325 ps.h_align = i.h_align.get();
328 ps.v_position = i.v_position.get();
331 ps.v_align = i.v_align.get();
334 ps.direction = i.direction.get();
340 ps.out = i.out.get();
342 if (i.fade_up_time) {
343 ps.fade_up_time = i.fade_up_time.get();
345 if (i.fade_down_time) {
346 ps.fade_down_time = i.fade_down_time.get();
349 ps.type = i.type.get();
353 if (!ps.in || !ps.out) {
354 /* We're not in a <Subtitle> node; just ignore this content */
358 DCP_ASSERT (ps.type);
360 switch (ps.type.get()) {
361 case ParseState::TEXT:
362 _subtitles.push_back (
363 shared_ptr<Subtitle> (
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),
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),
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())
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> (
394 standard == INTEROP ? text.substr(0, text.size() - 4) : text,
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())
410 list<shared_ptr<Subtitle> >
411 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
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)) {
424 SubtitleAsset::add (shared_ptr<Subtitle> s)
426 _subtitles.push_back (s);
430 SubtitleAsset::latest_subtitle_out () const
433 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
443 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
445 if (!Asset::equals (other_asset, options, note)) {
449 shared_ptr<const SubtitleAsset> other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
454 if (_subtitles.size() != other->_subtitles.size()) {
455 note (DCP_ERROR, String::compose("different number of subtitles: %1 vs %2", _subtitles.size(), other->_subtitles.size()));
459 list<shared_ptr<Subtitle> >::const_iterator i = _subtitles.begin ();
460 list<shared_ptr<Subtitle> >::const_iterator j = other->_subtitles.begin ();
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);
468 if ((string_i && !string_j) || (image_i && !image_j)) {
469 note (DCP_ERROR, "subtitles differ: string vs. image");
473 if (string_i && *string_i != *string_j) {
474 note (DCP_ERROR, String::compose("subtitles differ in text or metadata: %1 vs %2", string_i->text(), string_j->text()));
478 if (image_i && !image_i->equals(image_j, options, note)) {
489 struct SubtitleSorter
491 bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
492 if (a->in() != b->in()) {
493 return a->in() < b->in();
495 return a->v_position() < b->v_position();
500 SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
502 if (part->children.empty ()) {
506 /* Pull up from children */
507 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
512 /* Establish the common font features that each of part's children have;
513 these features go into part's font.
515 part->font = part->children.front()->font;
516 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
517 part->font.take_intersection (i->font);
520 /* Remove common values from part's children's fonts */
521 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
522 i->font.take_difference (part->font);
526 /* Merge adjacent children with the same font */
527 list<shared_ptr<order::Part> >::const_iterator i = part->children.begin();
528 list<shared_ptr<order::Part> > merged;
530 while (i != part->children.end()) {
532 if ((*i)->font.empty ()) {
533 merged.push_back (*i);
536 list<shared_ptr<order::Part> >::const_iterator j = i;
538 while (j != part->children.end() && (*i)->font == (*j)->font) {
541 if (std::distance (i, j) == 1) {
542 merged.push_back (*i);
545 shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
546 for (list<shared_ptr<order::Part> >::const_iterator k = i; k != j; ++k) {
548 group->children.push_back (*k);
550 merged.push_back (group);
556 part->children = merged;
559 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
560 * class because the differences between the two are fairly subtle.
563 SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, Standard standard) const
565 list<shared_ptr<Subtitle> > sorted = _subtitles;
566 sorted.sort (SubtitleSorter ());
568 /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
569 font information into the bottom level (String) objects.
572 shared_ptr<order::Part> root (new order::Part (shared_ptr<order::Part> ()));
573 shared_ptr<order::Subtitle> subtitle;
574 shared_ptr<order::Text> text;
578 Time last_fade_up_time;
579 Time last_fade_down_time;
581 float last_h_position;
583 float last_v_position;
584 Direction last_direction;
586 BOOST_FOREACH (shared_ptr<Subtitle> i, sorted) {
588 (last_in != i->in() ||
589 last_out != i->out() ||
590 last_fade_up_time != i->fade_up_time() ||
591 last_fade_down_time != i->fade_down_time())
594 subtitle.reset (new order::Subtitle (root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time()));
595 root->children.push_back (subtitle);
598 last_out = i->out ();
599 last_fade_up_time = i->fade_up_time ();
600 last_fade_down_time = i->fade_down_time ();
604 shared_ptr<SubtitleString> is = dynamic_pointer_cast<SubtitleString>(i);
607 last_h_align != is->h_align() ||
608 fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
609 last_v_align != is->v_align() ||
610 fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
611 last_direction != is->direction()
613 text.reset (new order::Text (subtitle, is->h_align(), is->h_position(), is->v_align(), is->v_position(), is->direction()));
614 subtitle->children.push_back (text);
616 last_h_align = is->h_align ();
617 last_h_position = is->h_position ();
618 last_v_align = is->v_align ();
619 last_v_position = is->v_position ();
620 last_direction = is->direction ();
623 text->children.push_back (shared_ptr<order::String> (new order::String (text, order::Font (is, standard), is->text())));
626 shared_ptr<SubtitleImage> ii = dynamic_pointer_cast<SubtitleImage>(i);
629 subtitle->children.push_back (
630 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()))
635 /* Pull font changes as high up the hierarchy as we can */
641 order::Context context;
642 context.time_code_rate = time_code_rate;
643 context.standard = standard;
644 context.spot_number = 1;
646 root->write_xml (xml_root, context);
649 map<string, ArrayData>
650 SubtitleAsset::font_data () const
652 map<string, ArrayData> out;
653 BOOST_FOREACH (Font const & i, _fonts) {
654 out[i.load_id] = i.data;
660 map<string, boost::filesystem::path>
661 SubtitleAsset::font_filenames () const
663 map<string, boost::filesystem::path> out;
664 BOOST_FOREACH (Font const& i, _fonts) {
666 out[i.load_id] = *i.file;
673 /** Replace empty IDs in any <LoadFontId> and <Font> tags with
674 * a dummy string. Some systems give errors with empty font IDs
675 * (see DCP-o-matic bug #1689).
678 SubtitleAsset::fix_empty_font_ids ()
680 bool have_empty = false;
682 BOOST_FOREACH (shared_ptr<LoadFontNode> i, load_font_nodes()) {
686 ids.push_back (i->id);
694 string const empty_id = unique_string (ids, "font");
696 BOOST_FOREACH (shared_ptr<LoadFontNode> i, load_font_nodes()) {
702 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
703 shared_ptr<SubtitleString> j = dynamic_pointer_cast<SubtitleString> (i);
704 if (j && j->font() && j->font().get() == "") {
705 j->set_font (empty_id);