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/>.
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 "font_node.h"
41 #include "exceptions.h"
43 #include "raw_convert.h"
44 #include "dcp_assert.h"
48 #include "compose.hpp"
49 #include <libxml++/libxml++.h>
50 #include <boost/foreach.hpp>
51 #include <boost/algorithm/string.hpp>
55 using std::stringstream;
59 using boost::shared_ptr;
61 using boost::is_any_of;
62 using boost::shared_array;
63 using boost::dynamic_pointer_cast;
66 SMPTESubtitleAsset::SMPTESubtitleAsset ()
67 : _intrinsic_duration (0)
69 , _time_code_rate (24)
74 /** Construct a SMPTESubtitleAsset by reading an MXF or XML file.
75 * @param file Filename.
77 SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file)
78 : SubtitleAsset (file)
80 shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
82 shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
83 Kumu::Result_t r = reader->OpenRead (file.string().c_str ());
85 if (!ASDCP_FAILURE (r)) {
87 reader->ReadTimedTextResource (s, 0, 0);
91 ASDCP::WriterInfo info;
92 reader->FillWriterInfo (info);
93 _id = read_writer_info (info);
97 xml->read_file (file);
98 _id = remove_urn_uuid (xml->string_child ("Id"));
99 } catch (cxml::Error& e) {
100 boost::throw_exception (
102 String::compose ("MXF failed with %1, XML failed with %2", file, static_cast<int> (r), e.what ())
108 _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
110 _content_title_text = xml->string_child ("ContentTitleText");
111 _annotation_text = xml->optional_string_child ("AnnotationText");
112 _issue_date = LocalTime (xml->string_child ("IssueDate"));
113 _reel_number = xml->optional_number_child<int> ("ReelNumber");
114 _language = xml->optional_string_child ("Language");
116 /* This is supposed to be two numbers, but a single number has been seen in the wild */
117 string const er = xml->string_child ("EditRate");
118 vector<string> er_parts;
119 split (er_parts, er, is_any_of (" "));
120 if (er_parts.size() == 1) {
121 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
122 } else if (er_parts.size() == 2) {
123 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
125 throw XMLError ("malformed EditRate " + er);
128 _time_code_rate = xml->number_child<int> ("TimeCodeRate");
129 if (xml->optional_string_child ("StartTime")) {
130 _start_time = Time (xml->string_child ("StartTime"), _time_code_rate);
133 shared_ptr<cxml::Node> subtitle_list = xml->optional_node_child ("SubtitleList");
135 list<shared_ptr<dcp::FontNode> > font_nodes;
136 BOOST_FOREACH (cxml::NodePtr const & i, subtitle_list->node_children ("Font")) {
137 font_nodes.push_back (shared_ptr<FontNode> (new FontNode (i, _time_code_rate, "ID")));
140 list<shared_ptr<dcp::SubtitleNode> > subtitle_nodes;
141 BOOST_FOREACH (cxml::NodePtr const & i, subtitle_list->node_children ("Subtitle")) {
142 subtitle_nodes.push_back (shared_ptr<SubtitleNode> (new SubtitleNode (i, _time_code_rate, "ID")));
145 parse_subtitles (xml, font_nodes, subtitle_nodes);
148 ASDCP::TimedText::TimedTextDescriptor descriptor;
149 reader->FillTimedTextDescriptor (descriptor);
154 ASDCP::TimedText::ResourceList_t::const_iterator i = descriptor.ResourceList.begin();
155 i != descriptor.ResourceList.end();
158 if (i->Type == ASDCP::TimedText::MT_OPENTYPE) {
159 ASDCP::TimedText::FrameBuffer buffer;
160 buffer.Capacity (10 * 1024 * 1024);
161 reader->ReadAncillaryResource (i->ResourceID, buffer);
164 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof (id));
166 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
167 memcpy (data.get(), buffer.RoData(), buffer.Size());
169 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = _load_font_nodes.begin ();
170 while (j != _load_font_nodes.end() && (*j)->urn != id) {
174 if (j != _load_font_nodes.end ()) {
175 _fonts.push_back (Font ((*j)->id, (*j)->urn, Data (data, buffer.Size ())));
180 /* Get intrinsic duration */
181 _intrinsic_duration = descriptor.ContainerDuration;
183 /* Guess intrinsic duration */
184 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
188 list<shared_ptr<LoadFontNode> >
189 SMPTESubtitleAsset::load_font_nodes () const
191 list<shared_ptr<LoadFontNode> > lf;
192 copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
197 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
199 ASDCP::TimedText::MXFReader reader;
200 Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
201 return !ASDCP_FAILURE (r);
205 SMPTESubtitleAsset::xml_as_string () const
208 xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel");
209 root->set_namespace_declaration ("http://www.smpte-ra.org/schemas/428-7/2010/DCST", "dcst");
210 root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
212 root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _id);
213 root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
214 if (_annotation_text) {
215 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
217 root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
219 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
222 root->add_child("Language", "dcst")->add_child_text (_language.get ());
224 root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
225 root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
227 root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string (SMPTE));
230 BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) {
231 xmlpp::Element* load_font = root->add_child("LoadFont", "dcst");
232 load_font->add_child_text ("urn:uuid:" + i->urn);
233 load_font->set_attribute ("ID", i->id);
236 subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, SMPTE);
238 return doc.write_to_string_formatted ("UTF-8");
241 /** Write this content to a MXF file */
243 SMPTESubtitleAsset::write (boost::filesystem::path p) const
245 ASDCP::WriterInfo writer_info;
246 fill_writer_info (&writer_info, _id, SMPTE);
248 ASDCP::TimedText::TimedTextDescriptor descriptor;
249 descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
250 descriptor.EncodingName = "UTF-8";
252 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
253 list<Font>::const_iterator j = _fonts.begin ();
254 while (j != _fonts.end() && j->load_id != i->id) {
257 if (j != _fonts.end ()) {
258 ASDCP::TimedText::TimedTextResourceDescriptor res;
260 Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
261 DCP_ASSERT (c == Kumu::UUID_Length);
262 res.Type = ASDCP::TimedText::MT_OPENTYPE;
263 descriptor.ResourceList.push_back (res);
267 descriptor.NamespaceName = "dcst";
268 memcpy (descriptor.AssetID, writer_info.AssetUUID, ASDCP::UUIDlen);
269 descriptor.ContainerDuration = _intrinsic_duration;
271 ASDCP::TimedText::MXFWriter writer;
272 ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor);
273 if (ASDCP_FAILURE (r)) {
274 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
277 /* XXX: no encryption */
278 r = writer.WriteTimedTextResource (xml_as_string ());
279 if (ASDCP_FAILURE (r)) {
280 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
283 BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
284 list<Font>::const_iterator j = _fonts.begin ();
285 while (j != _fonts.end() && j->load_id != i->id) {
288 if (j != _fonts.end ()) {
289 ASDCP::TimedText::FrameBuffer buffer;
290 buffer.SetData (j->data.data().get(), j->data.size());
291 buffer.Size (j->data.size());
292 r = writer.WriteAncillaryResource (buffer);
293 if (ASDCP_FAILURE (r)) {
294 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
305 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
307 if (!SubtitleAsset::equals (other_asset, options, note)) {
311 shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
313 note (DCP_ERROR, "Subtitles are in different standards");
317 list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
318 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
320 while (i != _load_font_nodes.end ()) {
321 if (j == other->_load_font_nodes.end ()) {
322 note (DCP_ERROR, "<LoadFont> nodes differ");
326 if ((*i)->id != (*j)->id) {
327 note (DCP_ERROR, "<LoadFont> nodes differ");
335 if (_content_title_text != other->_content_title_text) {
336 note (DCP_ERROR, "Subtitle content title texts differ");
340 if (_language != other->_language) {
341 note (DCP_ERROR, "Subtitle languages differ");
345 if (_annotation_text != other->_annotation_text) {
346 note (DCP_ERROR, "Subtitle annotation texts differ");
350 if (_issue_date != other->_issue_date) {
351 if (options.issue_dates_can_differ) {
352 note (DCP_NOTE, "Subtitle issue dates differ");
354 note (DCP_ERROR, "Subtitle issue dates differ");
359 if (_reel_number != other->_reel_number) {
360 note (DCP_ERROR, "Subtitle reel numbers differ");
364 if (_edit_rate != other->_edit_rate) {
365 note (DCP_ERROR, "Subtitle edit rates differ");
369 if (_time_code_rate != other->_time_code_rate) {
370 note (DCP_ERROR, "Subtitle time code rates differ");
374 if (_start_time != other->_start_time) {
375 note (DCP_ERROR, "Subtitle start times differ");
383 SMPTESubtitleAsset::add_font (string load_id, boost::filesystem::path file)
385 string const uuid = make_uuid ();
386 _fonts.push_back (Font (load_id, uuid, file));
387 _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
391 SMPTESubtitleAsset::add (dcp::SubtitleString s)
393 SubtitleAsset::add (s);
394 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);