Re-read MXF descriptor after adding a key to a SMPTE subtitle asset (DoM #2660).
[libdcp.git] / src / smpte_subtitle_asset.cc
index b568139a952d573f0fb38f8622c19d77d48c93d3..0ff1d7ef5048996e7861e2bd7c02e453e68d66de 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2012-2018 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2021 Carl Hetherington <cth@carlh.net>
 
     This file is part of libdcp.
 
     files in the program, then also delete it here.
 */
 
+
 /** @file  src/smpte_subtitle_asset.cc
- *  @brief SMPTESubtitleAsset class.
+ *  @brief SMPTESubtitleAsset class
  */
 
-#include "smpte_subtitle_asset.h"
-#include "smpte_load_font_node.h"
-#include "exceptions.h"
-#include "xml.h"
-#include "raw_convert.h"
-#include "dcp_assert.h"
-#include "util.h"
+
 #include "compose.hpp"
 #include "crypto_context.h"
+#include "dcp_assert.h"
+#include "equality_options.h"
+#include "exceptions.h"
+#include "filesystem.h"
+#include "raw_convert.h"
+#include "smpte_load_font_node.h"
+#include "smpte_subtitle_asset.h"
 #include "subtitle_image.h"
+#include "util.h"
+#include "warnings.h"
+#include "xml.h"
+LIBDCP_DISABLE_WARNINGS
 #include <asdcp/AS_DCP.h>
 #include <asdcp/KM_util.h>
+#include <asdcp/KM_log.h>
 #include <libxml++/libxml++.h>
-#include <boost/foreach.hpp>
+LIBDCP_ENABLE_WARNINGS
 #include <boost/algorithm/string.hpp>
 
+
 using std::string;
 using std::list;
 using std::vector;
 using std::map;
-using boost::shared_ptr;
+using std::shared_ptr;
+using std::dynamic_pointer_cast;
+using std::make_shared;
 using boost::split;
 using boost::is_any_of;
 using boost::shared_array;
-using boost::dynamic_pointer_cast;
 using boost::optional;
+using boost::starts_with;
 using namespace dcp;
 
-SMPTESubtitleAsset::SMPTESubtitleAsset ()
-       : MXF (SMPTE)
-       , _intrinsic_duration (0)
+
+static string const subtitle_smpte_ns_2007 = "http://www.smpte-ra.org/schemas/428-7/2007/DCST";
+static string const subtitle_smpte_ns_2010 = "http://www.smpte-ra.org/schemas/428-7/2010/DCST";
+static string const subtitle_smpte_ns_2014 = "http://www.smpte-ra.org/schemas/428-7/2014/DCST";
+
+
+SMPTESubtitleAsset::SMPTESubtitleAsset(SubtitleStandard standard)
+       : MXF(Standard::SMPTE)
        , _edit_rate (24, 1)
        , _time_code_rate (24)
-       , _xml_id (make_uuid ())
+       , _subtitle_standard(standard)
+       , _xml_id (make_uuid())
 {
 
 }
 
-/** Construct a SMPTESubtitleAsset by reading an MXF or XML file.
- *  @param file Filename.
- */
+
 SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file)
        : SubtitleAsset (file)
 {
-       shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
+       auto xml = make_shared<cxml::Document>("SubtitleReel");
 
-       shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
-       Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
-       if (!ASDCP_FAILURE (r)) {
+       auto reader = make_shared<ASDCP::TimedText::MXFReader>();
+       auto r = Kumu::RESULT_OK;
+       {
+               ASDCPErrorSuspender sus;
+               r = reader->OpenRead(dcp::filesystem::fix_long_path(*_file).string().c_str());
+       }
+       if (!ASDCP_FAILURE(r)) {
                /* MXF-wrapped */
                ASDCP::WriterInfo info;
                reader->FillWriterInfo (info);
                _id = read_writer_info (info);
                if (!_key_id) {
                        /* Not encrypted; read it in now */
-                       string s;
-                       reader->ReadTimedTextResource (s);
-                       xml->read_string (s);
+                       string xml_string;
+                       reader->ReadTimedTextResource (xml_string);
+                       _raw_xml = xml_string;
+                       xml->read_string (xml_string);
                        parse_xml (xml);
-                       read_mxf_descriptor (reader, shared_ptr<DecryptionContext> (new DecryptionContext (optional<Key>(), SMPTE)));
+                       read_mxf_descriptor (reader);
+                       read_mxf_resources(reader, std::make_shared<DecryptionContext>(optional<Key>(), Standard::SMPTE));
+               } else {
+                       read_mxf_descriptor (reader);
                }
        } else {
                /* Plain XML */
                try {
-                       xml.reset (new cxml::Document ("SubtitleReel"));
-                       xml->read_file (file);
+                       _raw_xml = dcp::file_to_string (file);
+                       xml = make_shared<cxml::Document>("SubtitleReel");
+                       xml->read_file(dcp::filesystem::fix_long_path(file));
                        parse_xml (xml);
-                       _id = _xml_id = remove_urn_uuid (xml->string_child ("Id"));
                } catch (cxml::Error& e) {
                        boost::throw_exception (
-                               DCPReadError (
+                               ReadError (
                                        String::compose (
                                                "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
-                                               file, static_cast<int> (r), e.what ()
+                                               file, static_cast<int>(r), e.what()
                                                )
                                        )
                                );
                }
+
+               /* Try to read PNG files from the same folder that the XML is in; the wisdom of this is
+                  debatable, at best...
+               */
+               for (auto i: _subtitles) {
+                       auto im = dynamic_pointer_cast<SubtitleImage>(i);
+                       if (im && im->png_image().size() == 0) {
+                               /* Even more dubious; allow <id>.png or urn:uuid:<id>.png */
+                               auto p = file.parent_path() / String::compose("%1.png", im->id());
+                               if (filesystem::is_regular_file(p)) {
+                                       im->read_png_file (p);
+                               } else if (starts_with (im->id(), "urn:uuid:")) {
+                                       p = file.parent_path() / String::compose("%1.png", remove_urn_uuid(im->id()));
+                                       if (filesystem::is_regular_file(p)) {
+                                               im->read_png_file (p);
+                                       }
+                               }
+                       }
+               }
+               _standard = Standard::SMPTE;
+       }
+
+       /* Check that all required image data have been found */
+       for (auto i: _subtitles) {
+               auto im = dynamic_pointer_cast<SubtitleImage>(i);
+               if (im && im->png_image().size() == 0) {
+                       throw MissingSubtitleImageError (im->id());
+               }
        }
 }
 
+
 void
 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
 {
+       if (xml->namespace_uri() == subtitle_smpte_ns_2007) {
+               _subtitle_standard = SubtitleStandard::SMPTE_2007;
+       } else if (xml->namespace_uri() == subtitle_smpte_ns_2010) {
+               _subtitle_standard = SubtitleStandard::SMPTE_2010;
+       } else if (xml->namespace_uri() == subtitle_smpte_ns_2014) {
+               _subtitle_standard = SubtitleStandard::SMPTE_2014;
+       } else {
+               throw XMLError("Unrecognised subtitle namespace " + xml->namespace_uri());
+       }
        _xml_id = remove_urn_uuid(xml->string_child("Id"));
        _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
 
@@ -129,7 +189,7 @@ SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
        _language = xml->optional_string_child ("Language");
 
        /* This is supposed to be two numbers, but a single number has been seen in the wild */
-       string const er = xml->string_child ("EditRate");
+       auto const er = xml->string_child ("EditRate");
        vector<string> er_parts;
        split (er_parts, er, is_any_of (" "));
        if (er_parts.size() == 1) {
@@ -142,69 +202,76 @@ SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
 
        _time_code_rate = xml->number_child<int> ("TimeCodeRate");
        if (xml->optional_string_child ("StartTime")) {
-               _start_time = Time (xml->string_child ("StartTime"), _time_code_rate);
+               _start_time = Time (xml->string_child("StartTime"), _time_code_rate);
        }
 
        /* Now we need to drop down to xmlpp */
 
-       list<ParseState> ps;
-       xmlpp::Node::NodeList c = xml->node()->get_children ();
-       for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
-               xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
+       vector<ParseState> ps;
+       for (auto i: xml->node()->get_children()) {
+               auto const e = dynamic_cast<xmlpp::Element const *>(i);
                if (e && e->get_name() == "SubtitleList") {
-                       parse_subtitles (e, ps, _time_code_rate, SMPTE);
+                       parse_subtitles (e, ps, _time_code_rate, Standard::SMPTE);
                }
        }
 
        /* Guess intrinsic duration */
-       _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
+       _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);
 }
 
+
 void
-SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
+SMPTESubtitleAsset::read_mxf_resources (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
 {
        ASDCP::TimedText::TimedTextDescriptor descriptor;
        reader->FillTimedTextDescriptor (descriptor);
 
-       /* Load fonts */
+       /* Load fonts and images */
 
        for (
-               ASDCP::TimedText::ResourceList_t::const_iterator i = descriptor.ResourceList.begin();
+               auto i = descriptor.ResourceList.begin();
                i != descriptor.ResourceList.end();
                ++i) {
 
                ASDCP::TimedText::FrameBuffer buffer;
-               buffer.Capacity (10 * 1024 * 1024);
-               reader->ReadAncillaryResource (i->ResourceID, buffer, dec->context(), dec->hmac());
+               buffer.Capacity(32 * 1024 * 1024);
+               auto const result = reader->ReadAncillaryResource(i->ResourceID, buffer, dec->context(), dec->hmac());
+               if (ASDCP_FAILURE(result)) {
+                       switch (i->Type) {
+                       case ASDCP::TimedText::MT_OPENTYPE:
+                               throw ReadError(String::compose("Could not read font from MXF file (%1)", static_cast<int>(result)));
+                       case ASDCP::TimedText::MT_PNG:
+                               throw ReadError(String::compose("Could not read subtitle image from MXF file (%1)", static_cast<int>(result)));
+                       default:
+                               throw ReadError(String::compose("Could not read resource from MXF file (%1)", static_cast<int>(result)));
+                       }
+               }
 
                char id[64];
-               Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof (id));
-
-               shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
-               memcpy (data.get(), buffer.RoData(), buffer.Size());
+               Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof(id));
 
                switch (i->Type) {
                case ASDCP::TimedText::MT_OPENTYPE:
                {
-                       list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = _load_font_nodes.begin ();
+                       auto j = _load_font_nodes.begin();
                        while (j != _load_font_nodes.end() && (*j)->urn != id) {
                                ++j;
                        }
 
                        if (j != _load_font_nodes.end ()) {
-                               _fonts.push_back (Font ((*j)->id, (*j)->urn, Data (data, buffer.Size ())));
+                               _fonts.push_back(Font((*j)->id, (*j)->urn, ArrayData(buffer.RoData(), buffer.Size())));
                        }
                        break;
                }
                case ASDCP::TimedText::MT_PNG:
                {
-                       list<shared_ptr<Subtitle> >::const_iterator j = _subtitles.begin ();
+                       auto j = _subtitles.begin();
                        while (j != _subtitles.end() && ((!dynamic_pointer_cast<SubtitleImage>(*j)) || dynamic_pointer_cast<SubtitleImage>(*j)->id() != id)) {
                                ++j;
                        }
 
                        if (j != _subtitles.end()) {
-                               dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image (Data(data, buffer.Size()));
+                               dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image(ArrayData(buffer.RoData(), buffer.Size()));
                        }
                        break;
                }
@@ -212,22 +279,38 @@ SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader>
                        break;
                }
        }
+}
+
+
+void
+SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader)
+{
+       ASDCP::TimedText::TimedTextDescriptor descriptor;
+       reader->FillTimedTextDescriptor (descriptor);
 
-       /* Get intrinsic duration */
        _intrinsic_duration = descriptor.ContainerDuration;
+       /* The thing which is called AssetID in the descriptor is also known as the
+        * ResourceID of the MXF.  We store that, at present just for verification
+        * purposes.
+        */
+       char id[64];
+       Kumu::bin2UUIDhex (descriptor.AssetID, ASDCP::UUIDlen, id, sizeof(id));
+       _resource_id = id;
 }
 
