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 = "http://www.smpte-ra.org/schemas/428-7/2010/DCST";
78 SMPTESubtitleAsset::SMPTESubtitleAsset ()
79 : MXF (Standard::SMPTE)
81 , _time_code_rate (24)
82 , _xml_id (make_uuid())
88 SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file)
89 : SubtitleAsset (file)
91 auto xml = make_shared<cxml::Document>("SubtitleReel");
93 auto reader = make_shared<ASDCP::TimedText::MXFReader>();
94 auto r = Kumu::RESULT_OK;
96 ASDCPErrorSuspender sus;
97 r = reader->OpenRead (_file->string().c_str ());
99 if (!ASDCP_FAILURE(r)) {
101 ASDCP::WriterInfo info;
102 reader->FillWriterInfo (info);
103 _id = read_writer_info (info);
105 /* Not encrypted; read it in now */
107 reader->ReadTimedTextResource (xml_string);
108 _raw_xml = xml_string;
109 xml->read_string (xml_string);
111 read_mxf_descriptor (reader);
112 read_mxf_resources (reader, make_shared<DecryptionContext>(optional<Key>(), Standard::SMPTE));
114 read_mxf_descriptor (reader);
119 _raw_xml = dcp::file_to_string (file);
120 xml = make_shared<cxml::Document>("SubtitleReel");
121 xml->read_file (file);
123 } catch (cxml::Error& e) {
124 boost::throw_exception (
127 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
128 file, static_cast<int>(r), e.what()
134 /* Try to read PNG files from the same folder that the XML is in; the wisdom of this is
135 debatable, at best...
137 for (auto i: _subtitles) {
138 auto im = dynamic_pointer_cast<SubtitleImage>(i);
139 if (im && im->png_image().size() == 0) {
140 /* Even more dubious; allow <id>.png or urn:uuid:<id>.png */
141 auto p = file.parent_path() / String::compose("%1.png", im->id());
142 if (boost::filesystem::is_regular_file(p)) {
143 im->read_png_file (p);
144 } else if (starts_with (im->id(), "urn:uuid:")) {
145 p = file.parent_path() / String::compose("%1.png", remove_urn_uuid(im->id()));
146 if (boost::filesystem::is_regular_file(p)) {
147 im->read_png_file (p);
152 _standard = Standard::SMPTE;
155 /* Check that all required image data have been found */
156 for (auto i: _subtitles) {
157 auto im = dynamic_pointer_cast<SubtitleImage>(i);
158 if (im && im->png_image().size() == 0) {
159 throw MissingSubtitleImageError (im->id());
166 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
168 _xml_id = remove_urn_uuid(xml->string_child("Id"));
169 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
171 _content_title_text = xml->string_child ("ContentTitleText");
172 _annotation_text = xml->optional_string_child ("AnnotationText");
173 _issue_date = LocalTime (xml->string_child ("IssueDate"));
174 _reel_number = xml->optional_number_child<int> ("ReelNumber");
175 _language = xml->optional_string_child ("Language");
177 /* This is supposed to be two numbers, but a single number has been seen in the wild */
178 auto const er = xml->string_child ("EditRate");
179 vector<string> er_parts;
180 split (er_parts, er, is_any_of (" "));
181 if (er_parts.size() == 1) {
182 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
183 } else if (er_parts.size() == 2) {
184 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
186 throw XMLError ("malformed EditRate " + er);
189 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
190 if (xml->optional_string_child ("StartTime")) {
191 _start_time = Time (xml->string_child("StartTime"), _time_code_rate);
194 /* Now we need to drop down to xmlpp */
196 vector<ParseState> ps;
197 for (auto i: xml->node()->get_children()) {
198 auto const e = dynamic_cast<xmlpp::Element const *>(i);
199 if (e && e->get_name() == "SubtitleList") {
200 parse_subtitles (e, ps, _time_code_rate, Standard::SMPTE);
204 /* Guess intrinsic duration */
205 _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);
210 SMPTESubtitleAsset::read_mxf_resources (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
212 ASDCP::TimedText::TimedTextDescriptor descriptor;
213 reader->FillTimedTextDescriptor (descriptor);
215 /* Load fonts and images */
218 auto i = descriptor.ResourceList.begin();
219 i != descriptor.ResourceList.end();
222 ASDCP::TimedText::FrameBuffer buffer;
223 buffer.Capacity (10 * 1024 * 1024);
224 reader->ReadAncillaryResource (i->ResourceID, buffer, dec->context(), dec->hmac());
227 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof(id));
229 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
230 memcpy (data.get(), buffer.RoData(), buffer.Size());
233 case ASDCP::TimedText::MT_OPENTYPE:
235 auto j = _load_font_nodes.begin();
236 while (j != _load_font_nodes.end() && (*j)->urn != id) {
240 if (j != _load_font_nodes.end ()) {
241 _fonts.push_back (Font ((*j)->id, (*j)->urn, ArrayData (data, buffer.Size ())));
245 case ASDCP::TimedText::MT_PNG:
247 auto j = _subtitles.begin();
248 while (j != _subtitles.end() && ((!dynamic_pointer_cast<SubtitleImage>(*j)) || dynamic_pointer_cast<SubtitleImage>(*j)->id() != id)) {
252 if (j != _subtitles.end()) {
253 dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image (ArrayData(data, buffer.Size()));
265 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader)
267 ASDCP::TimedText::TimedTextDescriptor descriptor;
268 reader->FillTimedTextDescriptor (descriptor);
270 _intrinsic_duration = descriptor.ContainerDuration;
271 /* The thing which is called AssetID in the descriptor is also known as the
272 * ResourceID of the MXF. We store that, at present just for verification
276 Kumu::bin2UUIDhex (descriptor.AssetID, ASDCP::UUIDlen, id, sizeof(id));
282 SMPTESubtitleAsset::set_key (Key key)
284 /* See if we already have a key; if we do, and we have a file, we'll already
287 auto const had_key = static_cast<bool>(_key);
291 if (!_key_id || !_file || had_key) {
292 /* Either we don't have any data to read, it wasn't
293 encrypted, or we've already read it, so we don't
294 need to do anything else.
299 /* Our data was encrypted; now we can decrypt it */
301 auto reader = make_shared<ASDCP::TimedText::MXFReader>();
302 auto r = reader->OpenRead (_file->string().c_str ());
303 if (ASDCP_FAILURE (r)) {
304 boost::throw_exception (
306 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
311 auto dec = make_shared<DecryptionContext>(key, Standard::SMPTE);
313 reader->ReadTimedTextResource (xml_string, dec->context(), dec->hmac());
314 _raw_xml = xml_string;
315 auto xml = make_shared<cxml::Document>("SubtitleReel");
316 xml->read_string (xml_string);
318 read_mxf_resources (reader, dec);
322 vector<shared_ptr<LoadFontNode>>
323 SMPTESubtitleAsset::load_font_nodes () const
325 vector<shared_ptr<LoadFontNode>> lf;
326 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter(lf));
332 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
334 ASDCP::TimedText::MXFReader reader;
335 Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
336 auto r = reader.OpenRead (file.string().c_str ());
337 Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
338 return !ASDCP_FAILURE (r);
343 SMPTESubtitleAsset::xml_as_string () const
346 auto root = doc.create_root_node ("dcst:SubtitleReel");
347 root->set_namespace_declaration (subtitle_smpte_ns, "dcst");
348 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
350 DCP_ASSERT (_xml_id);
351 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + *_xml_id);
352 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
353 if (_annotation_text) {
354 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
356 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
358 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
361 root->add_child("Language", "dcst")->add_child_text (_language.get ());
363 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
364 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
366 root->add_child("StartTime", "dcst")->add_child_text(_start_time.get().as_string(Standard::SMPTE));
369 for (auto i: _load_font_nodes) {
370 auto load_font = root->add_child("LoadFont", "dcst");
371 load_font->add_child_text ("urn:uuid:" + i->urn);
372 load_font->set_attribute ("ID", i->id);
375 subtitles_as_xml (root->add_child("SubtitleList", "dcst"), _time_code_rate, Standard::SMPTE);
377 return doc.write_to_string ("UTF-8");
382 SMPTESubtitleAsset::write (boost::filesystem::path p) const
384 EncryptionContext enc (key(), Standard::SMPTE);
386 ASDCP::WriterInfo writer_info;
387 fill_writer_info (&writer_info, _id);
389 ASDCP::TimedText::TimedTextDescriptor descriptor;
390 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
391 descriptor.EncodingName = "UTF-8";
393 /* Font references */
395 for (auto i: _load_font_nodes) {
396 auto j = _fonts.begin();
397 while (j != _fonts.end() && j->load_id != i->id) {
400 if (j != _fonts.end ()) {
401 ASDCP::TimedText::TimedTextResourceDescriptor res;
403 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
404 DCP_ASSERT (c == Kumu::UUID_Length);
405 res.Type = ASDCP::TimedText::MT_OPENTYPE;
406 descriptor.ResourceList.push_back (res);
410 /* Image subtitle references */
412 for (auto i: _subtitles) {
413 auto si = dynamic_pointer_cast<SubtitleImage>(i);
415 ASDCP::TimedText::TimedTextResourceDescriptor res;
417 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
418 DCP_ASSERT (c == Kumu::UUID_Length);
419 res.Type = ASDCP::TimedText::MT_PNG;
420 descriptor.ResourceList.push_back (res);
424 descriptor.NamespaceName = subtitle_smpte_ns;
426 DCP_ASSERT (_xml_id);
427 Kumu::hex2bin (_xml_id->c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
428 DCP_ASSERT (c == Kumu::UUID_Length);
429 descriptor.ContainerDuration = _intrinsic_duration;
431 ASDCP::TimedText::MXFWriter writer;
432 /* This header size is a guess. Empirically it seems that each subtitle reference is 90 bytes, and we need some extra.
433 The default size is not enough for some feature-length PNG sub projects (see DCP-o-matic #1561).
435 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor, _subtitles.size() * 90 + 16384);
436 if (ASDCP_FAILURE (r)) {
437 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
440 _raw_xml = xml_as_string ();
442 r = writer.WriteTimedTextResource (*_raw_xml, enc.context(), enc.hmac());
443 if (ASDCP_FAILURE (r)) {
444 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
449 for (auto i: _load_font_nodes) {
450 auto j = _fonts.begin();
451 while (j != _fonts.end() && j->load_id != i->id) {
454 if (j != _fonts.end ()) {
455 ASDCP::TimedText::FrameBuffer buffer;
456 ArrayData data_copy(j->data);
457 buffer.SetData (data_copy.data(), data_copy.size());
458 buffer.Size (j->data.size());
459 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
460 if (ASDCP_FAILURE(r)) {
461 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
466 /* Image subtitle payload */
468 for (auto i: _subtitles) {
469 auto si = dynamic_pointer_cast<SubtitleImage>(i);
471 ASDCP::TimedText::FrameBuffer buffer;
472 buffer.SetData (si->png_image().data(), si->png_image().size());
473 buffer.Size (si->png_image().size());
474 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
475 if (ASDCP_FAILURE(r)) {
476 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
487 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
489 if (!SubtitleAsset::equals (other_asset, options, note)) {
493 auto other = dynamic_pointer_cast<const SMPTESubtitleAsset>(other_asset);
495 note (NoteType::ERROR, "Subtitles are in different standards");
499 auto i = _load_font_nodes.begin();
500 auto j = other->_load_font_nodes.begin();
502 while (i != _load_font_nodes.end ()) {
503 if (j == other->_load_font_nodes.end ()) {
504 note (NoteType::ERROR, "<LoadFont> nodes differ");
508 if ((*i)->id != (*j)->id) {
509 note (NoteType::ERROR, "<LoadFont> nodes differ");
517 if (_content_title_text != other->_content_title_text) {
518 note (NoteType::ERROR, "Subtitle content title texts differ");
522 if (_language != other->_language) {
523 note (NoteType::ERROR, String::compose("Subtitle languages differ (`%1' vs `%2')", _language.get_value_or("[none]"), other->_language.get_value_or("[none]")));
527 if (_annotation_text != other->_annotation_text) {
528 note (NoteType::ERROR, "Subtitle annotation texts differ");
532 if (_issue_date != other->_issue_date) {
533 if (options.issue_dates_can_differ) {
534 note (NoteType::NOTE, "Subtitle issue dates differ");
536 note (NoteType::ERROR, "Subtitle issue dates differ");
541 if (_reel_number != other->_reel_number) {
542 note (NoteType::ERROR, "Subtitle reel numbers differ");
546 if (_edit_rate != other->_edit_rate) {
547 note (NoteType::ERROR, "Subtitle edit rates differ");
551 if (_time_code_rate != other->_time_code_rate) {
552 note (NoteType::ERROR, "Subtitle time code rates differ");
556 if (_start_time != other->_start_time) {
557 note (NoteType::ERROR, "Subtitle start times differ");
566 SMPTESubtitleAsset::add_font (string load_id, dcp::ArrayData data)
568 string const uuid = make_uuid ();
569 _fonts.push_back (Font(load_id, uuid, data));
570 _load_font_nodes.push_back (make_shared<SMPTELoadFontNode>(load_id, uuid));
575 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
577 SubtitleAsset::add (s);
578 _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);