Merge branch '1.0' of git.carlh.net:git/libdcp into 1.0
[libdcp.git] / src / 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 #include "raw_convert.h"
35 #include "subtitle_asset.h"
36 #include "util.h"
37 #include "xml.h"
38 #include "font_node.h"
39 #include "text_node.h"
40 #include "subtitle_string.h"
41 #include "dcp_assert.h"
42 #include "AS_DCP.h"
43 #include "KM_util.h"
44 #include <libxml++/nodes/element.h>
45 #include <boost/algorithm/string.hpp>
46 #include <boost/shared_array.hpp>
47 #include <boost/foreach.hpp>
48 #include <fstream>
49
50 using std::string;
51 using std::list;
52 using std::ostream;
53 using std::ofstream;
54 using std::stringstream;
55 using std::cout;
56 using std::cerr;
57 using std::map;
58 using boost::shared_ptr;
59 using boost::shared_array;
60 using boost::optional;
61 using boost::dynamic_pointer_cast;
62 using namespace dcp;
63
64 SubtitleAsset::SubtitleAsset ()
65 {
66
67 }
68
69 SubtitleAsset::SubtitleAsset (boost::filesystem::path file)
70         : Asset (file)
71 {
72
73 }
74
75 void
76 SubtitleAsset::parse_subtitles (
77         shared_ptr<cxml::Document> xml,
78         list<shared_ptr<dcp::FontNode> > font_nodes,
79         list<shared_ptr<dcp::SubtitleNode> > subtitle_nodes
80         )
81 {
82         /* Make Subtitle objects to represent the raw XML nodes in a sane way */
83         ParseState parse_state;
84         examine_nodes (xml, font_nodes, parse_state);
85         examine_nodes (xml, subtitle_nodes, parse_state);
86 }
87
88 void
89 SubtitleAsset::examine_nodes (
90         shared_ptr<const cxml::Node> xml,
91         list<shared_ptr<dcp::SubtitleNode> > const & subtitle_nodes,
92         ParseState& parse_state
93         )
94 {
95         BOOST_FOREACH (shared_ptr<dcp::SubtitleNode> i, subtitle_nodes) {
96                 parse_state.subtitle_nodes.push_back (i);
97                 examine_nodes (xml, i->text_nodes, parse_state);
98                 examine_nodes (xml, i->font_nodes, parse_state);
99                 parse_state.subtitle_nodes.pop_back ();
100         }
101 }
102
103 void
104 SubtitleAsset::examine_nodes (
105         shared_ptr<const cxml::Node> xml,
106         list<shared_ptr<dcp::FontNode> > const & font_nodes,
107         ParseState& parse_state
108         )
109 {
110         BOOST_FOREACH (shared_ptr<dcp::FontNode> i, font_nodes) {
111
112                 parse_state.font_nodes.push_back (i);
113                 maybe_add_subtitle (i->text, parse_state);
114
115                 examine_nodes (xml, i->subtitle_nodes, parse_state);
116                 examine_nodes (xml, i->font_nodes, parse_state);
117                 examine_nodes (xml, i->text_nodes, parse_state);
118
119                 parse_state.font_nodes.pop_back ();
120         }
121 }
122
123 void
124 SubtitleAsset::examine_nodes (
125         shared_ptr<const cxml::Node> xml,
126         list<shared_ptr<dcp::TextNode> > const & text_nodes,
127         ParseState& parse_state
128         )
129 {
130         BOOST_FOREACH (shared_ptr<dcp::TextNode> i, text_nodes) {
131                 parse_state.text_nodes.push_back (i);
132                 maybe_add_subtitle (i->text, parse_state);
133                 examine_nodes (xml, i->font_nodes, parse_state);
134                 parse_state.text_nodes.pop_back ();
135         }
136 }
137
138 void
139 SubtitleAsset::maybe_add_subtitle (string text, ParseState const & parse_state)
140 {
141         if (empty_or_white_space (text)) {
142                 return;
143         }
144
145         if (parse_state.text_nodes.empty() || parse_state.subtitle_nodes.empty ()) {
146                 return;
147         }
148
149         DCP_ASSERT (!parse_state.text_nodes.empty ());
150         DCP_ASSERT (!parse_state.subtitle_nodes.empty ());
151
152         dcp::FontNode effective_font (parse_state.font_nodes);
153         dcp::TextNode effective_text (*parse_state.text_nodes.back ());
154         dcp::SubtitleNode effective_subtitle (*parse_state.subtitle_nodes.back ());
155
156         _subtitles.push_back (
157                 SubtitleString (
158                         effective_font.id,
159                         effective_font.italic.get_value_or (false),
160                         effective_font.bold.get_value_or (false),
161                         effective_font.underline.get_value_or (false),
162                         effective_font.colour.get_value_or (dcp::Colour (255, 255, 255)),
163                         effective_font.size,
164                         effective_font.aspect_adjust.get_value_or (1.0),
165                         effective_subtitle.in,
166                         effective_subtitle.out,
167                         effective_text.h_position,
168                         effective_text.h_align,
169                         effective_text.v_position,
170                         effective_text.v_align,
171                         effective_text.direction,
172                         text,
173                         effective_font.effect.get_value_or (NONE),
174                         effective_font.effect_colour.get_value_or (dcp::Colour (0, 0, 0)),
175                         effective_subtitle.fade_up_time,
176                         effective_subtitle.fade_down_time
177                         )
178                 );
179 }
180
181 list<SubtitleString>
182 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
183 {
184         list<SubtitleString> s;
185         BOOST_FOREACH (SubtitleString const & i, _subtitles) {
186                 if ((starting && from <= i.in() && i.in() < to) || (!starting && i.out() >= from && i.in() <= to)) {
187                         s.push_back (i);
188                 }
189         }
190
191         return s;
192 }
193
194 void
195 SubtitleAsset::add (SubtitleString s)
196 {
197         _subtitles.push_back (s);
198 }
199
200 Time
201 SubtitleAsset::latest_subtitle_out () const
202 {
203         Time t;
204         BOOST_FOREACH (SubtitleString const & i, _subtitles) {
205                 if (i.out() > t) {
206                         t = i.out ();
207                 }
208         }
209
210         return t;
211 }
212
213 bool
214 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
215 {
216         if (!Asset::equals (other_asset, options, note)) {
217                 return false;
218         }
219
220         shared_ptr<const SubtitleAsset> other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
221         if (!other) {
222                 return false;
223         }
224
225         if (_subtitles != other->_subtitles) {
226                 note (DCP_ERROR, "subtitles differ");
227                 return false;
228         }
229
230         return true;
231 }
232
233 struct SubtitleSorter {
234         bool operator() (SubtitleString const & a, SubtitleString const & b) {
235                 if (a.in() != b.in()) {
236                         return a.in() < b.in();
237                 }
238                 return a.v_position() < b.v_position();
239         }
240 };
241
242 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
243  *  class because the differences between the two are fairly subtle.
244  */
245 void
246 SubtitleAsset::subtitles_as_xml (xmlpp::Element* root, int time_code_rate, Standard standard) const
247 {
248         list<SubtitleString> sorted = _subtitles;
249         sorted.sort (SubtitleSorter ());
250
251         string const xmlns = standard == SMPTE ? "dcst" : "";
252
253         /* XXX: script not supported */
254
255         optional<string> font;
256         bool italic = false;
257         bool bold = false;
258         bool underline = false;
259         Colour colour;
260         int size = 0;
261         float aspect_adjust = 1.0;
262         Effect effect = NONE;
263         Colour effect_colour;
264         int spot_number = 1;
265         Time last_in;
266         Time last_out;
267         Time last_fade_up_time;
268         Time last_fade_down_time;
269
270         xmlpp::Element* font_element = 0;
271         xmlpp::Element* subtitle_element = 0;
272
273         BOOST_FOREACH (SubtitleString const & i, sorted) {
274
275                 /* We will start a new <Font>...</Font> whenever some font property changes.
276                    I suppose we should really make an optimal hierarchy of <Font> tags, but
277                    that seems hard.
278                 */
279
280                 bool const font_changed =
281                         font          != i.font()          ||
282                         italic        != i.italic()        ||
283                         bold          != i.bold()          ||
284                         underline     != i.underline()     ||
285                         colour        != i.colour()        ||
286                         size          != i.size()          ||
287                         fabs (aspect_adjust - i.aspect_adjust()) > ASPECT_ADJUST_EPSILON ||
288                         effect        != i.effect()        ||
289                         effect_colour != i.effect_colour();
290
291                 if (font_changed) {
292                         font = i.font ();
293                         italic = i.italic ();
294                         bold = i.bold ();
295                         underline = i.underline ();
296                         colour = i.colour ();
297                         size = i.size ();
298                         aspect_adjust = i.aspect_adjust ();
299                         effect = i.effect ();
300                         effect_colour = i.effect_colour ();
301                 }
302
303                 if (!font_element || font_changed) {
304                         font_element = root->add_child ("Font", xmlns);
305                         if (font) {
306                                 if (standard == SMPTE) {
307                                         font_element->set_attribute ("ID", font.get ());
308                                 } else {
309                                         font_element->set_attribute ("Id", font.get ());
310                                 }
311                         }
312                         font_element->set_attribute ("Italic", italic ? "yes" : "no");
313                         font_element->set_attribute ("Color", colour.to_argb_string());
314                         font_element->set_attribute ("Size", raw_convert<string> (size));
315                         if (fabs (aspect_adjust - 1.0) > ASPECT_ADJUST_EPSILON) {
316                                 font_element->set_attribute ("AspectAdjust", raw_convert<string> (aspect_adjust));
317                         }
318                         font_element->set_attribute ("Effect", effect_to_string (effect));
319                         font_element->set_attribute ("EffectColor", effect_colour.to_argb_string());
320                         font_element->set_attribute ("Script", "normal");
321                         if (standard == SMPTE) {
322                                 font_element->set_attribute ("Underline", underline ? "yes" : "no");
323                         } else {
324                                 font_element->set_attribute ("Underlined", underline ? "yes" : "no");
325                         }
326                         font_element->set_attribute ("Weight", bold ? "bold" : "normal");
327                 }
328
329                 if (!subtitle_element || font_changed ||
330                     (last_in != i.in() ||
331                      last_out != i.out() ||
332                      last_fade_up_time != i.fade_up_time() ||
333                      last_fade_down_time != i.fade_down_time()
334                             )) {
335
336                         subtitle_element = font_element->add_child ("Subtitle", xmlns);
337                         subtitle_element->set_attribute ("SpotNumber", raw_convert<string> (spot_number++));
338                         subtitle_element->set_attribute ("TimeIn", i.in().rebase(time_code_rate).as_string(standard));
339                         subtitle_element->set_attribute ("TimeOut", i.out().rebase(time_code_rate).as_string(standard));
340                         if (standard == SMPTE) {
341                                 subtitle_element->set_attribute ("FadeUpTime", i.fade_up_time().rebase(time_code_rate).as_string(standard));
342                                 subtitle_element->set_attribute ("FadeDownTime", i.fade_down_time().rebase(time_code_rate).as_string(standard));
343                         } else {
344                                 subtitle_element->set_attribute ("FadeUpTime", raw_convert<string> (i.fade_up_time().as_editable_units(time_code_rate)));
345                                 subtitle_element->set_attribute ("FadeDownTime", raw_convert<string> (i.fade_down_time().as_editable_units(time_code_rate)));
346                         }
347
348                         last_in = i.in ();
349                         last_out = i.out ();
350                         last_fade_up_time = i.fade_up_time ();
351                         last_fade_down_time = i.fade_down_time ();
352                 }
353
354                 xmlpp::Element* text = subtitle_element->add_child ("Text", xmlns);
355
356                 if (i.h_align() != HALIGN_CENTER) {
357                         if (standard == SMPTE) {
358                                 text->set_attribute ("Halign", halign_to_string (i.h_align ()));
359                         } else {
360                                 text->set_attribute ("HAlign", halign_to_string (i.h_align ()));
361                         }
362                 }
363
364                 if (i.h_position() > ALIGN_EPSILON) {
365                         if (standard == SMPTE) {
366                                 text->set_attribute ("Hposition", raw_convert<string> (i.h_position() * 100, 6));
367                         } else {
368                                 text->set_attribute ("HPosition", raw_convert<string> (i.h_position() * 100, 6));
369                         }
370                 }
371
372                 if (standard == SMPTE) {
373                         text->set_attribute ("Valign", valign_to_string (i.v_align()));
374                 } else {
375                         text->set_attribute ("VAlign", valign_to_string (i.v_align()));
376                 }
377
378                 if (i.v_position() > ALIGN_EPSILON) {
379                         if (standard == SMPTE) {
380                                 text->set_attribute ("Vposition", raw_convert<string> (i.v_position() * 100, 6));
381                         } else {
382                                 text->set_attribute ("VPosition", raw_convert<string> (i.v_position() * 100, 6));
383                         }
384                 } else {
385                         if (standard == SMPTE) {
386                                 text->set_attribute ("Vposition", "0");
387                         } else {
388                                 text->set_attribute ("VPosition", "0");
389                         }
390                 }
391
392                 /* Interop only supports "horizontal" or "vertical" for direction, so only write this
393                    for SMPTE.
394                 */
395                 if (i.direction() != DIRECTION_LTR && standard == SMPTE) {
396                         text->set_attribute ("Direction", direction_to_string (i.direction ()));
397                 }
398
399                 text->add_child_text (i.text());
400         }
401 }
402
403 map<string, Data>
404 SubtitleAsset::fonts_with_load_ids () const
405 {
406         map<string, Data> out;
407         BOOST_FOREACH (Font const & i, _fonts) {
408                 out[i.load_id] = i.data;
409         }
410         return out;
411 }