2 Copyright (C) 2012-2019 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.
34 /** @file src/smpte_subtitle_asset.cc
35 * @brief SMPTESubtitleAsset class.
38 #include "smpte_subtitle_asset.h"
39 #include "smpte_load_font_node.h"
40 #include "exceptions.h"
42 #include "raw_convert.h"
43 #include "dcp_assert.h"
45 #include "compose.hpp"
46 #include "crypto_context.h"
47 #include "subtitle_image.h"
48 #include <asdcp/AS_DCP.h>
49 #include <asdcp/KM_util.h>
50 #include <asdcp/KM_log.h>
51 #include <libxml++/libxml++.h>
52 #include <boost/foreach.hpp>
53 #include <boost/algorithm/string.hpp>
59 using boost::shared_ptr;
61 using boost::is_any_of;
62 using boost::shared_array;
63 using boost::dynamic_pointer_cast;
64 using boost::optional;
67 static string const subtitle_smpte_ns = "http://www.smpte-ra.org/schemas/428-7/2010/DCST";
69 SMPTESubtitleAsset::SMPTESubtitleAsset ()
71 , _intrinsic_duration (0)
73 , _time_code_rate (24)
74 , _xml_id (make_uuid ())
79 /** Construct a SMPTESubtitleAsset by reading an MXF or XML file.
80 * @param file Filename.
82 SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file)
83 : SubtitleAsset (file)
85 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
87 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
88 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
89 if (!ASDCP_FAILURE (r)) {
91 ASDCP::WriterInfo info;
92 reader->FillWriterInfo (info);
93 _id = read_writer_info (info);
95 /* Not encrypted; read it in now */
97 reader->ReadTimedTextResource (s);
100 read_mxf_descriptor (reader, shared_ptr<DecryptionContext> (new DecryptionContext (optional<Key>(), SMPTE)));
105 xml.reset (new cxml::Document ("SubtitleReel"));
106 xml->read_file (file);
108 _id = _xml_id = remove_urn_uuid (xml->string_child ("Id"));
109 } catch (cxml::Error& e) {
110 boost::throw_exception (
113 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
114 file, static_cast<int> (r), e.what ()
123 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
125 _xml_id = remove_urn_uuid(xml->string_child("Id"));
126 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
128 _content_title_text = xml->string_child ("ContentTitleText");
129 _annotation_text = xml->optional_string_child ("AnnotationText");
130 _issue_date = LocalTime (xml->string_child ("IssueDate"));
131 _reel_number = xml->optional_number_child<int> ("ReelNumber");
132 _language = xml->optional_string_child ("Language");
134 /* This is supposed to be two numbers, but a single number has been seen in the wild */
135 string const er = xml->string_child ("EditRate");
136 vector<string> er_parts;
137 split (er_parts, er, is_any_of (" "));
138 if (er_parts.size() == 1) {
139 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
140 } else if (er_parts.size() == 2) {
141 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
143 throw XMLError ("malformed EditRate " + er);
146 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
147 if (xml->optional_string_child ("StartTime")) {
148 _start_time = Time (xml->string_child ("StartTime"), _time_code_rate);
151 /* Now we need to drop down to xmlpp */
154 xmlpp::Node::NodeList c = xml->node()->get_children ();
155 for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
156 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
157 if (e && e->get_name() == "SubtitleList") {
158 parse_subtitles (e, ps, _time_code_rate, SMPTE);
162 /* Guess intrinsic duration */
163 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
167 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
169 ASDCP::TimedText::TimedTextDescriptor descriptor;
170 reader->FillTimedTextDescriptor (descriptor);
175 ASDCP::TimedText::ResourceList_t::const_iterator i = descriptor.ResourceList.begin();
176 i != descriptor.ResourceList.end();
179 ASDCP::TimedText::FrameBuffer buffer;
180 buffer.Capacity (10 * 1024 * 1024);
181 reader->ReadAncillaryResource (i->ResourceID, buffer, dec->context(), dec->hmac());
184 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof (id));
186 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
187 memcpy (data.get(), buffer.RoData(), buffer.Size());
190 case ASDCP::TimedText::MT_OPENTYPE:
192 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = _load_font_nodes.begin ();
193 while (j != _load_font_nodes.end() && (*j)->urn != id) {
197 if (j != _load_font_nodes.end ()) {
198 _fonts.push_back (Font ((*j)->id, (*j)->urn, Data (data, buffer.Size ())));
202 case ASDCP::TimedText::MT_PNG:
204 list<shared_ptr<Subtitle> >::const_iterator j = _subtitles.begin ();
205 while (j != _subtitles.end() && ((!dynamic_pointer_cast<SubtitleImage>(*j)) || dynamic_pointer_cast<SubtitleImage>(*j)->id() != id)) {
209 if (j != _subtitles.end()) {
210 dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image (Data(data, buffer.Size()));
219 /* Get intrinsic duration */
220 _intrinsic_duration = descriptor.ContainerDuration;
224 SMPTESubtitleAsset::set_key (Key key)
226 /* See if we already have a key; if we do, and we have a file, we'll already
229 bool const had_key = static_cast<bool> (_key);
233 if (!_key_id || !_file || had_key) {
234 /* Either we don't have any data to read, it wasn't
235 encrypted, or we've already read it, so we don't
236 need to do anything else.
241 /* Our data was encrypted; now we can decrypt it */
243 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
244 Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
245 if (ASDCP_FAILURE (r)) {
246 boost::throw_exception (
248 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
254 shared_ptr<DecryptionContext> dec (new DecryptionContext (key, SMPTE));
255 reader->ReadTimedTextResource (s, dec->context(), dec->hmac());
256 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
257 xml->read_string (s);
259 read_mxf_descriptor (reader, dec);
262 list<shared_ptr<LoadFontNode> >
263 SMPTESubtitleAsset::load_font_nodes () const
265 list<shared_ptr<LoadFontNode> > lf;
266 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
271 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
273 ASDCP::TimedText::MXFReader reader;
274 Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
275 Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
276 Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
277 return !ASDCP_FAILURE (r);
281 SMPTESubtitleAsset::xml_as_string () const
284 xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel");
285 root->set_namespace_declaration (subtitle_smpte_ns, "dcst");
286 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
288 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _xml_id);
289 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
290 if (_annotation_text) {
291 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
293 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
295 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
298 root->add_child("Language", "dcst")->add_child_text (_language.get ());
300 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
301 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
303 root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string (SMPTE));
306 BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) {
307 xmlpp::Element* load_font = root->add_child("LoadFont", "dcst");
308 load_font->add_child_text ("urn:uuid:" + i->urn);
309 load_font->set_attribute ("ID", i->id);
312 subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, SMPTE);
314 return doc.write_to_string ("UTF-8");
317 /** Write this content to a MXF file */
319 SMPTESubtitleAsset::write (boost::filesystem::path p) const
321 EncryptionContext enc (key(), SMPTE);
323 ASDCP::WriterInfo writer_info;
324 fill_writer_info (&writer_info, _id);
326 ASDCP::TimedText::TimedTextDescriptor descriptor;
327 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
328 descriptor.EncodingName = "UTF-8";
330 /* Font references */
332 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
333 list<Font>::const_iterator j = _fonts.begin ();
334 while (j != _fonts.end() && j->load_id != i->id) {
337 if (j != _fonts.end ()) {
338 ASDCP::TimedText::TimedTextResourceDescriptor res;
340 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
341 DCP_ASSERT (c == Kumu::UUID_Length);
342 res.Type = ASDCP::TimedText::MT_OPENTYPE;
343 descriptor.ResourceList.push_back (res);
347 /* Image subtitle references */
349 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
350 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
352 ASDCP::TimedText::TimedTextResourceDescriptor res;
354 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
355 DCP_ASSERT (c == Kumu::UUID_Length);
356 res.Type = ASDCP::TimedText::MT_PNG;
357 descriptor.ResourceList.push_back (res);
361 descriptor.NamespaceName = subtitle_smpte_ns;
363 Kumu::hex2bin (_xml_id.c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
364 DCP_ASSERT (c == Kumu::UUID_Length);
365 descriptor.ContainerDuration = _intrinsic_duration;
367 ASDCP::TimedText::MXFWriter writer;
368 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor);
369 if (ASDCP_FAILURE (r)) {
370 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
373 r = writer.WriteTimedTextResource (xml_as_string (), enc.context(), enc.hmac());
374 if (ASDCP_FAILURE (r)) {
375 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
380 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
381 list<Font>::const_iterator j = _fonts.begin ();
382 while (j != _fonts.end() && j->load_id != i->id) {
385 if (j != _fonts.end ()) {
386 ASDCP::TimedText::FrameBuffer buffer;
387 buffer.SetData (j->data.data().get(), j->data.size());
388 buffer.Size (j->data.size());
389 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
390 if (ASDCP_FAILURE (r)) {
391 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
396 /* Image subtitle payload */
398 BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
399 shared_ptr<SubtitleImage> si = dynamic_pointer_cast<SubtitleImage>(i);
401 ASDCP::TimedText::FrameBuffer buffer;
402 buffer.SetData (si->png_image().data().get(), si->png_image().size());
403 buffer.Size (si->png_image().size());
404 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
405 if (ASDCP_FAILURE(r)) {
406 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
417 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
419 if (!SubtitleAsset::equals (other_asset, options, note)) {
423 shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
425 note (DCP_ERROR, "Subtitles are in different standards");
429 list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
430 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
432 while (i != _load_font_nodes.end ()) {
433 if (j == other->_load_font_nodes.end ()) {
434 note (DCP_ERROR, "<LoadFont> nodes differ");
438 if ((*i)->id != (*j)->id) {
439 note (DCP_ERROR, "<LoadFont> nodes differ");
447 if (_content_title_text != other->_content_title_text) {
448 note (DCP_ERROR, "Subtitle content title texts differ");
452 if (_language != other->_language) {
453 note (DCP_ERROR, "Subtitle languages differ");
457 if (_annotation_text != other->_annotation_text) {
458 note (DCP_ERROR, "Subtitle annotation texts differ");
462 if (_issue_date != other->_issue_date) {
463 if (options.issue_dates_can_differ) {
464 note (DCP_NOTE, "Subtitle issue dates differ");
466 note (DCP_ERROR, "Subtitle issue dates differ");
471 if (_reel_number != other->_reel_number) {
472 note (DCP_ERROR, "Subtitle reel numbers differ");
476 if (_edit_rate != other->_edit_rate) {
477 note (DCP_ERROR, "Subtitle edit rates differ");
481 if (_time_code_rate != other->_time_code_rate) {
482 note (DCP_ERROR, "Subtitle time code rates differ");
486 if (_start_time != other->_start_time) {
487 note (DCP_ERROR, "Subtitle start times differ");
495 SMPTESubtitleAsset::add_font (string load_id, boost::filesystem::path file)
497 string const uuid = make_uuid ();
498 _fonts.push_back (Font (load_id, uuid, file));
499 _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
503 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
505 SubtitleAsset::add (s);
506 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);