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);
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 /* This header size is a guess. Empirically it seems that each subtitle reference is 90 bytes, and we need some extra.
397 The defualt size is not enough for some feature-length PNG sub projects (see DCP-o-matic #1561).
399 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor, _subtitles.size() * 90 + 16384);
400 if (ASDCP_FAILURE (r)) {
401 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
404 r = writer.WriteTimedTextResource (xml_as_string (), enc.context(), enc.hmac());
405 if (ASDCP_FAILURE (r)) {
406 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
411 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
412 list<Font>::const_iterator j = _fonts.begin ();
413 while (j != _fonts.end() && j->load_id != i->id) {
416 if (j != _fonts.end ()) {
417 ASDCP::TimedText::FrameBuffer buffer;
418 buffer.SetData (j->data.data().get(), j->data.size());
419 buffer.Size (j->data.size());
420 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
421 if (ASDCP_FAILURE (r)) {
422 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
427 /* Image subtitle payload */
429 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
430 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
432 ASDCP::TimedText::FrameBuffer buffer;
433 buffer.SetData (si->png_image().data().get(), si->png_image().size());
434 buffer.Size (si->png_image().size());
435 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
436 if (ASDCP_FAILURE(r)) {
437 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
448 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
450 if (!SubtitleAsset::equals (other_asset, options, note)) {
454 shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
456 note (DCP_ERROR, "Subtitles are in different standards");
460 list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
461 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
463 while (i != _load_font_nodes.end ()) {
464 if (j == other->_load_font_nodes.end ()) {
465 note (DCP_ERROR, "<LoadFont> nodes differ");
469 if ((*i)->id != (*j)->id) {
470 note (DCP_ERROR, "<LoadFont> nodes differ");
478 if (_content_title_text != other->_content_title_text) {
479 note (DCP_ERROR, "Subtitle content title texts differ");
483 if (_language != other->_language) {
484 note (DCP_ERROR, "Subtitle languages differ");
488 if (_annotation_text != other->_annotation_text) {
489 note (DCP_ERROR, "Subtitle annotation texts differ");
493 if (_issue_date != other->_issue_date) {
494 if (options.issue_dates_can_differ) {
495 note (DCP_NOTE, "Subtitle issue dates differ");
497 note (DCP_ERROR, "Subtitle issue dates differ");
502 if (_reel_number != other->_reel_number) {
503 note (DCP_ERROR, "Subtitle reel numbers differ");
507 if (_edit_rate != other->_edit_rate) {
508 note (DCP_ERROR, "Subtitle edit rates differ");
512 if (_time_code_rate != other->_time_code_rate) {
513 note (DCP_ERROR, "Subtitle time code rates differ");
517 if (_start_time != other->_start_time) {
518 note (DCP_ERROR, "Subtitle start times differ");
526 SMPTESubtitleAsset::add_font (string load_id, boost::filesystem::path file)
528 string const uuid = make_uuid ();
529 _fonts.push_back (Font (load_id, uuid, file));
530 _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
534 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
536 SubtitleAsset::add (s);
537 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);