41 #include "compose.hpp"
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)
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) {
391 DCP_ASSERT (ps.type);
393 switch (ps.type.get()) {
394 case ParseState::Type::TEXT:
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),
408 ps.v_position.get_value_or(0),
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) {
426 text = text.substr(0, text.size() - 4);
429 case Standard::SMPTE:
435 if (text.substr(0, 9) ==
"urn:uuid:") {
436 text = text.substr(9);
443 make_shared<SubtitleImage>(
448 ps.h_position.get_value_or(0),
450 ps.v_position.get_value_or(0),
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;
473 vector<shared_ptr<const Subtitle>>
474 SubtitleAsset::subtitles_during (
Time from,
Time to,
bool starting)
const
476 vector<shared_ptr<const Subtitle>> s;
478 if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
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)
515 SubtitleAsset::latest_subtitle_out ()
const
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()));
546 auto j = other->_subtitles.begin();
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();
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 ()) {
598 for (
auto i: part->children) {
606 part->font = part->children.front()->font;
607 for (
auto i: part->children) {
608 part->font.take_intersection (i->font);
612 for (
auto i: part->children) {
613 i->font.take_difference (part->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;
658 std::stable_sort(sorted.begin(), sorted.end(), SubtitleSorter());
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;
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() ||
701 last_v_align != is->v_align() ||
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())
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;
773 bool have_empty =
false;
775 for (
auto i: load_font_nodes()) {
779 ids.push_back (i->id);
789 for (
auto i: load_font_nodes()) {
796 auto j = dynamic_pointer_cast<SubtitleString> (i);
797 if (j && j->font() && j->font().get() ==
"") {
798 j->set_font (empty_id);
Class to hold an arbitrary block of data.
Parent class for DCP assets, i.e. picture, sound, subtitles, closed captions, CPLs,...
std::vector< std::shared_ptr< Subtitle > > _subtitles
std::vector< Font > _fonts
void fix_empty_font_ids()
void subtitles_as_xml(xmlpp::Element *root, int time_code_rate, Standard standard) const
A representation of time within a DCP.
Namespace for everything in libdcp.
std::string unique_string(std::vector< std::string > existing, std::string base)
@ CENTER
horizontal position is distance from centre of screen to centre of subtitle
bool empty_or_white_space(std::string s)
@ BOTTOM
vertical position is distance from bottom of screen to bottom of subtitle
@ CENTER
vertical position is distance from centre of screen to centre of subtitle
constexpr float ALIGN_EPSILON
Methods for conversion to/from string.
A class to describe what "equality" means for a particular test.
Internal SubtitleAsset helpers.
Utility methods and classes.
Helpers for XML reading with libcxml.