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);
377 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage> (s);
379 _image_subtitle_uuid[si] = make_uuid ();
384 SubtitleAsset::latest_subtitle_out () const
387 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
397 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
399 if (!Asset::equals (other_asset, options, note)) {
403 shared_ptr<const SubtitleAsset> other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
408 if (_subtitles.size() != other->_subtitles.size()) {
409 note (DCP_ERROR, "subtitles differ");
413 list<shared_ptr<Subtitle> >::const_iterator i = _subtitles.begin ();
414 list<shared_ptr<Subtitle> >::const_iterator j = other->_subtitles.begin ();
416 while (i != _subtitles.end()) {
417 shared_ptr<SubtitleString> string_i = dynamic_pointer_cast<SubtitleString> (*i);
418 shared_ptr<SubtitleString> string_j = dynamic_pointer_cast<SubtitleString> (*j);
419 shared_ptr<SubtitleImage> image_i = dynamic_pointer_cast<SubtitleImage> (*i);
420 shared_ptr<SubtitleImage> image_j = dynamic_pointer_cast<SubtitleImage> (*j);
422 if ((string_i && !string_j) || (image_i && !image_j)) {
423 note (DCP_ERROR, "subtitles differ");
427 if (string_i && *string_i != *string_j) {
428 note (DCP_ERROR, "subtitles differ");
432 if (image_i && *image_i != *image_j) {
433 note (DCP_ERROR, "subtitles differ");
441 struct SubtitleSorter
443 bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
444 if (a->in() != b->in()) {
445 return a->in() < b->in();
447 return a->v_position() < b->v_position();
452 SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
454 if (part->children.empty ()) {
458 /* Pull up from children */
459 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
464 /* Establish the common font features that each of part's children have;
465 these features go into part's font.
467 part->font = part->children.front()->font;
468 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
469 part->font.take_intersection (i->font);
472 /* Remove common values from part's children's fonts */
473 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
474 i->font.take_difference (part->font);
478 /* Merge adjacent children with the same font */
479 list<shared_ptr<order::Part> >::const_iterator i = part->children.begin();
480 list<shared_ptr<order::Part> > merged;
482 while (i != part->children.end()) {
484 if ((*i)->font.empty ()) {
485 merged.push_back (*i);
488 list<shared_ptr<order::Part> >::const_iterator j = i;
490 while (j != part->children.end() && (*i)->font == (*j)->font) {
493 if (distance (i, j) == 1) {
494 merged.push_back (*i);
497 shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
498 for (list<shared_ptr<order::Part> >::const_iterator k = i; k != j; ++k) {
500 group->children.push_back (*k);
502 merged.push_back (group);
508 part->children = merged;
511 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
512 * class because the differences between the two are fairly subtle.
515 SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, Standard standard) const
517 list<shared_ptr<Subtitle> > sorted = _subtitles;
518 sorted.sort (SubtitleSorter ());
520 /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
521 font information into the bottom level (String) objects.
524 shared_ptr<order::Part> root (new order::Part (shared_ptr<order::Part> ()));
525 shared_ptr<order::Subtitle> subtitle;
526 shared_ptr<order::Text> text;
530 Time last_fade_up_time;
531 Time last_fade_down_time;
533 float last_h_position;
535 float last_v_position;
536 Direction last_direction;
538 BOOST_FOREACH (shared_ptr<Subtitle> i, sorted) {
540 (last_in != i->in() ||
541 last_out != i->out() ||
542 last_fade_up_time != i->fade_up_time() ||
543 last_fade_down_time != i->fade_down_time())
546 subtitle.reset (new order::Subtitle (root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time()));
547 root->children.push_back (subtitle);
550 last_out = i->out ();
551 last_fade_up_time = i->fade_up_time ();
552 last_fade_down_time = i->fade_down_time ();
556 shared_ptr<SubtitleString> is = dynamic_pointer_cast<SubtitleString>(i);
559 last_h_align != is->h_align() ||
560 fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
561 last_v_align != is->v_align() ||
562 fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
563 last_direction != is->direction()
565 text.reset (new order::Text (subtitle, is->h_align(), is->h_position(), is->v_align(), is->v_position(), is->direction()));
566 subtitle->children.push_back (text);
568 last_h_align = is->h_align ();
569 last_h_position = is->h_position ();
570 last_v_align = is->v_align ();
571 last_v_position = is->v_position ();
572 last_direction = is->direction ();
575 text->children.push_back (shared_ptr<order::String> (new order::String (text, order::Font (is, standard), is->text())));
578 shared_ptr<SubtitleImage> ii = dynamic_pointer_cast<SubtitleImage>(i);
581 ImageUUIDMap::const_iterator uuid = _image_subtitle_uuid.find(ii);
582 DCP_ASSERT (uuid != _image_subtitle_uuid.end());
583 subtitle->children.push_back (
584 shared_ptr<order::Image> (new order::Image (subtitle, uuid->second, ii->png_image(), ii->h_align(), ii->h_position(), ii->v_align(), ii->v_position()))
589 /* Pull font changes as high up the hierarchy as we can */
595 order::Context context;
596 context.time_code_rate = time_code_rate;
597 context.standard = standard;
598 context.spot_number = 1;
600 root->write_xml (xml_root, context);
604 SubtitleAsset::fonts_with_load_ids () const
606 map<string, Data> out;
607 BOOST_FOREACH (Font const & i, _fonts) {
608 out[i.load_id] = i.data;