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 "subtitle_image.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 ASDCP::TimedText::FrameBuffer buffer;
177 buffer.Capacity (10 * 1024 * 1024);
178 reader->ReadAncillaryResource (i->ResourceID, buffer, dec->context(), dec->hmac());
181 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof (id));
183 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
184 memcpy (data.get(), buffer.RoData(), buffer.Size());
187 case ASDCP::TimedText::MT_OPENTYPE:
189 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = _load_font_nodes.begin ();
190 while (j != _load_font_nodes.end() && (*j)->urn != id) {
194 if (j != _load_font_nodes.end ()) {
195 _fonts.push_back (Font ((*j)->id, (*j)->urn, Data (data, buffer.Size ())));
199 case ASDCP::TimedText::MT_PNG:
201 list<shared_ptr<Subtitle> >::const_iterator j = _subtitles.begin ();
202 while (j != _subtitles.end() && ((!dynamic_pointer_cast<SubtitleImage>(*j)) || dynamic_pointer_cast<SubtitleImage>(*j)->id() != id)) {
206 if (j != _subtitles.end()) {
207 dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image (Data(data, buffer.Size()));
216 /* Get intrinsic duration */
217 _intrinsic_duration = descriptor.ContainerDuration;
221 SMPTESubtitleAsset::set_key (Key key)
223 /* See if we already have a key; if we do, and we have a file, we'll already
226 bool const had_key = static_cast<bool> (_key);
230 if (!_key_id || !_file || had_key) {
231 /* Either we don't have any data to read, it wasn't
232 encrypted, or we've already read it, so we don't
233 need to do anything else.
238 /* Our data was encrypted; now we can decrypt it */
240 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
241 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
242 if (ASDCP_FAILURE (r)) {
243 boost::throw_exception (
245 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
251 shared_ptr<DecryptionContext> dec (new DecryptionContext (key, SMPTE));
252 reader->ReadTimedTextResource (s, dec->context(), dec->hmac());
253 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
254 xml->read_string (s);
256 read_mxf_descriptor (reader, dec);
259 list<shared_ptr<LoadFontNode> >
260 SMPTESubtitleAsset::load_font_nodes () const
262 list<shared_ptr<LoadFontNode> > lf;
263 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
268 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
270 ASDCP::TimedText::MXFReader reader;
271 Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
272 return !ASDCP_FAILURE (r);
276 SMPTESubtitleAsset::xml_as_string () const
279 xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel");
280 root->set_namespace_declaration ("http://www.smpte-ra.org/schemas/428-7/2010/DCST", "dcst");
281 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
283 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _xml_id);
284 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
285 if (_annotation_text) {
286 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
288 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
290 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
293 root->add_child("Language", "dcst")->add_child_text (_language.get ());
295 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
296 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
298 root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string (SMPTE));
301 BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) {
302 xmlpp::Element* load_font = root->add_child("LoadFont", "dcst");
303 load_font->add_child_text ("urn:uuid:" + i->urn);
304 load_font->set_attribute ("ID", i->id);
307 subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, SMPTE);
309 return doc.write_to_string ("UTF-8");
312 /** Write this content to a MXF file */
314 SMPTESubtitleAsset::write (boost::filesystem::path p) const
316 EncryptionContext enc (key(), SMPTE);
318 ASDCP::WriterInfo writer_info;
319 fill_writer_info (&writer_info, _id);
321 ASDCP::TimedText::TimedTextDescriptor descriptor;
322 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
323 descriptor.EncodingName = "UTF-8";
325 /* Font references */
327 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
328 list<Font>::const_iterator j = _fonts.begin ();
329 while (j != _fonts.end() && j->load_id != i->id) {
332 if (j != _fonts.end ()) {
333 ASDCP::TimedText::TimedTextResourceDescriptor res;
335 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
336 DCP_ASSERT (c == Kumu::UUID_Length);
337 res.Type = ASDCP::TimedText::MT_OPENTYPE;
338 descriptor.ResourceList.push_back (res);
342 /* Image subtitle references */
344 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
345 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
347 ASDCP::TimedText::TimedTextResourceDescriptor res;
349 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
350 DCP_ASSERT (c == Kumu::UUID_Length);
351 res.Type = ASDCP::TimedText::MT_PNG;
352 descriptor.ResourceList.push_back (res);
356 descriptor.NamespaceName = "dcst";
357 memcpy (descriptor.AssetID, writer_info.AssetUUID, ASDCP::UUIDlen);
358 descriptor.ContainerDuration = _intrinsic_duration;
360 ASDCP::TimedText::MXFWriter writer;
361 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor);
362 if (ASDCP_FAILURE (r)) {
363 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
366 r = writer.WriteTimedTextResource (xml_as_string (), enc.context(), enc.hmac());
367 if (ASDCP_FAILURE (r)) {
368 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
373 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
374 list<Font>::const_iterator j = _fonts.begin ();
375 while (j != _fonts.end() && j->load_id != i->id) {
378 if (j != _fonts.end ()) {
379 ASDCP::TimedText::FrameBuffer buffer;
380 buffer.SetData (j->data.data().get(), j->data.size());
381 buffer.Size (j->data.size());
382 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
383 if (ASDCP_FAILURE (r)) {
384 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
389 /* Image subtitle payload */
391 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
392 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
394 ASDCP::TimedText::FrameBuffer buffer;
395 buffer.SetData (si->png_image().data().get(), si->png_image().size());
396 buffer.Size (si->png_image().size());
397 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
398 if (ASDCP_FAILURE(r)) {
399 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
410 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
412 if (!SubtitleAsset::equals (other_asset, options, note)) {
416 shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
418 note (DCP_ERROR, "Subtitles are in different standards");
422 list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
423 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
425 while (i != _load_font_nodes.end ()) {
426 if (j == other->_load_font_nodes.end ()) {
427 note (DCP_ERROR, "<LoadFont> nodes differ");
431 if ((*i)->id != (*j)->id) {
432 note (DCP_ERROR, "<LoadFont> nodes differ");
440 if (_content_title_text != other->_content_title_text) {
441 note (DCP_ERROR, "Subtitle content title texts differ");
445 if (_language != other->_language) {
446 note (DCP_ERROR, "Subtitle languages differ");
450 if (_annotation_text != other->_annotation_text) {
451 note (DCP_ERROR, "Subtitle annotation texts differ");
455 if (_issue_date != other->_issue_date) {
456 if (options.issue_dates_can_differ) {
457 note (DCP_NOTE, "Subtitle issue dates differ");
459 note (DCP_ERROR, "Subtitle issue dates differ");
464 if (_reel_number != other->_reel_number) {
465 note (DCP_ERROR, "Subtitle reel numbers differ");
469 if (_edit_rate != other->_edit_rate) {
470 note (DCP_ERROR, "Subtitle edit rates differ");
474 if (_time_code_rate != other->_time_code_rate) {
475 note (DCP_ERROR, "Subtitle time code rates differ");
479 if (_start_time != other->_start_time) {
480 note (DCP_ERROR, "Subtitle start times differ");
488 SMPTESubtitleAsset::add_font (string load_id, boost::filesystem::path file)
490 string const uuid = make_uuid ();
491 _fonts.push_back (Font (load_id, uuid, file));
492 _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
496 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
498 SubtitleAsset::add (s);
499 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);