2 Copyright (C) 2012-2015 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/>.
20 /** @file src/smpte_subtitle_asset.cc
21 * @brief SMPTESubtitleAsset class.
24 #include "smpte_subtitle_asset.h"
25 #include "smpte_load_font_node.h"
26 #include "font_node.h"
27 #include "exceptions.h"
29 #include "raw_convert.h"
30 #include "dcp_assert.h"
34 #include "compose.hpp"
35 #include <libxml++/libxml++.h>
36 #include <boost/foreach.hpp>
37 #include <boost/algorithm/string.hpp>
41 using std::stringstream;
45 using boost::shared_ptr;
47 using boost::is_any_of;
48 using boost::shared_array;
49 using boost::dynamic_pointer_cast;
52 SMPTESubtitleAsset::SMPTESubtitleAsset ()
53 : _intrinsic_duration (0)
55 , _time_code_rate (24)
60 /** Construct a SMPTESubtitleAsset by reading an MXF or XML file.
61 * @param file Filename.
63 SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file)
64 : SubtitleAsset (file)
66 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
68 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
69 Kumu::Result_t r = reader->OpenRead (file.string().c_str ());
71 if (!ASDCP_FAILURE (r)) {
73 reader->ReadTimedTextResource (s, 0, 0);
77 ASDCP::WriterInfo info;
78 reader->FillWriterInfo (info);
79 _id = read_writer_info (info);
83 xml->read_file (file);
84 _id = remove_urn_uuid (xml->string_child ("Id"));
85 } catch (cxml::Error& e) {
86 boost::throw_exception (
88 String::compose ("could not read subtitles from %1; MXF failed with %2, XML failed with %3", file, static_cast<int> (r), e.what ())
94 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
96 _content_title_text = xml->string_child ("ContentTitleText");
97 _annotation_text = xml->optional_string_child ("AnnotationText");
98 _issue_date = LocalTime (xml->string_child ("IssueDate"));
99 _reel_number = xml->optional_number_child<int> ("ReelNumber");
100 _language = xml->optional_string_child ("Language");
102 /* This is supposed to be two numbers, but a single number has been seen in the wild */
103 string const er = xml->string_child ("EditRate");
104 vector<string> er_parts;
105 split (er_parts, er, is_any_of (" "));
106 if (er_parts.size() == 1) {
107 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
108 } else if (er_parts.size() == 2) {
109 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
111 throw XMLError ("malformed EditRate " + er);
114 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
115 if (xml->optional_string_child ("StartTime")) {
116 _start_time = Time (xml->string_child ("StartTime"), _time_code_rate);
119 shared_ptr<cxml::Node> subtitle_list = xml->optional_node_child ("SubtitleList");
121 list<shared_ptr<dcp::FontNode> > font_nodes;
122 BOOST_FOREACH (cxml::NodePtr const & i, subtitle_list->node_children ("Font")) {
123 font_nodes.push_back (shared_ptr<FontNode> (new FontNode (i, _time_code_rate, "ID")));
126 list<shared_ptr<dcp::SubtitleNode> > subtitle_nodes;
127 BOOST_FOREACH (cxml::NodePtr const & i, subtitle_list->node_children ("Subtitle")) {
128 subtitle_nodes.push_back (shared_ptr<SubtitleNode> (new SubtitleNode (i, _time_code_rate, "ID")));
131 parse_subtitles (xml, font_nodes, subtitle_nodes);
134 ASDCP::TimedText::TimedTextDescriptor descriptor;
135 reader->FillTimedTextDescriptor (descriptor);
140 ASDCP::TimedText::ResourceList_t::const_iterator i = descriptor.ResourceList.begin();
141 i != descriptor.ResourceList.end();
144 if (i->Type == ASDCP::TimedText::MT_OPENTYPE) {
145 ASDCP::TimedText::FrameBuffer buffer;
146 buffer.Capacity (10 * 1024 * 1024);
147 reader->ReadAncillaryResource (i->ResourceID, buffer);
150 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof (id));
152 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
153 memcpy (data.get(), buffer.RoData(), buffer.Size());
155 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = _load_font_nodes.begin ();
156 while (j != _load_font_nodes.end() && (*j)->urn != id) {
160 if (j != _load_font_nodes.end ()) {
161 _fonts.push_back (Font ((*j)->id, (*j)->urn, Data (data, buffer.Size ())));
166 /* Get intrinsic duration */
167 _intrinsic_duration = descriptor.ContainerDuration;
169 /* Guess intrinsic duration */
170 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
174 list<shared_ptr<LoadFontNode> >
175 SMPTESubtitleAsset::load_font_nodes () const
177 list<shared_ptr<LoadFontNode> > lf;
178 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
183 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
185 ASDCP::TimedText::MXFReader reader;
186 Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
187 return !ASDCP_FAILURE (r);
191 SMPTESubtitleAsset::xml_as_string () const
194 xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel");
195 root->set_namespace_declaration ("http://www.smpte-ra.org/schemas/428-7/2010/DCST", "dcst");
196 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
198 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _id);
199 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
200 if (_annotation_text) {
201 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
203 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
205 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
208 root->add_child("Language", "dcst")->add_child_text (_language.get ());
210 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
211 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
213 root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string (SMPTE));
216 BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) {
217 xmlpp::Element* load_font = root->add_child("LoadFont", "dcst");
218 load_font->add_child_text ("urn:uuid:" + i->urn);
219 load_font->set_attribute ("ID", i->id);
222 subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, SMPTE);
224 return doc.write_to_string_formatted ("UTF-8");
227 /** Write this content to a MXF file */
229 SMPTESubtitleAsset::write (boost::filesystem::path p) const
231 ASDCP::WriterInfo writer_info;
232 fill_writer_info (&writer_info, _id, SMPTE);
234 ASDCP::TimedText::TimedTextDescriptor descriptor;
235 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
236 descriptor.EncodingName = "UTF-8";
238 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
239 list<Font>::const_iterator j = _fonts.begin ();
240 while (j != _fonts.end() && j->load_id != i->id) {
243 if (j != _fonts.end ()) {
244 ASDCP::TimedText::TimedTextResourceDescriptor res;
246 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
247 DCP_ASSERT (c == Kumu::UUID_Length);
248 res.Type = ASDCP::TimedText::MT_OPENTYPE;
249 descriptor.ResourceList.push_back (res);
253 descriptor.NamespaceName = "dcst";
254 memcpy (descriptor.AssetID, writer_info.AssetUUID, ASDCP::UUIDlen);
255 descriptor.ContainerDuration = _intrinsic_duration;
257 ASDCP::TimedText::MXFWriter writer;
258 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor);
259 if (ASDCP_FAILURE (r)) {
260 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
263 /* XXX: no encryption */
264 r = writer.WriteTimedTextResource (xml_as_string ());
265 if (ASDCP_FAILURE (r)) {
266 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
269 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
270 list<Font>::const_iterator j = _fonts.begin ();
271 while (j != _fonts.end() && j->load_id != i->id) {
274 if (j != _fonts.end ()) {
275 ASDCP::TimedText::FrameBuffer buffer;
276 buffer.SetData (j->data.data().get(), j->data.size());
277 buffer.Size (j->data.size());
278 r = writer.WriteAncillaryResource (buffer);
279 if (ASDCP_FAILURE (r)) {
280 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
291 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
293 if (!SubtitleAsset::equals (other_asset, options, note)) {
297 shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
299 note (DCP_ERROR, "Subtitles are in different standards");
303 list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
304 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
306 while (i != _load_font_nodes.end ()) {
307 if (j == other->_load_font_nodes.end ()) {
308 note (DCP_ERROR, "<LoadFont> nodes differ");
312 if ((*i)->id != (*j)->id) {
313 note (DCP_ERROR, "<LoadFont> nodes differ");
321 if (_content_title_text != other->_content_title_text) {
322 note (DCP_ERROR, "Subtitle content title texts differ");
326 if (_language != other->_language) {
327 note (DCP_ERROR, "Subtitle languages differ");
331 if (_annotation_text != other->_annotation_text) {
332 note (DCP_ERROR, "Subtitle annotation texts differ");
336 if (_issue_date != other->_issue_date) {
337 if (options.issue_dates_can_differ) {
338 note (DCP_NOTE, "Subtitle issue dates differ");
340 note (DCP_ERROR, "Subtitle issue dates differ");
345 if (_reel_number != other->_reel_number) {
346 note (DCP_ERROR, "Subtitle reel numbers differ");
350 if (_edit_rate != other->_edit_rate) {
351 note (DCP_ERROR, "Subtitle edit rates differ");
355 if (_time_code_rate != other->_time_code_rate) {
356 note (DCP_ERROR, "Subtitle time code rates differ");
360 if (_start_time != other->_start_time) {
361 note (DCP_ERROR, "Subtitle start times differ");
369 SMPTESubtitleAsset::add_font (string load_id, boost::filesystem::path file)
371 string const uuid = make_uuid ();
372 _fonts.push_back (Font (load_id, uuid, file));
373 _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
377 SMPTESubtitleAsset::add (dcp::SubtitleString s)
379 SubtitleAsset::add (s);
380 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);