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 for (auto i: node->get_children()) {
290 auto const v = dynamic_cast<xmlpp::ContentNode const *>(i);
292 maybe_add_subtitle (v->get_content(), state, standard);
294 auto const e = dynamic_cast<xmlpp::Element const *>(i);
296 parse_subtitles (e, state, tcr, standard);
305 SubtitleAsset::maybe_add_subtitle (string text, vector<ParseState> const & parse_state, Standard standard)
307 if (empty_or_white_space (text)) {
312 for (auto const& i: parse_state) {
314 ps.font_id = i.font_id.get();
317 ps.size = i.size.get();
319 if (i.aspect_adjust) {
320 ps.aspect_adjust = i.aspect_adjust.get();
323 ps.italic = i.italic.get();
326 ps.bold = i.bold.get();
329 ps.underline = i.underline.get();
332 ps.colour = i.colour.get();
335 ps.effect = i.effect.get();
337 if (i.effect_colour) {
338 ps.effect_colour = i.effect_colour.get();
341 ps.h_position = i.h_position.get();
344 ps.h_align = i.h_align.get();
347 ps.v_position = i.v_position.get();
350 ps.v_align = i.v_align.get();
353 ps.direction = i.direction.get();
359 ps.out = i.out.get();
361 if (i.fade_up_time) {
362 ps.fade_up_time = i.fade_up_time.get();
364 if (i.fade_down_time) {
365 ps.fade_down_time = i.fade_down_time.get();
368 ps.type = i.type.get();
372 if (!ps.in || !ps.out) {
373 /* We're not in a <Subtitle> node; just ignore this content */
377 DCP_ASSERT (ps.type);
379 switch (ps.type.get()) {
380 case ParseState::Type::TEXT:
381 _subtitles.push_back (
382 make_shared<SubtitleString>(
384 ps.italic.get_value_or (false),
385 ps.bold.get_value_or (false),
386 ps.underline.get_value_or (false),
387 ps.colour.get_value_or (dcp::Colour (255, 255, 255)),
388 ps.size.get_value_or (42),
389 ps.aspect_adjust.get_value_or (1.0),
392 ps.h_position.get_value_or(0),
393 ps.h_align.get_value_or(HAlign::CENTER),
394 ps.v_position.get_value_or(0),
395 ps.v_align.get_value_or(VAlign::CENTER),
396 ps.direction.get_value_or (Direction::LTR),
398 ps.effect.get_value_or (Effect::NONE),
399 ps.effect_colour.get_value_or (dcp::Colour (0, 0, 0)),
400 ps.fade_up_time.get_value_or(Time()),
401 ps.fade_down_time.get_value_or(Time())
405 case ParseState::Type::IMAGE:
408 case Standard::INTEROP:
409 if (text.size() >= 4) {
410 /* Remove file extension */
411 text = text.substr(0, text.size() - 4);
414 case Standard::SMPTE:
415 /* It looks like this urn:uuid: is required, but DoM wasn't expecting it (and not writing it)
416 * until around 2.15.140 so I guess either:
417 * a) it is not (always) used in the field, or
418 * b) nobody noticed / complained.
420 if (text.substr(0, 9) == "urn:uuid:") {
421 text = text.substr(9);
426 /* Add a subtitle with no image data and we'll fill that in later */
427 _subtitles.push_back (
428 make_shared<SubtitleImage>(
433 ps.h_position.get_value_or(0),
434 ps.h_align.get_value_or(HAlign::CENTER),
435 ps.v_position.get_value_or(0),
436 ps.v_align.get_value_or(VAlign::CENTER),
437 ps.fade_up_time.get_value_or(Time()),
438 ps.fade_down_time.get_value_or(Time())
447 vector<shared_ptr<const Subtitle>>
448 SubtitleAsset::subtitles () const
450 vector<shared_ptr<const Subtitle>> s;
451 for (auto i: _subtitles) {
458 vector<shared_ptr<const Subtitle>>
459 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
461 vector<shared_ptr<const Subtitle>> s;
462 for (auto i: _subtitles) {
463 if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
472 /* XXX: this needs a test */
473 vector<shared_ptr<const Subtitle>>
474 SubtitleAsset::subtitles_in_reel (shared_ptr<const dcp::ReelAsset> asset) const
476 auto frame_rate = asset->edit_rate().as_float();
477 auto start = dcp::Time(asset->entry_point().get_value_or(0), frame_rate, time_code_rate());
478 auto during = subtitles_during (start, start + dcp::Time(asset->intrinsic_duration(), frame_rate, time_code_rate()), false);
480 vector<shared_ptr<const dcp::Subtitle>> corrected;
481 for (auto i: during) {
482 auto c = make_shared<dcp::Subtitle>(*i);
483 c->set_in (c->in() - start);
484 c->set_out (c->out() - start);
485 corrected.push_back (c);
493 SubtitleAsset::add (shared_ptr<Subtitle> s)
495 _subtitles.push_back (s);
500 SubtitleAsset::latest_subtitle_out () const
503 for (auto i: _subtitles) {
514 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
516 if (!Asset::equals (other_asset, options, note)) {
520 auto other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
525 if (_subtitles.size() != other->_subtitles.size()) {
526 note (NoteType::ERROR, String::compose("different number of subtitles: %1 vs %2", _subtitles.size(), other->_subtitles.size()));
530 auto i = _subtitles.begin();
531 auto j = other->_subtitles.begin();
533 while (i != _subtitles.end()) {
534 auto string_i = dynamic_pointer_cast<SubtitleString> (*i);
535 auto string_j = dynamic_pointer_cast<SubtitleString> (*j);
536 auto image_i = dynamic_pointer_cast<SubtitleImage> (*i);
537 auto image_j = dynamic_pointer_cast<SubtitleImage> (*j);
539 if ((string_i && !string_j) || (image_i && !image_j)) {
540 note (NoteType::ERROR, "subtitles differ: string vs. image");
544 if (string_i && *string_i != *string_j) {
545 note (NoteType::ERROR, String::compose("subtitles differ in text or metadata: %1 vs %2", string_i->text(), string_j->text()));
549 if (image_i && !image_i->equals(image_j, options, note)) {
561 struct SubtitleSorter
563 bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
564 if (a->in() != b->in()) {
565 return a->in() < b->in();
567 return a->v_position() < b->v_position();
573 SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
575 if (part->children.empty ()) {
579 /* Pull up from children */
580 for (auto i: part->children) {
585 /* Establish the common font features that each of part's children have;
586 these features go into part's font.
588 part->font = part->children.front()->font;
589 for (auto i: part->children) {
590 part->font.take_intersection (i->font);
593 /* Remove common values from part's children's fonts */
594 for (auto i: part->children) {
595 i->font.take_difference (part->font);
599 /* Merge adjacent children with the same font */
600 auto i = part->children.begin();
601 vector<shared_ptr<order::Part>> merged;
603 while (i != part->children.end()) {
605 if ((*i)->font.empty ()) {
606 merged.push_back (*i);
611 while (j != part->children.end() && (*i)->font == (*j)->font) {
614 if (std::distance (i, j) == 1) {
615 merged.push_back (*i);
618 shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
619 for (auto k = i; k != j; ++k) {
621 group->children.push_back (*k);
623 merged.push_back (group);
629 part->children = merged;
633 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
634 * class because the differences between the two are fairly subtle.
637 SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, Standard standard) const
639 auto sorted = _subtitles;
640 std::stable_sort(sorted.begin(), sorted.end(), SubtitleSorter());
642 /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
643 font information into the bottom level (String) objects.
646 auto root = make_shared<order::Part>(shared_ptr<order::Part>());
647 shared_ptr<order::Subtitle> subtitle;
648 shared_ptr<order::Text> text;
652 Time last_fade_up_time;
653 Time last_fade_down_time;
655 float last_h_position;
657 float last_v_position;
658 Direction last_direction;
660 for (auto i: sorted) {
662 (last_in != i->in() ||
663 last_out != i->out() ||
664 last_fade_up_time != i->fade_up_time() ||
665 last_fade_down_time != i->fade_down_time())
668 subtitle = make_shared<order::Subtitle>(root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time());
669 root->children.push_back (subtitle);
672 last_out = i->out ();
673 last_fade_up_time = i->fade_up_time ();
674 last_fade_down_time = i->fade_down_time ();
678 auto is = dynamic_pointer_cast<SubtitleString>(i);
681 last_h_align != is->h_align() ||
682 fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
683 last_v_align != is->v_align() ||
684 fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
685 last_direction != is->direction()
687 text.reset (new order::Text (subtitle, is->h_align(), is->h_position(), is->v_align(), is->v_position(), is->direction()));
688 subtitle->children.push_back (text);
690 last_h_align = is->h_align ();
691 last_h_position = is->h_position ();
692 last_v_align = is->v_align ();
693 last_v_position = is->v_position ();
694 last_direction = is->direction ();
697 text->children.push_back (make_shared<order::String>(text, order::Font (is, standard), is->text()));
700 auto ii = dynamic_pointer_cast<SubtitleImage>(i);
703 subtitle->children.push_back (
704 make_shared<order::Image>(subtitle, ii->id(), ii->png_image(), ii->h_align(), ii->h_position(), ii->v_align(), ii->v_position())
709 /* Pull font changes as high up the hierarchy as we can */
715 order::Context context;
716 context.time_code_rate = time_code_rate;
717 context.standard = standard;
718 context.spot_number = 1;
720 root->write_xml (xml_root, context);
724 map<string, ArrayData>
725 SubtitleAsset::font_data () const
727 map<string, ArrayData> out;
728 for (auto const& i: _fonts) {
729 out[i.load_id] = i.data;
735 map<string, boost::filesystem::path>
736 SubtitleAsset::font_filenames () const
738 map<string, boost::filesystem::path> out;
739 for (auto const& i: _fonts) {
741 out[i.load_id] = *i.file;
748 /** Replace empty IDs in any <LoadFontId> and <Font> tags with
749 * a dummy string. Some systems give errors with empty font IDs
750 * (see DCP-o-matic bug #1689).
753 SubtitleAsset::fix_empty_font_ids ()
755 bool have_empty = false;
757 for (auto i: load_font_nodes()) {
761 ids.push_back (i->id);
769 string const empty_id = unique_string (ids, "font");
771 for (auto i: load_font_nodes()) {
777 for (auto i: _subtitles) {
778 auto j = dynamic_pointer_cast<SubtitleString> (i);
779 if (j && j->font() && j->font().get() == "") {
780 j->set_font (empty_id);