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 "compose.hpp"
47 #include <asdcp/AS_DCP.h>
48 #include <asdcp/KM_util.h>
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::vector;
56 using std::map;
57 using boost::shared_ptr;
58 using boost::split;
59 using boost::is_any_of;
60 using boost::shared_array;
61 using boost::dynamic_pointer_cast;
62 using namespace dcp;
63
64 SMPTESubtitleAsset::SMPTESubtitleAsset ()
65         : _intrinsic_duration (0)
66         , _edit_rate (24, 1)
67         , _time_code_rate (24)
68 {
69
70 }
71
72 /** Construct a SMPTESubtitleAsset by reading an MXF or XML file.
73  *  @param file Filename.
74  */
75 SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file)
76         : SubtitleAsset (file)
77 {
78         shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
79
80         shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
81         Kumu::Result_t r = reader->OpenRead (file.string().c_str ());
82
83         if (!ASDCP_FAILURE (r)) {
84                 string s;
85                 reader->ReadTimedTextResource (s, 0, 0);
86                 xml->read_string (s);
87                 ASDCP::WriterInfo info;
88                 reader->FillWriterInfo (info);
89                 _id = read_writer_info (info);
90         } else {
91                 reader.reset ();
92                 try {
93                         xml->read_file (file);
94                         _id = remove_urn_uuid (xml->string_child ("Id"));
95                 } catch (cxml::Error& e) {
96                         boost::throw_exception (
97                                 DCPReadError (
98                                         String::compose ("MXF failed with %1, XML failed with %2", file, static_cast<int> (r), e.what ())
99                                         )
100                                 );
101                 }
102         }
103
104         _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
105
106         _content_title_text = xml->string_child ("ContentTitleText");
107         _annotation_text = xml->optional_string_child ("AnnotationText");
108         _issue_date = LocalTime (xml->string_child ("IssueDate"));
109         _reel_number = xml->optional_number_child<int> ("ReelNumber");
110         _language = xml->optional_string_child ("Language");
111
112         /* This is supposed to be two numbers, but a single number has been seen in the wild */
113         string const er = xml->string_child ("EditRate");
114         vector<string> er_parts;
115         split (er_parts, er, is_any_of (" "));
116         if (er_parts.size() == 1) {
117                 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
118         } else if (er_parts.size() == 2) {
119                 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
120         } else {
121                 throw XMLError ("malformed EditRate " + er);
122         }
123
124         _time_code_rate = xml->number_child<int> ("TimeCodeRate");
125         if (xml->optional_string_child ("StartTime")) {
126                 _start_time = Time (xml->string_child ("StartTime"), _time_code_rate);
127         }
128
129         shared_ptr<cxml::Node> subtitle_list = xml->optional_node_child ("SubtitleList");
130
131         list<shared_ptr<dcp::FontNode> > font_nodes;
132         BOOST_FOREACH (cxml::NodePtr const & i, subtitle_list->node_children ("Font")) {
133                 font_nodes.push_back (shared_ptr<FontNode> (new FontNode (i, _time_code_rate, SMPTE)));
134         }
135
136         list<shared_ptr<dcp::SubtitleNode> > subtitle_nodes;
137         BOOST_FOREACH (cxml::NodePtr const & i, subtitle_list->node_children ("Subtitle")) {
138                 subtitle_nodes.push_back (shared_ptr<SubtitleNode> (new SubtitleNode (i, _time_code_rate, SMPTE)));
139         }
140
141         parse_subtitles (xml, font_nodes, subtitle_nodes);
142
143         if (reader) {
144                 ASDCP::TimedText::TimedTextDescriptor descriptor;
145                 reader->FillTimedTextDescriptor (descriptor);
146
147                 /* Load fonts */
148
149                 for (
150                         ASDCP::TimedText::ResourceList_t::const_iterator i = descriptor.ResourceList.begin();
151                         i != descriptor.ResourceList.end();
152                         ++i) {
153
154                         if (i->Type == ASDCP::TimedText::MT_OPENTYPE) {
155                                 ASDCP::TimedText::FrameBuffer buffer;
156                                 buffer.Capacity (10 * 1024 * 1024);
157                                 reader->ReadAncillaryResource (i->ResourceID, buffer);
158
159                                 char id[64];
160                                 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof (id));
161
162                                 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
163                                 memcpy (data.get(), buffer.RoData(), buffer.Size());
164
165                                 list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = _load_font_nodes.begin ();
166                                 while (j != _load_font_nodes.end() && (*j)->urn != id) {
167                                         ++j;
168                                 }
169
170                                 if (j != _load_font_nodes.end ()) {
171                                         _fonts.push_back (Font ((*j)->id, (*j)->urn, Data (data, buffer.Size ())));
172                                 }
173                         }
174                 }
175
176                 /* Get intrinsic duration */
177                 _intrinsic_duration = descriptor.ContainerDuration;
178         } else {
179                 /* Guess intrinsic duration */
180                 _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
181         }
182 }
183
184 list<shared_ptr<LoadFontNode> >
185 SMPTESubtitleAsset::load_font_nodes () const
186 {
187         list<shared_ptr<LoadFontNode> > lf;
188         copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
189         return lf;
190 }
191
192 bool
193 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
194 {
195         ASDCP::TimedText::MXFReader reader;
196         Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
197         return !ASDCP_FAILURE (r);
198 }
199
200 string
201 SMPTESubtitleAsset::xml_as_string () const
202 {
203         xmlpp::Document doc;
204         xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel");
205         root->set_namespace_declaration ("http://www.smpte-ra.org/schemas/428-7/2010/DCST", "dcst");
206         root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
207
208         root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _id);
209         root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
210         if (_annotation_text) {
211                 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
212         }
213         root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
214         if (_reel_number) {
215                 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
216         }
217         if (_language) {
218                 root->add_child("Language", "dcst")->add_child_text (_language.get ());
219         }
220         root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
221         root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
222         if (_start_time) {
223                 root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string (SMPTE));
224         }
225
226         BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) {
227                 xmlpp::Element* load_font = root->add_child("LoadFont", "dcst");
228                 load_font->add_child_text ("urn:uuid:" + i->urn);
229                 load_font->set_attribute ("ID", i->id);
230         }
231
232         subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, SMPTE);
233
234         return doc.write_to_string_formatted ("UTF-8");
235 }
236
237 /** Write this content to a MXF file */
238 void
239 SMPTESubtitleAsset::write (boost::filesystem::path p) const
240 {
241         ASDCP::WriterInfo writer_info;
242         fill_writer_info (&writer_info, _id, SMPTE);
243
244         ASDCP::TimedText::TimedTextDescriptor descriptor;
245         descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
246         descriptor.EncodingName = "UTF-8";
247
248         BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
249                 list<Font>::const_iterator j = _fonts.begin ();
250                 while (j != _fonts.end() && j->load_id != i->id) {
251                         ++j;
252                 }
253                 if (j != _fonts.end ()) {
254                         ASDCP::TimedText::TimedTextResourceDescriptor res;
255                         unsigned int c;
256                         Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
257                         DCP_ASSERT (c == Kumu::UUID_Length);
258                         res.Type = ASDCP::TimedText::MT_OPENTYPE;
259                         descriptor.ResourceList.push_back (res);
260                 }
261         }
262
263         descriptor.NamespaceName = "dcst";
264         memcpy (descriptor.AssetID, writer_info.AssetUUID, ASDCP::UUIDlen);
265         descriptor.ContainerDuration = _intrinsic_duration;
266
267         ASDCP::TimedText::MXFWriter writer;
268         ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor);
269         if (ASDCP_FAILURE (r)) {
270                 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
271         }
272
273         /* XXX: no encryption */
274         r = writer.WriteTimedTextResource (xml_as_string ());
275         if (ASDCP_FAILURE (r)) {
276                 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
277         }
278
279         BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
280                 list<Font>::const_iterator j = _fonts.begin ();
281                 while (j != _fonts.end() && j->load_id != i->id) {
282                         ++j;
283                 }
284                 if (j != _fonts.end ()) {
285                         ASDCP::TimedText::FrameBuffer buffer;
286                         buffer.SetData (j->data.data().get(), j->data.size());
287                         buffer.Size (j->data.size());
288                         r = writer.WriteAncillaryResource (buffer);
289                         if (ASDCP_FAILURE (r)) {
290                                 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
291                         }
292                 }
293         }
294
295         writer.Finalize ();
296
297         _file = p;
298 }
299
300 bool
301 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
302 {
303         if (!SubtitleAsset::equals (other_asset, options, note)) {
304                 return false;
305         }
306
307         shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
308         if (!other) {
309                 note (DCP_ERROR, "Subtitles are in different standards");
310                 return false;
311         }
312
313         list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
314         list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
315
316         while (i != _load_font_nodes.end ()) {
317                 if (j == other->_load_font_nodes.end ()) {
318                         note (DCP_ERROR, "<LoadFont> nodes differ");
319                         return false;
320                 }
321
322                 if ((*i)->id != (*j)->id) {
323                         note (DCP_ERROR, "<LoadFont> nodes differ");
324                         return false;
325                 }
326
327                 ++i;
328                 ++j;
329         }
330
331         if (_content_title_text != other->_content_title_text) {
332                 note (DCP_ERROR, "Subtitle content title texts differ");
333                 return false;
334         }
335
336         if (_language != other->_language) {
337                 note (DCP_ERROR, "Subtitle languages differ");
338                 return false;
339         }
340
341         if (_annotation_text != other->_annotation_text) {
342                 note (DCP_ERROR, "Subtitle annotation texts differ");
343                 return false;
344         }
345
346         if (_issue_date != other->_issue_date) {
347                 if (options.issue_dates_can_differ) {
348                         note (DCP_NOTE, "Subtitle issue dates differ");
349                 } else {
350                         note (DCP_ERROR, "Subtitle issue dates differ");
351                         return false;
352                 }
353         }
354
355         if (_reel_number != other->_reel_number) {
356                 note (DCP_ERROR, "Subtitle reel numbers differ");
357                 return false;
358         }
359
360         if (_edit_rate != other->_edit_rate) {
361                 note (DCP_ERROR, "Subtitle edit rates differ");
362                 return false;
363         }
364
365         if (_time_code_rate != other->_time_code_rate) {
366                 note (DCP_ERROR, "Subtitle time code rates differ");
367                 return false;
368         }
369
370         if (_start_time != other->_start_time) {
371                 note (DCP_ERROR, "Subtitle start times differ");
372                 return false;
373         }
374
375         return true;
376 }
377
378 void
379 SMPTESubtitleAsset::add_font (string load_id, boost::filesystem::path file)
380 {
381         string const uuid = make_uuid ();
382         _fonts.push_back (Font (load_id, uuid, file));
383         _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
384 }
385
386 void
387 SMPTESubtitleAsset::add (dcp::SubtitleString s)
388 {
389         SubtitleAsset::add (s);
390         _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
391 }