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_ceil(_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;
256 /* The thing which is called AssetID in the descriptor is also known as the
257 * ResourceID of the MXF. We store that, at present just for verification
261 Kumu::bin2UUIDhex (descriptor.AssetID, ASDCP::UUIDlen, id, sizeof(id));
267 SMPTESubtitleAsset::set_key (Key key)
269 /* See if we already have a key; if we do, and we have a file, we'll already
272 auto const had_key = static_cast<bool>(_key);
276 if (!_key_id || !_file || had_key) {
277 /* Either we don't have any data to read, it wasn't
278 encrypted, or we've already read it, so we don't
279 need to do anything else.
284 /* Our data was encrypted; now we can decrypt it */
286 auto reader = make_shared<ASDCP::TimedText::MXFReader>();
287 auto r = reader->OpenRead (_file->string().c_str ());
288 if (ASDCP_FAILURE (r)) {
289 boost::throw_exception (
291 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
296 auto dec = make_shared<DecryptionContext>(key, Standard::SMPTE);
297 reader->ReadTimedTextResource (_raw_xml, dec->context(), dec->hmac());
298 auto xml = make_shared<cxml::Document>("SubtitleReel");
299 xml->read_string (_raw_xml);
301 read_mxf_descriptor (reader, dec);
305 vector<shared_ptr<LoadFontNode>>
306 SMPTESubtitleAsset::load_font_nodes () const
308 vector<shared_ptr<LoadFontNode>> lf;
309 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter(lf));
315 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
317 ASDCP::TimedText::MXFReader reader;
318 Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
319 auto r = reader.OpenRead (file.string().c_str ());
320 Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
321 return !ASDCP_FAILURE (r);
326 SMPTESubtitleAsset::xml_as_string () const
329 auto root = doc.create_root_node ("dcst:SubtitleReel");
330 root->set_namespace_declaration (subtitle_smpte_ns, "dcst");
331 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
333 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _xml_id);
334 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
335 if (_annotation_text) {
336 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
338 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
340 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
343 root->add_child("Language", "dcst")->add_child_text (_language.get ());
345 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
346 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
348 root->add_child("StartTime", "dcst")->add_child_text(_start_time.get().as_string(Standard::SMPTE));
351 for (auto i: _load_font_nodes) {
352 auto load_font = root->add_child("LoadFont", "dcst");
353 load_font->add_child_text ("urn:uuid:" + i->urn);
354 load_font->set_attribute ("ID", i->id);
357 subtitles_as_xml (root->add_child("SubtitleList", "dcst"), _time_code_rate, Standard::SMPTE);
359 return doc.write_to_string ("UTF-8");
364 SMPTESubtitleAsset::write (boost::filesystem::path p) const
366 EncryptionContext enc (key(), Standard::SMPTE);
368 ASDCP::WriterInfo writer_info;
369 fill_writer_info (&writer_info, _id);
371 ASDCP::TimedText::TimedTextDescriptor descriptor;
372 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
373 descriptor.EncodingName = "UTF-8";
375 /* Font references */
377 for (auto i: _load_font_nodes) {
378 auto j = _fonts.begin();
379 while (j != _fonts.end() && j->load_id != i->id) {
382 if (j != _fonts.end ()) {
383 ASDCP::TimedText::TimedTextResourceDescriptor res;
385 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
386 DCP_ASSERT (c == Kumu::UUID_Length);
387 res.Type = ASDCP::TimedText::MT_OPENTYPE;
388 descriptor.ResourceList.push_back (res);
392 /* Image subtitle references */
394 for (auto i: _subtitles) {
395 auto si = dynamic_pointer_cast<SubtitleImage>(i);
397 ASDCP::TimedText::TimedTextResourceDescriptor res;
399 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
400 DCP_ASSERT (c == Kumu::UUID_Length);
401 res.Type = ASDCP::TimedText::MT_PNG;
402 descriptor.ResourceList.push_back (res);
406 descriptor.NamespaceName = subtitle_smpte_ns;
408 Kumu::hex2bin (_xml_id.c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
409 DCP_ASSERT (c == Kumu::UUID_Length);
410 descriptor.ContainerDuration = _intrinsic_duration;
412 ASDCP::TimedText::MXFWriter writer;
413 /* This header size is a guess. Empirically it seems that each subtitle reference is 90 bytes, and we need some extra.
414 The default size is not enough for some feature-length PNG sub projects (see DCP-o-matic #1561).
416 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor, _subtitles.size() * 90 + 16384);
417 if (ASDCP_FAILURE (r)) {
418 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
421 r = writer.WriteTimedTextResource (xml_as_string (), enc.context(), enc.hmac());
422 if (ASDCP_FAILURE (r)) {
423 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
428 for (auto i: _load_font_nodes) {
429 auto j = _fonts.begin();
430 while (j != _fonts.end() && j->load_id != i->id) {
433 if (j != _fonts.end ()) {
434 ASDCP::TimedText::FrameBuffer buffer;
435 ArrayData data_copy(j->data);
436 buffer.SetData (data_copy.data(), data_copy.size());
437 buffer.Size (j->data.size());
438 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
439 if (ASDCP_FAILURE(r)) {
440 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
445 /* Image subtitle payload */
447 for (auto i: _subtitles) {
448 auto si = dynamic_pointer_cast<SubtitleImage>(i);
450 ASDCP::TimedText::FrameBuffer buffer;
451 buffer.SetData (si->png_image().data(), si->png_image().size());
452 buffer.Size (si->png_image().size());
453 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
454 if (ASDCP_FAILURE(r)) {
455 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
466 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
468 if (!SubtitleAsset::equals (other_asset, options, note)) {
472 auto other = dynamic_pointer_cast<const SMPTESubtitleAsset>(other_asset);
474 note (NoteType::ERROR, "Subtitles are in different standards");
478 auto i = _load_font_nodes.begin();
479 auto j = other->_load_font_nodes.begin();
481 while (i != _load_font_nodes.end ()) {
482 if (j == other->_load_font_nodes.end ()) {
483 note (NoteType::ERROR, "<LoadFont> nodes differ");
487 if ((*i)->id != (*j)->id) {
488 note (NoteType::ERROR, "<LoadFont> nodes differ");
496 if (_content_title_text != other->_content_title_text) {
497 note (NoteType::ERROR, "Subtitle content title texts differ");
501 if (_language != other->_language) {
502 note (NoteType::ERROR, String::compose("Subtitle languages differ (`%1' vs `%2')", _language.get_value_or("[none]"), other->_language.get_value_or("[none]")));
506 if (_annotation_text != other->_annotation_text) {
507 note (NoteType::ERROR, "Subtitle annotation texts differ");
511 if (_issue_date != other->_issue_date) {
512 if (options.issue_dates_can_differ) {
513 note (NoteType::NOTE, "Subtitle issue dates differ");
515 note (NoteType::ERROR, "Subtitle issue dates differ");
520 if (_reel_number != other->_reel_number) {
521 note (NoteType::ERROR, "Subtitle reel numbers differ");
525 if (_edit_rate != other->_edit_rate) {
526 note (NoteType::ERROR, "Subtitle edit rates differ");
530 if (_time_code_rate != other->_time_code_rate) {
531 note (NoteType::ERROR, "Subtitle time code rates differ");
535 if (_start_time != other->_start_time) {
536 note (NoteType::ERROR, "Subtitle start times differ");
545 SMPTESubtitleAsset::add_font (string load_id, dcp::ArrayData data)
547 string const uuid = make_uuid ();
548 _fonts.push_back (Font(load_id, uuid, data));
549 _load_font_nodes.push_back (make_shared<SMPTELoadFontNode>(load_id, uuid));
554 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
556 SubtitleAsset::add (s);
557 _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);