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