2 Copyright (C) 2012-2021 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 "reel_asset.h"
45 #include <asdcp/AS_DCP.h>
46 #include <asdcp/KM_util.h>
47 #include <libxml++/nodes/element.h>
48 #include <boost/algorithm/string.hpp>
49 #include <boost/lexical_cast.hpp>
50 #include <boost/shared_array.hpp>
51 #include <boost/foreach.hpp>
53 using std::dynamic_pointer_cast;
58 using std::shared_ptr;
60 using std::make_shared;
61 using boost::shared_array;
62 using boost::optional;
63 using boost::lexical_cast;
66 SubtitleAsset::SubtitleAsset ()
71 SubtitleAsset::SubtitleAsset (boost::filesystem::path file)
78 string_attribute (xmlpp::Element const * node, string name)
80 xmlpp::Attribute* a = node->get_attribute (name);
82 throw XMLError (String::compose ("missing attribute %1", name));
84 return string (a->get_value ());
88 optional_string_attribute (xmlpp::Element const * node, string name)
90 xmlpp::Attribute* a = node->get_attribute (name);
92 return optional<string>();
94 return string (a->get_value ());
98 optional_bool_attribute (xmlpp::Element const * node, string name)
100 optional<string> s = optional_string_attribute (node, name);
102 return optional<bool> ();
105 return (s.get() == "1" || s.get() == "yes");
110 optional_number_attribute (xmlpp::Element const * node, string name)
112 boost::optional<std::string> s = optional_string_attribute (node, name);
114 return boost::optional<T> ();
117 std::string t = s.get ();
118 boost::erase_all (t, " ");
119 return raw_convert<T> (t);
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 ());
158 SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, 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 ());
194 SubtitleAsset::ParseState
195 SubtitleAsset::text_node_state (xmlpp::Element const * node) const
199 position_align (ps, node);
201 optional<string> d = optional_string_attribute (node, "Direction");
203 ps.direction = string_to_direction (d.get ());
206 ps.type = ParseState::TEXT;
211 SubtitleAsset::ParseState
212 SubtitleAsset::image_node_state (xmlpp::Element const * node) const
216 position_align (ps, node);
218 ps.type = ParseState::IMAGE;
223 SubtitleAsset::ParseState
224 SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional<int> tcr) const
227 ps.in = Time (string_attribute(node, "TimeIn"), tcr);
228 ps.out = Time (string_attribute(node, "TimeOut"), tcr);
229 ps.fade_up_time = fade_time (node, "FadeUpTime", tcr);
230 ps.fade_down_time = fade_time (node, "FadeDownTime", tcr);
235 SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional<int> tcr) const
237 string const u = optional_string_attribute(node, name).get_value_or ("");
241 t = Time (0, 0, 0, 20, 250);
242 } else if (u.find (":") != string::npos) {
245 t = Time (0, 0, 0, lexical_cast<int> (u), tcr.get_value_or(250));
248 if (t > Time (0, 0, 8, 0, 250)) {
249 t = Time (0, 0, 8, 0, 250);
256 SubtitleAsset::parse_subtitles (xmlpp::Element const * node, vector<ParseState>& state, optional<int> tcr, Standard standard)
258 if (node->get_name() == "Font") {
259 state.push_back (font_node_state (node, standard));
260 } else if (node->get_name() == "Subtitle") {
261 state.push_back (subtitle_node_state (node, tcr));
262 } else if (node->get_name() == "Text") {
263 state.push_back (text_node_state (node));
264 } else if (node->get_name() == "SubtitleList") {
265 state.push_back (ParseState ());
266 } else if (node->get_name() == "Image") {
267 state.push_back (image_node_state (node));
269 throw XMLError ("unexpected node " + node->get_name());
272 xmlpp::Node::NodeList c = node->get_children ();
273 for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
274 xmlpp::ContentNode const * v = dynamic_cast<xmlpp::ContentNode const *> (*i);
276 maybe_add_subtitle (v->get_content(), state, standard);
278 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
280 parse_subtitles (e, state, tcr, standard);
288 SubtitleAsset::maybe_add_subtitle (string text, vector<ParseState> const & parse_state, Standard standard)
290 if (empty_or_white_space (text)) {
295 BOOST_FOREACH (ParseState const & i, parse_state) {
297 ps.font_id = i.font_id.get();
300 ps.size = i.size.get();
302 if (i.aspect_adjust) {
303 ps.aspect_adjust = i.aspect_adjust.get();
306 ps.italic = i.italic.get();
309 ps.bold = i.bold.get();
312 ps.underline = i.underline.get();
315 ps.colour = i.colour.get();
318 ps.effect = i.effect.get();
320 if (i.effect_colour) {
321 ps.effect_colour = i.effect_colour.get();
324 ps.h_position = i.h_position.get();
327 ps.h_align = i.h_align.get();
330 ps.v_position = i.v_position.get();
333 ps.v_align = i.v_align.get();
336 ps.direction = i.direction.get();
342 ps.out = i.out.get();
344 if (i.fade_up_time) {
345 ps.fade_up_time = i.fade_up_time.get();
347 if (i.fade_down_time) {
348 ps.fade_down_time = i.fade_down_time.get();
351 ps.type = i.type.get();
355 if (!ps.in || !ps.out) {
356 /* We're not in a <Subtitle> node; just ignore this content */
360 DCP_ASSERT (ps.type);
362 switch (ps.type.get()) {
363 case ParseState::TEXT:
364 _subtitles.push_back (
365 shared_ptr<Subtitle> (
368 ps.italic.get_value_or (false),
369 ps.bold.get_value_or (false),
370 ps.underline.get_value_or (false),
371 ps.colour.get_value_or (dcp::Colour (255, 255, 255)),
372 ps.size.get_value_or (42),
373 ps.aspect_adjust.get_value_or (1.0),
376 ps.h_position.get_value_or(0),
377 ps.h_align.get_value_or(HALIGN_CENTER),
378 ps.v_position.get_value_or(0),
379 ps.v_align.get_value_or(VALIGN_CENTER),
380 ps.direction.get_value_or (DIRECTION_LTR),
382 ps.effect.get_value_or (NONE),
383 ps.effect_colour.get_value_or (dcp::Colour (0, 0, 0)),
384 ps.fade_up_time.get_value_or(Time()),
385 ps.fade_down_time.get_value_or(Time())
390 case ParseState::IMAGE:
391 /* Add a subtitle with no image data and we'll fill that in later */
392 _subtitles.push_back (
393 shared_ptr<Subtitle> (
396 standard == INTEROP ? text.substr(0, text.size() - 4) : text,
399 ps.h_position.get_value_or(0),
400 ps.h_align.get_value_or(HALIGN_CENTER),
401 ps.v_position.get_value_or(0),
402 ps.v_align.get_value_or(VALIGN_CENTER),
403 ps.fade_up_time.get_value_or(Time()),
404 ps.fade_down_time.get_value_or(Time())
413 vector<shared_ptr<const Subtitle>>
414 SubtitleAsset::subtitles () const
416 vector<shared_ptr<const Subtitle>> s;
417 for (auto i: _subtitles) {
424 vector<shared_ptr<const Subtitle>>
425 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
427 vector<shared_ptr<const Subtitle>> s;
428 for (auto i: _subtitles) {
429 if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
438 /* XXX: this needs a test */
439 vector<shared_ptr<const Subtitle>>
440 SubtitleAsset::subtitles_in_reel (shared_ptr<const dcp::ReelAsset> asset) const
442 auto frame_rate = asset->edit_rate().as_float();
443 auto start = dcp::Time(asset->entry_point().get_value_or(0), frame_rate, time_code_rate());
444 auto during = subtitles_during (start, start + dcp::Time(asset->intrinsic_duration(), frame_rate, time_code_rate()), false);
446 vector<shared_ptr<const dcp::Subtitle>> corrected;
447 for (auto i: during) {
448 auto c = make_shared<dcp::Subtitle>(*i);
449 c->set_in (c->in() - start);
450 c->set_out (c->out() - start);
451 corrected.push_back (c);
459 SubtitleAsset::add (shared_ptr<Subtitle> s)
461 _subtitles.push_back (s);
465 SubtitleAsset::latest_subtitle_out () const
468 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
478 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
480 if (!Asset::equals (other_asset, options, note)) {
484 shared_ptr<const SubtitleAsset> other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
489 if (_subtitles.size() != other->_subtitles.size()) {
490 note (DCP_ERROR, String::compose("different number of subtitles: %1 vs %2", _subtitles.size(), other->_subtitles.size()));
494 auto i = _subtitles.begin();
495 auto j = other->_subtitles.begin();
497 while (i != _subtitles.end()) {
498 shared_ptr<SubtitleString> string_i = dynamic_pointer_cast<SubtitleString> (*i);
499 shared_ptr<SubtitleString> string_j = dynamic_pointer_cast<SubtitleString> (*j);
500 shared_ptr<SubtitleImage> image_i = dynamic_pointer_cast<SubtitleImage> (*i);
501 shared_ptr<SubtitleImage> image_j = dynamic_pointer_cast<SubtitleImage> (*j);
503 if ((string_i && !string_j) || (image_i && !image_j)) {
504 note (DCP_ERROR, "subtitles differ: string vs. image");
508 if (string_i && *string_i != *string_j) {
509 note (DCP_ERROR, String::compose("subtitles differ in text or metadata: %1 vs %2", string_i->text(), string_j->text()));
513 if (image_i && !image_i->equals(image_j, options, note)) {
524 struct SubtitleSorter
526 bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
527 if (a->in() != b->in()) {
528 return a->in() < b->in();
530 return a->v_position() < b->v_position();
535 SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
537 if (part->children.empty ()) {
541 /* Pull up from children */
542 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
547 /* Establish the common font features that each of part's children have;
548 these features go into part's font.
550 part->font = part->children.front()->font;
551 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
552 part->font.take_intersection (i->font);
555 /* Remove common values from part's children's fonts */
556 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
557 i->font.take_difference (part->font);
561 /* Merge adjacent children with the same font */
562 auto i = part->children.begin();
563 vector<shared_ptr<order::Part>> merged;
565 while (i != part->children.end()) {
567 if ((*i)->font.empty ()) {
568 merged.push_back (*i);
573 while (j != part->children.end() && (*i)->font == (*j)->font) {
576 if (std::distance (i, j) == 1) {
577 merged.push_back (*i);
580 shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
581 for (auto k = i; k != j; ++k) {
583 group->children.push_back (*k);
585 merged.push_back (group);
591 part->children = merged;
594 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
595 * class because the differences between the two are fairly subtle.
598 SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, Standard standard) const
600 vector<shared_ptr<Subtitle> > sorted = _subtitles;
601 std::stable_sort(sorted.begin(), sorted.end(), SubtitleSorter());
603 /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
604 font information into the bottom level (String) objects.
607 shared_ptr<order::Part> root (new order::Part (shared_ptr<order::Part> ()));
608 shared_ptr<order::Subtitle> subtitle;
609 shared_ptr<order::Text> text;
613 Time last_fade_up_time;
614 Time last_fade_down_time;
616 float last_h_position;
618 float last_v_position;
619 Direction last_direction;
621 BOOST_FOREACH (shared_ptr<Subtitle> i, sorted) {
623 (last_in != i->in() ||
624 last_out != i->out() ||
625 last_fade_up_time != i->fade_up_time() ||
626 last_fade_down_time != i->fade_down_time())
629 subtitle.reset (new order::Subtitle (root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time()));
630 root->children.push_back (subtitle);
633 last_out = i->out ();
634 last_fade_up_time = i->fade_up_time ();
635 last_fade_down_time = i->fade_down_time ();
639 shared_ptr<SubtitleString> is = dynamic_pointer_cast<SubtitleString>(i);
642 last_h_align != is->h_align() ||
643 fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
644 last_v_align != is->v_align() ||
645 fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
646 last_direction != is->direction()
648 text.reset (new order::Text (subtitle, is->h_align(), is->h_position(), is->v_align(), is->v_position(), is->direction()));
649 subtitle->children.push_back (text);
651 last_h_align = is->h_align ();
652 last_h_position = is->h_position ();
653 last_v_align = is->v_align ();
654 last_v_position = is->v_position ();
655 last_direction = is->direction ();
658 text->children.push_back (shared_ptr<order::String> (new order::String (text, order::Font (is, standard), is->text())));
661 shared_ptr<SubtitleImage> ii = dynamic_pointer_cast<SubtitleImage>(i);
664 subtitle->children.push_back (
665 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()))
670 /* Pull font changes as high up the hierarchy as we can */
676 order::Context context;
677 context.time_code_rate = time_code_rate;
678 context.standard = standard;
679 context.spot_number = 1;
681 root->write_xml (xml_root, context);
684 map<string, ArrayData>
685 SubtitleAsset::font_data () const
687 map<string, ArrayData> out;
688 BOOST_FOREACH (Font const & i, _fonts) {
689 out[i.load_id] = i.data;
695 map<string, boost::filesystem::path>
696 SubtitleAsset::font_filenames () const
698 map<string, boost::filesystem::path> out;
699 BOOST_FOREACH (Font const& i, _fonts) {
701 out[i.load_id] = *i.file;
708 /** Replace empty IDs in any <LoadFontId> and <Font> tags with
709 * a dummy string. Some systems give errors with empty font IDs
710 * (see DCP-o-matic bug #1689).
713 SubtitleAsset::fix_empty_font_ids ()
715 bool have_empty = false;
717 BOOST_FOREACH (shared_ptr<LoadFontNode> i, load_font_nodes()) {
721 ids.push_back (i->id);
729 string const empty_id = unique_string (ids, "font");
731 BOOST_FOREACH (shared_ptr<LoadFontNode> i, load_font_nodes()) {
737 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
738 shared_ptr<SubtitleString> j = dynamic_pointer_cast<SubtitleString> (i);
739 if (j && j->font() && j->font().get() == "") {
740 j->set_font (empty_id);