2 Copyright (C) 2012-2019 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 "load_font_node.h"
44 #include <asdcp/AS_DCP.h>
45 #include <asdcp/KM_util.h>
46 #include <libxml++/nodes/element.h>
47 #include <boost/algorithm/string.hpp>
48 #include <boost/lexical_cast.hpp>
49 #include <boost/shared_array.hpp>
50 #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 ());
156 SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, xmlpp::Element const * node) const
158 optional<float> hp = optional_number_attribute<float> (node, "HPosition");
160 hp = optional_number_attribute<float> (node, "Hposition");
163 ps.h_position = hp.get () / 100;
166 optional<string> ha = optional_string_attribute (node, "HAlign");
168 ha = optional_string_attribute (node, "Halign");
171 ps.h_align = string_to_halign (ha.get ());
174 optional<float> vp = optional_number_attribute<float> (node, "VPosition");
176 vp = optional_number_attribute<float> (node, "Vposition");
179 ps.v_position = vp.get () / 100;
182 optional<string> va = optional_string_attribute (node, "VAlign");
184 va = optional_string_attribute (node, "Valign");
187 ps.v_align = string_to_valign (va.get ());
192 SubtitleAsset::ParseState
193 SubtitleAsset::text_node_state (xmlpp::Element const * node) const
197 position_align (ps, node);
199 optional<string> d = optional_string_attribute (node, "Direction");
201 ps.direction = string_to_direction (d.get ());
204 ps.type = ParseState::TEXT;
209 SubtitleAsset::ParseState
210 SubtitleAsset::image_node_state (xmlpp::Element const * node) const
214 position_align (ps, node);
216 ps.type = ParseState::IMAGE;
221 SubtitleAsset::ParseState
222 SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional<int> tcr) const
225 ps.in = Time (string_attribute(node, "TimeIn"), tcr);
226 ps.out = Time (string_attribute(node, "TimeOut"), tcr);
227 ps.fade_up_time = fade_time (node, "FadeUpTime", tcr);
228 ps.fade_down_time = fade_time (node, "FadeDownTime", tcr);
233 SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional<int> tcr) const
235 string const u = optional_string_attribute(node, name).get_value_or ("");
239 t = Time (0, 0, 0, 20, 250);
240 } else if (u.find (":") != string::npos) {
243 t = Time (0, 0, 0, lexical_cast<int> (u), tcr.get_value_or(250));
246 if (t > Time (0, 0, 8, 0, 250)) {
247 t = Time (0, 0, 8, 0, 250);
254 SubtitleAsset::parse_subtitles (xmlpp::Element const * node, list<ParseState>& state, optional<int> tcr, Standard standard)
256 if (node->get_name() == "Font") {
257 state.push_back (font_node_state (node, standard));
258 } else if (node->get_name() == "Subtitle") {
259 state.push_back (subtitle_node_state (node, tcr));
260 } else if (node->get_name() == "Text") {
261 state.push_back (text_node_state (node));
262 } else if (node->get_name() == "SubtitleList") {
263 state.push_back (ParseState ());
264 } else if (node->get_name() == "Image") {
265 state.push_back (image_node_state (node));
267 throw XMLError ("unexpected node " + node->get_name());
270 xmlpp::Node::NodeList c = node->get_children ();
271 for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
272 xmlpp::ContentNode const * v = dynamic_cast<xmlpp::ContentNode const *> (*i);
274 maybe_add_subtitle (v->get_content(), state, standard);
276 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
278 parse_subtitles (e, state, tcr, standard);
286 SubtitleAsset::maybe_add_subtitle (string text, list<ParseState> const & parse_state, Standard standard)
288 if (empty_or_white_space (text)) {
293 BOOST_FOREACH (ParseState const & i, parse_state) {
295 ps.font_id = i.font_id.get();
298 ps.size = i.size.get();
300 if (i.aspect_adjust) {
301 ps.aspect_adjust = i.aspect_adjust.get();
304 ps.italic = i.italic.get();
307 ps.bold = i.bold.get();
310 ps.underline = i.underline.get();
313 ps.colour = i.colour.get();
316 ps.effect = i.effect.get();
318 if (i.effect_colour) {
319 ps.effect_colour = i.effect_colour.get();
322 ps.h_position = i.h_position.get();
325 ps.h_align = i.h_align.get();
328 ps.v_position = i.v_position.get();
331 ps.v_align = i.v_align.get();
334 ps.direction = i.direction.get();
340 ps.out = i.out.get();
342 if (i.fade_up_time) {
343 ps.fade_up_time = i.fade_up_time.get();
345 if (i.fade_down_time) {
346 ps.fade_down_time = i.fade_down_time.get();
349 ps.type = i.type.get();
353 if (!ps.in || !ps.out) {
354 /* We're not in a <Subtitle> node; just ignore this content */
358 DCP_ASSERT (ps.type);
360 switch (ps.type.get()) {
361 case ParseState::TEXT:
362 _subtitles.push_back (
363 shared_ptr<Subtitle> (
366 ps.italic.get_value_or (false),
367 ps.bold.get_value_or (false),
368 ps.underline.get_value_or (false),
369 ps.colour.get_value_or (dcp::Colour (255, 255, 255)),
370 ps.size.get_value_or (42),
371 ps.aspect_adjust.get_value_or (1.0),
374 ps.h_position.get_value_or(0),
375 ps.h_align.get_value_or(HALIGN_CENTER),
376 ps.v_position.get_value_or(0),
377 ps.v_align.get_value_or(VALIGN_CENTER),
378 ps.direction.get_value_or (DIRECTION_LTR),
380 ps.effect.get_value_or (NONE),
381 ps.effect_colour.get_value_or (dcp::Colour (0, 0, 0)),
382 ps.fade_up_time.get_value_or(Time()),
383 ps.fade_down_time.get_value_or(Time())
388 case ParseState::IMAGE:
389 /* Add a subtitle with no image data and we'll fill that in later */
390 _subtitles.push_back (
391 shared_ptr<Subtitle> (
394 standard == INTEROP ? text.substr(0, text.size() - 4) : text,
397 ps.h_position.get_value_or(0),
398 ps.h_align.get_value_or(HALIGN_CENTER),
399 ps.v_position.get_value_or(0),
400 ps.v_align.get_value_or(VALIGN_CENTER),
401 ps.fade_up_time.get_value_or(Time()),
402 ps.fade_down_time.get_value_or(Time())
410 list<shared_ptr<Subtitle> >
411 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
413 list<shared_ptr<Subtitle> > s;
414 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
415 if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
424 SubtitleAsset::add (shared_ptr<Subtitle> s)
426 _subtitles.push_back (s);
430 SubtitleAsset::latest_subtitle_out () const
433 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
443 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
445 if (!Asset::equals (other_asset, options, note)) {
449 shared_ptr<const SubtitleAsset> other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
454 if (_subtitles.size() != other->_subtitles.size()) {
455 note (DCP_ERROR, "subtitles differ");
459 list<shared_ptr<Subtitle> >::const_iterator i = _subtitles.begin ();
460 list<shared_ptr<Subtitle> >::const_iterator j = other->_subtitles.begin ();
462 while (i != _subtitles.end()) {
463 shared_ptr<SubtitleString> string_i = dynamic_pointer_cast<SubtitleString> (*i);
464 shared_ptr<SubtitleString> string_j = dynamic_pointer_cast<SubtitleString> (*j);
465 shared_ptr<SubtitleImage> image_i = dynamic_pointer_cast<SubtitleImage> (*i);
466 shared_ptr<SubtitleImage> image_j = dynamic_pointer_cast<SubtitleImage> (*j);
468 if ((string_i && !string_j) || (image_i && !image_j)) {
469 note (DCP_ERROR, "subtitles differ");
473 if (string_i && *string_i != *string_j) {
474 note (DCP_ERROR, "subtitles differ");
478 if (image_i && *image_i != *image_j) {
479 note (DCP_ERROR, "subtitles differ");
490 struct SubtitleSorter
492 bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
493 if (a->in() != b->in()) {
494 return a->in() < b->in();
496 return a->v_position() < b->v_position();
501 SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
503 if (part->children.empty ()) {
507 /* Pull up from children */
508 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
513 /* Establish the common font features that each of part's children have;
514 these features go into part's font.
516 part->font = part->children.front()->font;
517 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
518 part->font.take_intersection (i->font);
521 /* Remove common values from part's children's fonts */
522 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
523 i->font.take_difference (part->font);
527 /* Merge adjacent children with the same font */
528 list<shared_ptr<order::Part> >::const_iterator i = part->children.begin();
529 list<shared_ptr<order::Part> > merged;
531 while (i != part->children.end()) {
533 if ((*i)->font.empty ()) {
534 merged.push_back (*i);
537 list<shared_ptr<order::Part> >::const_iterator j = i;
539 while (j != part->children.end() && (*i)->font == (*j)->font) {
542 if (std::distance (i, j) == 1) {
543 merged.push_back (*i);
546 shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
547 for (list<shared_ptr<order::Part> >::const_iterator k = i; k != j; ++k) {
549 group->children.push_back (*k);
551 merged.push_back (group);
557 part->children = merged;
560 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
561 * class because the differences between the two are fairly subtle.
564 SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, Standard standard) const
566 list<shared_ptr<Subtitle> > sorted = _subtitles;
567 sorted.sort (SubtitleSorter ());
569 /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
570 font information into the bottom level (String) objects.
573 shared_ptr<order::Part> root (new order::Part (shared_ptr<order::Part> ()));
574 shared_ptr<order::Subtitle> subtitle;
575 shared_ptr<order::Text> text;
579 Time last_fade_up_time;
580 Time last_fade_down_time;
582 float last_h_position;
584 float last_v_position;
585 Direction last_direction;
587 BOOST_FOREACH (shared_ptr<Subtitle> i, sorted) {
589 (last_in != i->in() ||
590 last_out != i->out() ||
591 last_fade_up_time != i->fade_up_time() ||
592 last_fade_down_time != i->fade_down_time())
595 subtitle.reset (new order::Subtitle (root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time()));
596 root->children.push_back (subtitle);
599 last_out = i->out ();
600 last_fade_up_time = i->fade_up_time ();
601 last_fade_down_time = i->fade_down_time ();
605 shared_ptr<SubtitleString> is = dynamic_pointer_cast<SubtitleString>(i);
608 last_h_align != is->h_align() ||
609 fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
610 last_v_align != is->v_align() ||
611 fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
612 last_direction != is->direction()
614 text.reset (new order::Text (subtitle, is->h_align(), is->h_position(), is->v_align(), is->v_position(), is->direction()));
615 subtitle->children.push_back (text);
617 last_h_align = is->h_align ();
618 last_h_position = is->h_position ();
619 last_v_align = is->v_align ();
620 last_v_position = is->v_position ();
621 last_direction = is->direction ();
624 text->children.push_back (shared_ptr<order::String> (new order::String (text, order::Font (is, standard), is->text())));
627 shared_ptr<SubtitleImage> ii = dynamic_pointer_cast<SubtitleImage>(i);
630 subtitle->children.push_back (
631 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()))
636 /* Pull font changes as high up the hierarchy as we can */
642 order::Context context;
643 context.time_code_rate = time_code_rate;
644 context.standard = standard;
645 context.spot_number = 1;
647 root->write_xml (xml_root, context);
651 SubtitleAsset::fonts_with_load_ids () const
653 map<string, Data> out;
654 BOOST_FOREACH (Font const & i, _fonts) {
655 out[i.load_id] = i.data;
660 /** Replace empty IDs in any <LoadFontId> and <Font> tags with
661 * a dummy string. Some systems give errors with empty font IDs
662 * (see DCP-o-matic bug #1689).
665 SubtitleAsset::fix_empty_font_ids ()
667 bool have_empty = false;
669 BOOST_FOREACH (shared_ptr<LoadFontNode> i, load_font_nodes()) {
673 ids.push_back (i->id);
681 string const empty_id = unique_string (ids, "font");
683 BOOST_FOREACH (shared_ptr<LoadFontNode> i, load_font_nodes()) {
689 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
690 shared_ptr<SubtitleString> j = dynamic_pointer_cast<SubtitleString> (i);
691 if (j && j->font() && j->font().get() == "") {
692 j->set_font (empty_id);