2 Copyright (C) 2012-2016 Carl Hetherington <cth@carlh.net>
4 This file is part of libdcp.
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.
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.
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/>.
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
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.
34 /** @file src/smpte_subtitle_asset.cc
35 * @brief SMPTESubtitleAsset class.
38 #include "smpte_subtitle_asset.h"
39 #include "smpte_load_font_node.h"
40 #include "exceptions.h"
42 #include "raw_convert.h"
43 #include "dcp_assert.h"
45 #include "compose.hpp"
46 #include "encryption_context.h"
47 #include "decryption_context.h"
48 #include <asdcp/AS_DCP.h>
49 #include <asdcp/KM_util.h>
50 #include <libxml++/libxml++.h>
51 #include <boost/foreach.hpp>
52 #include <boost/algorithm/string.hpp>
58 using boost::shared_ptr;
60 using boost::is_any_of;
61 using boost::shared_array;
62 using boost::dynamic_pointer_cast;
65 SMPTESubtitleAsset::SMPTESubtitleAsset ()
67 , _intrinsic_duration (0)
69 , _time_code_rate (24)
70 , _xml_id (make_uuid ())
75 /** Construct a SMPTESubtitleAsset by reading an MXF or XML file.
76 * @param file Filename.
78 SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file)
79 : SubtitleAsset (file)
81 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
83 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
84 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
85 if (!ASDCP_FAILURE (r)) {
87 ASDCP::WriterInfo info;
88 reader->FillWriterInfo (info);
89 _id = read_writer_info (info);
91 /* Not encrypted; read it in now */
93 reader->ReadTimedTextResource (s);
96 read_mxf_descriptor (reader, shared_ptr<DecryptionContext> (new DecryptionContext ()));
101 xml.reset (new cxml::Document ("SubtitleReel"));
102 xml->read_file (file);
104 _id = _xml_id = remove_urn_uuid (xml->string_child ("Id"));
105 } catch (cxml::Error& e) {
106 boost::throw_exception (
109 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
110 file, static_cast<int> (r), e.what ()
119 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
121 _xml_id = remove_urn_uuid(xml->string_child("Id"));
122 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
124 _content_title_text = xml->string_child ("ContentTitleText");
125 _annotation_text = xml->optional_string_child ("AnnotationText");
126 _issue_date = LocalTime (xml->string_child ("IssueDate"));
127 _reel_number = xml->optional_number_child<int> ("ReelNumber");
128 _language = xml->optional_string_child ("Language");
130 /* This is supposed to be two numbers, but a single number has been seen in the wild */
131 string const er = xml->string_child ("EditRate");
132 vector<string> er_parts;
133 split (er_parts, er, is_any_of (" "));
134 if (er_parts.size() == 1) {
135 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
136 } else if (er_parts.size() == 2) {
137 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
139 throw XMLError ("malformed EditRate " + er);
142 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
143 if (xml->optional_string_child ("StartTime")) {
144 _start_time = Time (xml->string_child ("StartTime"), _time_code_rate);
147 /* Now we need to drop down to xmlpp */
150 xmlpp::Node::NodeList c = xml->node()->get_children ();
151 for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
152 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
153 if (e && e->get_name() == "SubtitleList") {
154 parse_subtitles (e, ps, _time_code_rate, SMPTE);
158 /* Guess intrinsic duration */
159 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
163 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
165 ASDCP::TimedText::TimedTextDescriptor descriptor;
166 reader->FillTimedTextDescriptor (descriptor);
171 ASDCP::TimedText::ResourceList_t::const_iterator i = descriptor.ResourceList.begin();
172 i != descriptor.ResourceList.end();
175 if (i->Type == ASDCP::TimedText::MT_OPENTYPE) {
176 ASDCP::TimedText::FrameBuffer buffer;
177 buffer.Capacity (10 * 1024 * 1024);
178 reader->ReadAncillaryResource (i->ResourceID, buffer, dec->decryption());
181 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof (id));
183 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
184 memcpy (data.get(), buffer.RoData(), buffer.Size());
186 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = _load_font_nodes.begin ();
187 while (j != _load_font_nodes.end() && (*j)->urn != id) {
191 if (j != _load_font_nodes.end ()) {
192 _fonts.push_back (Font ((*j)->id, (*j)->urn, Data (data, buffer.Size ())));
197 /* Get intrinsic duration */
198 _intrinsic_duration = descriptor.ContainerDuration;
202 SMPTESubtitleAsset::set_key (Key key)
204 /* See if we already have a key; if we do, and we have a file, we'll already
207 bool const had_key = static_cast<bool> (_key);
211 if (!_key_id || !_file || had_key) {
212 /* Either we don't have any data to read, it wasn't
213 encrypted, or we've already read it, so we don't
214 need to do anything else.
219 /* Our data was encrypted; now we can decrypt it */
221 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
222 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
223 if (ASDCP_FAILURE (r)) {
224 boost::throw_exception (
226 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
232 shared_ptr<DecryptionContext> dec (new DecryptionContext (key));
233 reader->ReadTimedTextResource (s, dec->decryption());
234 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
235 xml->read_string (s);
237 read_mxf_descriptor (reader, dec);
240 list<shared_ptr<LoadFontNode> >
241 SMPTESubtitleAsset::load_font_nodes () const
243 list<shared_ptr<LoadFontNode> > lf;
244 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
249 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
251 ASDCP::TimedText::MXFReader reader;
252 Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
253 return !ASDCP_FAILURE (r);
257 SMPTESubtitleAsset::xml_as_string () const
260 xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel");
261 root->set_namespace_declaration ("http://www.smpte-ra.org/schemas/428-7/2010/DCST", "dcst");
262 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
264 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _xml_id);
265 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
266 if (_annotation_text) {
267 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
269 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
271 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
274 root->add_child("Language", "dcst")->add_child_text (_language.get ());
276 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
277 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
279 root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string (SMPTE));
282 BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) {
283 xmlpp::Element* load_font = root->add_child("LoadFont", "dcst");
284 load_font->add_child_text ("urn:uuid:" + i->urn);
285 load_font->set_attribute ("ID", i->id);
288 subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, SMPTE);
290 return doc.write_to_string ("UTF-8");
293 /** Write this content to a MXF file */
295 SMPTESubtitleAsset::write (boost::filesystem::path p) const
297 EncryptionContext enc (key(), SMPTE);
299 ASDCP::WriterInfo writer_info;
300 fill_writer_info (&writer_info, _id);
302 ASDCP::TimedText::TimedTextDescriptor descriptor;
303 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
304 descriptor.EncodingName = "UTF-8";
306 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
307 list<Font>::const_iterator j = _fonts.begin ();
308 while (j != _fonts.end() && j->load_id != i->id) {
311 if (j != _fonts.end ()) {
312 ASDCP::TimedText::TimedTextResourceDescriptor res;
314 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
315 DCP_ASSERT (c == Kumu::UUID_Length);
316 res.Type = ASDCP::TimedText::MT_OPENTYPE;
317 descriptor.ResourceList.push_back (res);
321 descriptor.NamespaceName = "dcst";
322 memcpy (descriptor.AssetID, writer_info.AssetUUID, ASDCP::UUIDlen);
323 descriptor.ContainerDuration = _intrinsic_duration;
325 ASDCP::TimedText::MXFWriter writer;
326 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor);
327 if (ASDCP_FAILURE (r)) {
328 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
331 /* XXX: no encryption */
332 r = writer.WriteTimedTextResource (xml_as_string (), enc.encryption(), enc.hmac());
333 if (ASDCP_FAILURE (r)) {
334 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
337 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
338 list<Font>::const_iterator j = _fonts.begin ();
339 while (j != _fonts.end() && j->load_id != i->id) {
342 if (j != _fonts.end ()) {
343 ASDCP::TimedText::FrameBuffer buffer;
344 buffer.SetData (j->data.data().get(), j->data.size());
345 buffer.Size (j->data.size());
346 r = writer.WriteAncillaryResource (buffer, enc.encryption(), enc.hmac());
347 if (ASDCP_FAILURE (r)) {
348 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
359 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
361 if (!SubtitleAsset::equals (other_asset, options, note)) {
365 shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
367 note (DCP_ERROR, "Subtitles are in different standards");
371 list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
372 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
374 while (i != _load_font_nodes.end ()) {
375 if (j == other->_load_font_nodes.end ()) {
376 note (DCP_ERROR, "<LoadFont> nodes differ");
380 if ((*i)->id != (*j)->id) {
381 note (DCP_ERROR, "<LoadFont> nodes differ");
389 if (_content_title_text != other->_content_title_text) {
390 note (DCP_ERROR, "Subtitle content title texts differ");
394 if (_language != other->_language) {
395 note (DCP_ERROR, "Subtitle languages differ");
399 if (_annotation_text != other->_annotation_text) {
400 note (DCP_ERROR, "Subtitle annotation texts differ");
404 if (_issue_date != other->_issue_date) {
405 if (options.issue_dates_can_differ) {
406 note (DCP_NOTE, "Subtitle issue dates differ");
408 note (DCP_ERROR, "Subtitle issue dates differ");
413 if (_reel_number != other->_reel_number) {
414 note (DCP_ERROR, "Subtitle reel numbers differ");
418 if (_edit_rate != other->_edit_rate) {
419 note (DCP_ERROR, "Subtitle edit rates differ");
423 if (_time_code_rate != other->_time_code_rate) {
424 note (DCP_ERROR, "Subtitle time code rates differ");
428 if (_start_time != other->_start_time) {
429 note (DCP_ERROR, "Subtitle start times differ");
437 SMPTESubtitleAsset::add_font (string load_id, boost::filesystem::path file)
439 string const uuid = make_uuid ();
440 _fonts.push_back (Font (load_id, uuid, file));
441 _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
445 SMPTESubtitleAsset::add (dcp::SubtitleString s)
447 SubtitleAsset::add (s);
448 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);