2 Copyright (C) 2012-2019 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;
65 using boost::starts_with;
68 static string const subtitle_smpte_ns = "http://www.smpte-ra.org/schemas/428-7/2010/DCST";
70 SMPTESubtitleAsset::SMPTESubtitleAsset ()
72 , _intrinsic_duration (0)
74 , _time_code_rate (24)
75 , _xml_id (make_uuid ())
80 /** Construct a SMPTESubtitleAsset by reading an MXF or XML file.
81 * @param file Filename.
83 SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file)
84 : SubtitleAsset (file)
86 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
88 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
89 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
90 if (!ASDCP_FAILURE (r)) {
92 ASDCP::WriterInfo info;
93 reader->FillWriterInfo (info);
94 _id = read_writer_info (info);
96 /* Not encrypted; read it in now */
98 reader->ReadTimedTextResource (s);
101 read_mxf_descriptor (reader, shared_ptr<DecryptionContext> (new DecryptionContext (optional<Key>(), SMPTE)));
106 xml.reset (new cxml::Document ("SubtitleReel"));
107 xml->read_file (file);
109 _id = _xml_id = remove_urn_uuid (xml->string_child ("Id"));
110 } catch (cxml::Error& e) {
111 boost::throw_exception (
114 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
115 file, static_cast<int> (r), e.what ()
121 /* Try to read PNG files from the same folder that the XML is in; the wisdom of this is
122 debatable, at best...
124 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
125 shared_ptr<SubtitleImage> im = dynamic_pointer_cast<SubtitleImage>(i);
126 if (im && im->png_image().size() == 0) {
127 /* Even more dubious; allow <id>.png or urn:uuid:<id>.png */
128 boost::filesystem::path p = file.parent_path() / String::compose("%1.png", im->id());
129 if (boost::filesystem::is_regular_file(p)) {
130 im->read_png_file (p);
131 } else if (starts_with (im->id(), "urn:uuid:")) {
132 p = file.parent_path() / String::compose("%1.png", remove_urn_uuid(im->id()));
133 if (boost::filesystem::is_regular_file(p)) {
134 im->read_png_file (p);
141 /* Check that all required image data have been found */
142 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
143 shared_ptr<SubtitleImage> im = dynamic_pointer_cast<SubtitleImage>(i);
144 if (im && im->png_image().size() == 0) {
145 throw MissingSubtitleImageError (im->id());
151 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
153 _xml_id = remove_urn_uuid(xml->string_child("Id"));
154 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
156 _content_title_text = xml->string_child ("ContentTitleText");
157 _annotation_text = xml->optional_string_child ("AnnotationText");
158 _issue_date = LocalTime (xml->string_child ("IssueDate"));
159 _reel_number = xml->optional_number_child<int> ("ReelNumber");
160 _language = xml->optional_string_child ("Language");
162 /* This is supposed to be two numbers, but a single number has been seen in the wild */
163 string const er = xml->string_child ("EditRate");
164 vector<string> er_parts;
165 split (er_parts, er, is_any_of (" "));
166 if (er_parts.size() == 1) {
167 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
168 } else if (er_parts.size() == 2) {
169 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
171 throw XMLError ("malformed EditRate " + er);
174 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
175 if (xml->optional_string_child ("StartTime")) {
176 _start_time = Time (xml->string_child ("StartTime"), _time_code_rate);
179 /* Now we need to drop down to xmlpp */
182 xmlpp::Node::NodeList c = xml->node()->get_children ();
183 for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
184 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
185 if (e && e->get_name() == "SubtitleList") {
186 parse_subtitles (e, ps, _time_code_rate, SMPTE);
190 /* Guess intrinsic duration */
191 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
195 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
197 ASDCP::TimedText::TimedTextDescriptor descriptor;
198 reader->FillTimedTextDescriptor (descriptor);
200 /* Load fonts and images */
203 ASDCP::TimedText::ResourceList_t::const_iterator i = descriptor.ResourceList.begin();
204 i != descriptor.ResourceList.end();
207 ASDCP::TimedText::FrameBuffer buffer;
208 buffer.Capacity (10 * 1024 * 1024);
209 reader->ReadAncillaryResource (i->ResourceID, buffer, dec->context(), dec->hmac());
212 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof (id));
214 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
215 memcpy (data.get(), buffer.RoData(), buffer.Size());
218 case ASDCP::TimedText::MT_OPENTYPE:
220 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = _load_font_nodes.begin ();
221 while (j != _load_font_nodes.end() && (*j)->urn != id) {
225 if (j != _load_font_nodes.end ()) {
226 _fonts.push_back (Font ((*j)->id, (*j)->urn, Data (data, buffer.Size ())));
230 case ASDCP::TimedText::MT_PNG:
232 list<shared_ptr<Subtitle> >::const_iterator j = _subtitles.begin ();
233 while (j != _subtitles.end() && ((!dynamic_pointer_cast<SubtitleImage>(*j)) || dynamic_pointer_cast<SubtitleImage>(*j)->id() != id)) {
237 if (j != _subtitles.end()) {
238 dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image (Data(data, buffer.Size()));
247 /* Get intrinsic duration */
248 _intrinsic_duration = descriptor.ContainerDuration;
252 SMPTESubtitleAsset::set_key (Key key)
254 /* See if we already have a key; if we do, and we have a file, we'll already
257 bool const had_key = static_cast<bool> (_key);
261 if (!_key_id || !_file || had_key) {
262 /* Either we don't have any data to read, it wasn't
263 encrypted, or we've already read it, so we don't
264 need to do anything else.
269 /* Our data was encrypted; now we can decrypt it */
271 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
272 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
273 if (ASDCP_FAILURE (r)) {
274 boost::throw_exception (
276 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
282 shared_ptr<DecryptionContext> dec (new DecryptionContext (key, SMPTE));
283 reader->ReadTimedTextResource (s, dec->context(), dec->hmac());
284 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
285 xml->read_string (s);
287 read_mxf_descriptor (reader, dec);
290 list<shared_ptr<LoadFontNode> >
291 SMPTESubtitleAsset::load_font_nodes () const
293 list<shared_ptr<LoadFontNode> > lf;
294 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
299 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
301 ASDCP::TimedText::MXFReader reader;
302 Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
303 Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
304 Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
305 return !ASDCP_FAILURE (r);
309 SMPTESubtitleAsset::xml_as_string () const
312 xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel");
313 root->set_namespace_declaration (subtitle_smpte_ns, "dcst");
314 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
316 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _xml_id);
317 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
318 if (_annotation_text) {
319 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
321 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
323 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
326 root->add_child("Language", "dcst")->add_child_text (_language.get ());
328 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
329 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
331 root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string (SMPTE));
334 BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) {
335 xmlpp::Element* load_font = root->add_child("LoadFont", "dcst");
336 load_font->add_child_text ("urn:uuid:" + i->urn);
337 load_font->set_attribute ("ID", i->id);
340 subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, SMPTE);
342 return doc.write_to_string ("UTF-8");
345 /** Write this content to a MXF file */
347 SMPTESubtitleAsset::write (boost::filesystem::path p) const
349 EncryptionContext enc (key(), SMPTE);
351 ASDCP::WriterInfo writer_info;
352 fill_writer_info (&writer_info, _id);
354 ASDCP::TimedText::TimedTextDescriptor descriptor;
355 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
356 descriptor.EncodingName = "UTF-8";
358 /* Font references */
360 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
361 list<Font>::const_iterator j = _fonts.begin ();
362 while (j != _fonts.end() && j->load_id != i->id) {
365 if (j != _fonts.end ()) {
366 ASDCP::TimedText::TimedTextResourceDescriptor res;
368 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
369 DCP_ASSERT (c == Kumu::UUID_Length);
370 res.Type = ASDCP::TimedText::MT_OPENTYPE;
371 descriptor.ResourceList.push_back (res);
375 /* Image subtitle references */
377 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
378 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
380 ASDCP::TimedText::TimedTextResourceDescriptor res;
382 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
383 DCP_ASSERT (c == Kumu::UUID_Length);
384 res.Type = ASDCP::TimedText::MT_PNG;
385 descriptor.ResourceList.push_back (res);
389 descriptor.NamespaceName = subtitle_smpte_ns;
391 Kumu::hex2bin (_xml_id.c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
392 DCP_ASSERT (c == Kumu::UUID_Length);
393 descriptor.ContainerDuration = _intrinsic_duration;
395 ASDCP::TimedText::MXFWriter writer;
396 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor);
397 if (ASDCP_FAILURE (r)) {
398 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
401 r = writer.WriteTimedTextResource (xml_as_string (), enc.context(), enc.hmac());
402 if (ASDCP_FAILURE (r)) {
403 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
408 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
409 list<Font>::const_iterator j = _fonts.begin ();
410 while (j != _fonts.end() && j->load_id != i->id) {
413 if (j != _fonts.end ()) {
414 ASDCP::TimedText::FrameBuffer buffer;
415 buffer.SetData (j->data.data().get(), j->data.size());
416 buffer.Size (j->data.size());
417 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
418 if (ASDCP_FAILURE (r)) {
419 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
424 /* Image subtitle payload */
426 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
427 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
429 ASDCP::TimedText::FrameBuffer buffer;
430 buffer.SetData (si->png_image().data().get(), si->png_image().size());
431 buffer.Size (si->png_image().size());
432 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
433 if (ASDCP_FAILURE(r)) {
434 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
445 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
447 if (!SubtitleAsset::equals (other_asset, options, note)) {
451 shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
453 note (DCP_ERROR, "Subtitles are in different standards");
457 list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
458 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
460 while (i != _load_font_nodes.end ()) {
461 if (j == other->_load_font_nodes.end ()) {
462 note (DCP_ERROR, "<LoadFont> nodes differ");
466 if ((*i)->id != (*j)->id) {
467 note (DCP_ERROR, "<LoadFont> nodes differ");
475 if (_content_title_text != other->_content_title_text) {
476 note (DCP_ERROR, "Subtitle content title texts differ");
480 if (_language != other->_language) {
481 note (DCP_ERROR, "Subtitle languages differ");
485 if (_annotation_text != other->_annotation_text) {
486 note (DCP_ERROR, "Subtitle annotation texts differ");
490 if (_issue_date != other->_issue_date) {
491 if (options.issue_dates_can_differ) {
492 note (DCP_NOTE, "Subtitle issue dates differ");
494 note (DCP_ERROR, "Subtitle issue dates differ");
499 if (_reel_number != other->_reel_number) {
500 note (DCP_ERROR, "Subtitle reel numbers differ");
504 if (_edit_rate != other->_edit_rate) {
505 note (DCP_ERROR, "Subtitle edit rates differ");
509 if (_time_code_rate != other->_time_code_rate) {
510 note (DCP_ERROR, "Subtitle time code rates differ");
514 if (_start_time != other->_start_time) {
515 note (DCP_ERROR, "Subtitle start times differ");
523 SMPTESubtitleAsset::add_font (string load_id, boost::filesystem::path file)
525 string const uuid = make_uuid ();
526 _fonts.push_back (Font (load_id, uuid, file));
527 _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
531 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
533 SubtitleAsset::add (s);
534 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);