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:
407 /* Add a subtitle with no image data and we'll fill that in later */
408 _subtitles.push_back (
409 make_shared<SubtitleImage>(
411 standard == Standard::INTEROP ? text.substr(0, text.size() - 4) : text,
414 ps.h_position.get_value_or(0),
415 ps.h_align.get_value_or(HAlign::CENTER),
416 ps.v_position.get_value_or(0),
417 ps.v_align.get_value_or(VAlign::CENTER),
418 ps.fade_up_time.get_value_or(Time()),
419 ps.fade_down_time.get_value_or(Time())
427 vector<shared_ptr<const Subtitle>>
428 SubtitleAsset::subtitles () const
430 vector<shared_ptr<const Subtitle>> s;
431 for (auto i: _subtitles) {
438 vector<shared_ptr<const Subtitle>>
439 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
441 vector<shared_ptr<const Subtitle>> s;
442 for (auto i: _subtitles) {
443 if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
452 /* XXX: this needs a test */
453 vector<shared_ptr<const Subtitle>>
454 SubtitleAsset::subtitles_in_reel (shared_ptr<const dcp::ReelAsset> asset) const
456 auto frame_rate = asset->edit_rate().as_float();
457 auto start = dcp::Time(asset->entry_point().get_value_or(0), frame_rate, time_code_rate());
458 auto during = subtitles_during (start, start + dcp::Time(asset->intrinsic_duration(), frame_rate, time_code_rate()), false);
460 vector<shared_ptr<const dcp::Subtitle>> corrected;
461 for (auto i: during) {
462 auto c = make_shared<dcp::Subtitle>(*i);
463 c->set_in (c->in() - start);
464 c->set_out (c->out() - start);
465 corrected.push_back (c);
473 SubtitleAsset::add (shared_ptr<Subtitle> s)
475 _subtitles.push_back (s);
480 SubtitleAsset::latest_subtitle_out () const
483 for (auto i: _subtitles) {
494 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
496 if (!Asset::equals (other_asset, options, note)) {
500 auto other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
505 if (_subtitles.size() != other->_subtitles.size()) {
506 note (NoteType::ERROR, String::compose("different number of subtitles: %1 vs %2", _subtitles.size(), other->_subtitles.size()));
510 auto i = _subtitles.begin();
511 auto j = other->_subtitles.begin();
513 while (i != _subtitles.end()) {
514 auto string_i = dynamic_pointer_cast<SubtitleString> (*i);
515 auto string_j = dynamic_pointer_cast<SubtitleString> (*j);
516 auto image_i = dynamic_pointer_cast<SubtitleImage> (*i);
517 auto image_j = dynamic_pointer_cast<SubtitleImage> (*j);
519 if ((string_i && !string_j) || (image_i && !image_j)) {
520 note (NoteType::ERROR, "subtitles differ: string vs. image");
524 if (string_i && *string_i != *string_j) {
525 note (NoteType::ERROR, String::compose("subtitles differ in text or metadata: %1 vs %2", string_i->text(), string_j->text()));
529 if (image_i && !image_i->equals(image_j, options, note)) {
541 struct SubtitleSorter
543 bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
544 if (a->in() != b->in()) {
545 return a->in() < b->in();
547 return a->v_position() < b->v_position();
553 SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
555 if (part->children.empty ()) {
559 /* Pull up from children */
560 for (auto i: part->children) {
565 /* Establish the common font features that each of part's children have;
566 these features go into part's font.
568 part->font = part->children.front()->font;
569 for (auto i: part->children) {
570 part->font.take_intersection (i->font);
573 /* Remove common values from part's children's fonts */
574 for (auto i: part->children) {
575 i->font.take_difference (part->font);
579 /* Merge adjacent children with the same font */
580 auto i = part->children.begin();
581 vector<shared_ptr<order::Part>> merged;
583 while (i != part->children.end()) {
585 if ((*i)->font.empty ()) {
586 merged.push_back (*i);
591 while (j != part->children.end() && (*i)->font == (*j)->font) {
594 if (std::distance (i, j) == 1) {
595 merged.push_back (*i);
598 shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
599 for (auto k = i; k != j; ++k) {
601 group->children.push_back (*k);
603 merged.push_back (group);
609 part->children = merged;
613 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
614 * class because the differences between the two are fairly subtle.
617 SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, Standard standard) const
619 auto sorted = _subtitles;
620 std::stable_sort(sorted.begin(), sorted.end(), SubtitleSorter());
622 /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
623 font information into the bottom level (String) objects.
626 auto root = make_shared<order::Part>(shared_ptr<order::Part>());
627 shared_ptr<order::Subtitle> subtitle;
628 shared_ptr<order::Text> text;
632 Time last_fade_up_time;
633 Time last_fade_down_time;
635 float last_h_position;
637 float last_v_position;
638 Direction last_direction;
640 for (auto i: sorted) {
642 (last_in != i->in() ||
643 last_out != i->out() ||
644 last_fade_up_time != i->fade_up_time() ||
645 last_fade_down_time != i->fade_down_time())
648 subtitle = make_shared<order::Subtitle>(root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time());
649 root->children.push_back (subtitle);
652 last_out = i->out ();
653 last_fade_up_time = i->fade_up_time ();
654 last_fade_down_time = i->fade_down_time ();
658 auto is = dynamic_pointer_cast<SubtitleString>(i);
661 last_h_align != is->h_align() ||
662 fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
663 last_v_align != is->v_align() ||
664 fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
665 last_direction != is->direction()
667 text.reset (new order::Text (subtitle, is->h_align(), is->h_position(), is->v_align(), is->v_position(), is->direction()));
668 subtitle->children.push_back (text);
670 last_h_align = is->h_align ();
671 last_h_position = is->h_position ();
672 last_v_align = is->v_align ();
673 last_v_position = is->v_position ();
674 last_direction = is->direction ();
677 text->children.push_back (shared_ptr<order::String> (new order::String (text, order::Font (is, standard), is->text())));
680 auto ii = dynamic_pointer_cast<SubtitleImage>(i);
683 subtitle->children.push_back (
684 shared_ptr<order::Image> (new order::Image (subtitle, ii->id(), ii->png_image(), ii->h_align(), ii->h_position(), ii->v_align(), ii->v_position()))
689 /* Pull font changes as high up the hierarchy as we can */
695 order::Context context;
696 context.time_code_rate = time_code_rate;
697 context.standard = standard;
698 context.spot_number = 1;
700 root->write_xml (xml_root, context);
704 map<string, ArrayData>
705 SubtitleAsset::font_data () const
707 map<string, ArrayData> out;
708 for (auto const& i: _fonts) {
709 out[i.load_id] = i.data;
715 map<string, boost::filesystem::path>
716 SubtitleAsset::font_filenames () const
718 map<string, boost::filesystem::path> out;
719 for (auto const& i: _fonts) {
721 out[i.load_id] = *i.file;
728 /** Replace empty IDs in any <LoadFontId> and <Font> tags with
729 * a dummy string. Some systems give errors with empty font IDs
730 * (see DCP-o-matic bug #1689).
733 SubtitleAsset::fix_empty_font_ids ()
735 bool have_empty = false;
737 for (auto i: load_font_nodes()) {
741 ids.push_back (i->id);
749 string const empty_id = unique_string (ids, "font");
751 for (auto i: load_font_nodes()) {
757 for (auto i: _subtitles) {
758 auto j = dynamic_pointer_cast<SubtitleString> (i);
759 if (j && j->font() && j->font().get() == "") {
760 j->set_font (empty_id);