+
 void
 SMPTESubtitleAsset::set_key (Key key)
 {
        /* See if we already have a key; if we do, and we have a file, we'll already
           have read that file.
        */
-       bool const had_key = static_cast<bool> (_key);
+       auto const had_key = static_cast<bool>(_key);
+       auto const had_key_id = static_cast<bool>(_key_id);
 
        MXF::set_key (key);
 
-       if (!_key_id || !_file || had_key) {
+       if (!had_key_id || !_file || had_key) {
                /* Either we don't have any data to read, it wasn't
                   encrypted, or we've already read it, so we don't
                   need to do anything else.
@@ -237,83 +320,89 @@ SMPTESubtitleAsset::set_key (Key key)
 
        /* Our data was encrypted; now we can decrypt it */
 
-       shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
-       Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
+       auto reader = make_shared<ASDCP::TimedText::MXFReader>();
+       auto r = reader->OpenRead(dcp::filesystem::fix_long_path(*_file).string().c_str());
        if (ASDCP_FAILURE (r)) {
                boost::throw_exception (
-                       DCPReadError (
+                       ReadError (
                                String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
                                )
                        );
        }
 
-       string s;
-       shared_ptr<DecryptionContext> dec (new DecryptionContext (key, SMPTE));
-       reader->ReadTimedTextResource (s, dec->context(), dec->hmac());
-       shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
-       xml->read_string (s);
+       auto dec = make_shared<DecryptionContext>(key, Standard::SMPTE);
+       string xml_string;
+       reader->ReadTimedTextResource (xml_string, dec->context(), dec->hmac());
+       _raw_xml = xml_string;
+       auto xml = make_shared<cxml::Document>("SubtitleReel");
+       xml->read_string (xml_string);
        parse_xml (xml);
-       read_mxf_descriptor (reader, dec);
+       read_mxf_descriptor(reader);
+       read_mxf_resources (reader, dec);
 }
 
-list<shared_ptr<LoadFontNode> >
+
+vector<shared_ptr<LoadFontNode>>
 SMPTESubtitleAsset::load_font_nodes () const
 {
-       list<shared_ptr<LoadFontNode> > lf;
-       copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
+       vector<shared_ptr<LoadFontNode>> lf;
+       copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter(lf));
        return lf;
 }
 
+
 bool
 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
 {
        ASDCP::TimedText::MXFReader reader;
-       Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
+       Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
+       auto r = reader.OpenRead(dcp::filesystem::fix_long_path(file).string().c_str());
+       Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
        return !ASDCP_FAILURE (r);
 }
 
+
 string
 SMPTESubtitleAsset::xml_as_string () const
 {
        xmlpp::Document doc;
-       xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel");
-       root->set_namespace_declaration ("http://www.smpte-ra.org/schemas/428-7/2010/DCST", "dcst");
-       root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
+       auto root = doc.create_root_node ("SubtitleReel");
 
-       root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _xml_id);
-       root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
+       DCP_ASSERT (_xml_id);
+       root->add_child("Id")->add_child_text("urn:uuid:" + *_xml_id);
+       root->add_child("ContentTitleText")->add_child_text(_content_title_text);
        if (_annotation_text) {
-               root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
+               root->add_child("AnnotationText")->add_child_text(_annotation_text.get());
        }
-       root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
+       root->add_child("IssueDate")->add_child_text(_issue_date.as_string(false, false));
        if (_reel_number) {
-               root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
+               root->add_child("ReelNumber")->add_child_text(raw_convert<string>(_reel_number.get()));
        }
        if (_language) {
-               root->add_child("Language", "dcst")->add_child_text (_language.get ());
+               root->add_child("Language")->add_child_text(_language.get());
        }
-       root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
-       root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
+       root->add_child("EditRate")->add_child_text(_edit_rate.as_string());
+       root->add_child("TimeCodeRate")->add_child_text(raw_convert<string>(_time_code_rate));
        if (_start_time) {
-               root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string (SMPTE));
+               root->add_child("StartTime")->add_child_text(_start_time.get().as_string(Standard::SMPTE));
        }
 
-       BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) {
-               xmlpp::Element* load_font = root->add_child("LoadFont", "dcst");
+       for (auto i: _load_font_nodes) {
+               auto load_font = root->add_child("LoadFont");
                load_font->add_child_text ("urn:uuid:" + i->urn);
                load_font->set_attribute ("ID", i->id);
        }
 
-       subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, SMPTE);
+       subtitles_as_xml (root->add_child("SubtitleList"), _time_code_rate, Standard::SMPTE);
 
