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::shared_array;
68 using boost::optional;
69 using boost::lexical_cast;
73 SubtitleAsset::SubtitleAsset ()
79 SubtitleAsset::SubtitleAsset (boost::filesystem::path file)
87 string_attribute (xmlpp::Element const * node, string name)
89 auto a = node->get_attribute (name);
91 throw XMLError (String::compose ("missing attribute %1", name));
93 return string (a->get_value ());
98 optional_string_attribute (xmlpp::Element const * node, string name)
100 auto a = node->get_attribute (name);
104 return string (a->get_value ());
109 optional_bool_attribute (xmlpp::Element const * node, string name)
111 auto s = optional_string_attribute (node, name);
116 return (s.get() == "1" || s.get() == "yes");
122 optional_number_attribute (xmlpp::Element const * node, string name)
124 auto s = optional_string_attribute (node, name);
126 return boost::optional<T> ();
129 std::string t = s.get ();
130 boost::erase_all (t, " ");
131 return raw_convert<T> (t);
135 SubtitleAsset::ParseState
136 SubtitleAsset::font_node_state (xmlpp::Element const * node, Standard standard) const
140 if (standard == Standard::INTEROP) {
141 ps.font_id = optional_string_attribute (node, "Id");
143 ps.font_id = optional_string_attribute (node, "ID");
145 ps.size = optional_number_attribute<int64_t> (node, "Size");
146 ps.aspect_adjust = optional_number_attribute<float> (node, "AspectAdjust");
147 ps.italic = optional_bool_attribute (node, "Italic");
148 ps.bold = optional_string_attribute(node, "Weight").get_value_or("normal") == "bold";
149 if (standard == Standard::INTEROP) {
150 ps.underline = optional_bool_attribute (node, "Underlined");
152 ps.underline = optional_bool_attribute (node, "Underline");
154 auto c = optional_string_attribute (node, "Color");
156 ps.colour = Colour (c.get ());
158 auto const e = optional_string_attribute (node, "Effect");
160 ps.effect = string_to_effect (e.get ());
162 c = optional_string_attribute (node, "EffectColor");
164 ps.effect_colour = Colour (c.get ());
171 SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, xmlpp::Element const * node) const
173 auto hp = optional_number_attribute<float> (node, "HPosition");
175 hp = optional_number_attribute<float> (node, "Hposition");
178 ps.h_position = hp.get () / 100;
181 auto ha = optional_string_attribute (node, "HAlign");
183 ha = optional_string_attribute (node, "Halign");
186 ps.h_align = string_to_halign (ha.get ());
189 auto vp = optional_number_attribute<float> (node, "VPosition");
191 vp = optional_number_attribute<float> (node, "Vposition");
194 ps.v_position = vp.get () / 100;
197 auto va = optional_string_attribute (node, "VAlign");
199 va = optional_string_attribute (node, "Valign");
202 ps.v_align = string_to_valign (va.get ());
208 SubtitleAsset::ParseState
209 SubtitleAsset::text_node_state (xmlpp::Element const * node) const
213 position_align (ps, node);
215 auto d = optional_string_attribute (node, "Direction");
217 ps.direction = string_to_direction (d.get ());
220 ps.type = ParseState::Type::TEXT;
226 SubtitleAsset::ParseState
227 SubtitleAsset::image_node_state (xmlpp::Element const * node) const
231 position_align (ps, node);
233 ps.type = ParseState::Type::IMAGE;
239 SubtitleAsset::ParseState
240 SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional<int> tcr) const
243 ps.in = Time (string_attribute(node, "TimeIn"), tcr);
244 ps.out = Time (string_attribute(node, "TimeOut"), tcr);
245 ps.fade_up_time = fade_time (node, "FadeUpTime", tcr);
246 ps.fade_down_time = fade_time (node, "FadeDownTime", tcr);
252 SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional<int> tcr) const
254 auto const u = optional_string_attribute(node, name).get_value_or ("");
258 t = Time (0, 0, 0, 20, 250);
259 } else if (u.find (":") != string::npos) {
262 t = Time (0, 0, 0, lexical_cast<int> (u), tcr.get_value_or(250));
265 if (t > Time (0, 0, 8, 0, 250)) {
266 t = Time (0, 0, 8, 0, 250);
274 SubtitleAsset::parse_subtitles (xmlpp::Element const * node, vector<ParseState>& state, optional<int> tcr, Standard standard)
276 if (node->get_name() == "Font") {
277 state.push_back (font_node_state (node, standard));
278 } else if (node->get_name() == "Subtitle") {
279 state.push_back (subtitle_node_state (node, tcr));
280 } else if (node->get_name() == "Text") {
281 state.push_back (text_node_state (node));
282 } else if (node->get_name() == "SubtitleList") {
283 state.push_back (ParseState ());
284 } else if (node->get_name() == "Image") {
285 state.push_back (image_node_state (node));
287 throw XMLError ("unexpected node " + node->get_name());
290 for (auto i: node->get_children()) {
291 auto const v = dynamic_cast<xmlpp::ContentNode const *>(i);
293 maybe_add_subtitle (v->get_content(), state, standard);
295 auto const e = dynamic_cast<xmlpp::Element const *>(i);
297 parse_subtitles (e, state, tcr, standard);
306 SubtitleAsset::maybe_add_subtitle (string text, vector<ParseState> const & parse_state, Standard standard)
308 if (empty_or_white_space (text)) {
313 for (auto const& i: parse_state) {
315 ps.font_id = i.font_id.get();
318 ps.size = i.size.get();
320 if (i.aspect_adjust) {
321 ps.aspect_adjust = i.aspect_adjust.get();
324 ps.italic = i.italic.get();
327 ps.bold = i.bold.get();
330 ps.underline = i.underline.get();
333 ps.colour = i.colour.get();
336 ps.effect = i.effect.get();
338 if (i.effect_colour) {
339 ps.effect_colour = i.effect_colour.get();
342 ps.h_position = i.h_position.get();
345 ps.h_align = i.h_align.get();
348 ps.v_position = i.v_position.get();
351 ps.v_align = i.v_align.get();
354 ps.direction = i.direction.get();
360 ps.out = i.out.get();
362 if (i.fade_up_time) {
363 ps.fade_up_time = i.fade_up_time.get();
365 if (i.fade_down_time) {
366 ps.fade_down_time = i.fade_down_time.get();
369 ps.type = i.type.get();
373 if (!ps.in || !ps.out) {
374 /* We're not in a <Subtitle> node; just ignore this content */
378 DCP_ASSERT (ps.type);
380 switch (ps.type.get()) {
381 case ParseState::Type::TEXT:
382 _subtitles.push_back (
383 make_shared<SubtitleString>(
385 ps.italic.get_value_or (false),
386 ps.bold.get_value_or (false),
387 ps.underline.get_value_or (false),
388 ps.colour.get_value_or (dcp::Colour (255, 255, 255)),
389 ps.size.get_value_or (42),
390 ps.aspect_adjust.get_value_or (1.0),
393 ps.h_position.get_value_or(0),
394 ps.h_align.get_value_or(HAlign::CENTER),
395 ps.v_position.get_value_or(0),
396 ps.v_align.get_value_or(VAlign::CENTER),
397 ps.direction.get_value_or (Direction::LTR),
399 ps.effect.get_value_or (Effect::NONE),
400 ps.effect_colour.get_value_or (dcp::Colour (0, 0, 0)),
401 ps.fade_up_time.get_value_or(Time()),
402 ps.fade_down_time.get_value_or(Time())
406 case ParseState::Type::IMAGE:
409 case Standard::INTEROP:
410 if (text.size() >= 4) {
411 /* Remove file extension */
412 text = text.substr(0, text.size() - 4);
415 case Standard::SMPTE:
416 /* It looks like this urn:uuid: is required, but DoM wasn't expecting it (and not writing it)
417 * until around 2.15.140 so I guess either:
418 * a) it is not (always) used in the field, or
419 * b) nobody noticed / complained.
421 if (text.substr(0, 9) == "urn:uuid:") {
422 text = text.substr(9);
427 /* Add a subtitle with no image data and we'll fill that in later */
428 _subtitles.push_back (
429 make_shared<SubtitleImage>(
434 ps.h_position.get_value_or(0),
435 ps.h_align.get_value_or(HAlign::CENTER),
436 ps.v_position.get_value_or(0),
437 ps.v_align.get_value_or(VAlign::CENTER),
438 ps.fade_up_time.get_value_or(Time()),
439 ps.fade_down_time.get_value_or(Time())
448 vector<shared_ptr<const Subtitle>>
449 SubtitleAsset::subtitles () const
451 vector<shared_ptr<const Subtitle>> s;
452 for (auto i: _subtitles) {
459 vector<shared_ptr<const Subtitle>>
460 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
462 vector<shared_ptr<const Subtitle>> s;
463 for (auto i: _subtitles) {
464 if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
473 /* XXX: this needs a test */
474 vector<shared_ptr<const Subtitle>>
475 SubtitleAsset::subtitles_in_reel (shared_ptr<const dcp::ReelAsset> asset) const
477 auto frame_rate = asset->edit_rate().as_float();
478 auto start = dcp::Time(asset->entry_point().get_value_or(0), frame_rate, time_code_rate());
479 auto during = subtitles_during (start, start + dcp::Time(asset->intrinsic_duration(), frame_rate, time_code_rate()), false);
481 vector<shared_ptr<const dcp::Subtitle>> corrected;
482 for (auto i: during) {
483 auto c = make_shared<dcp::Subtitle>(*i);
484 c->set_in (c->in() - start);
485 c->set_out (c->out() - start);
486 corrected.push_back (c);
494 SubtitleAsset::add (shared_ptr<Subtitle> s)
496 _subtitles.push_back (s);
501 SubtitleAsset::latest_subtitle_out () const
504 for (auto i: _subtitles) {
515 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
517 if (!Asset::equals (other_asset, options, note)) {
521 auto other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
526 if (_subtitles.size() != other->_subtitles.size()) {
527 note (NoteType::ERROR, String::compose("different number of subtitles: %1 vs %2", _subtitles.size(), other->_subtitles.size()));
531 auto i = _subtitles.begin();
532 auto j = other->_subtitles.begin();
534 while (i != _subtitles.end()) {
535 auto string_i = dynamic_pointer_cast<SubtitleString> (*i);
536 auto string_j = dynamic_pointer_cast<SubtitleString> (*j);
537 auto image_i = dynamic_pointer_cast<SubtitleImage> (*i);
538 auto image_j = dynamic_pointer_cast<SubtitleImage> (*j);
540 if ((string_i && !string_j) || (image_i && !image_j)) {
541 note (NoteType::ERROR, "subtitles differ: string vs. image");
545 if (string_i && *string_i != *string_j) {
546 note (NoteType::ERROR, String::compose("subtitles differ in text or metadata: %1 vs %2", string_i->text(), string_j->text()));
550 if (image_i && !image_i->equals(image_j, options, note)) {
562 struct SubtitleSorter
564 bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
565 if (a->in() != b->in()) {
566 return a->in() < b->in();
568 return a->v_position() < b->v_position();
574 SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
576 if (part->children.empty ()) {
580 /* Pull up from children */
581 for (auto i: part->children) {
586 /* Establish the common font features that each of part's children have;
587 these features go into part's font.
589 part->font = part->children.front()->font;
590 for (auto i: part->children) {
591 part->font.take_intersection (i->font);
594 /* Remove common values from part's children's fonts */
595 for (auto i: part->children) {
596 i->font.take_difference (part->font);
600 /* Merge adjacent children with the same font */
601 auto i = part->children.begin();
602 vector<shared_ptr<order::Part>> merged;
604 while (i != part->children.end()) {
606 if ((*i)->font.empty ()) {
607 merged.push_back (*i);
612 while (j != part->children.end() && (*i)->font == (*j)->font) {
615 if (std::distance (i, j) == 1) {
616 merged.push_back (*i);
619 shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
620 for (auto k = i; k != j; ++k) {
622 group->children.push_back (*k);
624 merged.push_back (group);
630 part->children = merged;
634 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
635 * class because the differences between the two are fairly subtle.
638 SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, Standard standard) const
640 auto sorted = _subtitles;
641 std::stable_sort(sorted.begin(), sorted.end(), SubtitleSorter());
643 /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
644 font information into the bottom level (String) objects.
647 auto root = make_shared<order::Part>(shared_ptr<order::Part>());
648 shared_ptr<order::Subtitle> subtitle;
649 shared_ptr<order::Text> text;
653 Time last_fade_up_time;
654 Time last_fade_down_time;
656 float last_h_position;
658 float last_v_position;
659 Direction last_direction;
661 for (auto i: sorted) {
663 (last_in != i->in() ||
664 last_out != i->out() ||
665 last_fade_up_time != i->fade_up_time() ||
666 last_fade_down_time != i->fade_down_time())
669 subtitle = make_shared<order::Subtitle>(root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time());
670 root->children.push_back (subtitle);
673 last_out = i->out ();
674 last_fade_up_time = i->fade_up_time ();
675 last_fade_down_time = i->fade_down_time ();
679 auto is = dynamic_pointer_cast<SubtitleString>(i);
682 last_h_align != is->h_align() ||
683 fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
684 last_v_align != is->v_align() ||
685 fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
686 last_direction != is->direction()
688 text.reset (new order::Text (subtitle, is->h_align(), is->h_position(), is->v_align(), is->v_position(), is->direction()));
689 subtitle->children.push_back (text);
691 last_h_align = is->h_align ();
692 last_h_position = is->h_position ();
693 last_v_align = is->v_align ();
694 last_v_position = is->v_position ();
695 last_direction = is->direction ();
698 text->children.push_back (make_shared<order::String>(text, order::Font (is, standard), is->text()));
701 auto ii = dynamic_pointer_cast<SubtitleImage>(i);
704 subtitle->children.push_back (
705 make_shared<order::Image>(subtitle, ii->id(), ii->png_image(), ii->h_align(), ii->h_position(), ii->v_align(), ii->v_position())
710 /* Pull font changes as high up the hierarchy as we can */
716 order::Context context;
717 context.time_code_rate = time_code_rate;
718 context.standard = standard;
719 context.spot_number = 1;
721 root->write_xml (xml_root, context);
725 map<string, ArrayData>
726 SubtitleAsset::font_data () const
728 map<string, ArrayData> out;
729 for (auto const& i: _fonts) {
730 out[i.load_id] = i.data;
736 map<string, boost::filesystem::path>
737 SubtitleAsset::font_filenames () const
739 map<string, boost::filesystem::path> out;
740 for (auto const& i: _fonts) {
742 out[i.load_id] = *i.file;
749 /** Replace empty IDs in any <LoadFontId> and <Font> tags with
750 * a dummy string. Some systems give errors with empty font IDs
751 * (see DCP-o-matic bug #1689).
754 SubtitleAsset::fix_empty_font_ids ()
756 bool have_empty = false;
758 for (auto i: load_font_nodes()) {
762 ids.push_back (i->id);
770 string const empty_id = unique_string (ids, "font");
772 for (auto i: load_font_nodes()) {
778 for (auto i: _subtitles) {
779 auto j = dynamic_pointer_cast<SubtitleString> (i);
780 if (j && j->font() && j->font().get() == "") {
781 j->set_font (empty_id);