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));
242 case ASDCP::TimedText::MT_OPENTYPE:
244 auto j = _load_font_nodes.begin();
245 while (j != _load_font_nodes.end() && (*j)->urn != id) {
249 if (j != _load_font_nodes.end ()) {
250 _fonts.push_back(Font((*j)->id, (*j)->urn, ArrayData(buffer.RoData(), buffer.Size())));
254 case ASDCP::TimedText::MT_PNG:
256 auto j = _subtitles.begin();
257 while (j != _subtitles.end() && ((!dynamic_pointer_cast<SubtitleImage>(*j)) || dynamic_pointer_cast<SubtitleImage>(*j)->id() != id)) {
261 if (j != _subtitles.end()) {
262 dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image(ArrayData(buffer.RoData(), buffer.Size()));
274 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader)
276 ASDCP::TimedText::TimedTextDescriptor descriptor;
277 reader->FillTimedTextDescriptor (descriptor);
279 _intrinsic_duration = descriptor.ContainerDuration;
280 /* The thing which is called AssetID in the descriptor is also known as the
281 * ResourceID of the MXF. We store that, at present just for verification
285 Kumu::bin2UUIDhex (descriptor.AssetID, ASDCP::UUIDlen, id, sizeof(id));
291 SMPTESubtitleAsset::set_key (Key key)
293 /* See if we already have a key; if we do, and we have a file, we'll already
296 auto const had_key = static_cast<bool>(_key);
300 if (!_key_id || !_file || had_key) {
301 /* Either we don't have any data to read, it wasn't
302 encrypted, or we've already read it, so we don't
303 need to do anything else.
308 /* Our data was encrypted; now we can decrypt it */
310 auto reader = make_shared<ASDCP::TimedText::MXFReader>();
311 auto r = reader->OpenRead (_file->string().c_str ());
312 if (ASDCP_FAILURE (r)) {
313 boost::throw_exception (
315 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
320 auto dec = make_shared<DecryptionContext>(key, Standard::SMPTE);
322 reader->ReadTimedTextResource (xml_string, dec->context(), dec->hmac());
323 _raw_xml = xml_string;
324 auto xml = make_shared<cxml::Document>("SubtitleReel");
325 xml->read_string (xml_string);
327 read_mxf_resources (reader, dec);
331 vector<shared_ptr<LoadFontNode>>
332 SMPTESubtitleAsset::load_font_nodes () const
334 vector<shared_ptr<LoadFontNode>> lf;
335 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter(lf));
341 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
343 ASDCP::TimedText::MXFReader reader;
344 Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
345 auto r = reader.OpenRead (file.string().c_str ());
346 Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
347 return !ASDCP_FAILURE (r);
352 SMPTESubtitleAsset::xml_as_string () const
355 auto root = doc.create_root_node ("SubtitleReel");
357 DCP_ASSERT (_xml_id);
358 root->add_child("Id")->add_child_text("urn:uuid:" + *_xml_id);
359 root->add_child("ContentTitleText")->add_child_text(_content_title_text);
360 if (_annotation_text) {
361 root->add_child("AnnotationText")->add_child_text(_annotation_text.get());
363 root->add_child("IssueDate")->add_child_text(_issue_date.as_string(false, false));
365 root->add_child("ReelNumber")->add_child_text(raw_convert<string>(_reel_number.get()));
368 root->add_child("Language")->add_child_text(_language.get());
370 root->add_child("EditRate")->add_child_text(_edit_rate.as_string());
371 root->add_child("TimeCodeRate")->add_child_text(raw_convert<string>(_time_code_rate));
373 root->add_child("StartTime")->add_child_text(_start_time.get().as_string(Standard::SMPTE));
376 for (auto i: _load_font_nodes) {
377 auto load_font = root->add_child("LoadFont");
378 load_font->add_child_text ("urn:uuid:" + i->urn);
379 load_font->set_attribute ("ID", i->id);
382 subtitles_as_xml (root->add_child("SubtitleList"), _time_code_rate, Standard::SMPTE);
384 return format_xml(doc, std::make_pair(string{}, schema_namespace()));
389 SMPTESubtitleAsset::write (boost::filesystem::path p) const
391 EncryptionContext enc (key(), Standard::SMPTE);
393 ASDCP::WriterInfo writer_info;
394 fill_writer_info (&writer_info, _id);
396 ASDCP::TimedText::TimedTextDescriptor descriptor;
397 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
398 descriptor.EncodingName = "UTF-8";
400 /* Font references */
402 for (auto i: _load_font_nodes) {
403 auto j = _fonts.begin();
404 while (j != _fonts.end() && j->load_id != i->id) {
407 if (j != _fonts.end ()) {
408 ASDCP::TimedText::TimedTextResourceDescriptor res;
410 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
411 DCP_ASSERT (c == Kumu::UUID_Length);
412 res.Type = ASDCP::TimedText::MT_OPENTYPE;
413 descriptor.ResourceList.push_back (res);
417 /* Image subtitle references */
419 for (auto i: _subtitles) {
420 auto si = dynamic_pointer_cast<SubtitleImage>(i);
422 ASDCP::TimedText::TimedTextResourceDescriptor res;
424 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
425 DCP_ASSERT (c == Kumu::UUID_Length);
426 res.Type = ASDCP::TimedText::MT_PNG;
427 descriptor.ResourceList.push_back (res);
431 descriptor.NamespaceName = schema_namespace();
433 DCP_ASSERT (_xml_id);
434 Kumu::hex2bin (_xml_id->c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
435 DCP_ASSERT (c == Kumu::UUID_Length);
436 descriptor.ContainerDuration = _intrinsic_duration;
438 ASDCP::TimedText::MXFWriter writer;
439 /* This header size is a guess. Empirically it seems that each subtitle reference is 90 bytes, and we need some extra.
440 The default size is not enough for some feature-length PNG sub projects (see DCP-o-matic #1561).
442 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor, _subtitles.size() * 90 + 16384);
443 if (ASDCP_FAILURE (r)) {
444 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
447 _raw_xml = xml_as_string ();
449 r = writer.WriteTimedTextResource (*_raw_xml, enc.context(), enc.hmac());
450 if (ASDCP_FAILURE (r)) {
451 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
456 for (auto i: _load_font_nodes) {
457 auto j = _fonts.begin();
458 while (j != _fonts.end() && j->load_id != i->id) {
461 if (j != _fonts.end ()) {
462 ASDCP::TimedText::FrameBuffer buffer;
463 ArrayData data_copy(j->data);
464 buffer.SetData (data_copy.data(), data_copy.size());
465 buffer.Size (j->data.size());
466 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
467 if (ASDCP_FAILURE(r)) {
468 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
473 /* Image subtitle payload */
475 for (auto i: _subtitles) {
476 auto si = dynamic_pointer_cast<SubtitleImage>(i);
478 ASDCP::TimedText::FrameBuffer buffer;
479 buffer.SetData (si->png_image().data(), si->png_image().size());
480 buffer.Size (si->png_image().size());
481 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
482 if (ASDCP_FAILURE(r)) {
483 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
494 SMPTESubtitleAsset::equals(shared_ptr<const Asset> other_asset, EqualityOptions const& options, NoteHandler note) const
496 if (!SubtitleAsset::equals (other_asset, options, note)) {
500 auto other = dynamic_pointer_cast<const SMPTESubtitleAsset>(other_asset);
502 note (NoteType::ERROR, "Subtitles are in different standards");
506 auto i = _load_font_nodes.begin();
507 auto j = other->_load_font_nodes.begin();
509 while (i != _load_font_nodes.end ()) {
510 if (j == other->_load_font_nodes.end ()) {
511 note (NoteType::ERROR, "<LoadFont> nodes differ");
515 if ((*i)->id != (*j)->id) {
516 note (NoteType::ERROR, "<LoadFont> nodes differ");
524 if (_content_title_text != other->_content_title_text) {
525 note (NoteType::ERROR, "Subtitle content title texts differ");
529 if (_language != other->_language) {
530 note (NoteType::ERROR, String::compose("Subtitle languages differ (`%1' vs `%2')", _language.get_value_or("[none]"), other->_language.get_value_or("[none]")));
534 if (_annotation_text != other->_annotation_text) {
535 note (NoteType::ERROR, "Subtitle annotation texts differ");
539 if (_issue_date != other->_issue_date) {
540 if (options.issue_dates_can_differ) {
541 note (NoteType::NOTE, "Subtitle issue dates differ");
543 note (NoteType::ERROR, "Subtitle issue dates differ");
548 if (_reel_number != other->_reel_number) {
549 note (NoteType::ERROR, "Subtitle reel numbers differ");
553 if (_edit_rate != other->_edit_rate) {
554 note (NoteType::ERROR, "Subtitle edit rates differ");
558 if (_time_code_rate != other->_time_code_rate) {
559 note (NoteType::ERROR, "Subtitle time code rates differ");
563 if (_start_time != other->_start_time) {
564 note (NoteType::ERROR, "Subtitle start times differ");
573 SMPTESubtitleAsset::add_font (string load_id, dcp::ArrayData data)
575 string const uuid = make_uuid ();
576 _fonts.push_back (Font(load_id, uuid, data));
577 _load_font_nodes.push_back (make_shared<SMPTELoadFontNode>(load_id, uuid));
582 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
584 SubtitleAsset::add (s);
585 _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);
590 SMPTESubtitleAsset::schema_namespace() const
592 switch (_subtitle_standard) {
593 case SubtitleStandard::SMPTE_2007:
594 return subtitle_smpte_ns_2007;
595 case SubtitleStandard::SMPTE_2010:
596 return subtitle_smpte_ns_2010;
597 case SubtitleStandard::SMPTE_2014:
598 return subtitle_smpte_ns_2014;