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)
201 /* See if we already have a key; if we do, and we have a file, we'll already
204 bool const had_key = static_cast<bool> (_key);
208 if (!_key_id || !_file || had_key) {
209 /* Either we don't have any data to read, it wasn't
210 encrypted, or we've already read it, so we don't
211 need to do anything else.
216 /* Our data was encrypted; now we can decrypt it */
218 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
219 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
220 if (ASDCP_FAILURE (r)) {
221 boost::throw_exception (
223 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
229 shared_ptr<DecryptionContext> dec (new DecryptionContext (key));
230 reader->ReadTimedTextResource (s, dec->decryption());
231 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
232 xml->read_string (s);
234 read_mxf_descriptor (reader, dec);
237 list<shared_ptr<LoadFontNode> >
238 SMPTESubtitleAsset::load_font_nodes () const
240 list<shared_ptr<LoadFontNode> > lf;
241 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
246 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
248 ASDCP::TimedText::MXFReader reader;
249 Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
250 return !ASDCP_FAILURE (r);
254 SMPTESubtitleAsset::xml_as_string () const
257 xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel");
258 root->set_namespace_declaration ("http://www.smpte-ra.org/schemas/428-7/2010/DCST", "dcst");
259 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
261 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _id);
262 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
263 if (_annotation_text) {
264 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
266 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
268 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
271 root->add_child("Language", "dcst")->add_child_text (_language.get ());
273 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
274 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
276 root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string (SMPTE));
279 BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) {
280 xmlpp::Element* load_font = root->add_child("LoadFont", "dcst");
281 load_font->add_child_text ("urn:uuid:" + i->urn);
282 load_font->set_attribute ("ID", i->id);
285 subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, SMPTE);
287 return doc.write_to_string ("UTF-8");
290 /** Write this content to a MXF file */
292 SMPTESubtitleAsset::write (boost::filesystem::path p) const
294 EncryptionContext enc (key (), SMPTE);
296 ASDCP::WriterInfo writer_info;
297 fill_writer_info (&writer_info, _id, SMPTE);
299 ASDCP::TimedText::TimedTextDescriptor descriptor;
300 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
301 descriptor.EncodingName = "UTF-8";
303 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
304 list<Font>::const_iterator j = _fonts.begin ();
305 while (j != _fonts.end() && j->load_id != i->id) {
308 if (j != _fonts.end ()) {
309 ASDCP::TimedText::TimedTextResourceDescriptor res;
311 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
312 DCP_ASSERT (c == Kumu::UUID_Length);
313 res.Type = ASDCP::TimedText::MT_OPENTYPE;
314 descriptor.ResourceList.push_back (res);
318 descriptor.NamespaceName = "dcst";
319 memcpy (descriptor.AssetID, writer_info.AssetUUID, ASDCP::UUIDlen);
320 descriptor.ContainerDuration = _intrinsic_duration;
322 ASDCP::TimedText::MXFWriter writer;
323 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor);
324 if (ASDCP_FAILURE (r)) {
325 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
328 /* XXX: no encryption */
329 r = writer.WriteTimedTextResource (xml_as_string (), enc.encryption(), enc.hmac());
330 if (ASDCP_FAILURE (r)) {
331 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
334 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
335 list<Font>::const_iterator j = _fonts.begin ();
336 while (j != _fonts.end() && j->load_id != i->id) {
339 if (j != _fonts.end ()) {
340 ASDCP::TimedText::FrameBuffer buffer;
341 buffer.SetData (j->data.data().get(), j->data.size());
342 buffer.Size (j->data.size());
343 r = writer.WriteAncillaryResource (buffer, enc.encryption(), enc.hmac());
344 if (ASDCP_FAILURE (r)) {
345 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
356 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
358 if (!SubtitleAsset::equals (other_asset, options, note)) {
362 shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
364 note (DCP_ERROR, "Subtitles are in different standards");
368 list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
369 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
371 while (i != _load_font_nodes.end ()) {
372 if (j == other->_load_font_nodes.end ()) {
373 note (DCP_ERROR, "<LoadFont> nodes differ");
377 if ((*i)->id != (*j)->id) {
378 note (DCP_ERROR, "<LoadFont> nodes differ");
386 if (_content_title_text != other->_content_title_text) {
387 note (DCP_ERROR, "Subtitle content title texts differ");
391 if (_language != other->_language) {
392 note (DCP_ERROR, "Subtitle languages differ");
396 if (_annotation_text != other->_annotation_text) {
397 note (DCP_ERROR, "Subtitle annotation texts differ");
401 if (_issue_date != other->_issue_date) {
402 if (options.issue_dates_can_differ) {
403 note (DCP_NOTE, "Subtitle issue dates differ");
405 note (DCP_ERROR, "Subtitle issue dates differ");
410 if (_reel_number != other->_reel_number) {
411 note (DCP_ERROR, "Subtitle reel numbers differ");
415 if (_edit_rate != other->_edit_rate) {
416 note (DCP_ERROR, "Subtitle edit rates differ");
420 if (_time_code_rate != other->_time_code_rate) {
421 note (DCP_ERROR, "Subtitle time code rates differ");
425 if (_start_time != other->_start_time) {
426 note (DCP_ERROR, "Subtitle start times differ");
434 SMPTESubtitleAsset::add_font (string load_id, boost::filesystem::path file)
436 string const uuid = make_uuid ();
437 _fonts.push_back (Font (load_id, uuid, file));
438 _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
442 SMPTESubtitleAsset::add (dcp::SubtitleString s)
444 SubtitleAsset::add (s);
445 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);