Merge branch '1.0' of git.carlh.net:git/libdcp into 1.0
[libdcp.git] / src / smpte_subtitle_asset.cc
1 /*
2     Copyright (C) 2012-2015 Carl Hetherington <cth@carlh.net>
3
4     This file is part of libdcp.
5
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.
10
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.
15
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/>.
18
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
23     including the two.
24
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.
32 */
33
34 /** @file  src/smpte_subtitle_asset.cc
35  *  @brief SMPTESubtitleAsset class.
36  */
37
38 #include "smpte_subtitle_asset.h"
39 #include "smpte_load_font_node.h"
40 #include "font_node.h"
41 #include "exceptions.h"
42 #include "xml.h"
43 #include "raw_convert.h"
44 #include "dcp_assert.h"
45 #include "util.h"
46 #include "AS_DCP.h"
47 #include "KM_util.h"
48 #include "compose.hpp"
49 #include <libxml++/libxml++.h>
50 #include <boost/foreach.hpp>
51 #include <boost/algorithm/string.hpp>
52
53 using std::string;
54 using std::list;
55 using std::stringstream;
56 using std::cout;
57 using std::vector;
58 using std::map;
59 using boost::shared_ptr;
60 using boost::split;
61 using boost::is_any_of;
62 using boost::shared_array;
63 using boost::dynamic_pointer_cast;
64 using namespace dcp;
65
66 SMPTESubtitleAsset::SMPTESubtitleAsset ()
67         : _intrinsic_duration (0)
68         , _edit_rate (24, 1)
69         , _time_code_rate (24)
70 {
71
72 }
73
74 /** Construct a SMPTESubtitleAsset by reading an MXF or XML file.
75  *  @param file Filename.
76  */
77 SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file)
78         : SubtitleAsset (file)
79 {
80         shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
81
82         shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
83         Kumu::Result_t r = reader->OpenRead (file.string().c_str ());
84
85         if (!ASDCP_FAILURE (r)) {
86                 string s;
87                 reader->ReadTimedTextResource (s, 0, 0);
88                 stringstream t;
89                 t << s;
90                 xml->read_stream (t);
91                 ASDCP::WriterInfo info;
92                 reader->FillWriterInfo (info);
93                 _id = read_writer_info (info);
94         } else {
95                 reader.reset ();
96                 try {
97                         xml->read_file (file);
98                         _id = remove_urn_uuid (xml->string_child ("Id"));
99                 } catch (cxml::Error& e) {
100                         boost::throw_exception (
101                                 DCPReadError (
102                                         String::compose ("MXF failed with %1, XML failed with %2", file, static_cast<int> (r), e.what ())
103                                         )
104                                 );
105                 }
106         }
107
108         _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
109
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");
115
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]));
124         } else {
125                 throw XMLError ("malformed EditRate " + er);
126         }
127
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);
131         }
132
133         shared_ptr<cxml::Node> subtitle_list = xml->optional_node_child ("SubtitleList");
134
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")));
138         }
139
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")));
143         }
144
145         parse_subtitles (xml, font_nodes, subtitle_nodes);
146
147         if (reader) {
148                 ASDCP::TimedText::TimedTextDescriptor descriptor;
149                 reader->FillTimedTextDescriptor (descriptor);
150
151                 /* Load fonts */
152
153                 for (
154                         ASDCP::TimedText::ResourceList_t::const_iterator i = descriptor.ResourceList.begin();
155                         i != descriptor.ResourceList.end();
156                         ++i) {
157
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);
162
163                                 char id[64];
164                                 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof (id));
165
166                                 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
167                                 memcpy (data.get(), buffer.RoData(), buffer.Size());
168
169                                 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = _load_font_nodes.begin ();
170                                 while (j != _load_font_nodes.end() && (*j)->urn != id) {
171                                         ++j;
172                                 }
173
174                                 if (j != _load_font_nodes.end ()) {
175                                         _fonts.push_back (Font ((*j)->id, (*j)->urn, Data (data, buffer.Size ())));
176                                 }
177                         }
178                 }
179
180                 /* Get intrinsic duration */
181                 _intrinsic_duration = descriptor.ContainerDuration;
182         } else {
183                 /* Guess intrinsic duration */
184                 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
185         }
186 }
187
188 list<shared_ptr<LoadFontNode> >
189 SMPTESubtitleAsset::load_font_nodes () const
190 {
191         list<shared_ptr<LoadFontNode> > lf;
192         copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
193         return lf;
194 }
195
196 bool
197 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
198 {
199         ASDCP::TimedText::MXFReader reader;
200         Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
201         return !ASDCP_FAILURE (r);
202 }
203
204 string
205 SMPTESubtitleAsset::xml_as_string () const
206 {
207         xmlpp::Document doc;
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");
211
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 ());
216         }
217         root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
218         if (_reel_number) {
219                 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
220         }
221         if (_language) {
222                 root->add_child("Language", "dcst")->add_child_text (_language.get ());
223         }
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));
226         if (_start_time) {
227                 root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string (SMPTE));
228         }
229
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);
234         }
235
236         subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, SMPTE);
237
238         return doc.write_to_string_formatted ("UTF-8");
239 }
240
241 /** Write this content to a MXF file */
242 void
243 SMPTESubtitleAsset::write (boost::filesystem::path p) const
244 {
245         ASDCP::WriterInfo writer_info;
246         fill_writer_info (&writer_info, _id, SMPTE);
247
248         ASDCP::TimedText::TimedTextDescriptor descriptor;
249         descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
250         descriptor.EncodingName = "UTF-8";
251
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) {
255                         ++j;
256                 }
257                 if (j != _fonts.end ()) {
258                         ASDCP::TimedText::TimedTextResourceDescriptor res;
259                         unsigned int c;
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);
264                 }
265         }
266
267         descriptor.NamespaceName = "dcst";
268         memcpy (descriptor.AssetID, writer_info.AssetUUID, ASDCP::UUIDlen);
269         descriptor.ContainerDuration = _intrinsic_duration;
270
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));
275         }
276
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));
281         }
282
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) {
286                         ++j;
287                 }
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));
295                         }
296                 }
297         }
298
299         writer.Finalize ();
300
301         _file = p;
302 }
303
304 bool
305 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
306 {
307         if (!SubtitleAsset::equals (other_asset, options, note)) {
308                 return false;
309         }
310
311         shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
312         if (!other) {
313                 note (DCP_ERROR, "Subtitles are in different standards");
314                 return false;
315         }
316
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 ();
319
320         while (i != _load_font_nodes.end ()) {
321                 if (j == other->_load_font_nodes.end ()) {
322                         note (DCP_ERROR, "<LoadFont> nodes differ");
323                         return false;
324                 }
325
326                 if ((*i)->id != (*j)->id) {
327                         note (DCP_ERROR, "<LoadFont> nodes differ");
328                         return false;
329                 }
330
331                 ++i;
332                 ++j;
333         }
334
335         if (_content_title_text != other->_content_title_text) {
336                 note (DCP_ERROR, "Subtitle content title texts differ");
337                 return false;
338         }
339
340         if (_language != other->_language) {
341                 note (DCP_ERROR, "Subtitle languages differ");
342                 return false;
343         }
344
345         if (_annotation_text != other->_annotation_text) {
346                 note (DCP_ERROR, "Subtitle annotation texts differ");
347                 return false;
348         }
349
350         if (_issue_date != other->_issue_date) {
351                 if (options.issue_dates_can_differ) {
352                         note (DCP_NOTE, "Subtitle issue dates differ");
353                 } else {
354                         note (DCP_ERROR, "Subtitle issue dates differ");
355                         return false;
356                 }
357         }
358
359         if (_reel_number != other->_reel_number) {
360                 note (DCP_ERROR, "Subtitle reel numbers differ");
361                 return false;
362         }
363
364         if (_edit_rate != other->_edit_rate) {
365                 note (DCP_ERROR, "Subtitle edit rates differ");
366                 return false;
367         }
368
369         if (_time_code_rate != other->_time_code_rate) {
370                 note (DCP_ERROR, "Subtitle time code rates differ");
371                 return false;
372         }
373
374         if (_start_time != other->_start_time) {
375                 note (DCP_ERROR, "Subtitle start times differ");
376                 return false;
377         }
378
379         return true;
380 }
381
382 void
383 SMPTESubtitleAsset::add_font (string load_id, boost::filesystem::path file)
384 {
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)));
388 }
389
390 void
391 SMPTESubtitleAsset::add (dcp::SubtitleString s)
392 {
393         SubtitleAsset::add (s);
394         _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
395 }