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 */
97 reader->ReadTimedTextResource (_raw_xml);
98 xml->read_string (_raw_xml);
100 read_mxf_descriptor (reader, shared_ptr<DecryptionContext> (new DecryptionContext (optional<Key>(), SMPTE)));
105 _raw_xml = dcp::file_to_string (file);
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))
281 shared_ptr<DecryptionContext> dec (new DecryptionContext (key, SMPTE));
282 reader->ReadTimedTextResource (_raw_xml, dec->context(), dec->hmac());
283 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
284 xml->read_string (_raw_xml);
286 read_mxf_descriptor (reader, dec);
289 list<shared_ptr<LoadFontNode> >
290 SMPTESubtitleAsset::load_font_nodes () const
292 list<shared_ptr<LoadFontNode> > lf;
293 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
298 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
300 ASDCP::TimedText::MXFReader reader;
301 Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
302 Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
303 Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
304 return !ASDCP_FAILURE (r);
308 SMPTESubtitleAsset::xml_as_string () const
311 xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel");
312 root->set_namespace_declaration (subtitle_smpte_ns, "dcst");
313 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
315 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _xml_id);
316 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
317 if (_annotation_text) {
318 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
320 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
322 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
325 root->add_child("Language", "dcst")->add_child_text (_language.get ());
327 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
328 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
330 root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string (SMPTE));
333 BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) {
334 xmlpp::Element* load_font = root->add_child("LoadFont", "dcst");
335 load_font->add_child_text ("urn:uuid:" + i->urn);
336 load_font->set_attribute ("ID", i->id);
339 subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, SMPTE);
341 return doc.write_to_string ("UTF-8");
344 /** Write this content to a MXF file */
346 SMPTESubtitleAsset::write (boost::filesystem::path p) const
348 EncryptionContext enc (key(), SMPTE);
350 ASDCP::WriterInfo writer_info;
351 fill_writer_info (&writer_info, _id);
353 ASDCP::TimedText::TimedTextDescriptor descriptor;
354 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
355 descriptor.EncodingName = "UTF-8";
357 /* Font references */
359 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
360 list<Font>::const_iterator j = _fonts.begin ();
361 while (j != _fonts.end() && j->load_id != i->id) {
364 if (j != _fonts.end ()) {
365 ASDCP::TimedText::TimedTextResourceDescriptor res;
367 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
368 DCP_ASSERT (c == Kumu::UUID_Length);
369 res.Type = ASDCP::TimedText::MT_OPENTYPE;
370 descriptor.ResourceList.push_back (res);
374 /* Image subtitle references */
376 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
377 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
379 ASDCP::TimedText::TimedTextResourceDescriptor res;
381 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
382 DCP_ASSERT (c == Kumu::UUID_Length);
383 res.Type = ASDCP::TimedText::MT_PNG;
384 descriptor.ResourceList.push_back (res);
388 descriptor.NamespaceName = subtitle_smpte_ns;
390 Kumu::hex2bin (_xml_id.c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
391 DCP_ASSERT (c == Kumu::UUID_Length);
392 descriptor.ContainerDuration = _intrinsic_duration;
394 ASDCP::TimedText::MXFWriter writer;
395 /* This header size is a guess. Empirically it seems that each subtitle reference is 90 bytes, and we need some extra.
396 The defualt size is not enough for some feature-length PNG sub projects (see DCP-o-matic #1561).
398 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor, _subtitles.size() * 90 + 16384);
399 if (ASDCP_FAILURE (r)) {
400 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
403 r = writer.WriteTimedTextResource (xml_as_string (), enc.context(), enc.hmac());
404 if (ASDCP_FAILURE (r)) {
405 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
410 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
411 list<Font>::const_iterator j = _fonts.begin ();
412 while (j != _fonts.end() && j->load_id != i->id) {
415 if (j != _fonts.end ()) {
416 ASDCP::TimedText::FrameBuffer buffer;
417 buffer.SetData (j->data.data().get(), j->data.size());
418 buffer.Size (j->data.size());
419 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
420 if (ASDCP_FAILURE (r)) {
421 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
426 /* Image subtitle payload */
428 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
429 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
431 ASDCP::TimedText::FrameBuffer buffer;
432 buffer.SetData (si->png_image().data().get(), si->png_image().size());
433 buffer.Size (si->png_image().size());
434 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
435 if (ASDCP_FAILURE(r)) {
436 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
447 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
449 if (!SubtitleAsset::equals (other_asset, options, note)) {
453 shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
455 note (DCP_ERROR, "Subtitles are in different standards");
459 list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
460 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
462 while (i != _load_font_nodes.end ()) {
463 if (j == other->_load_font_nodes.end ()) {
464 note (DCP_ERROR, "<LoadFont> nodes differ");
468 if ((*i)->id != (*j)->id) {
469 note (DCP_ERROR, "<LoadFont> nodes differ");
477 if (_content_title_text != other->_content_title_text) {
478 note (DCP_ERROR, "Subtitle content title texts differ");
482 if (_language != other->_language) {
483 note (DCP_ERROR, "Subtitle languages differ");
487 if (_annotation_text != other->_annotation_text) {
488 note (DCP_ERROR, "Subtitle annotation texts differ");
492 if (_issue_date != other->_issue_date) {
493 if (options.issue_dates_can_differ) {
494 note (DCP_NOTE, "Subtitle issue dates differ");
496 note (DCP_ERROR, "Subtitle issue dates differ");
501 if (_reel_number != other->_reel_number) {
502 note (DCP_ERROR, "Subtitle reel numbers differ");
506 if (_edit_rate != other->_edit_rate) {
507 note (DCP_ERROR, "Subtitle edit rates differ");
511 if (_time_code_rate != other->_time_code_rate) {
512 note (DCP_ERROR, "Subtitle time code rates differ");
516 if (_start_time != other->_start_time) {
517 note (DCP_ERROR, "Subtitle start times differ");
525 SMPTESubtitleAsset::add_font (string load_id, boost::filesystem::path file)
527 string const uuid = make_uuid ();
528 _fonts.push_back (Font (load_id, uuid, file));
529 _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
533 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
535 SubtitleAsset::add (s);
536 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);