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)
69 , _xml_id (make_uuid ())
74 /** Construct a SMPTESubtitleAsset by reading an MXF or XML file.
75 * @param file Filename.
77 SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file)
78 : SubtitleAsset (file)
80 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
82 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
83 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
84 if (!ASDCP_FAILURE (r)) {
86 ASDCP::WriterInfo info;
87 reader->FillWriterInfo (info);
88 _id = read_writer_info (info);
90 /* Not encrypted; read it in now */
92 reader->ReadTimedTextResource (s);
95 read_mxf_descriptor (reader, shared_ptr<DecryptionContext> (new DecryptionContext ()));
100 xml.reset (new cxml::Document ("SubtitleReel"));
101 xml->read_file (file);
103 _id = _xml_id = remove_urn_uuid (xml->string_child ("Id"));
104 } catch (cxml::Error& e) {
105 boost::throw_exception (
108 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
109 file, static_cast<int> (r), e.what ()
118 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
120 _xml_id = remove_urn_uuid(xml->string_child("Id"));
121 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
123 _content_title_text = xml->string_child ("ContentTitleText");
124 _annotation_text = xml->optional_string_child ("AnnotationText");
125 _issue_date = LocalTime (xml->string_child ("IssueDate"));
126 _reel_number = xml->optional_number_child<int> ("ReelNumber");
127 _language = xml->optional_string_child ("Language");
129 /* This is supposed to be two numbers, but a single number has been seen in the wild */
130 string const er = xml->string_child ("EditRate");
131 vector<string> er_parts;
132 split (er_parts, er, is_any_of (" "));
133 if (er_parts.size() == 1) {
134 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
135 } else if (er_parts.size() == 2) {
136 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
138 throw XMLError ("malformed EditRate " + er);
141 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
142 if (xml->optional_string_child ("StartTime")) {
143 _start_time = Time (xml->string_child ("StartTime"), _time_code_rate);
146 /* Now we need to drop down to xmlpp */
149 xmlpp::Node::NodeList c = xml->node()->get_children ();
150 for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
151 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
152 if (e && e->get_name() == "SubtitleList") {
153 parse_subtitles (e, ps, _time_code_rate, SMPTE);
157 /* Guess intrinsic duration */
158 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
162 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
164 ASDCP::TimedText::TimedTextDescriptor descriptor;
165 reader->FillTimedTextDescriptor (descriptor);
170 ASDCP::TimedText::ResourceList_t::const_iterator i = descriptor.ResourceList.begin();
171 i != descriptor.ResourceList.end();
174 if (i->Type == ASDCP::TimedText::MT_OPENTYPE) {
175 ASDCP::TimedText::FrameBuffer buffer;
176 buffer.Capacity (10 * 1024 * 1024);
177 reader->ReadAncillaryResource (i->ResourceID, buffer, dec->decryption());
180 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof (id));
182 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
183 memcpy (data.get(), buffer.RoData(), buffer.Size());
185 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = _load_font_nodes.begin ();
186 while (j != _load_font_nodes.end() && (*j)->urn != id) {
190 if (j != _load_font_nodes.end ()) {
191 _fonts.push_back (Font ((*j)->id, (*j)->urn, Data (data, buffer.Size ())));
196 /* Get intrinsic duration */
197 _intrinsic_duration = descriptor.ContainerDuration;
201 SMPTESubtitleAsset::set_key (Key key)
203 /* See if we already have a key; if we do, and we have a file, we'll already
206 bool const had_key = static_cast<bool> (_key);
210 if (!_key_id || !_file || had_key) {
211 /* Either we don't have any data to read, it wasn't
212 encrypted, or we've already read it, so we don't
213 need to do anything else.
218 /* Our data was encrypted; now we can decrypt it */
220 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
221 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
222 if (ASDCP_FAILURE (r)) {
223 boost::throw_exception (
225 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
231 shared_ptr<DecryptionContext> dec (new DecryptionContext (key));
232 reader->ReadTimedTextResource (s, dec->decryption());
233 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
234 xml->read_string (s);
236 read_mxf_descriptor (reader, dec);
239 list<shared_ptr<LoadFontNode> >
240 SMPTESubtitleAsset::load_font_nodes () const
242 list<shared_ptr<LoadFontNode> > lf;
243 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
248 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
250 ASDCP::TimedText::MXFReader reader;
251 Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
252 return !ASDCP_FAILURE (r);
256 SMPTESubtitleAsset::xml_as_string () const
259 xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel");
260 root->set_namespace_declaration ("http://www.smpte-ra.org/schemas/428-7/2010/DCST", "dcst");
261 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
263 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _xml_id);
264 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
265 if (_annotation_text) {
266 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
268 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
270 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
273 root->add_child("Language", "dcst")->add_child_text (_language.get ());
275 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
276 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
278 root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string (SMPTE));
281 BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) {
282 xmlpp::Element* load_font = root->add_child("LoadFont", "dcst");
283 load_font->add_child_text ("urn:uuid:" + i->urn);
284 load_font->set_attribute ("ID", i->id);
287 subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, SMPTE);
289 return doc.write_to_string ("UTF-8");
292 /** Write this content to a MXF file */
294 SMPTESubtitleAsset::write (boost::filesystem::path p) const
296 EncryptionContext enc (key (), SMPTE);
298 ASDCP::WriterInfo writer_info;
299 fill_writer_info (&writer_info, _id, SMPTE);
301 ASDCP::TimedText::TimedTextDescriptor descriptor;
302 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
303 descriptor.EncodingName = "UTF-8";
305 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
306 list<Font>::const_iterator j = _fonts.begin ();
307 while (j != _fonts.end() && j->load_id != i->id) {
310 if (j != _fonts.end ()) {
311 ASDCP::TimedText::TimedTextResourceDescriptor res;
313 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
314 DCP_ASSERT (c == Kumu::UUID_Length);
315 res.Type = ASDCP::TimedText::MT_OPENTYPE;
316 descriptor.ResourceList.push_back (res);
320 descriptor.NamespaceName = "dcst";
321 memcpy (descriptor.AssetID, writer_info.AssetUUID, ASDCP::UUIDlen);
322 descriptor.ContainerDuration = _intrinsic_duration;
324 ASDCP::TimedText::MXFWriter writer;
325 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor);
326 if (ASDCP_FAILURE (r)) {
327 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
330 /* XXX: no encryption */
331 r = writer.WriteTimedTextResource (xml_as_string (), enc.encryption(), enc.hmac());
332 if (ASDCP_FAILURE (r)) {
333 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
336 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
337 list<Font>::const_iterator j = _fonts.begin ();
338 while (j != _fonts.end() && j->load_id != i->id) {
341 if (j != _fonts.end ()) {
342 ASDCP::TimedText::FrameBuffer buffer;
343 buffer.SetData (j->data.data().get(), j->data.size());
344 buffer.Size (j->data.size());
345 r = writer.WriteAncillaryResource (buffer, enc.encryption(), enc.hmac());
346 if (ASDCP_FAILURE (r)) {
347 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
358 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
360 if (!SubtitleAsset::equals (other_asset, options, note)) {
364 shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
366 note (DCP_ERROR, "Subtitles are in different standards");
370 list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
371 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
373 while (i != _load_font_nodes.end ()) {
374 if (j == other->_load_font_nodes.end ()) {
375 note (DCP_ERROR, "<LoadFont> nodes differ");
379 if ((*i)->id != (*j)->id) {
380 note (DCP_ERROR, "<LoadFont> nodes differ");
388 if (_content_title_text != other->_content_title_text) {
389 note (DCP_ERROR, "Subtitle content title texts differ");
393 if (_language != other->_language) {
394 note (DCP_ERROR, "Subtitle languages differ");
398 if (_annotation_text != other->_annotation_text) {
399 note (DCP_ERROR, "Subtitle annotation texts differ");
403 if (_issue_date != other->_issue_date) {
404 if (options.issue_dates_can_differ) {
405 note (DCP_NOTE, "Subtitle issue dates differ");
407 note (DCP_ERROR, "Subtitle issue dates differ");
412 if (_reel_number != other->_reel_number) {
413 note (DCP_ERROR, "Subtitle reel numbers differ");
417 if (_edit_rate != other->_edit_rate) {
418 note (DCP_ERROR, "Subtitle edit rates differ");
422 if (_time_code_rate != other->_time_code_rate) {
423 note (DCP_ERROR, "Subtitle time code rates differ");
427 if (_start_time != other->_start_time) {
428 note (DCP_ERROR, "Subtitle start times differ");
436 SMPTESubtitleAsset::add_font (string load_id, boost::filesystem::path file)
438 string const uuid = make_uuid ();
439 _fonts.push_back (Font (load_id, uuid, file));
440 _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
444 SMPTESubtitleAsset::add (dcp::SubtitleString s)
446 SubtitleAsset::add (s);
447 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);