libdcp
text_asset.cc
Go to the documentation of this file.
1 /*
2  Copyright (C) 2012-2021 Carl Hetherington <cth@carlh.net>
3 
4  This file is part of libdcp.
5 
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.
10 
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.
15 
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/>.
18 
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
23  including the two.
24 
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.
32 */
33 
34 
40 #include "compose.hpp"
41 #include "dcp_assert.h"
42 #include "load_font_node.h"
43 #include "raw_convert.h"
44 #include "reel_asset.h"
45 #include "text_image.h"
46 #include "text_string.h"
47 #include "text_asset.h"
48 #include "text_asset_internal.h"
49 #include "util.h"
50 #include "xml.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>
57 #include <algorithm>
58 
59 
60 using std::cerr;
61 using std::cout;
62 using std::dynamic_pointer_cast;
63 using std::make_shared;
64 using std::map;
65 using std::pair;
66 using std::shared_ptr;
67 using std::string;
68 using std::vector;
69 using boost::lexical_cast;
70 using boost::optional;
71 using namespace dcp;
72 
73 
74 TextAsset::TextAsset()
75 {
76 
77 }
78 
79 
80 TextAsset::TextAsset(boost::filesystem::path file)
81  : Asset (file)
82 {
83 
84 }
85 
86 
87 string
88 string_attribute (xmlpp::Element const * node, string name)
89 {
90  auto a = node->get_attribute (name);
91  if (!a) {
92  throw XMLError (String::compose ("missing attribute %1", name));
93  }
94  return string (a->get_value ());
95 }
96 
97 
98 optional<string>
99 optional_string_attribute (xmlpp::Element const * node, string name)
100 {
101  auto a = node->get_attribute (name);
102  if (!a) {
103  return {};
104  }
105  return string (a->get_value ());
106 }
107 
108 
109 optional<bool>
110 optional_bool_attribute (xmlpp::Element const * node, string name)
111 {
112  auto s = optional_string_attribute (node, name);
113  if (!s) {
114  return {};
115  }
116 
117  return (s.get() == "1" || s.get() == "yes");
118 }
119 
120 
121 template <class T>
122 optional<T>
123 optional_number_attribute (xmlpp::Element const * node, string name)
124 {
125  auto s = optional_string_attribute (node, name);
126  if (!s) {
127  return boost::optional<T> ();
128  }
129 
130  std::string t = s.get ();
131  boost::erase_all (t, " ");
132  return raw_convert<T> (t);
133 }
134 
135 
136 TextAsset::ParseState
137 TextAsset::font_node_state(xmlpp::Element const * node, Standard standard) const
138 {
139  ParseState ps;
140 
141  if (standard == Standard::INTEROP) {
142  ps.font_id = optional_string_attribute (node, "Id");
143  } else {
144  ps.font_id = optional_string_attribute (node, "ID");
145  }
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");
152  } else {
153  ps.underline = optional_bool_attribute (node, "Underline");
154  }
155  auto c = optional_string_attribute (node, "Color");
156  if (c) {
157  ps.colour = Colour (c.get ());
158  }
159  auto const e = optional_string_attribute (node, "Effect");
160  if (e) {
161  ps.effect = string_to_effect (e.get ());
162  }
163  c = optional_string_attribute (node, "EffectColor");
164  if (c) {
165  ps.effect_colour = Colour (c.get ());
166  }
167 
168  return ps;
169 }
170 
171 void
172 TextAsset::position_align(TextAsset::ParseState& ps, xmlpp::Element const * node) const
173 {
174  auto hp = optional_number_attribute<float> (node, "HPosition");
175  if (!hp) {
176  hp = optional_number_attribute<float> (node, "Hposition");
177  }
178  if (hp) {
179  ps.h_position = hp.get () / 100;
180  }
181 
182  auto ha = optional_string_attribute (node, "HAlign");
183  if (!ha) {
184  ha = optional_string_attribute (node, "Halign");
185  }
186  if (ha) {
187  ps.h_align = string_to_halign (ha.get ());
188  }
189 
190  auto vp = optional_number_attribute<float> (node, "VPosition");
191  if (!vp) {
192  vp = optional_number_attribute<float> (node, "Vposition");
193  }
194  if (vp) {
195  ps.v_position = vp.get () / 100;
196  }
197 
198  auto va = optional_string_attribute (node, "VAlign");
199  if (!va) {
200  va = optional_string_attribute (node, "Valign");
201  }
202  if (va) {
203  ps.v_align = string_to_valign (va.get ());
204  }
205 
206  auto zp = optional_number_attribute<float>(node, "Zposition");
207  if (zp) {
208  ps.z_position = zp.get() / 100;
209  }
210 }
211 
212 
213 TextAsset::ParseState
214 TextAsset::text_node_state(xmlpp::Element const * node) const
215 {
216  ParseState ps;
217 
218  position_align (ps, node);
219 
220  auto d = optional_string_attribute (node, "Direction");
221  if (d) {
222  ps.direction = string_to_direction (d.get ());
223  }
224 
225  ps.type = ParseState::Type::TEXT;
226 
227  return ps;
228 }
229 
230 
231 TextAsset::ParseState
232 TextAsset::image_node_state(xmlpp::Element const * node) const
233 {
234  ParseState ps;
235 
236  position_align (ps, node);
237 
238  ps.type = ParseState::Type::IMAGE;
239 
240  return ps;
241 }
242 
243 
244 TextAsset::ParseState
245 TextAsset::subtitle_node_state(xmlpp::Element const * node, optional<int> tcr) const
246 {
247  ParseState ps;
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);
252  return ps;
253 }
254 
255 
256 Time
257 TextAsset::fade_time(xmlpp::Element const * node, string name, optional<int> tcr) const
258 {
259  auto const u = optional_string_attribute(node, name).get_value_or ("");
260  Time t;
261 
262  if (u.empty ()) {
263  t = Time (0, 0, 0, 20, 250);
264  } else if (u.find (":") != string::npos) {
265  t = Time (u, tcr);
266  } else {
267  t = Time (0, 0, 0, lexical_cast<int> (u), tcr.get_value_or(250));
268  }
269 
270  if (t > Time (0, 0, 8, 0, 250)) {
271  t = Time (0, 0, 8, 0, 250);
272  }
273 
274  return t;
275 }
276 
277 
278 void
279 TextAsset::parse_texts(xmlpp::Element const * node, vector<ParseState>& state, optional<int> tcr, Standard standard)
280 {
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));
291  } else {
292  throw XMLError ("unexpected node " + node->get_name());
293  }
294 
295  float space_before = 0;
296 
297  /* Collect <Ruby>s first */
298  auto get_text_content = [](xmlpp::Element const* element) {
299  string all_content;
300  for (auto child: element->get_children()) {
301  auto content = dynamic_cast<xmlpp::ContentNode const*>(child);
302  if (content) {
303  all_content += content->get_content();
304  }
305  }
306  return all_content;
307  };
308 
309  vector<Ruby> rubies;
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;
332  } else {
333  DCP_ASSERT(false);
334  }
335  }
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");
339  }
340  }
341  }
342  DCP_ASSERT(base);
343  DCP_ASSERT(annotation);
344  auto ruby = Ruby{*base, *annotation};
345  if (size) {
346  ruby.size = *size;
347  }
348  if (position) {
349  ruby.position = *position;
350  }
351  if (offset) {
352  ruby.offset = *offset;
353  }
354  if (spacing) {
355  ruby.spacing = *spacing;
356  }
357  if (aspect_adjust) {
358  ruby.aspect_adjust = *aspect_adjust;
359  }
360  rubies.push_back(ruby);
361  }
362  }
363 
364  for (auto i: node->get_children()) {
365 
366  /* Handle actual content e.g. text */
367  auto const v = dynamic_cast<xmlpp::ContentNode const *>(i);
368  if (v) {
369  maybe_add_text(v->get_content(), state, space_before, standard, rubies);
370  space_before = 0;
371  }
372 
373  /* Handle other nodes */
374  auto const e = dynamic_cast<xmlpp::Element const *>(i);
375  if (e) {
376  if (e->get_name() == "Space") {
377  if (node->get_name() != "Text") {
378  throw XMLError ("Space node found outside Text");
379  }
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", "");
383  }
384  space_before += raw_convert<float>(size);
385  } else if (e->get_name() != "Ruby") {
386  parse_texts (e, state, tcr, standard);
387  }
388  }
389  }
390 
391  state.pop_back ();
392 }
393 
394 
395 void
396 TextAsset::maybe_add_text(
397  string text,
398  vector<ParseState> const & parse_state,
399  float space_before,
400  Standard standard,
401  vector<Ruby> const& rubies
402  )
403 {
404  auto wanted = [](ParseState const& ps) {
405  return ps.type && (ps.type.get() == ParseState::Type::TEXT || ps.type.get() == ParseState::Type::IMAGE);
406  };
407 
408  if (find_if(parse_state.begin(), parse_state.end(), wanted) == parse_state.end()) {
409  return;
410  }
411 
412  ParseState ps;
413  for (auto const& i: parse_state) {
414  if (i.font_id) {
415  ps.font_id = i.font_id.get();
416  }
417  if (i.size) {
418  ps.size = i.size.get();
419  }
420  if (i.aspect_adjust) {
421  ps.aspect_adjust = i.aspect_adjust.get();
422  }
423  if (i.italic) {
424  ps.italic = i.italic.get();
425  }
426  if (i.bold) {
427  ps.bold = i.bold.get();
428  }
429  if (i.underline) {
430  ps.underline = i.underline.get();
431  }
432  if (i.colour) {
433  ps.colour = i.colour.get();
434  }
435  if (i.effect) {
436  ps.effect = i.effect.get();
437  }
438  if (i.effect_colour) {
439  ps.effect_colour = i.effect_colour.get();
440  }
441  if (i.h_position) {
442  ps.h_position = i.h_position.get();
443  }
444  if (i.h_align) {
445  ps.h_align = i.h_align.get();
446  }
447  if (i.v_position) {
448  ps.v_position = i.v_position.get();
449  }
450  if (i.v_align) {
451  ps.v_align = i.v_align.get();
452  }
453  if (i.z_position) {
454  ps.z_position = i.z_position.get();
455  }
456  if (i.direction) {
457  ps.direction = i.direction.get();
458  }
459  if (i.in) {
460  ps.in = i.in.get();
461  }
462  if (i.out) {
463  ps.out = i.out.get();
464  }
465  if (i.fade_up_time) {
466  ps.fade_up_time = i.fade_up_time.get();
467  }
468  if (i.fade_down_time) {
469  ps.fade_down_time = i.fade_down_time.get();
470  }
471  if (i.type) {
472  ps.type = i.type.get();
473  }
474  }
475 
476  if (!ps.in || !ps.out) {
477  /* We're not in a <Subtitle> node; just ignore this content */
478  return;
479  }
480 
481  DCP_ASSERT (ps.type);
482 
483  switch (ps.type.get()) {
484  case ParseState::Type::TEXT:
485  _texts.push_back (
486  make_shared<TextString>(
487  ps.font_id,
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),
494  ps.in.get(),
495  ps.out.get(),
496  ps.h_position.get_value_or(0),
497  ps.h_align.get_value_or(HAlign::CENTER),
498  ps.v_position.get_value_or(0),
499  ps.v_align.get_value_or(VAlign::CENTER),
500  ps.z_position.get_value_or(0),
501  ps.direction.get_value_or (Direction::LTR),
502  text,
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()),
507  space_before,
508  rubies
509  )
510  );
511  break;
512  case ParseState::Type::IMAGE:
513  {
514  switch (standard) {
515  case Standard::INTEROP:
516  if (text.size() >= 4) {
517  /* Remove file extension */
518  text = text.substr(0, text.size() - 4);
519  }
520  break;
521  case Standard::SMPTE:
522  /* It looks like this urn:uuid: is required, but DoM wasn't expecting it (and not writing it)
523  * until around 2.15.140 so I guess either:
524  * a) it is not (always) used in the field, or
525  * b) nobody noticed / complained.
526  */
527  if (text.substr(0, 9) == "urn:uuid:") {
528  text = text.substr(9);
529  }
530  break;
531  }
532 
533  /* Add a text with no image data and we'll fill that in later */
534  _texts.push_back(
535  make_shared<TextImage>(
536  ArrayData(),
537  text,
538  ps.in.get(),
539  ps.out.get(),
540  ps.h_position.get_value_or(0),
541  ps.h_align.get_value_or(HAlign::CENTER),
542  ps.v_position.get_value_or(0),
543  ps.v_align.get_value_or(VAlign::CENTER),
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())
547  )
548  );
549  break;
550  }
551  }
552 }
553 
554 
555 vector<shared_ptr<const Text>>
556 TextAsset::texts() const
557 {
558  vector<shared_ptr<const Text>> s;
559  for (auto i: _texts) {
560  s.push_back (i);
561  }
562  return s;
563 }
564 
565 
566 vector<shared_ptr<const Text>>
567 TextAsset::texts_during(Time from, Time to, bool starting) const
568 {
569  vector<shared_ptr<const Text>> s;
570  for (auto i: _texts) {
571  if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
572  s.push_back (i);
573  }
574  }
575 
576  return s;
577 }
578 
579 
580 void
581 TextAsset::add(shared_ptr<Text> s)
582 {
583  _texts.push_back(s);
584 }
585 
586 
587 Time
588 TextAsset::latest_text_out() const
589 {
590  Time t;
591  for (auto i: _texts) {
592  if (i->out() > t) {
593  t = i->out ();
594  }
595  }
596 
597  return t;
598 }
599 
600 
601 bool
602 TextAsset::equals(shared_ptr<const Asset> other_asset, EqualityOptions const& options, NoteHandler note) const
603 {
604  if (!Asset::equals (other_asset, options, note)) {
605  return false;
606  }
607 
608  auto other = dynamic_pointer_cast<const TextAsset> (other_asset);
609  if (!other) {
610  return false;
611  }
612 
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()));
615  return false;
616  }
617 
618  auto i = _texts.begin();
619  auto j = other->_texts.begin();
620 
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);
626 
627  if ((string_i && !string_j) || (image_i && !image_j)) {
628  note (NoteType::ERROR, "texts differ: string vs. image");
629  return false;
630  }
631 
632  if (string_i && !string_i->equals(string_j, options, note)) {
633  return false;
634  }
635 
636  if (image_i && !image_i->equals(image_j, options, note)) {
637  return false;
638  }
639 
640  ++i;
641  ++j;
642  }
643 
644  return true;
645 }
646 
647 
649 {
650  bool operator()(shared_ptr<Text> a, shared_ptr<Text> b) {
651  if (a->in() != b->in()) {
652  return a->in() < b->in();
653  }
654  if (a->v_align() == VAlign::BOTTOM) {
655  return a->v_position() > b->v_position();
656  }
657  return a->v_position() < b->v_position();
658  }
659 };
660 
661 
662 void
663 TextAsset::pull_fonts(shared_ptr<order::Part> part)
664 {
665  if (part->children.empty ()) {
666  return;
667  }
668 
669  /* Pull up from children */
670  for (auto i: part->children) {
671  pull_fonts (i);
672  }
673 
674  if (part->parent) {
675  /* Establish the common font features that each of part's children have;
676  these features go into part's font.
677  */
678  part->font = part->children.front()->font;
679  for (auto i: part->children) {
680  part->font.take_intersection (i->font);
681  }
682 
683  /* Remove common values from part's children's fonts */
684  for (auto i: part->children) {
685  i->font.take_difference (part->font);
686  }
687  }
688 
689  /* Merge adjacent children with the same font */
690  auto i = part->children.begin();
691  vector<shared_ptr<order::Part>> merged;
692 
693  while (i != part->children.end()) {
694 
695  if ((*i)->font.empty ()) {
696  merged.push_back (*i);
697  ++i;
698  } else {
699  auto j = i;
700  ++j;
701  while (j != part->children.end() && (*i)->font == (*j)->font) {
702  ++j;
703  }
704  if (std::distance (i, j) == 1) {
705  merged.push_back (*i);
706  ++i;
707  } else {
708  shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
709  for (auto k = i; k != j; ++k) {
710  (*k)->font.clear ();
711  group->children.push_back (*k);
712  }
713  merged.push_back (group);
714  i = j;
715  }
716  }
717  }
718 
719  part->children = merged;
720 }
721 
722 
726 void
727 TextAsset::texts_as_xml(xmlpp::Element* xml_root, int time_code_rate, Standard standard) const
728 {
729  auto sorted = _texts;
730  std::stable_sort(sorted.begin(), sorted.end(), TextSorter());
731 
732  /* Gather our texts into a hierarchy of Subtitle/Text/String objects, writing
733  font information into the bottom level (String) objects.
734  */
735 
736  auto root = make_shared<order::Part>(shared_ptr<order::Part>());
737  shared_ptr<order::Subtitle> subtitle;
738  shared_ptr<order::Text> text;
739 
740  Time last_in;
741  Time last_out;
742  Time last_fade_up_time;
743  Time last_fade_down_time;
744  HAlign last_h_align;
745  float last_h_position;
746  VAlign last_v_align;
747  float last_v_position;
748  float last_z_position;
749  Direction last_direction;
750 
751  for (auto i: sorted) {
752  if (!subtitle ||
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())
757  ) {
758 
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);
761 
762  last_in = i->in ();
763  last_out = i->out ();
764  last_fade_up_time = i->fade_up_time ();
765  last_fade_down_time = i->fade_down_time ();
766  text.reset ();
767  }
768 
769  auto is = dynamic_pointer_cast<TextString>(i);
770  if (is) {
771  if (!text ||
772  last_h_align != is->h_align() ||
773  fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
774  last_v_align != is->v_align() ||
775  fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
776  fabs(last_z_position - is->z_position()) > ALIGN_EPSILON ||
777  last_direction != is->direction()
778  ) {
779  text = make_shared<order::Text>(
780  subtitle,
781  is->h_align(),
782  is->h_position(),
783  is->v_align(),
784  is->v_position(),
785  is->z_position(),
786  is->direction(),
787  is->rubies()
788  );
789  subtitle->children.push_back (text);
790 
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 ();
797  }
798 
799  text->children.push_back (make_shared<order::String>(text, order::Font (is, standard), is->text(), is->space_before()));
800  }
801 
802  if (auto ii = dynamic_pointer_cast<TextImage>(i)) {
803  text.reset ();
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())
806  );
807  }
808  }
809 
810  /* Pull font changes as high up the hierarchy as we can */
811 
812  pull_fonts (root);
813 
814  /* Write XML */
815 
816  order::Context context;
817  context.time_code_rate = time_code_rate;
818  context.standard = standard;
819  context.spot_number = 1;
820 
821  root->write_xml (xml_root, context);
822 }
823 
824 
825 map<string, ArrayData>
826 TextAsset::font_data() const
827 {
828  map<string, ArrayData> out;
829  for (auto const& i: _fonts) {
830  out[i.load_id] = i.data;
831  }
832  return out;
833 }
834 
835 
836 map<string, boost::filesystem::path>
837 TextAsset::font_filenames() const
838 {
839  map<string, boost::filesystem::path> out;
840  for (auto const& i: _fonts) {
841  if (i.file) {
842  out[i.load_id] = *i.file;
843  }
844  }
845  return out;
846 }
847 
848 
853 void
855 {
856  bool have_empty = false;
857  vector<string> ids;
858  for (auto i: load_font_nodes()) {
859  if (i->id == "") {
860  have_empty = true;
861  } else {
862  ids.push_back (i->id);
863  }
864  }
865 
866  if (!have_empty) {
867  return;
868  }
869 
870  string const empty_id = unique_string (ids, "font");
871 
872  for (auto i: load_font_nodes()) {
873  if (i->id == "") {
874  i->id = empty_id;
875  }
876  }
877 
878  for (auto i: _texts) {
879  auto j = dynamic_pointer_cast<TextString>(i);
880  if (j && j->font() && j->font().get() == "") {
881  j->set_font (empty_id);
882  }
883  }
884 }
885 
886 
887 namespace {
888 
889 struct State
890 {
891  int indent;
892  string xml;
893  int disable_formatting;
894 };
895 
896 }
897 
898 
899 static
900 void
901 format_xml_node (xmlpp::Node const* node, State& state)
902 {
903  if (auto text_node = dynamic_cast<const xmlpp::TextNode*>(node)) {
904  string content = text_node->get_content();
905  boost::replace_all(content, "&", "&amp;");
906  boost::replace_all(content, "<", "&lt;");
907  boost::replace_all(content, ">", "&gt;");
908  state.xml += content;
909  } else if (auto element = dynamic_cast<const xmlpp::Element*>(node)) {
910  ++state.indent;
911 
912  auto children = element->get_children();
913  auto const should_disable_formatting =
914  std::any_of(
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";
918 
919  if (!state.disable_formatting) {
920  state.xml += "\n" + string(state.indent * 2, ' ');
921  }
922 
923  state.xml += "<" + element->get_name();
924 
925  for (auto attribute: element->get_attributes()) {
926  state.xml += String::compose(" %1=\"%2\"", attribute->get_name().raw(), attribute->get_value().raw());
927  }
928 
929  if (children.empty()) {
930  state.xml += "/>";
931  } else {
932  state.xml += ">";
933 
934  if (should_disable_formatting) {
935  ++state.disable_formatting;
936  }
937 
938  for (auto child: children) {
939  format_xml_node(child, state);
940  }
941 
942  if (!state.disable_formatting) {
943  state.xml += "\n" + string(state.indent * 2, ' ');
944  }
945 
946  state.xml += String::compose("</%1>", element->get_name().raw());
947 
948  if (should_disable_formatting) {
949  --state.disable_formatting;
950  }
951  }
952 
953  --state.indent;
954  }
955 }
956 
957 
966 string
967 TextAsset::format_xml(xmlpp::Document const& document, optional<pair<string, string>> xml_namespace)
968 {
969  auto root = document.get_root_node();
970 
971  State state = {};
972  state.xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<" + root->get_name();
973 
974  if (xml_namespace) {
975  if (xml_namespace->first.empty()) {
976  state.xml += String::compose(" xmlns=\"%1\"", xml_namespace->second);
977  } else {
978  state.xml += String::compose(" xmlns:%1=\"%2\"", xml_namespace->first, xml_namespace->second);
979  }
980  }
981 
982  for (auto attribute: root->get_attributes()) {
983  state.xml += String::compose(" %1=\"%2\"", attribute->get_name().raw(), attribute->get_value().raw());
984  }
985 
986  state.xml += ">";
987 
988  for (auto child: document.get_root_node()->get_children()) {
989  format_xml_node(child, state);
990  }
991 
992  state.xml += String::compose("\n</%1>\n", root->get_name().raw());
993 
994  return state.xml;
995 }
996 
997 
998 void
999 TextAsset::ensure_font(string load_id, dcp::ArrayData data)
1000 {
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);
1003  }
1004 }
1005 
Class to hold an arbitrary block of data.
Definition: array_data.h:55
Parent class for DCP assets, i.e. picture, sound, subtitles, closed captions, CPLs,...
Definition: asset.h:73
An RGB colour.
Definition: types.h:224
A class to describe what "equality" means for a particular test.
Definition: ruby.h:49
void fix_empty_font_ids()
Definition: text_asset.cc:854
void texts_as_xml(xmlpp::Element *root, int time_code_rate, Standard standard) const
Definition: text_asset.cc:727
std::vector< std::shared_ptr< Text > > _texts
Definition: text_asset.h:183
std::vector< Font > _fonts
Definition: text_asset.h:209
static std::string format_xml(xmlpp::Document const &document, boost::optional< std::pair< std::string, std::string >> xml_namespace)
Definition: text_asset.cc:967
A representation of time within a DCP.
Definition: dcp_time.h:73
An XML error.
Definition: exceptions.h:191
DCP_ASSERT macro.
LoadFontNode class.
Namespace for everything in libdcp.
Definition: array_data.h:50
Direction
Definition: types.h:145
@ LTR
left-to-right
std::string unique_string(std::vector< std::string > existing, std::string base)
Definition: util.cc:387
HAlign
Definition: h_align.h:46
@ CENTER
horizontal position is distance from centre of screen to centre of subtitle
VAlign
Definition: v_align.h:46
constexpr float ALIGN_EPSILON
Definition: types.h:272
Methods for conversion to/from string.
ReelAsset class.
TextAsset class.
Internal TextAsset helpers.
TextImage class.
TextString class.
Utility methods and classes.
Helpers for XML reading with libcxml.