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 "font_node.h"
41 #include "exceptions.h"
43 #include "raw_convert.h"
44 #include "dcp_assert.h"
46 #include "compose.hpp"
47 #include "encryption_context.h"
48 #include "decryption_context.h"
49 #include <asdcp/AS_DCP.h>
50 #include <asdcp/KM_util.h>
51 #include <libxml++/libxml++.h>
52 #include <boost/foreach.hpp>
53 #include <boost/algorithm/string.hpp>
59 using boost::shared_ptr;
61 using boost::is_any_of;
62 using boost::shared_array;
63 using boost::dynamic_pointer_cast;
66 SMPTESubtitleAsset::SMPTESubtitleAsset ()
67 : _intrinsic_duration (0)
69 , _time_code_rate (24)
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 = remove_urn_uuid (xml->string_child ("Id"));
104 } catch (cxml::Error& e) {
105 boost::throw_exception (
107 String::compose ("MXF failed with %1, XML failed with %2", file, static_cast<int> (r), e.what ())
115 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
117 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
119 _content_title_text = xml->string_child ("ContentTitleText");
120 _annotation_text = xml->optional_string_child ("AnnotationText");
121 _issue_date = LocalTime (xml->string_child ("IssueDate"));
122 _reel_number = xml->optional_number_child<int> ("ReelNumber");
123 _language = xml->optional_string_child ("Language");
125 /* This is supposed to be two numbers, but a single number has been seen in the wild */
126 string const er = xml->string_child ("EditRate");
127 vector<string> er_parts;
128 split (er_parts, er, is_any_of (" "));
129 if (er_parts.size() == 1) {
130 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
131 } else if (er_parts.size() == 2) {
132 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
134 throw XMLError ("malformed EditRate " + er);
137 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
138 if (xml->optional_string_child ("StartTime")) {
139 _start_time = Time (xml->string_child ("StartTime"), _time_code_rate);
142 shared_ptr<cxml::Node> subtitle_list = xml->optional_node_child ("SubtitleList");
144 list<shared_ptr<dcp::FontNode> > font_nodes;
145 BOOST_FOREACH (cxml::NodePtr const & i, subtitle_list->node_children ("Font")) {
146 font_nodes.push_back (shared_ptr<FontNode> (new FontNode (i, _time_code_rate, SMPTE)));
149 list<shared_ptr<dcp::SubtitleNode> > subtitle_nodes;
150 BOOST_FOREACH (cxml::NodePtr const & i, subtitle_list->node_children ("Subtitle")) {
151 subtitle_nodes.push_back (shared_ptr<SubtitleNode> (new SubtitleNode (i, _time_code_rate, SMPTE)));
154 parse_subtitles (xml, font_nodes, subtitle_nodes);
156 /* Guess intrinsic duration */
157 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
161 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
163 ASDCP::TimedText::TimedTextDescriptor descriptor;
164 reader->FillTimedTextDescriptor (descriptor);
169 ASDCP::TimedText::ResourceList_t::const_iterator i = descriptor.ResourceList.begin();
170 i != descriptor.ResourceList.end();
173 if (i->Type == ASDCP::TimedText::MT_OPENTYPE) {
174 ASDCP::TimedText::FrameBuffer buffer;
175 buffer.Capacity (10 * 1024 * 1024);
176 reader->ReadAncillaryResource (i->ResourceID, buffer, dec->decryption());
179 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof (id));
181 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
182 memcpy (data.get(), buffer.RoData(), buffer.Size());
184 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = _load_font_nodes.begin ();
185 while (j != _load_font_nodes.end() && (*j)->urn != id) {
189 if (j != _load_font_nodes.end ()) {
190 _fonts.push_back (Font ((*j)->id, (*j)->urn, Data (data, buffer.Size ())));
195 /* Get intrinsic duration */
196 _intrinsic_duration = descriptor.ContainerDuration;
200 SMPTESubtitleAsset::set_key (Key key)
204 if (!_key_id || !_file) {
205 /* Either we don't have any data to read, or it wasn't
206 encrypted, so we don't need to do anything else.
211 /* Our data was encrypted; now we can decrypt it */
213 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
214 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
215 if (ASDCP_FAILURE (r)) {
216 boost::throw_exception (
218 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
224 shared_ptr<DecryptionContext> dec (new DecryptionContext (key));
225 reader->ReadTimedTextResource (s, dec->decryption());
226 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
227 xml->read_string (s);
229 read_mxf_descriptor (reader, dec);
232 list<shared_ptr<LoadFontNode> >
233 SMPTESubtitleAsset::load_font_nodes () const
235 list<shared_ptr<LoadFontNode> > lf;
236 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
241 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
243 ASDCP::TimedText::MXFReader reader;
244 Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
245 return !ASDCP_FAILURE (r);
249 SMPTESubtitleAsset::xml_as_string () const
252 xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel");
253 root->set_namespace_declaration ("http://www.smpte-ra.org/schemas/428-7/2010/DCST", "dcst");
254 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
256 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _id);
257 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
258 if (_annotation_text) {
259 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
261 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
263 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
266 root->add_child("Language", "dcst")->add_child_text (_language.get ());
268 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
269 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
271 root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string (SMPTE));
274 BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) {
275 xmlpp::Element* load_font = root->add_child("LoadFont", "dcst");
276 load_font->add_child_text ("urn:uuid:" + i->urn);
277 load_font->set_attribute ("ID", i->id);
280 subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, SMPTE);
282 return doc.write_to_string_formatted ("UTF-8");
285 /** Write this content to a MXF file */
287 SMPTESubtitleAsset::write (boost::filesystem::path p) const
289 EncryptionContext enc (key (), SMPTE);
291 ASDCP::WriterInfo writer_info;
292 fill_writer_info (&writer_info, _id, SMPTE);
294 ASDCP::TimedText::TimedTextDescriptor descriptor;
295 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
296 descriptor.EncodingName = "UTF-8";
298 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
299 list<Font>::const_iterator j = _fonts.begin ();
300 while (j != _fonts.end() && j->load_id != i->id) {
303 if (j != _fonts.end ()) {
304 ASDCP::TimedText::TimedTextResourceDescriptor res;
306 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
307 DCP_ASSERT (c == Kumu::UUID_Length);
308 res.Type = ASDCP::TimedText::MT_OPENTYPE;
309 descriptor.ResourceList.push_back (res);
313 descriptor.NamespaceName = "dcst";
314 memcpy (descriptor.AssetID, writer_info.AssetUUID, ASDCP::UUIDlen);
315 descriptor.ContainerDuration = _intrinsic_duration;
317 ASDCP::TimedText::MXFWriter writer;
318 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor);
319 if (ASDCP_FAILURE (r)) {
320 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
323 /* XXX: no encryption */
324 r = writer.WriteTimedTextResource (xml_as_string (), enc.encryption(), enc.hmac());
325 if (ASDCP_FAILURE (r)) {
326 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
329 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
330 list<Font>::const_iterator j = _fonts.begin ();
331 while (j != _fonts.end() && j->load_id != i->id) {
334 if (j != _fonts.end ()) {
335 ASDCP::TimedText::FrameBuffer buffer;
336 buffer.SetData (j->data.data().get(), j->data.size());
337 buffer.Size (j->data.size());
338 r = writer.WriteAncillaryResource (buffer, enc.encryption(), enc.hmac());
339 if (ASDCP_FAILURE (r)) {
340 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
351 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
353 if (!SubtitleAsset::equals (other_asset, options, note)) {
357 shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
359 note (DCP_ERROR, "Subtitles are in different standards");
363 list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
364 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
366 while (i != _load_font_nodes.end ()) {
367 if (j == other->_load_font_nodes.end ()) {
368 note (DCP_ERROR, "<LoadFont> nodes differ");
372 if ((*i)->id != (*j)->id) {
373 note (DCP_ERROR, "<LoadFont> nodes differ");
381 if (_content_title_text != other->_content_title_text) {
382 note (DCP_ERROR, "Subtitle content title texts differ");
386 if (_language != other->_language) {
387 note (DCP_ERROR, "Subtitle languages differ");
391 if (_annotation_text != other->_annotation_text) {
392 note (DCP_ERROR, "Subtitle annotation texts differ");
396 if (_issue_date != other->_issue_date) {
397 if (options.issue_dates_can_differ) {
398 note (DCP_NOTE, "Subtitle issue dates differ");
400 note (DCP_ERROR, "Subtitle issue dates differ");
405 if (_reel_number != other->_reel_number) {
406 note (DCP_ERROR, "Subtitle reel numbers differ");
410 if (_edit_rate != other->_edit_rate) {
411 note (DCP_ERROR, "Subtitle edit rates differ");
415 if (_time_code_rate != other->_time_code_rate) {
416 note (DCP_ERROR, "Subtitle time code rates differ");
420 if (_start_time != other->_start_time) {
421 note (DCP_ERROR, "Subtitle start times differ");
429 SMPTESubtitleAsset::add_font (string load_id, boost::filesystem::path file)
431 string const uuid = make_uuid ();
432 _fonts.push_back (Font (load_id, uuid, file));
433 _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
437 SMPTESubtitleAsset::add (dcp::SubtitleString s)
439 SubtitleAsset::add (s);
440 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);