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.
34 #include "raw_convert.h"
35 #include "compose.hpp"
36 #include "subtitle_asset.h"
37 #include "subtitle_asset_internal.h"
40 #include "subtitle_string.h"
41 #include "subtitle_image.h"
42 #include "dcp_assert.h"
43 #include "load_font_node.h"
44 #include "reel_asset.h"
45 #include <asdcp/AS_DCP.h>
46 #include <asdcp/KM_util.h>
47 #include <libxml++/nodes/element.h>
48 #include <boost/algorithm/string.hpp>
49 #include <boost/lexical_cast.hpp>
50 #include <boost/shared_array.hpp>
52 using std::dynamic_pointer_cast;
57 using std::shared_ptr;
59 using std::make_shared;
60 using boost::shared_array;
61 using boost::optional;
62 using boost::lexical_cast;
65 SubtitleAsset::SubtitleAsset ()
70 SubtitleAsset::SubtitleAsset (boost::filesystem::path file)
77 string_attribute (xmlpp::Element const * node, string name)
79 xmlpp::Attribute* a = node->get_attribute (name);
81 throw XMLError (String::compose ("missing attribute %1", name));
83 return string (a->get_value ());
87 optional_string_attribute (xmlpp::Element const * node, string name)
89 xmlpp::Attribute* a = node->get_attribute (name);
91 return optional<string>();
93 return string (a->get_value ());
97 optional_bool_attribute (xmlpp::Element const * node, string name)
99 optional<string> s = optional_string_attribute (node, name);
101 return optional<bool> ();
104 return (s.get() == "1" || s.get() == "yes");
109 optional_number_attribute (xmlpp::Element const * node, string name)
111 boost::optional<std::string> s = optional_string_attribute (node, name);
113 return boost::optional<T> ();
116 std::string t = s.get ();
117 boost::erase_all (t, " ");
118 return raw_convert<T> (t);
121 SubtitleAsset::ParseState
122 SubtitleAsset::font_node_state (xmlpp::Element const * node, Standard standard) const
126 if (standard == Standard::INTEROP) {
127 ps.font_id = optional_string_attribute (node, "Id");
129 ps.font_id = optional_string_attribute (node, "ID");
131 ps.size = optional_number_attribute<int64_t> (node, "Size");
132 ps.aspect_adjust = optional_number_attribute<float> (node, "AspectAdjust");
133 ps.italic = optional_bool_attribute (node, "Italic");
134 ps.bold = optional_string_attribute(node, "Weight").get_value_or("normal") == "bold";
135 if (standard == Standard::INTEROP) {
136 ps.underline = optional_bool_attribute (node, "Underlined");
138 ps.underline = optional_bool_attribute (node, "Underline");
140 optional<string> c = optional_string_attribute (node, "Color");
142 ps.colour = Colour (c.get ());
144 optional<string> const e = optional_string_attribute (node, "Effect");
146 ps.effect = string_to_effect (e.get ());
148 c = optional_string_attribute (node, "EffectColor");
150 ps.effect_colour = Colour (c.get ());
157 SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, xmlpp::Element const * node) const
159 optional<float> hp = optional_number_attribute<float> (node, "HPosition");
161 hp = optional_number_attribute<float> (node, "Hposition");
164 ps.h_position = hp.get () / 100;
167 optional<string> ha = optional_string_attribute (node, "HAlign");
169 ha = optional_string_attribute (node, "Halign");
172 ps.h_align = string_to_halign (ha.get ());
175 optional<float> vp = optional_number_attribute<float> (node, "VPosition");
177 vp = optional_number_attribute<float> (node, "Vposition");
180 ps.v_position = vp.get () / 100;
183 optional<string> va = optional_string_attribute (node, "VAlign");
185 va = optional_string_attribute (node, "Valign");
188 ps.v_align = string_to_valign (va.get ());
193 SubtitleAsset::ParseState
194 SubtitleAsset::text_node_state (xmlpp::Element const * node) const
198 position_align (ps, node);
200 optional<string> d = optional_string_attribute (node, "Direction");
202 ps.direction = string_to_direction (d.get ());
205 ps.type = ParseState::Type::TEXT;
210 SubtitleAsset::ParseState
211 SubtitleAsset::image_node_state (xmlpp::Element const * node) const
215 position_align (ps, node);
217 ps.type = ParseState::Type::IMAGE;
222 SubtitleAsset::ParseState
223 SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional<int> tcr) const
226 ps.in = Time (string_attribute(node, "TimeIn"), tcr);
227 ps.out = Time (string_attribute(node, "TimeOut"), tcr);
228 ps.fade_up_time = fade_time (node, "FadeUpTime", tcr);
229 ps.fade_down_time = fade_time (node, "FadeDownTime", tcr);
234 SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional<int> tcr) const
236 string const u = optional_string_attribute(node, name).get_value_or ("");
240 t = Time (0, 0, 0, 20, 250);
241 } else if (u.find (":") != string::npos) {
244 t = Time (0, 0, 0, lexical_cast<int> (u), tcr.get_value_or(250));
247 if (t > Time (0, 0, 8, 0, 250)) {
248 t = Time (0, 0, 8, 0, 250);
255 SubtitleAsset::parse_subtitles (xmlpp::Element const * node, vector<ParseState>& state, optional<int> tcr, Standard standard)
257 if (node->get_name() == "Font") {
258 state.push_back (font_node_state (node, standard));
259 } else if (node->get_name() == "Subtitle") {
260 state.push_back (subtitle_node_state (node, tcr));
261 } else if (node->get_name() == "Text") {
262 state.push_back (text_node_state (node));
263 } else if (node->get_name() == "SubtitleList") {
264 state.push_back (ParseState ());
265 } else if (node->get_name() == "Image") {
266 state.push_back (image_node_state (node));
268 throw XMLError ("unexpected node " + node->get_name());
271 xmlpp::Node::NodeList c = node->get_children ();
272 for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
273 xmlpp::ContentNode const * v = dynamic_cast<xmlpp::ContentNode const *> (*i);
275 maybe_add_subtitle (v->get_content(), state, standard);
277 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
279 parse_subtitles (e, state, tcr, standard);
287 SubtitleAsset::maybe_add_subtitle (string text, vector<ParseState> const & parse_state, Standard standard)
289 if (empty_or_white_space (text)) {
294 for (auto const& i: parse_state) {
296 ps.font_id = i.font_id.get();
299 ps.size = i.size.get();
301 if (i.aspect_adjust) {
302 ps.aspect_adjust = i.aspect_adjust.get();
305 ps.italic = i.italic.get();
308 ps.bold = i.bold.get();
311 ps.underline = i.underline.get();
314 ps.colour = i.colour.get();
317 ps.effect = i.effect.get();
319 if (i.effect_colour) {
320 ps.effect_colour = i.effect_colour.get();
323 ps.h_position = i.h_position.get();
326 ps.h_align = i.h_align.get();
329 ps.v_position = i.v_position.get();
332 ps.v_align = i.v_align.get();
335 ps.direction = i.direction.get();
341 ps.out = i.out.get();
343 if (i.fade_up_time) {
344 ps.fade_up_time = i.fade_up_time.get();
346 if (i.fade_down_time) {
347 ps.fade_down_time = i.fade_down_time.get();
350 ps.type = i.type.get();
354 if (!ps.in || !ps.out) {
355 /* We're not in a <Subtitle> node; just ignore this content */
359 DCP_ASSERT (ps.type);
361 switch (ps.type.get()) {
362 case ParseState::Type::TEXT:
363 _subtitles.push_back (
364 shared_ptr<Subtitle> (
367 ps.italic.get_value_or (false),
368 ps.bold.get_value_or (false),
369 ps.underline.get_value_or (false),
370 ps.colour.get_value_or (dcp::Colour (255, 255, 255)),
371 ps.size.get_value_or (42),
372 ps.aspect_adjust.get_value_or (1.0),
375 ps.h_position.get_value_or(0),
376 ps.h_align.get_value_or(HAlign::CENTER),
377 ps.v_position.get_value_or(0),
378 ps.v_align.get_value_or(VAlign::CENTER),
379 ps.direction.get_value_or (Direction::LTR),
381 ps.effect.get_value_or (Effect::NONE),
382 ps.effect_colour.get_value_or (dcp::Colour (0, 0, 0)),
383 ps.fade_up_time.get_value_or(Time()),
384 ps.fade_down_time.get_value_or(Time())
389 case ParseState::Type::IMAGE:
390 /* Add a subtitle with no image data and we'll fill that in later */
391 _subtitles.push_back (
392 shared_ptr<Subtitle> (
395 standard == Standard::INTEROP ? text.substr(0, text.size() - 4) : text,
398 ps.h_position.get_value_or(0),
399 ps.h_align.get_value_or(HAlign::CENTER),
400 ps.v_position.get_value_or(0),
401 ps.v_align.get_value_or(VAlign::CENTER),
402 ps.fade_up_time.get_value_or(Time()),
403 ps.fade_down_time.get_value_or(Time())
412 vector<shared_ptr<const Subtitle>>
413 SubtitleAsset::subtitles () const
415 vector<shared_ptr<const Subtitle>> s;
416 for (auto i: _subtitles) {
423 vector<shared_ptr<const Subtitle>>
424 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
426 vector<shared_ptr<const Subtitle>> s;
427 for (auto i: _subtitles) {
428 if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
437 /* XXX: this needs a test */
438 vector<shared_ptr<const Subtitle>>
439 SubtitleAsset::subtitles_in_reel (shared_ptr<const dcp::ReelAsset> asset) const
441 auto frame_rate = asset->edit_rate().as_float();
442 auto start = dcp::Time(asset->entry_point().get_value_or(0), frame_rate, time_code_rate());
443 auto during = subtitles_during (start, start + dcp::Time(asset->intrinsic_duration(), frame_rate, time_code_rate()), false);
445 vector<shared_ptr<const dcp::Subtitle>> corrected;
446 for (auto i: during) {
447 auto c = make_shared<dcp::Subtitle>(*i);
448 c->set_in (c->in() - start);
449 c->set_out (c->out() - start);
450 corrected.push_back (c);
458 SubtitleAsset::add (shared_ptr<Subtitle> s)
460 _subtitles.push_back (s);
464 SubtitleAsset::latest_subtitle_out () const
467 for (auto i: _subtitles) {
477 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
479 if (!Asset::equals (other_asset, options, note)) {
483 auto other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
488 if (_subtitles.size() != other->_subtitles.size()) {
489 note (NoteType::ERROR, String::compose("different number of subtitles: %1 vs %2", _subtitles.size(), other->_subtitles.size()));
493 auto i = _subtitles.begin();
494 auto j = other->_subtitles.begin();
496 while (i != _subtitles.end()) {
497 shared_ptr<SubtitleString> string_i = dynamic_pointer_cast<SubtitleString> (*i);
498 shared_ptr<SubtitleString> string_j = dynamic_pointer_cast<SubtitleString> (*j);
499 shared_ptr<SubtitleImage> image_i = dynamic_pointer_cast<SubtitleImage> (*i);
500 shared_ptr<SubtitleImage> image_j = dynamic_pointer_cast<SubtitleImage> (*j);
502 if ((string_i && !string_j) || (image_i && !image_j)) {
503 note (NoteType::ERROR, "subtitles differ: string vs. image");
507 if (string_i && *string_i != *string_j) {
508 note (NoteType::ERROR, String::compose("subtitles differ in text or metadata: %1 vs %2", string_i->text(), string_j->text()));
512 if (image_i && !image_i->equals(image_j, options, note)) {
523 struct SubtitleSorter
525 bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
526 if (a->in() != b->in()) {
527 return a->in() < b->in();
529 return a->v_position() < b->v_position();
534 SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
536 if (part->children.empty ()) {
540 /* Pull up from children */
541 for (auto i: part->children) {
546 /* Establish the common font features that each of part's children have;
547 these features go into part's font.
549 part->font = part->children.front()->font;
550 for (auto i: part->children) {
551 part->font.take_intersection (i->font);
554 /* Remove common values from part's children's fonts */
555 for (auto i: part->children) {
556 i->font.take_difference (part->font);
560 /* Merge adjacent children with the same font */
561 auto i = part->children.begin();
562 vector<shared_ptr<order::Part>> merged;
564 while (i != part->children.end()) {
566 if ((*i)->font.empty ()) {
567 merged.push_back (*i);
572 while (j != part->children.end() && (*i)->font == (*j)->font) {
575 if (std::distance (i, j) == 1) {
576 merged.push_back (*i);
579 shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
580 for (auto k = i; k != j; ++k) {
582 group->children.push_back (*k);
584 merged.push_back (group);
590 part->children = merged;
593 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
594 * class because the differences between the two are fairly subtle.
597 SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, Standard standard) const
599 auto sorted = _subtitles;
600 std::stable_sort(sorted.begin(), sorted.end(), SubtitleSorter());
602 /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
603 font information into the bottom level (String) objects.
606 auto root = make_shared<order::Part>(shared_ptr<order::Part>());
607 shared_ptr<order::Subtitle> subtitle;
608 shared_ptr<order::Text> text;
612 Time last_fade_up_time;
613 Time last_fade_down_time;
615 float last_h_position;
617 float last_v_position;
618 Direction last_direction;
620 for (auto i: sorted) {
622 (last_in != i->in() ||
623 last_out != i->out() ||
624 last_fade_up_time != i->fade_up_time() ||
625 last_fade_down_time != i->fade_down_time())
628 subtitle = make_shared<order::Subtitle>(root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time());
629 root->children.push_back (subtitle);
632 last_out = i->out ();
633 last_fade_up_time = i->fade_up_time ();
634 last_fade_down_time = i->fade_down_time ();
638 auto is = dynamic_pointer_cast<SubtitleString>(i);
641 last_h_align != is->h_align() ||
642 fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
643 last_v_align != is->v_align() ||
644 fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
645 last_direction != is->direction()
647 text.reset (new order::Text (subtitle, is->h_align(), is->h_position(), is->v_align(), is->v_position(), is->direction()));
648 subtitle->children.push_back (text);
650 last_h_align = is->h_align ();
651 last_h_position = is->h_position ();
652 last_v_align = is->v_align ();
653 last_v_position = is->v_position ();
654 last_direction = is->direction ();
657 text->children.push_back (shared_ptr<order::String> (new order::String (text, order::Font (is, standard), is->text())));
660 shared_ptr<SubtitleImage> ii = dynamic_pointer_cast<SubtitleImage>(i);
663 subtitle->children.push_back (
664 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()))
669 /* Pull font changes as high up the hierarchy as we can */
675 order::Context context;
676 context.time_code_rate = time_code_rate;
677 context.standard = standard;
678 context.spot_number = 1;
680 root->write_xml (xml_root, context);
683 map<string, ArrayData>
684 SubtitleAsset::font_data () const
686 map<string, ArrayData> out;
687 for (auto const& i: _fonts) {
688 out[i.load_id] = i.data;
694 map<string, boost::filesystem::path>
695 SubtitleAsset::font_filenames () const
697 map<string, boost::filesystem::path> out;
698 for (auto const& i: _fonts) {
700 out[i.load_id] = *i.file;
707 /** Replace empty IDs in any <LoadFontId> and <Font> tags with
708 * a dummy string. Some systems give errors with empty font IDs
709 * (see DCP-o-matic bug #1689).
712 SubtitleAsset::fix_empty_font_ids ()
714 bool have_empty = false;
716 for (auto i: load_font_nodes()) {
720 ids.push_back (i->id);
728 string const empty_id = unique_string (ids, "font");
730 for (auto i: load_font_nodes()) {
736 for (auto i: _subtitles) {
737 auto j = dynamic_pointer_cast<SubtitleString> (i);
738 if (j && j->font() && j->font().get() == "") {
739 j->set_font (empty_id);