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>
59 using std::dynamic_pointer_cast;
64 using std::shared_ptr;
66 using std::make_shared;
67 using boost::optional;
68 using boost::lexical_cast;
72 SubtitleAsset::SubtitleAsset ()
78 SubtitleAsset::SubtitleAsset (boost::filesystem::path file)
86 string_attribute (xmlpp::Element const * node, string name)
88 auto a = node->get_attribute (name);
90 throw XMLError (String::compose ("missing attribute %1", name));
92 return string (a->get_value ());
97 optional_string_attribute (xmlpp::Element const * node, string name)
99 auto a = node->get_attribute (name);
103 return string (a->get_value ());
108 optional_bool_attribute (xmlpp::Element const * node, string name)
110 auto s = optional_string_attribute (node, name);
115 return (s.get() == "1" || s.get() == "yes");
121 optional_number_attribute (xmlpp::Element const * node, string name)
123 auto s = optional_string_attribute (node, name);
125 return boost::optional<T> ();
128 std::string t = s.get ();
129 boost::erase_all (t, " ");
130 return raw_convert<T> (t);
134 SubtitleAsset::ParseState
135 SubtitleAsset::font_node_state (xmlpp::Element const * node, Standard standard) const
139 if (standard == Standard::INTEROP) {
140 ps.font_id = optional_string_attribute (node, "Id");
142 ps.font_id = optional_string_attribute (node, "ID");
144 ps.size = optional_number_attribute<int64_t> (node, "Size");
145 ps.aspect_adjust = optional_number_attribute<float> (node, "AspectAdjust");
146 ps.italic = optional_bool_attribute (node, "Italic");
147 ps.bold = optional_string_attribute(node, "Weight").get_value_or("normal") == "bold";
148 if (standard == Standard::INTEROP) {
149 ps.underline = optional_bool_attribute (node, "Underlined");
151 ps.underline = optional_bool_attribute (node, "Underline");
153 auto c = optional_string_attribute (node, "Color");
155 ps.colour = Colour (c.get ());
157 auto const e = optional_string_attribute (node, "Effect");
159 ps.effect = string_to_effect (e.get ());
161 c = optional_string_attribute (node, "EffectColor");
163 ps.effect_colour = Colour (c.get ());
170 SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, xmlpp::Element const * node) const
172 auto hp = optional_number_attribute<float> (node, "HPosition");
174 hp = optional_number_attribute<float> (node, "Hposition");
177 ps.h_position = hp.get () / 100;
180 auto ha = optional_string_attribute (node, "HAlign");
182 ha = optional_string_attribute (node, "Halign");
185 ps.h_align = string_to_halign (ha.get ());
188 auto vp = optional_number_attribute<float> (node, "VPosition");
190 vp = optional_number_attribute<float> (node, "Vposition");
193 ps.v_position = vp.get () / 100;
196 auto va = optional_string_attribute (node, "VAlign");
198 va = optional_string_attribute (node, "Valign");
201 ps.v_align = string_to_valign (va.get ());
207 SubtitleAsset::ParseState
208 SubtitleAsset::text_node_state (xmlpp::Element const * node) const
212 position_align (ps, node);
214 auto d = optional_string_attribute (node, "Direction");
216 ps.direction = string_to_direction (d.get ());
219 ps.type = ParseState::Type::TEXT;
225 SubtitleAsset::ParseState
226 SubtitleAsset::image_node_state (xmlpp::Element const * node) const
230 position_align (ps, node);
232 ps.type = ParseState::Type::IMAGE;
238 SubtitleAsset::ParseState
239 SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional<int> tcr) const
242 ps.in = Time (string_attribute(node, "TimeIn"), tcr);
243 ps.out = Time (string_attribute(node, "TimeOut"), tcr);
244 ps.fade_up_time = fade_time (node, "FadeUpTime", tcr);
245 ps.fade_down_time = fade_time (node, "FadeDownTime", tcr);
251 SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional<int> tcr) const
253 auto const u = optional_string_attribute(node, name).get_value_or ("");
257 t = Time (0, 0, 0, 20, 250);
258 } else if (u.find (":") != string::npos) {
261 t = Time (0, 0, 0, lexical_cast<int> (u), tcr.get_value_or(250));
264 if (t > Time (0, 0, 8, 0, 250)) {
265 t = Time (0, 0, 8, 0, 250);
273 SubtitleAsset::parse_subtitles (xmlpp::Element const * node, vector<ParseState>& state, optional<int> tcr, Standard standard)
275 if (node->get_name() == "Font") {
276 state.push_back (font_node_state (node, standard));
277 } else if (node->get_name() == "Subtitle") {
278 state.push_back (subtitle_node_state (node, tcr));
279 } else if (node->get_name() == "Text") {
280 state.push_back (text_node_state (node));
281 } else if (node->get_name() == "SubtitleList") {
282 state.push_back (ParseState ());
283 } else if (node->get_name() == "Image") {
284 state.push_back (image_node_state (node));
286 throw XMLError ("unexpected node " + node->get_name());
289 float space_before = 0;
291 for (auto i: node->get_children()) {
292 auto const v = dynamic_cast<xmlpp::ContentNode const *>(i);
294 maybe_add_subtitle (v->get_content(), state, space_before, standard);
297 auto const e = dynamic_cast<xmlpp::Element const *>(i);
299 if (e->get_name() == "Space") {
300 if (node->get_name() != "Text") {
301 throw XMLError ("Space node found outside Text");
303 auto size = optional_string_attribute(e, "Size").get_value_or("0.5");
304 if (standard == dcp::Standard::INTEROP) {
305 boost::replace_all(size, "em", "");
307 space_before += raw_convert<float>(size);
309 parse_subtitles (e, state, tcr, standard);
319 SubtitleAsset::maybe_add_subtitle (string text, vector<ParseState> const & parse_state, float space_before, Standard standard)
321 if (empty_or_white_space (text)) {
326 for (auto const& i: parse_state) {
328 ps.font_id = i.font_id.get();
331 ps.size = i.size.get();
333 if (i.aspect_adjust) {
334 ps.aspect_adjust = i.aspect_adjust.get();
337 ps.italic = i.italic.get();
340 ps.bold = i.bold.get();
343 ps.underline = i.underline.get();
346 ps.colour = i.colour.get();
349 ps.effect = i.effect.get();
351 if (i.effect_colour) {
352 ps.effect_colour = i.effect_colour.get();
355 ps.h_position = i.h_position.get();
358 ps.h_align = i.h_align.get();
361 ps.v_position = i.v_position.get();
364 ps.v_align = i.v_align.get();
367 ps.direction = i.direction.get();
373 ps.out = i.out.get();
375 if (i.fade_up_time) {
376 ps.fade_up_time = i.fade_up_time.get();
378 if (i.fade_down_time) {
379 ps.fade_down_time = i.fade_down_time.get();
382 ps.type = i.type.get();
386 if (!ps.in || !ps.out) {
387 /* We're not in a <Subtitle> node; just ignore this content */
391 DCP_ASSERT (ps.type);
393 switch (ps.type.get()) {
394 case ParseState::Type::TEXT:
395 _subtitles.push_back (
396 make_shared<SubtitleString>(
398 ps.italic.get_value_or (false),
399 ps.bold.get_value_or (false),
400 ps.underline.get_value_or (false),
401 ps.colour.get_value_or (dcp::Colour (255, 255, 255)),
402 ps.size.get_value_or (42),
403 ps.aspect_adjust.get_value_or (1.0),
406 ps.h_position.get_value_or(0),
407 ps.h_align.get_value_or(HAlign::CENTER),
408 ps.v_position.get_value_or(0),
409 ps.v_align.get_value_or(VAlign::CENTER),
410 ps.direction.get_value_or (Direction::LTR),
412 ps.effect.get_value_or (Effect::NONE),
413 ps.effect_colour.get_value_or (dcp::Colour (0, 0, 0)),
414 ps.fade_up_time.get_value_or(Time()),
415 ps.fade_down_time.get_value_or(Time()),
420 case ParseState::Type::IMAGE:
423 case Standard::INTEROP:
424 if (text.size() >= 4) {
425 /* Remove file extension */
426 text = text.substr(0, text.size() - 4);
429 case Standard::SMPTE:
430 /* It looks like this urn:uuid: is required, but DoM wasn't expecting it (and not writing it)
431 * until around 2.15.140 so I guess either:
432 * a) it is not (always) used in the field, or
433 * b) nobody noticed / complained.
435 if (text.substr(0, 9) == "urn:uuid:") {
436 text = text.substr(9);
441 /* Add a subtitle with no image data and we'll fill that in later */
442 _subtitles.push_back (
443 make_shared<SubtitleImage>(
448 ps.h_position.get_value_or(0),
449 ps.h_align.get_value_or(HAlign::CENTER),
450 ps.v_position.get_value_or(0),
451 ps.v_align.get_value_or(VAlign::CENTER),
452 ps.fade_up_time.get_value_or(Time()),
453 ps.fade_down_time.get_value_or(Time())
462 vector<shared_ptr<const Subtitle>>
463 SubtitleAsset::subtitles () const
465 vector<shared_ptr<const Subtitle>> s;
466 for (auto i: _subtitles) {
473 vector<shared_ptr<const Subtitle>>
474 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
476 vector<shared_ptr<const Subtitle>> s;
477 for (auto i: _subtitles) {
478 if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
487 /* XXX: this needs a test */
488 vector<shared_ptr<const Subtitle>>
489 SubtitleAsset::subtitles_in_reel (shared_ptr<const dcp::ReelAsset> asset) const
491 auto frame_rate = asset->edit_rate().as_float();
492 auto start = dcp::Time(asset->entry_point().get_value_or(0), frame_rate, time_code_rate());
493 auto during = subtitles_during (start, start + dcp::Time(asset->intrinsic_duration(), frame_rate, time_code_rate()), false);
495 vector<shared_ptr<const dcp::Subtitle>> corrected;
496 for (auto i: during) {
497 auto c = make_shared<dcp::Subtitle>(*i);
498 c->set_in (c->in() - start);
499 c->set_out (c->out() - start);
500 corrected.push_back (c);
508 SubtitleAsset::add (shared_ptr<Subtitle> s)
510 _subtitles.push_back (s);
515 SubtitleAsset::latest_subtitle_out () const
518 for (auto i: _subtitles) {
529 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
531 if (!Asset::equals (other_asset, options, note)) {
535 auto other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
540 if (_subtitles.size() != other->_subtitles.size()) {
541 note (NoteType::ERROR, String::compose("different number of subtitles: %1 vs %2", _subtitles.size(), other->_subtitles.size()));
545 auto i = _subtitles.begin();
546 auto j = other->_subtitles.begin();
548 while (i != _subtitles.end()) {
549 auto string_i = dynamic_pointer_cast<SubtitleString> (*i);
550 auto string_j = dynamic_pointer_cast<SubtitleString> (*j);
551 auto image_i = dynamic_pointer_cast<SubtitleImage> (*i);
552 auto image_j = dynamic_pointer_cast<SubtitleImage> (*j);
554 if ((string_i && !string_j) || (image_i && !image_j)) {
555 note (NoteType::ERROR, "subtitles differ: string vs. image");
559 if (string_i && *string_i != *string_j) {
560 note (NoteType::ERROR, String::compose("subtitles differ in text or metadata: %1 vs %2", string_i->text(), string_j->text()));
564 if (image_i && !image_i->equals(image_j, options, note)) {
576 struct SubtitleSorter
578 bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
579 if (a->in() != b->in()) {
580 return a->in() < b->in();
582 if (a->v_align() == VAlign::BOTTOM) {
583 return a->v_position() > b->v_position();
585 return a->v_position() < b->v_position();
591 SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
593 if (part->children.empty ()) {
597 /* Pull up from children */
598 for (auto i: part->children) {
603 /* Establish the common font features that each of part's children have;
604 these features go into part's font.
606 part->font = part->children.front()->font;
607 for (auto i: part->children) {
608 part->font.take_intersection (i->font);
611 /* Remove common values from part's children's fonts */
612 for (auto i: part->children) {
613 i->font.take_difference (part->font);
617 /* Merge adjacent children with the same font */
618 auto i = part->children.begin();
619 vector<shared_ptr<order::Part>> merged;
621 while (i != part->children.end()) {
623 if ((*i)->font.empty ()) {
624 merged.push_back (*i);
629 while (j != part->children.end() && (*i)->font == (*j)->font) {
632 if (std::distance (i, j) == 1) {
633 merged.push_back (*i);
636 shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
637 for (auto k = i; k != j; ++k) {
639 group->children.push_back (*k);
641 merged.push_back (group);
647 part->children = merged;
651 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
652 * class because the differences between the two are fairly subtle.
655 SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, Standard standard) const
657 auto sorted = _subtitles;
658 std::stable_sort(sorted.begin(), sorted.end(), SubtitleSorter());
660 /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
661 font information into the bottom level (String) objects.
664 auto root = make_shared<order::Part>(shared_ptr<order::Part>());
665 shared_ptr<order::Subtitle> subtitle;
666 shared_ptr<order::Text> text;
670 Time last_fade_up_time;
671 Time last_fade_down_time;
673 float last_h_position;
675 float last_v_position;
676 Direction last_direction;
678 for (auto i: sorted) {
680 (last_in != i->in() ||
681 last_out != i->out() ||
682 last_fade_up_time != i->fade_up_time() ||
683 last_fade_down_time != i->fade_down_time())
686 subtitle = make_shared<order::Subtitle>(root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time());
687 root->children.push_back (subtitle);
690 last_out = i->out ();
691 last_fade_up_time = i->fade_up_time ();
692 last_fade_down_time = i->fade_down_time ();
696 auto is = dynamic_pointer_cast<SubtitleString>(i);
699 last_h_align != is->h_align() ||
700 fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
701 last_v_align != is->v_align() ||
702 fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
703 last_direction != is->direction()
705 text = make_shared<order::Text>(subtitle, is->h_align(), is->h_position(), is->v_align(), is->v_position(), is->direction());
706 subtitle->children.push_back (text);
708 last_h_align = is->h_align ();
709 last_h_position = is->h_position ();
710 last_v_align = is->v_align ();
711 last_v_position = is->v_position ();
712 last_direction = is->direction ();
715 text->children.push_back (make_shared<order::String>(text, order::Font (is, standard), is->text(), is->space_before()));
718 auto ii = dynamic_pointer_cast<SubtitleImage>(i);
721 subtitle->children.push_back (
722 make_shared<order::Image>(subtitle, ii->id(), ii->png_image(), ii->h_align(), ii->h_position(), ii->v_align(), ii->v_position())
727 /* Pull font changes as high up the hierarchy as we can */
733 order::Context context;
734 context.time_code_rate = time_code_rate;
735 context.standard = standard;
736 context.spot_number = 1;
738 root->write_xml (xml_root, context);
742 map<string, ArrayData>
743 SubtitleAsset::font_data () const
745 map<string, ArrayData> out;
746 for (auto const& i: _fonts) {
747 out[i.load_id] = i.data;
753 map<string, boost::filesystem::path>
754 SubtitleAsset::font_filenames () const
756 map<string, boost::filesystem::path> out;
757 for (auto const& i: _fonts) {
759 out[i.load_id] = *i.file;
766 /** Replace empty IDs in any <LoadFontId> and <Font> tags with
767 * a dummy string. Some systems give errors with empty font IDs
768 * (see DCP-o-matic bug #1689).
771 SubtitleAsset::fix_empty_font_ids ()
773 bool have_empty = false;
775 for (auto i: load_font_nodes()) {
779 ids.push_back (i->id);
787 string const empty_id = unique_string (ids, "font");
789 for (auto i: load_font_nodes()) {
795 for (auto i: _subtitles) {
796 auto j = dynamic_pointer_cast<SubtitleString> (i);
797 if (j && j->font() && j->font().get() == "") {
798 j->set_font (empty_id);