Rework subtitle writing to fix incorrect output with
authorCarl Hetherington <cth@carlh.net>
Tue, 20 Sep 2016 22:55:47 +0000 (23:55 +0100)
committerCarl Hetherington <cth@carlh.net>
Thu, 6 Oct 2016 23:02:47 +0000 (00:02 +0100)
in-line font changes (#952).

run/tests
src/object.h
src/subtitle_asset.cc
src/subtitle_asset.h
src/subtitle_asset_internal.cc [new file with mode: 0644]
src/subtitle_asset_internal.h [new file with mode: 0644]
src/wscript
test/test.cc
test/write_subtitle_test.cc

index 4bc3e5f3f763a909b63584865c97d6475a613e19..2ea87bedc214e71fd7e2b7f27dbbbee6b485dbbe 100755 (executable)
--- a/run/tests
+++ b/run/tests
@@ -19,7 +19,7 @@ done
 # Run the unit tests in test/
 if [ "$1" == "--debug" ]; then
     shift
-    gdb --args $work/tests $private
+    gdb --args $work/tests $private $*
 elif [ "$1" == "--valgrind" ]; then
     shift
     valgrind --tool="memcheck" $work/tests $private
index 31fb52751add9b5e73748efc823238298bbbc70f..0804f4cfa1f8e8b3458d0dde40d3fdb9e507d2c5 100644 (file)
@@ -43,6 +43,7 @@
 
 class write_interop_subtitle_test;
 class write_smpte_subtitle_test;
+class write_smpte_subtitle_test2;
 
 namespace dcp {
 
@@ -64,6 +65,7 @@ public:
 protected:
        friend class ::write_interop_subtitle_test;
        friend class ::write_smpte_subtitle_test;
+       friend class ::write_smpte_subtitle_test2;
 
        /** ID */
        std::string _id;
index ac87148dc4cf1e4db7b25ce2a6884b107d183619..19f7fa375e10020faf1ada0d2f736151a47d7e47 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2012-2015 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2016 Carl Hetherington <cth@carlh.net>
 
     This file is part of libdcp.
 
@@ -34,6 +34,7 @@
 #include "raw_convert.h"
 #include "compose.hpp"
 #include "subtitle_asset.h"
+#include "subtitle_asset_internal.h"
 #include "util.h"
 #include "xml.h"
 #include "subtitle_string.h"
@@ -51,6 +52,7 @@ using std::list;
 using std::cout;
 using std::cerr;
 using std::map;
+using std::distance;
 using boost::shared_ptr;
 using boost::shared_array;
 using boost::optional;
@@ -403,7 +405,8 @@ SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions opti
        return true;
 }
 
-struct SubtitleSorter {
+struct SubtitleSorter
+{
        bool operator() (SubtitleString const & a, SubtitleString const & b) {
                if (a.in() != b.in()) {
                        return a.in() < b.in();
@@ -412,165 +415,144 @@ struct SubtitleSorter {
        }
 };
 
+void
+SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
+{
+       if (part->children.empty ()) {
+               return;
+       }
+
+       /* Pull up from children */
+       BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
+               pull_fonts (i);
+       }
+
+       if (part->parent) {
+               /* Establish the common font features that each of part's children have;
+                  these features go into part's font.
+               */
+               part->font = part->children.front()->font;
+               BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
+                       part->font.take_intersection (i->font);
+               }
+
+               /* Remove common values from part's children's fonts */
+               BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
+                       i->font.take_difference (part->font);
+               }
+       }
+
+       /* Merge adjacent children with the same font */
+       list<shared_ptr<order::Part> >::const_iterator i = part->children.begin();
+       list<shared_ptr<order::Part> > merged;
+
+       while (i != part->children.end()) {
+
+               if ((*i)->font.empty ()) {
+                       merged.push_back (*i);
+                       ++i;
+               } else {
+                       list<shared_ptr<order::Part> >::const_iterator j = i;
+                       ++j;
+                       while (j != part->children.end() && (*i)->font == (*j)->font) {
+                               ++j;
+                       }
+                       if (distance (i, j) == 1) {
+                               merged.push_back (*i);
+                               ++i;
+                       } else {
+                               shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
+                               for (list<shared_ptr<order::Part> >::const_iterator k = i; k != j; ++k) {
+                                       (*k)->font.clear ();
+                                       group->children.push_back (*k);
+                               }
+                               merged.push_back (group);
+                               i = j;
+                       }
+               }
+       }
+
+       part->children = merged;
+}
+
 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
  *  class because the differences between the two are fairly subtle.
  */
 void
-SubtitleAsset::subtitles_as_xml (xmlpp::Element* root, int time_code_rate, Standard standard) const
+SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, Standard standard) const
 {
        list<SubtitleString> sorted = _subtitles;
        sorted.sort (SubtitleSorter ());
 
-       string const xmlns = standard == SMPTE ? "dcst" : "";
+       /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
+          font information into the bottom level (String) objects.
+       */
 
-       /* XXX: script not supported */
+       shared_ptr<order::Part> root (new order::Part (shared_ptr<order::Part> ()));
+       shared_ptr<order::Subtitle> subtitle;
+       shared_ptr<order::Text> text;
 
-       optional<string> font;
-       bool italic = false;
-       bool bold = false;
-       bool underline = false;
-       Colour colour;
-       int size = 0;
-       float aspect_adjust = 1.0;
-       Effect effect = NONE;
-       Colour effect_colour;
-       int spot_number = 1;
        Time last_in;
        Time last_out;
        Time last_fade_up_time;
        Time last_fade_down_time;
-
-       xmlpp::Element* font_element = 0;
-       xmlpp::Element* subtitle_element = 0;
+       HAlign last_h_align;
+       float last_h_position;
+       VAlign last_v_align;
+       float last_v_position;
+       Direction last_direction;
 
        BOOST_FOREACH (SubtitleString const & i, sorted) {
-
-               /* We will start a new <Font>...</Font> whenever some font property changes.
-                  I suppose we should really make an optimal hierarchy of <Font> tags, but
-                  that seems hard.
-               */
-
-               bool const font_changed =
-                       font          != i.font()          ||
-                       italic        != i.italic()        ||
-                       bold          != i.bold()          ||
-                       underline     != i.underline()     ||
-                       colour        != i.colour()        ||
-                       size          != i.size()          ||
-                       fabs (aspect_adjust - i.aspect_adjust()) > ASPECT_ADJUST_EPSILON ||
-                       effect        != i.effect()        ||
-                       effect_colour != i.effect_colour();
-
-               if (font_changed) {
-                       font = i.font ();
-                       italic = i.italic ();
-                       bold = i.bold ();
-                       underline = i.underline ();
-                       colour = i.colour ();
-                       size = i.size ();
-                       aspect_adjust = i.aspect_adjust ();
-                       effect = i.effect ();
-                       effect_colour = i.effect_colour ();
-               }
-
-               if (!font_element || font_changed) {
-                       font_element = root->add_child ("Font", xmlns);
-                       if (font) {
-                               if (standard == SMPTE) {
-                                       font_element->set_attribute ("ID", font.get ());
-                               } else {
-                                       font_element->set_attribute ("Id", font.get ());
-                               }
-                       }
-                       font_element->set_attribute ("Italic", italic ? "yes" : "no");
-                       font_element->set_attribute ("Color", colour.to_argb_string());
-                       font_element->set_attribute ("Size", raw_convert<string> (size));
-                       if (fabs (aspect_adjust - 1.0) > ASPECT_ADJUST_EPSILON) {
-                               font_element->set_attribute ("AspectAdjust", raw_convert<string> (aspect_adjust));
-                       }
-                       font_element->set_attribute ("Effect", effect_to_string (effect));
-                       font_element->set_attribute ("EffectColor", effect_colour.to_argb_string());
-                       font_element->set_attribute ("Script", "normal");
-                       if (standard == SMPTE) {
-                               font_element->set_attribute ("Underline", underline ? "yes" : "no");
-                       } else {
-                               font_element->set_attribute ("Underlined", underline ? "yes" : "no");
-                       }
-                       font_element->set_attribute ("Weight", bold ? "bold" : "normal");
-               }
-
-               if (!subtitle_element || font_changed ||
+               if (!subtitle ||
                    (last_in != i.in() ||
                     last_out != i.out() ||
                     last_fade_up_time != i.fade_up_time() ||
-                    last_fade_down_time != i.fade_down_time()
-                           )) {
-
-                       subtitle_element = font_element->add_child ("Subtitle", xmlns);
-                       subtitle_element->set_attribute ("SpotNumber", raw_convert<string> (spot_number++));
-                       subtitle_element->set_attribute ("TimeIn", i.in().rebase(time_code_rate).as_string(standard));
-                       subtitle_element->set_attribute ("TimeOut", i.out().rebase(time_code_rate).as_string(standard));
-                       if (standard == SMPTE) {
-                               subtitle_element->set_attribute ("FadeUpTime", i.fade_up_time().rebase(time_code_rate).as_string(standard));
-                               subtitle_element->set_attribute ("FadeDownTime", i.fade_down_time().rebase(time_code_rate).as_string(standard));
-                       } else {
-                               subtitle_element->set_attribute ("FadeUpTime", raw_convert<string> (i.fade_up_time().as_editable_units(time_code_rate)));
-                               subtitle_element->set_attribute ("FadeDownTime", raw_convert<string> (i.fade_down_time().as_editable_units(time_code_rate)));
-                       }
+                    last_fade_down_time != i.fade_down_time())
+                       ) {
+
+                       subtitle.reset (new order::Subtitle (root, i.in(), i.out(), i.fade_up_time(), i.fade_down_time()));
+                       root->children.push_back (subtitle);
 
                        last_in = i.in ();
                        last_out = i.out ();
                        last_fade_up_time = i.fade_up_time ();
                        last_fade_down_time = i.fade_down_time ();
+                       text.reset ();
                }
 
-               xmlpp::Element* text = subtitle_element->add_child ("Text", xmlns);
-
-               if (i.h_align() != HALIGN_CENTER) {
-                       if (standard == SMPTE) {
-                               text->set_attribute ("Halign", halign_to_string (i.h_align ()));
-                       } else {
-                               text->set_attribute ("HAlign", halign_to_string (i.h_align ()));
-                       }
+               if (!text ||
+                   last_h_align != i.h_align() ||
+                   fabs(last_h_position - i.h_position()) > ALIGN_EPSILON ||
+                   last_v_align != i.v_align() ||
+                   fabs(last_v_position - i.v_position()) > ALIGN_EPSILON ||
+                   last_direction != i.direction()
+                       ) {
+
+                       text.reset (new order::Text (subtitle, i.h_align(), i.h_position(), i.v_align(), i.v_position(), i.direction()));
+                       subtitle->children.push_back (text);
+
+                       last_h_align = i.h_align ();
+                       last_h_position = i.h_position ();
+                       last_v_align = i.v_align ();
+                       last_v_position = i.v_position ();
+                       last_direction = i.direction ();
                }
 
-               if (i.h_position() > ALIGN_EPSILON) {
-                       if (standard == SMPTE) {
-                               text->set_attribute ("Hposition", raw_convert<string> (i.h_position() * 100, 6));
-                       } else {
-                               text->set_attribute ("HPosition", raw_convert<string> (i.h_position() * 100, 6));
-                       }
-               }
+               text->children.push_back (shared_ptr<order::String> (new order::String (text, order::Font (i, standard), i.text())));
+       }
 
-               if (standard == SMPTE) {
-                       text->set_attribute ("Valign", valign_to_string (i.v_align()));
-               } else {
-                       text->set_attribute ("VAlign", valign_to_string (i.v_align()));
-               }
+       /* Pull font changes as high up the hierarchy as we can */
 
-               if (i.v_position() > ALIGN_EPSILON) {
-                       if (standard == SMPTE) {
-                               text->set_attribute ("Vposition", raw_convert<string> (i.v_position() * 100, 6));
-                       } else {
-                               text->set_attribute ("VPosition", raw_convert<string> (i.v_position() * 100, 6));
-                       }
-               } else {
-                       if (standard == SMPTE) {
-                               text->set_attribute ("Vposition", "0");
-                       } else {
-                               text->set_attribute ("VPosition", "0");
-                       }
-               }
+       pull_fonts (root);
 
-               /* Interop only supports "horizontal" or "vertical" for direction, so only write this
-                  for SMPTE.
-               */
-               if (i.direction() != DIRECTION_LTR && standard == SMPTE) {
-                       text->set_attribute ("Direction", direction_to_string (i.direction ()));
-               }
+       /* Write XML */
 
-               text->add_child_text (i.text());
-       }
+       order::Context context;
+       context.time_code_rate = time_code_rate;
+       context.standard = standard;
+       context.spot_number = 1;
+
+       root->write_xml (xml_root, context);
 }
 
 map<string, Data>
