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, make_shared<DecryptionContext>(optional<Key>(), Standard::SMPTE));
114 _raw_xml = dcp::file_to_string (file);
115 xml = make_shared<cxml::Document>("SubtitleReel");
116 xml->read_file (file);
118 } catch (cxml::Error& e) {
119 boost::throw_exception (
122 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
123 file, static_cast<int>(r), e.what()
129 /* Try to read PNG files from the same folder that the XML is in; the wisdom of this is
130 debatable, at best...
132 for (auto i: _subtitles) {
133 auto im = dynamic_pointer_cast<SubtitleImage>(i);
134 if (im && im->png_image().size() == 0) {
135 /* Even more dubious; allow <id>.png or urn:uuid:<id>.png */
136 auto p = file.parent_path() / String::compose("%1.png", im->id());
137 if (boost::filesystem::is_regular_file(p)) {
138 im->read_png_file (p);
139 } else if (starts_with (im->id(), "urn:uuid:")) {
140 p = file.parent_path() / String::compose("%1.png", remove_urn_uuid(im->id()));
141 if (boost::filesystem::is_regular_file(p)) {
142 im->read_png_file (p);
147 _standard = Standard::SMPTE;
150 /* Check that all required image data have been found */
151 for (auto i: _subtitles) {
152 auto im = dynamic_pointer_cast<SubtitleImage>(i);
153 if (im && im->png_image().size() == 0) {
154 throw MissingSubtitleImageError (im->id());
161 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
163 _xml_id = remove_urn_uuid(xml->string_child("Id"));
164 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
166 _content_title_text = xml->string_child ("ContentTitleText");
167 _annotation_text = xml->optional_string_child ("AnnotationText");
168 _issue_date = LocalTime (xml->string_child ("IssueDate"));
169 _reel_number = xml->optional_number_child<int> ("ReelNumber");
170 _language = xml->optional_string_child ("Language");
172 /* This is supposed to be two numbers, but a single number has been seen in the wild */
173 auto const er = xml->string_child ("EditRate");
174 vector<string> er_parts;
175 split (er_parts, er, is_any_of (" "));
176 if (er_parts.size() == 1) {
177 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
178 } else if (er_parts.size() == 2) {
179 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
181 throw XMLError ("malformed EditRate " + er);
184 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
185 if (xml->optional_string_child ("StartTime")) {
186 _start_time = Time (xml->string_child("StartTime"), _time_code_rate);
189 /* Now we need to drop down to xmlpp */
191 vector<ParseState> ps;
192 for (auto i: xml->node()->get_children()) {
193 auto const e = dynamic_cast<xmlpp::Element const *>(i);
194 if (e && e->get_name() == "SubtitleList") {
195 parse_subtitles (e, ps, _time_code_rate, Standard::SMPTE);
199 /* Guess intrinsic duration */
200 _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);
205 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
207 ASDCP::TimedText::TimedTextDescriptor descriptor;
208 reader->FillTimedTextDescriptor (descriptor);
210 /* Load fonts and images */
213 auto i = descriptor.ResourceList.begin();
214 i != descriptor.ResourceList.end();
217 ASDCP::TimedText::FrameBuffer buffer;
218 buffer.Capacity (10 * 1024 * 1024);
219 reader->ReadAncillaryResource (i->ResourceID, buffer, dec->context(), dec->hmac());
222 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof(id));
224 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
225 memcpy (data.get(), buffer.RoData(), buffer.Size());
228 case ASDCP::TimedText::MT_OPENTYPE:
230 auto j = _load_font_nodes.begin();
231 while (j != _load_font_nodes.end() && (*j)->urn != id) {
235 if (j != _load_font_nodes.end ()) {
236 _fonts.push_back (Font ((*j)->id, (*j)->urn, ArrayData (data, buffer.Size ())));
240 case ASDCP::TimedText::MT_PNG:
242 auto j = _subtitles.begin();
243 while (j != _subtitles.end() && ((!dynamic_pointer_cast<SubtitleImage>(*j)) || dynamic_pointer_cast<SubtitleImage>(*j)->id() != id)) {
247 if (j != _subtitles.end()) {
248 dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image (ArrayData(data, buffer.Size()));
257 _intrinsic_duration = descriptor.ContainerDuration;
258 /* The thing which is called AssetID in the descriptor is also known as the
259 * ResourceID of the MXF. We store that, at present just for verification
263 Kumu::bin2UUIDhex (descriptor.AssetID, ASDCP::UUIDlen, id, sizeof(id));
269 SMPTESubtitleAsset::set_key (Key key)
271 /* See if we already have a key; if we do, and we have a file, we'll already
274 auto const had_key = static_cast<bool>(_key);
278 if (!_key_id || !_file || had_key) {
279 /* Either we don't have any data to read, it wasn't
280 encrypted, or we've already read it, so we don't
281 need to do anything else.
286 /* Our data was encrypted; now we can decrypt it */
288 auto reader = make_shared<ASDCP::TimedText::MXFReader>();
289 auto r = reader->OpenRead (_file->string().c_str ());
290 if (ASDCP_FAILURE (r)) {
291 boost::throw_exception (
293 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
298 auto dec = make_shared<DecryptionContext>(key, Standard::SMPTE);
299 reader->ReadTimedTextResource (_raw_xml, dec->context(), dec->hmac());
300 auto xml = make_shared<cxml::Document>("SubtitleReel");
301 xml->read_string (_raw_xml);
303 read_mxf_descriptor (reader, dec);
307 vector<shared_ptr<LoadFontNode>>
308 SMPTESubtitleAsset::load_font_nodes () const
310 vector<shared_ptr<LoadFontNode>> lf;
311 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter(lf));
317 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
319 ASDCP::TimedText::MXFReader reader;
320 Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
321 auto r = reader.OpenRead (file.string().c_str ());
322 Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
323 return !ASDCP_FAILURE (r);
328 SMPTESubtitleAsset::xml_as_string () const
331 auto root = doc.create_root_node ("dcst:SubtitleReel");
332 root->set_namespace_declaration (subtitle_smpte_ns, "dcst");
333 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
335 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _xml_id);
336 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
337 if (_annotation_text) {
338 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
340 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
342 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
345 root->add_child("Language", "dcst")->add_child_text (_language.get ());
347 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
348 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
350 root->add_child("StartTime", "dcst")->add_child_text(_start_time.get().as_string(Standard::SMPTE));
353 for (auto i: _load_font_nodes) {
354 auto load_font = root->add_child("LoadFont", "dcst");
355 load_font->add_child_text ("urn:uuid:" + i->urn);
356 load_font->set_attribute ("ID", i->id);
359 subtitles_as_xml (root->add_child("SubtitleList", "dcst"), _time_code_rate, Standard::SMPTE);
361 return doc.write_to_string ("UTF-8");
366 SMPTESubtitleAsset::write (boost::filesystem::path p) const
368 EncryptionContext enc (key(), Standard::SMPTE);
370 ASDCP::WriterInfo writer_info;
371 fill_writer_info (&writer_info, _id);
373 ASDCP::TimedText::TimedTextDescriptor descriptor;
374 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
375 descriptor.EncodingName = "UTF-8";
377 /* Font references */
379 for (auto i: _load_font_nodes) {
380 auto j = _fonts.begin();
381 while (j != _fonts.end() && j->load_id != i->id) {
384 if (j != _fonts.end ()) {
385 ASDCP::TimedText::TimedTextResourceDescriptor res;
387 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
388 DCP_ASSERT (c == Kumu::UUID_Length);
389 res.Type = ASDCP::TimedText::MT_OPENTYPE;
390 descriptor.ResourceList.push_back (res);
394 /* Image subtitle references */
396 for (auto i: _subtitles) {
397 auto si = dynamic_pointer_cast<SubtitleImage>(i);
399 ASDCP::TimedText::TimedTextResourceDescriptor res;
401 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
402 DCP_ASSERT (c == Kumu::UUID_Length);
403 res.Type = ASDCP::TimedText::MT_PNG;
404 descriptor.ResourceList.push_back (res);
408 descriptor.NamespaceName = subtitle_smpte_ns;
410 Kumu::hex2bin (_xml_id.c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
411 DCP_ASSERT (c == Kumu::UUID_Length);
412 descriptor.ContainerDuration = _intrinsic_duration;
414 ASDCP::TimedText::MXFWriter writer;
415 /* This header size is a guess. Empirically it seems that each subtitle reference is 90 bytes, and we need some extra.
416 The default size is not enough for some feature-length PNG sub projects (see DCP-o-matic #1561).
418 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor, _subtitles.size() * 90 + 16384);
419 if (ASDCP_FAILURE (r)) {
420 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
423 r = writer.WriteTimedTextResource (xml_as_string (), enc.context(), enc.hmac());
424 if (ASDCP_FAILURE (r)) {
425 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
430 for (auto i: _load_font_nodes) {
431 auto j = _fonts.begin();
432 while (j != _fonts.end() && j->load_id != i->id) {
435 if (j != _fonts.end ()) {
436 ASDCP::TimedText::FrameBuffer buffer;
437 ArrayData data_copy(j->data);
438 buffer.SetData (data_copy.data(), data_copy.size());
439 buffer.Size (j->data.size());
440 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
441 if (ASDCP_FAILURE(r)) {
442 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
447 /* Image subtitle payload */
449 for (auto i: _subtitles) {
450 auto si = dynamic_pointer_cast<SubtitleImage>(i);
452 ASDCP::TimedText::FrameBuffer buffer;
453 buffer.SetData (si->png_image().data(), si->png_image().size());
454 buffer.Size (si->png_image().size());
455 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
456 if (ASDCP_FAILURE(r)) {
457 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
468 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
470 if (!SubtitleAsset::equals (other_asset, options, note)) {
474 auto other = dynamic_pointer_cast<const SMPTESubtitleAsset>(other_asset);
476 note (NoteType::ERROR, "Subtitles are in different standards");
480 auto i = _load_font_nodes.begin();
481 auto j = other->_load_font_nodes.begin();
483 while (i != _load_font_nodes.end ()) {
484 if (j == other->_load_font_nodes.end ()) {
485 note (NoteType::ERROR, "<LoadFont> nodes differ");
489 if ((*i)->id != (*j)->id) {
490 note (NoteType::ERROR, "<LoadFont> nodes differ");
498 if (_content_title_text != other->_content_title_text) {
499 note (NoteType::ERROR, "Subtitle content title texts differ");
503 if (_language != other->_language) {
504 note (NoteType::ERROR, String::compose("Subtitle languages differ (`%1' vs `%2')", _language.get_value_or("[none]"), other->_language.get_value_or("[none]")));
508 if (_annotation_text != other->_annotation_text) {
509 note (NoteType::ERROR, "Subtitle annotation texts differ");
513 if (_issue_date != other->_issue_date) {
514 if (options.issue_dates_can_differ) {
515 note (NoteType::NOTE, "Subtitle issue dates differ");
517 note (NoteType::ERROR, "Subtitle issue dates differ");
522 if (_reel_number != other->_reel_number) {
523 note (NoteType::ERROR, "Subtitle reel numbers differ");
527 if (_edit_rate != other->_edit_rate) {
528 note (NoteType::ERROR, "Subtitle edit rates differ");
532 if (_time_code_rate != other->_time_code_rate) {
533 note (NoteType::ERROR, "Subtitle time code rates differ");
537 if (_start_time != other->_start_time) {
538 note (NoteType::ERROR, "Subtitle start times differ");
547 SMPTESubtitleAsset::add_font (string load_id, dcp::ArrayData data)
549 string const uuid = make_uuid ();
550 _fonts.push_back (Font(load_id, uuid, data));
551 _load_font_nodes.push_back (make_shared<SMPTELoadFontNode>(load_id, uuid));
556 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
558 SubtitleAsset::add (s);
559 _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);