40 #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>
62 using std::dynamic_pointer_cast;
63 using std::make_shared;
66 using std::shared_ptr;
69 using boost::lexical_cast;
70 using boost::optional;
74 TextAsset::TextAsset()
80 TextAsset::TextAsset(boost::filesystem::path file)
88 string_attribute (xmlpp::Element
const * node,
string name)
90 auto a = node->get_attribute (name);
92 throw XMLError (String::compose (
"missing attribute %1", name));
94 return string (a->get_value ());
99 optional_string_attribute (xmlpp::Element
const * node,
string name)
101 auto a = node->get_attribute (name);
105 return string (a->get_value ());
110 optional_bool_attribute (xmlpp::Element
const * node,
string name)
112 auto s = optional_string_attribute (node, name);
117 return (s.get() ==
"1" || s.get() ==
"yes");
123 optional_number_attribute (xmlpp::Element
const * node,
string name)
125 auto s = optional_string_attribute (node, name);
127 return boost::optional<T> ();
130 std::string t = s.get ();
131 boost::erase_all (t,
" ");
132 return raw_convert<T> (t);
136 TextAsset::ParseState
137 TextAsset::font_node_state(xmlpp::Element
const * node, Standard standard)
const
141 if (standard == Standard::INTEROP) {
142 ps.font_id = optional_string_attribute (node,
"Id");
144 ps.font_id = optional_string_attribute (node,
"ID");
146 ps.size = optional_number_attribute<int64_t> (node,
"Size");
147 ps.aspect_adjust = optional_number_attribute<float> (node,
"AspectAdjust");
148 ps.italic = optional_bool_attribute (node,
"Italic");
149 ps.bold = optional_string_attribute(node,
"Weight").get_value_or(
"normal") ==
"bold";
150 if (standard == Standard::INTEROP) {
151 ps.underline = optional_bool_attribute (node,
"Underlined");
153 ps.underline = optional_bool_attribute (node,
"Underline");
155 auto c = optional_string_attribute (node,
"Color");
157 ps.colour =
Colour (c.get ());
159 auto const e = optional_string_attribute (node,
"Effect");
161 ps.effect = string_to_effect (e.get ());
163 c = optional_string_attribute (node,
"EffectColor");
165 ps.effect_colour =
Colour (c.get ());
172 TextAsset::position_align(TextAsset::ParseState& ps, xmlpp::Element
const * node)
const
174 auto hp = optional_number_attribute<float> (node,
"HPosition");
176 hp = optional_number_attribute<float> (node,
"Hposition");
179 ps.h_position = hp.get () / 100;
182 auto ha = optional_string_attribute (node,
"HAlign");
184 ha = optional_string_attribute (node,
"Halign");
187 ps.h_align = string_to_halign (ha.get ());
190 auto vp = optional_number_attribute<float> (node,
"VPosition");
192 vp = optional_number_attribute<float> (node,
"Vposition");
195 ps.v_position = vp.get () / 100;
198 auto va = optional_string_attribute (node,
"VAlign");
200 va = optional_string_attribute (node,
"Valign");
203 ps.v_align = string_to_valign (va.get ());
206 auto zp = optional_number_attribute<float>(node,
"Zposition");
208 ps.z_position = zp.get() / 100;
213 TextAsset::ParseState
214 TextAsset::text_node_state(xmlpp::Element
const * node)
const
218 position_align (ps, node);
220 auto d = optional_string_attribute (node,
"Direction");
222 ps.direction = string_to_direction (d.get ());
225 ps.type = ParseState::Type::TEXT;
231 TextAsset::ParseState
232 TextAsset::image_node_state(xmlpp::Element
const * node)
const
236 position_align (ps, node);
238 ps.type = ParseState::Type::IMAGE;
244 TextAsset::ParseState
245 TextAsset::subtitle_node_state(xmlpp::Element
const * node, optional<int> tcr)
const
248 ps.in =
Time (string_attribute(node,
"TimeIn"), tcr);
249 ps.out =
Time (string_attribute(node,
"TimeOut"), tcr);
250 ps.fade_up_time = fade_time (node,
"FadeUpTime", tcr);
251 ps.fade_down_time = fade_time (node,
"FadeDownTime", tcr);
257 TextAsset::fade_time(xmlpp::Element
const * node,
string name, optional<int> tcr)
const
259 auto const u = optional_string_attribute(node, name).get_value_or (
"");
263 t =
Time (0, 0, 0, 20, 250);
264 }
else if (u.find (
":") != string::npos) {
267 t =
Time (0, 0, 0, lexical_cast<int> (u), tcr.get_value_or(250));
270 if (t >
Time (0, 0, 8, 0, 250)) {
271 t =
Time (0, 0, 8, 0, 250);
279 TextAsset::parse_texts(xmlpp::Element
const * node, vector<ParseState>& state, optional<int> tcr, Standard standard)
281 if (node->get_name() ==
"Font") {
282 state.push_back (font_node_state (node, standard));
283 }
else if (node->get_name() ==
"Subtitle") {
284 state.push_back (subtitle_node_state (node, tcr));
285 }
else if (node->get_name() ==
"Text") {
286 state.push_back (text_node_state (node));
287 }
else if (node->get_name() ==
"SubtitleList") {
288 state.push_back (ParseState ());
289 }
else if (node->get_name() ==
"Image") {
290 state.push_back (image_node_state (node));
292 throw XMLError (
"unexpected node " + node->get_name());
295 float space_before = 0;
298 auto get_text_content = [](xmlpp::Element
const* element) {
300 for (
auto child: element->get_children()) {
301 auto content =
dynamic_cast<xmlpp::ContentNode const*
>(child);
303 all_content += content->get_content();
310 for (
auto child: node->get_children()) {
311 auto element =
dynamic_cast<xmlpp::Element const*
>(child);
312 if (element && element->get_name() ==
"Ruby") {
313 optional<string> base;
314 optional<string> annotation;
315 optional<float> size;
316 optional<RubyPosition> position;
317 optional<float> offset;
318 optional<float> spacing;
319 optional<float> aspect_adjust;
320 for (
auto ruby_child: element->get_children()) {
321 if (
auto ruby_element =
dynamic_cast<xmlpp::Element const*
>(ruby_child)) {
322 if (ruby_element->get_name() ==
"Rb") {
323 base = get_text_content(ruby_element);
324 }
else if (ruby_element->get_name() ==
"Rt") {
325 annotation = get_text_content(ruby_element);
326 size = optional_number_attribute<float>(ruby_element,
"Size");
327 if (
auto position_string = optional_string_attribute(ruby_element,
"Position")) {
328 if (*position_string ==
"before") {
329 position = RubyPosition::BEFORE;
330 }
else if (*position_string ==
"after") {
331 position = RubyPosition::AFTER;
336 offset = optional_number_attribute<float>(ruby_element,
"Offset");
337 spacing = optional_number_attribute<float>(ruby_element,
"Spacing");
338 aspect_adjust = optional_number_attribute<float>(ruby_element,
"AspectAdjust");
343 DCP_ASSERT(annotation);
344 auto ruby =
Ruby{*base, *annotation};
349 ruby.position = *position;
352 ruby.offset = *offset;
355 ruby.spacing = *spacing;
358 ruby.aspect_adjust = *aspect_adjust;
360 rubies.push_back(ruby);
364 for (
auto i: node->get_children()) {
367 auto const v =
dynamic_cast<xmlpp::ContentNode
const *
>(i);
369 maybe_add_text(v->get_content(), state, space_before, standard, rubies);
374 auto const e =
dynamic_cast<xmlpp::Element
const *
>(i);
376 if (e->get_name() ==
"Space") {
377 if (node->get_name() !=
"Text") {
378 throw XMLError (
"Space node found outside Text");
380 auto size = optional_string_attribute(e,
"Size").get_value_or(
"0.5");
381 if (standard == dcp::Standard::INTEROP) {
382 boost::replace_all(size,
"em",
"");
384 space_before += raw_convert<float>(size);
385 }
else if (e->get_name() !=
"Ruby") {
386 parse_texts (e, state, tcr, standard);
396 TextAsset::maybe_add_text(
398 vector<ParseState>
const & parse_state,
401 vector<Ruby>
const& rubies
404 auto wanted = [](ParseState
const& ps) {
405 return ps.type && (ps.type.get() == ParseState::Type::TEXT || ps.type.get() == ParseState::Type::IMAGE);
408 if (find_if(parse_state.begin(), parse_state.end(), wanted) == parse_state.end()) {
413 for (
auto const& i: parse_state) {
415 ps.font_id = i.font_id.get();
418 ps.size = i.size.get();
420 if (i.aspect_adjust) {
421 ps.aspect_adjust = i.aspect_adjust.get();
424 ps.italic = i.italic.get();
427 ps.bold = i.bold.get();
430 ps.underline = i.underline.get();
433 ps.colour = i.colour.get();
436 ps.effect = i.effect.get();
438 if (i.effect_colour) {
439 ps.effect_colour = i.effect_colour.get();
442 ps.h_position = i.h_position.get();
445 ps.h_align = i.h_align.get();
448 ps.v_position = i.v_position.get();
451 ps.v_align = i.v_align.get();
454 ps.z_position = i.z_position.get();
457 ps.direction = i.direction.get();
463 ps.out = i.out.get();
465 if (i.fade_up_time) {
466 ps.fade_up_time = i.fade_up_time.get();
468 if (i.fade_down_time) {
469 ps.fade_down_time = i.fade_down_time.get();
472 ps.type = i.type.get();
476 if (!ps.in || !ps.out) {
481 DCP_ASSERT (ps.type);
483 switch (ps.type.get()) {
484 case ParseState::Type::TEXT:
486 make_shared<TextString>(
488 ps.italic.get_value_or (
false),
489 ps.bold.get_value_or (
false),
490 ps.underline.get_value_or (
false),
491 ps.colour.get_value_or (
dcp::Colour (255, 255, 255)),
492 ps.size.get_value_or (42),
493 ps.aspect_adjust.get_value_or (1.0),
496 ps.h_position.get_value_or(0),
498 ps.v_position.get_value_or(0),
500 ps.z_position.get_value_or(0),
503 ps.effect.get_value_or (Effect::NONE),
504 ps.effect_colour.get_value_or (
dcp::Colour (0, 0, 0)),
505 ps.fade_up_time.get_value_or(
Time()),
506 ps.fade_down_time.get_value_or(
Time()),
512 case ParseState::Type::IMAGE:
515 case Standard::INTEROP:
516 if (text.size() >= 4) {
518 text = text.substr(0, text.size() - 4);
521 case Standard::SMPTE:
527 if (text.substr(0, 9) ==
"urn:uuid:") {
528 text = text.substr(9);
535 make_shared<TextImage>(
540 ps.h_position.get_value_or(0),
542 ps.v_position.get_value_or(0),
544 ps.z_position.get_value_or(0),
545 ps.fade_up_time.get_value_or(
Time()),
546 ps.fade_down_time.get_value_or(
Time())
555 vector<shared_ptr<const Text>>
556 TextAsset::texts()
const
558 vector<shared_ptr<const Text>> s;
566 vector<shared_ptr<const Text>>
567 TextAsset::texts_during(
Time from,
Time to,
bool starting)
const
569 vector<shared_ptr<const Text>> s;
571 if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
581 TextAsset::add(shared_ptr<Text> s)
588 TextAsset::latest_text_out()
const
602 TextAsset::equals(shared_ptr<const Asset> other_asset,
EqualityOptions const& options, NoteHandler note)
const
604 if (!Asset::equals (other_asset, options, note)) {
608 auto other = dynamic_pointer_cast<const TextAsset> (other_asset);
613 if (
_texts.size() != other->_texts.size()) {
614 note (NoteType::ERROR, String::compose(
"different number of texts: %1 vs %2",
_texts.size(), other->_texts.size()));
619 auto j = other->_texts.begin();
621 while (i !=
_texts.end()) {
622 auto string_i = dynamic_pointer_cast<TextString>(*i);
623 auto string_j = dynamic_pointer_cast<TextString>(*j);
624 auto image_i = dynamic_pointer_cast<TextImage>(*i);
625 auto image_j = dynamic_pointer_cast<TextImage>(*j);
627 if ((string_i && !string_j) || (image_i && !image_j)) {
628 note (NoteType::ERROR,
"texts differ: string vs. image");
632 if (string_i && !string_i->equals(string_j, options, note)) {
636 if (image_i && !image_i->equals(image_j, options, note)) {
650 bool operator()(shared_ptr<Text> a, shared_ptr<Text> b) {
651 if (a->in() != b->in()) {
652 return a->in() < b->in();
655 return a->v_position() > b->v_position();
657 return a->v_position() < b->v_position();
663 TextAsset::pull_fonts(shared_ptr<order::Part> part)
665 if (part->children.empty ()) {
670 for (
auto i: part->children) {
678 part->font = part->children.front()->font;
679 for (
auto i: part->children) {
680 part->font.take_intersection (i->font);
684 for (
auto i: part->children) {
685 i->font.take_difference (part->font);
690 auto i = part->children.begin();
691 vector<shared_ptr<order::Part>> merged;
693 while (i != part->children.end()) {
695 if ((*i)->font.empty ()) {
696 merged.push_back (*i);
701 while (j != part->children.end() && (*i)->font == (*j)->font) {
704 if (std::distance (i, j) == 1) {
705 merged.push_back (*i);
708 shared_ptr<order::Part> group (
new order::Part (part, (*i)->font));
709 for (
auto k = i; k != j; ++k) {
711 group->children.push_back (*k);
713 merged.push_back (group);
719 part->children = merged;
730 std::stable_sort(sorted.begin(), sorted.end(),
TextSorter());
736 auto root = make_shared<order::Part>(shared_ptr<order::Part>());
737 shared_ptr<order::Subtitle> subtitle;
738 shared_ptr<order::Text> text;
742 Time last_fade_up_time;
743 Time last_fade_down_time;
745 float last_h_position;
747 float last_v_position;
748 float last_z_position;
751 for (
auto i: sorted) {
753 (last_in != i->in() ||
754 last_out != i->out() ||
755 last_fade_up_time != i->fade_up_time() ||
756 last_fade_down_time != i->fade_down_time())
759 subtitle = make_shared<order::Subtitle>(root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time());
760 root->children.push_back (subtitle);
763 last_out = i->out ();
764 last_fade_up_time = i->fade_up_time ();
765 last_fade_down_time = i->fade_down_time ();
769 auto is = dynamic_pointer_cast<TextString>(i);
772 last_h_align != is->h_align() ||
774 last_v_align != is->v_align() ||
777 last_direction != is->direction()
779 text = make_shared<order::Text>(
789 subtitle->children.push_back (text);
791 last_h_align = is->h_align ();
792 last_h_position = is->h_position ();
793 last_v_align = is->v_align ();
794 last_v_position = is->v_position ();
795 last_z_position = is->z_position();
796 last_direction = is->direction ();
799 text->children.push_back (make_shared<order::String>(text,
order::Font (is, standard), is->text(), is->space_before()));
802 if (
auto ii = dynamic_pointer_cast<TextImage>(i)) {
804 subtitle->children.push_back (
805 make_shared<order::Image>(subtitle, ii->id(), ii->png_image(), ii->h_align(), ii->h_position(), ii->v_align(), ii->v_position(), ii->z_position())
817 context.time_code_rate = time_code_rate;
818 context.standard = standard;
819 context.spot_number = 1;
821 root->write_xml (xml_root, context);
825 map<string, ArrayData>
826 TextAsset::font_data()
const
828 map<string, ArrayData> out;
829 for (
auto const& i:
_fonts) {
830 out[i.load_id] = i.data;
836 map<string, boost::filesystem::path>
837 TextAsset::font_filenames()
const
839 map<string, boost::filesystem::path> out;
840 for (
auto const& i:
_fonts) {
842 out[i.load_id] = *i.file;
856 bool have_empty =
false;
858 for (
auto i: load_font_nodes()) {
862 ids.push_back (i->id);
872 for (
auto i: load_font_nodes()) {
879 auto j = dynamic_pointer_cast<TextString>(i);
880 if (j && j->font() && j->font().get() ==
"") {
881 j->set_font (empty_id);
893 int disable_formatting;
901 format_xml_node (xmlpp::Node
const* node, State& state)
903 if (
auto text_node =
dynamic_cast<const xmlpp::TextNode*
>(node)) {
904 string content = text_node->get_content();
905 boost::replace_all(content,
"&",
"&");
906 boost::replace_all(content,
"<",
"<");
907 boost::replace_all(content,
">",
">");
908 state.xml += content;
909 }
else if (
auto element =
dynamic_cast<const xmlpp::Element*
>(node)) {
912 auto children = element->get_children();
913 auto const should_disable_formatting =
915 children.begin(), children.end(),
916 [](xmlpp::Node
const* node) { return static_cast<bool>(dynamic_cast<const xmlpp::ContentNode*>(node)); }
917 ) || element->get_name() ==
"Text";
919 if (!state.disable_formatting) {
920 state.xml +=
"\n" + string(state.indent * 2,
' ');
923 state.xml +=
"<" + element->get_name();
925 for (
auto attribute: element->get_attributes()) {
926 state.xml += String::compose(
" %1=\"%2\"", attribute->get_name().raw(), attribute->get_value().raw());
929 if (children.empty()) {
934 if (should_disable_formatting) {
935 ++state.disable_formatting;
938 for (
auto child: children) {
939 format_xml_node(child, state);
942 if (!state.disable_formatting) {
943 state.xml +=
"\n" + string(state.indent * 2,
' ');
946 state.xml += String::compose(
"</%1>", element->get_name().raw());
948 if (should_disable_formatting) {
949 --state.disable_formatting;
969 auto root = document.get_root_node();
972 state.xml =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<" + root->get_name();
975 if (xml_namespace->first.empty()) {
976 state.xml += String::compose(
" xmlns=\"%1\"", xml_namespace->second);
978 state.xml += String::compose(
" xmlns:%1=\"%2\"", xml_namespace->first, xml_namespace->second);
982 for (
auto attribute: root->get_attributes()) {
983 state.xml += String::compose(
" %1=\"%2\"", attribute->get_name().raw(), attribute->get_value().raw());
988 for (
auto child: document.get_root_node()->get_children()) {
989 format_xml_node(child, state);
992 state.xml += String::compose(
"\n</%1>\n", root->get_name().raw());
1001 if (std::find_if(
_fonts.begin(),
_fonts.end(), [load_id](Font
const& font) { return font.load_id == load_id; }) ==
_fonts.end()) {
1002 add_font(load_id, data);
Class to hold an arbitrary block of data.
Parent class for DCP assets, i.e. picture, sound, subtitles, closed captions, CPLs,...
A class to describe what "equality" means for a particular test.
void fix_empty_font_ids()
void texts_as_xml(xmlpp::Element *root, int time_code_rate, Standard standard) const
std::vector< std::shared_ptr< Text > > _texts
std::vector< Font > _fonts
static std::string format_xml(xmlpp::Document const &document, boost::optional< std::pair< std::string, std::string >> xml_namespace)
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
constexpr float ALIGN_EPSILON
Methods for conversion to/from string.
Internal TextAsset helpers.
Utility methods and classes.
Helpers for XML reading with libcxml.