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 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 /* XXX: load PNG and attach them to _subtitles */
200 /* Get intrinsic duration */
201 _intrinsic_duration = descriptor.ContainerDuration;
205 SMPTESubtitleAsset::set_key (Key key)
207 /* See if we already have a key; if we do, and we have a file, we'll already
210 bool const had_key = static_cast<bool> (_key);
214 if (!_key_id || !_file || had_key) {
215 /* Either we don't have any data to read, it wasn't
216 encrypted, or we've already read it, so we don't
217 need to do anything else.
222 /* Our data was encrypted; now we can decrypt it */
224 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
225 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
226 if (ASDCP_FAILURE (r)) {
227 boost::throw_exception (
229 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
235 shared_ptr<DecryptionContext> dec (new DecryptionContext (key, SMPTE));
236 reader->ReadTimedTextResource (s, dec->context(), dec->hmac());
237 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
238 xml->read_string (s);
240 read_mxf_descriptor (reader, dec);
243 list<shared_ptr<LoadFontNode> >
244 SMPTESubtitleAsset::load_font_nodes () const
246 list<shared_ptr<LoadFontNode> > lf;
247 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
252 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
254 ASDCP::TimedText::MXFReader reader;
255 Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
256 return !ASDCP_FAILURE (r);
260 SMPTESubtitleAsset::xml_as_string () const
263 xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel");
264 root->set_namespace_declaration ("http://www.smpte-ra.org/schemas/428-7/2010/DCST", "dcst");
265 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
267 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _xml_id);
268 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
269 if (_annotation_text) {
270 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
272 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
274 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
277 root->add_child("Language", "dcst")->add_child_text (_language.get ());
279 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
280 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
282 root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string (SMPTE));
285 BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) {
286 xmlpp::Element* load_font = root->add_child("LoadFont", "dcst");
287 load_font->add_child_text ("urn:uuid:" + i->urn);
288 load_font->set_attribute ("ID", i->id);
291 subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, SMPTE);
293 return doc.write_to_string ("UTF-8");
296 /** Write this content to a MXF file */
298 SMPTESubtitleAsset::write (boost::filesystem::path p) const
300 EncryptionContext enc (key(), SMPTE);
302 ASDCP::WriterInfo writer_info;
303 fill_writer_info (&writer_info, _id);
305 ASDCP::TimedText::TimedTextDescriptor descriptor;
306 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
307 descriptor.EncodingName = "UTF-8";
309 /* Font references */
311 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
312 list<Font>::const_iterator j = _fonts.begin ();
313 while (j != _fonts.end() && j->load_id != i->id) {
316 if (j != _fonts.end ()) {
317 ASDCP::TimedText::TimedTextResourceDescriptor res;
319 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
320 DCP_ASSERT (c == Kumu::UUID_Length);
321 res.Type = ASDCP::TimedText::MT_OPENTYPE;
322 descriptor.ResourceList.push_back (res);
326 /* Image subtitle references */
328 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
329 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
331 ASDCP::TimedText::TimedTextResourceDescriptor res;
333 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
334 DCP_ASSERT (c == Kumu::UUID_Length);
335 res.Type = ASDCP::TimedText::MT_PNG;
336 descriptor.ResourceList.push_back (res);
340 descriptor.NamespaceName = "dcst";
341 memcpy (descriptor.AssetID, writer_info.AssetUUID, ASDCP::UUIDlen);
342 descriptor.ContainerDuration = _intrinsic_duration;
344 ASDCP::TimedText::MXFWriter writer;
345 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor);
346 if (ASDCP_FAILURE (r)) {
347 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
350 r = writer.WriteTimedTextResource (xml_as_string (), enc.context(), enc.hmac());
351 if (ASDCP_FAILURE (r)) {
352 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
357 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
358 list<Font>::const_iterator j = _fonts.begin ();
359 while (j != _fonts.end() && j->load_id != i->id) {
362 if (j != _fonts.end ()) {
363 ASDCP::TimedText::FrameBuffer buffer;
364 buffer.SetData (j->data.data().get(), j->data.size());
365 buffer.Size (j->data.size());
366 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
367 if (ASDCP_FAILURE (r)) {
368 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
373 /* Image subtitle payload */
375 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
376 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
378 ASDCP::TimedText::FrameBuffer buffer;
379 buffer.SetData (si->png_image().data().get(), si->png_image().size());
380 buffer.Size (si->png_image().size());
381 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
382 if (ASDCP_FAILURE(r)) {
383 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
394 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
396 if (!SubtitleAsset::equals (other_asset, options, note)) {
400 shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
402 note (DCP_ERROR, "Subtitles are in different standards");
406 list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
407 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
409 while (i != _load_font_nodes.end ()) {
410 if (j == other->_load_font_nodes.end ()) {
411 note (DCP_ERROR, "<LoadFont> nodes differ");
415 if ((*i)->id != (*j)->id) {
416 note (DCP_ERROR, "<LoadFont> nodes differ");
424 if (_content_title_text != other->_content_title_text) {
425 note (DCP_ERROR, "Subtitle content title texts differ");
429 if (_language != other->_language) {
430 note (DCP_ERROR, "Subtitle languages differ");
434 if (_annotation_text != other->_annotation_text) {
435 note (DCP_ERROR, "Subtitle annotation texts differ");
439 if (_issue_date != other->_issue_date) {
440 if (options.issue_dates_can_differ) {
441 note (DCP_NOTE, "Subtitle issue dates differ");
443 note (DCP_ERROR, "Subtitle issue dates differ");
448 if (_reel_number != other->_reel_number) {
449 note (DCP_ERROR, "Subtitle reel numbers differ");
453 if (_edit_rate != other->_edit_rate) {
454 note (DCP_ERROR, "Subtitle edit rates differ");
458 if (_time_code_rate != other->_time_code_rate) {
459 note (DCP_ERROR, "Subtitle time code rates differ");
463 if (_start_time != other->_start_time) {
464 note (DCP_ERROR, "Subtitle start times differ");
472 SMPTESubtitleAsset::add_font (string load_id, boost::filesystem::path file)
474 string const uuid = make_uuid ();
475 _fonts.push_back (Font (load_id, uuid, file));
476 _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
480 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
482 SubtitleAsset::add (s);
483 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);