2 Copyright (C) 2012-2018 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 "crypto_context.h"
47 #include "crypto_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;
63 using boost::optional;
66 SMPTESubtitleAsset::SMPTESubtitleAsset ()
68 , _intrinsic_duration (0)
70 , _time_code_rate (24)
71 , _xml_id (make_uuid ())
76 /** Construct a SMPTESubtitleAsset by reading an MXF or XML file.
77 * @param file Filename.
79 SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file)
80 : SubtitleAsset (file)
82 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
84 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
85 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
86 if (!ASDCP_FAILURE (r)) {
88 ASDCP::WriterInfo info;
89 reader->FillWriterInfo (info);
90 _id = read_writer_info (info);
92 /* Not encrypted; read it in now */
94 reader->ReadTimedTextResource (s);
97 read_mxf_descriptor (reader, shared_ptr<DecryptionContext> (new DecryptionContext (optional<Key>(), SMPTE)));
102 xml.reset (new cxml::Document ("SubtitleReel"));
103 xml->read_file (file);
105 _id = _xml_id = remove_urn_uuid (xml->string_child ("Id"));
106 } catch (cxml::Error& e) {
107 boost::throw_exception (
110 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
111 file, static_cast<int> (r), e.what ()
120 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
122 _xml_id = remove_urn_uuid(xml->string_child("Id"));
123 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
125 _content_title_text = xml->string_child ("ContentTitleText");
126 _annotation_text = xml->optional_string_child ("AnnotationText");
127 _issue_date = LocalTime (xml->string_child ("IssueDate"));
128 _reel_number = xml->optional_number_child<int> ("ReelNumber");
129 _language = xml->optional_string_child ("Language");
131 /* This is supposed to be two numbers, but a single number has been seen in the wild */
132 string const er = xml->string_child ("EditRate");
133 vector<string> er_parts;
134 split (er_parts, er, is_any_of (" "));
135 if (er_parts.size() == 1) {
136 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
137 } else if (er_parts.size() == 2) {
138 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
140 throw XMLError ("malformed EditRate " + er);
143 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
144 if (xml->optional_string_child ("StartTime")) {
145 _start_time = Time (xml->string_child ("StartTime"), _time_code_rate);
148 /* Now we need to drop down to xmlpp */
151 xmlpp::Node::NodeList c = xml->node()->get_children ();
152 for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
153 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
154 if (e && e->get_name() == "SubtitleList") {
155 parse_subtitles (e, ps, _time_code_rate, SMPTE);
159 /* Guess intrinsic duration */
160 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
164 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
166 ASDCP::TimedText::TimedTextDescriptor descriptor;
167 reader->FillTimedTextDescriptor (descriptor);
172 ASDCP::TimedText::ResourceList_t::const_iterator i = descriptor.ResourceList.begin();
173 i != descriptor.ResourceList.end();
176 if (i->Type == ASDCP::TimedText::MT_OPENTYPE) {
177 ASDCP::TimedText::FrameBuffer buffer;
178 buffer.Capacity (10 * 1024 * 1024);
179 reader->ReadAncillaryResource (i->ResourceID, buffer, dec->context(), dec->hmac());
182 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof (id));
184 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
185 memcpy (data.get(), buffer.RoData(), buffer.Size());
187 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = _load_font_nodes.begin ();
188 while (j != _load_font_nodes.end() && (*j)->urn != id) {
192 if (j != _load_font_nodes.end ()) {
193 _fonts.push_back (Font ((*j)->id, (*j)->urn, Data (data, buffer.Size ())));
198 /* Get intrinsic duration */
199 _intrinsic_duration = descriptor.ContainerDuration;
203 SMPTESubtitleAsset::set_key (Key key)
205 /* See if we already have a key; if we do, and we have a file, we'll already
208 bool const had_key = static_cast<bool> (_key);
212 if (!_key_id || !_file || had_key) {
213 /* Either we don't have any data to read, it wasn't
214 encrypted, or we've already read it, so we don't
215 need to do anything else.
220 /* Our data was encrypted; now we can decrypt it */
222 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
223 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
224 if (ASDCP_FAILURE (r)) {
225 boost::throw_exception (
227 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
233 shared_ptr<DecryptionContext> dec (new DecryptionContext (key, SMPTE));
234 reader->ReadTimedTextResource (s, dec->context(), dec->hmac());
235 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
236 xml->read_string (s);
238 read_mxf_descriptor (reader, dec);
241 list<shared_ptr<LoadFontNode> >
242 SMPTESubtitleAsset::load_font_nodes () const
244 list<shared_ptr<LoadFontNode> > lf;
245 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
250 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
252 ASDCP::TimedText::MXFReader reader;
253 Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
254 return !ASDCP_FAILURE (r);
258 SMPTESubtitleAsset::xml_as_string () const
261 xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel");
262 root->set_namespace_declaration ("http://www.smpte-ra.org/schemas/428-7/2010/DCST", "dcst");
263 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
265 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _xml_id);
266 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
267 if (_annotation_text) {
268 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
270 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
272 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
275 root->add_child("Language", "dcst")->add_child_text (_language.get ());
277 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
278 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
280 root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string (SMPTE));
283 BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) {
284 xmlpp::Element* load_font = root->add_child("LoadFont", "dcst");
285 load_font->add_child_text ("urn:uuid:" + i->urn);
286 load_font->set_attribute ("ID", i->id);
289 subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, SMPTE);
291 return doc.write_to_string ("UTF-8");
294 /** Write this content to a MXF file */
296 SMPTESubtitleAsset::write (boost::filesystem::path p) const
298 EncryptionContext enc (key(), SMPTE);
300 ASDCP::WriterInfo writer_info;
301 fill_writer_info (&writer_info, _id);
303 ASDCP::TimedText::TimedTextDescriptor descriptor;
304 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
305 descriptor.EncodingName = "UTF-8";
307 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
308 list<Font>::const_iterator j = _fonts.begin ();
309 while (j != _fonts.end() && j->load_id != i->id) {
312 if (j != _fonts.end ()) {
313 ASDCP::TimedText::TimedTextResourceDescriptor res;
315 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
316 DCP_ASSERT (c == Kumu::UUID_Length);
317 res.Type = ASDCP::TimedText::MT_OPENTYPE;
318 descriptor.ResourceList.push_back (res);
322 descriptor.NamespaceName = "dcst";
323 memcpy (descriptor.AssetID, writer_info.AssetUUID, ASDCP::UUIDlen);
324 descriptor.ContainerDuration = _intrinsic_duration;
326 ASDCP::TimedText::MXFWriter writer;
327 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor);
328 if (ASDCP_FAILURE (r)) {
329 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
332 /* XXX: no encryption */
333 r = writer.WriteTimedTextResource (xml_as_string (), enc.context(), enc.hmac());
334 if (ASDCP_FAILURE (r)) {
335 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
338 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
339 list<Font>::const_iterator j = _fonts.begin ();
340 while (j != _fonts.end() && j->load_id != i->id) {
343 if (j != _fonts.end ()) {
344 ASDCP::TimedText::FrameBuffer buffer;
345 buffer.SetData (j->data.data().get(), j->data.size());
346 buffer.Size (j->data.size());
347 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
348 if (ASDCP_FAILURE (r)) {
349 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
360 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
362 if (!SubtitleAsset::equals (other_asset, options, note)) {
366 shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
368 note (DCP_ERROR, "Subtitles are in different standards");
372 list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
373 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
375 while (i != _load_font_nodes.end ()) {
376 if (j == other->_load_font_nodes.end ()) {
377 note (DCP_ERROR, "<LoadFont> nodes differ");
381 if ((*i)->id != (*j)->id) {
382 note (DCP_ERROR, "<LoadFont> nodes differ");
390 if (_content_title_text != other->_content_title_text) {
391 note (DCP_ERROR, "Subtitle content title texts differ");
395 if (_language != other->_language) {
396 note (DCP_ERROR, "Subtitle languages differ");
400 if (_annotation_text != other->_annotation_text) {
401 note (DCP_ERROR, "Subtitle annotation texts differ");
405 if (_issue_date != other->_issue_date) {
406 if (options.issue_dates_can_differ) {
407 note (DCP_NOTE, "Subtitle issue dates differ");
409 note (DCP_ERROR, "Subtitle issue dates differ");
414 if (_reel_number != other->_reel_number) {
415 note (DCP_ERROR, "Subtitle reel numbers differ");
419 if (_edit_rate != other->_edit_rate) {
420 note (DCP_ERROR, "Subtitle edit rates differ");
424 if (_time_code_rate != other->_time_code_rate) {
425 note (DCP_ERROR, "Subtitle time code rates differ");
429 if (_start_time != other->_start_time) {
430 note (DCP_ERROR, "Subtitle start times differ");
438 SMPTESubtitleAsset::add_font (string load_id, boost::filesystem::path file)
440 string const uuid = make_uuid ();
441 _fonts.push_back (Font (load_id, uuid, file));
442 _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
446 SMPTESubtitleAsset::add (dcp::SubtitleString s)
448 SubtitleAsset::add (s);
449 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);