2 Copyright (C) 2012-2021 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.
35 /** @file src/smpte_subtitle_asset.cc
36 * @brief SMPTESubtitleAsset class
40 #include "compose.hpp"
41 #include "crypto_context.h"
42 #include "dcp_assert.h"
43 #include "equality_options.h"
44 #include "exceptions.h"
45 #include "filesystem.h"
46 #include "raw_convert.h"
47 #include "smpte_load_font_node.h"
48 #include "smpte_subtitle_asset.h"
49 #include "subtitle_image.h"
53 LIBDCP_DISABLE_WARNINGS
54 #include <asdcp/AS_DCP.h>
55 #include <asdcp/KM_util.h>
56 #include <asdcp/KM_log.h>
57 #include <libxml++/libxml++.h>
58 LIBDCP_ENABLE_WARNINGS
59 #include <boost/algorithm/string.hpp>
66 using std::shared_ptr;
67 using std::dynamic_pointer_cast;
68 using std::make_shared;
70 using boost::is_any_of;
71 using boost::shared_array;
72 using boost::optional;
73 using boost::starts_with;
77 static string const subtitle_smpte_ns_2007 = "http://www.smpte-ra.org/schemas/428-7/2007/DCST";
78 static string const subtitle_smpte_ns_2010 = "http://www.smpte-ra.org/schemas/428-7/2010/DCST";
79 static string const subtitle_smpte_ns_2014 = "http://www.smpte-ra.org/schemas/428-7/2014/DCST";
82 SMPTESubtitleAsset::SMPTESubtitleAsset(SubtitleStandard standard)
83 : MXF(Standard::SMPTE)
85 , _time_code_rate (24)
86 , _subtitle_standard(standard)
87 , _xml_id (make_uuid())
93 SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file)
94 : SubtitleAsset (file)
96 auto xml = make_shared<cxml::Document>("SubtitleReel");
98 auto reader = make_shared<ASDCP::TimedText::MXFReader>();
99 auto r = Kumu::RESULT_OK;
101 ASDCPErrorSuspender sus;
102 r = reader->OpenRead(dcp::filesystem::fix_long_path(*_file).string().c_str());
104 if (!ASDCP_FAILURE(r)) {
106 ASDCP::WriterInfo info;
107 reader->FillWriterInfo (info);
108 _id = read_writer_info (info);
110 /* Not encrypted; read it in now */
112 reader->ReadTimedTextResource (xml_string);
113 _raw_xml = xml_string;
114 xml->read_string (xml_string);
116 read_mxf_descriptor (reader);
117 read_mxf_resources(reader, std::make_shared<DecryptionContext>(optional<Key>(), Standard::SMPTE));
119 read_mxf_descriptor (reader);
124 _raw_xml = dcp::file_to_string (file);
125 xml = make_shared<cxml::Document>("SubtitleReel");
126 xml->read_file(dcp::filesystem::fix_long_path(file));
128 } catch (cxml::Error& e) {
129 boost::throw_exception (
132 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
133 file, static_cast<int>(r), e.what()
139 /* Try to read PNG files from the same folder that the XML is in; the wisdom of this is
140 debatable, at best...
142 for (auto i: _subtitles) {
143 auto im = dynamic_pointer_cast<SubtitleImage>(i);
144 if (im && im->png_image().size() == 0) {
145 /* Even more dubious; allow <id>.png or urn:uuid:<id>.png */
146 auto p = file.parent_path() / String::compose("%1.png", im->id());
147 if (filesystem::is_regular_file(p)) {
148 im->read_png_file (p);
149 } else if (starts_with (im->id(), "urn:uuid:")) {
150 p = file.parent_path() / String::compose("%1.png", remove_urn_uuid(im->id()));
151 if (filesystem::is_regular_file(p)) {
152 im->read_png_file (p);
157 _standard = Standard::SMPTE;
160 /* Check that all required image data have been found */
161 for (auto i: _subtitles) {
162 auto im = dynamic_pointer_cast<SubtitleImage>(i);
163 if (im && im->png_image().size() == 0) {
164 throw MissingSubtitleImageError (im->id());
171 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
173 if (xml->namespace_uri() == subtitle_smpte_ns_2007) {
174 _subtitle_standard = SubtitleStandard::SMPTE_2007;
175 } else if (xml->namespace_uri() == subtitle_smpte_ns_2010) {
176 _subtitle_standard = SubtitleStandard::SMPTE_2010;
177 } else if (xml->namespace_uri() == subtitle_smpte_ns_2014) {
178 _subtitle_standard = SubtitleStandard::SMPTE_2014;
180 throw XMLError("Unrecognised subtitle namespace " + xml->namespace_uri());
182 _xml_id = remove_urn_uuid(xml->string_child("Id"));
183 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
185 _content_title_text = xml->string_child ("ContentTitleText");
186 _annotation_text = xml->optional_string_child ("AnnotationText");
187 _issue_date = LocalTime (xml->string_child ("IssueDate"));
188 _reel_number = xml->optional_number_child<int> ("ReelNumber");
189 _language = xml->optional_string_child ("Language");
191 /* This is supposed to be two numbers, but a single number has been seen in the wild */
192 auto const er = xml->string_child ("EditRate");
193 vector<string> er_parts;
194 split (er_parts, er, is_any_of (" "));
195 if (er_parts.size() == 1) {
196 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
197 } else if (er_parts.size() == 2) {
198 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
200 throw XMLError ("malformed EditRate " + er);
203 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
204 if (xml->optional_string_child ("StartTime")) {
205 _start_time = Time (xml->string_child("StartTime"), _time_code_rate);
208 /* Now we need to drop down to xmlpp */
210 vector<ParseState> ps;
211 for (auto i: xml->node()->get_children()) {
212 auto const e = dynamic_cast<xmlpp::Element const *>(i);
213 if (e && e->get_name() == "SubtitleList") {
214 parse_subtitles (e, ps, _time_code_rate, Standard::SMPTE);
218 /* Guess intrinsic duration */
219 _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);
224 SMPTESubtitleAsset::read_mxf_resources (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
226 ASDCP::TimedText::TimedTextDescriptor descriptor;
227 reader->FillTimedTextDescriptor (descriptor);
229 /* Load fonts and images */
232 auto i = descriptor.ResourceList.begin();
233 i != descriptor.ResourceList.end();
236 ASDCP::TimedText::FrameBuffer buffer;
237 buffer.Capacity(32 * 1024 * 1024);
238 auto const result = reader->ReadAncillaryResource(i->ResourceID, buffer, dec->context(), dec->hmac());
239 if (ASDCP_FAILURE(result)) {
241 case ASDCP::TimedText::MT_OPENTYPE:
242 throw ReadError(String::compose("Could not read font from MXF file (%1)", static_cast<int>(result)));
243 case ASDCP::TimedText::MT_PNG:
244 throw ReadError(String::compose("Could not read subtitle image from MXF file (%1)", static_cast<int>(result)));
246 throw ReadError(String::compose("Could not read resource from MXF file (%1)", static_cast<int>(result)));
251 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof(id));
254 case ASDCP::TimedText::MT_OPENTYPE:
256 auto j = _load_font_nodes.begin();
257 while (j != _load_font_nodes.end() && (*j)->urn != id) {
261 if (j != _load_font_nodes.end ()) {
262 _fonts.push_back(Font((*j)->id, (*j)->urn, ArrayData(buffer.RoData(), buffer.Size())));
266 case ASDCP::TimedText::MT_PNG:
268 auto j = _subtitles.begin();
269 while (j != _subtitles.end() && ((!dynamic_pointer_cast<SubtitleImage>(*j)) || dynamic_pointer_cast<SubtitleImage>(*j)->id() != id)) {
273 if (j != _subtitles.end()) {
274 dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image(ArrayData(buffer.RoData(), buffer.Size()));
286 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader)
288 ASDCP::TimedText::TimedTextDescriptor descriptor;
289 reader->FillTimedTextDescriptor (descriptor);
291 _intrinsic_duration = descriptor.ContainerDuration;
292 /* The thing which is called AssetID in the descriptor is also known as the
293 * ResourceID of the MXF. We store that, at present just for verification
297 Kumu::bin2UUIDhex (descriptor.AssetID, ASDCP::UUIDlen, id, sizeof(id));
303 SMPTESubtitleAsset::set_key (Key key)
305 /* See if we already have a key; if we do, and we have a file, we'll already
308 auto const had_key = static_cast<bool>(_key);
309 auto const had_key_id = static_cast<bool>(_key_id);
313 if (!had_key_id || !_file || had_key) {
314 /* Either we don't have any data to read, it wasn't
315 encrypted, or we've already read it, so we don't
316 need to do anything else.
321 /* Our data was encrypted; now we can decrypt it */
323 auto reader = make_shared<ASDCP::TimedText::MXFReader>();
324 auto r = reader->OpenRead(dcp::filesystem::fix_long_path(*_file).string().c_str());
325 if (ASDCP_FAILURE (r)) {
326 boost::throw_exception (
328 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
333 auto dec = make_shared<DecryptionContext>(key, Standard::SMPTE);
335 reader->ReadTimedTextResource (xml_string, dec->context(), dec->hmac());
336 _raw_xml = xml_string;
337 auto xml = make_shared<cxml::Document>("SubtitleReel");
338 xml->read_string (xml_string);
340 read_mxf_descriptor(reader);
341 read_mxf_resources (reader, dec);
345 vector<shared_ptr<LoadFontNode>>
346 SMPTESubtitleAsset::load_font_nodes () const
348 vector<shared_ptr<LoadFontNode>> lf;
349 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter(lf));
355 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
357 ASDCP::TimedText::MXFReader reader;
358 Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
359 auto r = reader.OpenRead(dcp::filesystem::fix_long_path(file).string().c_str());
360 Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
361 return !ASDCP_FAILURE (r);
366 SMPTESubtitleAsset::xml_as_string () const
369 auto root = doc.create_root_node ("SubtitleReel");
371 DCP_ASSERT (_xml_id);
372 root->add_child("Id")->add_child_text("urn:uuid:" + *_xml_id);
373 root->add_child("ContentTitleText")->add_child_text(_content_title_text);
374 if (_annotation_text) {
375 root->add_child("AnnotationText")->add_child_text(_annotation_text.get());
377 root->add_child("IssueDate")->add_child_text(_issue_date.as_string(false, false));
379 root->add_child("ReelNumber")->add_child_text(raw_convert<string>(_reel_number.get()));
382 root->add_child("Language")->add_child_text(_language.get());
384 root->add_child("EditRate")->add_child_text(_edit_rate.as_string());
385 root->add_child("TimeCodeRate")->add_child_text(raw_convert<string>(_time_code_rate));
387 root->add_child("StartTime")->add_child_text(_start_time.get().as_string(Standard::SMPTE));
390 for (auto i: _load_font_nodes) {
391 auto load_font = root->add_child("LoadFont");
392 load_font->add_child_text ("urn:uuid:" + i->urn);
393 load_font->set_attribute ("ID", i->id);
396 subtitles_as_xml (root->add_child("SubtitleList"), _time_code_rate, Standard::SMPTE);
398 return format_xml(doc, std::make_pair(string{}, schema_namespace()));
403 SMPTESubtitleAsset::write (boost::filesystem::path p) const
405 EncryptionContext enc (key(), Standard::SMPTE);
407 ASDCP::WriterInfo writer_info;
408 fill_writer_info (&writer_info, _id);
410 ASDCP::TimedText::TimedTextDescriptor descriptor;
411 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
412 descriptor.EncodingName = "UTF-8";
414 /* Font references */
416 for (auto 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::TimedTextResourceDescriptor res;
424 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
425 DCP_ASSERT (c == Kumu::UUID_Length);
426 res.Type = ASDCP::TimedText::MT_OPENTYPE;
427 descriptor.ResourceList.push_back (res);
431 /* Image subtitle references */
433 for (auto i: _subtitles) {
434 auto si = dynamic_pointer_cast<SubtitleImage>(i);
436 ASDCP::TimedText::TimedTextResourceDescriptor res;
438 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
439 DCP_ASSERT (c == Kumu::UUID_Length);
440 res.Type = ASDCP::TimedText::MT_PNG;
441 descriptor.ResourceList.push_back (res);
445 descriptor.NamespaceName = schema_namespace();
447 DCP_ASSERT (_xml_id);
448 Kumu::hex2bin (_xml_id->c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
449 DCP_ASSERT (c == Kumu::UUID_Length);
450 descriptor.ContainerDuration = _intrinsic_duration;
452 ASDCP::TimedText::MXFWriter writer;
453 /* This header size is a guess. Empirically it seems that each subtitle reference is 90 bytes, and we need some extra.
454 The default size is not enough for some feature-length PNG sub projects (see DCP-o-matic #1561).
456 ASDCP::Result_t r = writer.OpenWrite(dcp::filesystem::fix_long_path(p).string().c_str(), writer_info, descriptor, _subtitles.size() * 90 + 16384);
457 if (ASDCP_FAILURE (r)) {
458 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
461 _raw_xml = xml_as_string ();
463 r = writer.WriteTimedTextResource (*_raw_xml, enc.context(), enc.hmac());
464 if (ASDCP_FAILURE (r)) {
465 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
470 for (auto i: _load_font_nodes) {
471 auto j = _fonts.begin();
472 while (j != _fonts.end() && j->load_id != i->id) {
475 if (j != _fonts.end ()) {
476 ASDCP::TimedText::FrameBuffer buffer;
477 ArrayData data_copy(j->data);
478 buffer.SetData (data_copy.data(), data_copy.size());
479 buffer.Size (j->data.size());
480 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
481 if (ASDCP_FAILURE(r)) {
482 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
487 /* Image subtitle payload */
489 for (auto i: _subtitles) {
490 auto si = dynamic_pointer_cast<SubtitleImage>(i);
492 ASDCP::TimedText::FrameBuffer buffer;
493 buffer.SetData (si->png_image().data(), si->png_image().size());
494 buffer.Size (si->png_image().size());
495 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
496 if (ASDCP_FAILURE(r)) {
497 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
508 SMPTESubtitleAsset::equals(shared_ptr<const Asset> other_asset, EqualityOptions const& options, NoteHandler note) const
510 if (!SubtitleAsset::equals (other_asset, options, note)) {
514 auto other = dynamic_pointer_cast<const SMPTESubtitleAsset>(other_asset);
516 note (NoteType::ERROR, "Subtitles are in different standards");
520 auto i = _load_font_nodes.begin();
521 auto j = other->_load_font_nodes.begin();
523 while (i != _load_font_nodes.end ()) {
524 if (j == other->_load_font_nodes.end ()) {
525 note (NoteType::ERROR, "<LoadFont> nodes differ");
529 if ((*i)->id != (*j)->id) {
530 note (NoteType::ERROR, "<LoadFont> nodes differ");
538 if (_content_title_text != other->_content_title_text) {
539 note (NoteType::ERROR, "Subtitle content title texts differ");
543 if (_language != other->_language) {
544 note (NoteType::ERROR, String::compose("Subtitle languages differ (`%1' vs `%2')", _language.get_value_or("[none]"), other->_language.get_value_or("[none]")));
548 if (_annotation_text != other->_annotation_text) {
549 note (NoteType::ERROR, "Subtitle annotation texts differ");
553 if (_issue_date != other->_issue_date) {
554 if (options.issue_dates_can_differ) {
555 note (NoteType::NOTE, "Subtitle issue dates differ");
557 note (NoteType::ERROR, "Subtitle issue dates differ");
562 if (_reel_number != other->_reel_number) {
563 note (NoteType::ERROR, "Subtitle reel numbers differ");
567 if (_edit_rate != other->_edit_rate) {
568 note (NoteType::ERROR, "Subtitle edit rates differ");
572 if (_time_code_rate != other->_time_code_rate) {
573 note (NoteType::ERROR, "Subtitle time code rates differ");
577 if (_start_time != other->_start_time) {
578 note (NoteType::ERROR, "Subtitle start times differ");
587 SMPTESubtitleAsset::add_font (string load_id, dcp::ArrayData data)
589 string const uuid = make_uuid ();
590 _fonts.push_back (Font(load_id, uuid, data));
591 _load_font_nodes.push_back (make_shared<SMPTELoadFontNode>(load_id, uuid));
596 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
598 SubtitleAsset::add (s);
599 _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);
604 SMPTESubtitleAsset::schema_namespace() const
606 switch (_subtitle_standard) {
607 case SubtitleStandard::SMPTE_2007:
608 return subtitle_smpte_ns_2007;
609 case SubtitleStandard::SMPTE_2010:
610 return subtitle_smpte_ns_2010;
611 case SubtitleStandard::SMPTE_2014:
612 return subtitle_smpte_ns_2014;