2 Copyright (C) 2012-2018 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 <asdcp/AS_DCP.h>
44 #include <asdcp/KM_util.h>
45 #include <libxml++/nodes/element.h>
46 #include <boost/algorithm/string.hpp>
47 #include <boost/lexical_cast.hpp>
48 #include <boost/shared_array.hpp>
49 #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 ());
155 SubtitleAsset::ParseState
156 SubtitleAsset::text_node_state (xmlpp::Element const * node) const
160 optional<float> hp = optional_number_attribute<float> (node, "HPosition");
162 hp = optional_number_attribute<float> (node, "Hposition");
165 ps.h_position = hp.get () / 100;
168 optional<string> ha = optional_string_attribute (node, "HAlign");
170 ha = optional_string_attribute (node, "Halign");
173 ps.h_align = string_to_halign (ha.get ());
176 optional<float> vp = optional_number_attribute<float> (node, "VPosition");
178 vp = optional_number_attribute<float> (node, "Vposition");
181 ps.v_position = vp.get () / 100;
184 optional<string> va = optional_string_attribute (node, "VAlign");
186 va = optional_string_attribute (node, "Valign");
189 ps.v_align = string_to_valign (va.get ());
192 optional<string> d = optional_string_attribute (node, "Direction");
194 ps.direction = string_to_direction (d.get ());
200 SubtitleAsset::ParseState
201 SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional<int> tcr) const
204 ps.in = Time (string_attribute(node, "TimeIn"), tcr);
205 ps.out = Time (string_attribute(node, "TimeOut"), tcr);
206 ps.fade_up_time = fade_time (node, "FadeUpTime", tcr);
207 ps.fade_down_time = fade_time (node, "FadeDownTime", tcr);
212 SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional<int> tcr) const
214 string const u = optional_string_attribute(node, name).get_value_or ("");
218 t = Time (0, 0, 0, 20, 250);
219 } else if (u.find (":") != string::npos) {
222 t = Time (0, 0, 0, lexical_cast<int> (u), tcr.get_value_or(250));
225 if (t > Time (0, 0, 8, 0, 250)) {
226 t = Time (0, 0, 8, 0, 250);
233 SubtitleAsset::parse_subtitles (xmlpp::Element const * node, list<ParseState>& state, optional<int> tcr, Standard standard)
235 if (node->get_name() == "Font") {
236 state.push_back (font_node_state (node, standard));
237 } else if (node->get_name() == "Subtitle") {
238 state.push_back (subtitle_node_state (node, tcr));
239 } else if (node->get_name() == "Text") {
240 state.push_back (text_node_state (node));
241 } else if (node->get_name() == "SubtitleList") {
242 state.push_back (ParseState ());
244 throw XMLError ("unexpected node " + node->get_name());
247 xmlpp::Node::NodeList c = node->get_children ();
248 for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
249 xmlpp::ContentNode const * v = dynamic_cast<xmlpp::ContentNode const *> (*i);
251 maybe_add_subtitle (v->get_content(), state);
253 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
255 parse_subtitles (e, state, tcr, standard);
263 SubtitleAsset::maybe_add_subtitle (string text, list<ParseState> const & parse_state)
265 if (empty_or_white_space (text)) {
270 BOOST_FOREACH (ParseState const & i, parse_state) {
272 ps.font_id = i.font_id.get();
275 ps.size = i.size.get();
277 if (i.aspect_adjust) {
278 ps.aspect_adjust = i.aspect_adjust.get();
281 ps.italic = i.italic.get();
284 ps.bold = i.bold.get();
287 ps.underline = i.underline.get();
290 ps.colour = i.colour.get();
293 ps.effect = i.effect.get();
295 if (i.effect_colour) {
296 ps.effect_colour = i.effect_colour.get();
299 ps.h_position = i.h_position.get();
302 ps.h_align = i.h_align.get();
305 ps.v_position = i.v_position.get();
308 ps.v_align = i.v_align.get();
311 ps.direction = i.direction.get();
317 ps.out = i.out.get();
319 if (i.fade_up_time) {
320 ps.fade_up_time = i.fade_up_time.get();
322 if (i.fade_down_time) {
323 ps.fade_down_time = i.fade_down_time.get();
327 if (!ps.in || !ps.out) {
328 /* We're not in a <Text> node; just ignore this content */
332 _subtitles.push_back (
333 shared_ptr<Subtitle> (
336 ps.italic.get_value_or (false),
337 ps.bold.get_value_or (false),
338 ps.underline.get_value_or (false),
339 ps.colour.get_value_or (dcp::Colour (255, 255, 255)),
340 ps.size.get_value_or (42),
341 ps.aspect_adjust.get_value_or (1.0),
344 ps.h_position.get_value_or(0),
345 ps.h_align.get_value_or(HALIGN_CENTER),
346 ps.v_position.get_value_or(0),
347 ps.v_align.get_value_or(VALIGN_CENTER),
348 ps.direction.get_value_or (DIRECTION_LTR),
350 ps.effect.get_value_or (NONE),
351 ps.effect_colour.get_value_or (dcp::Colour (0, 0, 0)),
352 ps.fade_up_time.get_value_or(Time()),
353 ps.fade_down_time.get_value_or(Time())
359 list<shared_ptr<Subtitle> >
360 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
362 list<shared_ptr<Subtitle> > s;
363 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
364 if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
373 SubtitleAsset::add (shared_ptr<Subtitle> s)
375 _subtitles.push_back (s);
379 SubtitleAsset::latest_subtitle_out () const
382 BOOST_FOREACH (shared_ptr<Subtitle> 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.size() != other->_subtitles.size()) {
404 note (DCP_ERROR, "subtitles differ");
408 list<shared_ptr<Subtitle> >::const_iterator i = _subtitles.begin ();
409 list<shared_ptr<Subtitle> >::const_iterator j = other->_subtitles.begin ();
411 while (i != _subtitles.end()) {
412 shared_ptr<SubtitleString> string_i = dynamic_pointer_cast<SubtitleString> (*i);
413 shared_ptr<SubtitleString> string_j = dynamic_pointer_cast<SubtitleString> (*j);
414 shared_ptr<SubtitleImage> image_i = dynamic_pointer_cast<SubtitleImage> (*i);
415 shared_ptr<SubtitleImage> image_j = dynamic_pointer_cast<SubtitleImage> (*j);
417 if ((string_i && !string_j) || (image_i && !image_j)) {
418 note (DCP_ERROR, "subtitles differ");
422 if (string_i && *string_i != *string_j) {
423 note (DCP_ERROR, "subtitles differ");
427 if (image_i && *image_i != *image_j) {
428 note (DCP_ERROR, "subtitles differ");
436 struct SubtitleSorter
438 bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
439 if (a->in() != b->in()) {
440 return a->in() < b->in();
442 return a->v_position() < b->v_position();
447 SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
449 if (part->children.empty ()) {
453 /* Pull up from children */
454 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
459 /* Establish the common font features that each of part's children have;
460 these features go into part's font.
462 part->font = part->children.front()->font;
463 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
464 part->font.take_intersection (i->font);
467 /* Remove common values from part's children's fonts */
468 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
469 i->font.take_difference (part->font);
473 /* Merge adjacent children with the same font */
474 list<shared_ptr<order::Part> >::const_iterator i = part->children.begin();
475 list<shared_ptr<order::Part> > merged;
477 while (i != part->children.end()) {
479 if ((*i)->font.empty ()) {
480 merged.push_back (*i);
483 list<shared_ptr<order::Part> >::const_iterator j = i;
485 while (j != part->children.end() && (*i)->font == (*j)->font) {
488 if (distance (i, j) == 1) {
489 merged.push_back (*i);
492 shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
493 for (list<shared_ptr<order::Part> >::const_iterator k = i; k != j; ++k) {
495 group->children.push_back (*k);
497 merged.push_back (group);
503 part->children = merged;
506 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
507 * class because the differences between the two are fairly subtle.
510 SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, Standard standard) const
512 list<shared_ptr<Subtitle> > sorted = _subtitles;
513 sorted.sort (SubtitleSorter ());
515 /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
516 font information into the bottom level (String) objects.
519 shared_ptr<order::Part> root (new order::Part (shared_ptr<order::Part> ()));
520 shared_ptr<order::Subtitle> subtitle;
521 shared_ptr<order::Text> text;
525 Time last_fade_up_time;
526 Time last_fade_down_time;
528 float last_h_position;
530 float last_v_position;
531 Direction last_direction;
533 BOOST_FOREACH (shared_ptr<Subtitle> i, sorted) {
535 (last_in != i->in() ||
536 last_out != i->out() ||
537 last_fade_up_time != i->fade_up_time() ||
538 last_fade_down_time != i->fade_down_time())
541 subtitle.reset (new order::Subtitle (root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time()));
542 root->children.push_back (subtitle);
545 last_out = i->out ();
546 last_fade_up_time = i->fade_up_time ();
547 last_fade_down_time = i->fade_down_time ();
551 shared_ptr<SubtitleString> is = dynamic_pointer_cast<SubtitleString>(i);
555 last_h_align != is->h_align() ||
556 fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
557 last_v_align != is->v_align() ||
558 fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
559 last_direction != is->direction()
561 text.reset (new order::Text (subtitle, is->h_align(), is->h_position(), is->v_align(), is->v_position(), is->direction()));
562 subtitle->children.push_back (text);
564 last_h_align = is->h_align ();
565 last_h_position = is->h_position ();
566 last_v_align = is->v_align ();
567 last_v_position = is->v_position ();
568 last_direction = is->direction ();
571 text->children.push_back (shared_ptr<order::String> (new order::String (text, order::Font (is, standard), is->text())));
577 /* Pull font changes as high up the hierarchy as we can */
583 order::Context context;
584 context.time_code_rate = time_code_rate;
585 context.standard = standard;
586 context.spot_number = 1;
588 root->write_xml (xml_root, context);
592 SubtitleAsset::fonts_with_load_ids () const
594 map<string, Data> out;
595 BOOST_FOREACH (Font const & i, _fonts) {
596 out[i.load_id] = i.data;