index 62314263010c8a1bfa3ed1e79760f6eee33516a5..e40118c4710c907777ab0f9d112891c8abd238a7 100644 (file)
@@ -48,6 +48,9 @@ namespace xmlpp {
 
 struct interop_dcp_font_test;
 struct smpte_dcp_font_test;
+struct pull_fonts_test1;
+struct pull_fonts_test2;
+struct pull_fonts_test3;
 
 namespace dcp
 {
@@ -58,6 +61,11 @@ class TextNode;
 class SubtitleNode;
 class LoadFontNode;
 
+namespace order {
+       class Part;
+       class Context;
+}
+
 /** @class SubtitleAsset
  *  @brief A parent for classes representing a file containing subtitles.
  *
@@ -156,7 +164,13 @@ protected:
        std::list<Font> _fonts;
 
 private:
+       friend struct ::pull_fonts_test1;
+       friend struct ::pull_fonts_test2;
+       friend struct ::pull_fonts_test3;
+
        void maybe_add_subtitle (std::string text, std::list<ParseState> const & parse_state);
+
+       static void pull_fonts (boost::shared_ptr<order::Part> part);
 };
 
 }
diff --git a/src/subtitle_asset_internal.cc b/src/subtitle_asset_internal.cc
new file mode 100644 (file)
index 0000000..bd28cdc
--- /dev/null
@@ -0,0 +1,224 @@
+/*
+    Copyright (C) 2012-2016 Carl Hetherington <cth@carlh.net>
+
+    This file is part of libdcp.
+
+    libdcp is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    libdcp is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with libdcp.  If not, see <http://www.gnu.org/licenses/>.
+
+    In addition, as a special exception, the copyright holders give
+    permission to link the code of portions of this program with the
+    OpenSSL library under certain conditions as described in each
+    individual source file, and distribute linked combinations
+    including the two.
+
+    You must obey the GNU General Public License in all respects
+    for all of the code used other than OpenSSL.  If you modify
+    file(s) with this exception, you may extend this exception to your
+    version of the file(s), but you are not obligated to do so.  If you
+    do not wish to do so, delete this exception statement from your
+    version.  If you delete this exception statement from all source
+    files in the program, then also delete it here.
+*/
+
+#include "subtitle_asset_internal.h"
+#include "subtitle_string.h"
+
+using std::string;
+using std::map;
+using namespace dcp;
+
+string
+order::Context::xmlns () const
+{
+       return standard == SMPTE ? "dcst" : "";
+}
+
+order::Font::Font (SubtitleString const & s, Standard standard)
+{
+       if (s.font()) {
+               if (standard == SMPTE) {
+                       _values["ID"] = s.font().get ();
+               } else {
+                       _values["Id"] = s.font().get ();
+               }
+       }
+       _values["Italic"] = s.italic() ? "yes" : "no";
+       _values["Color"] = s.colour().to_argb_string();
+       _values["Size"] = raw_convert<string> (s.size());
+       _values["AspectAdjust"] = raw_convert<string>(s.aspect_adjust(), 1, true);
+       _values["Effect"] = effect_to_string (s.effect());
+       _values["EffectColor"] = s.effect_colour().to_argb_string();
+       _values["Script"] = "normal";
+       if (standard == SMPTE) {
+               _values["Underline"] = s.underline() ? "yes" : "no";
+       } else {
+               _values["Underlined"] = s.underline() ? "yes" : "no";
+       }
+       _values["Weight"] = s.bold() ? "bold" : "normal";
+}
+
+xmlpp::Element*
+order::Font::as_xml (xmlpp::Element* parent, Context& context) const
+{
+       xmlpp::Element* e = parent->add_child ("Font", context.xmlns());
+       for (map<string, string>::const_iterator i = _values.begin(); i != _values.end(); ++i) {
+               e->set_attribute (i->first, i->second);
+       }
+       return e;
+}
+
+/** Modify our values so that they contain only those that are common to us and
+ *  other.
+ */
+void
+order::Font::take_intersection (Font other)
+{
+       map<string, string> inter;
+
+       for (map<string, string>::const_iterator i = other._values.begin(); i != other._values.end(); ++i) {
+               map<string, string>::iterator t = _values.find (i->first);
+               if (t != _values.end() && t->second == i->second) {
+                       inter.insert (*i);
+               }
+       }
+
+       _values = inter;
+}
+
+/** Modify our values so that it contains only those keys that are not in other */
+void
+order::Font::take_difference (Font other)
+{
+       map<string, string> diff;
+       for (map<string, string>::const_iterator i = _values.begin(); i != _values.end(); ++i) {
+               if (other._values.find (i->first) == other._values.end ()) {
+                       diff.insert (*i);
+               }
+       }
+
+       _values = diff;
+}
+
+bool
+order::Font::empty () const
+{
+       return _values.empty ();
+}
+
+xmlpp::Element*
+order::Part::as_xml (xmlpp::Element* parent, Context &) const
+{
+       return parent;
+}
+
+xmlpp::Element*
+order::String::as_xml (xmlpp::Element* parent, Context &) const
+{
+       parent->add_child_text (text);
+       return 0;
+}
+
+void
+order::Part::write_xml (xmlpp::Element* parent, order::Context& context) const
+{
+       if (!font.empty ()) {
+               parent = font.as_xml (parent, context);
+       }
+
+       parent = as_xml (parent, context);
+
+       BOOST_FOREACH (boost::shared_ptr<order::Part> i, children) {
+               i->write_xml (parent, context);
+       }
+}
+
+xmlpp::Element*
+order::Text::as_xml (xmlpp::Element* parent, Context& context) const
+{
+       xmlpp::Element* e = parent->add_child ("Text", context.xmlns());
+
+       if (_h_align != HALIGN_CENTER) {
+               if (context.standard == SMPTE) {
+                       e->set_attribute ("Halign", halign_to_string (_h_align));
+               } else {
+                       e->set_attribute ("HAlign", halign_to_string (_h_align));
+               }
+       }
+
+       if (_h_position > ALIGN_EPSILON) {
+               if (context.standard == SMPTE) {
+                       e->set_attribute ("Hposition", raw_convert<string> (_h_position * 100, 6));
+               } else {
+                       e->set_attribute ("HPosition", raw_convert<string> (_h_position * 100, 6));
+               }
+       }
+
+       if (context.standard == SMPTE) {
+               e->set_attribute ("Valign", valign_to_string (_v_align));
+       } else {
+               e->set_attribute ("VAlign", valign_to_string (_v_align));
+       }
+
+       if (_v_position > ALIGN_EPSILON) {
+               if (context.standard == SMPTE) {
+                       e->set_attribute ("Vposition", raw_convert<string> (_v_position * 100, 6));
+               } else {
+                       e->set_attribute ("VPosition", raw_convert<string> (_v_position * 100, 6));
+               }
+       } else {
+               if (context.standard == SMPTE) {
+                       e->set_attribute ("Vposition", "0");
+               } else {
+                       e->set_attribute ("VPosition", "0");
+               }
+       }
+
+       /* Interop only supports "horizontal" or "vertical" for direction, so only write this
+          for SMPTE.
+       */
+       if (_direction != DIRECTION_LTR && context.standard == SMPTE) {
+               e->set_attribute ("Direction", direction_to_string (_direction));
+       }
+
+       return e;
+}
+
+xmlpp::Element*
+order::Subtitle::as_xml (xmlpp::Element* parent, Context& context) const
+{
+       xmlpp::Element* e = parent->add_child ("Subtitle", context.xmlns());
+       e->set_attribute ("SpotNumber", raw_convert<string> (context.spot_number++));
+       e->set_attribute ("TimeIn", _in.rebase(context.time_code_rate).as_string(context.standard));
+       e->set_attribute ("TimeOut", _out.rebase(context.time_code_rate).as_string(context.standard));
+       if (context.standard == SMPTE) {
+               e->set_attribute ("FadeUpTime", _fade_up.rebase(context.time_code_rate).as_string(context.standard));
+               e->set_attribute ("FadeDownTime", _fade_down.rebase(context.time_code_rate).as_string(context.standard));
+       } else {
+               e->set_attribute ("FadeUpTime", raw_convert<string> (_fade_up.as_editable_units(context.time_code_rate)));
+               e->set_attribute ("FadeDownTime", raw_convert<string> (_fade_down.as_editable_units(context.time_code_rate)));
+       }
+       return e;
+}
+
+bool
+order::Font::operator== (Font const & other) const
+{
+       return _values == other._values;
+}
+
+void
+order::Font::clear ()
+{
+       _values.clear ();
+}
diff --git a/src/subtitle_asset_internal.h b/src/subtitle_asset_internal.h
new file mode 100644 (file)
index 0000000..cea230a
--- /dev/null
@@ -0,0 +1,167 @@
+/*
+    Copyright (C) 2012-2016 Carl Hetherington <cth@carlh.net>
+
+    This file is part of libdcp.
+
+    libdcp is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    libdcp is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with libdcp.  If not, see <http://www.gnu.org/licenses/>.
+
+    In addition, as a special exception, the copyright holders give
+    permission to link the code of portions of this program with the
+    OpenSSL library under certain conditions as described in each
+    individual source file, and distribute linked combinations
+    including the two.
+
+    You must obey the GNU General Public License in all respects
+    for all of the code used other than OpenSSL.  If you modify
+    file(s) with this exception, you may extend this exception to your
+    version of the file(s), but you are not obligated to do so.  If you
+    do not wish to do so, delete this exception statement from your
+    version.  If you delete this exception statement from all source
+    files in the program, then also delete it here.
+*/
+
+#ifndef LIBDCP_SUBTITLE_ASSET_INTERNAL_H
+#define LIBDCP_SUBTITLE_ASSET_INTERNAL_H
+
+#include "raw_convert.h"
+#include "types.h"
+#include "dcp_time.h"
+#include <libxml++/libxml++.h>
+#include <boost/foreach.hpp>
+
+struct take_intersection_test;
+struct take_difference_test;
+struct pull_fonts_test1;
+struct pull_fonts_test2;
+struct pull_fonts_test3;
+
+namespace dcp {
+
+class SubtitleString;
+
+namespace order {
+
+struct Context
+{
+       std::string xmlns () const;
+
+       int time_code_rate;
+       Standard standard;
+       int spot_number;
+};
+
+class Font
+{
+public:
+       Font () {}
+
+       Font (SubtitleString const & s, Standard standard);
+
+       xmlpp::Element* as_xml (xmlpp::Element* parent, Context& context) const;
+
+       void take_intersection (Font other);
+       void take_difference (Font other);
+       bool empty () const;
+       void clear ();
+       bool operator== (Font const & other) const;
+
+private:
+       friend struct ::take_intersection_test;
+       friend struct ::take_difference_test;
+       friend struct ::pull_fonts_test1;
+       friend struct ::pull_fonts_test2;
+       friend struct ::pull_fonts_test3;
+
+       std::map<std::string, std::string> _values;
+};
+
+class Part
+{
+public:
+       Part (boost::shared_ptr<Part> parent_)
+               : parent (parent_)
+       {}
+
+       Part (boost::shared_ptr<Part> parent_, Font font_)
+               : parent (parent_)
+               , font (font_)
+       {}
+
+       virtual xmlpp::Element* as_xml (xmlpp::Element* parent, Context &) const;
+       void write_xml (xmlpp::Element* parent, order::Context& context) const;
+
+       boost::shared_ptr<Part> parent;
+       Font font;
+       std::list<boost::shared_ptr<Part> > children;
+};
+
+class String : public Part
+{
+public:
+       String (boost::shared_ptr<Part> parent, Font font, std::string text_)
+               : Part (parent, font)
+               , text (text_)
+       {}
+
+       virtual xmlpp::Element* as_xml (xmlpp::Element* parent, Context &) const;
+
+       std::string text;
+};
+
+class Text : public Part
+{
+public:
+       Text (boost::shared_ptr<Part> parent, HAlign h_align, float h_position, VAlign v_align, float v_position, Direction direction)
+               : Part (parent)
+               , _h_align (h_align)
+               , _h_position (h_position)
+               , _v_align (v_align)
+               , _v_position (v_position)
+               , _direction (direction)
+       {}
+
+       xmlpp::Element* as_xml (xmlpp::Element* parent, Context& context) const;
+
+private:
+       HAlign _h_align;
+       float _h_position;
+       VAlign _v_align;
+       float _v_position;
+       Direction _direction;
+};
+
+class Subtitle : public Part
+{
+public:
+       Subtitle (boost::shared_ptr<Part> parent, Time in, Time out, Time fade_up, Time fade_down)
+               : Part (parent)
+               , _in (in)
+               , _out (out)
+               , _fade_up (fade_up)
+               , _fade_down (fade_down)
+       {}
+
+       xmlpp::Element* as_xml (xmlpp::Element* parent, Context& context) const;
+
+private:
+       Time _in;
+       Time _out;
+       Time _fade_up;
+       Time _fade_down;
+};
+
+}
+}
+
+#endif
index 365f3df391c028fd435973288cee19775d6c423c..8c35c62cfeeba8b93550a649dfd48d2e733138c5 100644 (file)
@@ -92,6 +92,7 @@ def build(bld):
              stereo_picture_asset_writer.cc
              stereo_picture_frame.cc
              subtitle_asset.cc
+             subtitle_asset_internal.cc
              subtitle_string.cc
              transfer_function.cc
              types.cc
index 44f519c05bafdc3586eaff1120c4c92030c510b3..0b99b4b77f610bbbd2f5407231d7502ece1fdcd0 100644 (file)
@@ -24,6 +24,7 @@
 #include <libxml++/libxml++.h>
 #include <boost/test/unit_test.hpp>
 #include <cstdio>
+#include <iostream>
 
 using std::string;
 using std::min;
@@ -54,7 +55,7 @@ check_xml (xmlpp::Element* ref, xmlpp::Element* test, list<string> ignore)
 
        xmlpp::Element::NodeList ref_children = ref->get_children ();
        xmlpp::Element::NodeList test_children = test->get_children ();
-       BOOST_REQUIRE_EQUAL (ref_children.size (), test_children.size ());
+       BOOST_REQUIRE_MESSAGE (ref_children.size () == test_children.size (), "child counts of " << ref->get_name() << " differ");
 
        xmlpp::Element::NodeList::iterator k = ref_children.begin ();
        xmlpp::Element::NodeList::iterator l = test_children.begin ();
index 14d19d8b873b25d9103204e8463bdf89239e2c15..292a00d579a6f4aa8a848fbfeb727023896ba1d0 100644 (file)
@@ -20,6 +20,7 @@
 #include "interop_subtitle_asset.h"
 #include "smpte_subtitle_asset.h"
 #include "subtitle_string.h"
+#include "subtitle_asset_internal.h"
 #include "test.h"
 #include <boost/test/unit_test.hpp>
 
@@ -27,7 +28,116 @@ using std::list;
 using std::string;
 using boost::shared_ptr;
 
-/* Write some subtitle content as Interop XML and check that it is right */
+/** Test dcp::order::Font::take_intersection */
+BOOST_AUTO_TEST_CASE (take_intersection_test)
+{
+       dcp::order::Font A;
+       A._values["foo"] = "bar";
+       A._values["fred"] = "jim";
+
+       dcp::order::Font B;
+       B._values["foo"] = "bar";
+       B._values["sheila"] = "baz";
+
+       A.take_intersection (B);
+       BOOST_REQUIRE_EQUAL (A._values.size(), 1);
+       BOOST_CHECK_EQUAL (A._values["foo"], "bar");
+
+       A._values.clear ();
+       B._values.clear ();
+
+       A._values["foo"] = "bar";
+       A._values["fred"] = "jim";
+
+       B._values["foo"] = "hello";
+       B._values["sheila"] = "baz";
+
+       A.take_intersection (B);
+       BOOST_CHECK_EQUAL (A._values.size(), 0);
+}
+
+/** Test dcp::order::Font::take_difference */
+BOOST_AUTO_TEST_CASE (take_difference_test)
+{
+       dcp::order::Font A;
+       A._values["foo"] = "bar";
+       A._values["fred"] = "jim";
+
+       dcp::order::Font B;
+       B._values["foo"] = "bar";
+       B._values["sheila"] = "baz";
+
+       A.take_difference (B);
+       BOOST_REQUIRE_EQUAL (A._values.size(), 1);
+       BOOST_CHECK_EQUAL (A._values["fred"], "jim");
+}
+
+/** Test dcp::order::Subtitle::pull_fonts */
+BOOST_AUTO_TEST_CASE (pull_fonts_test1)
+{
+       shared_ptr<dcp::order::Part> root (new dcp::order::Part (shared_ptr<dcp::order::Part> ()));
+       shared_ptr<dcp::order::Subtitle> sub1 (new dcp::order::Subtitle (root, dcp::Time(), dcp::Time(), dcp::Time(), dcp::Time()));
+       root->children.push_back (sub1);
+       shared_ptr<dcp::order::Text> text1 (new dcp::order::Text (sub1, dcp::HALIGN_CENTER, 0, dcp::VALIGN_TOP, 0, dcp::DIRECTION_LTR));
+       sub1->children.push_back (text1);
+       text1->font._values["font"] = "Inconsolata";
+       text1->font._values["size"] = "42";
+
+       dcp::SubtitleAsset::pull_fonts (root);
+
+       BOOST_REQUIRE_EQUAL (sub1->font._values.size(), 2);
+       BOOST_CHECK_EQUAL (sub1->font._values["font"], "Inconsolata");
+       BOOST_CHECK_EQUAL (sub1->font._values["size"], "42");
+       BOOST_CHECK_EQUAL (text1->font._values.size(), 0);
+}
+
+/** Test dcp::order::Subtitle::pull_fonts */
+BOOST_AUTO_TEST_CASE (pull_fonts_test2)
+{
+       shared_ptr<dcp::order::Part> root (new dcp::order::Part (shared_ptr<dcp::order::Part> ()));
+       shared_ptr<dcp::order::Subtitle> sub1 (new dcp::order::Subtitle (root, dcp::Time(), dcp::Time(), dcp::Time(), dcp::Time()));
+       root->children.push_back (sub1);
+       shared_ptr<dcp::order::Text> text1 (new dcp::order::Text (sub1, dcp::HALIGN_CENTER, 0, dcp::VALIGN_TOP, 0, dcp::DIRECTION_LTR));
+       sub1->children.push_back (text1);
+       text1->font._values["font"] = "Inconsolata";
+       text1->font._values["size"] = "42";
+       shared_ptr<dcp::order::Text> text2 (new dcp::order::Text (sub1, dcp::HALIGN_CENTER, 0, dcp::VALIGN_TOP, 0, dcp::DIRECTION_LTR));
+       sub1->children.push_back (text2);
+       text2->font._values["font"] = "Inconsolata";
+       text2->font._values["size"] = "48";
+
+       dcp::SubtitleAsset::pull_fonts (root);
+
+       BOOST_REQUIRE_EQUAL (sub1->font._values.size(), 1);
+       BOOST_CHECK_EQUAL (sub1->font._values["font"], "Inconsolata");
+       BOOST_REQUIRE_EQUAL (text1->font._values.size(), 1);
+       BOOST_CHECK_EQUAL (text1->font._values["size"], "42");
+       BOOST_REQUIRE_EQUAL (text2->font._values.size(), 1);
+       BOOST_CHECK_EQUAL (text2->font._values["size"], "48");
+}
+
+/** Test dcp::order::Subtitle::pull_fonts */
+BOOST_AUTO_TEST_CASE (pull_fonts_test3)
+{
+       shared_ptr<dcp::order::Part> root (new dcp::order::Part (shared_ptr<dcp::order::Part> ()));
+       shared_ptr<dcp::order::Subtitle> sub1 (new dcp::order::Subtitle (root, dcp::Time(), dcp::Time(), dcp::Time(), dcp::Time()));
+       root->children.push_back (sub1);
+       shared_ptr<dcp::order::Text> text1 (new dcp::order::Text (sub1, dcp::HALIGN_CENTER, 0, dcp::VALIGN_TOP, 0, dcp::DIRECTION_LTR));
+       sub1->children.push_back (text1);
+       dcp::order::Font font;
+       font._values["font"] = "Inconsolata";
+       font._values["size"] = "42";
+       shared_ptr<dcp::order::String> string1 (new dcp::order::String (text1, font, "Hello world"));
+       text1->children.push_back (string1);
+
+       dcp::SubtitleAsset::pull_fonts (root);
+
+       BOOST_REQUIRE_EQUAL (sub1->font._values.size(), 2);
+       BOOST_CHECK_EQUAL (sub1->font._values["font"], "Inconsolata");
+       BOOST_CHECK_EQUAL (sub1->font._values["size"], "42");
+}
+
+/** Write some subtitle content as Interop XML and check that it is right */
 BOOST_AUTO_TEST_CASE (write_interop_subtitle_test)
 {
        dcp::InteropSubtitleAsset c;
@@ -92,12 +202,12 @@ BOOST_AUTO_TEST_CASE (write_interop_subtitle_test)
                "  <MovieTitle>Test</MovieTitle>\n"
                "  <ReelNumber>1</ReelNumber>\n"
                "  <Language>EN</Language>\n"
-               "  <Font Id=\"Frutiger\" Italic=\"no\" Color=\"FFFFFFFF\" Size=\"48\" Effect=\"none\" EffectColor=\"FF000000\" Script=\"normal\" Underlined=\"no\" Weight=\"normal\">\n"
+               "  <Font AspectAdjust=\"1.0\" Color=\"FFFFFFFF\" Effect=\"none\" EffectColor=\"FF000000\" Id=\"Frutiger\" Italic=\"no\" Script=\"normal\" Size=\"48\" Underlined=\"no\" Weight=\"normal\">\n"
                "    <Subtitle SpotNumber=\"1\" TimeIn=\"00:04:09:229\" TimeOut=\"00:04:11:229\" FadeUpTime=\"0\" FadeDownTime=\"0\">\n"
                "      <Text VAlign=\"top\" VPosition=\"80\">Hello world</Text>\n"
                "    </Subtitle>\n"
                "  </Font>\n"
-               "  <Font Italic=\"yes\" Color=\"FF800040\" Size=\"91\" Effect=\"border\" EffectColor=\"FF010203\" Script=\"normal\" Underlined=\"yes\" Weight=\"bold\">\n"
+               "  <Font AspectAdjust=\"1.0\" Color=\"FF800040\" Effect=\"border\" EffectColor=\"FF010203\" Italic=\"yes\" Script=\"normal\" Size=\"91\" Underlined=\"yes\" Weight=\"bold\">\n"
                "    <Subtitle SpotNumber=\"2\" TimeIn=\"05:41:00:218\" TimeOut=\"06:12:15:218\" FadeUpTime=\"930792\" FadeDownTime=\"4591834\">\n"
                "      <Text VAlign=\"bottom\" VPosition=\"40\">What's going on</Text>\n"
                "    </Subtitle>\n"
@@ -178,12 +288,12 @@ BOOST_AUTO_TEST_CASE (write_smpte_subtitle_test)
                "  <dcst:EditRate>24 1</dcst:EditRate>\n"
                "  <dcst:TimeCodeRate>24</dcst:TimeCodeRate>\n"
                "  <dcst:SubtitleList>\n"
-               "    <dcst:Font ID=\"Frutiger\" Italic=\"no\" Color=\"FFFFFFFF\" Size=\"48\" Effect=\"none\" EffectColor=\"FF000000\" Script=\"normal\" Underline=\"no\" Weight=\"normal\">\n"
+               "    <dcst:Font AspectAdjust=\"1.0\" Color=\"FFFFFFFF\" Effect=\"none\" EffectColor=\"FF000000\" ID=\"Frutiger\" Italic=\"no\" Script=\"normal\" Size=\"48\" Underline=\"no\" Weight=\"normal\">\n"
                "      <dcst:Subtitle SpotNumber=\"1\" TimeIn=\"00:04:09:22\" TimeOut=\"00:04:11:22\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">\n"
                "        <dcst:Text Valign=\"top\" Vposition=\"80\">Hello world</dcst:Text>\n"
                "      </dcst:Subtitle>\n"
                "    </dcst:Font>\n"
-               "    <dcst:Font Italic=\"yes\" Color=\"FF800040\" Size=\"91\" Effect=\"border\" EffectColor=\"FF010203\" Script=\"normal\" Underline=\"yes\" Weight=\"bold\">\n"
+               "    <dcst:Font AspectAdjust=\"1.0\" Color=\"FF800040\" Effect=\"border\" EffectColor=\"FF010203\" Italic=\"yes\" Script=\"normal\" Size=\"91\" Underline=\"yes\" Weight=\"bold\">\n"
                "      <dcst:Subtitle SpotNumber=\"2\" TimeIn=\"05:41:00:21\" TimeOut=\"06:12:15:21\" FadeUpTime=\"01:02:03:04\" FadeDownTime=\"05:06:07:08\">\n"
                "        <dcst:Text Valign=\"bottom\" Vposition=\"40\" Direction=\"rtl\">What's going on</dcst:Text>\n"
                "      </dcst:Subtitle>\n"
@@ -193,3 +303,192 @@ BOOST_AUTO_TEST_CASE (write_smpte_subtitle_test)
                list<string> ()
                );
 }
+
+/* Write some subtitle content as SMPTE XML and check that it is right.
+   This includes in-line font changes.
+*/
+BOOST_AUTO_TEST_CASE (write_smpte_subtitle_test2)
+{
+       dcp::SMPTESubtitleAsset c;
+       c.set_reel_number (1);
+       c.set_language ("EN");
+       c.set_content_title_text ("Test");
+       c.set_issue_date (dcp::LocalTime ("2016-04-01T03:52:00+00:00"));
+
+       c.add (
+               dcp::SubtitleString (
+                       string ("Arial"),
+                       false,
+                       false,
+                       false,
+                       dcp::Colour (255, 255, 255),
+                       48,
+                       1.0,
+                       dcp::Time (0, 0, 1, 0, 24),
+                       dcp::Time (0, 0, 9, 0, 24),
+                       0,
+                       dcp::HALIGN_CENTER,
+                       0.8,
+                       dcp::VALIGN_TOP,
+                       dcp::DIRECTION_LTR,
+                       "Testing is ",
+                       dcp::NONE,
+                       dcp::Colour (0, 0, 0),
+                       dcp::Time (0, 0, 0, 0, 24),
+                       dcp::Time (0, 0, 0, 0, 24)
+                       )
+               );
+
+       c.add (
+               dcp::SubtitleString (
+                       string ("Arial"),
+                       true,
+                       false,
+                       false,
+                       dcp::Colour (255, 255, 255),
+                       48,
+                       1.0,
+                       dcp::Time (0, 0, 1, 0, 24),
+                       dcp::Time (0, 0, 9, 0, 24),
+                       0,
+                       dcp::HALIGN_CENTER,
+                       0.8,
+                       dcp::VALIGN_TOP,
+                       dcp::DIRECTION_LTR,
+                       "really",
+                       dcp::NONE,
+                       dcp::Colour (0, 0, 0),
+                       dcp::Time (0, 0, 0, 0, 24),
+                       dcp::Time (0, 0, 0, 0, 24)
+                       )
+               );
+
+       c.add (
+               dcp::SubtitleString (
+                       string ("Arial"),
+                       false,
+                       false,
+                       false,
+                       dcp::Colour (255, 255, 255),
+                       48,
+                       1.0,
+                       dcp::Time (0, 0, 1, 0, 24),
+                       dcp::Time (0, 0, 9, 0, 24),
+                       0,
+                       dcp::HALIGN_CENTER,
+                       0.8,
+                       dcp::VALIGN_TOP,
+                       dcp::DIRECTION_LTR,
+                       " fun",
+                       dcp::NONE,
+                       dcp::Colour (0, 0, 0),
+                       dcp::Time (0, 0, 0, 0, 24),
+                       dcp::Time (0, 0, 0, 0, 24)
+                       )
+               );
+
+       c.add (
+               dcp::SubtitleString (
+                       string ("Arial"),
+                       false,
+                       false,
+                       false,
+                       dcp::Colour (255, 255, 255),
+                       48,
+                       1.0,
+                       dcp::Time (0, 0, 1, 0, 24),
+                       dcp::Time (0, 0, 9, 0, 24),
+                       0,
+                       dcp::HALIGN_CENTER,
+                       0.9,
+                       dcp::VALIGN_TOP,
+                       dcp::DIRECTION_LTR,
+                       "This is the ",
+                       dcp::NONE,
+                       dcp::Colour (0, 0, 0),
+                       dcp::Time (0, 0, 0, 0, 24),
+                       dcp::Time (0, 0, 0, 0, 24)
+                       )
+               );
+
+       c.add (
+               dcp::SubtitleString (
+                       string ("Arial"),
+                       true,
+                       false,
+                       false,
+                       dcp::Colour (255, 255, 255),
+                       48,
+                       1.0,
+                       dcp::Time (0, 0, 1, 0, 24),
+                       dcp::Time (0, 0, 9, 0, 24),
+                       0,
+                       dcp::HALIGN_CENTER,
+                       0.9,
+                       dcp::VALIGN_TOP,
+                       dcp::DIRECTION_LTR,
+                       "second",
+                       dcp::NONE,
+                       dcp::Colour (0, 0, 0),
+                       dcp::Time (0, 0, 0, 0, 24),
+                       dcp::Time (0, 0, 0, 0, 24)
+                       )
+               );
+
+       c.add (
+               dcp::SubtitleString (
+                       string ("Arial"),
+                       false,
+                       false,
+                       false,
+                       dcp::Colour (255, 255, 255),
+                       48,
+                       1.0,
+                       dcp::Time (0, 0, 1, 0, 24),
+                       dcp::Time (0, 0, 9, 0, 24),
+                       0,
+                       dcp::HALIGN_CENTER,
+                       0.9,
+                       dcp::VALIGN_TOP,
+                       dcp::DIRECTION_LTR,
+                       " line",
+                       dcp::NONE,
+                       dcp::Colour (0, 0, 0),
+                       dcp::Time (0, 0, 0, 0, 24),
+                       dcp::Time (0, 0, 0, 0, 24)
+                       )
+               );
+
+       c._id = "a6c58cff-3e1e-4b38-acec-a42224475ef6";
+
+       check_xml (
+               c.xml_as_string (),
+               "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+               "<dcst:SubtitleReel xmlns:dcst=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">\n"
+               "  <dcst:Id>urn:uuid:a6c58cff-3e1e-4b38-acec-a42224475ef6</dcst:Id>\n"
+               "  <dcst:ContentTitleText>Test</dcst:ContentTitleText>\n"
+               "  <dcst:IssueDate>2016-04-01T03:52:00.000+00:00</dcst:IssueDate>\n"
+               "  <dcst:ReelNumber>1</dcst:ReelNumber>\n"
+               "  <dcst:Language>EN</dcst:Language>\n"
+               "  <dcst:EditRate>24 1</dcst:EditRate>\n"
+               "  <dcst:TimeCodeRate>24</dcst:TimeCodeRate>\n"
+               "  <dcst:SubtitleList>\n"
+               "    <dcst:Font AspectAdjust=\"1.0\" Color=\"FFFFFFFF\" Effect=\"none\" EffectColor=\"FF000000\" ID=\"Arial\" Script=\"normal\" Size=\"48\" Underline=\"no\" Weight=\"normal\">\n"
+               "      <dcst:Subtitle SpotNumber=\"1\" TimeIn=\"00:00:01:00\" TimeOut=\"00:00:09:00\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">\n"
+               "        <dcst:Text Valign=\"top\" Vposition=\"80\">\n"
+               "          <dcst:Font Italic=\"no\">Testing is </dcst:Font>\n"
+               "          <dcst:Font Italic=\"yes\">really</dcst:Font>\n"
+               "          <dcst:Font Italic=\"no\"> fun</dcst:Font>\n"
+               "        </dcst:Text>\n"
+               "        <dcst:Text Valign=\"top\" Vposition=\"90\">\n"
+               "          <dcst:Font Italic=\"no\">This is the </dcst:Font>\n"
+               "          <dcst:Font Italic=\"yes\">second</dcst:Font>\n"
+               "          <dcst:Font Italic=\"no\"> line</dcst:Font>\n"
+               "        </dcst:Text>\n"
+               "      </dcst:Subtitle>\n"
+               "    </dcst:Font>\n"
+               "  </dcst:SubtitleList>\n"
+               "</dcst:SubtitleReel>\n",
+               list<string> ()
+               );
+}