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