40 #include "compose.hpp"
45 #include "filesystem.h"
53 LIBDCP_DISABLE_WARNINGS
54 #include <asdcp/AS_DCP.h>
55 #include <asdcp/KM_util.h>
56 #include <asdcp/KM_log.h>
57 #include <libxml++/libxml++.h>
58 LIBDCP_ENABLE_WARNINGS
59 #include <boost/algorithm/string.hpp>
66 using std::shared_ptr;
67 using std::dynamic_pointer_cast;
68 using std::make_shared;
70 using boost::is_any_of;
71 using boost::shared_array;
72 using boost::optional;
73 using boost::starts_with;
77 static string const subtitle_smpte_ns_2007 =
"http://www.smpte-ra.org/schemas/428-7/2007/DCST";
78 static string const subtitle_smpte_ns_2010 =
"http://www.smpte-ra.org/schemas/428-7/2010/DCST";
79 static string const subtitle_smpte_ns_2014 =
"http://www.smpte-ra.org/schemas/428-7/2014/DCST";
82 SMPTETextAsset::SMPTETextAsset(SubtitleStandard standard)
83 :
MXF(Standard::SMPTE)
85 , _time_code_rate (24)
86 , _subtitle_standard(standard)
87 , _xml_id (make_uuid())
93 SMPTETextAsset::SMPTETextAsset(boost::filesystem::path file)
96 auto xml = make_shared<cxml::Document>(
"SubtitleReel");
98 Kumu::FileReaderFactory factory;
99 auto reader = make_shared<ASDCP::TimedText::MXFReader>(factory);
100 auto r = Kumu::RESULT_OK;
103 r = reader->OpenRead(dcp::filesystem::fix_long_path(*_file).string().c_str());
105 if (!ASDCP_FAILURE(r)) {
107 ASDCP::WriterInfo info;
108 reader->FillWriterInfo (info);
113 reader->ReadTimedTextResource (xml_string);
115 xml->read_string (xml_string);
117 read_mxf_descriptor (reader);
118 read_mxf_resources(reader, std::make_shared<DecryptionContext>(optional<Key>(), Standard::SMPTE));
120 read_mxf_descriptor (reader);
126 xml = make_shared<cxml::Document>(
"SubtitleReel");
127 xml->read_file(dcp::filesystem::fix_long_path(
file));
129 }
catch (cxml::Error& e) {
130 boost::throw_exception (
133 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
134 file,
static_cast<int>(r), e.what()
144 auto im = dynamic_pointer_cast<TextImage>(i);
145 if (im && im->png_image().size() == 0) {
147 auto p =
file.parent_path() / String::compose(
"%1.png", im->id());
148 if (filesystem::is_regular_file(p)) {
149 im->read_png_file (p);
150 }
else if (starts_with (im->id(),
"urn:uuid:")) {
151 p =
file.parent_path() / String::compose(
"%1.png", remove_urn_uuid(im->id()));
152 if (filesystem::is_regular_file(p)) {
153 im->read_png_file (p);
158 _standard = Standard::SMPTE;
163 auto im = dynamic_pointer_cast<TextImage>(i);
164 if (im && im->png_image().size() == 0) {
172 SMPTETextAsset::parse_xml(shared_ptr<cxml::Document> xml)
174 if (xml->namespace_uri() == subtitle_smpte_ns_2007) {
176 }
else if (xml->namespace_uri() == subtitle_smpte_ns_2010) {
178 }
else if (xml->namespace_uri() == subtitle_smpte_ns_2014) {
181 throw XMLError(
"Unrecognised subtitle namespace " + xml->namespace_uri());
183 _xml_id = remove_urn_uuid(xml->string_child(
"Id"));
184 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml,
"LoadFont");
187 _annotation_text = xml->optional_string_child (
"AnnotationText");
188 _issue_date =
LocalTime (xml->string_child (
"IssueDate"));
189 _reel_number = xml->optional_number_child<
int> (
"ReelNumber");
190 _language = xml->optional_string_child (
"Language");
193 auto const er = xml->string_child (
"EditRate");
194 vector<string> er_parts;
195 split (er_parts, er, is_any_of (
" "));
196 if (er_parts.size() == 1) {
197 _edit_rate =
Fraction (raw_convert<int> (er_parts[0]), 1);
198 }
else if (er_parts.size() == 2) {
199 _edit_rate =
Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
201 throw XMLError (
"malformed EditRate " + er);
204 _time_code_rate = xml->number_child<
int> (
"TimeCodeRate");
205 if (xml->optional_string_child (
"StartTime")) {
206 _start_time =
Time (xml->string_child(
"StartTime"), _time_code_rate);
211 vector<ParseState> ps;
212 for (
auto i: xml->node()->get_children()) {
213 auto const e =
dynamic_cast<xmlpp::Element
const *
>(i);
214 if (e && e->get_name() ==
"SubtitleList") {
215 parse_texts(e, ps, _time_code_rate, Standard::SMPTE);
225 SMPTETextAsset::read_mxf_resources(shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
227 ASDCP::TimedText::TimedTextDescriptor descriptor;
228 reader->FillTimedTextDescriptor (descriptor);
233 auto i = descriptor.ResourceList.begin();
234 i != descriptor.ResourceList.end();
237 ASDCP::TimedText::FrameBuffer buffer;
238 buffer.Capacity(32 * 1024 * 1024);
239 auto const result = reader->ReadAncillaryResource(i->ResourceID, buffer, dec->context(), dec->hmac());
240 if (ASDCP_FAILURE(result)) {
242 case ASDCP::TimedText::MT_OPENTYPE:
243 throw ReadError(String::compose(
"Could not read font from MXF file (%1)",
static_cast<int>(result)));
244 case ASDCP::TimedText::MT_PNG:
245 throw ReadError(String::compose(
"Could not read subtitle image from MXF file (%1)",
static_cast<int>(result)));
247 throw ReadError(String::compose(
"Could not read resource from MXF file (%1)",
static_cast<int>(result)));
252 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen,
id,
sizeof(
id));
255 case ASDCP::TimedText::MT_OPENTYPE:
257 auto j = _load_font_nodes.begin();
258 while (j != _load_font_nodes.end() && (*j)->urn !=
id) {
262 if (j != _load_font_nodes.end ()) {
263 _fonts.push_back(Font((*j)->id, (*j)->urn,
ArrayData(buffer.RoData(), buffer.Size())));
267 case ASDCP::TimedText::MT_PNG:
270 while (j !=
_texts.end() && ((!dynamic_pointer_cast<TextImage>(*j)) || dynamic_pointer_cast<TextImage>(*j)->id() !=
id)) {
275 dynamic_pointer_cast<TextImage>(*j)->set_png_image(
ArrayData(buffer.RoData(), buffer.Size()));
287 SMPTETextAsset::read_mxf_descriptor(shared_ptr<ASDCP::TimedText::MXFReader> reader)
289 ASDCP::TimedText::TimedTextDescriptor descriptor;
290 reader->FillTimedTextDescriptor (descriptor);
298 Kumu::bin2UUIDhex (descriptor.AssetID, ASDCP::UUIDlen,
id,
sizeof(
id));
309 auto const had_key =
static_cast<bool>(
_key);
310 auto const had_key_id =
static_cast<bool>(
_key_id);
314 if (!had_key_id || !
_file || had_key) {
324 Kumu::FileReaderFactory factory;
325 auto reader = make_shared<ASDCP::TimedText::MXFReader>(factory);
326 auto r = reader->OpenRead(dcp::filesystem::fix_long_path(*_file).string().c_str());
327 if (ASDCP_FAILURE (r)) {
328 boost::throw_exception (
330 String::compose (
"Could not read encrypted subtitle MXF (%1)",
static_cast<int> (r))
335 auto dec = make_shared<DecryptionContext>(
key, Standard::SMPTE);
337 reader->ReadTimedTextResource (xml_string, dec->context(), dec->hmac());
339 auto xml = make_shared<cxml::Document>(
"SubtitleReel");
340 xml->read_string (xml_string);
342 read_mxf_descriptor(reader);
343 read_mxf_resources (reader, dec);
347 vector<shared_ptr<LoadFontNode>>
348 SMPTETextAsset::load_font_nodes()
const
350 vector<shared_ptr<LoadFontNode>> lf;
351 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter(lf));
357 SMPTETextAsset::valid_mxf(boost::filesystem::path file)
359 Kumu::FileReaderFactory factory;
360 ASDCP::TimedText::MXFReader reader(factory);
361 Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
362 auto r = reader.OpenRead(dcp::filesystem::fix_long_path(
file).
string().c_str());
363 Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
364 return !ASDCP_FAILURE (r);
369 SMPTETextAsset::xml_as_string()
const
372 auto root = doc.create_root_node (
"SubtitleReel");
375 cxml::add_text_child(root,
"Id",
"urn:uuid:" + *
_xml_id);
377 if (_annotation_text) {
378 cxml::add_text_child(root,
"AnnotationText", _annotation_text.get());
380 cxml::add_text_child(root,
"IssueDate", _issue_date.
as_string(
false,
false));
382 cxml::add_text_child(root,
"ReelNumber", raw_convert<string>(_reel_number.get()));
385 cxml::add_text_child(root,
"Language",
_language.get());
387 cxml::add_text_child(root,
"EditRate", _edit_rate.as_string());
388 cxml::add_text_child(root,
"TimeCodeRate", raw_convert<string>(_time_code_rate));
390 cxml::add_text_child(root,
"StartTime", _start_time.get().as_string(Standard::SMPTE));
393 for (
auto i: _load_font_nodes) {
394 auto load_font = cxml::add_child(root,
"LoadFont");
395 load_font->add_child_text (
"urn:uuid:" + i->urn);
396 load_font->set_attribute (
"ID", i->id);
399 texts_as_xml(cxml::add_child(root,
"SubtitleList"), _time_code_rate, Standard::SMPTE);
401 return format_xml(doc, std::make_pair(
string{}, schema_namespace()));
410 ASDCP::WriterInfo writer_info;
413 ASDCP::TimedText::TimedTextDescriptor descriptor;
414 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
415 descriptor.EncodingName =
"UTF-8";
419 for (
auto i: _load_font_nodes) {
421 while (j !=
_fonts.end() && j->load_id != i->id) {
425 ASDCP::TimedText::TimedTextResourceDescriptor res;
427 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
428 DCP_ASSERT (c == Kumu::UUID_Length);
429 res.Type = ASDCP::TimedText::MT_OPENTYPE;
430 descriptor.ResourceList.push_back (res);
437 auto si = dynamic_pointer_cast<TextImage>(i);
439 ASDCP::TimedText::TimedTextResourceDescriptor res;
441 Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
442 DCP_ASSERT (c == Kumu::UUID_Length);
443 res.Type = ASDCP::TimedText::MT_PNG;
444 descriptor.ResourceList.push_back (res);
448 descriptor.NamespaceName = schema_namespace();
451 Kumu::hex2bin (
_xml_id->c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
452 DCP_ASSERT (c == Kumu::UUID_Length);
455 ASDCP::TimedText::MXFWriter writer;
459 ASDCP::Result_t r = writer.OpenWrite(dcp::filesystem::fix_long_path(p).
string().c_str(), writer_info, descriptor,
_texts.size() * 90 + 16384);
460 if (ASDCP_FAILURE (r)) {
461 boost::throw_exception (
FileError (
"could not open subtitle MXF for writing", p.string(), r));
466 r = writer.WriteTimedTextResource (*
_raw_xml, enc.context(), enc.hmac());
467 if (ASDCP_FAILURE (r)) {
468 boost::throw_exception (
MXFFileError (
"could not write XML to timed text resource", p.string(), r));
473 for (
auto i: _load_font_nodes) {
475 while (j !=
_fonts.end() && j->load_id != i->id) {
479 ASDCP::TimedText::FrameBuffer buffer;
481 buffer.SetData (data_copy.data(), data_copy.
size());
482 buffer.Size (j->data.size());
483 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
484 if (ASDCP_FAILURE(r)) {
485 boost::throw_exception (
MXFFileError (
"could not write font to timed text resource", p.string(), r));
493 if (
auto si = dynamic_pointer_cast<TextImage>(i)) {
494 ASDCP::TimedText::FrameBuffer buffer;
495 buffer.SetData (si->png_image().data(), si->png_image().size());
496 buffer.Size (si->png_image().size());
497 r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
498 if (ASDCP_FAILURE(r)) {
499 boost::throw_exception (
MXFFileError (
"could not write PNG data to timed text resource", p.string(), r));
510 SMPTETextAsset::equals(shared_ptr<const Asset> other_asset,
EqualityOptions const& options, NoteHandler note)
const
512 if (!TextAsset::equals (other_asset, options, note)) {
516 auto other = dynamic_pointer_cast<const SMPTETextAsset>(other_asset);
518 note (NoteType::ERROR,
"Subtitles/captions are in different standards");
522 auto i = _load_font_nodes.begin();
523 auto j = other->_load_font_nodes.begin();
525 while (i != _load_font_nodes.end ()) {
526 if (j == other->_load_font_nodes.end ()) {
527 note (NoteType::ERROR,
"<LoadFont> nodes differ");
531 if ((*i)->id != (*j)->id) {
532 note (NoteType::ERROR,
"<LoadFont> nodes differ");
541 note (NoteType::ERROR,
"Subtitle/caption content title texts differ");
546 note (NoteType::ERROR, String::compose(
"Subtitle/caption languages differ (`%1' vs `%2')",
_language.get_value_or(
"[none]"), other->_language.get_value_or(
"[none]")));
550 if (_annotation_text != other->_annotation_text) {
551 note (NoteType::ERROR,
"Subtitle/caption annotation texts differ");
555 if (_issue_date != other->_issue_date) {
557 note (NoteType::NOTE,
"Subtitle/caption issue dates differ");
559 note (NoteType::ERROR,
"Subtitle/caption issue dates differ");
564 if (_reel_number != other->_reel_number) {
565 note (NoteType::ERROR,
"Subtitle/caption reel numbers differ");
569 if (_edit_rate != other->_edit_rate) {
570 note (NoteType::ERROR,
"Subtitle/caption edit rates differ");
574 if (_time_code_rate != other->_time_code_rate) {
575 note (NoteType::ERROR,
"Subtitle/caption time code rates differ");
579 if (_start_time != other->_start_time) {
580 note (NoteType::ERROR,
"Subtitle/caption start times differ");
591 string const uuid = make_uuid ();
592 _fonts.push_back (Font(load_id, uuid, data));
593 _load_font_nodes.push_back (make_shared<SMPTELoadFontNode>(load_id, uuid));
598 SMPTETextAsset::add(shared_ptr<Text> s)
606 SMPTETextAsset::schema_namespace()
const
609 case SubtitleStandard::SMPTE_2007:
610 return subtitle_smpte_ns_2007;
611 case SubtitleStandard::SMPTE_2010:
612 return subtitle_smpte_ns_2010;
613 case SubtitleStandard::SMPTE_2014:
614 return subtitle_smpte_ns_2014;
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
A class to describe what "equality" means for a particular test.
bool issue_dates_can_differ
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, bool with_timezone=true) 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.
int64_t _intrinsic_duration
boost::optional< std::string > _language
std::string _content_title_text
SubtitleStandard _subtitle_standard
boost::optional< std::string > _xml_id
void write(boost::filesystem::path path) const override
void set_key(Key key) override
boost::optional< std::string > _resource_id
A parent for classes representing a file containing subtitles or captions.
boost::optional< std::string > _raw_xml
void texts_as_xml(xmlpp::Element *root, int time_code_rate, Standard standard) const
std::vector< std::shared_ptr< Text > > _texts
std::vector< Font > _fonts
static std::string format_xml(xmlpp::Document const &document, boost::optional< std::pair< std::string, std::string >> xml_namespace)
A representation of time within a DCP.
int64_t as_editable_units_ceil(int tcr_) const
Class to describe what equality means when calling Asset::equals().
Exceptions thrown by libdcp.
Namespace for everything in libdcp.
Methods for conversion to/from string.
Utility methods and classes.
Helpers for XML reading with libcxml.