-       return doc.write_to_string ("UTF-8");
+       return format_xml(doc, std::make_pair(string{}, schema_namespace()));
 }
 
-/** Write this content to a MXF file */
+
 void
 SMPTESubtitleAsset::write (boost::filesystem::path p) const
 {
-       EncryptionContext enc (key(), SMPTE);
+       EncryptionContext enc (key(), Standard::SMPTE);
 
        ASDCP::WriterInfo writer_info;
        fill_writer_info (&writer_info, _id);
@@ -324,8 +413,8 @@ SMPTESubtitleAsset::write (boost::filesystem::path p) const
 
        /* Font references */
 
-       BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
-               list<Font>::const_iterator j = _fonts.begin ();
+       for (auto i: _load_font_nodes) {
+               auto j = _fonts.begin();
                while (j != _fonts.end() && j->load_id != i->id) {
                        ++j;
                }
@@ -341,8 +430,8 @@ SMPTESubtitleAsset::write (boost::filesystem::path p) const
 
        /* Image subtitle references */
 
-       BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
-               shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
+       for (auto i: _subtitles) {
+               auto si = dynamic_pointer_cast<SubtitleImage>(i);
                if (si) {
                        ASDCP::TimedText::TimedTextResourceDescriptor res;
                        unsigned int c;
@@ -353,34 +442,43 @@ SMPTESubtitleAsset::write (boost::filesystem::path p) const
                }
        }
 
-       descriptor.NamespaceName = "dcst";
-       memcpy (descriptor.AssetID, writer_info.AssetUUID, ASDCP::UUIDlen);
+       descriptor.NamespaceName = schema_namespace();
+       unsigned int c;
+       DCP_ASSERT (_xml_id);
+       Kumu::hex2bin (_xml_id->c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
+       DCP_ASSERT (c == Kumu::UUID_Length);
        descriptor.ContainerDuration = _intrinsic_duration;
 
        ASDCP::TimedText::MXFWriter writer;
-       ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor);
+       /* This header size is a guess.  Empirically it seems that each subtitle reference is 90 bytes, and we need some extra.
+          The default size is not enough for some feature-length PNG sub projects (see DCP-o-matic #1561).
+       */
+       ASDCP::Result_t r = writer.OpenWrite(dcp::filesystem::fix_long_path(p).string().c_str(), writer_info, descriptor, _subtitles.size() * 90 + 16384);
        if (ASDCP_FAILURE (r)) {
                boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
        }
 
-       r = writer.WriteTimedTextResource (xml_as_string (), enc.context(), enc.hmac());
+       _raw_xml = xml_as_string ();
+
+       r = writer.WriteTimedTextResource (*_raw_xml, enc.context(), enc.hmac());
        if (ASDCP_FAILURE (r)) {
                boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
        }
 
        /* Font payload */
 
-       BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
-               list<Font>::const_iterator j = _fonts.begin ();
+       for (auto i: _load_font_nodes) {
+               auto j = _fonts.begin();
                while (j != _fonts.end() && j->load_id != i->id) {
                        ++j;
                }
                if (j != _fonts.end ()) {
                        ASDCP::TimedText::FrameBuffer buffer;
-                       buffer.SetData (j->data.data().get(), j->data.size());
+                       ArrayData data_copy(j->data);
+                       buffer.SetData (data_copy.data(), data_copy.size());
                        buffer.Size (j->data.size());
                        r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
-                       if (ASDCP_FAILURE (r)) {
+                       if (ASDCP_FAILURE(r)) {
                                boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
                        }
                }
@@ -388,11 +486,11 @@ SMPTESubtitleAsset::write (boost::filesystem::path p) const
 
        /* Image subtitle payload */
 
-       BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
-               shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
+       for (auto i: _subtitles) {
+               auto si = dynamic_pointer_cast<SubtitleImage>(i);
                if (si) {
                        ASDCP::TimedText::FrameBuffer buffer;
-                       buffer.SetData (si->png_image().data().get(), si->png_image().size());
+                       buffer.SetData (si->png_image().data(), si->png_image().size());
                        buffer.Size (si->png_image().size());
                        r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
                        if (ASDCP_FAILURE(r)) {
@@ -407,29 +505,29 @@ SMPTESubtitleAsset::write (boost::filesystem::path p) const
 }
 
 bool
-SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
+SMPTESubtitleAsset::equals(shared_ptr<const Asset> other_asset, EqualityOptions const& options, NoteHandler note) const
 {
        if (!SubtitleAsset::equals (other_asset, options, note)) {
                return false;
        }
 
-       shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
+       auto other = dynamic_pointer_cast<const SMPTESubtitleAsset>(other_asset);
        if (!other) {
-               note (DCP_ERROR, "Subtitles are in different standards");
+               note (NoteType::ERROR, "Subtitles are in different standards");
                return false;
        }
 
-       list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
-       list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
+       auto i = _load_font_nodes.begin();
+       auto j = other->_load_font_nodes.begin();
 
        while (i != _load_font_nodes.end ()) {
                if (j == other->_load_font_nodes.end ()) {
-                       note (DCP_ERROR, "<LoadFont> nodes differ");
+                       note (NoteType::ERROR, "<LoadFont> nodes differ");
                        return false;
                }
 
                if ((*i)->id != (*j)->id) {
-                       note (DCP_ERROR, "<LoadFont> nodes differ");
+                       note (NoteType::ERROR, "<LoadFont> nodes differ");
                        return false;
                }
 
@@ -438,63 +536,83 @@ SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions
        }
 
        if (_content_title_text != other->_content_title_text) {
-               note (DCP_ERROR, "Subtitle content title texts differ");
+               note (NoteType::ERROR, "Subtitle content title texts differ");
                return false;
        }
 
        if (_language != other->_language) {
-               note (DCP_ERROR, "Subtitle languages differ");
+               note (NoteType::ERROR, String::compose("Subtitle languages differ (`%1' vs `%2')", _language.get_value_or("[none]"), other->_language.get_value_or("[none]")));
                return false;
        }
 
        if (_annotation_text != other->_annotation_text) {
-               note (DCP_ERROR, "Subtitle annotation texts differ");
+               note (NoteType::ERROR, "Subtitle annotation texts differ");
                return false;
        }
 
        if (_issue_date != other->_issue_date) {
                if (options.issue_dates_can_differ) {
-                       note (DCP_NOTE, "Subtitle issue dates differ");
+                       note (NoteType::NOTE, "Subtitle issue dates differ");
                } else {
-                       note (DCP_ERROR, "Subtitle issue dates differ");
+                       note (NoteType::ERROR, "Subtitle issue dates differ");
                        return false;
                }
        }
 
        if (_reel_number != other->_reel_number) {
-               note (DCP_ERROR, "Subtitle reel numbers differ");
+               note (NoteType::ERROR, "Subtitle reel numbers differ");
                return false;
        }
 
        if (_edit_rate != other->_edit_rate) {
-               note (DCP_ERROR, "Subtitle edit rates differ");
+               note (NoteType::ERROR, "Subtitle edit rates differ");
                return false;
        }
 
        if (_time_code_rate != other->_time_code_rate) {
-               note (DCP_ERROR, "Subtitle time code rates differ");
+               note (NoteType::ERROR, "Subtitle time code rates differ");
                return false;
        }
 
        if (_start_time != other->_start_time) {
-               note (DCP_ERROR, "Subtitle start times differ");
+               note (NoteType::ERROR, "Subtitle start times differ");
                return false;
        }
 
        return true;
 }
 
+
 void
-SMPTESubtitleAsset::add_font (string load_id, boost::filesystem::path file)
+SMPTESubtitleAsset::add_font (string load_id, dcp::ArrayData data)
 {
        string const uuid = make_uuid ();
-       _fonts.push_back (Font (load_id, uuid, file));
-       _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
+       _fonts.push_back (Font(load_id, uuid, data));
+       _load_font_nodes.push_back (make_shared<SMPTELoadFontNode>(load_id, uuid));
 }
 
+
 void
 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
 {
        SubtitleAsset::add (s);
-       _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
+       _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);
+}
+
+
+string
+SMPTESubtitleAsset::schema_namespace() const
+{
+       switch (_subtitle_standard) {
+       case SubtitleStandard::SMPTE_2007:
+               return subtitle_smpte_ns_2007;
+       case SubtitleStandard::SMPTE_2010:
+               return subtitle_smpte_ns_2010;
+       case SubtitleStandard::SMPTE_2014:
+               return subtitle_smpte_ns_2014;
+       default:
+               DCP_ASSERT(false);
+       }
+
+       DCP_ASSERT(false);
 }