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