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.
35 /** @file src/subtitle_asset.cc
36 * @brief SubtitleAsset class
40 #include "raw_convert.h"
41 #include "compose.hpp"
42 #include "subtitle_asset.h"
43 #include "subtitle_asset_internal.h"
46 #include "subtitle_string.h"
47 #include "subtitle_image.h"
48 #include "dcp_assert.h"
49 #include "load_font_node.h"
50 #include "reel_asset.h"
51 #include <asdcp/AS_DCP.h>
52 #include <asdcp/KM_util.h>
53 #include <libxml++/nodes/element.h>
54 #include <boost/algorithm/string.hpp>
55 #include <boost/lexical_cast.hpp>
56 #include <boost/shared_array.hpp>
62 using std::dynamic_pointer_cast;
63 using std::make_shared;
66 using std::shared_ptr;
69 using boost::lexical_cast;
70 using boost::optional;
74 SubtitleAsset::SubtitleAsset ()
80 SubtitleAsset::SubtitleAsset (boost::filesystem::path file)
88 string_attribute (xmlpp::Element const * node, string name)
90 auto a = node->get_attribute (name);
92 throw XMLError (String::compose ("missing attribute %1", name));
94 return string (a->get_value ());
99 optional_string_attribute (xmlpp::Element const * node, string name)
101 auto a = node->get_attribute (name);
105 return string (a->get_value ());
110 optional_bool_attribute (xmlpp::Element const * node, string name)
112 auto s = optional_string_attribute (node, name);
117 return (s.get() == "1" || s.get() == "yes");
123 optional_number_attribute (xmlpp::Element const * node, string name)
125 auto s = optional_string_attribute (node, name);
127 return boost::optional<T> ();
130 std::string t = s.get ();
131 boost::erase_all (t, " ");
132 return raw_convert<T> (t);
136 SubtitleAsset::ParseState
137 SubtitleAsset::font_node_state (xmlpp::Element const * node, Standard standard) const
141 if (standard == Standard::INTEROP) {
142 ps.font_id = optional_string_attribute (node, "Id");
144 ps.font_id = optional_string_attribute (node, "ID");
146 ps.size = optional_number_attribute<int64_t> (node, "Size");
147 ps.aspect_adjust = optional_number_attribute<float> (node, "AspectAdjust");
148 ps.italic = optional_bool_attribute (node, "Italic");
149 ps.bold = optional_string_attribute(node, "Weight").get_value_or("normal") == "bold";
150 if (standard == Standard::INTEROP) {
151 ps.underline = optional_bool_attribute (node, "Underlined");
153 ps.underline = optional_bool_attribute (node, "Underline");
155 auto c = optional_string_attribute (node, "Color");
157 ps.colour = Colour (c.get ());
159 auto const e = optional_string_attribute (node, "Effect");
161 ps.effect = string_to_effect (e.get ());
163 c = optional_string_attribute (node, "EffectColor");
165 ps.effect_colour = Colour (c.get ());
172 SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, xmlpp::Element const * node) const
174 auto hp = optional_number_attribute<float> (node, "HPosition");
176 hp = optional_number_attribute<float> (node, "Hposition");
179 ps.h_position = hp.get () / 100;
182 auto ha = optional_string_attribute (node, "HAlign");
184 ha = optional_string_attribute (node, "Halign");
187 ps.h_align = string_to_halign (ha.get ());
190 auto vp = optional_number_attribute<float> (node, "VPosition");
192 vp = optional_number_attribute<float> (node, "Vposition");
195 ps.v_position = vp.get () / 100;
198 auto va = optional_string_attribute (node, "VAlign");
200 va = optional_string_attribute (node, "Valign");
203 ps.v_align = string_to_valign (va.get ());
206 auto zp = optional_number_attribute<float>(node, "Zposition");
208 ps.z_position = zp.get() / 100;
213 SubtitleAsset::ParseState
214 SubtitleAsset::text_node_state (xmlpp::Element const * node) const
218 position_align (ps, node);
220 auto d = optional_string_attribute (node, "Direction");
222 ps.direction = string_to_direction (d.get ());
225 ps.type = ParseState::Type::TEXT;
231 SubtitleAsset::ParseState
232 SubtitleAsset::image_node_state (xmlpp::Element const * node) const
236 position_align (ps, node);
238 ps.type = ParseState::Type::IMAGE;
244 SubtitleAsset::ParseState
245 SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional<int> tcr) const
248 ps.in = Time (string_attribute(node, "TimeIn"), tcr);
249 ps.out = Time (string_attribute(node, "TimeOut"), tcr);
250 ps.fade_up_time = fade_time (node, "FadeUpTime", tcr);
251 ps.fade_down_time = fade_time (node, "FadeDownTime", tcr);
257 SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional<int> tcr) const
259 auto const u = optional_string_attribute(node, name).get_value_or ("");
263 t = Time (0, 0, 0, 20, 250);
264 } else if (u.find (":") != string::npos) {
267 t = Time (0, 0, 0, lexical_cast<int> (u), tcr.get_value_or(250));
270 if (t > Time (0, 0, 8, 0, 250)) {
271 t = Time (0, 0, 8, 0, 250);
279 SubtitleAsset::parse_subtitles (xmlpp::Element const * node, vector<ParseState>& state, optional<int> tcr, Standard standard)
281 if (node->get_name() == "Font") {
282 state.push_back (font_node_state (node, standard));
283 } else if (node->get_name() == "Subtitle") {
284 state.push_back (subtitle_node_state (node, tcr));
285 } else if (node->get_name() == "Text") {
286 state.push_back (text_node_state (node));
287 } else if (node->get_name() == "SubtitleList") {
288 state.push_back (ParseState ());
289 } else if (node->get_name() == "Image") {
290 state.push_back (image_node_state (node));
292 throw XMLError ("unexpected node " + node->get_name());
295 float space_before = 0;
297 for (auto i: node->get_children()) {
298 auto const v = dynamic_cast<xmlpp::ContentNode const *>(i);
300 maybe_add_subtitle (v->get_content(), state, space_before, standard);
303 auto const e = dynamic_cast<xmlpp::Element const *>(i);
305 if (e->get_name() == "Space") {
306 if (node->get_name() != "Text") {
307 throw XMLError ("Space node found outside Text");
309 auto size = optional_string_attribute(e, "Size").get_value_or("0.5");
310 if (standard == dcp::Standard::INTEROP) {
311 boost::replace_all(size, "em", "");
313 space_before += raw_convert<float>(size);
315 parse_subtitles (e, state, tcr, standard);
325 SubtitleAsset::maybe_add_subtitle (string text, vector<ParseState> const & parse_state, float space_before, Standard standard)
327 auto wanted = [](ParseState const& ps) {
328 return ps.type && (ps.type.get() == ParseState::Type::TEXT || ps.type.get() == ParseState::Type::IMAGE);
331 if (find_if(parse_state.begin(), parse_state.end(), wanted) == parse_state.end()) {
336 for (auto const& i: parse_state) {
338 ps.font_id = i.font_id.get();
341 ps.size = i.size.get();
343 if (i.aspect_adjust) {
344 ps.aspect_adjust = i.aspect_adjust.get();
347 ps.italic = i.italic.get();
350 ps.bold = i.bold.get();
353 ps.underline = i.underline.get();
356 ps.colour = i.colour.get();
359 ps.effect = i.effect.get();
361 if (i.effect_colour) {
362 ps.effect_colour = i.effect_colour.get();
365 ps.h_position = i.h_position.get();
368 ps.h_align = i.h_align.get();
371 ps.v_position = i.v_position.get();
374 ps.v_align = i.v_align.get();
377 ps.z_position = i.z_position.get();
380 ps.direction = i.direction.get();
386 ps.out = i.out.get();
388 if (i.fade_up_time) {
389 ps.fade_up_time = i.fade_up_time.get();
391 if (i.fade_down_time) {
392 ps.fade_down_time = i.fade_down_time.get();
395 ps.type = i.type.get();
399 if (!ps.in || !ps.out) {
400 /* We're not in a <Subtitle> node; just ignore this content */
404 DCP_ASSERT (ps.type);
406 switch (ps.type.get()) {
407 case ParseState::Type::TEXT:
408 _subtitles.push_back (
409 make_shared<SubtitleString>(
411 ps.italic.get_value_or (false),
412 ps.bold.get_value_or (false),
413 ps.underline.get_value_or (false),
414 ps.colour.get_value_or (dcp::Colour (255, 255, 255)),
415 ps.size.get_value_or (42),
416 ps.aspect_adjust.get_value_or (1.0),
419 ps.h_position.get_value_or(0),
420 ps.h_align.get_value_or(HAlign::CENTER),
421 ps.v_position.get_value_or(0),
422 ps.v_align.get_value_or(VAlign::CENTER),
423 ps.z_position.get_value_or(0),
424 ps.direction.get_value_or (Direction::LTR),
426 ps.effect.get_value_or (Effect::NONE),
427 ps.effect_colour.get_value_or (dcp::Colour (0, 0, 0)),
428 ps.fade_up_time.get_value_or(Time()),
429 ps.fade_down_time.get_value_or(Time()),
434 case ParseState::Type::IMAGE:
437 case Standard::INTEROP:
438 if (text.size() >= 4) {
439 /* Remove file extension */
440 text = text.substr(0, text.size() - 4);
443 case Standard::SMPTE:
444 /* It looks like this urn:uuid: is required, but DoM wasn't expecting it (and not writing it)
445 * until around 2.15.140 so I guess either:
446 * a) it is not (always) used in the field, or
447 * b) nobody noticed / complained.
449 if (text.substr(0, 9) == "urn:uuid:") {
450 text = text.substr(9);
455 /* Add a subtitle with no image data and we'll fill that in later */
456 _subtitles.push_back (
457 make_shared<SubtitleImage>(
462 ps.h_position.get_value_or(0),
463 ps.h_align.get_value_or(HAlign::CENTER),
464 ps.v_position.get_value_or(0),
465 ps.v_align.get_value_or(VAlign::CENTER),
466 ps.z_position.get_value_or(0),
467 ps.fade_up_time.get_value_or(Time()),
468 ps.fade_down_time.get_value_or(Time())
477 vector<shared_ptr<const Subtitle>>
478 SubtitleAsset::subtitles () const
480 vector<shared_ptr<const Subtitle>> s;
481 for (auto i: _subtitles) {
488 vector<shared_ptr<const Subtitle>>
489 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
491 vector<shared_ptr<const Subtitle>> s;
492 for (auto i: _subtitles) {
493 if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
502 /* XXX: this needs a test */
503 vector<shared_ptr<const Subtitle>>
504 SubtitleAsset::subtitles_in_reel (shared_ptr<const dcp::ReelAsset> asset) const
506 auto frame_rate = asset->edit_rate().as_float();
507 auto start = dcp::Time(asset->entry_point().get_value_or(0), frame_rate, time_code_rate());
508 auto during = subtitles_during (start, start + dcp::Time(asset->intrinsic_duration(), frame_rate, time_code_rate()), false);
510 vector<shared_ptr<const dcp::Subtitle>> corrected;
511 for (auto i: during) {
512 auto c = make_shared<dcp::Subtitle>(*i);
513 c->set_in (c->in() - start);
514 c->set_out (c->out() - start);
515 corrected.push_back (c);
523 SubtitleAsset::add (shared_ptr<Subtitle> s)
525 _subtitles.push_back (s);
530 SubtitleAsset::latest_subtitle_out () const
533 for (auto i: _subtitles) {
544 SubtitleAsset::equals(shared_ptr<const Asset> other_asset, EqualityOptions const& options, NoteHandler note) const
546 if (!Asset::equals (other_asset, options, note)) {
550 auto other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
555 if (_subtitles.size() != other->_subtitles.size()) {
556 note (NoteType::ERROR, String::compose("different number of subtitles: %1 vs %2", _subtitles.size(), other->_subtitles.size()));
560 auto i = _subtitles.begin();
561 auto j = other->_subtitles.begin();
563 while (i != _subtitles.end()) {
564 auto string_i = dynamic_pointer_cast<SubtitleString> (*i);
565 auto string_j = dynamic_pointer_cast<SubtitleString> (*j);
566 auto image_i = dynamic_pointer_cast<SubtitleImage> (*i);
567 auto image_j = dynamic_pointer_cast<SubtitleImage> (*j);
569 if ((string_i && !string_j) || (image_i && !image_j)) {
570 note (NoteType::ERROR, "subtitles differ: string vs. image");
574 if (string_i && !string_i->equals(string_j, options, note)) {
578 if (image_i && !image_i->equals(image_j, options, note)) {
590 struct SubtitleSorter
592 bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
593 if (a->in() != b->in()) {
594 return a->in() < b->in();
596 if (a->v_align() == VAlign::BOTTOM) {
597 return a->v_position() > b->v_position();
599 return a->v_position() < b->v_position();
605 SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
607 if (part->children.empty ()) {
611 /* Pull up from children */
612 for (auto i: part->children) {
617 /* Establish the common font features that each of part's children have;
618 these features go into part's font.
620 part->font = part->children.front()->font;
621 for (auto i: part->children) {
622 part->font.take_intersection (i->font);
625 /* Remove common values from part's children's fonts */
626 for (auto i: part->children) {
627 i->font.take_difference (part->font);
631 /* Merge adjacent children with the same font */
632 auto i = part->children.begin();
633 vector<shared_ptr<order::Part>> merged;
635 while (i != part->children.end()) {
637 if ((*i)->font.empty ()) {
638 merged.push_back (*i);
643 while (j != part->children.end() && (*i)->font == (*j)->font) {
646 if (std::distance (i, j) == 1) {
647 merged.push_back (*i);
650 shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
651 for (auto k = i; k != j; ++k) {
653 group->children.push_back (*k);
655 merged.push_back (group);
661 part->children = merged;
665 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
666 * class because the differences between the two are fairly subtle.
669 SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, Standard standard) const
671 auto sorted = _subtitles;
672 std::stable_sort(sorted.begin(), sorted.end(), SubtitleSorter());
674 /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
675 font information into the bottom level (String) objects.
678 auto root = make_shared<order::Part>(shared_ptr<order::Part>());
679 shared_ptr<order::Subtitle> subtitle;
680 shared_ptr<order::Text> text;
684 Time last_fade_up_time;
685 Time last_fade_down_time;
687 float last_h_position;
689 float last_v_position;
690 float last_z_position;
691 Direction last_direction;
693 for (auto i: sorted) {
695 (last_in != i->in() ||
696 last_out != i->out() ||
697 last_fade_up_time != i->fade_up_time() ||
698 last_fade_down_time != i->fade_down_time())
701 subtitle = make_shared<order::Subtitle>(root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time());
702 root->children.push_back (subtitle);
705 last_out = i->out ();
706 last_fade_up_time = i->fade_up_time ();
707 last_fade_down_time = i->fade_down_time ();
711 auto is = dynamic_pointer_cast<SubtitleString>(i);
714 last_h_align != is->h_align() ||
715 fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
716 last_v_align != is->v_align() ||
717 fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
718 fabs(last_z_position - is->z_position()) > ALIGN_EPSILON ||
719 last_direction != is->direction()
721 text = make_shared<order::Text>(subtitle, is->h_align(), is->h_position(), is->v_align(), is->v_position(), is->z_position(), is->direction());
722 subtitle->children.push_back (text);
724 last_h_align = is->h_align ();
725 last_h_position = is->h_position ();
726 last_v_align = is->v_align ();
727 last_v_position = is->v_position ();
728 last_z_position = is->z_position();
729 last_direction = is->direction ();
732 text->children.push_back (make_shared<order::String>(text, order::Font (is, standard), is->text(), is->space_before()));
735 auto ii = dynamic_pointer_cast<SubtitleImage>(i);
738 subtitle->children.push_back (
739 make_shared<order::Image>(subtitle, ii->id(), ii->png_image(), ii->h_align(), ii->h_position(), ii->v_align(), ii->v_position(), ii->z_position())
744 /* Pull font changes as high up the hierarchy as we can */
750 order::Context context;
751 context.time_code_rate = time_code_rate;
752 context.standard = standard;
753 context.spot_number = 1;
755 root->write_xml (xml_root, context);
759 map<string, ArrayData>
760 SubtitleAsset::font_data () const
762 map<string, ArrayData> out;
763 for (auto const& i: _fonts) {
764 out[i.load_id] = i.data;
770 map<string, boost::filesystem::path>
771 SubtitleAsset::font_filenames () const
773 map<string, boost::filesystem::path> out;
774 for (auto const& i: _fonts) {
776 out[i.load_id] = *i.file;
783 /** Replace empty IDs in any <LoadFontId> and <Font> tags with
784 * a dummy string. Some systems give errors with empty font IDs
785 * (see DCP-o-matic bug #1689).
788 SubtitleAsset::fix_empty_font_ids ()
790 bool have_empty = false;
792 for (auto i: load_font_nodes()) {
796 ids.push_back (i->id);
804 string const empty_id = unique_string (ids, "font");
806 for (auto i: load_font_nodes()) {
812 for (auto i: _subtitles) {
813 auto j = dynamic_pointer_cast<SubtitleString> (i);
814 if (j && j->font() && j->font().get() == "") {
815 j->set_font (empty_id);
827 int disable_formatting;
835 format_xml_node (xmlpp::Node const* node, State& state)
837 if (auto text_node = dynamic_cast<const xmlpp::TextNode*>(node)) {
838 string content = text_node->get_content();
839 boost::replace_all(content, "&", "&");
840 boost::replace_all(content, "<", "<");
841 boost::replace_all(content, ">", ">");
842 state.xml += content;
843 } else if (auto element = dynamic_cast<const xmlpp::Element*>(node)) {
846 auto children = element->get_children();
847 auto const should_disable_formatting =
849 children.begin(), children.end(),
850 [](xmlpp::Node const* node) { return static_cast<bool>(dynamic_cast<const xmlpp::ContentNode*>(node)); }
851 ) || element->get_name() == "Text";
853 if (!state.disable_formatting) {
854 state.xml += "\n" + string(state.indent * 2, ' ');
857 state.xml += "<" + element->get_name();
859 for (auto attribute: element->get_attributes()) {
860 state.xml += String::compose(" %1=\"%2\"", attribute->get_name().raw(), attribute->get_value().raw());
863 if (children.empty()) {
868 if (should_disable_formatting) {
869 ++state.disable_formatting;
872 for (auto child: children) {
873 format_xml_node(child, state);
876 if (!state.disable_formatting) {
877 state.xml += "\n" + string(state.indent * 2, ' ');
880 state.xml += String::compose("</%1>", element->get_name().raw());
882 if (should_disable_formatting) {
883 --state.disable_formatting;
892 /** Format XML much as write_to_string_formatted() would do, except without adding any white space
893 * to <Text> nodes. This is an attempt to avoid changing what is actually displayed as subtitles
894 * while also formatting the XML in such a way as to avoid DoM bug 2205.
896 * xml_namespace is an optional namespace for the root node; it would be nicer to set this up with
897 * set_namespace_declaration in the caller and then to extract it here but I couldn't find a way
898 * to get all namespaces with the libxml++ API.
901 SubtitleAsset::format_xml(xmlpp::Document const& document, optional<pair<string, string>> xml_namespace)
903 auto root = document.get_root_node();
906 state.xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<" + root->get_name();
909 if (xml_namespace->first.empty()) {
910 state.xml += String::compose(" xmlns=\"%1\"", xml_namespace->second);
912 state.xml += String::compose(" xmlns:%1=\"%2\"", xml_namespace->first, xml_namespace->second);
916 for (auto attribute: root->get_attributes()) {
917 state.xml += String::compose(" %1=\"%2\"", attribute->get_name().raw(), attribute->get_value().raw());
922 for (auto child: document.get_root_node()->get_children()) {
923 format_xml_node(child, state);
926 state.xml += String::compose("\n</%1>\n", root->get_name().raw());
933 SubtitleAsset::ensure_font(string load_id, dcp::ArrayData data)
935 if (std::find_if(_fonts.begin(), _fonts.end(), [load_id](Font const& font) { return font.load_id == load_id; }) == _fonts.end()) {
936 add_font(load_id, data);