Merge master.
[libdcp.git] / src / subtitle_asset.cc
1 /*
2     Copyright (C) 2012 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 #include <fstream>
21 #include <boost/lexical_cast.hpp>
22 #include <boost/algorithm/string.hpp>
23 #include <libxml++/nodes/element.h>
24 #include "subtitle_asset.h"
25 #include "util.h"
26 #include "xml.h"
27
28 using std::string;
29 using std::list;
30 using std::ostream;
31 using std::ofstream;
32 using std::stringstream;
33 using boost::shared_ptr;
34 using boost::lexical_cast;
35 using boost::optional;
36 using namespace libdcp;
37
38 SubtitleAsset::SubtitleAsset (string directory, string xml_file)
39         : Asset (directory, xml_file)
40         , _need_sort (false)
41 {
42         read_xml (path().string());
43 }
44
45 SubtitleAsset::SubtitleAsset (string directory, string movie_title, string language)
46         : Asset (directory)
47         , _movie_title (movie_title)
48         , _reel_number ("1")
49         , _language (language)
50         , _need_sort (false)
51 {
52
53 }
54
55 void
56 SubtitleAsset::read_xml (string xml_file)
57 {
58         shared_ptr<cxml::File> xml (new cxml::File (xml_file, "DCSubtitle"));
59         
60         _uuid = xml->string_child ("SubtitleID");
61         _movie_title = xml->string_child ("MovieTitle");
62         _reel_number = xml->string_child ("ReelNumber");
63         _language = xml->string_child ("Language");
64
65         xml->ignore_child ("LoadFont");
66
67         list<shared_ptr<libdcp::parse::Font> > font_nodes = type_children<libdcp::parse::Font> (xml, "Font");
68         _load_font_nodes = type_children<libdcp::parse::LoadFont> (xml, "LoadFont");
69
70         /* Now make Subtitle objects to represent the raw XML nodes
71            in a sane way.
72         */
73
74         ParseState parse_state;
75         examine_font_nodes (xml, font_nodes, parse_state);
76 }
77
78 void
79 SubtitleAsset::examine_font_nodes (
80         shared_ptr<const cxml::Node> xml,
81         list<shared_ptr<libdcp::parse::Font> > const & font_nodes,
82         ParseState& parse_state
83         )
84 {
85         for (list<shared_ptr<libdcp::parse::Font> >::const_iterator i = font_nodes.begin(); i != font_nodes.end(); ++i) {
86
87                 parse_state.font_nodes.push_back (*i);
88                 maybe_add_subtitle ((*i)->text, parse_state);
89
90                 for (list<shared_ptr<libdcp::parse::Subtitle> >::iterator j = (*i)->subtitle_nodes.begin(); j != (*i)->subtitle_nodes.end(); ++j) {
91                         parse_state.subtitle_nodes.push_back (*j);
92                         examine_text_nodes (xml, (*j)->text_nodes, parse_state);
93                         examine_font_nodes (xml, (*j)->font_nodes, parse_state);
94                         parse_state.subtitle_nodes.pop_back ();
95                 }
96         
97                 examine_font_nodes (xml, (*i)->font_nodes, parse_state);
98                 examine_text_nodes (xml, (*i)->text_nodes, parse_state);
99                 
100                 parse_state.font_nodes.pop_back ();
101         }
102 }
103
104 void
105 SubtitleAsset::examine_text_nodes (
106         shared_ptr<const cxml::Node> xml,
107         list<shared_ptr<libdcp::parse::Text> > const & text_nodes,
108         ParseState& parse_state
109         )
110 {
111         for (list<shared_ptr<libdcp::parse::Text> >::const_iterator i = text_nodes.begin(); i != text_nodes.end(); ++i) {
112                 parse_state.text_nodes.push_back (*i);
113                 maybe_add_subtitle ((*i)->text, parse_state);
114                 examine_font_nodes (xml, (*i)->font_nodes, parse_state);
115                 parse_state.text_nodes.pop_back ();
116         }
117 }
118
119 void
120 SubtitleAsset::maybe_add_subtitle (string text, ParseState const & parse_state)
121 {
122         if (empty_or_white_space (text)) {
123                 return;
124         }
125         
126         if (parse_state.text_nodes.empty() || parse_state.subtitle_nodes.empty ()) {
127                 return;
128         }
129
130         assert (!parse_state.text_nodes.empty ());
131         assert (!parse_state.subtitle_nodes.empty ());
132         
133         libdcp::parse::Font effective_font (parse_state.font_nodes);
134         libdcp::parse::Text effective_text (*parse_state.text_nodes.back ());
135         libdcp::parse::Subtitle effective_subtitle (*parse_state.subtitle_nodes.back ());
136
137         _subtitles.push_back (
138                 shared_ptr<Subtitle> (
139                         new Subtitle (
140                                 font_id_to_name (effective_font.id),
141                                 effective_font.italic.get(),
142                                 effective_font.color.get(),
143                                 effective_font.size,
144                                 effective_subtitle.in,
145                                 effective_subtitle.out,
146                                 effective_text.v_position,
147                                 effective_text.v_align,
148                                 text,
149                                 effective_font.effect ? effective_font.effect.get() : NONE,
150                                 effective_font.effect_color.get(),
151                                 effective_subtitle.fade_up_time,
152                                 effective_subtitle.fade_down_time
153                                 )
154                         )
155                 );
156 }
157
158 list<shared_ptr<Subtitle> >
159 SubtitleAsset::subtitles_at (Time t) const
160 {
161         list<shared_ptr<Subtitle> > s;
162         for (list<shared_ptr<Subtitle> >::const_iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) {
163                 if ((*i)->in() <= t && t <= (*i)->out ()) {
164                         s.push_back (*i);
165                 }
166         }
167
168         return s;
169 }
170
171 std::string
172 SubtitleAsset::font_id_to_name (string id) const
173 {
174         list<shared_ptr<libdcp::parse::LoadFont> >::const_iterator i = _load_font_nodes.begin();
175         while (i != _load_font_nodes.end() && (*i)->id != id) {
176                 ++i;
177         }
178
179         if (i == _load_font_nodes.end ()) {
180                 return "";
181         }
182
183         if ((*i)->uri == "arial.ttf") {
184                 return "Arial";
185         }
186
187         return "";
188 }
189
190 Subtitle::Subtitle (
191         string font,
192         bool italic,
193         Color color,
194         int size,
195         Time in,
196         Time out,
197         float v_position,
198         VAlign v_align,
199         string text,
200         Effect effect,
201         Color effect_color,
202         Time fade_up_time,
203         Time fade_down_time
204         )
205         : _font (font)
206         , _italic (italic)
207         , _color (color)
208         , _size (size)
209         , _in (in)
210         , _out (out)
211         , _v_position (v_position)
212         , _v_align (v_align)
213         , _text (text)
214         , _effect (effect)
215         , _effect_color (effect_color)
216         , _fade_up_time (fade_up_time)
217         , _fade_down_time (fade_down_time)
218 {
219
220 }
221
222 int
223 Subtitle::size_in_pixels (int screen_height) const
224 {
225         /* Size in the subtitle file is given in points as if the screen
226            height is 11 inches, so a 72pt font would be 1/11th of the screen
227            height.
228         */
229         
230         return _size * screen_height / (11 * 72);
231 }
232
233 bool
234 libdcp::operator== (Subtitle const & a, Subtitle const & b)
235 {
236         return (
237                 a.font() == b.font() &&
238                 a.italic() == b.italic() &&
239                 a.color() == b.color() &&
240                 a.size() == b.size() &&
241                 a.in() == b.in() &&
242                 a.out() == b.out() &&
243                 a.v_position() == b.v_position() &&
244                 a.v_align() == b.v_align() &&
245                 a.text() == b.text() &&
246                 a.effect() == b.effect() &&
247                 a.effect_color() == b.effect_color() &&
248                 a.fade_up_time() == b.fade_up_time() &&
249                 a.fade_down_time() == b.fade_down_time()
250                 );
251 }
252
253 ostream&
254 libdcp::operator<< (ostream& s, Subtitle const & sub)
255 {
256         s << "\n`" << sub.text() << "' from " << sub.in() << " to " << sub.out() << ";\n"
257           << "fade up " << sub.fade_up_time() << ", fade down " << sub.fade_down_time() << ";\n"
258           << "font " << sub.font() << ", ";
259
260         if (sub.italic()) {
261                 s << "italic";
262         } else {
263                 s << "non-italic";
264         }
265         
266         s << ", size " << sub.size() << ", color " << sub.color() << ", vpos " << sub.v_position() << ", valign " << ((int) sub.v_align()) << ";\n"
267           << "effect " << ((int) sub.effect()) << ", effect color " << sub.effect_color();
268
269         return s;
270 }
271
272 void
273 SubtitleAsset::add (shared_ptr<Subtitle> s)
274 {
275         _subtitles.push_back (s);
276         _need_sort = true;
277 }
278
279 void
280 SubtitleAsset::write_to_cpl (xmlpp::Node* node) const
281 {
282         /* XXX: should EditRate, Duration and IntrinsicDuration be in here? */
283
284         xmlpp::Node* ms = node->add_child ("MainSubtitle");
285         ms->add_child("Id")->add_child_text("urn:uuid:" + _uuid);
286         ms->add_child("AnnotationText")->add_child_text (_file_name);
287         /* XXX */
288         ms->add_child("EntryPoint")->add_child_text ("0");
289 }
290
291 struct SubtitleSorter {
292         bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
293                 if (a->in() != b->in()) {
294                         return a->in() < b->in();
295                 }
296                 return a->v_position() < b->v_position();
297         }
298 };
299
300 void
301 SubtitleAsset::write_xml () const
302 {
303         ofstream s (path().string().c_str());
304         write_xml (s);
305 }
306
307 void
308 SubtitleAsset::write_xml (ostream& s) const
309 {
310         xmlpp::Document doc;
311         xmlpp::Element* root = doc.create_root_node ("DCSubtitle");
312         root->set_attribute ("Version", "1.0");
313
314         root->add_child("SubtitleID")->add_child_text (_uuid);
315         root->add_child("MovieTitle")->add_child_text (_movie_title);
316         root->add_child("ReelNumber")->add_child_text (lexical_cast<string> (_reel_number));
317         root->add_child("Language")->add_child_text (_language);
318
319         if (_load_font_nodes.size() > 1) {
320                 boost::throw_exception (MiscError ("multiple LoadFont nodes not supported"));
321         }
322
323         if (!_load_font_nodes.empty ()) {
324                 xmlpp::Element* load_font = root->add_child("LoadFont");
325                 load_font->set_attribute("Id", _load_font_nodes.front()->id);
326                 load_font->set_attribute("URI",  _load_font_nodes.front()->uri);
327         }
328
329         list<shared_ptr<Subtitle> > sorted = _subtitles;
330         if (_need_sort) {
331                 sorted.sort (SubtitleSorter ());
332         }
333
334         /* XXX: multiple fonts not supported */
335         /* XXX: script, underlined, weight not supported */
336
337         bool italic = false;
338         Color color;
339         int size = 0;
340         Effect effect = NONE;
341         Color effect_color;
342         int spot_number = 1;
343         Time last_in;
344         Time last_out;
345         Time last_fade_up_time;
346         Time last_fade_down_time;
347
348         xmlpp::Element* font = 0;
349         xmlpp::Element* subtitle = 0;
350
351         for (list<shared_ptr<Subtitle> >::iterator i = sorted.begin(); i != sorted.end(); ++i) {
352
353                 /* We will start a new <Font>...</Font> whenever some font property changes.
354                    I suppose we should really make an optimal hierarchy of <Font> tags, but
355                    that seems hard.
356                 */
357
358                 bool const font_changed =
359                         italic       != (*i)->italic()       ||
360                         color        != (*i)->color()        ||
361                         size         != (*i)->size()         ||
362                         effect       != (*i)->effect()       ||
363                         effect_color != (*i)->effect_color();
364
365                 if (font_changed) {
366                         italic = (*i)->italic ();
367                         color = (*i)->color ();
368                         size = (*i)->size ();
369                         effect = (*i)->effect ();
370                         effect_color = (*i)->effect_color ();
371                 }
372
373                 if (!font || font_changed) {
374                         font = root->add_child ("Font");
375                         string id = "theFontId";
376                         if (!_load_font_nodes.empty()) {
377                                 id = _load_font_nodes.front()->id;
378                         }
379                         font->set_attribute ("Id", id);
380                         font->set_attribute ("Italic", italic ? "yes" : "no");
381                         font->set_attribute ("Color", color.to_argb_string());
382                         font->set_attribute ("Size", lexical_cast<string> (size));
383                         font->set_attribute ("Effect", effect_to_string (effect));
384                         font->set_attribute ("EffectColor", effect_color.to_argb_string());
385                         font->set_attribute ("Script", "normal");
386                         font->set_attribute ("Underlined", "no");
387                         font->set_attribute ("Weight", "normal");
388                 }
389
390                 if (!subtitle || font_changed ||
391                     (last_in != (*i)->in() ||
392                      last_out != (*i)->out() ||
393                      last_fade_up_time != (*i)->fade_up_time() ||
394                      last_fade_down_time != (*i)->fade_down_time()
395                             )) {
396
397                         subtitle = font->add_child ("Subtitle");
398                         subtitle->set_attribute ("SpotNumber", lexical_cast<string> (spot_number++));
399                         subtitle->set_attribute ("TimeIn", (*i)->in().to_string());
400                         subtitle->set_attribute ("TimeOut", (*i)->out().to_string());
401                         subtitle->set_attribute ("FadeUpTime", lexical_cast<string> ((*i)->fade_up_time().to_ticks()));
402                         subtitle->set_attribute ("FadeDownTime", lexical_cast<string> ((*i)->fade_down_time().to_ticks()));
403
404                         last_in = (*i)->in ();
405                         last_out = (*i)->out ();
406                         last_fade_up_time = (*i)->fade_up_time ();
407                         last_fade_down_time = (*i)->fade_down_time ();
408                 }
409
410                 xmlpp::Element* text = subtitle->add_child ("Text");
411                 text->set_attribute ("VAlign", valign_to_string ((*i)->v_align()));             
412                 text->set_attribute ("VPosition", lexical_cast<string> ((*i)->v_position()));
413                 text->add_child_text ((*i)->text());
414         }
415
416         doc.write_to_stream_formatted (s, "UTF-8");
417 }
418