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 } catch (cxml::Error& e) {
116 boost::throw_exception (
119 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
120 file, static_cast<int>(r), e.what()
126 /* Try to read PNG files from the same folder that the XML is in; the wisdom of this is
127 debatable, at best...
129 for (auto i: _subtitles) {
130 auto im = dynamic_pointer_cast<SubtitleImage>(i);
131 if (im && im->png_image().size() == 0) {
132 /* Even more dubious; allow <id>.png or urn:uuid:<id>.png */
133 auto p = file.parent_path() / String::compose("%1.png", im->id());
134 if (boost::filesystem::is_regular_file(p)) {
135 im->read_png_file (p);
136 } else if (starts_with (im->id(), "urn:uuid:")) {
137 p = file.parent_path() / String::compose("%1.png", remove_urn_uuid(im->id()));
138 if (boost::filesystem::is_regular_file(p)) {
139 im->read_png_file (p);
144 _standard = Standard::SMPTE;
147 /* Check that all required image data have been found */
148 for (auto i: _subtitles) {
149 auto im = dynamic_pointer_cast<SubtitleImage>(i);
150 if (im && im->png_image().size() == 0) {
151 throw MissingSubtitleImageError (im->id());
158 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
160 _xml_id = remove_urn_uuid(xml->string_child("Id"));
161 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
163 _content_title_text = xml->string_child ("ContentTitleText");
164 _annotation_text = xml->optional_string_child ("AnnotationText");
165 _issue_date = LocalTime (xml->string_child ("IssueDate"));
166 _reel_number = xml->optional_number_child<int> ("ReelNumber");
167 _language = xml->optional_string_child ("Language");
169 /* This is supposed to be two numbers, but a single number has been seen in the wild */
170 auto const er = xml->string_child ("EditRate");
171 vector<string> er_parts;
172 split (er_parts, er, is_any_of (" "));
173 if (er_parts.size() == 1) {
174 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
175 } else if (er_parts.size() == 2) {
176 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
178 throw XMLError ("malformed EditRate " + er);
181 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
182 if (xml->optional_string_child ("StartTime")) {
183 _start_time = Time (xml->string_child("StartTime"), _time_code_rate);
186 /* Now we need to drop down to xmlpp */
188 vector<ParseState> ps;
189 for (auto i: xml->node()->get_children()) {
190 auto const e = dynamic_cast<xmlpp::Element const *>(i);
191 if (e && e->get_name() == "SubtitleList") {
192 parse_subtitles (e, ps, _time_code_rate, Standard::SMPTE);
196 /* Guess intrinsic duration */
197 _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);
202 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
204 ASDCP::TimedText::TimedTextDescriptor descriptor;
205 reader->FillTimedTextDescriptor (descriptor);
207 /* Load fonts and images */
210 auto i = descriptor.ResourceList.begin();
211 i != descriptor.ResourceList.end();
214 ASDCP::TimedText::FrameBuffer buffer;
215 buffer.Capacity (10 * 1024 * 1024);
216 reader->ReadAncillaryResource (i->ResourceID, buffer, dec->context(), dec->hmac());
219 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof(id));
221 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
222 memcpy (data.get(), buffer.RoData(), buffer.Size());
225 case ASDCP::TimedText::MT_OPENTYPE:
227 auto j = _load_font_nodes.begin();
228 while (j != _load_font_nodes.end() && (*j)->urn != id) {
232 if (j != _load_font_nodes.end ()) {
233 _fonts.push_back (Font ((*j)->id, (*j)->urn, ArrayData (data, buffer.Size ())));
237 case ASDCP::TimedText::MT_PNG:
239 auto j = _subtitles.begin();
240 while (j != _subtitles.end() && ((!dynamic_pointer_cast<SubtitleImage>(*j)) || dynamic_pointer_cast<SubtitleImage>(*j)->id() != id)) {
244 if (j != _subtitles.end()) {
245 dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image (ArrayData(data, buffer.Size()));
254 _intrinsic_duration = descriptor.ContainerDuration;
255 /* The thing which is called AssetID in the descriptor is also known as the
256 * ResourceID of the MXF. We store that, at present just for verification
260 Kumu::bin2UUIDhex (descriptor.AssetID, ASDCP::UUIDlen, id, sizeof(id));
266 SMPTESubtitleAsset::set_key (Key key)
268 /* See if we already have a key; if we do, and we have a file, we'll already
271 auto const had_key = static_cast<bool>(_key);
275 if (!_key_id || !_file || had_key) {
276 /* Either we don't have any data to read, it wasn't
277 encrypted, or we've already read it, so we don't
278 need to do anything else.
283 /* Our data was encrypted; now we can decrypt it */
285 auto reader = make_shared<ASDCP::TimedText::MXFReader>();
286 auto r = reader->OpenRead (_file->string().c_str ());
287 if (ASDCP_FAILURE (r)) {
288 boost::throw_exception (
290 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
295 auto dec = make_shared<DecryptionContext>(key, Standard::SMPTE);
296 reader->ReadTimedTextResource (_raw_xml, dec->context(), dec->hmac());
297 auto xml = make_shared<cxml::Document>("SubtitleReel");
298 xml->read_string (_raw_xml);
300 read_mxf_descriptor (reader, dec);
304 vector<shared_ptr<LoadFontNode>>
305 SMPTESubtitleAsset::load_font_nodes () const
307 vector<shared_ptr<LoadFontNode>> lf;
308 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter(lf));
314 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
316 ASDCP::TimedText::MXFReader reader;
317 Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
318 auto r = reader.OpenRead (file.string().c_str ());
319 Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
320 return !ASDCP_FAILURE (r);
325 SMPTESubtitleAsset::xml_as_string () const
328 auto root = doc.create_root_node ("dcst:SubtitleReel");
329 root->set_namespace_declaration (subtitle_smpte_ns, "dcst");
330 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
332 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _xml_id);
333 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
334 if (_annotation_text) {
335 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
337 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
339 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
342 root->add_child("Language", "dcst")->add_child_text (_language.get ());
344 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
345 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
347 root->add_child("StartTime", "dcst")->add_child_text(_start_time.get().as_string(Standard::SMPTE));
350 for (auto i: _load_font_nodes) {
351 auto load_font = root->add_child("LoadFont", "dcst");
352 load_font->add_child_text ("urn:uuid:" + i->urn);
353 load_font->set_attribute ("ID", i->id);
356 subtitles_as_xml (root->add_child("SubtitleList", "dcst"), _time_code_rate, Standard::SMPTE);
358 return doc.write_to_string ("UTF-8");
363 SMPTESubtitleAsset::write (boost::filesystem::path p) const
365 EncryptionContext enc (key(), Standard::SMPTE);
367 ASDCP::WriterInfo writer_info;
368 fill_writer_info (&writer_info, _id);
370 ASDCP::TimedText::TimedTextDescriptor descriptor;
371 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
372 descriptor.EncodingName = "UTF-8";
374 /* Font references */
376 for (auto i: _load_font_nodes) {
377 auto j = _fonts.begin();
378 while (j != _fonts.end() && j->load_id != i->id) {
381 if (j != _fonts.end ()) {
382 ASDCP::TimedText::TimedTextResourceDescriptor res;
384 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
385 DCP_ASSERT (c == Kumu::UUID_Length);
386 res.Type = ASDCP::TimedText::MT_OPENTYPE;
387 descriptor.ResourceList.push_back (res);
391 /* Image subtitle references */
393 for (auto i: _subtitles) {
394 auto si = dynamic_pointer_cast<SubtitleImage>(i);
396 ASDCP::TimedText::TimedTextResourceDescriptor res;
398 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
399 DCP_ASSERT (c == Kumu::UUID_Length);
400 res.Type = ASDCP::TimedText::MT_PNG;
401 descriptor.ResourceList.push_back (res);
405 descriptor.NamespaceName = subtitle_smpte_ns;
407 Kumu::hex2bin (_xml_id.c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
408 DCP_ASSERT (c == Kumu::UUID_Length);
409 descriptor.ContainerDuration = _intrinsic_duration;
411 ASDCP::TimedText::MXFWriter writer;
412 /* This header size is a guess. Empirically it seems that each subtitle reference is 90 bytes, and we need some extra.
413 The default size is not enough for some feature-length PNG sub projects (see DCP-o-matic #1561).
415 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor, _subtitles.size() * 90 + 16384);
416 if (ASDCP_FAILURE (r)) {
417 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
420 r = writer.WriteTimedTextResource (xml_as_string (), enc.context(), enc.hmac());
421 if (ASDCP_FAILURE (r)) {
422 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
427 for (auto i: _load_font_nodes) {
428 auto j = _fonts.begin();
429 while (j != _fonts.end() && j->load_id != i->id) {
432 if (j != _fonts.end ()) {
433 ASDCP::TimedText::FrameBuffer buffer;
434 ArrayData data_copy(j->data);
435 buffer.SetData (data_copy.data(), data_copy.size());
436 buffer.Size (j->data.size());
437 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
438 if (ASDCP_FAILURE(r)) {
439 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
444 /* Image subtitle payload */
446 for (auto i: _subtitles) {
447 auto si = dynamic_pointer_cast<SubtitleImage>(i);
449 ASDCP::TimedText::FrameBuffer buffer;
450 buffer.SetData (si->png_image().data(), si->png_image().size());
451 buffer.Size (si->png_image().size());
452 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
453 if (ASDCP_FAILURE(r)) {
454 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
465 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
467 if (!SubtitleAsset::equals (other_asset, options, note)) {
471 auto other = dynamic_pointer_cast<const SMPTESubtitleAsset>(other_asset);
473 note (NoteType::ERROR, "Subtitles are in different standards");
477 auto i = _load_font_nodes.begin();
478 auto j = other->_load_font_nodes.begin();
480 while (i != _load_font_nodes.end ()) {
481 if (j == other->_load_font_nodes.end ()) {
482 note (NoteType::ERROR, "<LoadFont> nodes differ");
486 if ((*i)->id != (*j)->id) {
487 note (NoteType::ERROR, "<LoadFont> nodes differ");
495 if (_content_title_text != other->_content_title_text) {
496 note (NoteType::ERROR, "Subtitle content title texts differ");
500 if (_language != other->_language) {
501 note (NoteType::ERROR, String::compose("Subtitle languages differ (`%1' vs `%2')", _language.get_value_or("[none]"), other->_language.get_value_or("[none]")));
505 if (_annotation_text != other->_annotation_text) {
506 note (NoteType::ERROR, "Subtitle annotation texts differ");
510 if (_issue_date != other->_issue_date) {
511 if (options.issue_dates_can_differ) {
512 note (NoteType::NOTE, "Subtitle issue dates differ");
514 note (NoteType::ERROR, "Subtitle issue dates differ");
519 if (_reel_number != other->_reel_number) {
520 note (NoteType::ERROR, "Subtitle reel numbers differ");
524 if (_edit_rate != other->_edit_rate) {
525 note (NoteType::ERROR, "Subtitle edit rates differ");
529 if (_time_code_rate != other->_time_code_rate) {
530 note (NoteType::ERROR, "Subtitle time code rates differ");
534 if (_start_time != other->_start_time) {
535 note (NoteType::ERROR, "Subtitle start times differ");
544 SMPTESubtitleAsset::add_font (string load_id, dcp::ArrayData data)
546 string const uuid = make_uuid ();
547 _fonts.push_back (Font(load_id, uuid, data));
548 _load_font_nodes.push_back (make_shared<SMPTELoadFontNode>(load_id, uuid));
553 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
555 SubtitleAsset::add (s);
556 _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);