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 std::shared_ptr;
60 using std::dynamic_pointer_cast;
61 using std::make_shared;
63 using boost::is_any_of;
64 using boost::shared_array;
65 using boost::optional;
66 using boost::starts_with;
69 static string const subtitle_smpte_ns = "http://www.smpte-ra.org/schemas/428-7/2010/DCST";
71 SMPTESubtitleAsset::SMPTESubtitleAsset ()
72 : MXF (Standard::SMPTE)
73 , _intrinsic_duration (0)
75 , _time_code_rate (24)
76 , _xml_id (make_uuid ())
81 /** Construct a SMPTESubtitleAsset by reading an MXF or XML file.
82 * @param file Filename.
84 SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file)
85 : SubtitleAsset (file)
87 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
89 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
90 Kumu::Result_t r = Kumu::RESULT_OK;
92 ASDCPErrorSuspender sus;
93 r = reader->OpenRead (_file->string().c_str ());
95 if (!ASDCP_FAILURE (r)) {
97 ASDCP::WriterInfo info;
98 reader->FillWriterInfo (info);
99 _id = read_writer_info (info);
101 /* Not encrypted; read it in now */
102 reader->ReadTimedTextResource (_raw_xml);
103 xml->read_string (_raw_xml);
105 read_mxf_descriptor (reader, shared_ptr<DecryptionContext> (new DecryptionContext (optional<Key>(), Standard::SMPTE)));
110 _raw_xml = dcp::file_to_string (file);
111 xml.reset (new cxml::Document ("SubtitleReel"));
112 xml->read_file (file);
114 _id = _xml_id = remove_urn_uuid (xml->string_child ("Id"));
115 } catch (cxml::Error& e) {
116 boost::throw_exception (
119 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
120 file, static_cast<int> (r), e.what ()
126 /* Try to read PNG files from the same folder that the XML is in; the wisdom of this is
127 debatable, at best...
129 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
130 shared_ptr<SubtitleImage> im = dynamic_pointer_cast<SubtitleImage>(i);
131 if (im && im->png_image().size() == 0) {
132 /* Even more dubious; allow <id>.png or urn:uuid:<id>.png */
133 boost::filesystem::path p = file.parent_path() / String::compose("%1.png", im->id());
134 if (boost::filesystem::is_regular_file(p)) {
135 im->read_png_file (p);
136 } else if (starts_with (im->id(), "urn:uuid:")) {
137 p = file.parent_path() / String::compose("%1.png", remove_urn_uuid(im->id()));
138 if (boost::filesystem::is_regular_file(p)) {
139 im->read_png_file (p);
144 _standard = Standard::SMPTE;
147 /* Check that all required image data have been found */
148 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
149 shared_ptr<SubtitleImage> im = dynamic_pointer_cast<SubtitleImage>(i);
150 if (im && im->png_image().size() == 0) {
151 throw MissingSubtitleImageError (im->id());
157 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
159 _xml_id = remove_urn_uuid(xml->string_child("Id"));
160 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
162 _content_title_text = xml->string_child ("ContentTitleText");
163 _annotation_text = xml->optional_string_child ("AnnotationText");
164 _issue_date = LocalTime (xml->string_child ("IssueDate"));
165 _reel_number = xml->optional_number_child<int> ("ReelNumber");
166 _language = xml->optional_string_child ("Language");
168 /* This is supposed to be two numbers, but a single number has been seen in the wild */
169 string const er = xml->string_child ("EditRate");
170 vector<string> er_parts;
171 split (er_parts, er, is_any_of (" "));
172 if (er_parts.size() == 1) {
173 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
174 } else if (er_parts.size() == 2) {
175 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
177 throw XMLError ("malformed EditRate " + er);
180 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
181 if (xml->optional_string_child ("StartTime")) {
182 _start_time = Time (xml->string_child ("StartTime"), _time_code_rate);
185 /* Now we need to drop down to xmlpp */
187 vector<ParseState> ps;
188 xmlpp::Node::NodeList c = xml->node()->get_children ();
189 for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
190 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
191 if (e && e->get_name() == "SubtitleList") {
192 parse_subtitles (e, ps, _time_code_rate, Standard::SMPTE);
196 /* Guess intrinsic duration */
197 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
201 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
203 ASDCP::TimedText::TimedTextDescriptor descriptor;
204 reader->FillTimedTextDescriptor (descriptor);
206 /* Load fonts and images */
209 ASDCP::TimedText::ResourceList_t::const_iterator i = descriptor.ResourceList.begin();
210 i != descriptor.ResourceList.end();
213 ASDCP::TimedText::FrameBuffer buffer;
214 buffer.Capacity (10 * 1024 * 1024);
215 reader->ReadAncillaryResource (i->ResourceID, buffer, dec->context(), dec->hmac());
218 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof (id));
220 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
221 memcpy (data.get(), buffer.RoData(), buffer.Size());
224 case ASDCP::TimedText::MT_OPENTYPE:
226 auto j = _load_font_nodes.begin();
227 while (j != _load_font_nodes.end() && (*j)->urn != id) {
231 if (j != _load_font_nodes.end ()) {
232 _fonts.push_back (Font ((*j)->id, (*j)->urn, ArrayData (data, buffer.Size ())));
236 case ASDCP::TimedText::MT_PNG:
238 auto j = _subtitles.begin();
239 while (j != _subtitles.end() && ((!dynamic_pointer_cast<SubtitleImage>(*j)) || dynamic_pointer_cast<SubtitleImage>(*j)->id() != id)) {
243 if (j != _subtitles.end()) {
244 dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image (ArrayData(data, buffer.Size()));
253 /* Get intrinsic duration */
254 _intrinsic_duration = descriptor.ContainerDuration;
258 SMPTESubtitleAsset::set_key (Key key)
260 /* See if we already have a key; if we do, and we have a file, we'll already
263 bool const had_key = static_cast<bool> (_key);
267 if (!_key_id || !_file || had_key) {
268 /* Either we don't have any data to read, it wasn't
269 encrypted, or we've already read it, so we don't
270 need to do anything else.
275 /* Our data was encrypted; now we can decrypt it */
277 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
278 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
279 if (ASDCP_FAILURE (r)) {
280 boost::throw_exception (
282 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
287 auto dec = make_shared<DecryptionContext>(key, Standard::SMPTE);
288 reader->ReadTimedTextResource (_raw_xml, dec->context(), dec->hmac());
289 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
290 xml->read_string (_raw_xml);
292 read_mxf_descriptor (reader, dec);
295 vector<shared_ptr<LoadFontNode>>
296 SMPTESubtitleAsset::load_font_nodes () const
298 vector<shared_ptr<LoadFontNode>> lf;
299 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
304 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
306 ASDCP::TimedText::MXFReader reader;
307 Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
308 Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
309 Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
310 return !ASDCP_FAILURE (r);
314 SMPTESubtitleAsset::xml_as_string () const
317 xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel");
318 root->set_namespace_declaration (subtitle_smpte_ns, "dcst");
319 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
321 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _xml_id);
322 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
323 if (_annotation_text) {
324 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
326 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
328 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
331 root->add_child("Language", "dcst")->add_child_text (_language.get ());
333 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
334 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
336 root->add_child("StartTime", "dcst")->add_child_text(_start_time.get().as_string(Standard::SMPTE));
339 BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) {
340 xmlpp::Element* load_font = root->add_child("LoadFont", "dcst");
341 load_font->add_child_text ("urn:uuid:" + i->urn);
342 load_font->set_attribute ("ID", i->id);
345 subtitles_as_xml (root->add_child("SubtitleList", "dcst"), _time_code_rate, Standard::SMPTE);
347 return doc.write_to_string ("UTF-8");
350 /** Write this content to a MXF file */
352 SMPTESubtitleAsset::write (boost::filesystem::path p) const
354 EncryptionContext enc (key(), Standard::SMPTE);
356 ASDCP::WriterInfo writer_info;
357 fill_writer_info (&writer_info, _id);
359 ASDCP::TimedText::TimedTextDescriptor descriptor;
360 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
361 descriptor.EncodingName = "UTF-8";
363 /* Font references */
365 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
366 auto j = _fonts.begin();
367 while (j != _fonts.end() && j->load_id != i->id) {
370 if (j != _fonts.end ()) {
371 ASDCP::TimedText::TimedTextResourceDescriptor res;
373 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
374 DCP_ASSERT (c == Kumu::UUID_Length);
375 res.Type = ASDCP::TimedText::MT_OPENTYPE;
376 descriptor.ResourceList.push_back (res);
380 /* Image subtitle references */
382 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
383 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
385 ASDCP::TimedText::TimedTextResourceDescriptor res;
387 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
388 DCP_ASSERT (c == Kumu::UUID_Length);
389 res.Type = ASDCP::TimedText::MT_PNG;
390 descriptor.ResourceList.push_back (res);
394 descriptor.NamespaceName = subtitle_smpte_ns;
396 Kumu::hex2bin (_xml_id.c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
397 DCP_ASSERT (c == Kumu::UUID_Length);
398 descriptor.ContainerDuration = _intrinsic_duration;
400 ASDCP::TimedText::MXFWriter writer;
401 /* This header size is a guess. Empirically it seems that each subtitle reference is 90 bytes, and we need some extra.
402 The default size is not enough for some feature-length PNG sub projects (see DCP-o-matic #1561).
404 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor, _subtitles.size() * 90 + 16384);
405 if (ASDCP_FAILURE (r)) {
406 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
409 r = writer.WriteTimedTextResource (xml_as_string (), enc.context(), enc.hmac());
410 if (ASDCP_FAILURE (r)) {
411 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
416 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
417 auto j = _fonts.begin();
418 while (j != _fonts.end() && j->load_id != i->id) {
421 if (j != _fonts.end ()) {
422 ASDCP::TimedText::FrameBuffer buffer;
423 ArrayData data_copy(j->data);
424 buffer.SetData (data_copy.data(), data_copy.size());
425 buffer.Size (j->data.size());
426 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
427 if (ASDCP_FAILURE (r)) {
428 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
433 /* Image subtitle payload */
435 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
436 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
438 ASDCP::TimedText::FrameBuffer buffer;
439 buffer.SetData (si->png_image().data(), si->png_image().size());
440 buffer.Size (si->png_image().size());
441 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
442 if (ASDCP_FAILURE(r)) {
443 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
454 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
456 if (!SubtitleAsset::equals (other_asset, options, note)) {
460 shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
462 note (NoteType::ERROR, "Subtitles are in different standards");
466 auto i = _load_font_nodes.begin();
467 auto j = other->_load_font_nodes.begin();
469 while (i != _load_font_nodes.end ()) {
470 if (j == other->_load_font_nodes.end ()) {
471 note (NoteType::ERROR, "<LoadFont> nodes differ");
475 if ((*i)->id != (*j)->id) {
476 note (NoteType::ERROR, "<LoadFont> nodes differ");
484 if (_content_title_text != other->_content_title_text) {
485 note (NoteType::ERROR, "Subtitle content title texts differ");
489 if (_language != other->_language) {
490 note (NoteType::ERROR, String::compose("Subtitle languages differ (`%1' vs `%2')", _language.get_value_or("[none]"), other->_language.get_value_or("[none]")));
494 if (_annotation_text != other->_annotation_text) {
495 note (NoteType::ERROR, "Subtitle annotation texts differ");
499 if (_issue_date != other->_issue_date) {
500 if (options.issue_dates_can_differ) {
501 note (NoteType::NOTE, "Subtitle issue dates differ");
503 note (NoteType::ERROR, "Subtitle issue dates differ");
508 if (_reel_number != other->_reel_number) {
509 note (NoteType::ERROR, "Subtitle reel numbers differ");
513 if (_edit_rate != other->_edit_rate) {
514 note (NoteType::ERROR, "Subtitle edit rates differ");
518 if (_time_code_rate != other->_time_code_rate) {
519 note (NoteType::ERROR, "Subtitle time code rates differ");
523 if (_start_time != other->_start_time) {
524 note (NoteType::ERROR, "Subtitle start times differ");
532 SMPTESubtitleAsset::add_font (string load_id, dcp::ArrayData data)
534 string const uuid = make_uuid ();
535 _fonts.push_back (Font(load_id, uuid, data));
536 _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
540 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
542 SubtitleAsset::add (s);
543 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);