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 locked_stringstream u;
115 u.imbue (std::locale::classic ());
122 SubtitleAsset::ParseState
123 SubtitleAsset::font_node_state (xmlpp::Element const * node, Standard standard) const
127 if (standard == INTEROP) {
128 ps.font_id = optional_string_attribute (node, "Id");
130 ps.font_id = optional_string_attribute (node, "ID");
132 ps.size = optional_number_attribute<int64_t> (node, "Size");
133 ps.aspect_adjust = optional_number_attribute<float> (node, "AspectAdjust");
134 ps.italic = optional_bool_attribute (node, "Italic");
135 ps.bold = optional_string_attribute(node, "Weight").get_value_or("normal") == "bold";
136 if (standard == INTEROP) {
137 ps.underline = optional_bool_attribute (node, "Underlined");
139 ps.underline = optional_bool_attribute (node, "Underline");
141 optional<string> c = optional_string_attribute (node, "Color");
143 ps.colour = Colour (c.get ());
145 optional<string> const e = optional_string_attribute (node, "Effect");
147 ps.effect = string_to_effect (e.get ());
149 c = optional_string_attribute (node, "EffectColor");
151 ps.effect_colour = Colour (c.get ());
157 SubtitleAsset::ParseState
158 SubtitleAsset::text_node_state (xmlpp::Element const * node) const
162 optional<float> hp = optional_number_attribute<float> (node, "HPosition");
164 hp = optional_number_attribute<float> (node, "Hposition");
167 ps.h_position = hp.get () / 100;
170 optional<string> ha = optional_string_attribute (node, "HAlign");
172 ha = optional_string_attribute (node, "Halign");
175 ps.h_align = string_to_halign (ha.get ());
178 optional<float> vp = optional_number_attribute<float> (node, "VPosition");
180 vp = optional_number_attribute<float> (node, "Vposition");
183 ps.v_position = vp.get () / 100;
186 optional<string> va = optional_string_attribute (node, "VAlign");
188 va = optional_string_attribute (node, "Valign");
191 ps.v_align = string_to_valign (va.get ());
194 optional<string> d = optional_string_attribute (node, "Direction");
196 ps.direction = string_to_direction (d.get ());
202 SubtitleAsset::ParseState
203 SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional<int> tcr) const
206 ps.in = Time (string_attribute(node, "TimeIn"), tcr);
207 ps.out = Time (string_attribute(node, "TimeOut"), tcr);
208 ps.fade_up_time = fade_time (node, "FadeUpTime", tcr);
209 ps.fade_down_time = fade_time (node, "FadeDownTime", tcr);
214 SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional<int> tcr) const
216 string const u = optional_string_attribute(node, name).get_value_or ("");
220 t = Time (0, 0, 0, 20, 250);
221 } else if (u.find (":") != string::npos) {
224 t = Time (0, 0, 0, lexical_cast<int> (u), tcr.get_value_or(250));
227 if (t > Time (0, 0, 8, 0, 250)) {
228 t = Time (0, 0, 8, 0, 250);
235 SubtitleAsset::parse_subtitles (xmlpp::Element const * node, list<ParseState>& state, optional<int> tcr, Standard standard)
237 if (node->get_name() == "Font") {
238 state.push_back (font_node_state (node, standard));
239 } else if (node->get_name() == "Subtitle") {
240 state.push_back (subtitle_node_state (node, tcr));
241 } else if (node->get_name() == "Text") {
242 state.push_back (text_node_state (node));
243 } else if (node->get_name() == "SubtitleList") {
244 state.push_back (ParseState ());
246 throw XMLError ("unexpected node " + node->get_name());
249 xmlpp::Node::NodeList c = node->get_children ();
250 for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
251 xmlpp::ContentNode const * v = dynamic_cast<xmlpp::ContentNode const *> (*i);
253 maybe_add_subtitle (v->get_content(), state);
255 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
257 parse_subtitles (e, state, tcr, standard);
265 SubtitleAsset::maybe_add_subtitle (string text, list<ParseState> const & parse_state)
267 if (empty_or_white_space (text)) {
272 BOOST_FOREACH (ParseState const & i, parse_state) {
274 ps.font_id = i.font_id.get();
277 ps.size = i.size.get();
279 if (i.aspect_adjust) {
280 ps.aspect_adjust = i.aspect_adjust.get();
283 ps.italic = i.italic.get();
286 ps.bold = i.bold.get();
289 ps.underline = i.underline.get();
292 ps.colour = i.colour.get();
295 ps.effect = i.effect.get();
297 if (i.effect_colour) {
298 ps.effect_colour = i.effect_colour.get();
301 ps.h_position = i.h_position.get();
304 ps.h_align = i.h_align.get();
307 ps.v_position = i.v_position.get();
310 ps.v_align = i.v_align.get();
313 ps.direction = i.direction.get();
319 ps.out = i.out.get();
321 if (i.fade_up_time) {
322 ps.fade_up_time = i.fade_up_time.get();
324 if (i.fade_down_time) {
325 ps.fade_down_time = i.fade_down_time.get();
329 if (!ps.in || !ps.out) {
330 /* We're not in a <Text> node; just ignore this content */
334 _subtitles.push_back (
337 ps.italic.get_value_or (false),
338 ps.bold.get_value_or (false),
339 ps.underline.get_value_or (false),
340 ps.colour.get_value_or (dcp::Colour (255, 255, 255)),
341 ps.size.get_value_or (42),
342 ps.aspect_adjust.get_value_or (1.0),
345 ps.h_position.get_value_or(0),
346 ps.h_align.get_value_or(HALIGN_CENTER),
347 ps.v_position.get_value_or(0),
348 ps.v_align.get_value_or(VALIGN_CENTER),
349 ps.direction.get_value_or (DIRECTION_LTR),
351 ps.effect.get_value_or (NONE),
352 ps.effect_colour.get_value_or (dcp::Colour (255, 255, 255)),
353 ps.fade_up_time.get_value_or(Time()),
354 ps.fade_down_time.get_value_or(Time())
360 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
362 list<SubtitleString> s;
363 BOOST_FOREACH (SubtitleString const & i, _subtitles) {
364 if ((starting && from <= i.in() && i.in() < to) || (!starting && i.out() >= from && i.in() <= to)) {
373 SubtitleAsset::add (SubtitleString s)
375 _subtitles.push_back (s);
379 SubtitleAsset::latest_subtitle_out () const
382 BOOST_FOREACH (SubtitleString const & i, _subtitles) {
392 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
394 if (!Asset::equals (other_asset, options, note)) {
398 shared_ptr<const SubtitleAsset> other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
403 if (_subtitles != other->_subtitles) {
404 note (DCP_ERROR, "subtitles differ");
411 struct SubtitleSorter {
412 bool operator() (SubtitleString const & a, SubtitleString const & b) {
413 if (a.in() != b.in()) {
414 return a.in() < b.in();
416 return a.v_position() < b.v_position();
420 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
421 * class because the differences between the two are fairly subtle.
424 SubtitleAsset::subtitles_as_xml (xmlpp::Element* root, int time_code_rate, Standard standard) const
426 list<SubtitleString> sorted = _subtitles;
427 sorted.sort (SubtitleSorter ());
429 string const xmlns = standard == SMPTE ? "dcst" : "";
431 /* XXX: script not supported */
433 optional<string> font;
436 bool underline = false;
439 float aspect_adjust = 1.0;
440 Effect effect = NONE;
441 Colour effect_colour;
445 Time last_fade_up_time;
446 Time last_fade_down_time;
448 xmlpp::Element* font_element = 0;
449 xmlpp::Element* subtitle_element = 0;
451 BOOST_FOREACH (SubtitleString const & i, sorted) {
453 /* We will start a new <Font>...</Font> whenever some font property changes.
454 I suppose we should really make an optimal hierarchy of <Font> tags, but
458 bool const font_changed =
460 italic != i.italic() ||
462 underline != i.underline() ||
463 colour != i.colour() ||
465 fabs (aspect_adjust - i.aspect_adjust()) > ASPECT_ADJUST_EPSILON ||
466 effect != i.effect() ||
467 effect_colour != i.effect_colour();
471 italic = i.italic ();
473 underline = i.underline ();
474 colour = i.colour ();
476 aspect_adjust = i.aspect_adjust ();
477 effect = i.effect ();
478 effect_colour = i.effect_colour ();
481 if (!font_element || font_changed) {
482 font_element = root->add_child ("Font", xmlns);
484 if (standard == SMPTE) {
485 font_element->set_attribute ("ID", font.get ());
487 font_element->set_attribute ("Id", font.get ());
490 font_element->set_attribute ("Italic", italic ? "yes" : "no");
491 font_element->set_attribute ("Color", colour.to_argb_string());
492 font_element->set_attribute ("Size", raw_convert<string> (size));
493 if (fabs (aspect_adjust - 1.0) > ASPECT_ADJUST_EPSILON) {
494 font_element->set_attribute ("AspectAdjust", raw_convert<string> (aspect_adjust));
496 font_element->set_attribute ("Effect", effect_to_string (effect));
497 font_element->set_attribute ("EffectColor", effect_colour.to_argb_string());
498 font_element->set_attribute ("Script", "normal");
499 if (standard == SMPTE) {
500 font_element->set_attribute ("Underline", underline ? "yes" : "no");
502 font_element->set_attribute ("Underlined", underline ? "yes" : "no");
504 font_element->set_attribute ("Weight", bold ? "bold" : "normal");
507 if (!subtitle_element || font_changed ||
508 (last_in != i.in() ||
509 last_out != i.out() ||
510 last_fade_up_time != i.fade_up_time() ||
511 last_fade_down_time != i.fade_down_time()
514 subtitle_element = font_element->add_child ("Subtitle", xmlns);
515 subtitle_element->set_attribute ("SpotNumber", raw_convert<string> (spot_number++));
516 subtitle_element->set_attribute ("TimeIn", i.in().rebase(time_code_rate).as_string(standard));
517 subtitle_element->set_attribute ("TimeOut", i.out().rebase(time_code_rate).as_string(standard));
518 if (standard == SMPTE) {
519 subtitle_element->set_attribute ("FadeUpTime", i.fade_up_time().rebase(time_code_rate).as_string(standard));
520 subtitle_element->set_attribute ("FadeDownTime", i.fade_down_time().rebase(time_code_rate).as_string(standard));
522 subtitle_element->set_attribute ("FadeUpTime", raw_convert<string> (i.fade_up_time().as_editable_units(time_code_rate)));
523 subtitle_element->set_attribute ("FadeDownTime", raw_convert<string> (i.fade_down_time().as_editable_units(time_code_rate)));
528 last_fade_up_time = i.fade_up_time ();
529 last_fade_down_time = i.fade_down_time ();
532 xmlpp::Element* text = subtitle_element->add_child ("Text", xmlns);
534 if (i.h_align() != HALIGN_CENTER) {
535 if (standard == SMPTE) {
536 text->set_attribute ("Halign", halign_to_string (i.h_align ()));
538 text->set_attribute ("HAlign", halign_to_string (i.h_align ()));
542 if (i.h_position() > ALIGN_EPSILON) {
543 if (standard == SMPTE) {
544 text->set_attribute ("Hposition", raw_convert<string> (i.h_position() * 100, 6));
546 text->set_attribute ("HPosition", raw_convert<string> (i.h_position() * 100, 6));
550 if (standard == SMPTE) {
551 text->set_attribute ("Valign", valign_to_string (i.v_align()));
553 text->set_attribute ("VAlign", valign_to_string (i.v_align()));
556 if (i.v_position() > ALIGN_EPSILON) {
557 if (standard == SMPTE) {
558 text->set_attribute ("Vposition", raw_convert<string> (i.v_position() * 100, 6));
560 text->set_attribute ("VPosition", raw_convert<string> (i.v_position() * 100, 6));
563 if (standard == SMPTE) {
564 text->set_attribute ("Vposition", "0");
566 text->set_attribute ("VPosition", "0");
570 /* Interop only supports "horizontal" or "vertical" for direction, so only write this
573 if (i.direction() != DIRECTION_LTR && standard == SMPTE) {
574 text->set_attribute ("Direction", direction_to_string (i.direction ()));
577 text->add_child_text (i.text());
582 SubtitleAsset::fonts_with_load_ids () const
584 map<string, Data> out;
585 BOOST_FOREACH (Font const & i, _fonts) {
586 out[i.load_id] = i.data;