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>
56 using boost::shared_ptr;
57 using boost::shared_array;
58 using boost::optional;
59 using boost::dynamic_pointer_cast;
60 using boost::lexical_cast;
63 SubtitleAsset::SubtitleAsset ()
68 SubtitleAsset::SubtitleAsset (boost::filesystem::path file)
75 string_attribute (xmlpp::Element const * node, string name)
77 xmlpp::Attribute* a = node->get_attribute (name);
79 throw XMLError (String::compose ("missing attribute %1", name));
81 return string (a->get_value ());
85 optional_string_attribute (xmlpp::Element const * node, string name)
87 xmlpp::Attribute* a = node->get_attribute (name);
89 return optional<string>();
91 return string (a->get_value ());
95 optional_bool_attribute (xmlpp::Element const * node, string name)
97 optional<string> s = optional_string_attribute (node, name);
99 return optional<bool> ();
102 return (s.get() == "1" || s.get() == "yes");
107 optional_number_attribute (xmlpp::Element const * node, string name)
109 boost::optional<std::string> s = optional_string_attribute (node, name);
111 return boost::optional<T> ();
114 std::string t = s.get ();
115 boost::erase_all (t, " ");
116 return raw_convert<T> (t);
119 SubtitleAsset::ParseState
120 SubtitleAsset::font_node_state (xmlpp::Element const * node, Standard standard) const
124 if (standard == INTEROP) {
125 ps.font_id = optional_string_attribute (node, "Id");
127 ps.font_id = optional_string_attribute (node, "ID");
129 ps.size = optional_number_attribute<int64_t> (node, "Size");
130 ps.aspect_adjust = optional_number_attribute<float> (node, "AspectAdjust");
131 ps.italic = optional_bool_attribute (node, "Italic");
132 ps.bold = optional_string_attribute(node, "Weight").get_value_or("normal") == "bold";
133 if (standard == INTEROP) {
134 ps.underline = optional_bool_attribute (node, "Underlined");
136 ps.underline = optional_bool_attribute (node, "Underline");
138 optional<string> c = optional_string_attribute (node, "Color");
140 ps.colour = Colour (c.get ());
142 optional<string> const e = optional_string_attribute (node, "Effect");
144 ps.effect = string_to_effect (e.get ());
146 c = optional_string_attribute (node, "EffectColor");
148 ps.effect_colour = Colour (c.get ());
155 SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, 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 ());
191 SubtitleAsset::ParseState
192 SubtitleAsset::text_node_state (xmlpp::Element const * node) const
196 position_align (ps, node);
198 optional<string> d = optional_string_attribute (node, "Direction");
200 ps.direction = string_to_direction (d.get ());
203 ps.type = ParseState::TEXT;
208 SubtitleAsset::ParseState
209 SubtitleAsset::image_node_state (xmlpp::Element const * node) const
213 position_align (ps, node);
215 ps.type = ParseState::IMAGE;
220 SubtitleAsset::ParseState
221 SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional<int> tcr) const
224 ps.in = Time (string_attribute(node, "TimeIn"), tcr);
225 ps.out = Time (string_attribute(node, "TimeOut"), tcr);
226 ps.fade_up_time = fade_time (node, "FadeUpTime", tcr);
227 ps.fade_down_time = fade_time (node, "FadeDownTime", tcr);
232 SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional<int> tcr) const
234 string const u = optional_string_attribute(node, name).get_value_or ("");
238 t = Time (0, 0, 0, 20, 250);
239 } else if (u.find (":") != string::npos) {
242 t = Time (0, 0, 0, lexical_cast<int> (u), tcr.get_value_or(250));
245 if (t > Time (0, 0, 8, 0, 250)) {
246 t = Time (0, 0, 8, 0, 250);
253 SubtitleAsset::parse_subtitles (xmlpp::Element const * node, list<ParseState>& state, optional<int> tcr, Standard standard)
255 if (node->get_name() == "Font") {
256 state.push_back (font_node_state (node, standard));
257 } else if (node->get_name() == "Subtitle") {
258 state.push_back (subtitle_node_state (node, tcr));
259 } else if (node->get_name() == "Text") {
260 state.push_back (text_node_state (node));
261 } else if (node->get_name() == "SubtitleList") {
262 state.push_back (ParseState ());
263 } else if (node->get_name() == "Image") {
264 state.push_back (image_node_state (node));
266 throw XMLError ("unexpected node " + node->get_name());
269 xmlpp::Node::NodeList c = node->get_children ();
270 for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
271 xmlpp::ContentNode const * v = dynamic_cast<xmlpp::ContentNode const *> (*i);
273 maybe_add_subtitle (v->get_content(), state, standard);
275 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
277 parse_subtitles (e, state, tcr, standard);
285 SubtitleAsset::maybe_add_subtitle (string text, list<ParseState> const & parse_state, Standard standard)
287 if (empty_or_white_space (text)) {
292 BOOST_FOREACH (ParseState const & i, parse_state) {
294 ps.font_id = i.font_id.get();
297 ps.size = i.size.get();
299 if (i.aspect_adjust) {
300 ps.aspect_adjust = i.aspect_adjust.get();
303 ps.italic = i.italic.get();
306 ps.bold = i.bold.get();
309 ps.underline = i.underline.get();
312 ps.colour = i.colour.get();
315 ps.effect = i.effect.get();
317 if (i.effect_colour) {
318 ps.effect_colour = i.effect_colour.get();
321 ps.h_position = i.h_position.get();
324 ps.h_align = i.h_align.get();
327 ps.v_position = i.v_position.get();
330 ps.v_align = i.v_align.get();
333 ps.direction = i.direction.get();
339 ps.out = i.out.get();
341 if (i.fade_up_time) {
342 ps.fade_up_time = i.fade_up_time.get();
344 if (i.fade_down_time) {
345 ps.fade_down_time = i.fade_down_time.get();
348 ps.type = i.type.get();
352 if (!ps.in || !ps.out) {
353 /* We're not in a <Subtitle> node; just ignore this content */
357 DCP_ASSERT (ps.type);
359 switch (ps.type.get()) {
360 case ParseState::TEXT:
361 _subtitles.push_back (
362 shared_ptr<Subtitle> (
365 ps.italic.get_value_or (false),
366 ps.bold.get_value_or (false),
367 ps.underline.get_value_or (false),
368 ps.colour.get_value_or (dcp::Colour (255, 255, 255)),
369 ps.size.get_value_or (42),
370 ps.aspect_adjust.get_value_or (1.0),
373 ps.h_position.get_value_or(0),
374 ps.h_align.get_value_or(HALIGN_CENTER),
375 ps.v_position.get_value_or(0),
376 ps.v_align.get_value_or(VALIGN_CENTER),
377 ps.direction.get_value_or (DIRECTION_LTR),
379 ps.effect.get_value_or (NONE),
380 ps.effect_colour.get_value_or (dcp::Colour (0, 0, 0)),
381 ps.fade_up_time.get_value_or(Time()),
382 ps.fade_down_time.get_value_or(Time())
387 case ParseState::IMAGE:
388 /* Add a subtitle with no image data and we'll fill that in later */
389 _subtitles.push_back (
390 shared_ptr<Subtitle> (
393 standard == INTEROP ? text.substr(0, text.size() - 4) : text,
396 ps.h_position.get_value_or(0),
397 ps.h_align.get_value_or(HALIGN_CENTER),
398 ps.v_position.get_value_or(0),
399 ps.v_align.get_value_or(VALIGN_CENTER),
400 ps.fade_up_time.get_value_or(Time()),
401 ps.fade_down_time.get_value_or(Time())
409 list<shared_ptr<Subtitle> >
410 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
412 list<shared_ptr<Subtitle> > s;
413 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
414 if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
423 SubtitleAsset::add (shared_ptr<Subtitle> s)
425 _subtitles.push_back (s);
429 SubtitleAsset::latest_subtitle_out () const
432 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
442 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
444 if (!Asset::equals (other_asset, options, note)) {
448 shared_ptr<const SubtitleAsset> other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
453 if (_subtitles.size() != other->_subtitles.size()) {
454 note (DCP_ERROR, "subtitles differ");
458 list<shared_ptr<Subtitle> >::const_iterator i = _subtitles.begin ();
459 list<shared_ptr<Subtitle> >::const_iterator j = other->_subtitles.begin ();
461 while (i != _subtitles.end()) {
462 shared_ptr<SubtitleString> string_i = dynamic_pointer_cast<SubtitleString> (*i);
463 shared_ptr<SubtitleString> string_j = dynamic_pointer_cast<SubtitleString> (*j);
464 shared_ptr<SubtitleImage> image_i = dynamic_pointer_cast<SubtitleImage> (*i);
465 shared_ptr<SubtitleImage> image_j = dynamic_pointer_cast<SubtitleImage> (*j);
467 if ((string_i && !string_j) || (image_i && !image_j)) {
468 note (DCP_ERROR, "subtitles differ");
472 if (string_i && *string_i != *string_j) {
473 note (DCP_ERROR, "subtitles differ");
477 if (image_i && *image_i != *image_j) {
478 note (DCP_ERROR, "subtitles differ");
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);
650 SubtitleAsset::fonts_with_load_ids () const
652 map<string, Data> out;
653 BOOST_FOREACH (Font const & i, _fonts) {
654 out[i.load_id] = i.data;