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 "exceptions.h"
44 #include "raw_convert.h"
45 #include "smpte_load_font_node.h"
46 #include "smpte_subtitle_asset.h"
47 #include "subtitle_image.h"
51 LIBDCP_DISABLE_WARNINGS
52 #include <asdcp/AS_DCP.h>
53 #include <asdcp/KM_util.h>
54 #include <asdcp/KM_log.h>
55 #include <libxml++/libxml++.h>
56 LIBDCP_ENABLE_WARNINGS
57 #include <boost/algorithm/string.hpp>
64 using std::shared_ptr;
65 using std::dynamic_pointer_cast;
66 using std::make_shared;
68 using boost::is_any_of;
69 using boost::shared_array;
70 using boost::optional;
71 using boost::starts_with;
75 static string const subtitle_smpte_ns_2007 = "http://www.smpte-ra.org/schemas/428-7/2007/DCST";
76 static string const subtitle_smpte_ns_2010 = "http://www.smpte-ra.org/schemas/428-7/2010/DCST";
77 static string const subtitle_smpte_ns_2014 = "http://www.smpte-ra.org/schemas/428-7/2014/DCST";
80 SMPTESubtitleAsset::SMPTESubtitleAsset(SubtitleStandard standard)
81 : MXF(Standard::SMPTE)
83 , _time_code_rate (24)
84 , _subtitle_standard(standard)
85 , _xml_id (make_uuid())
91 SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file)
92 : SubtitleAsset (file)
94 auto xml = make_shared<cxml::Document>("SubtitleReel");
96 auto reader = make_shared<ASDCP::TimedText::MXFReader>();
97 auto r = Kumu::RESULT_OK;
99 ASDCPErrorSuspender sus;
100 r = reader->OpenRead (_file->string().c_str ());
102 if (!ASDCP_FAILURE(r)) {
104 ASDCP::WriterInfo info;
105 reader->FillWriterInfo (info);
106 _id = read_writer_info (info);
108 /* Not encrypted; read it in now */
110 reader->ReadTimedTextResource (xml_string);
111 _raw_xml = xml_string;
112 xml->read_string (xml_string);
114 read_mxf_descriptor (reader);
115 read_mxf_resources(reader, std::make_shared<DecryptionContext>(optional<Key>(), Standard::SMPTE));
117 read_mxf_descriptor (reader);
122 _raw_xml = dcp::file_to_string (file);
123 xml = make_shared<cxml::Document>("SubtitleReel");
124 xml->read_file (file);
126 } catch (cxml::Error& e) {
127 boost::throw_exception (
130 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
131 file, static_cast<int>(r), e.what()
137 /* Try to read PNG files from the same folder that the XML is in; the wisdom of this is
138 debatable, at best...
140 for (auto i: _subtitles) {
141 auto im = dynamic_pointer_cast<SubtitleImage>(i);
142 if (im && im->png_image().size() == 0) {
143 /* Even more dubious; allow <id>.png or urn:uuid:<id>.png */
144 auto p = file.parent_path() / String::compose("%1.png", im->id());
145 if (boost::filesystem::is_regular_file(p)) {
146 im->read_png_file (p);
147 } else if (starts_with (im->id(), "urn:uuid:")) {
148 p = file.parent_path() / String::compose("%1.png", remove_urn_uuid(im->id()));
149 if (boost::filesystem::is_regular_file(p)) {
150 im->read_png_file (p);
155 _standard = Standard::SMPTE;
158 /* Check that all required image data have been found */
159 for (auto i: _subtitles) {
160 auto im = dynamic_pointer_cast<SubtitleImage>(i);
161 if (im && im->png_image().size() == 0) {
162 throw MissingSubtitleImageError (im->id());
169 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
171 if (xml->namespace_uri() == subtitle_smpte_ns_2007) {
172 _subtitle_standard = SubtitleStandard::SMPTE_2007;
173 } else if (xml->namespace_uri() == subtitle_smpte_ns_2010) {
174 _subtitle_standard = SubtitleStandard::SMPTE_2010;
175 } else if (xml->namespace_uri() == subtitle_smpte_ns_2014) {
176 _subtitle_standard = SubtitleStandard::SMPTE_2014;
178 throw XMLError("Unrecognised subtitle namespace " + xml->namespace_uri());
180 _xml_id = remove_urn_uuid(xml->string_child("Id"));
181 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
183 _content_title_text = xml->string_child ("ContentTitleText");
184 _annotation_text = xml->optional_string_child ("AnnotationText");
185 _issue_date = LocalTime (xml->string_child ("IssueDate"));
186 _reel_number = xml->optional_number_child<int> ("ReelNumber");
187 _language = xml->optional_string_child ("Language");
189 /* This is supposed to be two numbers, but a single number has been seen in the wild */
190 auto const er = xml->string_child ("EditRate");
191 vector<string> er_parts;
192 split (er_parts, er, is_any_of (" "));
193 if (er_parts.size() == 1) {
194 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
195 } else if (er_parts.size() == 2) {
196 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
198 throw XMLError ("malformed EditRate " + er);
201 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
202 if (xml->optional_string_child ("StartTime")) {
203 _start_time = Time (xml->string_child("StartTime"), _time_code_rate);
206 /* Now we need to drop down to xmlpp */
208 vector<ParseState> ps;
209 for (auto i: xml->node()->get_children()) {
210 auto const e = dynamic_cast<xmlpp::Element const *>(i);
211 if (e && e->get_name() == "SubtitleList") {
212 parse_subtitles (e, ps, _time_code_rate, Standard::SMPTE);
216 /* Guess intrinsic duration */
217 _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);
222 SMPTESubtitleAsset::read_mxf_resources (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
224 ASDCP::TimedText::TimedTextDescriptor descriptor;
225 reader->FillTimedTextDescriptor (descriptor);
227 /* Load fonts and images */
230 auto i = descriptor.ResourceList.begin();
231 i != descriptor.ResourceList.end();
234 ASDCP::TimedText::FrameBuffer buffer;
235 buffer.Capacity (10 * 1024 * 1024);
236 reader->ReadAncillaryResource (i->ResourceID, buffer, dec->context(), dec->hmac());
239 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof(id));
241 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
242 memcpy (data.get(), buffer.RoData(), buffer.Size());
245 case ASDCP::TimedText::MT_OPENTYPE:
247 auto j = _load_font_nodes.begin();
248 while (j != _load_font_nodes.end() && (*j)->urn != id) {
252 if (j != _load_font_nodes.end ()) {
253 _fonts.push_back (Font ((*j)->id, (*j)->urn, ArrayData (data, buffer.Size ())));
257 case ASDCP::TimedText::MT_PNG:
259 auto j = _subtitles.begin();
260 while (j != _subtitles.end() && ((!dynamic_pointer_cast<SubtitleImage>(*j)) || dynamic_pointer_cast<SubtitleImage>(*j)->id() != id)) {
264 if (j != _subtitles.end()) {
265 dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image (ArrayData(data, buffer.Size()));
277 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader)
279 ASDCP::TimedText::TimedTextDescriptor descriptor;
280 reader->FillTimedTextDescriptor (descriptor);
282 _intrinsic_duration = descriptor.ContainerDuration;
283 /* The thing which is called AssetID in the descriptor is also known as the
284 * ResourceID of the MXF. We store that, at present just for verification
288 Kumu::bin2UUIDhex (descriptor.AssetID, ASDCP::UUIDlen, id, sizeof(id));
294 SMPTESubtitleAsset::set_key (Key key)
296 /* See if we already have a key; if we do, and we have a file, we'll already
299 auto const had_key = static_cast<bool>(_key);
303 if (!_key_id || !_file || had_key) {
304 /* Either we don't have any data to read, it wasn't
305 encrypted, or we've already read it, so we don't
306 need to do anything else.
311 /* Our data was encrypted; now we can decrypt it */
313 auto reader = make_shared<ASDCP::TimedText::MXFReader>();
314 auto r = reader->OpenRead (_file->string().c_str ());
315 if (ASDCP_FAILURE (r)) {
316 boost::throw_exception (
318 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
323 auto dec = make_shared<DecryptionContext>(key, Standard::SMPTE);
325 reader->ReadTimedTextResource (xml_string, dec->context(), dec->hmac());
326 _raw_xml = xml_string;
327 auto xml = make_shared<cxml::Document>("SubtitleReel");
328 xml->read_string (xml_string);
330 read_mxf_resources (reader, dec);
334 vector<shared_ptr<LoadFontNode>>
335 SMPTESubtitleAsset::load_font_nodes () const
337 vector<shared_ptr<LoadFontNode>> lf;
338 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter(lf));
344 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
346 ASDCP::TimedText::MXFReader reader;
347 Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
348 auto r = reader.OpenRead (file.string().c_str ());
349 Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
350 return !ASDCP_FAILURE (r);
355 SMPTESubtitleAsset::xml_as_string () const
358 auto root = doc.create_root_node ("SubtitleReel");
360 DCP_ASSERT (_xml_id);
361 root->add_child("Id")->add_child_text("urn:uuid:" + *_xml_id);
362 root->add_child("ContentTitleText")->add_child_text(_content_title_text);
363 if (_annotation_text) {
364 root->add_child("AnnotationText")->add_child_text(_annotation_text.get());
366 root->add_child("IssueDate")->add_child_text(_issue_date.as_string(false, false));
368 root->add_child("ReelNumber")->add_child_text(raw_convert<string>(_reel_number.get()));
371 root->add_child("Language")->add_child_text(_language.get());
373 root->add_child("EditRate")->add_child_text(_edit_rate.as_string());
374 root->add_child("TimeCodeRate")->add_child_text(raw_convert<string>(_time_code_rate));
376 root->add_child("StartTime")->add_child_text(_start_time.get().as_string(Standard::SMPTE));
379 for (auto i: _load_font_nodes) {
380 auto load_font = root->add_child("LoadFont");
381 load_font->add_child_text ("urn:uuid:" + i->urn);
382 load_font->set_attribute ("ID", i->id);
385 subtitles_as_xml (root->add_child("SubtitleList"), _time_code_rate, Standard::SMPTE);
387 return format_xml(doc, { {"", schema_namespace()}, {"xs", "http://www.w3.org/2001/XMLSchema"} });
392 SMPTESubtitleAsset::write (boost::filesystem::path p) const
394 EncryptionContext enc (key(), Standard::SMPTE);
396 ASDCP::WriterInfo writer_info;
397 fill_writer_info (&writer_info, _id);
399 ASDCP::TimedText::TimedTextDescriptor descriptor;
400 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
401 descriptor.EncodingName = "UTF-8";
403 /* Font references */
405 for (auto i: _load_font_nodes) {
406 auto j = _fonts.begin();
407 while (j != _fonts.end() && j->load_id != i->id) {
410 if (j != _fonts.end ()) {
411 ASDCP::TimedText::TimedTextResourceDescriptor res;
413 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
414 DCP_ASSERT (c == Kumu::UUID_Length);
415 res.Type = ASDCP::TimedText::MT_OPENTYPE;
416 descriptor.ResourceList.push_back (res);
420 /* Image subtitle references */
422 for (auto i: _subtitles) {
423 auto si = dynamic_pointer_cast<SubtitleImage>(i);
425 ASDCP::TimedText::TimedTextResourceDescriptor res;
427 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
428 DCP_ASSERT (c == Kumu::UUID_Length);
429 res.Type = ASDCP::TimedText::MT_PNG;
430 descriptor.ResourceList.push_back (res);
434 descriptor.NamespaceName = schema_namespace();
436 DCP_ASSERT (_xml_id);
437 Kumu::hex2bin (_xml_id->c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
438 DCP_ASSERT (c == Kumu::UUID_Length);
439 descriptor.ContainerDuration = _intrinsic_duration;
441 ASDCP::TimedText::MXFWriter writer;
442 /* This header size is a guess. Empirically it seems that each subtitle reference is 90 bytes, and we need some extra.
443 The default size is not enough for some feature-length PNG sub projects (see DCP-o-matic #1561).
445 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor, _subtitles.size() * 90 + 16384);
446 if (ASDCP_FAILURE (r)) {
447 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
450 _raw_xml = xml_as_string ();
452 r = writer.WriteTimedTextResource (*_raw_xml, enc.context(), enc.hmac());
453 if (ASDCP_FAILURE (r)) {
454 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
459 for (auto i: _load_font_nodes) {
460 auto j = _fonts.begin();
461 while (j != _fonts.end() && j->load_id != i->id) {
464 if (j != _fonts.end ()) {
465 ASDCP::TimedText::FrameBuffer buffer;
466 ArrayData data_copy(j->data);
467 buffer.SetData (data_copy.data(), data_copy.size());
468 buffer.Size (j->data.size());
469 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
470 if (ASDCP_FAILURE(r)) {
471 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
476 /* Image subtitle payload */
478 for (auto i: _subtitles) {
479 auto si = dynamic_pointer_cast<SubtitleImage>(i);
481 ASDCP::TimedText::FrameBuffer buffer;
482 buffer.SetData (si->png_image().data(), si->png_image().size());
483 buffer.Size (si->png_image().size());
484 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
485 if (ASDCP_FAILURE(r)) {
486 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
497 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
499 if (!SubtitleAsset::equals (other_asset, options, note)) {
503 auto other = dynamic_pointer_cast<const SMPTESubtitleAsset>(other_asset);
505 note (NoteType::ERROR, "Subtitles are in different standards");
509 auto i = _load_font_nodes.begin();
510 auto j = other->_load_font_nodes.begin();
512 while (i != _load_font_nodes.end ()) {
513 if (j == other->_load_font_nodes.end ()) {
514 note (NoteType::ERROR, "<LoadFont> nodes differ");
518 if ((*i)->id != (*j)->id) {
519 note (NoteType::ERROR, "<LoadFont> nodes differ");
527 if (_content_title_text != other->_content_title_text) {
528 note (NoteType::ERROR, "Subtitle content title texts differ");
532 if (_language != other->_language) {
533 note (NoteType::ERROR, String::compose("Subtitle languages differ (`%1' vs `%2')", _language.get_value_or("[none]"), other->_language.get_value_or("[none]")));
537 if (_annotation_text != other->_annotation_text) {
538 note (NoteType::ERROR, "Subtitle annotation texts differ");
542 if (_issue_date != other->_issue_date) {
543 if (options.issue_dates_can_differ) {
544 note (NoteType::NOTE, "Subtitle issue dates differ");
546 note (NoteType::ERROR, "Subtitle issue dates differ");
551 if (_reel_number != other->_reel_number) {
552 note (NoteType::ERROR, "Subtitle reel numbers differ");
556 if (_edit_rate != other->_edit_rate) {
557 note (NoteType::ERROR, "Subtitle edit rates differ");
561 if (_time_code_rate != other->_time_code_rate) {
562 note (NoteType::ERROR, "Subtitle time code rates differ");
566 if (_start_time != other->_start_time) {
567 note (NoteType::ERROR, "Subtitle start times differ");
576 SMPTESubtitleAsset::add_font (string load_id, dcp::ArrayData data)
578 string const uuid = make_uuid ();
579 _fonts.push_back (Font(load_id, uuid, data));
580 _load_font_nodes.push_back (make_shared<SMPTELoadFontNode>(load_id, uuid));
585 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
587 SubtitleAsset::add (s);
588 _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);
593 SMPTESubtitleAsset::schema_namespace() const
595 switch (_subtitle_standard) {
596 case SubtitleStandard::SMPTE_2007:
597 return subtitle_smpte_ns_2007;
598 case SubtitleStandard::SMPTE_2010:
599 return subtitle_smpte_ns_2010;
600 case SubtitleStandard::SMPTE_2014:
601 return subtitle_smpte_ns_2014;