2 Copyright (C) 2012-2015 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"
39 #include "subtitle_string.h"
40 #include "dcp_assert.h"
41 #include <asdcp/AS_DCP.h>
42 #include <asdcp/KM_util.h>
43 #include <libxml++/nodes/element.h>
44 #include <boost/algorithm/string.hpp>
45 #include <boost/lexical_cast.hpp>
46 #include <boost/shared_array.hpp>
47 #include <boost/foreach.hpp>
54 using boost::shared_ptr;
55 using boost::shared_array;
56 using boost::optional;
57 using boost::dynamic_pointer_cast;
58 using boost::lexical_cast;
61 SubtitleAsset::SubtitleAsset ()
66 SubtitleAsset::SubtitleAsset (boost::filesystem::path file)
73 string_attribute (xmlpp::Element const * node, string name)
75 xmlpp::Attribute* a = node->get_attribute (name);
77 throw XMLError (String::compose ("missing attribute %1", name));
79 return string (a->get_value ());
83 optional_string_attribute (xmlpp::Element const * node, string name)
85 xmlpp::Attribute* a = node->get_attribute (name);
87 return optional<string>();
89 return string (a->get_value ());
93 optional_bool_attribute (xmlpp::Element const * node, string name)
95 optional<string> s = optional_string_attribute (node, name);
97 return optional<bool> ();
100 return (s.get() == "1" || s.get() == "yes");
105 optional_number_attribute (xmlpp::Element const * node, string name)
107 boost::optional<std::string> s = optional_string_attribute (node, name);
109 return boost::optional<T> ();
112 std::string t = s.get ();
113 boost::erase_all (t, " ");
114 return raw_convert<T> (t);
117 SubtitleAsset::ParseState
118 SubtitleAsset::font_node_state (xmlpp::Element const * node, Standard standard) const
122 if (standard == INTEROP) {
123 ps.font_id = optional_string_attribute (node, "Id");
125 ps.font_id = optional_string_attribute (node, "ID");
127 ps.size = optional_number_attribute<int64_t> (node, "Size");
128 ps.aspect_adjust = optional_number_attribute<float> (node, "AspectAdjust");
129 ps.italic = optional_bool_attribute (node, "Italic");
130 ps.bold = optional_string_attribute(node, "Weight").get_value_or("normal") == "bold";
131 if (standard == INTEROP) {
132 ps.underline = optional_bool_attribute (node, "Underlined");
134 ps.underline = optional_bool_attribute (node, "Underline");
136 optional<string> c = optional_string_attribute (node, "Color");
138 ps.colour = Colour (c.get ());
140 optional<string> const e = optional_string_attribute (node, "Effect");
142 ps.effect = string_to_effect (e.get ());
144 c = optional_string_attribute (node, "EffectColor");
146 ps.effect_colour = Colour (c.get ());
152 SubtitleAsset::ParseState
153 SubtitleAsset::text_node_state (xmlpp::Element const * node) const
157 optional<float> hp = optional_number_attribute<float> (node, "HPosition");
159 hp = optional_number_attribute<float> (node, "Hposition");
162 ps.h_position = hp.get () / 100;
165 optional<string> ha = optional_string_attribute (node, "HAlign");
167 ha = optional_string_attribute (node, "Halign");
170 ps.h_align = string_to_halign (ha.get ());
173 optional<float> vp = optional_number_attribute<float> (node, "VPosition");
175 vp = optional_number_attribute<float> (node, "Vposition");
178 ps.v_position = vp.get () / 100;
181 optional<string> va = optional_string_attribute (node, "VAlign");
183 va = optional_string_attribute (node, "Valign");
186 ps.v_align = string_to_valign (va.get ());
189 optional<string> d = optional_string_attribute (node, "Direction");
191 ps.direction = string_to_direction (d.get ());
197 SubtitleAsset::ParseState
198 SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional<int> tcr) const
201 ps.in = Time (string_attribute(node, "TimeIn"), tcr);
202 ps.out = Time (string_attribute(node, "TimeOut"), tcr);
203 ps.fade_up_time = fade_time (node, "FadeUpTime", tcr);
204 ps.fade_down_time = fade_time (node, "FadeDownTime", tcr);
209 SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional<int> tcr) const
211 string const u = optional_string_attribute(node, name).get_value_or ("");
215 t = Time (0, 0, 0, 20, 250);
216 } else if (u.find (":") != string::npos) {
219 t = Time (0, 0, 0, lexical_cast<int> (u), tcr.get_value_or(250));
222 if (t > Time (0, 0, 8, 0, 250)) {
223 t = Time (0, 0, 8, 0, 250);
230 SubtitleAsset::parse_subtitles (xmlpp::Element const * node, list<ParseState>& state, optional<int> tcr, Standard standard)
232 if (node->get_name() == "Font") {
233 state.push_back (font_node_state (node, standard));
234 } else if (node->get_name() == "Subtitle") {
235 state.push_back (subtitle_node_state (node, tcr));
236 } else if (node->get_name() == "Text") {
237 state.push_back (text_node_state (node));
238 } else if (node->get_name() == "SubtitleList") {
239 state.push_back (ParseState ());
241 throw XMLError ("unexpected node " + node->get_name());
244 xmlpp::Node::NodeList c = node->get_children ();
245 for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
246 xmlpp::ContentNode const * v = dynamic_cast<xmlpp::ContentNode const *> (*i);
248 maybe_add_subtitle (v->get_content(), state);
250 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
252 parse_subtitles (e, state, tcr, standard);
260 SubtitleAsset::maybe_add_subtitle (string text, list<ParseState> const & parse_state)
262 if (empty_or_white_space (text)) {
267 BOOST_FOREACH (ParseState const & i, parse_state) {
269 ps.font_id = i.font_id.get();
272 ps.size = i.size.get();
274 if (i.aspect_adjust) {
275 ps.aspect_adjust = i.aspect_adjust.get();
278 ps.italic = i.italic.get();
281 ps.bold = i.bold.get();
284 ps.underline = i.underline.get();
287 ps.colour = i.colour.get();
290 ps.effect = i.effect.get();
292 if (i.effect_colour) {
293 ps.effect_colour = i.effect_colour.get();
296 ps.h_position = i.h_position.get();
299 ps.h_align = i.h_align.get();
302 ps.v_position = i.v_position.get();
305 ps.v_align = i.v_align.get();
308 ps.direction = i.direction.get();
314 ps.out = i.out.get();
316 if (i.fade_up_time) {
317 ps.fade_up_time = i.fade_up_time.get();
319 if (i.fade_down_time) {
320 ps.fade_down_time = i.fade_down_time.get();
324 if (!ps.in || !ps.out) {
325 /* We're not in a <Text> node; just ignore this content */
329 _subtitles.push_back (
332 ps.italic.get_value_or (false),
333 ps.bold.get_value_or (false),
334 ps.underline.get_value_or (false),
335 ps.colour.get_value_or (dcp::Colour (255, 255, 255)),
336 ps.size.get_value_or (42),
337 ps.aspect_adjust.get_value_or (1.0),
340 ps.h_position.get_value_or(0),
341 ps.h_align.get_value_or(HALIGN_CENTER),
342 ps.v_position.get_value_or(0),
343 ps.v_align.get_value_or(VALIGN_CENTER),
344 ps.direction.get_value_or (DIRECTION_LTR),
346 ps.effect.get_value_or (NONE),
347 ps.effect_colour.get_value_or (dcp::Colour (255, 255, 255)),
348 ps.fade_up_time.get_value_or(Time()),
349 ps.fade_down_time.get_value_or(Time())
355 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
357 list<SubtitleString> s;
358 BOOST_FOREACH (SubtitleString const & i, _subtitles) {
359 if ((starting && from <= i.in() && i.in() < to) || (!starting && i.out() >= from && i.in() <= to)) {
368 SubtitleAsset::add (SubtitleString s)
370 _subtitles.push_back (s);
374 SubtitleAsset::latest_subtitle_out () const
377 BOOST_FOREACH (SubtitleString const & i, _subtitles) {
387 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
389 if (!Asset::equals (other_asset, options, note)) {
393 shared_ptr<const SubtitleAsset> other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
398 if (_subtitles != other->_subtitles) {
399 note (DCP_ERROR, "subtitles differ");
406 struct SubtitleSorter {
407 bool operator() (SubtitleString const & a, SubtitleString const & b) {
408 if (a.in() != b.in()) {
409 return a.in() < b.in();
411 return a.v_position() < b.v_position();
415 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
416 * class because the differences between the two are fairly subtle.
419 SubtitleAsset::subtitles_as_xml (xmlpp::Element* root, int time_code_rate, Standard standard) const
421 list<SubtitleString> sorted = _subtitles;
422 sorted.sort (SubtitleSorter ());
424 string const xmlns = standard == SMPTE ? "dcst" : "";
426 /* XXX: script not supported */
428 optional<string> font;
431 bool underline = false;
434 float aspect_adjust = 1.0;
435 Effect effect = NONE;
436 Colour effect_colour;
440 Time last_fade_up_time;
441 Time last_fade_down_time;
443 xmlpp::Element* font_element = 0;
444 xmlpp::Element* subtitle_element = 0;
446 BOOST_FOREACH (SubtitleString const & i, sorted) {
448 /* We will start a new <Font>...</Font> whenever some font property changes.
449 I suppose we should really make an optimal hierarchy of <Font> tags, but
453 bool const font_changed =
455 italic != i.italic() ||
457 underline != i.underline() ||
458 colour != i.colour() ||
460 fabs (aspect_adjust - i.aspect_adjust()) > ASPECT_ADJUST_EPSILON ||
461 effect != i.effect() ||
462 effect_colour != i.effect_colour();
466 italic = i.italic ();
468 underline = i.underline ();
469 colour = i.colour ();
471 aspect_adjust = i.aspect_adjust ();
472 effect = i.effect ();
473 effect_colour = i.effect_colour ();
476 if (!font_element || font_changed) {
477 font_element = root->add_child ("Font", xmlns);
479 if (standard == SMPTE) {
480 font_element->set_attribute ("ID", font.get ());
482 font_element->set_attribute ("Id", font.get ());
485 font_element->set_attribute ("Italic", italic ? "yes" : "no");
486 font_element->set_attribute ("Color", colour.to_argb_string());
487 font_element->set_attribute ("Size", raw_convert<string> (size));
488 if (fabs (aspect_adjust - 1.0) > ASPECT_ADJUST_EPSILON) {
489 font_element->set_attribute ("AspectAdjust", raw_convert<string> (aspect_adjust));
491 font_element->set_attribute ("Effect", effect_to_string (effect));
492 font_element->set_attribute ("EffectColor", effect_colour.to_argb_string());
493 font_element->set_attribute ("Script", "normal");
494 if (standard == SMPTE) {
495 font_element->set_attribute ("Underline", underline ? "yes" : "no");
497 font_element->set_attribute ("Underlined", underline ? "yes" : "no");
499 font_element->set_attribute ("Weight", bold ? "bold" : "normal");
502 if (!subtitle_element || font_changed ||
503 (last_in != i.in() ||
504 last_out != i.out() ||
505 last_fade_up_time != i.fade_up_time() ||
506 last_fade_down_time != i.fade_down_time()
509 subtitle_element = font_element->add_child ("Subtitle", xmlns);
510 subtitle_element->set_attribute ("SpotNumber", raw_convert<string> (spot_number++));
511 subtitle_element->set_attribute ("TimeIn", i.in().rebase(time_code_rate).as_string(standard));
512 subtitle_element->set_attribute ("TimeOut", i.out().rebase(time_code_rate).as_string(standard));
513 if (standard == SMPTE) {
514 subtitle_element->set_attribute ("FadeUpTime", i.fade_up_time().rebase(time_code_rate).as_string(standard));
515 subtitle_element->set_attribute ("FadeDownTime", i.fade_down_time().rebase(time_code_rate).as_string(standard));
517 subtitle_element->set_attribute ("FadeUpTime", raw_convert<string> (i.fade_up_time().as_editable_units(time_code_rate)));
518 subtitle_element->set_attribute ("FadeDownTime", raw_convert<string> (i.fade_down_time().as_editable_units(time_code_rate)));
523 last_fade_up_time = i.fade_up_time ();
524 last_fade_down_time = i.fade_down_time ();
527 xmlpp::Element* text = subtitle_element->add_child ("Text", xmlns);
529 if (i.h_align() != HALIGN_CENTER) {
530 if (standard == SMPTE) {
531 text->set_attribute ("Halign", halign_to_string (i.h_align ()));
533 text->set_attribute ("HAlign", halign_to_string (i.h_align ()));
537 if (i.h_position() > ALIGN_EPSILON) {
538 if (standard == SMPTE) {
539 text->set_attribute ("Hposition", raw_convert<string> (i.h_position() * 100, 6));
541 text->set_attribute ("HPosition", raw_convert<string> (i.h_position() * 100, 6));
545 if (standard == SMPTE) {
546 text->set_attribute ("Valign", valign_to_string (i.v_align()));
548 text->set_attribute ("VAlign", valign_to_string (i.v_align()));
551 if (i.v_position() > ALIGN_EPSILON) {
552 if (standard == SMPTE) {
553 text->set_attribute ("Vposition", raw_convert<string> (i.v_position() * 100, 6));
555 text->set_attribute ("VPosition", raw_convert<string> (i.v_position() * 100, 6));
558 if (standard == SMPTE) {
559 text->set_attribute ("Vposition", "0");
561 text->set_attribute ("VPosition", "0");
565 /* Interop only supports "horizontal" or "vertical" for direction, so only write this
568 if (i.direction() != DIRECTION_LTR && standard == SMPTE) {
569 text->set_attribute ("Direction", direction_to_string (i.direction ()));
572 text->add_child_text (i.text());
577 SubtitleAsset::fonts_with_load_ids () const
579 map<string, Data> out;
580 BOOST_FOREACH (Font const & i, _fonts) {
581 out[i.load_id] = i.data;