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 "compose.hpp"
41 #include "dcp_assert.h"
42 #include "load_font_node.h"
43 #include "raw_convert.h"
44 #include "reel_asset.h"
45 #include "subtitle_asset.h"
46 #include "subtitle_asset_internal.h"
47 #include "subtitle_image.h"
48 #include "subtitle_string.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 /* Collect <Ruby>s first */
298 auto get_text_content = [](xmlpp::Element const* element) {
300 for (auto child: element->get_children()) {
301 auto content = dynamic_cast<xmlpp::ContentNode const*>(child);
303 all_content += content->get_content();
310 for (auto child: node->get_children()) {
311 auto element = dynamic_cast<xmlpp::Element const*>(child);
312 if (element && element->get_name() == "Ruby") {
313 optional<string> base;
314 optional<string> annotation;
315 optional<float> size;
316 optional<RubyPosition> position;
317 optional<float> offset;
318 optional<float> spacing;
319 optional<float> aspect_adjust;
320 for (auto ruby_child: element->get_children()) {
321 if (auto ruby_element = dynamic_cast<xmlpp::Element const*>(ruby_child)) {
322 if (ruby_element->get_name() == "Rb") {
323 base = get_text_content(ruby_element);
324 } else if (ruby_element->get_name() == "Rt") {
325 annotation = get_text_content(ruby_element);
326 size = optional_number_attribute<float>(ruby_element, "Size");
327 if (auto position_string = optional_string_attribute(ruby_element, "Position")) {
328 if (*position_string == "before") {
329 position = RubyPosition::BEFORE;
330 } else if (*position_string == "after") {
331 position = RubyPosition::AFTER;
336 offset = optional_number_attribute<float>(ruby_element, "Offset");
337 spacing = optional_number_attribute<float>(ruby_element, "Spacing");
338 aspect_adjust = optional_number_attribute<float>(ruby_element, "AspectAdjust");
343 DCP_ASSERT(annotation);
344 auto ruby = Ruby{*base, *annotation};
349 ruby.position = *position;
352 ruby.offset = *offset;
355 ruby.spacing = *spacing;
358 ruby.aspect_adjust = *aspect_adjust;
360 rubies.push_back(ruby);
364 for (auto i: node->get_children()) {
366 /* Handle actual content e.g. text */
367 auto const v = dynamic_cast<xmlpp::ContentNode const *>(i);
369 maybe_add_subtitle (v->get_content(), state, space_before, standard, rubies);
373 /* Handle other nodes */
374 auto const e = dynamic_cast<xmlpp::Element const *>(i);
376 if (e->get_name() == "Space") {
377 if (node->get_name() != "Text") {
378 throw XMLError ("Space node found outside Text");
380 auto size = optional_string_attribute(e, "Size").get_value_or("0.5");
381 if (standard == dcp::Standard::INTEROP) {
382 boost::replace_all(size, "em", "");
384 space_before += raw_convert<float>(size);
385 } else if (e->get_name() != "Ruby") {
386 parse_subtitles (e, state, tcr, standard);
396 SubtitleAsset::maybe_add_subtitle(
398 vector<ParseState> const & parse_state,
401 vector<Ruby> const& rubies
404 auto wanted = [](ParseState const& ps) {
405 return ps.type && (ps.type.get() == ParseState::Type::TEXT || ps.type.get() == ParseState::Type::IMAGE);
408 if (find_if(parse_state.begin(), parse_state.end(), wanted) == parse_state.end()) {
413 for (auto const& i: parse_state) {
415 ps.font_id = i.font_id.get();
418 ps.size = i.size.get();
420 if (i.aspect_adjust) {
421 ps.aspect_adjust = i.aspect_adjust.get();
424 ps.italic = i.italic.get();
427 ps.bold = i.bold.get();
430 ps.underline = i.underline.get();
433 ps.colour = i.colour.get();
436 ps.effect = i.effect.get();
438 if (i.effect_colour) {
439 ps.effect_colour = i.effect_colour.get();
442 ps.h_position = i.h_position.get();
445 ps.h_align = i.h_align.get();
448 ps.v_position = i.v_position.get();
451 ps.v_align = i.v_align.get();
454 ps.z_position = i.z_position.get();
457 ps.direction = i.direction.get();
463 ps.out = i.out.get();
465 if (i.fade_up_time) {
466 ps.fade_up_time = i.fade_up_time.get();
468 if (i.fade_down_time) {
469 ps.fade_down_time = i.fade_down_time.get();
472 ps.type = i.type.get();
476 if (!ps.in || !ps.out) {
477 /* We're not in a <Subtitle> node; just ignore this content */
481 DCP_ASSERT (ps.type);
483 switch (ps.type.get()) {
484 case ParseState::Type::TEXT:
485 _subtitles.push_back (
486 make_shared<SubtitleString>(
488 ps.italic.get_value_or (false),
489 ps.bold.get_value_or (false),
490 ps.underline.get_value_or (false),
491 ps.colour.get_value_or (dcp::Colour (255, 255, 255)),
492 ps.size.get_value_or (42),
493 ps.aspect_adjust.get_value_or (1.0),
496 ps.h_position.get_value_or(0),
497 ps.h_align.get_value_or(HAlign::CENTER),
498 ps.v_position.get_value_or(0),
499 ps.v_align.get_value_or(VAlign::CENTER),
500 ps.z_position.get_value_or(0),
501 ps.direction.get_value_or (Direction::LTR),
503 ps.effect.get_value_or (Effect::NONE),
504 ps.effect_colour.get_value_or (dcp::Colour (0, 0, 0)),
505 ps.fade_up_time.get_value_or(Time()),
506 ps.fade_down_time.get_value_or(Time()),
512 case ParseState::Type::IMAGE:
515 case Standard::INTEROP:
516 if (text.size() >= 4) {
517 /* Remove file extension */
518 text = text.substr(0, text.size() - 4);
521 case Standard::SMPTE:
522 /* It looks like this urn:uuid: is required, but DoM wasn't expecting it (and not writing it)
523 * until around 2.15.140 so I guess either:
524 * a) it is not (always) used in the field, or
525 * b) nobody noticed / complained.
527 if (text.substr(0, 9) == "urn:uuid:") {
528 text = text.substr(9);
533 /* Add a subtitle with no image data and we'll fill that in later */
534 _subtitles.push_back (
535 make_shared<SubtitleImage>(
540 ps.h_position.get_value_or(0),
541 ps.h_align.get_value_or(HAlign::CENTER),
542 ps.v_position.get_value_or(0),
543 ps.v_align.get_value_or(VAlign::CENTER),
544 ps.z_position.get_value_or(0),
545 ps.fade_up_time.get_value_or(Time()),
546 ps.fade_down_time.get_value_or(Time())
555 vector<shared_ptr<const Subtitle>>
556 SubtitleAsset::subtitles () const
558 vector<shared_ptr<const Subtitle>> s;
559 for (auto i: _subtitles) {
566 vector<shared_ptr<const Subtitle>>
567 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
569 vector<shared_ptr<const Subtitle>> s;
570 for (auto i: _subtitles) {
571 if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
581 SubtitleAsset::add (shared_ptr<Subtitle> s)
583 _subtitles.push_back (s);
588 SubtitleAsset::latest_subtitle_out () const
591 for (auto i: _subtitles) {
602 SubtitleAsset::equals(shared_ptr<const Asset> other_asset, EqualityOptions const& options, NoteHandler note) const
604 if (!Asset::equals (other_asset, options, note)) {
608 auto other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
613 if (_subtitles.size() != other->_subtitles.size()) {
614 note (NoteType::ERROR, String::compose("different number of subtitles: %1 vs %2", _subtitles.size(), other->_subtitles.size()));
618 auto i = _subtitles.begin();
619 auto j = other->_subtitles.begin();
621 while (i != _subtitles.end()) {
622 auto string_i = dynamic_pointer_cast<SubtitleString> (*i);
623 auto string_j = dynamic_pointer_cast<SubtitleString> (*j);
624 auto image_i = dynamic_pointer_cast<SubtitleImage> (*i);
625 auto image_j = dynamic_pointer_cast<SubtitleImage> (*j);
627 if ((string_i && !string_j) || (image_i && !image_j)) {
628 note (NoteType::ERROR, "subtitles differ: string vs. image");
632 if (string_i && !string_i->equals(string_j, options, note)) {
636 if (image_i && !image_i->equals(image_j, options, note)) {
648 struct SubtitleSorter
650 bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
651 if (a->in() != b->in()) {
652 return a->in() < b->in();
654 if (a->v_align() == VAlign::BOTTOM) {
655 return a->v_position() > b->v_position();
657 return a->v_position() < b->v_position();
663 SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
665 if (part->children.empty ()) {
669 /* Pull up from children */
670 for (auto i: part->children) {
675 /* Establish the common font features that each of part's children have;
676 these features go into part's font.
678 part->font = part->children.front()->font;
679 for (auto i: part->children) {
680 part->font.take_intersection (i->font);
683 /* Remove common values from part's children's fonts */
684 for (auto i: part->children) {
685 i->font.take_difference (part->font);
689 /* Merge adjacent children with the same font */
690 auto i = part->children.begin();
691 vector<shared_ptr<order::Part>> merged;
693 while (i != part->children.end()) {
695 if ((*i)->font.empty ()) {
696 merged.push_back (*i);
701 while (j != part->children.end() && (*i)->font == (*j)->font) {
704 if (std::distance (i, j) == 1) {
705 merged.push_back (*i);
708 shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
709 for (auto k = i; k != j; ++k) {
711 group->children.push_back (*k);
713 merged.push_back (group);
719 part->children = merged;
723 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
724 * class because the differences between the two are fairly subtle.
727 SubtitleAsset::subtitles_as_xml(xmlpp::Element* xml_root, int time_code_rate, Standard standard, SubtitleOptimisation optimisation) const
729 auto sorted = _subtitles;
730 std::stable_sort(sorted.begin(), sorted.end(), SubtitleSorter());
732 /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
733 font information into the bottom level (String) objects.
736 auto root = make_shared<order::Part>(shared_ptr<order::Part>());
737 shared_ptr<order::Subtitle> subtitle;
738 shared_ptr<order::Text> text;
742 Time last_fade_up_time;
743 Time last_fade_down_time;
745 float last_h_position;
747 float last_v_position;
748 float last_z_position;
749 Direction last_direction;
751 for (auto i: sorted) {
753 (last_in != i->in() ||
754 last_out != i->out() ||
755 last_fade_up_time != i->fade_up_time() ||
756 last_fade_down_time != i->fade_down_time())
759 subtitle = make_shared<order::Subtitle>(root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time());
760 root->children.push_back (subtitle);
763 last_out = i->out ();
764 last_fade_up_time = i->fade_up_time ();
765 last_fade_down_time = i->fade_down_time ();
769 auto is = dynamic_pointer_cast<SubtitleString>(i);
772 last_h_align != is->h_align() ||
773 fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
774 last_v_align != is->v_align() ||
775 fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
776 fabs(last_z_position - is->z_position()) > ALIGN_EPSILON ||
777 last_direction != is->direction()
779 text = make_shared<order::Text>(
789 subtitle->children.push_back (text);
791 last_h_align = is->h_align ();
792 last_h_position = is->h_position ();
793 last_v_align = is->v_align ();
794 last_v_position = is->v_position ();
795 last_z_position = is->z_position();
796 last_direction = is->direction ();
799 text->children.push_back (make_shared<order::String>(text, order::Font (is, standard), is->text(), is->space_before()));
802 auto ii = dynamic_pointer_cast<SubtitleImage>(i);
805 subtitle->children.push_back (
806 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())
811 /* Pull font changes as high up the hierarchy as we can */
817 order::Context context(time_code_rate, standard, 1, optimisation);
819 root->write_xml (xml_root, context);
823 map<string, ArrayData>
824 SubtitleAsset::font_data () const
826 map<string, ArrayData> out;
827 for (auto const& i: _fonts) {
828 out[i.load_id] = i.data;
834 map<string, boost::filesystem::path>
835 SubtitleAsset::font_filenames () const
837 map<string, boost::filesystem::path> out;
838 for (auto const& i: _fonts) {
840 out[i.load_id] = *i.file;
847 /** Replace empty IDs in any <LoadFontId> and <Font> tags with
848 * a dummy string. Some systems give errors with empty font IDs
849 * (see DCP-o-matic bug #1689).
852 SubtitleAsset::fix_empty_font_ids ()
854 bool have_empty = false;
856 for (auto i: load_font_nodes()) {
860 ids.push_back (i->id);
868 string const empty_id = unique_string (ids, "font");
870 for (auto i: load_font_nodes()) {
876 for (auto i: _subtitles) {
877 auto j = dynamic_pointer_cast<SubtitleString> (i);
878 if (j && j->font() && j->font().get() == "") {
879 j->set_font (empty_id);
891 int disable_formatting;
899 format_xml_node (xmlpp::Node const* node, State& state)
901 if (auto text_node = dynamic_cast<const xmlpp::TextNode*>(node)) {
902 string content = text_node->get_content();
903 boost::replace_all(content, "&", "&");
904 boost::replace_all(content, "<", "<");
905 boost::replace_all(content, ">", ">");
906 state.xml += content;
907 } else if (auto element = dynamic_cast<const xmlpp::Element*>(node)) {
910 auto children = element->get_children();
911 auto const should_disable_formatting =
913 children.begin(), children.end(),
914 [](xmlpp::Node const* node) { return static_cast<bool>(dynamic_cast<const xmlpp::ContentNode*>(node)); }
915 ) || element->get_name() == "Text";
917 if (!state.disable_formatting) {
918 state.xml += "\n" + string(state.indent * 2, ' ');
921 state.xml += "<" + element->get_name();
923 for (auto attribute: element->get_attributes()) {
924 state.xml += String::compose(" %1=\"%2\"", attribute->get_name().raw(), attribute->get_value().raw());
927 if (children.empty()) {
932 if (should_disable_formatting) {
933 ++state.disable_formatting;
936 for (auto child: children) {
937 format_xml_node(child, state);
940 if (!state.disable_formatting) {
941 state.xml += "\n" + string(state.indent * 2, ' ');
944 state.xml += String::compose("</%1>", element->get_name().raw());
946 if (should_disable_formatting) {
947 --state.disable_formatting;
956 /** Format XML much as write_to_string_formatted() would do, except without adding any white space
957 * to <Text> nodes. This is an attempt to avoid changing what is actually displayed as subtitles
958 * while also formatting the XML in such a way as to avoid DoM bug 2205.
960 * xml_namespace is an optional namespace for the root node; it would be nicer to set this up with
961 * set_namespace_declaration in the caller and then to extract it here but I couldn't find a way
962 * to get all namespaces with the libxml++ API.
965 SubtitleAsset::format_xml(xmlpp::Document const& document, optional<pair<string, string>> xml_namespace)
967 auto root = document.get_root_node();
970 state.xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<" + root->get_name();
973 if (xml_namespace->first.empty()) {
974 state.xml += String::compose(" xmlns=\"%1\"", xml_namespace->second);
976 state.xml += String::compose(" xmlns:%1=\"%2\"", xml_namespace->first, xml_namespace->second);
980 for (auto attribute: root->get_attributes()) {
981 state.xml += String::compose(" %1=\"%2\"", attribute->get_name().raw(), attribute->get_value().raw());
986 for (auto child: document.get_root_node()->get_children()) {
987 format_xml_node(child, state);
990 state.xml += String::compose("\n</%1>\n", root->get_name().raw());
997 SubtitleAsset::ensure_font(string load_id, dcp::ArrayData data)
999 if (std::find_if(_fonts.begin(), _fonts.end(), [load_id](Font const& font) { return font.load_id == load_id; }) == _fonts.end()) {
1000 add_font(load_id, data);