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 "raw_convert.h"
46 #include "smpte_load_font_node.h"
47 #include "smpte_subtitle_asset.h"
48 #include "subtitle_image.h"
52 LIBDCP_DISABLE_WARNINGS
53 #include <asdcp/AS_DCP.h>
54 #include <asdcp/KM_util.h>
55 #include <asdcp/KM_log.h>
56 #include <libxml++/libxml++.h>
57 LIBDCP_ENABLE_WARNINGS
58 #include <boost/algorithm/string.hpp>
65 using std::shared_ptr;
66 using std::dynamic_pointer_cast;
67 using std::make_shared;
69 using boost::is_any_of;
70 using boost::shared_array;
71 using boost::optional;
72 using boost::starts_with;
76 static string const subtitle_smpte_ns_2007 = "http://www.smpte-ra.org/schemas/428-7/2007/DCST";
77 static string const subtitle_smpte_ns_2010 = "http://www.smpte-ra.org/schemas/428-7/2010/DCST";
78 static string const subtitle_smpte_ns_2014 = "http://www.smpte-ra.org/schemas/428-7/2014/DCST";
81 SMPTESubtitleAsset::SMPTESubtitleAsset(SubtitleStandard standard)
82 : MXF(Standard::SMPTE)
84 , _time_code_rate (24)
85 , _subtitle_standard(standard)
86 , _xml_id (make_uuid())
92 SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file)
93 : SubtitleAsset (file)
95 auto xml = make_shared<cxml::Document>("SubtitleReel");
97 auto reader = make_shared<ASDCP::TimedText::MXFReader>();
98 auto r = Kumu::RESULT_OK;
100 ASDCPErrorSuspender sus;
101 r = reader->OpenRead (_file->string().c_str ());
103 if (!ASDCP_FAILURE(r)) {
105 ASDCP::WriterInfo info;
106 reader->FillWriterInfo (info);
107 _id = read_writer_info (info);
109 /* Not encrypted; read it in now */
111 reader->ReadTimedTextResource (xml_string);
112 _raw_xml = xml_string;
113 xml->read_string (xml_string);
115 read_mxf_descriptor (reader);
116 read_mxf_resources(reader, std::make_shared<DecryptionContext>(optional<Key>(), Standard::SMPTE));
118 read_mxf_descriptor (reader);
123 _raw_xml = dcp::file_to_string (file);
124 xml = make_shared<cxml::Document>("SubtitleReel");
125 xml->read_file (file);
127 } catch (cxml::Error& e) {
128 boost::throw_exception (
131 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
132 file, static_cast<int>(r), e.what()
138 /* Try to read PNG files from the same folder that the XML is in; the wisdom of this is
139 debatable, at best...
141 for (auto i: _subtitles) {
142 auto im = dynamic_pointer_cast<SubtitleImage>(i);
143 if (im && im->png_image().size() == 0) {
144 /* Even more dubious; allow <id>.png or urn:uuid:<id>.png */
145 auto p = file.parent_path() / String::compose("%1.png", im->id());
146 if (boost::filesystem::is_regular_file(p)) {
147 im->read_png_file (p);
148 } else if (starts_with (im->id(), "urn:uuid:")) {
149 p = file.parent_path() / String::compose("%1.png", remove_urn_uuid(im->id()));
150 if (boost::filesystem::is_regular_file(p)) {
151 im->read_png_file (p);
156 _standard = Standard::SMPTE;
159 /* Check that all required image data have been found */
160 for (auto i: _subtitles) {
161 auto im = dynamic_pointer_cast<SubtitleImage>(i);
162 if (im && im->png_image().size() == 0) {
163 throw MissingSubtitleImageError (im->id());
170 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
172 if (xml->namespace_uri() == subtitle_smpte_ns_2007) {
173 _subtitle_standard = SubtitleStandard::SMPTE_2007;
174 } else if (xml->namespace_uri() == subtitle_smpte_ns_2010) {
175 _subtitle_standard = SubtitleStandard::SMPTE_2010;
176 } else if (xml->namespace_uri() == subtitle_smpte_ns_2014) {
177 _subtitle_standard = SubtitleStandard::SMPTE_2014;
179 throw XMLError("Unrecognised subtitle namespace " + xml->namespace_uri());
181 _xml_id = remove_urn_uuid(xml->string_child("Id"));
182 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
184 _content_title_text = xml->string_child ("ContentTitleText");
185 _annotation_text = xml->optional_string_child ("AnnotationText");
186 _issue_date = LocalTime (xml->string_child ("IssueDate"));
187 _reel_number = xml->optional_number_child<int> ("ReelNumber");
188 _language = xml->optional_string_child ("Language");
190 /* This is supposed to be two numbers, but a single number has been seen in the wild */
191 auto const er = xml->string_child ("EditRate");
192 vector<string> er_parts;
193 split (er_parts, er, is_any_of (" "));
194 if (er_parts.size() == 1) {
195 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
196 } else if (er_parts.size() == 2) {
197 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
199 throw XMLError ("malformed EditRate " + er);
202 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
203 if (xml->optional_string_child ("StartTime")) {
204 _start_time = Time (xml->string_child("StartTime"), _time_code_rate);
207 /* Now we need to drop down to xmlpp */
209 vector<ParseState> ps;
210 for (auto i: xml->node()->get_children()) {
211 auto const e = dynamic_cast<xmlpp::Element const *>(i);
212 if (e && e->get_name() == "SubtitleList") {
213 parse_subtitles (e, ps, _time_code_rate, Standard::SMPTE);
217 /* Guess intrinsic duration */
218 _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);
223 SMPTESubtitleAsset::read_mxf_resources (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
225 ASDCP::TimedText::TimedTextDescriptor descriptor;
226 reader->FillTimedTextDescriptor (descriptor);
228 /* Load fonts and images */
231 auto i = descriptor.ResourceList.begin();
232 i != descriptor.ResourceList.end();
235 ASDCP::TimedText::FrameBuffer buffer;
236 buffer.Capacity(32 * 1024 * 1024);
237 auto const result = reader->ReadAncillaryResource(i->ResourceID, buffer, dec->context(), dec->hmac());
238 if (ASDCP_FAILURE(result)) {
240 case ASDCP::TimedText::MT_OPENTYPE:
241 throw ReadError(String::compose("Could not read font from MXF file (%1)", static_cast<int>(result)));
242 case ASDCP::TimedText::MT_PNG:
243 throw ReadError(String::compose("Could not read subtitle image from MXF file (%1)", static_cast<int>(result)));
245 throw ReadError(String::compose("Could not read resource from MXF file (%1)", static_cast<int>(result)));
250 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof(id));
253 case ASDCP::TimedText::MT_OPENTYPE:
255 auto j = _load_font_nodes.begin();
256 while (j != _load_font_nodes.end() && (*j)->urn != id) {
260 if (j != _load_font_nodes.end ()) {
261 _fonts.push_back(Font((*j)->id, (*j)->urn, ArrayData(buffer.RoData(), buffer.Size())));
265 case ASDCP::TimedText::MT_PNG:
267 auto j = _subtitles.begin();
268 while (j != _subtitles.end() && ((!dynamic_pointer_cast<SubtitleImage>(*j)) || dynamic_pointer_cast<SubtitleImage>(*j)->id() != id)) {
272 if (j != _subtitles.end()) {
273 dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image(ArrayData(buffer.RoData(), buffer.Size()));
285 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader)
287 ASDCP::TimedText::TimedTextDescriptor descriptor;
288 reader->FillTimedTextDescriptor (descriptor);
290 _intrinsic_duration = descriptor.ContainerDuration;
291 /* The thing which is called AssetID in the descriptor is also known as the
292 * ResourceID of the MXF. We store that, at present just for verification
296 Kumu::bin2UUIDhex (descriptor.AssetID, ASDCP::UUIDlen, id, sizeof(id));
302 SMPTESubtitleAsset::set_key (Key key)
304 /* See if we already have a key; if we do, and we have a file, we'll already
307 auto const had_key = static_cast<bool>(_key);
311 if (!_key_id || !_file || had_key) {
312 /* Either we don't have any data to read, it wasn't
313 encrypted, or we've already read it, so we don't
314 need to do anything else.
319 /* Our data was encrypted; now we can decrypt it */
321 auto reader = make_shared<ASDCP::TimedText::MXFReader>();
322 auto r = reader->OpenRead (_file->string().c_str ());
323 if (ASDCP_FAILURE (r)) {
324 boost::throw_exception (
326 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
331 auto dec = make_shared<DecryptionContext>(key, Standard::SMPTE);
333 reader->ReadTimedTextResource (xml_string, dec->context(), dec->hmac());
334 _raw_xml = xml_string;
335 auto xml = make_shared<cxml::Document>("SubtitleReel");
336 xml->read_string (xml_string);
338 read_mxf_resources (reader, dec);
342 vector<shared_ptr<LoadFontNode>>
343 SMPTESubtitleAsset::load_font_nodes () const
345 vector<shared_ptr<LoadFontNode>> lf;
346 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter(lf));
352 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
354 ASDCP::TimedText::MXFReader reader;
355 Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
356 auto r = reader.OpenRead (file.string().c_str ());
357 Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
358 return !ASDCP_FAILURE (r);
363 SMPTESubtitleAsset::xml_as_string () const
366 auto root = doc.create_root_node ("SubtitleReel");
368 DCP_ASSERT (_xml_id);
369 root->add_child("Id")->add_child_text("urn:uuid:" + *_xml_id);
370 root->add_child("ContentTitleText")->add_child_text(_content_title_text);
371 if (_annotation_text) {
372 root->add_child("AnnotationText")->add_child_text(_annotation_text.get());
374 root->add_child("IssueDate")->add_child_text(_issue_date.as_string(false, false));
376 root->add_child("ReelNumber")->add_child_text(raw_convert<string>(_reel_number.get()));
379 root->add_child("Language")->add_child_text(_language.get());
381 root->add_child("EditRate")->add_child_text(_edit_rate.as_string());
382 root->add_child("TimeCodeRate")->add_child_text(raw_convert<string>(_time_code_rate));
384 root->add_child("StartTime")->add_child_text(_start_time.get().as_string(Standard::SMPTE));
387 for (auto i: _load_font_nodes) {
388 auto load_font = root->add_child("LoadFont");
389 load_font->add_child_text ("urn:uuid:" + i->urn);
390 load_font->set_attribute ("ID", i->id);
393 subtitles_as_xml (root->add_child("SubtitleList"), _time_code_rate, Standard::SMPTE);
395 return format_xml(doc, std::make_pair(string{}, schema_namespace()));
400 SMPTESubtitleAsset::write (boost::filesystem::path p) const
402 EncryptionContext enc (key(), Standard::SMPTE);
404 ASDCP::WriterInfo writer_info;
405 fill_writer_info (&writer_info, _id);
407 ASDCP::TimedText::TimedTextDescriptor descriptor;
408 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
409 descriptor.EncodingName = "UTF-8";
411 /* Font references */
413 for (auto i: _load_font_nodes) {
414 auto j = _fonts.begin();
415 while (j != _fonts.end() && j->load_id != i->id) {
418 if (j != _fonts.end ()) {
419 ASDCP::TimedText::TimedTextResourceDescriptor res;
421 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
422 DCP_ASSERT (c == Kumu::UUID_Length);
423 res.Type = ASDCP::TimedText::MT_OPENTYPE;
424 descriptor.ResourceList.push_back (res);
428 /* Image subtitle references */
430 for (auto i: _subtitles) {
431 auto si = dynamic_pointer_cast<SubtitleImage>(i);
433 ASDCP::TimedText::TimedTextResourceDescriptor res;
435 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
436 DCP_ASSERT (c == Kumu::UUID_Length);
437 res.Type = ASDCP::TimedText::MT_PNG;
438 descriptor.ResourceList.push_back (res);
442 descriptor.NamespaceName = schema_namespace();
444 DCP_ASSERT (_xml_id);
445 Kumu::hex2bin (_xml_id->c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
446 DCP_ASSERT (c == Kumu::UUID_Length);
447 descriptor.ContainerDuration = _intrinsic_duration;
449 ASDCP::TimedText::MXFWriter writer;
450 /* This header size is a guess. Empirically it seems that each subtitle reference is 90 bytes, and we need some extra.
451 The default size is not enough for some feature-length PNG sub projects (see DCP-o-matic #1561).
453 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor, _subtitles.size() * 90 + 16384);
454 if (ASDCP_FAILURE (r)) {
455 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
458 _raw_xml = xml_as_string ();
460 r = writer.WriteTimedTextResource (*_raw_xml, enc.context(), enc.hmac());
461 if (ASDCP_FAILURE (r)) {
462 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
467 for (auto i: _load_font_nodes) {
468 auto j = _fonts.begin();
469 while (j != _fonts.end() && j->load_id != i->id) {
472 if (j != _fonts.end ()) {
473 ASDCP::TimedText::FrameBuffer buffer;
474 ArrayData data_copy(j->data);
475 buffer.SetData (data_copy.data(), data_copy.size());
476 buffer.Size (j->data.size());
477 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
478 if (ASDCP_FAILURE(r)) {
479 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
484 /* Image subtitle payload */
486 for (auto i: _subtitles) {
487 auto si = dynamic_pointer_cast<SubtitleImage>(i);
489 ASDCP::TimedText::FrameBuffer buffer;
490 buffer.SetData (si->png_image().data(), si->png_image().size());
491 buffer.Size (si->png_image().size());
492 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
493 if (ASDCP_FAILURE(r)) {
494 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
505 SMPTESubtitleAsset::equals(shared_ptr<const Asset> other_asset, EqualityOptions const& options, NoteHandler note) const
507 if (!SubtitleAsset::equals (other_asset, options, note)) {
511 auto other = dynamic_pointer_cast<const SMPTESubtitleAsset>(other_asset);
513 note (NoteType::ERROR, "Subtitles are in different standards");
517 auto i = _load_font_nodes.begin();
518 auto j = other->_load_font_nodes.begin();
520 while (i != _load_font_nodes.end ()) {
521 if (j == other->_load_font_nodes.end ()) {
522 note (NoteType::ERROR, "<LoadFont> nodes differ");
526 if ((*i)->id != (*j)->id) {
527 note (NoteType::ERROR, "<LoadFont> nodes differ");
535 if (_content_title_text != other->_content_title_text) {
536 note (NoteType::ERROR, "Subtitle content title texts differ");
540 if (_language != other->_language) {
541 note (NoteType::ERROR, String::compose("Subtitle languages differ (`%1' vs `%2')", _language.get_value_or("[none]"), other->_language.get_value_or("[none]")));
545 if (_annotation_text != other->_annotation_text) {
546 note (NoteType::ERROR, "Subtitle annotation texts differ");
550 if (_issue_date != other->_issue_date) {
551 if (options.issue_dates_can_differ) {
552 note (NoteType::NOTE, "Subtitle issue dates differ");
554 note (NoteType::ERROR, "Subtitle issue dates differ");
559 if (_reel_number != other->_reel_number) {
560 note (NoteType::ERROR, "Subtitle reel numbers differ");
564 if (_edit_rate != other->_edit_rate) {
565 note (NoteType::ERROR, "Subtitle edit rates differ");
569 if (_time_code_rate != other->_time_code_rate) {
570 note (NoteType::ERROR, "Subtitle time code rates differ");
574 if (_start_time != other->_start_time) {
575 note (NoteType::ERROR, "Subtitle start times differ");
584 SMPTESubtitleAsset::add_font (string load_id, dcp::ArrayData data)
586 string const uuid = make_uuid ();
587 _fonts.push_back (Font(load_id, uuid, data));
588 _load_font_nodes.push_back (make_shared<SMPTELoadFontNode>(load_id, uuid));
593 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
595 SubtitleAsset::add (s);
596 _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);
601 SMPTESubtitleAsset::schema_namespace() const
603 switch (_subtitle_standard) {
604 case SubtitleStandard::SMPTE_2007:
605 return subtitle_smpte_ns_2007;
606 case SubtitleStandard::SMPTE_2010:
607 return subtitle_smpte_ns_2010;
608 case SubtitleStandard::SMPTE_2014:
609 return subtitle_smpte_ns_2014;