15ddb2a8fd57f6711986b50a0978c4dd6cde3615
[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 "subtitle_asset.h"
23 #include "util.h"
24
25 using std::string;
26 using std::list;
27 using std::ostream;
28 using std::ofstream;
29 using std::stringstream;
30 using boost::shared_ptr;
31 using boost::lexical_cast;
32 using namespace libdcp;
33
34 SubtitleAsset::SubtitleAsset (string directory, string xml_file)
35         : Asset (directory, xml_file)
36 {
37         shared_ptr<XMLFile> xml (new XMLFile (path().string(), "DCSubtitle"));
38         
39         _uuid = xml->string_child ("SubtitleID");
40         _movie_title = xml->string_child ("MovieTitle");
41         _reel_number = xml->string_child ("ReelNumber");
42         _language = xml->string_child ("Language");
43
44         xml->ignore_child ("LoadFont");
45
46         list<shared_ptr<FontNode> > font_nodes = xml->type_children<FontNode> ("Font");
47         _load_font_nodes = xml->type_children<LoadFontNode> ("LoadFont");
48
49         /* Now make Subtitle objects to represent the raw XML nodes
50            in a sane way.
51         */
52
53         ParseState parse_state;
54         examine_font_nodes (xml, font_nodes, parse_state);
55 }
56
57 SubtitleAsset::SubtitleAsset (string directory, string movie_title, string language)
58         : Asset (directory)
59         , _movie_title (movie_title)
60         , _reel_number ("1")
61         , _language (language)
62 {
63
64 }
65
66 void
67 SubtitleAsset::examine_font_nodes (
68         shared_ptr<XMLFile> xml,
69         list<shared_ptr<FontNode> > const & font_nodes,
70         ParseState& parse_state
71         )
72 {
73         for (list<shared_ptr<FontNode> >::const_iterator i = font_nodes.begin(); i != font_nodes.end(); ++i) {
74
75                 parse_state.font_nodes.push_back (*i);
76                 maybe_add_subtitle ((*i)->text, parse_state);
77
78                 for (list<shared_ptr<SubtitleNode> >::iterator j = (*i)->subtitle_nodes.begin(); j != (*i)->subtitle_nodes.end(); ++j) {
79                         parse_state.subtitle_nodes.push_back (*j);
80                         examine_text_nodes (xml, (*j)->text_nodes, parse_state);
81                         examine_font_nodes (xml, (*j)->font_nodes, parse_state);
82                         parse_state.subtitle_nodes.pop_back ();
83                 }
84         
85                 examine_font_nodes (xml, (*i)->font_nodes, parse_state);
86                 examine_text_nodes (xml, (*i)->text_nodes, parse_state);
87                 
88                 parse_state.font_nodes.pop_back ();
89         }
90 }
91
92 void
93 SubtitleAsset::examine_text_nodes (
94         shared_ptr<XMLFile> xml,
95         list<shared_ptr<TextNode> > const & text_nodes,
96         ParseState& parse_state
97         )
98 {
99         for (list<shared_ptr<TextNode> >::const_iterator i = text_nodes.begin(); i != text_nodes.end(); ++i) {
100                 parse_state.text_nodes.push_back (*i);
101                 maybe_add_subtitle ((*i)->text, parse_state);
102                 examine_font_nodes (xml, (*i)->font_nodes, parse_state);
103                 parse_state.text_nodes.pop_back ();
104         }
105 }
106
107 void
108 SubtitleAsset::maybe_add_subtitle (string text, ParseState const & parse_state)
109 {
110         if (empty_or_white_space (text)) {
111                 return;
112         }
113         
114         if (parse_state.text_nodes.empty() || parse_state.subtitle_nodes.empty ()) {
115                 return;
116         }
117
118         assert (!parse_state.text_nodes.empty ());
119         assert (!parse_state.subtitle_nodes.empty ());
120         
121         FontNode effective_font (parse_state.font_nodes);
122         TextNode effective_text (*parse_state.text_nodes.back ());
123         SubtitleNode effective_subtitle (*parse_state.subtitle_nodes.back ());
124
125         _subtitles.push_back (
126                 shared_ptr<Subtitle> (
127                         new Subtitle (
128                                 font_id_to_name (effective_font.id),
129                                 effective_font.italic.get(),
130                                 effective_font.color.get(),
131                                 effective_font.size,
132                                 effective_subtitle.in,
133                                 effective_subtitle.out,
134                                 effective_text.v_position,
135                                 effective_text.v_align,
136                                 text,
137                                 effective_font.effect.get(),
138                                 effective_font.effect_color.get(),
139                                 effective_subtitle.fade_up_time,
140                                 effective_subtitle.fade_down_time
141                                 )
142                         )
143                 );
144 }
145
146 FontNode::FontNode (xmlpp::Node const * node)
147         : XMLNode (node)
148 {
149         text = content ();
150         
151         id = optional_string_attribute ("Id");
152         size = optional_int64_attribute ("Size");
153         italic = optional_bool_attribute ("Italic");
154         color = optional_color_attribute ("Color");
155         string const e = optional_string_attribute ("Effect");
156         if (!e.empty ()) {
157                 effect = string_to_effect (e);
158         }
159         effect_color = optional_color_attribute ("EffectColor");
160         subtitle_nodes = type_children<SubtitleNode> ("Subtitle");
161         font_nodes = type_children<FontNode> ("Font");
162         text_nodes = type_children<TextNode> ("Text");
163 }
164
165 FontNode::FontNode (list<shared_ptr<FontNode> > const & font_nodes)
166         : size (0)
167         , italic (false)
168         , color ("FFFFFFFF")
169         , effect_color ("FFFFFFFF")
170 {
171         for (list<shared_ptr<FontNode> >::const_iterator i = font_nodes.begin(); i != font_nodes.end(); ++i) {
172                 if (!(*i)->id.empty ()) {
173                         id = (*i)->id;
174                 }
175                 if ((*i)->size != 0) {
176                         size = (*i)->size;
177                 }
178                 if ((*i)->italic) {
179                         italic = (*i)->italic.get ();
180                 }
181                 if ((*i)->color) {
182                         color = (*i)->color.get ();
183                 }
184                 if ((*i)->effect) {
185                         effect = (*i)->effect.get ();
186                 }
187                 if ((*i)->effect_color) {
188                         effect_color = (*i)->effect_color.get ();
189                 }
190         }
191 }
192
193 LoadFontNode::LoadFontNode (xmlpp::Node const * node)
194         : XMLNode (node)
195 {
196         id = string_attribute ("Id");
197         uri = string_attribute ("URI");
198 }
199         
200
201 SubtitleNode::SubtitleNode (xmlpp::Node const * node)
202         : XMLNode (node)
203 {
204         in = time_attribute ("TimeIn");
205         out = time_attribute ("TimeOut");
206         font_nodes = type_children<FontNode> ("Font");
207         text_nodes = type_children<TextNode> ("Text");
208         fade_up_time = fade_time ("FadeUpTime");
209         fade_down_time = fade_time ("FadeDownTime");
210 }
211
212 Time
213 SubtitleNode::fade_time (string name)
214 {
215         string const u = optional_string_attribute (name);
216         Time t;
217         
218         if (u.empty ()) {
219                 t = Time (0, 0, 0, 20);
220         } else if (u.find (":") != string::npos) {
221                 t = Time (u);
222         } else {
223                 t = Time (0, 0, 0, lexical_cast<int> (u));
224         }
225
226         if (t > Time (0, 0, 8, 0)) {
227                 t = Time (0, 0, 8, 0);
228         }
229
230         return t;
231 }
232
233 TextNode::TextNode (xmlpp::Node const * node)
234         : XMLNode (node)
235         , v_align (CENTER)
236 {
237         text = content ();
238         v_position = float_attribute ("VPosition");
239         string const v = optional_string_attribute ("VAlign");
240         if (!v.empty ()) {
241                 v_align = string_to_valign (v);
242         }
243
244         font_nodes = type_children<FontNode> ("Font");
245 }
246
247 list<shared_ptr<Subtitle> >
248 SubtitleAsset::subtitles_at (Time t) const
249 {
250         list<shared_ptr<Subtitle> > s;
251         for (list<shared_ptr<Subtitle> >::const_iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) {
252                 if ((*i)->in() <= t && t <= (*i)->out ()) {
253                         s.push_back (*i);
254                 }
255         }
256
257         return s;
258 }
259
260 std::string
261 SubtitleAsset::font_id_to_name (string id) const
262 {
263         list<shared_ptr<LoadFontNode> >::const_iterator i = _load_font_nodes.begin();
264         while (i != _load_font_nodes.end() && (*i)->id != id) {
265                 ++i;
266         }
267
268         if (i == _load_font_nodes.end ()) {
269                 return "";
270         }
271
272         if ((*i)->uri == "arial.ttf") {
273                 return "Arial";
274         }
275
276         return "";
277 }
278
279 Subtitle::Subtitle (
280         string font,
281         bool italic,
282         Color color,
283         int size,
284         Time in,
285         Time out,
286         float v_position,
287         VAlign v_align,
288         string text,
289         Effect effect,
290         Color effect_color,
291         Time fade_up_time,
292         Time fade_down_time
293         )
294         : _font (font)
295         , _italic (italic)
296         , _color (color)
297         , _size (size)
298         , _in (in)
299         , _out (out)
300         , _v_position (v_position)
301         , _v_align (v_align)
302         , _text (text)
303         , _effect (effect)
304         , _effect_color (effect_color)
305         , _fade_up_time (fade_up_time)
306         , _fade_down_time (fade_down_time)
307 {
308
309 }
310
311 int
312 Subtitle::size_in_pixels (int screen_height) const
313 {
314         /* Size in the subtitle file is given in points as if the screen
315            height is 11 inches, so a 72pt font would be 1/11th of the screen
316            height.
317         */
318         
319         return _size * screen_height / (11 * 72);
320 }
321
322 bool
323 libdcp::operator== (Subtitle const & a, Subtitle const & b)
324 {
325         return (
326                 a.font() == b.font() &&
327                 a.italic() == b.italic() &&
328                 a.color() == b.color() &&
329                 a.size() == b.size() &&
330                 a.in() == b.in() &&
331                 a.out() == b.out() &&
332                 a.v_position() == b.v_position() &&
333                 a.v_align() == b.v_align() &&
334                 a.text() == b.text() &&
335                 a.effect() == b.effect() &&
336                 a.effect_color() == b.effect_color() &&
337                 a.fade_up_time() == b.fade_up_time() &&
338                 a.fade_down_time() == b.fade_down_time()
339                 );
340 }
341
342 ostream&
343 libdcp::operator<< (ostream& s, Subtitle const & sub)
344 {
345         s << "\n`" << sub.text() << "' from " << sub.in() << " to " << sub.out() << ";\n"
346           << "fade up " << sub.fade_up_time() << ", fade down " << sub.fade_down_time() << ";\n"
347           << "font " << sub.font() << ", ";
348
349         if (sub.italic()) {
350                 s << "italic";
351         } else {
352                 s << "non-italic";
353         }
354         
355         s << ", size " << sub.size() << ", color " << sub.color() << ", vpos " << sub.v_position() << ", valign " << ((int) sub.v_align()) << ";\n"
356           << "effect " << ((int) sub.effect()) << ", effect color " << sub.effect_color();
357
358         return s;
359 }
360
361 void
362 SubtitleAsset::add (shared_ptr<Subtitle> s)
363 {
364         _subtitles.push_back (s);
365 }
366
367 void
368 SubtitleAsset::write_to_cpl (ostream& s) const
369 {
370         /* XXX: should EditRate, Duration and IntrinsicDuration be in here? */
371         
372         s << "        <MainSubtitle>\n"
373           << "          <Id>urn:uuid:" << _uuid << "</Id>\n"
374           << "          <AnnotationText>" << _file_name << "</AnnotationText>\n"
375           << "          <EntryPoint>0</EntryPoint>\n"
376           << "        </MainSubtitle>\n";
377 }
378
379 struct SubtitleSorter {
380         bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
381                 return a->in() < b->in();
382         }
383 };
384
385 void
386 SubtitleAsset::write_xml ()
387 {
388         ofstream f (path().string().c_str());
389
390         f << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
391           << "<DCSubtitle Version=\"1.0\">\n"
392           << "  <SubtitleID>" << _uuid << "</SubtitleID>\n"
393           << "  <MovieTitle>" << _movie_title << "</MovieTitle>\n"
394           << "  <ReelNumber>" << _reel_number << "</ReelNumber>\n"
395           << "  <Language>" << _language << "</Language>\n"
396           << "  <LoadFont Id=\"theFontId\" URI=\"arial.ttf\"/>";
397
398         _subtitles.sort (SubtitleSorter ());
399
400         /* XXX: multiple fonts not supported */
401         /* XXX: script, underlined, weight not supported */
402
403         bool first = true;
404         bool italic = false;
405         Color color;
406         int size = 0;
407         Effect effect = NONE;
408         Color effect_color;
409         int spot_number = 1;
410         Time last_in;
411         Time last_out;
412         Time last_fade_up_time;
413         Time last_fade_down_time;
414
415         for (list<shared_ptr<Subtitle> >::iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) {
416
417                 stringstream a;
418                 if (first || italic != (*i)->italic()) {
419                         italic = (*i)->italic ();
420                         a << "Italic=\"" << (italic ? "yes" : "no") << "\" ";
421                 }
422
423                 if (first || color != (*i)->color()) {
424                         color = (*i)->color ();
425                         a << "Color=\"" << color.to_argb_string() << "\" ";
426                 }
427
428                 if (size || size != (*i)->size()) {
429                         size = (*i)->size ();
430                         a << "Size=\"" << size << "\" ";
431                 }
432
433                 if (first || effect != (*i)->effect()) {
434                         effect = (*i)->effect ();
435                         a << "Effect=\"" << effect_to_string(effect) << "\" ";
436                 }
437
438                 if (first || effect_color != (*i)->effect_color()) {
439                         effect_color = (*i)->effect_color ();
440                         a << "EffectColor=\"" << effect_color.to_argb_string() << "\" ";
441                 }
442
443                 if (first) {
444                         a << "Script=\"normal\" Underlined=\"no\" Weight=\"normal\">";
445                 }
446
447                 if (!a.str().empty()) {
448                         if (!first) {
449                                 f << "  </Font>\n";
450                         } else {
451                                 f << "  <Font Id=\"theFontId\" " << a << ">\n";
452                         }
453                 }
454
455                 if (first ||
456                     (last_in != (*i)->in() ||
457                      last_out != (*i)->out() ||
458                      last_fade_up_time != (*i)->fade_up_time() ||
459                      last_fade_down_time != (*i)->fade_down_time()
460                             )) {
461
462                         if (!first) {
463                                 f << "  </Subtitle>\n";
464                         }
465
466                         f << "  <Subtitle "
467                           << "SpotNumber=\"" << spot_number++ << "\" "
468                           << "TimeIn=" << (*i)->in().to_string() << "\" "
469                           << "TimeOut=\"" << (*i)->out().to_string() << "\" "
470                           << "FadeUpTime=\"" << (*i)->fade_up_time().to_ticks() << "\" "
471                           << "FadeDownTime=\"" << (*i)->fade_down_time().to_ticks() << "\" "
472                           << ">\n";
473
474                         last_in = (*i)->in ();
475                         last_out = (*i)->out ();
476                         last_fade_up_time = (*i)->fade_up_time ();
477                         last_fade_down_time = (*i)->fade_down_time ();
478                 }
479
480                 f << "      <Text "
481                   << "VAlign=\"" << valign_to_string ((*i)->v_align()) << "\" "
482                   << "VPosition=\"" << (*i)->v_position() << "\" "
483                   << ">" << (*i)->text() << "</Text>\n";
484
485                 first = false;
486         }
487
488         f << "  </Subtitle>\n";
489         f << "</Font>\n";
490 }