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 = 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 buffer.SetData (j->data.data().get(), j->data.size());
422 buffer.Size (j->data.size());
423 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
424 if (ASDCP_FAILURE (r)) {
425 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
430 /* Image subtitle payload */
432 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
433 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
435 ASDCP::TimedText::FrameBuffer buffer;
436 buffer.SetData (si->png_image().data().get(), si->png_image().size());
437 buffer.Size (si->png_image().size());
438 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
439 if (ASDCP_FAILURE(r)) {
440 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
451 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
453 if (!SubtitleAsset::equals (other_asset, options, note)) {
457 shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
459 note (DCP_ERROR, "Subtitles are in different standards");
463 list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
464 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
466 while (i != _load_font_nodes.end ()) {
467 if (j == other->_load_font_nodes.end ()) {
468 note (DCP_ERROR, "<LoadFont> nodes differ");
472 if ((*i)->id != (*j)->id) {
473 note (DCP_ERROR, "<LoadFont> nodes differ");
481 if (_content_title_text != other->_content_title_text) {
482 note (DCP_ERROR, "Subtitle content title texts differ");
486 if (_language != other->_language) {
487 note (DCP_ERROR, "Subtitle languages differ");
491 if (_annotation_text != other->_annotation_text) {
492 note (DCP_ERROR, "Subtitle annotation texts differ");
496 if (_issue_date != other->_issue_date) {
497 if (options.issue_dates_can_differ) {
498 note (DCP_NOTE, "Subtitle issue dates differ");
500 note (DCP_ERROR, "Subtitle issue dates differ");
505 if (_reel_number != other->_reel_number) {
506 note (DCP_ERROR, "Subtitle reel numbers differ");
510 if (_edit_rate != other->_edit_rate) {
511 note (DCP_ERROR, "Subtitle edit rates differ");
515 if (_time_code_rate != other->_time_code_rate) {
516 note (DCP_ERROR, "Subtitle time code rates differ");
520 if (_start_time != other->_start_time) {
521 note (DCP_ERROR, "Subtitle start times differ");
529 SMPTESubtitleAsset::add_font (string load_id, boost::filesystem::path file)
531 string const uuid = make_uuid ();
532 _fonts.push_back (Font (load_id, uuid, file));
533 _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
537 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
539 SubtitleAsset::add (s);
540 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);