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)) {
503 SubtitleAsset::add (shared_ptr<Subtitle> s)
505 _subtitles.push_back (s);
510 SubtitleAsset::latest_subtitle_out () const
513 for (auto i: _subtitles) {
524 SubtitleAsset::equals(shared_ptr<const Asset> other_asset, EqualityOptions const& options, NoteHandler note) const
526 if (!Asset::equals (other_asset, options, note)) {
530 auto other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
535 if (_subtitles.size() != other->_subtitles.size()) {
536 note (NoteType::ERROR, String::compose("different number of subtitles: %1 vs %2", _subtitles.size(), other->_subtitles.size()));
540 auto i = _subtitles.begin();
541 auto j = other->_subtitles.begin();
543 while (i != _subtitles.end()) {
544 auto string_i = dynamic_pointer_cast<SubtitleString> (*i);
545 auto string_j = dynamic_pointer_cast<SubtitleString> (*j);
546 auto image_i = dynamic_pointer_cast<SubtitleImage> (*i);
547 auto image_j = dynamic_pointer_cast<SubtitleImage> (*j);
549 if ((string_i && !string_j) || (image_i && !image_j)) {
550 note (NoteType::ERROR, "subtitles differ: string vs. image");
554 if (string_i && !string_i->equals(string_j, options, note)) {
558 if (image_i && !image_i->equals(image_j, options, note)) {
570 struct SubtitleSorter
572 bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
573 if (a->in() != b->in()) {
574 return a->in() < b->in();
576 if (a->v_align() == VAlign::BOTTOM) {
577 return a->v_position() > b->v_position();
579 return a->v_position() < b->v_position();
585 SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
587 if (part->children.empty ()) {
591 /* Pull up from children */
592 for (auto i: part->children) {
597 /* Establish the common font features that each of part's children have;
598 these features go into part's font.
600 part->font = part->children.front()->font;
601 for (auto i: part->children) {
602 part->font.take_intersection (i->font);
605 /* Remove common values from part's children's fonts */
606 for (auto i: part->children) {
607 i->font.take_difference (part->font);
611 /* Merge adjacent children with the same font */
612 auto i = part->children.begin();
613 vector<shared_ptr<order::Part>> merged;
615 while (i != part->children.end()) {
617 if ((*i)->font.empty ()) {
618 merged.push_back (*i);
623 while (j != part->children.end() && (*i)->font == (*j)->font) {
626 if (std::distance (i, j) == 1) {
627 merged.push_back (*i);
630 shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
631 for (auto k = i; k != j; ++k) {
633 group->children.push_back (*k);
635 merged.push_back (group);
641 part->children = merged;
645 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
646 * class because the differences between the two are fairly subtle.
649 SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, Standard standard) const
651 auto sorted = _subtitles;
652 std::stable_sort(sorted.begin(), sorted.end(), SubtitleSorter());
654 /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
655 font information into the bottom level (String) objects.
658 auto root = make_shared<order::Part>(shared_ptr<order::Part>());
659 shared_ptr<order::Subtitle> subtitle;
660 shared_ptr<order::Text> text;
664 Time last_fade_up_time;
665 Time last_fade_down_time;
667 float last_h_position;
669 float last_v_position;
670 float last_z_position;
671 Direction last_direction;
673 for (auto i: sorted) {
675 (last_in != i->in() ||
676 last_out != i->out() ||
677 last_fade_up_time != i->fade_up_time() ||
678 last_fade_down_time != i->fade_down_time())
681 subtitle = make_shared<order::Subtitle>(root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time());
682 root->children.push_back (subtitle);
685 last_out = i->out ();
686 last_fade_up_time = i->fade_up_time ();
687 last_fade_down_time = i->fade_down_time ();
691 auto is = dynamic_pointer_cast<SubtitleString>(i);
694 last_h_align != is->h_align() ||
695 fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
696 last_v_align != is->v_align() ||
697 fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
698 fabs(last_z_position - is->z_position()) > ALIGN_EPSILON ||
699 last_direction != is->direction()
701 text = make_shared<order::Text>(subtitle, is->h_align(), is->h_position(), is->v_align(), is->v_position(), is->z_position(), is->direction());
702 subtitle->children.push_back (text);
704 last_h_align = is->h_align ();
705 last_h_position = is->h_position ();
706 last_v_align = is->v_align ();
707 last_v_position = is->v_position ();
708 last_z_position = is->z_position();
709 last_direction = is->direction ();
712 text->children.push_back (make_shared<order::String>(text, order::Font (is, standard), is->text(), is->space_before()));
715 auto ii = dynamic_pointer_cast<SubtitleImage>(i);
718 subtitle->children.push_back (
719 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())
724 /* Pull font changes as high up the hierarchy as we can */
730 order::Context context;
731 context.time_code_rate = time_code_rate;
732 context.standard = standard;
733 context.spot_number = 1;
735 root->write_xml (xml_root, context);
739 map<string, ArrayData>
740 SubtitleAsset::font_data () const
742 map<string, ArrayData> out;
743 for (auto const& i: _fonts) {
744 out[i.load_id] = i.data;
750 map<string, boost::filesystem::path>
751 SubtitleAsset::font_filenames () const
753 map<string, boost::filesystem::path> out;
754 for (auto const& i: _fonts) {
756 out[i.load_id] = *i.file;
763 /** Replace empty IDs in any <LoadFontId> and <Font> tags with
764 * a dummy string. Some systems give errors with empty font IDs
765 * (see DCP-o-matic bug #1689).
768 SubtitleAsset::fix_empty_font_ids ()
770 bool have_empty = false;
772 for (auto i: load_font_nodes()) {
776 ids.push_back (i->id);
784 string const empty_id = unique_string (ids, "font");
786 for (auto i: load_font_nodes()) {
792 for (auto i: _subtitles) {
793 auto j = dynamic_pointer_cast<SubtitleString> (i);
794 if (j && j->font() && j->font().get() == "") {
795 j->set_font (empty_id);
807 int disable_formatting;
815 format_xml_node (xmlpp::Node const* node, State& state)
817 if (auto text_node = dynamic_cast<const xmlpp::TextNode*>(node)) {
818 string content = text_node->get_content();
819 boost::replace_all(content, "&", "&");
820 boost::replace_all(content, "<", "<");
821 boost::replace_all(content, ">", ">");
822 state.xml += content;
823 } else if (auto element = dynamic_cast<const xmlpp::Element*>(node)) {
826 auto children = element->get_children();
827 auto const should_disable_formatting =
829 children.begin(), children.end(),
830 [](xmlpp::Node const* node) { return static_cast<bool>(dynamic_cast<const xmlpp::ContentNode*>(node)); }
831 ) || element->get_name() == "Text";
833 if (!state.disable_formatting) {
834 state.xml += "\n" + string(state.indent * 2, ' ');
837 state.xml += "<" + element->get_name();
839 for (auto attribute: element->get_attributes()) {
840 state.xml += String::compose(" %1=\"%2\"", attribute->get_name().raw(), attribute->get_value().raw());
843 if (children.empty()) {
848 if (should_disable_formatting) {
849 ++state.disable_formatting;
852 for (auto child: children) {
853 format_xml_node(child, state);
856 if (!state.disable_formatting) {
857 state.xml += "\n" + string(state.indent * 2, ' ');
860 state.xml += String::compose("</%1>", element->get_name().raw());
862 if (should_disable_formatting) {
863 --state.disable_formatting;
872 /** Format XML much as write_to_string_formatted() would do, except without adding any white space
873 * to <Text> nodes. This is an attempt to avoid changing what is actually displayed as subtitles
874 * while also formatting the XML in such a way as to avoid DoM bug 2205.
876 * xml_namespace is an optional namespace for the root node; it would be nicer to set this up with
877 * set_namespace_declaration in the caller and then to extract it here but I couldn't find a way
878 * to get all namespaces with the libxml++ API.
881 SubtitleAsset::format_xml(xmlpp::Document const& document, optional<pair<string, string>> xml_namespace)
883 auto root = document.get_root_node();
886 state.xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<" + root->get_name();
889 if (xml_namespace->first.empty()) {
890 state.xml += String::compose(" xmlns=\"%1\"", xml_namespace->second);
892 state.xml += String::compose(" xmlns:%1=\"%2\"", xml_namespace->first, xml_namespace->second);
896 for (auto attribute: root->get_attributes()) {
897 state.xml += String::compose(" %1=\"%2\"", attribute->get_name().raw(), attribute->get_value().raw());
902 for (auto child: document.get_root_node()->get_children()) {
903 format_xml_node(child, state);
906 state.xml += String::compose("\n</%1>\n", root->get_name().raw());
913 SubtitleAsset::ensure_font(string load_id, dcp::ArrayData data)
915 if (std::find_if(_fonts.begin(), _fonts.end(), [load_id](Font const& font) { return font.load_id == load_id; }) == _fonts.end()) {
916 add_font(load_id, data);