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 ()
66 : _intrinsic_duration (0)
68 , _time_code_rate (24)
73 /** Construct a SMPTESubtitleAsset by reading an MXF or XML file.
74 * @param file Filename.
76 SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file)
77 : SubtitleAsset (file)
79 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
81 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
82 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
83 if (!ASDCP_FAILURE (r)) {
85 ASDCP::WriterInfo info;
86 reader->FillWriterInfo (info);
87 _id = read_writer_info (info);
89 /* Not encrypted; read it in now */
91 reader->ReadTimedTextResource (s);
94 read_mxf_descriptor (reader, shared_ptr<DecryptionContext> (new DecryptionContext ()));
99 xml.reset (new cxml::Document ("SubtitleReel"));
100 xml->read_file (file);
102 _id = remove_urn_uuid (xml->string_child ("Id"));
103 } catch (cxml::Error& e) {
104 boost::throw_exception (
107 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
108 file, static_cast<int> (r), e.what ()
117 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
119 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
121 _content_title_text = xml->string_child ("ContentTitleText");
122 _annotation_text = xml->optional_string_child ("AnnotationText");
123 _issue_date = LocalTime (xml->string_child ("IssueDate"));
124 _reel_number = xml->optional_number_child<int> ("ReelNumber");
125 _language = xml->optional_string_child ("Language");
127 /* This is supposed to be two numbers, but a single number has been seen in the wild */
128 string const er = xml->string_child ("EditRate");
129 vector<string> er_parts;
130 split (er_parts, er, is_any_of (" "));
131 if (er_parts.size() == 1) {
132 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
133 } else if (er_parts.size() == 2) {
134 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
136 throw XMLError ("malformed EditRate " + er);
139 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
140 if (xml->optional_string_child ("StartTime")) {
141 _start_time = Time (xml->string_child ("StartTime"), _time_code_rate);
144 /* Now we need to drop down to xmlpp */
147 xmlpp::Node::NodeList c = xml->node()->get_children ();
148 for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
149 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
150 if (e && e->get_name() == "SubtitleList") {
151 parse_subtitles (e, ps, _time_code_rate, SMPTE);
155 /* Guess intrinsic duration */
156 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
160 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
162 ASDCP::TimedText::TimedTextDescriptor descriptor;
163 reader->FillTimedTextDescriptor (descriptor);
168 ASDCP::TimedText::ResourceList_t::const_iterator i = descriptor.ResourceList.begin();
169 i != descriptor.ResourceList.end();
172 if (i->Type == ASDCP::TimedText::MT_OPENTYPE) {
173 ASDCP::TimedText::FrameBuffer buffer;
174 buffer.Capacity (10 * 1024 * 1024);
175 reader->ReadAncillaryResource (i->ResourceID, buffer, dec->decryption());
178 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof (id));
180 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
181 memcpy (data.get(), buffer.RoData(), buffer.Size());
183 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = _load_font_nodes.begin ();
184 while (j != _load_font_nodes.end() && (*j)->urn != id) {
188 if (j != _load_font_nodes.end ()) {
189 _fonts.push_back (Font ((*j)->id, (*j)->urn, Data (data, buffer.Size ())));
194 /* Get intrinsic duration */
195 _intrinsic_duration = descriptor.ContainerDuration;
199 SMPTESubtitleAsset::set_key (Key key)
203 if (!_key_id || !_file) {
204 /* Either we don't have any data to read, or it wasn't
205 encrypted, so we don't need to do anything else.
210 /* Our data was encrypted; now we can decrypt it */
212 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
213 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
214 if (ASDCP_FAILURE (r)) {
215 boost::throw_exception (
217 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
223 shared_ptr<DecryptionContext> dec (new DecryptionContext (key));
224 reader->ReadTimedTextResource (s, dec->decryption());
225 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
226 xml->read_string (s);
228 read_mxf_descriptor (reader, dec);
231 list<shared_ptr<LoadFontNode> >
232 SMPTESubtitleAsset::load_font_nodes () const
234 list<shared_ptr<LoadFontNode> > lf;
235 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
240 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
242 ASDCP::TimedText::MXFReader reader;
243 Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
244 return !ASDCP_FAILURE (r);
248 SMPTESubtitleAsset::xml_as_string () const
251 xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel");
252 root->set_namespace_declaration ("http://www.smpte-ra.org/schemas/428-7/2010/DCST", "dcst");
253 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
255 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _id);
256 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
257 if (_annotation_text) {
258 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
260 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
262 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
265 root->add_child("Language", "dcst")->add_child_text (_language.get ());
267 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
268 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
270 root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string (SMPTE));
273 BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) {
274 xmlpp::Element* load_font = root->add_child("LoadFont", "dcst");
275 load_font->add_child_text ("urn:uuid:" + i->urn);
276 load_font->set_attribute ("ID", i->id);
279 subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, SMPTE);
281 return doc.write_to_string ("UTF-8");
284 /** Write this content to a MXF file */
286 SMPTESubtitleAsset::write (boost::filesystem::path p) const
288 EncryptionContext enc (key (), SMPTE);
290 ASDCP::WriterInfo writer_info;
291 fill_writer_info (&writer_info, _id, SMPTE);
293 ASDCP::TimedText::TimedTextDescriptor descriptor;
294 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
295 descriptor.EncodingName = "UTF-8";
297 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
298 list<Font>::const_iterator j = _fonts.begin ();
299 while (j != _fonts.end() && j->load_id != i->id) {
302 if (j != _fonts.end ()) {
303 ASDCP::TimedText::TimedTextResourceDescriptor res;
305 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
306 DCP_ASSERT (c == Kumu::UUID_Length);
307 res.Type = ASDCP::TimedText::MT_OPENTYPE;
308 descriptor.ResourceList.push_back (res);
312 descriptor.NamespaceName = "dcst";
313 memcpy (descriptor.AssetID, writer_info.AssetUUID, ASDCP::UUIDlen);
314 descriptor.ContainerDuration = _intrinsic_duration;
316 ASDCP::TimedText::MXFWriter writer;
317 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor);
318 if (ASDCP_FAILURE (r)) {
319 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
322 /* XXX: no encryption */
323 r = writer.WriteTimedTextResource (xml_as_string (), enc.encryption(), enc.hmac());
324 if (ASDCP_FAILURE (r)) {
325 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
328 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
329 list<Font>::const_iterator j = _fonts.begin ();
330 while (j != _fonts.end() && j->load_id != i->id) {
333 if (j != _fonts.end ()) {
334 ASDCP::TimedText::FrameBuffer buffer;
335 buffer.SetData (j->data.data().get(), j->data.size());
336 buffer.Size (j->data.size());
337 r = writer.WriteAncillaryResource (buffer, enc.encryption(), enc.hmac());
338 if (ASDCP_FAILURE (r)) {
339 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
350 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
352 if (!SubtitleAsset::equals (other_asset, options, note)) {
356 shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
358 note (DCP_ERROR, "Subtitles are in different standards");
362 list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
363 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
365 while (i != _load_font_nodes.end ()) {
366 if (j == other->_load_font_nodes.end ()) {
367 note (DCP_ERROR, "<LoadFont> nodes differ");
371 if ((*i)->id != (*j)->id) {
372 note (DCP_ERROR, "<LoadFont> nodes differ");
380 if (_content_title_text != other->_content_title_text) {
381 note (DCP_ERROR, "Subtitle content title texts differ");
385 if (_language != other->_language) {
386 note (DCP_ERROR, "Subtitle languages differ");
390 if (_annotation_text != other->_annotation_text) {
391 note (DCP_ERROR, "Subtitle annotation texts differ");
395 if (_issue_date != other->_issue_date) {
396 if (options.issue_dates_can_differ) {
397 note (DCP_NOTE, "Subtitle issue dates differ");
399 note (DCP_ERROR, "Subtitle issue dates differ");
404 if (_reel_number != other->_reel_number) {
405 note (DCP_ERROR, "Subtitle reel numbers differ");
409 if (_edit_rate != other->_edit_rate) {
410 note (DCP_ERROR, "Subtitle edit rates differ");
414 if (_time_code_rate != other->_time_code_rate) {
415 note (DCP_ERROR, "Subtitle time code rates differ");
419 if (_start_time != other->_start_time) {
420 note (DCP_ERROR, "Subtitle start times differ");
428 SMPTESubtitleAsset::add_font (string load_id, boost::filesystem::path file)
430 string const uuid = make_uuid ();
431 _fonts.push_back (Font (load_id, uuid, file));
432 _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
436 SMPTESubtitleAsset::add (dcp::SubtitleString s)
438 SubtitleAsset::add (s);
439 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);