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 "smpte_subtitle_asset.h"
41 #include "smpte_load_font_node.h"
42 #include "exceptions.h"
44 #include "raw_convert.h"
45 #include "dcp_assert.h"
47 #include "compose.hpp"
48 #include "crypto_context.h"
49 #include "subtitle_image.h"
50 #include <asdcp/AS_DCP.h>
51 #include <asdcp/KM_util.h>
52 #include <asdcp/KM_log.h>
53 #include <libxml++/libxml++.h>
54 #include <boost/algorithm/string.hpp>
61 using std::shared_ptr;
62 using std::dynamic_pointer_cast;
63 using std::make_shared;
65 using boost::is_any_of;
66 using boost::shared_array;
67 using boost::optional;
68 using boost::starts_with;
72 static string const subtitle_smpte_ns = "http://www.smpte-ra.org/schemas/428-7/2010/DCST";
75 SMPTESubtitleAsset::SMPTESubtitleAsset ()
76 : MXF (Standard::SMPTE)
78 , _time_code_rate (24)
79 , _xml_id (make_uuid())
85 SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file)
86 : SubtitleAsset (file)
88 auto xml = make_shared<cxml::Document>("SubtitleReel");
90 auto reader = make_shared<ASDCP::TimedText::MXFReader>();
91 auto r = Kumu::RESULT_OK;
93 ASDCPErrorSuspender sus;
94 r = reader->OpenRead (_file->string().c_str ());
96 if (!ASDCP_FAILURE(r)) {
98 ASDCP::WriterInfo info;
99 reader->FillWriterInfo (info);
100 _id = read_writer_info (info);
102 /* Not encrypted; read it in now */
103 reader->ReadTimedTextResource (_raw_xml);
104 xml->read_string (_raw_xml);
106 read_mxf_descriptor (reader, make_shared<DecryptionContext>(optional<Key>(), Standard::SMPTE));
111 _raw_xml = dcp::file_to_string (file);
112 xml = make_shared<cxml::Document>("SubtitleReel");
113 xml->read_file (file);
115 _id = _xml_id = remove_urn_uuid (xml->string_child ("Id"));
116 } catch (cxml::Error& e) {
117 boost::throw_exception (
120 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
121 file, static_cast<int>(r), e.what()
127 /* Try to read PNG files from the same folder that the XML is in; the wisdom of this is
128 debatable, at best...
130 for (auto i: _subtitles) {
131 auto im = dynamic_pointer_cast<SubtitleImage>(i);
132 if (im && im->png_image().size() == 0) {
133 /* Even more dubious; allow <id>.png or urn:uuid:<id>.png */
134 auto p = file.parent_path() / String::compose("%1.png", im->id());
135 if (boost::filesystem::is_regular_file(p)) {
136 im->read_png_file (p);
137 } else if (starts_with (im->id(), "urn:uuid:")) {
138 p = file.parent_path() / String::compose("%1.png", remove_urn_uuid(im->id()));
139 if (boost::filesystem::is_regular_file(p)) {
140 im->read_png_file (p);
145 _standard = Standard::SMPTE;
148 /* Check that all required image data have been found */
149 for (auto i: _subtitles) {
150 auto im = dynamic_pointer_cast<SubtitleImage>(i);
151 if (im && im->png_image().size() == 0) {
152 throw MissingSubtitleImageError (im->id());
159 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
161 _xml_id = remove_urn_uuid(xml->string_child("Id"));
162 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
164 _content_title_text = xml->string_child ("ContentTitleText");
165 _annotation_text = xml->optional_string_child ("AnnotationText");
166 _issue_date = LocalTime (xml->string_child ("IssueDate"));
167 _reel_number = xml->optional_number_child<int> ("ReelNumber");
168 _language = xml->optional_string_child ("Language");
170 /* This is supposed to be two numbers, but a single number has been seen in the wild */
171 auto const er = xml->string_child ("EditRate");
172 vector<string> er_parts;
173 split (er_parts, er, is_any_of (" "));
174 if (er_parts.size() == 1) {
175 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
176 } else if (er_parts.size() == 2) {
177 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
179 throw XMLError ("malformed EditRate " + er);
182 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
183 if (xml->optional_string_child ("StartTime")) {
184 _start_time = Time (xml->string_child("StartTime"), _time_code_rate);
187 /* Now we need to drop down to xmlpp */
189 vector<ParseState> ps;
190 for (auto i: xml->node()->get_children()) {
191 auto const e = dynamic_cast<xmlpp::Element const *>(i);
192 if (e && e->get_name() == "SubtitleList") {
193 parse_subtitles (e, ps, _time_code_rate, Standard::SMPTE);
197 /* Guess intrinsic duration */
198 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
203 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
205 ASDCP::TimedText::TimedTextDescriptor descriptor;
206 reader->FillTimedTextDescriptor (descriptor);
208 /* Load fonts and images */
211 auto i = descriptor.ResourceList.begin();
212 i != descriptor.ResourceList.end();
215 ASDCP::TimedText::FrameBuffer buffer;
216 buffer.Capacity (10 * 1024 * 1024);
217 reader->ReadAncillaryResource (i->ResourceID, buffer, dec->context(), dec->hmac());
220 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof(id));
222 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
223 memcpy (data.get(), buffer.RoData(), buffer.Size());
226 case ASDCP::TimedText::MT_OPENTYPE:
228 auto j = _load_font_nodes.begin();
229 while (j != _load_font_nodes.end() && (*j)->urn != id) {
233 if (j != _load_font_nodes.end ()) {
234 _fonts.push_back (Font ((*j)->id, (*j)->urn, ArrayData (data, buffer.Size ())));
238 case ASDCP::TimedText::MT_PNG:
240 auto j = _subtitles.begin();
241 while (j != _subtitles.end() && ((!dynamic_pointer_cast<SubtitleImage>(*j)) || dynamic_pointer_cast<SubtitleImage>(*j)->id() != id)) {
245 if (j != _subtitles.end()) {
246 dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image (ArrayData(data, buffer.Size()));
255 _intrinsic_duration = descriptor.ContainerDuration;
260 SMPTESubtitleAsset::set_key (Key key)
262 /* See if we already have a key; if we do, and we have a file, we'll already
265 auto const had_key = static_cast<bool>(_key);
269 if (!_key_id || !_file || had_key) {
270 /* Either we don't have any data to read, it wasn't
271 encrypted, or we've already read it, so we don't
272 need to do anything else.
277 /* Our data was encrypted; now we can decrypt it */
279 auto reader = make_shared<ASDCP::TimedText::MXFReader>();
280 auto r = reader->OpenRead (_file->string().c_str ());
281 if (ASDCP_FAILURE (r)) {
282 boost::throw_exception (
284 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
289 auto dec = make_shared<DecryptionContext>(key, Standard::SMPTE);
290 reader->ReadTimedTextResource (_raw_xml, dec->context(), dec->hmac());
291 auto xml = make_shared<cxml::Document>("SubtitleReel");
292 xml->read_string (_raw_xml);
294 read_mxf_descriptor (reader, dec);
298 vector<shared_ptr<LoadFontNode>>
299 SMPTESubtitleAsset::load_font_nodes () const
301 vector<shared_ptr<LoadFontNode>> lf;
302 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter(lf));
308 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
310 ASDCP::TimedText::MXFReader reader;
311 Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
312 auto r = reader.OpenRead (file.string().c_str ());
313 Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
314 return !ASDCP_FAILURE (r);
319 SMPTESubtitleAsset::xml_as_string () const
322 auto root = doc.create_root_node ("dcst:SubtitleReel");
323 root->set_namespace_declaration (subtitle_smpte_ns, "dcst");
324 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
326 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _xml_id);
327 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
328 if (_annotation_text) {
329 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
331 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
333 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
336 root->add_child("Language", "dcst")->add_child_text (_language.get ());
338 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
339 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
341 root->add_child("StartTime", "dcst")->add_child_text(_start_time.get().as_string(Standard::SMPTE));
344 for (auto i: _load_font_nodes) {
345 auto load_font = root->add_child("LoadFont", "dcst");
346 load_font->add_child_text ("urn:uuid:" + i->urn);
347 load_font->set_attribute ("ID", i->id);
350 subtitles_as_xml (root->add_child("SubtitleList", "dcst"), _time_code_rate, Standard::SMPTE);
352 return doc.write_to_string ("UTF-8");
357 SMPTESubtitleAsset::write (boost::filesystem::path p) const
359 EncryptionContext enc (key(), Standard::SMPTE);
361 ASDCP::WriterInfo writer_info;
362 fill_writer_info (&writer_info, _id);
364 ASDCP::TimedText::TimedTextDescriptor descriptor;
365 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
366 descriptor.EncodingName = "UTF-8";
368 /* Font references */
370 for (auto i: _load_font_nodes) {
371 auto j = _fonts.begin();
372 while (j != _fonts.end() && j->load_id != i->id) {
375 if (j != _fonts.end ()) {
376 ASDCP::TimedText::TimedTextResourceDescriptor res;
378 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
379 DCP_ASSERT (c == Kumu::UUID_Length);
380 res.Type = ASDCP::TimedText::MT_OPENTYPE;
381 descriptor.ResourceList.push_back (res);
385 /* Image subtitle references */
387 for (auto i: _subtitles) {
388 auto si = dynamic_pointer_cast<SubtitleImage>(i);
390 ASDCP::TimedText::TimedTextResourceDescriptor res;
392 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
393 DCP_ASSERT (c == Kumu::UUID_Length);
394 res.Type = ASDCP::TimedText::MT_PNG;
395 descriptor.ResourceList.push_back (res);
399 descriptor.NamespaceName = subtitle_smpte_ns;
401 Kumu::hex2bin (_xml_id.c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
402 DCP_ASSERT (c == Kumu::UUID_Length);
403 descriptor.ContainerDuration = _intrinsic_duration;
405 ASDCP::TimedText::MXFWriter writer;
406 /* This header size is a guess. Empirically it seems that each subtitle reference is 90 bytes, and we need some extra.
407 The default size is not enough for some feature-length PNG sub projects (see DCP-o-matic #1561).
409 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor, _subtitles.size() * 90 + 16384);
410 if (ASDCP_FAILURE (r)) {
411 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
414 r = writer.WriteTimedTextResource (xml_as_string (), enc.context(), enc.hmac());
415 if (ASDCP_FAILURE (r)) {
416 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
421 for (auto i: _load_font_nodes) {
422 auto j = _fonts.begin();
423 while (j != _fonts.end() && j->load_id != i->id) {
426 if (j != _fonts.end ()) {
427 ASDCP::TimedText::FrameBuffer buffer;
428 ArrayData data_copy(j->data);
429 buffer.SetData (data_copy.data(), data_copy.size());
430 buffer.Size (j->data.size());
431 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
432 if (ASDCP_FAILURE(r)) {
433 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
438 /* Image subtitle payload */
440 for (auto i: _subtitles) {
441 auto si = dynamic_pointer_cast<SubtitleImage>(i);
443 ASDCP::TimedText::FrameBuffer buffer;
444 buffer.SetData (si->png_image().data(), si->png_image().size());
445 buffer.Size (si->png_image().size());
446 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
447 if (ASDCP_FAILURE(r)) {
448 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
459 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
461 if (!SubtitleAsset::equals (other_asset, options, note)) {
465 auto other = dynamic_pointer_cast<const SMPTESubtitleAsset>(other_asset);
467 note (NoteType::ERROR, "Subtitles are in different standards");
471 auto i = _load_font_nodes.begin();
472 auto j = other->_load_font_nodes.begin();
474 while (i != _load_font_nodes.end ()) {
475 if (j == other->_load_font_nodes.end ()) {
476 note (NoteType::ERROR, "<LoadFont> nodes differ");
480 if ((*i)->id != (*j)->id) {
481 note (NoteType::ERROR, "<LoadFont> nodes differ");
489 if (_content_title_text != other->_content_title_text) {
490 note (NoteType::ERROR, "Subtitle content title texts differ");
494 if (_language != other->_language) {
495 note (NoteType::ERROR, String::compose("Subtitle languages differ (`%1' vs `%2')", _language.get_value_or("[none]"), other->_language.get_value_or("[none]")));
499 if (_annotation_text != other->_annotation_text) {
500 note (NoteType::ERROR, "Subtitle annotation texts differ");
504 if (_issue_date != other->_issue_date) {
505 if (options.issue_dates_can_differ) {
506 note (NoteType::NOTE, "Subtitle issue dates differ");
508 note (NoteType::ERROR, "Subtitle issue dates differ");
513 if (_reel_number != other->_reel_number) {
514 note (NoteType::ERROR, "Subtitle reel numbers differ");
518 if (_edit_rate != other->_edit_rate) {
519 note (NoteType::ERROR, "Subtitle edit rates differ");
523 if (_time_code_rate != other->_time_code_rate) {
524 note (NoteType::ERROR, "Subtitle time code rates differ");
528 if (_start_time != other->_start_time) {
529 note (NoteType::ERROR, "Subtitle start times differ");
538 SMPTESubtitleAsset::add_font (string load_id, dcp::ArrayData data)
540 string const uuid = make_uuid ();
541 _fonts.push_back (Font(load_id, uuid, data));
542 _load_font_nodes.push_back (make_shared<SMPTELoadFontNode>(load_id, uuid));
547 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
549 SubtitleAsset::add (s);
550 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);