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 Kumu::FileReaderFactory factory;
99 auto reader = make_shared<ASDCP::TimedText::MXFReader>(factory);
100 auto r = Kumu::RESULT_OK;
102 ASDCPErrorSuspender sus;
103 r = reader->OpenRead(dcp::filesystem::fix_long_path(*_file).string().c_str());
105 if (!ASDCP_FAILURE(r)) {
107 ASDCP::WriterInfo info;
108 reader->FillWriterInfo (info);
109 _id = read_writer_info (info);
111 /* Not encrypted; read it in now */
113 reader->ReadTimedTextResource (xml_string);
114 _raw_xml = xml_string;
115 xml->read_string (xml_string);
117 read_mxf_descriptor (reader);
118 read_mxf_resources(reader, std::make_shared<DecryptionContext>(optional<Key>(), Standard::SMPTE));
120 read_mxf_descriptor (reader);
125 _raw_xml = dcp::file_to_string (file);
126 xml = make_shared<cxml::Document>("SubtitleReel");
127 xml->read_file(dcp::filesystem::fix_long_path(file));
129 } catch (cxml::Error& e) {
130 boost::throw_exception (
133 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
134 file, static_cast<int>(r), e.what()
140 /* Try to read PNG files from the same folder that the XML is in; the wisdom of this is
141 debatable, at best...
143 for (auto i: _subtitles) {
144 auto im = dynamic_pointer_cast<SubtitleImage>(i);
145 if (im && im->png_image().size() == 0) {
146 /* Even more dubious; allow <id>.png or urn:uuid:<id>.png */
147 auto p = file.parent_path() / String::compose("%1.png", im->id());
148 if (filesystem::is_regular_file(p)) {
149 im->read_png_file (p);
150 } else if (starts_with (im->id(), "urn:uuid:")) {
151 p = file.parent_path() / String::compose("%1.png", remove_urn_uuid(im->id()));
152 if (filesystem::is_regular_file(p)) {
153 im->read_png_file (p);
158 _standard = Standard::SMPTE;
161 /* Check that all required image data have been found */
162 for (auto i: _subtitles) {
163 auto im = dynamic_pointer_cast<SubtitleImage>(i);
164 if (im && im->png_image().size() == 0) {
165 throw MissingSubtitleImageError (im->id());
172 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
174 if (xml->namespace_uri() == subtitle_smpte_ns_2007) {
175 _subtitle_standard = SubtitleStandard::SMPTE_2007;
176 } else if (xml->namespace_uri() == subtitle_smpte_ns_2010) {
177 _subtitle_standard = SubtitleStandard::SMPTE_2010;
178 } else if (xml->namespace_uri() == subtitle_smpte_ns_2014) {
179 _subtitle_standard = SubtitleStandard::SMPTE_2014;
181 throw XMLError("Unrecognised subtitle namespace " + xml->namespace_uri());
183 _xml_id = remove_urn_uuid(xml->string_child("Id"));
184 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
186 _content_title_text = xml->string_child ("ContentTitleText");
187 _annotation_text = xml->optional_string_child ("AnnotationText");
188 _issue_date = LocalTime (xml->string_child ("IssueDate"));
189 _reel_number = xml->optional_number_child<int> ("ReelNumber");
190 _language = xml->optional_string_child ("Language");
192 /* This is supposed to be two numbers, but a single number has been seen in the wild */
193 auto const er = xml->string_child ("EditRate");
194 vector<string> er_parts;
195 split (er_parts, er, is_any_of (" "));
196 if (er_parts.size() == 1) {
197 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
198 } else if (er_parts.size() == 2) {
199 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
201 throw XMLError ("malformed EditRate " + er);
204 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
205 if (xml->optional_string_child ("StartTime")) {
206 _start_time = Time (xml->string_child("StartTime"), _time_code_rate);
209 /* Now we need to drop down to xmlpp */
211 vector<ParseState> ps;
212 for (auto i: xml->node()->get_children()) {
213 auto const e = dynamic_cast<xmlpp::Element const *>(i);
214 if (e && e->get_name() == "SubtitleList") {
215 parse_subtitles (e, ps, _time_code_rate, Standard::SMPTE);
219 /* Guess intrinsic duration */
220 _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);
225 SMPTESubtitleAsset::read_mxf_resources (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
227 ASDCP::TimedText::TimedTextDescriptor descriptor;
228 reader->FillTimedTextDescriptor (descriptor);
230 /* Load fonts and images */
233 auto i = descriptor.ResourceList.begin();
234 i != descriptor.ResourceList.end();
237 ASDCP::TimedText::FrameBuffer buffer;
238 buffer.Capacity(32 * 1024 * 1024);
239 auto const result = reader->ReadAncillaryResource(i->ResourceID, buffer, dec->context(), dec->hmac());
240 if (ASDCP_FAILURE(result)) {
242 case ASDCP::TimedText::MT_OPENTYPE:
243 throw ReadError(String::compose("Could not read font from MXF file (%1)", static_cast<int>(result)));
244 case ASDCP::TimedText::MT_PNG:
245 throw ReadError(String::compose("Could not read subtitle image from MXF file (%1)", static_cast<int>(result)));
247 throw ReadError(String::compose("Could not read resource from MXF file (%1)", static_cast<int>(result)));
252 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof(id));
255 case ASDCP::TimedText::MT_OPENTYPE:
257 auto j = _load_font_nodes.begin();
258 while (j != _load_font_nodes.end() && (*j)->urn != id) {
262 if (j != _load_font_nodes.end ()) {
263 _fonts.push_back(Font((*j)->id, (*j)->urn, ArrayData(buffer.RoData(), buffer.Size())));
267 case ASDCP::TimedText::MT_PNG:
269 auto j = _subtitles.begin();
270 while (j != _subtitles.end() && ((!dynamic_pointer_cast<SubtitleImage>(*j)) || dynamic_pointer_cast<SubtitleImage>(*j)->id() != id)) {
274 if (j != _subtitles.end()) {
275 dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image(ArrayData(buffer.RoData(), buffer.Size()));
287 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader)
289 ASDCP::TimedText::TimedTextDescriptor descriptor;
290 reader->FillTimedTextDescriptor (descriptor);
292 _intrinsic_duration = descriptor.ContainerDuration;
293 /* The thing which is called AssetID in the descriptor is also known as the
294 * ResourceID of the MXF. We store that, at present just for verification
298 Kumu::bin2UUIDhex (descriptor.AssetID, ASDCP::UUIDlen, id, sizeof(id));
304 SMPTESubtitleAsset::set_key (Key key)
306 /* See if we already have a key; if we do, and we have a file, we'll already
309 auto const had_key = static_cast<bool>(_key);
310 auto const had_key_id = static_cast<bool>(_key_id);
314 if (!had_key_id || !_file || had_key) {
315 /* Either we don't have any data to read, it wasn't
316 encrypted, or we've already read it, so we don't
317 need to do anything else.
322 /* Our data was encrypted; now we can decrypt it */
324 Kumu::FileReaderFactory factory;
325 auto reader = make_shared<ASDCP::TimedText::MXFReader>(factory);
326 auto r = reader->OpenRead(dcp::filesystem::fix_long_path(*_file).string().c_str());
327 if (ASDCP_FAILURE (r)) {
328 boost::throw_exception (
330 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
335 auto dec = make_shared<DecryptionContext>(key, Standard::SMPTE);
337 reader->ReadTimedTextResource (xml_string, dec->context(), dec->hmac());
338 _raw_xml = xml_string;
339 auto xml = make_shared<cxml::Document>("SubtitleReel");
340 xml->read_string (xml_string);
342 read_mxf_descriptor(reader);
343 read_mxf_resources (reader, dec);
347 vector<shared_ptr<LoadFontNode>>
348 SMPTESubtitleAsset::load_font_nodes () const
350 vector<shared_ptr<LoadFontNode>> lf;
351 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter(lf));
357 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
359 Kumu::FileReaderFactory factory;
360 ASDCP::TimedText::MXFReader reader(factory);
361 Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
362 auto r = reader.OpenRead(dcp::filesystem::fix_long_path(file).string().c_str());
363 Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
364 return !ASDCP_FAILURE (r);
369 SMPTESubtitleAsset::xml_as_string () const
372 auto root = doc.create_root_node ("SubtitleReel");
374 DCP_ASSERT (_xml_id);
375 cxml::add_text_child(root, "Id", "urn:uuid:" + *_xml_id);
376 cxml::add_text_child(root, "ContentTitleText", _content_title_text);
377 if (_annotation_text) {
378 cxml::add_text_child(root, "AnnotationText", _annotation_text.get());
380 cxml::add_text_child(root, "IssueDate", _issue_date.as_string(false, false));
382 cxml::add_text_child(root, "ReelNumber", raw_convert<string>(_reel_number.get()));
385 cxml::add_text_child(root, "Language", _language.get());
387 cxml::add_text_child(root, "EditRate", _edit_rate.as_string());
388 cxml::add_text_child(root, "TimeCodeRate", raw_convert<string>(_time_code_rate));
390 cxml::add_text_child(root, "StartTime", _start_time.get().as_string(Standard::SMPTE));
393 for (auto i: _load_font_nodes) {
394 auto load_font = cxml::add_child(root, "LoadFont");
395 load_font->add_child_text ("urn:uuid:" + i->urn);
396 load_font->set_attribute ("ID", i->id);
399 subtitles_as_xml(cxml::add_child(root, "SubtitleList"), _time_code_rate, Standard::SMPTE);
401 return format_xml(doc, std::make_pair(string{}, schema_namespace()));
406 SMPTESubtitleAsset::write (boost::filesystem::path p) const
408 EncryptionContext enc (key(), Standard::SMPTE);
410 ASDCP::WriterInfo writer_info;
411 fill_writer_info (&writer_info, _id);
413 ASDCP::TimedText::TimedTextDescriptor descriptor;
414 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
415 descriptor.EncodingName = "UTF-8";
417 /* Font references */
419 for (auto i: _load_font_nodes) {
420 auto j = _fonts.begin();
421 while (j != _fonts.end() && j->load_id != i->id) {
424 if (j != _fonts.end ()) {
425 ASDCP::TimedText::TimedTextResourceDescriptor res;
427 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
428 DCP_ASSERT (c == Kumu::UUID_Length);
429 res.Type = ASDCP::TimedText::MT_OPENTYPE;
430 descriptor.ResourceList.push_back (res);
434 /* Image subtitle references */
436 for (auto i: _subtitles) {
437 auto si = dynamic_pointer_cast<SubtitleImage>(i);
439 ASDCP::TimedText::TimedTextResourceDescriptor res;
441 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
442 DCP_ASSERT (c == Kumu::UUID_Length);
443 res.Type = ASDCP::TimedText::MT_PNG;
444 descriptor.ResourceList.push_back (res);
448 descriptor.NamespaceName = schema_namespace();
450 DCP_ASSERT (_xml_id);
451 Kumu::hex2bin (_xml_id->c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
452 DCP_ASSERT (c == Kumu::UUID_Length);
453 descriptor.ContainerDuration = _intrinsic_duration;
455 ASDCP::TimedText::MXFWriter writer;
456 /* This header size is a guess. Empirically it seems that each subtitle reference is 90 bytes, and we need some extra.
457 The default size is not enough for some feature-length PNG sub projects (see DCP-o-matic #1561).
459 ASDCP::Result_t r = writer.OpenWrite(dcp::filesystem::fix_long_path(p).string().c_str(), writer_info, descriptor, _subtitles.size() * 90 + 16384);
460 if (ASDCP_FAILURE (r)) {
461 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
464 _raw_xml = xml_as_string ();
466 r = writer.WriteTimedTextResource (*_raw_xml, enc.context(), enc.hmac());
467 if (ASDCP_FAILURE (r)) {
468 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
473 for (auto i: _load_font_nodes) {
474 auto j = _fonts.begin();
475 while (j != _fonts.end() && j->load_id != i->id) {
478 if (j != _fonts.end ()) {
479 ASDCP::TimedText::FrameBuffer buffer;
480 ArrayData data_copy(j->data);
481 buffer.SetData (data_copy.data(), data_copy.size());
482 buffer.Size (j->data.size());
483 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
484 if (ASDCP_FAILURE(r)) {
485 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
490 /* Image subtitle payload */
492 for (auto i: _subtitles) {
493 auto si = dynamic_pointer_cast<SubtitleImage>(i);
495 ASDCP::TimedText::FrameBuffer buffer;
496 buffer.SetData (si->png_image().data(), si->png_image().size());
497 buffer.Size (si->png_image().size());
498 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
499 if (ASDCP_FAILURE(r)) {
500 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
511 SMPTESubtitleAsset::equals(shared_ptr<const Asset> other_asset, EqualityOptions const& options, NoteHandler note) const
513 if (!SubtitleAsset::equals (other_asset, options, note)) {
517 auto other = dynamic_pointer_cast<const SMPTESubtitleAsset>(other_asset);
519 note (NoteType::ERROR, "Subtitles are in different standards");
523 auto i = _load_font_nodes.begin();
524 auto j = other->_load_font_nodes.begin();
526 while (i != _load_font_nodes.end ()) {
527 if (j == other->_load_font_nodes.end ()) {
528 note (NoteType::ERROR, "<LoadFont> nodes differ");
532 if ((*i)->id != (*j)->id) {
533 note (NoteType::ERROR, "<LoadFont> nodes differ");
541 if (_content_title_text != other->_content_title_text) {
542 note (NoteType::ERROR, "Subtitle content title texts differ");
546 if (_language != other->_language) {
547 note (NoteType::ERROR, String::compose("Subtitle languages differ (`%1' vs `%2')", _language.get_value_or("[none]"), other->_language.get_value_or("[none]")));
551 if (_annotation_text != other->_annotation_text) {
552 note (NoteType::ERROR, "Subtitle annotation texts differ");
556 if (_issue_date != other->_issue_date) {
557 if (options.issue_dates_can_differ) {
558 note (NoteType::NOTE, "Subtitle issue dates differ");
560 note (NoteType::ERROR, "Subtitle issue dates differ");
565 if (_reel_number != other->_reel_number) {
566 note (NoteType::ERROR, "Subtitle reel numbers differ");
570 if (_edit_rate != other->_edit_rate) {
571 note (NoteType::ERROR, "Subtitle edit rates differ");
575 if (_time_code_rate != other->_time_code_rate) {
576 note (NoteType::ERROR, "Subtitle time code rates differ");
580 if (_start_time != other->_start_time) {
581 note (NoteType::ERROR, "Subtitle start times differ");
590 SMPTESubtitleAsset::add_font (string load_id, dcp::ArrayData data)
592 string const uuid = make_uuid ();
593 _fonts.push_back (Font(load_id, uuid, data));
594 _load_font_nodes.push_back (make_shared<SMPTELoadFontNode>(load_id, uuid));
599 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
601 SubtitleAsset::add (s);
602 _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);
607 SMPTESubtitleAsset::schema_namespace() const
609 switch (_subtitle_standard) {
610 case SubtitleStandard::SMPTE_2007:
611 return subtitle_smpte_ns_2007;
612 case SubtitleStandard::SMPTE_2010:
613 return subtitle_smpte_ns_2010;
614 case SubtitleStandard::SMPTE_2014:
615 return subtitle_smpte_ns_2014;