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