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