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 <asdcp/KM_log.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;
64 using boost::optional;
67 SMPTESubtitleAsset::SMPTESubtitleAsset ()
69 , _intrinsic_duration (0)
71 , _time_code_rate (24)
72 , _xml_id (make_uuid ())
77 /** Construct a SMPTESubtitleAsset by reading an MXF or XML file.
78 * @param file Filename.
80 SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file)
81 : SubtitleAsset (file)
83 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
85 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
86 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
87 if (!ASDCP_FAILURE (r)) {
89 ASDCP::WriterInfo info;
90 reader->FillWriterInfo (info);
91 _id = read_writer_info (info);
93 /* Not encrypted; read it in now */
95 reader->ReadTimedTextResource (s);
98 read_mxf_descriptor (reader, shared_ptr<DecryptionContext> (new DecryptionContext (optional<Key>(), SMPTE)));
103 xml.reset (new cxml::Document ("SubtitleReel"));
104 xml->read_file (file);
106 _id = _xml_id = remove_urn_uuid (xml->string_child ("Id"));
107 } catch (cxml::Error& e) {
108 boost::throw_exception (
111 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
112 file, static_cast<int> (r), e.what ()
121 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
123 _xml_id = remove_urn_uuid(xml->string_child("Id"));
124 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
126 _content_title_text = xml->string_child ("ContentTitleText");
127 _annotation_text = xml->optional_string_child ("AnnotationText");
128 _issue_date = LocalTime (xml->string_child ("IssueDate"));
129 _reel_number = xml->optional_number_child<int> ("ReelNumber");
130 _language = xml->optional_string_child ("Language");
132 /* This is supposed to be two numbers, but a single number has been seen in the wild */
133 string const er = xml->string_child ("EditRate");
134 vector<string> er_parts;
135 split (er_parts, er, is_any_of (" "));
136 if (er_parts.size() == 1) {
137 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
138 } else if (er_parts.size() == 2) {
139 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
141 throw XMLError ("malformed EditRate " + er);
144 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
145 if (xml->optional_string_child ("StartTime")) {
146 _start_time = Time (xml->string_child ("StartTime"), _time_code_rate);
149 /* Now we need to drop down to xmlpp */
152 xmlpp::Node::NodeList c = xml->node()->get_children ();
153 for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
154 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
155 if (e && e->get_name() == "SubtitleList") {
156 parse_subtitles (e, ps, _time_code_rate, SMPTE);
160 /* Guess intrinsic duration */
161 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
165 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
167 ASDCP::TimedText::TimedTextDescriptor descriptor;
168 reader->FillTimedTextDescriptor (descriptor);
173 ASDCP::TimedText::ResourceList_t::const_iterator i = descriptor.ResourceList.begin();
174 i != descriptor.ResourceList.end();
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());
188 case ASDCP::TimedText::MT_OPENTYPE:
190 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = _load_font_nodes.begin ();
191 while (j != _load_font_nodes.end() && (*j)->urn != id) {
195 if (j != _load_font_nodes.end ()) {
196 _fonts.push_back (Font ((*j)->id, (*j)->urn, Data (data, buffer.Size ())));
200 case ASDCP::TimedText::MT_PNG:
202 list<shared_ptr<Subtitle> >::const_iterator j = _subtitles.begin ();
203 while (j != _subtitles.end() && ((!dynamic_pointer_cast<SubtitleImage>(*j)) || dynamic_pointer_cast<SubtitleImage>(*j)->id() != id)) {
207 if (j != _subtitles.end()) {
208 dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image (Data(data, buffer.Size()));
217 /* Get intrinsic duration */
218 _intrinsic_duration = descriptor.ContainerDuration;
222 SMPTESubtitleAsset::set_key (Key key)
224 /* See if we already have a key; if we do, and we have a file, we'll already
227 bool const had_key = static_cast<bool> (_key);
231 if (!_key_id || !_file || had_key) {
232 /* Either we don't have any data to read, it wasn't
233 encrypted, or we've already read it, so we don't
234 need to do anything else.
239 /* Our data was encrypted; now we can decrypt it */
241 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
242 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
243 if (ASDCP_FAILURE (r)) {
244 boost::throw_exception (
246 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
252 shared_ptr<DecryptionContext> dec (new DecryptionContext (key, SMPTE));
253 reader->ReadTimedTextResource (s, dec->context(), dec->hmac());
254 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
255 xml->read_string (s);
257 read_mxf_descriptor (reader, dec);
260 list<shared_ptr<LoadFontNode> >
261 SMPTESubtitleAsset::load_font_nodes () const
263 list<shared_ptr<LoadFontNode> > lf;
264 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
269 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
271 ASDCP::TimedText::MXFReader reader;
272 Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
273 Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
274 Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
275 return !ASDCP_FAILURE (r);
279 SMPTESubtitleAsset::xml_as_string () const
282 xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel");
283 root->set_namespace_declaration ("http://www.smpte-ra.org/schemas/428-7/2010/DCST", "dcst");
284 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
286 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _xml_id);
287 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
288 if (_annotation_text) {
289 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
291 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
293 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
296 root->add_child("Language", "dcst")->add_child_text (_language.get ());
298 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
299 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
301 root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string (SMPTE));
304 BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) {
305 xmlpp::Element* load_font = root->add_child("LoadFont", "dcst");
306 load_font->add_child_text ("urn:uuid:" + i->urn);
307 load_font->set_attribute ("ID", i->id);
310 subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, SMPTE);
312 return doc.write_to_string ("UTF-8");
315 /** Write this content to a MXF file */
317 SMPTESubtitleAsset::write (boost::filesystem::path p) const
319 EncryptionContext enc (key(), SMPTE);
321 ASDCP::WriterInfo writer_info;
322 fill_writer_info (&writer_info, _id);
324 ASDCP::TimedText::TimedTextDescriptor descriptor;
325 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
326 descriptor.EncodingName = "UTF-8";
328 /* Font references */
330 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
331 list<Font>::const_iterator j = _fonts.begin ();
332 while (j != _fonts.end() && j->load_id != i->id) {
335 if (j != _fonts.end ()) {
336 ASDCP::TimedText::TimedTextResourceDescriptor res;
338 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
339 DCP_ASSERT (c == Kumu::UUID_Length);
340 res.Type = ASDCP::TimedText::MT_OPENTYPE;
341 descriptor.ResourceList.push_back (res);
345 /* Image subtitle references */
347 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
348 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
350 ASDCP::TimedText::TimedTextResourceDescriptor res;
352 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
353 DCP_ASSERT (c == Kumu::UUID_Length);
354 res.Type = ASDCP::TimedText::MT_PNG;
355 descriptor.ResourceList.push_back (res);
359 descriptor.NamespaceName = "dcst";
360 memcpy (descriptor.AssetID, writer_info.AssetUUID, ASDCP::UUIDlen);
361 descriptor.ContainerDuration = _intrinsic_duration;
363 ASDCP::TimedText::MXFWriter writer;
364 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor);
365 if (ASDCP_FAILURE (r)) {
366 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
369 r = writer.WriteTimedTextResource (xml_as_string (), enc.context(), enc.hmac());
370 if (ASDCP_FAILURE (r)) {
371 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
376 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
377 list<Font>::const_iterator j = _fonts.begin ();
378 while (j != _fonts.end() && j->load_id != i->id) {
381 if (j != _fonts.end ()) {
382 ASDCP::TimedText::FrameBuffer buffer;
383 buffer.SetData (j->data.data().get(), j->data.size());
384 buffer.Size (j->data.size());
385 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
386 if (ASDCP_FAILURE (r)) {
387 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
392 /* Image subtitle payload */
394 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
395 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
397 ASDCP::TimedText::FrameBuffer buffer;
398 buffer.SetData (si->png_image().data().get(), si->png_image().size());
399 buffer.Size (si->png_image().size());
400 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
401 if (ASDCP_FAILURE(r)) {
402 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
413 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
415 if (!SubtitleAsset::equals (other_asset, options, note)) {
419 shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
421 note (DCP_ERROR, "Subtitles are in different standards");
425 list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
426 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
428 while (i != _load_font_nodes.end ()) {
429 if (j == other->_load_font_nodes.end ()) {
430 note (DCP_ERROR, "<LoadFont> nodes differ");
434 if ((*i)->id != (*j)->id) {
435 note (DCP_ERROR, "<LoadFont> nodes differ");
443 if (_content_title_text != other->_content_title_text) {
444 note (DCP_ERROR, "Subtitle content title texts differ");
448 if (_language != other->_language) {
449 note (DCP_ERROR, "Subtitle languages differ");
453 if (_annotation_text != other->_annotation_text) {
454 note (DCP_ERROR, "Subtitle annotation texts differ");
458 if (_issue_date != other->_issue_date) {
459 if (options.issue_dates_can_differ) {
460 note (DCP_NOTE, "Subtitle issue dates differ");
462 note (DCP_ERROR, "Subtitle issue dates differ");
467 if (_reel_number != other->_reel_number) {
468 note (DCP_ERROR, "Subtitle reel numbers differ");
472 if (_edit_rate != other->_edit_rate) {
473 note (DCP_ERROR, "Subtitle edit rates differ");
477 if (_time_code_rate != other->_time_code_rate) {
478 note (DCP_ERROR, "Subtitle time code rates differ");
482 if (_start_time != other->_start_time) {
483 note (DCP_ERROR, "Subtitle start times differ");
491 SMPTESubtitleAsset::add_font (string load_id, boost::filesystem::path file)
493 string const uuid = make_uuid ();
494 _fonts.push_back (Font (load_id, uuid, file));
495 _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
499 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
501 SubtitleAsset::add (s);
502 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);