40 #include "compose.hpp"
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)
91 auto xml = make_shared<cxml::Document>(
"SubtitleReel");
93 auto reader = make_shared<ASDCP::TimedText::MXFReader>();
94 auto r = Kumu::RESULT_OK;
97 r = reader->OpenRead (
_file->string().c_str ());
99 if (!ASDCP_FAILURE(r)) {
101 ASDCP::WriterInfo info;
102 reader->FillWriterInfo (info);
107 reader->ReadTimedTextResource (xml_string);
109 xml->read_string (xml_string);
111 read_mxf_descriptor (reader);
112 read_mxf_resources (reader, make_shared<DecryptionContext>(optional<Key>(), Standard::SMPTE));
114 read_mxf_descriptor (reader);
120 xml = make_shared<cxml::Document>(
"SubtitleReel");
121 xml->read_file (
file);
123 }
catch (cxml::Error& e) {
124 boost::throw_exception (
127 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
128 file,
static_cast<int>(r), e.what()
138 auto im = dynamic_pointer_cast<SubtitleImage>(i);
139 if (im && im->png_image().size() == 0) {
141 auto p =
file.parent_path() / String::compose(
"%1.png", im->id());
142 if (boost::filesystem::is_regular_file(p)) {
143 im->read_png_file (p);
144 }
else if (starts_with (im->id(),
"urn:uuid:")) {
145 p =
file.parent_path() / String::compose(
"%1.png", remove_urn_uuid(im->id()));
146 if (boost::filesystem::is_regular_file(p)) {
147 im->read_png_file (p);
152 _standard = Standard::SMPTE;
157 auto im = dynamic_pointer_cast<SubtitleImage>(i);
158 if (im && im->png_image().size() == 0) {
166 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
168 _xml_id = remove_urn_uuid(xml->string_child(
"Id"));
169 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml,
"LoadFont");
172 _annotation_text = xml->optional_string_child (
"AnnotationText");
173 _issue_date =
LocalTime (xml->string_child (
"IssueDate"));
174 _reel_number = xml->optional_number_child<
int> (
"ReelNumber");
175 _language = xml->optional_string_child (
"Language");
178 auto const er = xml->string_child (
"EditRate");
179 vector<string> er_parts;
180 split (er_parts, er, is_any_of (
" "));
181 if (er_parts.size() == 1) {
182 _edit_rate =
Fraction (raw_convert<int> (er_parts[0]), 1);
183 }
else if (er_parts.size() == 2) {
184 _edit_rate =
Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
186 throw XMLError (
"malformed EditRate " + er);
189 _time_code_rate = xml->number_child<
int> (
"TimeCodeRate");
190 if (xml->optional_string_child (
"StartTime")) {
191 _start_time =
Time (xml->string_child(
"StartTime"), _time_code_rate);
196 vector<ParseState> ps;
197 for (
auto i: xml->node()->get_children()) {
198 auto const e =
dynamic_cast<xmlpp::Element
const *
>(i);
199 if (e && e->get_name() ==
"SubtitleList") {
200 parse_subtitles (e, ps, _time_code_rate, Standard::SMPTE);
210 SMPTESubtitleAsset::read_mxf_resources (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
212 ASDCP::TimedText::TimedTextDescriptor descriptor;
213 reader->FillTimedTextDescriptor (descriptor);
218 auto i = descriptor.ResourceList.begin();
219 i != descriptor.ResourceList.end();
222 ASDCP::TimedText::FrameBuffer buffer;
223 buffer.Capacity (10 * 1024 * 1024);
224 reader->ReadAncillaryResource (i->ResourceID, buffer, dec->context(), dec->hmac());
227 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen,
id,
sizeof(
id));
229 shared_array<uint8_t> data (
new uint8_t[buffer.Size()]);
230 memcpy (data.get(), buffer.RoData(), buffer.Size());
233 case ASDCP::TimedText::MT_OPENTYPE:
235 auto j = _load_font_nodes.begin();
236 while (j != _load_font_nodes.end() && (*j)->urn !=
id) {
240 if (j != _load_font_nodes.end ()) {
241 _fonts.push_back (Font ((*j)->id, (*j)->urn,
ArrayData (data, buffer.Size ())));
245 case ASDCP::TimedText::MT_PNG:
248 while (j !=
_subtitles.end() && ((!dynamic_pointer_cast<SubtitleImage>(*j)) || dynamic_pointer_cast<SubtitleImage>(*j)->id() !=
id)) {
253 dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image (
ArrayData(data, buffer.Size()));
265 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader)
267 ASDCP::TimedText::TimedTextDescriptor descriptor;
268 reader->FillTimedTextDescriptor (descriptor);
276 Kumu::bin2UUIDhex (descriptor.AssetID, ASDCP::UUIDlen,
id,
sizeof(
id));
287 auto const had_key =
static_cast<bool>(
_key);
301 auto reader = make_shared<ASDCP::TimedText::MXFReader>();
302 auto r = reader->OpenRead (
_file->string().c_str ());
303 if (ASDCP_FAILURE (r)) {
304 boost::throw_exception (
306 String::compose (
"Could not read encrypted subtitle MXF (%1)",
static_cast<int> (r))
311 auto dec = make_shared<DecryptionContext>(
key, Standard::SMPTE);
313 reader->ReadTimedTextResource (xml_string, dec->context(), dec->hmac());
315 auto xml = make_shared<cxml::Document>(
"SubtitleReel");
316 xml->read_string (xml_string);
318 read_mxf_resources (reader, dec);
322 vector<shared_ptr<LoadFontNode>>
323 SMPTESubtitleAsset::load_font_nodes ()
const
325 vector<shared_ptr<LoadFontNode>> lf;
326 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter(lf));
332 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
334 ASDCP::TimedText::MXFReader reader;
335 Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
336 auto r = reader.OpenRead (
file.string().c_str ());
337 Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
338 return !ASDCP_FAILURE (r);
343 SMPTESubtitleAsset::xml_as_string ()
const
346 auto root = doc.create_root_node (
"SubtitleReel");
347 root->set_namespace_declaration (subtitle_smpte_ns);
348 root->set_namespace_declaration (
"http://www.w3.org/2001/XMLSchema",
"xs");
351 root->add_child(
"Id")->add_child_text(
"urn:uuid:" + *
_xml_id);
353 if (_annotation_text) {
354 root->add_child(
"AnnotationText")->add_child_text(_annotation_text.get());
356 root->add_child(
"IssueDate")->add_child_text(_issue_date.
as_string(
true));
358 root->add_child(
"ReelNumber")->add_child_text(raw_convert<string>(_reel_number.get()));
361 root->add_child(
"Language")->add_child_text(
_language.get());
363 root->add_child(
"EditRate")->add_child_text(_edit_rate.as_string());
364 root->add_child(
"TimeCodeRate")->add_child_text(raw_convert<string>(_time_code_rate));
366 root->add_child(
"StartTime")->add_child_text(_start_time.get().as_string(Standard::SMPTE));
369 for (
auto i: _load_font_nodes) {
370 auto load_font = root->add_child(
"LoadFont");
371 load_font->add_child_text (
"urn:uuid:" + i->urn);
372 load_font->set_attribute (
"ID", i->id);
375 subtitles_as_xml (root->add_child(
"SubtitleList"), _time_code_rate, Standard::SMPTE);
377 return doc.write_to_string (
"UTF-8");
386 ASDCP::WriterInfo writer_info;
389 ASDCP::TimedText::TimedTextDescriptor descriptor;
390 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
391 descriptor.EncodingName =
"UTF-8";
395 for (
auto i: _load_font_nodes) {
397 while (j !=
_fonts.end() && j->load_id != i->id) {
401 ASDCP::TimedText::TimedTextResourceDescriptor res;
403 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
404 DCP_ASSERT (c == Kumu::UUID_Length);
405 res.Type = ASDCP::TimedText::MT_OPENTYPE;
406 descriptor.ResourceList.push_back (res);
413 auto si = dynamic_pointer_cast<SubtitleImage>(i);
415 ASDCP::TimedText::TimedTextResourceDescriptor res;
417 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
418 DCP_ASSERT (c == Kumu::UUID_Length);
419 res.Type = ASDCP::TimedText::MT_PNG;
420 descriptor.ResourceList.push_back (res);
424 descriptor.NamespaceName = subtitle_smpte_ns;
427 Kumu::hex2bin (
_xml_id->c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
428 DCP_ASSERT (c == Kumu::UUID_Length);
431 ASDCP::TimedText::MXFWriter writer;
435 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor,
_subtitles.size() * 90 + 16384);
436 if (ASDCP_FAILURE (r)) {
437 boost::throw_exception (
FileError (
"could not open subtitle MXF for writing", p.string(), r));
442 r = writer.WriteTimedTextResource (*
_raw_xml, enc.context(), enc.hmac());
443 if (ASDCP_FAILURE (r)) {
444 boost::throw_exception (
MXFFileError (
"could not write XML to timed text resource", p.string(), r));
449 for (
auto i: _load_font_nodes) {
451 while (j !=
_fonts.end() && j->load_id != i->id) {
455 ASDCP::TimedText::FrameBuffer buffer;
457 buffer.SetData (data_copy.data(), data_copy.
size());
458 buffer.Size (j->data.size());
459 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
460 if (ASDCP_FAILURE(r)) {
461 boost::throw_exception (
MXFFileError (
"could not write font to timed text resource", p.string(), r));
469 auto si = dynamic_pointer_cast<SubtitleImage>(i);
471 ASDCP::TimedText::FrameBuffer buffer;
472 buffer.SetData (si->png_image().data(), si->png_image().size());
473 buffer.Size (si->png_image().size());
474 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
475 if (ASDCP_FAILURE(r)) {
476 boost::throw_exception (
MXFFileError (
"could not write PNG data to timed text resource", p.string(), r));
487 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset,
EqualityOptions options, NoteHandler note)
const
489 if (!SubtitleAsset::equals (other_asset, options, note)) {
493 auto other = dynamic_pointer_cast<const SMPTESubtitleAsset>(other_asset);
495 note (NoteType::ERROR,
"Subtitles are in different standards");
499 auto i = _load_font_nodes.begin();
500 auto j = other->_load_font_nodes.begin();
502 while (i != _load_font_nodes.end ()) {
503 if (j == other->_load_font_nodes.end ()) {
504 note (NoteType::ERROR,
"<LoadFont> nodes differ");
508 if ((*i)->id != (*j)->id) {
509 note (NoteType::ERROR,
"<LoadFont> nodes differ");
518 note (NoteType::ERROR,
"Subtitle content title texts differ");
523 note (NoteType::ERROR, String::compose(
"Subtitle languages differ (`%1' vs `%2')",
_language.get_value_or(
"[none]"), other->_language.get_value_or(
"[none]")));
527 if (_annotation_text != other->_annotation_text) {
528 note (NoteType::ERROR,
"Subtitle annotation texts differ");
532 if (_issue_date != other->_issue_date) {
534 note (NoteType::NOTE,
"Subtitle issue dates differ");
536 note (NoteType::ERROR,
"Subtitle issue dates differ");
541 if (_reel_number != other->_reel_number) {
542 note (NoteType::ERROR,
"Subtitle reel numbers differ");
546 if (_edit_rate != other->_edit_rate) {
547 note (NoteType::ERROR,
"Subtitle edit rates differ");
551 if (_time_code_rate != other->_time_code_rate) {
552 note (NoteType::ERROR,
"Subtitle time code rates differ");
556 if (_start_time != other->_start_time) {
557 note (NoteType::ERROR,
"Subtitle start times differ");
566 SMPTESubtitleAsset::add_font (
string load_id,
dcp::ArrayData data)
568 string const uuid = make_uuid ();
569 _fonts.push_back (Font(load_id, uuid, data));
570 _load_font_nodes.push_back (make_shared<SMPTELoadFontNode>(load_id, uuid));
575 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
577 SubtitleAsset::add (s);
Class to hold an arbitrary block of data.
int size() const override
boost::optional< boost::filesystem::path > file() const
boost::optional< boost::filesystem::path > _file
An exception related to a file.
A fraction (i.e. a thing with an integer numerator and an integer denominator).
A key for decrypting/encrypting assets.
A representation of a local time (down to the second), including its offset from GMT (equivalent to x...
std::string as_string(bool with_millisecond=false) const
An exception related to an MXF file.
Parent for classes which represent MXF files.
virtual void set_key(Key)
boost::optional< Key > _key
std::string read_writer_info(ASDCP::WriterInfo const &)
void fill_writer_info(ASDCP::WriterInfo *w, std::string id) const
boost::optional< std::string > _key_id
boost::optional< Key > key() const
Any error that occurs when reading data from a DCP.
std::string _content_title_text
void write(boost::filesystem::path path) const override
void set_key(Key key) override
boost::optional< std::string > _xml_id
boost::optional< std::string > _language
int64_t _intrinsic_duration
boost::optional< std::string > _resource_id
A parent for classes representing a file containing subtitles.
std::vector< std::shared_ptr< Subtitle > > _subtitles
std::vector< Font > _fonts
void subtitles_as_xml(xmlpp::Element *root, int time_code_rate, Standard standard) const
boost::optional< std::string > _raw_xml
A representation of time within a DCP.
int64_t as_editable_units_ceil(int tcr_) const
Exceptions thrown by libdcp.
Namespace for everything in libdcp.
Methods for conversion to/from string.
SMPTESubtitleAsset class.
A class to describe what "equality" means for a particular test.
bool issue_dates_can_differ
Utility methods and classes.
Helpers for XML reading with libcxml.