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