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);
143 _standard = dcp::SMPTE;
146 /* Check that all required image data have been found */
147 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
148 shared_ptr<SubtitleImage> im = dynamic_pointer_cast<SubtitleImage>(i);
149 if (im && im->png_image().size() == 0) {
150 throw MissingSubtitleImageError (im->id());
156 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
158 _xml_id = remove_urn_uuid(xml->string_child("Id"));
159 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
161 _content_title_text = xml->string_child ("ContentTitleText");
162 _annotation_text = xml->optional_string_child ("AnnotationText");
163 _issue_date = LocalTime (xml->string_child ("IssueDate"));
164 _reel_number = xml->optional_number_child<int> ("ReelNumber");
165 _language = xml->optional_string_child ("Language");
167 /* This is supposed to be two numbers, but a single number has been seen in the wild */
168 string const er = xml->string_child ("EditRate");
169 vector<string> er_parts;
170 split (er_parts, er, is_any_of (" "));
171 if (er_parts.size() == 1) {
172 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
173 } else if (er_parts.size() == 2) {
174 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
176 throw XMLError ("malformed EditRate " + er);
179 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
180 if (xml->optional_string_child ("StartTime")) {
181 _start_time = Time (xml->string_child ("StartTime"), _time_code_rate);
184 /* Now we need to drop down to xmlpp */
187 xmlpp::Node::NodeList c = xml->node()->get_children ();
188 for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
189 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
190 if (e && e->get_name() == "SubtitleList") {
191 parse_subtitles (e, ps, _time_code_rate, SMPTE);
195 /* Guess intrinsic duration */
196 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
200 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
202 ASDCP::TimedText::TimedTextDescriptor descriptor;
203 reader->FillTimedTextDescriptor (descriptor);
205 /* Load fonts and images */
208 ASDCP::TimedText::ResourceList_t::const_iterator i = descriptor.ResourceList.begin();
209 i != descriptor.ResourceList.end();
212 ASDCP::TimedText::FrameBuffer buffer;
213 buffer.Capacity (10 * 1024 * 1024);
214 reader->ReadAncillaryResource (i->ResourceID, buffer, dec->context(), dec->hmac());
217 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof (id));
219 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
220 memcpy (data.get(), buffer.RoData(), buffer.Size());
223 case ASDCP::TimedText::MT_OPENTYPE:
225 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = _load_font_nodes.begin ();
226 while (j != _load_font_nodes.end() && (*j)->urn != id) {
230 if (j != _load_font_nodes.end ()) {
231 _fonts.push_back (Font ((*j)->id, (*j)->urn, ArrayData (data, buffer.Size ())));
235 case ASDCP::TimedText::MT_PNG:
237 list<shared_ptr<Subtitle> >::const_iterator j = _subtitles.begin ();
238 while (j != _subtitles.end() && ((!dynamic_pointer_cast<SubtitleImage>(*j)) || dynamic_pointer_cast<SubtitleImage>(*j)->id() != id)) {
242 if (j != _subtitles.end()) {
243 dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image (ArrayData(data, buffer.Size()));
252 /* Get intrinsic duration */
253 _intrinsic_duration = descriptor.ContainerDuration;
257 SMPTESubtitleAsset::set_key (Key key)
259 /* See if we already have a key; if we do, and we have a file, we'll already
262 bool const had_key = static_cast<bool> (_key);
266 if (!_key_id || !_file || had_key) {
267 /* Either we don't have any data to read, it wasn't
268 encrypted, or we've already read it, so we don't
269 need to do anything else.
274 /* Our data was encrypted; now we can decrypt it */
276 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
277 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
278 if (ASDCP_FAILURE (r)) {
279 boost::throw_exception (
281 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
286 shared_ptr<DecryptionContext> dec (new DecryptionContext (key, SMPTE));
287 reader->ReadTimedTextResource (_raw_xml, dec->context(), dec->hmac());
288 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
289 xml->read_string (_raw_xml);
291 read_mxf_descriptor (reader, dec);
294 list<shared_ptr<LoadFontNode> >
295 SMPTESubtitleAsset::load_font_nodes () const
297 list<shared_ptr<LoadFontNode> > lf;
298 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
303 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
305 ASDCP::TimedText::MXFReader reader;
306 Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
307 Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
308 Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
309 return !ASDCP_FAILURE (r);
313 SMPTESubtitleAsset::xml_as_string () const
316 xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel");
317 root->set_namespace_declaration (subtitle_smpte_ns, "dcst");
318 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
320 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _xml_id);
321 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
322 if (_annotation_text) {
323 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
325 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
327 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
330 root->add_child("Language", "dcst")->add_child_text (_language.get ());
332 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
333 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
335 root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string (SMPTE));
338 BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) {
339 xmlpp::Element* load_font = root->add_child("LoadFont", "dcst");
340 load_font->add_child_text ("urn:uuid:" + i->urn);
341 load_font->set_attribute ("ID", i->id);
344 subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, SMPTE);
346 return doc.write_to_string ("UTF-8");
349 /** Write this content to a MXF file */
351 SMPTESubtitleAsset::write (boost::filesystem::path p) const
353 EncryptionContext enc (key(), SMPTE);
355 ASDCP::WriterInfo writer_info;
356 fill_writer_info (&writer_info, _id);
358 ASDCP::TimedText::TimedTextDescriptor descriptor;
359 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
360 descriptor.EncodingName = "UTF-8";
362 /* Font references */
364 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
365 list<Font>::const_iterator j = _fonts.begin ();
366 while (j != _fonts.end() && j->load_id != i->id) {
369 if (j != _fonts.end ()) {
370 ASDCP::TimedText::TimedTextResourceDescriptor res;
372 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
373 DCP_ASSERT (c == Kumu::UUID_Length);
374 res.Type = ASDCP::TimedText::MT_OPENTYPE;
375 descriptor.ResourceList.push_back (res);
379 /* Image subtitle references */
381 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
382 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
384 ASDCP::TimedText::TimedTextResourceDescriptor res;
386 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
387 DCP_ASSERT (c == Kumu::UUID_Length);
388 res.Type = ASDCP::TimedText::MT_PNG;
389 descriptor.ResourceList.push_back (res);
393 descriptor.NamespaceName = subtitle_smpte_ns;
395 Kumu::hex2bin (_xml_id.c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
396 DCP_ASSERT (c == Kumu::UUID_Length);
397 descriptor.ContainerDuration = _intrinsic_duration;
399 ASDCP::TimedText::MXFWriter writer;
400 /* This header size is a guess. Empirically it seems that each subtitle reference is 90 bytes, and we need some extra.
401 The default size is not enough for some feature-length PNG sub projects (see DCP-o-matic #1561).
403 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor, _subtitles.size() * 90 + 16384);
404 if (ASDCP_FAILURE (r)) {
405 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
408 r = writer.WriteTimedTextResource (xml_as_string (), enc.context(), enc.hmac());
409 if (ASDCP_FAILURE (r)) {
410 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
415 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
416 list<Font>::const_iterator j = _fonts.begin ();
417 while (j != _fonts.end() && j->load_id != i->id) {
420 if (j != _fonts.end ()) {
421 ASDCP::TimedText::FrameBuffer buffer;
422 ArrayData data_copy(j->data);
423 buffer.SetData (data_copy.data(), data_copy.size());
424 buffer.Size (j->data.size());
425 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
426 if (ASDCP_FAILURE (r)) {
427 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
432 /* Image subtitle payload */
434 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
435 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
437 ASDCP::TimedText::FrameBuffer buffer;
438 buffer.SetData (si->png_image().data(), si->png_image().size());
439 buffer.Size (si->png_image().size());
440 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
441 if (ASDCP_FAILURE(r)) {
442 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
453 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
455 if (!SubtitleAsset::equals (other_asset, options, note)) {
459 shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
461 note (DCP_ERROR, "Subtitles are in different standards");
465 list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
466 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
468 while (i != _load_font_nodes.end ()) {
469 if (j == other->_load_font_nodes.end ()) {
470 note (DCP_ERROR, "<LoadFont> nodes differ");
474 if ((*i)->id != (*j)->id) {
475 note (DCP_ERROR, "<LoadFont> nodes differ");
483 if (_content_title_text != other->_content_title_text) {
484 note (DCP_ERROR, "Subtitle content title texts differ");
488 if (_language != other->_language) {
489 note (DCP_ERROR, String::compose("Subtitle languages differ (`%1' vs `%2')", _language.get_value_or("[none]"), other->_language.get_value_or("[none]")));
493 if (_annotation_text != other->_annotation_text) {
494 note (DCP_ERROR, "Subtitle annotation texts differ");
498 if (_issue_date != other->_issue_date) {
499 if (options.issue_dates_can_differ) {
500 note (DCP_NOTE, "Subtitle issue dates differ");
502 note (DCP_ERROR, "Subtitle issue dates differ");
507 if (_reel_number != other->_reel_number) {
508 note (DCP_ERROR, "Subtitle reel numbers differ");
512 if (_edit_rate != other->_edit_rate) {
513 note (DCP_ERROR, "Subtitle edit rates differ");
517 if (_time_code_rate != other->_time_code_rate) {
518 note (DCP_ERROR, "Subtitle time code rates differ");
522 if (_start_time != other->_start_time) {
523 note (DCP_ERROR, "Subtitle start times differ");
531 SMPTESubtitleAsset::add_font (string load_id, dcp::ArrayData data)
533 string const uuid = make_uuid ();
534 _fonts.push_back (Font(load_id, uuid, data));
535 _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
539 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
541 SubtitleAsset::add (s);
542 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);