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;
61 using boost::is_any_of;
62 using boost::shared_array;
63 using std::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 = Kumu::RESULT_OK;
91 ASDCPErrorSuspender sus;
92 r = reader->OpenRead (_file->string().c_str ());
94 if (!ASDCP_FAILURE (r)) {
96 ASDCP::WriterInfo info;
97 reader->FillWriterInfo (info);
98 _id = read_writer_info (info);
100 /* Not encrypted; read it in now */
101 reader->ReadTimedTextResource (_raw_xml);
102 xml->read_string (_raw_xml);
104 read_mxf_descriptor (reader, shared_ptr<DecryptionContext> (new DecryptionContext (optional<Key>(), SMPTE)));
109 _raw_xml = dcp::file_to_string (file);
110 xml.reset (new cxml::Document ("SubtitleReel"));
111 xml->read_file (file);
113 _id = _xml_id = remove_urn_uuid (xml->string_child ("Id"));
114 } catch (cxml::Error& e) {
115 boost::throw_exception (
118 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
119 file, static_cast<int> (r), e.what ()
125 /* Try to read PNG files from the same folder that the XML is in; the wisdom of this is
126 debatable, at best...
128 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
129 shared_ptr<SubtitleImage> im = dynamic_pointer_cast<SubtitleImage>(i);
130 if (im && im->png_image().size() == 0) {
131 /* Even more dubious; allow <id>.png or urn:uuid:<id>.png */
132 boost::filesystem::path p = file.parent_path() / String::compose("%1.png", im->id());
133 if (boost::filesystem::is_regular_file(p)) {
134 im->read_png_file (p);
135 } else if (starts_with (im->id(), "urn:uuid:")) {
136 p = file.parent_path() / String::compose("%1.png", remove_urn_uuid(im->id()));
137 if (boost::filesystem::is_regular_file(p)) {
138 im->read_png_file (p);
145 /* Check that all required image data have been found */
146 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
147 shared_ptr<SubtitleImage> im = dynamic_pointer_cast<SubtitleImage>(i);
148 if (im && im->png_image().size() == 0) {
149 throw MissingSubtitleImageError (im->id());
155 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
157 _xml_id = remove_urn_uuid(xml->string_child("Id"));
158 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
160 _content_title_text = xml->string_child ("ContentTitleText");
161 _annotation_text = xml->optional_string_child ("AnnotationText");
162 _issue_date = LocalTime (xml->string_child ("IssueDate"));
163 _reel_number = xml->optional_number_child<int> ("ReelNumber");
164 _language = xml->optional_string_child ("Language");
166 /* This is supposed to be two numbers, but a single number has been seen in the wild */
167 string const er = xml->string_child ("EditRate");
168 vector<string> er_parts;
169 split (er_parts, er, is_any_of (" "));
170 if (er_parts.size() == 1) {
171 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
172 } else if (er_parts.size() == 2) {
173 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
175 throw XMLError ("malformed EditRate " + er);
178 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
179 if (xml->optional_string_child ("StartTime")) {
180 _start_time = Time (xml->string_child ("StartTime"), _time_code_rate);
183 /* Now we need to drop down to xmlpp */
186 xmlpp::Node::NodeList c = xml->node()->get_children ();
187 for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
188 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
189 if (e && e->get_name() == "SubtitleList") {
190 parse_subtitles (e, ps, _time_code_rate, SMPTE);
194 /* Guess intrinsic duration */
195 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
199 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
201 ASDCP::TimedText::TimedTextDescriptor descriptor;
202 reader->FillTimedTextDescriptor (descriptor);
204 /* Load fonts and images */
207 ASDCP::TimedText::ResourceList_t::const_iterator i = descriptor.ResourceList.begin();
208 i != descriptor.ResourceList.end();
211 ASDCP::TimedText::FrameBuffer buffer;
212 buffer.Capacity (10 * 1024 * 1024);
213 reader->ReadAncillaryResource (i->ResourceID, buffer, dec->context(), dec->hmac());
216 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof (id));
218 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
219 memcpy (data.get(), buffer.RoData(), buffer.Size());
222 case ASDCP::TimedText::MT_OPENTYPE:
224 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = _load_font_nodes.begin ();
225 while (j != _load_font_nodes.end() && (*j)->urn != id) {
229 if (j != _load_font_nodes.end ()) {
230 _fonts.push_back (Font ((*j)->id, (*j)->urn, ArrayData (data, buffer.Size ())));
234 case ASDCP::TimedText::MT_PNG:
236 list<shared_ptr<Subtitle> >::const_iterator j = _subtitles.begin ();
237 while (j != _subtitles.end() && ((!dynamic_pointer_cast<SubtitleImage>(*j)) || dynamic_pointer_cast<SubtitleImage>(*j)->id() != id)) {
241 if (j != _subtitles.end()) {
242 dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image (ArrayData(data, buffer.Size()));
251 /* Get intrinsic duration */
252 _intrinsic_duration = descriptor.ContainerDuration;
256 SMPTESubtitleAsset::set_key (Key key)
258 /* See if we already have a key; if we do, and we have a file, we'll already
261 bool const had_key = static_cast<bool> (_key);
265 if (!_key_id || !_file || had_key) {
266 /* Either we don't have any data to read, it wasn't
267 encrypted, or we've already read it, so we don't
268 need to do anything else.
273 /* Our data was encrypted; now we can decrypt it */
275 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
276 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
277 if (ASDCP_FAILURE (r)) {
278 boost::throw_exception (
280 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
285 shared_ptr<DecryptionContext> dec (new DecryptionContext (key, SMPTE));
286 reader->ReadTimedTextResource (_raw_xml, dec->context(), dec->hmac());
287 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
288 xml->read_string (_raw_xml);
290 read_mxf_descriptor (reader, dec);
293 list<shared_ptr<LoadFontNode> >
294 SMPTESubtitleAsset::load_font_nodes () const
296 list<shared_ptr<LoadFontNode> > lf;
297 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
302 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
304 ASDCP::TimedText::MXFReader reader;
305 Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
306 Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
307 Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
308 return !ASDCP_FAILURE (r);
312 SMPTESubtitleAsset::xml_as_string () const
315 xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel");
316 root->set_namespace_declaration (subtitle_smpte_ns, "dcst");
317 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
319 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _xml_id);
320 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
321 if (_annotation_text) {
322 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
324 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
326 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
329 root->add_child("Language", "dcst")->add_child_text (_language.get ());
331 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
332 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
334 root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string (SMPTE));
337 BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) {
338 xmlpp::Element* load_font = root->add_child("LoadFont", "dcst");
339 load_font->add_child_text ("urn:uuid:" + i->urn);
340 load_font->set_attribute ("ID", i->id);
343 subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, SMPTE);
345 return doc.write_to_string ("UTF-8");
348 /** Write this content to a MXF file */
350 SMPTESubtitleAsset::write (boost::filesystem::path p) const
352 EncryptionContext enc (key(), SMPTE);
354 ASDCP::WriterInfo writer_info;
355 fill_writer_info (&writer_info, _id);
357 ASDCP::TimedText::TimedTextDescriptor descriptor;
358 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
359 descriptor.EncodingName = "UTF-8";
361 /* Font references */
363 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
364 list<Font>::const_iterator j = _fonts.begin ();
365 while (j != _fonts.end() && j->load_id != i->id) {
368 if (j != _fonts.end ()) {
369 ASDCP::TimedText::TimedTextResourceDescriptor res;
371 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
372 DCP_ASSERT (c == Kumu::UUID_Length);
373 res.Type = ASDCP::TimedText::MT_OPENTYPE;
374 descriptor.ResourceList.push_back (res);
378 /* Image subtitle references */
380 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
381 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
383 ASDCP::TimedText::TimedTextResourceDescriptor res;
385 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
386 DCP_ASSERT (c == Kumu::UUID_Length);
387 res.Type = ASDCP::TimedText::MT_PNG;
388 descriptor.ResourceList.push_back (res);
392 descriptor.NamespaceName = subtitle_smpte_ns;
394 Kumu::hex2bin (_xml_id.c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
395 DCP_ASSERT (c == Kumu::UUID_Length);
396 descriptor.ContainerDuration = _intrinsic_duration;
398 ASDCP::TimedText::MXFWriter writer;
399 /* This header size is a guess. Empirically it seems that each subtitle reference is 90 bytes, and we need some extra.
400 The default size is not enough for some feature-length PNG sub projects (see DCP-o-matic #1561).
402 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor, _subtitles.size() * 90 + 16384);
403 if (ASDCP_FAILURE (r)) {
404 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
407 r = writer.WriteTimedTextResource (xml_as_string (), enc.context(), enc.hmac());
408 if (ASDCP_FAILURE (r)) {
409 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
414 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
415 list<Font>::const_iterator j = _fonts.begin ();
416 while (j != _fonts.end() && j->load_id != i->id) {
419 if (j != _fonts.end ()) {
420 ASDCP::TimedText::FrameBuffer buffer;
421 ArrayData data_copy(j->data);
422 buffer.SetData (data_copy.data(), data_copy.size());
423 buffer.Size (j->data.size());
424 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
425 if (ASDCP_FAILURE (r)) {
426 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
431 /* Image subtitle payload */
433 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
434 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
436 ASDCP::TimedText::FrameBuffer buffer;
437 buffer.SetData (si->png_image().data(), si->png_image().size());
438 buffer.Size (si->png_image().size());
439 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
440 if (ASDCP_FAILURE(r)) {
441 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
452 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
454 if (!SubtitleAsset::equals (other_asset, options, note)) {
458 shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
460 note (DCP_ERROR, "Subtitles are in different standards");
464 list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
465 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
467 while (i != _load_font_nodes.end ()) {
468 if (j == other->_load_font_nodes.end ()) {
469 note (DCP_ERROR, "<LoadFont> nodes differ");
473 if ((*i)->id != (*j)->id) {
474 note (DCP_ERROR, "<LoadFont> nodes differ");
482 if (_content_title_text != other->_content_title_text) {
483 note (DCP_ERROR, "Subtitle content title texts differ");
487 if (_language != other->_language) {
488 note (DCP_ERROR, String::compose("Subtitle languages differ (`%1' vs `%2')", _language.get_value_or("[none]"), other->_language.get_value_or("[none]")));
492 if (_annotation_text != other->_annotation_text) {
493 note (DCP_ERROR, "Subtitle annotation texts differ");
497 if (_issue_date != other->_issue_date) {
498 if (options.issue_dates_can_differ) {
499 note (DCP_NOTE, "Subtitle issue dates differ");
501 note (DCP_ERROR, "Subtitle issue dates differ");
506 if (_reel_number != other->_reel_number) {
507 note (DCP_ERROR, "Subtitle reel numbers differ");
511 if (_edit_rate != other->_edit_rate) {
512 note (DCP_ERROR, "Subtitle edit rates differ");
516 if (_time_code_rate != other->_time_code_rate) {
517 note (DCP_ERROR, "Subtitle time code rates differ");
521 if (_start_time != other->_start_time) {
522 note (DCP_ERROR, "Subtitle start times differ");
530 SMPTESubtitleAsset::add_font (string load_id, dcp::ArrayData data)
532 string const uuid = make_uuid ();
533 _fonts.push_back (Font(load_id, uuid, data));
534 _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
538 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
540 SubtitleAsset::add (s);
541 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);