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 */
106 reader->ReadTimedTextResource (_raw_xml);
107 xml->read_string (_raw_xml);
109 read_mxf_descriptor (reader);
110 read_mxf_resources (reader, make_shared<DecryptionContext>(optional<Key>(), Standard::SMPTE));
112 read_mxf_descriptor (reader);
117 _raw_xml = dcp::file_to_string (file);
118 xml = make_shared<cxml::Document>("SubtitleReel");
119 xml->read_file (file);
121 } catch (cxml::Error& e) {
122 boost::throw_exception (
125 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
126 file, static_cast<int>(r), e.what()
132 /* Try to read PNG files from the same folder that the XML is in; the wisdom of this is
133 debatable, at best...
135 for (auto i: _subtitles) {
136 auto im = dynamic_pointer_cast<SubtitleImage>(i);
137 if (im && im->png_image().size() == 0) {
138 /* Even more dubious; allow <id>.png or urn:uuid:<id>.png */
139 auto p = file.parent_path() / String::compose("%1.png", im->id());
140 if (boost::filesystem::is_regular_file(p)) {
141 im->read_png_file (p);
142 } else if (starts_with (im->id(), "urn:uuid:")) {
143 p = file.parent_path() / String::compose("%1.png", remove_urn_uuid(im->id()));
144 if (boost::filesystem::is_regular_file(p)) {
145 im->read_png_file (p);
150 _standard = Standard::SMPTE;
153 /* Check that all required image data have been found */
154 for (auto i: _subtitles) {
155 auto im = dynamic_pointer_cast<SubtitleImage>(i);
156 if (im && im->png_image().size() == 0) {
157 throw MissingSubtitleImageError (im->id());
164 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
166 _xml_id = remove_urn_uuid(xml->string_child("Id"));
167 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
169 _content_title_text = xml->string_child ("ContentTitleText");
170 _annotation_text = xml->optional_string_child ("AnnotationText");
171 _issue_date = LocalTime (xml->string_child ("IssueDate"));
172 _reel_number = xml->optional_number_child<int> ("ReelNumber");
173 _language = xml->optional_string_child ("Language");
175 /* This is supposed to be two numbers, but a single number has been seen in the wild */
176 auto const er = xml->string_child ("EditRate");
177 vector<string> er_parts;
178 split (er_parts, er, is_any_of (" "));
179 if (er_parts.size() == 1) {
180 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
181 } else if (er_parts.size() == 2) {
182 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
184 throw XMLError ("malformed EditRate " + er);
187 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
188 if (xml->optional_string_child ("StartTime")) {
189 _start_time = Time (xml->string_child("StartTime"), _time_code_rate);
192 /* Now we need to drop down to xmlpp */
194 vector<ParseState> ps;
195 for (auto i: xml->node()->get_children()) {
196 auto const e = dynamic_cast<xmlpp::Element const *>(i);
197 if (e && e->get_name() == "SubtitleList") {
198 parse_subtitles (e, ps, _time_code_rate, Standard::SMPTE);
202 /* Guess intrinsic duration */
203 _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);
208 SMPTESubtitleAsset::read_mxf_resources (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
210 ASDCP::TimedText::TimedTextDescriptor descriptor;
211 reader->FillTimedTextDescriptor (descriptor);
213 /* Load fonts and images */
216 auto i = descriptor.ResourceList.begin();
217 i != descriptor.ResourceList.end();
220 ASDCP::TimedText::FrameBuffer buffer;
221 buffer.Capacity (10 * 1024 * 1024);
222 reader->ReadAncillaryResource (i->ResourceID, buffer, dec->context(), dec->hmac());
225 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof(id));
227 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
228 memcpy (data.get(), buffer.RoData(), buffer.Size());
231 case ASDCP::TimedText::MT_OPENTYPE:
233 auto j = _load_font_nodes.begin();
234 while (j != _load_font_nodes.end() && (*j)->urn != id) {
238 if (j != _load_font_nodes.end ()) {
239 _fonts.push_back (Font ((*j)->id, (*j)->urn, ArrayData (data, buffer.Size ())));
243 case ASDCP::TimedText::MT_PNG:
245 auto j = _subtitles.begin();
246 while (j != _subtitles.end() && ((!dynamic_pointer_cast<SubtitleImage>(*j)) || dynamic_pointer_cast<SubtitleImage>(*j)->id() != id)) {
250 if (j != _subtitles.end()) {
251 dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image (ArrayData(data, buffer.Size()));
263 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader)
265 ASDCP::TimedText::TimedTextDescriptor descriptor;
266 reader->FillTimedTextDescriptor (descriptor);
268 _intrinsic_duration = descriptor.ContainerDuration;
269 /* The thing which is called AssetID in the descriptor is also known as the
270 * ResourceID of the MXF. We store that, at present just for verification
274 Kumu::bin2UUIDhex (descriptor.AssetID, ASDCP::UUIDlen, id, sizeof(id));
280 SMPTESubtitleAsset::set_key (Key key)
282 /* See if we already have a key; if we do, and we have a file, we'll already
285 auto const had_key = static_cast<bool>(_key);
289 if (!_key_id || !_file || had_key) {
290 /* Either we don't have any data to read, it wasn't
291 encrypted, or we've already read it, so we don't
292 need to do anything else.
297 /* Our data was encrypted; now we can decrypt it */
299 auto reader = make_shared<ASDCP::TimedText::MXFReader>();
300 auto r = reader->OpenRead (_file->string().c_str ());
301 if (ASDCP_FAILURE (r)) {
302 boost::throw_exception (
304 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
309 auto dec = make_shared<DecryptionContext>(key, Standard::SMPTE);
310 reader->ReadTimedTextResource (_raw_xml, dec->context(), dec->hmac());
311 auto xml = make_shared<cxml::Document>("SubtitleReel");
312 xml->read_string (_raw_xml);
314 read_mxf_resources (reader, dec);
318 vector<shared_ptr<LoadFontNode>>
319 SMPTESubtitleAsset::load_font_nodes () const
321 vector<shared_ptr<LoadFontNode>> lf;
322 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter(lf));
328 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
330 ASDCP::TimedText::MXFReader reader;
331 Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
332 auto r = reader.OpenRead (file.string().c_str ());
333 Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
334 return !ASDCP_FAILURE (r);
339 SMPTESubtitleAsset::xml_as_string () const
342 auto root = doc.create_root_node ("dcst:SubtitleReel");
343 root->set_namespace_declaration (subtitle_smpte_ns, "dcst");
344 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
346 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _xml_id);
347 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
348 if (_annotation_text) {
349 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
351 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
353 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
356 root->add_child("Language", "dcst")->add_child_text (_language.get ());
358 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
359 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
361 root->add_child("StartTime", "dcst")->add_child_text(_start_time.get().as_string(Standard::SMPTE));
364 for (auto i: _load_font_nodes) {
365 auto load_font = root->add_child("LoadFont", "dcst");
366 load_font->add_child_text ("urn:uuid:" + i->urn);
367 load_font->set_attribute ("ID", i->id);
370 subtitles_as_xml (root->add_child("SubtitleList", "dcst"), _time_code_rate, Standard::SMPTE);
372 return doc.write_to_string ("UTF-8");
377 SMPTESubtitleAsset::write (boost::filesystem::path p) const
379 EncryptionContext enc (key(), Standard::SMPTE);
381 ASDCP::WriterInfo writer_info;
382 fill_writer_info (&writer_info, _id);
384 ASDCP::TimedText::TimedTextDescriptor descriptor;
385 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
386 descriptor.EncodingName = "UTF-8";
388 /* Font references */
390 for (auto i: _load_font_nodes) {
391 auto j = _fonts.begin();
392 while (j != _fonts.end() && j->load_id != i->id) {
395 if (j != _fonts.end ()) {
396 ASDCP::TimedText::TimedTextResourceDescriptor res;
398 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
399 DCP_ASSERT (c == Kumu::UUID_Length);
400 res.Type = ASDCP::TimedText::MT_OPENTYPE;
401 descriptor.ResourceList.push_back (res);
405 /* Image subtitle references */
407 for (auto i: _subtitles) {
408 auto si = dynamic_pointer_cast<SubtitleImage>(i);
410 ASDCP::TimedText::TimedTextResourceDescriptor res;
412 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
413 DCP_ASSERT (c == Kumu::UUID_Length);
414 res.Type = ASDCP::TimedText::MT_PNG;
415 descriptor.ResourceList.push_back (res);
419 descriptor.NamespaceName = subtitle_smpte_ns;
421 Kumu::hex2bin (_xml_id.c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
422 DCP_ASSERT (c == Kumu::UUID_Length);
423 descriptor.ContainerDuration = _intrinsic_duration;
425 ASDCP::TimedText::MXFWriter writer;
426 /* This header size is a guess. Empirically it seems that each subtitle reference is 90 bytes, and we need some extra.
427 The default size is not enough for some feature-length PNG sub projects (see DCP-o-matic #1561).
429 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor, _subtitles.size() * 90 + 16384);
430 if (ASDCP_FAILURE (r)) {
431 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
434 r = writer.WriteTimedTextResource (xml_as_string (), enc.context(), enc.hmac());
435 if (ASDCP_FAILURE (r)) {
436 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
441 for (auto i: _load_font_nodes) {
442 auto j = _fonts.begin();
443 while (j != _fonts.end() && j->load_id != i->id) {
446 if (j != _fonts.end ()) {
447 ASDCP::TimedText::FrameBuffer buffer;
448 ArrayData data_copy(j->data);
449 buffer.SetData (data_copy.data(), data_copy.size());
450 buffer.Size (j->data.size());
451 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
452 if (ASDCP_FAILURE(r)) {
453 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
458 /* Image subtitle payload */
460 for (auto i: _subtitles) {
461 auto si = dynamic_pointer_cast<SubtitleImage>(i);
463 ASDCP::TimedText::FrameBuffer buffer;
464 buffer.SetData (si->png_image().data(), si->png_image().size());
465 buffer.Size (si->png_image().size());
466 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
467 if (ASDCP_FAILURE(r)) {
468 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
479 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
481 if (!SubtitleAsset::equals (other_asset, options, note)) {
485 auto other = dynamic_pointer_cast<const SMPTESubtitleAsset>(other_asset);
487 note (NoteType::ERROR, "Subtitles are in different standards");
491 auto i = _load_font_nodes.begin();
492 auto j = other->_load_font_nodes.begin();
494 while (i != _load_font_nodes.end ()) {
495 if (j == other->_load_font_nodes.end ()) {
496 note (NoteType::ERROR, "<LoadFont> nodes differ");
500 if ((*i)->id != (*j)->id) {
501 note (NoteType::ERROR, "<LoadFont> nodes differ");
509 if (_content_title_text != other->_content_title_text) {
510 note (NoteType::ERROR, "Subtitle content title texts differ");
514 if (_language != other->_language) {
515 note (NoteType::ERROR, String::compose("Subtitle languages differ (`%1' vs `%2')", _language.get_value_or("[none]"), other->_language.get_value_or("[none]")));
519 if (_annotation_text != other->_annotation_text) {
520 note (NoteType::ERROR, "Subtitle annotation texts differ");
524 if (_issue_date != other->_issue_date) {
525 if (options.issue_dates_can_differ) {
526 note (NoteType::NOTE, "Subtitle issue dates differ");
528 note (NoteType::ERROR, "Subtitle issue dates differ");
533 if (_reel_number != other->_reel_number) {
534 note (NoteType::ERROR, "Subtitle reel numbers differ");
538 if (_edit_rate != other->_edit_rate) {
539 note (NoteType::ERROR, "Subtitle edit rates differ");
543 if (_time_code_rate != other->_time_code_rate) {
544 note (NoteType::ERROR, "Subtitle time code rates differ");
548 if (_start_time != other->_start_time) {
549 note (NoteType::ERROR, "Subtitle start times differ");
558 SMPTESubtitleAsset::add_font (string load_id, dcp::ArrayData data)
560 string const uuid = make_uuid ();
561 _fonts.push_back (Font(load_id, uuid, data));
562 _load_font_nodes.push_back (make_shared<SMPTELoadFontNode>(load_id, uuid));
567 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
569 SubtitleAsset::add (s);
570 _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);