libdcp
subtitle_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 "raw_convert.h"
41 #include "compose.hpp"
42 #include "subtitle_asset.h"
44 #include "util.h"
45 #include "xml.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>
57 
58 
59 using std::dynamic_pointer_cast;
60 using std::string;
61 using std::cout;
62 using std::cerr;
63 using std::map;
64 using std::shared_ptr;
65 using std::vector;
66 using std::make_shared;
67 using boost::optional;
68 using boost::lexical_cast;
69 using namespace dcp;
70 
71 
72 SubtitleAsset::SubtitleAsset ()
73 {
74 
75 }
76 
77 
78 SubtitleAsset::SubtitleAsset (boost::filesystem::path file)
79  : Asset (file)
80 {
81 
82 }
83 
84 
85 string
86 string_attribute (xmlpp::Element const * node, string name)
87 {
88  auto a = node->get_attribute (name);
89  if (!a) {
90  throw XMLError (String::compose ("missing attribute %1", name));
91  }
92  return string (a->get_value ());
93 }
94 
95 
96 optional<string>
97 optional_string_attribute (xmlpp::Element const * node, string name)
98 {
99  auto a = node->get_attribute (name);
100  if (!a) {
101  return {};
102  }
103  return string (a->get_value ());
104 }
105 
106 
107 optional<bool>
108 optional_bool_attribute (xmlpp::Element const * node, string name)
109 {
110  auto s = optional_string_attribute (node, name);
111  if (!s) {
112  return {};
113  }
114 
115  return (s.get() == "1" || s.get() == "yes");
116 }
117 
118 
119 template <class T>
120 optional<T>
121 optional_number_attribute (xmlpp::Element const * node, string name)
122 {
123  auto s = optional_string_attribute (node, name);
124  if (!s) {
125  return boost::optional<T> ();
126  }
127 
128  std::string t = s.get ();
129  boost::erase_all (t, " ");
130  return raw_convert<T> (t);
131 }
132 
133 
134 SubtitleAsset::ParseState
135 SubtitleAsset::font_node_state (xmlpp::Element const * node, Standard standard) const
136 {
137  ParseState ps;
138 
139  if (standard == Standard::INTEROP) {
140  ps.font_id = optional_string_attribute (node, "Id");
141  } else {
142  ps.font_id = optional_string_attribute (node, "ID");
143  }
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");
150  } else {
151  ps.underline = optional_bool_attribute (node, "Underline");
152  }
153  auto c = optional_string_attribute (node, "Color");
154  if (c) {
155  ps.colour = Colour (c.get ());
156  }
157  auto const e = optional_string_attribute (node, "Effect");
158  if (e) {
159  ps.effect = string_to_effect (e.get ());
160  }
161  c = optional_string_attribute (node, "EffectColor");
162  if (c) {
163  ps.effect_colour = Colour (c.get ());
164  }
165 
166  return ps;
167 }
168 
169 void
170 SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, xmlpp::Element const * node) const
171 {
172  auto hp = optional_number_attribute<float> (node, "HPosition");
173  if (!hp) {
174  hp = optional_number_attribute<float> (node, "Hposition");
175  }
176  if (hp) {
177  ps.h_position = hp.get () / 100;
178  }
179 
180  auto ha = optional_string_attribute (node, "HAlign");
181  if (!ha) {
182  ha = optional_string_attribute (node, "Halign");
183  }
184  if (ha) {
185  ps.h_align = string_to_halign (ha.get ());
186  }
187 
188  auto vp = optional_number_attribute<float> (node, "VPosition");
189  if (!vp) {
190  vp = optional_number_attribute<float> (node, "Vposition");
191  }
192  if (vp) {
193  ps.v_position = vp.get () / 100;
194  }
195 
196  auto va = optional_string_attribute (node, "VAlign");
197  if (!va) {
198  va = optional_string_attribute (node, "Valign");
199  }
200  if (va) {
201  ps.v_align = string_to_valign (va.get ());
202  }
203 
204 }
205 
206 
207 SubtitleAsset::ParseState
208 SubtitleAsset::text_node_state (xmlpp::Element const * node) const
209 {
210  ParseState ps;
211 
212  position_align (ps, node);
213 
214  auto d = optional_string_attribute (node, "Direction");
215  if (d) {
216  ps.direction = string_to_direction (d.get ());
217  }
218 
219  ps.type = ParseState::Type::TEXT;
220 
221  return ps;
222 }
223 
224 
225 SubtitleAsset::ParseState
226 SubtitleAsset::image_node_state (xmlpp::Element const * node) const
227 {
228  ParseState ps;
229 
230  position_align (ps, node);
231 
232  ps.type = ParseState::Type::IMAGE;
233 
234  return ps;
235 }
236 
237 
238 SubtitleAsset::ParseState
239 SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional<int> tcr) const
240 {
241  ParseState ps;
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);
246  return ps;
247 }
248 
249 
250 Time
251 SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional<int> tcr) const
252 {
253  auto const u = optional_string_attribute(node, name).get_value_or ("");
254  Time t;
255 
256  if (u.empty ()) {
257  t = Time (0, 0, 0, 20, 250);
258  } else if (u.find (":") != string::npos) {
259  t = Time (u, tcr);
260  } else {
261  t = Time (0, 0, 0, lexical_cast<int> (u), tcr.get_value_or(250));
262  }
263 
264  if (t > Time (0, 0, 8, 0, 250)) {
265  t = Time (0, 0, 8, 0, 250);
266  }
267 
268  return t;
269 }
270 
271 
272 void
273 SubtitleAsset::parse_subtitles (xmlpp::Element const * node, vector<ParseState>& state, optional<int> tcr, Standard standard)
274 {
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));
285  } else {
286  throw XMLError ("unexpected node " + node->get_name());
287  }
288 
289  float space_before = 0;
290 
291  for (auto i: node->get_children()) {
292  auto const v = dynamic_cast<xmlpp::ContentNode const *>(i);
293  if (v) {
294  maybe_add_subtitle (v->get_content(), state, space_before, standard);
295  space_before = 0;
296  }
297  auto const e = dynamic_cast<xmlpp::Element const *>(i);
298  if (e) {
299  if (e->get_name() == "Space") {
300  if (node->get_name() != "Text") {
301  throw XMLError ("Space node found outside Text");
302  }
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", "");
306  }
307  space_before += raw_convert<float>(size);
308  } else {
309  parse_subtitles (e, state, tcr, standard);
310  }
311  }
312  }
313 
314  state.pop_back ();
315 }
316 
317 
318 void
319 SubtitleAsset::maybe_add_subtitle (string text, vector<ParseState> const & parse_state, float space_before, Standard standard)
320 {
321  if (empty_or_white_space (text)) {
322  return;
323  }
324 
325  ParseState ps;
326  for (auto const& i: parse_state) {
327  if (i.font_id) {
328  ps.font_id = i.font_id.get();
329  }
330  if (i.size) {
331  ps.size = i.size.get();
332  }
333  if (i.aspect_adjust) {
334  ps.aspect_adjust = i.aspect_adjust.get();
335  }
336  if (i.italic) {
337  ps.italic = i.italic.get();
338  }
339  if (i.bold) {
340  ps.bold = i.bold.get();
341  }
342  if (i.underline) {
343  ps.underline = i.underline.get();
344  }
345  if (i.colour) {
346  ps.colour = i.colour.get();
347  }
348  if (i.effect) {
349  ps.effect = i.effect.get();
350  }
351  if (i.effect_colour) {
352  ps.effect_colour = i.effect_colour.get();
353  }
354  if (i.h_position) {
355  ps.h_position = i.h_position.get();
356  }
357  if (i.h_align) {
358  ps.h_align = i.h_align.get();
359  }
360  if (i.v_position) {
361  ps.v_position = i.v_position.get();
362  }
363  if (i.v_align) {
364  ps.v_align = i.v_align.get();
365  }
366  if (i.direction) {
367  ps.direction = i.direction.get();
368  }
369  if (i.in) {
370  ps.in = i.in.get();
371  }
372  if (i.out) {
373  ps.out = i.out.get();
374  }
375  if (i.fade_up_time) {
376  ps.fade_up_time = i.fade_up_time.get();
377  }
378  if (i.fade_down_time) {
379  ps.fade_down_time = i.fade_down_time.get();
380  }
381  if (i.type) {
382  ps.type = i.type.get();
383  }
384  }
385 
386  if (!ps.in || !ps.out) {
387  /* We're not in a <Subtitle> node; just ignore this content */
388  return;
389  }
390 
391  DCP_ASSERT (ps.type);
392 
393  switch (ps.type.get()) {
394  case ParseState::Type::TEXT:
395  _subtitles.push_back (
396  make_shared<SubtitleString>(
397  ps.font_id,
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),
404  ps.in.get(),
405  ps.out.get(),
406  ps.h_position.get_value_or(0),
407  ps.h_align.get_value_or(HAlign::CENTER),
408  ps.v_position.get_value_or(0),
409  ps.v_align.get_value_or(VAlign::CENTER),
410  ps.direction.get_value_or (Direction::LTR),
411  text,
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()),
416  space_before
417  )
418  );
419  break;
420  case ParseState::Type::IMAGE:
421  {
422  switch (standard) {
423  case Standard::INTEROP:
424  if (text.size() >= 4) {
425  /* Remove file extension */
426  text = text.substr(0, text.size() - 4);
427  }
428  break;
429  case Standard::SMPTE:
430  /* It looks like this urn:uuid: is required, but DoM wasn't expecting it (and not writing it)
431  * until around 2.15.140 so I guess either:
432  * a) it is not (always) used in the field, or
433  * b) nobody noticed / complained.
434  */
435  if (text.substr(0, 9) == "urn:uuid:") {
436  text = text.substr(9);
437  }
438  break;
439  }
440 
441  /* Add a subtitle with no image data and we'll fill that in later */
442  _subtitles.push_back (
443  make_shared<SubtitleImage>(
444  ArrayData(),
445  text,
446  ps.in.get(),
447  ps.out.get(),
448  ps.h_position.get_value_or(0),
449  ps.h_align.get_value_or(HAlign::CENTER),
450  ps.v_position.get_value_or(0),
451  ps.v_align.get_value_or(VAlign::CENTER),
452  ps.fade_up_time.get_value_or(Time()),
453  ps.fade_down_time.get_value_or(Time())
454  )
455  );
456  break;
457  }
458  }
459 }
460 
461 
462 vector<shared_ptr<const Subtitle>>
463 SubtitleAsset::subtitles () const
464 {
465  vector<shared_ptr<const Subtitle>> s;
466  for (auto i: _subtitles) {
467  s.push_back (i);
468  }
469  return s;
470 }
471 
472 
473 vector<shared_ptr<const Subtitle>>
474 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
475 {
476  vector<shared_ptr<const Subtitle>> s;
477  for (auto i: _subtitles) {
478  if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
479  s.push_back (i);
480  }
481  }
482 
483  return s;
484 }
485 
486 
487 /* XXX: this needs a test */
488 vector<shared_ptr<const Subtitle>>
489 SubtitleAsset::subtitles_in_reel (shared_ptr<const dcp::ReelAsset> asset) const
490 {
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);
494 
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);
501  }
502 
503  return corrected;
504 }
505 
506 
507 void
508 SubtitleAsset::add (shared_ptr<Subtitle> s)
509 {
510  _subtitles.push_back (s);
511 }
512 
513 
514 Time
515 SubtitleAsset::latest_subtitle_out () const
516 {
517  Time t;
518  for (auto i: _subtitles) {
519  if (i->out() > t) {
520  t = i->out ();
521  }
522  }
523 
524  return t;
525 }
526 
527 
528 bool
529 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
530 {
531  if (!Asset::equals (other_asset, options, note)) {
532  return false;
533  }
534 
535  auto other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
536  if (!other) {
537  return false;
538  }
539 
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()));
542  return false;
543  }
544 
545  auto i = _subtitles.begin();
546  auto j = other->_subtitles.begin();
547 
548  while (i != _subtitles.end()) {
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);
553 
554  if ((string_i && !string_j) || (image_i && !image_j)) {
555  note (NoteType::ERROR, "subtitles differ: string vs. image");
556  return false;
557  }
558 
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()));
561  return false;
562  }
563 
564  if (image_i && !image_i->equals(image_j, options, note)) {
565  return false;
566  }
567 
568  ++i;
569  ++j;
570  }
571 
572  return true;
573 }
574 
575 
576 struct SubtitleSorter
577 {
578  bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
579  if (a->in() != b->in()) {
580  return a->in() < b->in();
581  }
582  if (a->v_align() == VAlign::BOTTOM) {
583  return a->v_position() > b->v_position();
584  }
585  return a->v_position() < b->v_position();
586  }
587 };
588 
589 
590 void
591 SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
592 {
593  if (part->children.empty ()) {
594  return;
595  }
596 
597  /* Pull up from children */
598  for (auto i: part->children) {
599  pull_fonts (i);
600  }
601 
602  if (part->parent) {
603  /* Establish the common font features that each of part's children have;
604  these features go into part's font.
605  */
606  part->font = part->children.front()->font;
607  for (auto i: part->children) {
608  part->font.take_intersection (i->font);
609  }
610 
611  /* Remove common values from part's children's fonts */
612  for (auto i: part->children) {
613  i->font.take_difference (part->font);
614  }
615  }
616 
617  /* Merge adjacent children with the same font */
618  auto i = part->children.begin();
619  vector<shared_ptr<order::Part>> merged;
620 
621  while (i != part->children.end()) {
622 
623  if ((*i)->font.empty ()) {
624  merged.push_back (*i);
625  ++i;
626  } else {
627  auto j = i;
628  ++j;
629  while (j != part->children.end() && (*i)->font == (*j)->font) {
630  ++j;
631  }
632  if (std::distance (i, j) == 1) {
633  merged.push_back (*i);
634  ++i;
635  } else {
636  shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
637  for (auto k = i; k != j; ++k) {
638  (*k)->font.clear ();
639  group->children.push_back (*k);
640  }
641  merged.push_back (group);
642  i = j;
643  }
644  }
645  }
646 
647  part->children = merged;
648 }
649 
650 
654 void
655 SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, Standard standard) const
656 {
657  auto sorted = _subtitles;
658  std::stable_sort(sorted.begin(), sorted.end(), SubtitleSorter());
659 
660  /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
661  font information into the bottom level (String) objects.
662  */
663 
664  auto root = make_shared<order::Part>(shared_ptr<order::Part>());
665  shared_ptr<order::Subtitle> subtitle;
666  shared_ptr<order::Text> text;
667 
668  Time last_in;
669  Time last_out;
670  Time last_fade_up_time;
671  Time last_fade_down_time;
672  HAlign last_h_align;
673  float last_h_position;
674  VAlign last_v_align;
675  float last_v_position;
676  Direction last_direction;
677 
678  for (auto i: sorted) {
679  if (!subtitle ||
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())
684  ) {
685 
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);
688 
689  last_in = i->in ();
690  last_out = i->out ();
691  last_fade_up_time = i->fade_up_time ();
692  last_fade_down_time = i->fade_down_time ();
693  text.reset ();
694  }
695 
696  auto is = dynamic_pointer_cast<SubtitleString>(i);
697  if (is) {
698  if (!text ||
699  last_h_align != is->h_align() ||
700  fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
701  last_v_align != is->v_align() ||
702  fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
703  last_direction != is->direction()
704  ) {
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);
707 
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 ();
713  }
714 
715  text->children.push_back (make_shared<order::String>(text, order::Font (is, standard), is->text(), is->space_before()));
716  }
717 
718  auto ii = dynamic_pointer_cast<SubtitleImage>(i);
719  if (ii) {
720  text.reset ();
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())
723  );
724  }
725  }
726 
727  /* Pull font changes as high up the hierarchy as we can */
728 
729  pull_fonts (root);
730 
731  /* Write XML */
732 
733  order::Context context;
734  context.time_code_rate = time_code_rate;
735  context.standard = standard;
736  context.spot_number = 1;
737 
738  root->write_xml (xml_root, context);
739 }
740 
741 
742 map<string, ArrayData>
743 SubtitleAsset::font_data () const
744 {
745  map<string, ArrayData> out;
746  for (auto const& i: _fonts) {
747  out[i.load_id] = i.data;
748  }
749  return out;
750 }
751 
752 
753 map<string, boost::filesystem::path>
754 SubtitleAsset::font_filenames () const
755 {
756  map<string, boost::filesystem::path> out;
757  for (auto const& i: _fonts) {
758  if (i.file) {
759  out[i.load_id] = *i.file;
760  }
761  }
762  return out;
763 }
764 
765 
770 void
772 {
773  bool have_empty = false;
774  vector<string> ids;
775  for (auto i: load_font_nodes()) {
776  if (i->id == "") {
777  have_empty = true;
778  } else {
779  ids.push_back (i->id);
780  }
781  }
782 
783  if (!have_empty) {
784  return;
785  }
786 
787  string const empty_id = unique_string (ids, "font");
788 
789  for (auto i: load_font_nodes()) {
790  if (i->id == "") {
791  i->id = empty_id;
792  }
793  }
794 
795  for (auto i: _subtitles) {
796  auto j = dynamic_pointer_cast<SubtitleString> (i);
797  if (j && j->font() && j->font().get() == "") {
798  j->set_font (empty_id);
799  }
800  }
801 }
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:70
An RGB colour.
Definition: types.h:301
std::vector< std::shared_ptr< Subtitle > > _subtitles
std::vector< Font > _fonts
void subtitles_as_xml(xmlpp::Element *root, int time_code_rate, Standard standard) const
A representation of time within a DCP.
Definition: dcp_time.h:73
An XML error.
Definition: exceptions.h:164
DCP_ASSERT macro.
LoadFontNode class.
Namespace for everything in libdcp.
Definition: array_data.h:50
Direction
Definition: types.h:191
@ LTR
left-to-right
std::string unique_string(std::vector< std::string > existing, std::string base)
Definition: util.cc:413
HAlign
Definition: types.h:166
@ CENTER
horizontal position is distance from centre of screen to centre of subtitle
bool empty_or_white_space(std::string s)
Definition: util.cc:161
VAlign
Definition: types.h:178
@ 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
Definition: types.h:349
Methods for conversion to/from string.
ReelAsset class.
A class to describe what "equality" means for a particular test.
Definition: types.h:249
SubtitleAsset class.
Internal SubtitleAsset helpers.
SubtitleImage class.
SubtitleString class.
Utility methods and classes.
Helpers for XML reading with libcxml.