f338517e407e35a4136eb1864d7db84b7f5bbd30
[libdcp.git] / src / subtitle_content.cc
1 /*
2     Copyright (C) 2012-2014 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 "subtitle_content.h"
21 #include "util.h"
22 #include "xml.h"
23 #include "font.h"
24 #include "text.h"
25 #include "load_font.h"
26 #include "subtitle_string.h"
27 #include <libxml++/nodes/element.h>
28 #include <boost/lexical_cast.hpp>
29 #include <boost/algorithm/string.hpp>
30 #include <fstream>
31
32 using std::string;
33 using std::list;
34 using std::ostream;
35 using std::ofstream;
36 using std::stringstream;
37 using std::cout;
38 using boost::shared_ptr;
39 using boost::lexical_cast;
40 using boost::optional;
41 using namespace dcp;
42
43 SubtitleContent::SubtitleContent (boost::filesystem::path file)
44         : Content (file)
45         , _need_sort (false)
46 {
47         shared_ptr<cxml::Document> xml (new cxml::Document ("DCSubtitle"));
48         xml->read_file (file);
49         
50         _id = xml->string_child ("SubtitleID");
51         _movie_title = xml->string_child ("MovieTitle");
52         _reel_number = xml->string_child ("ReelNumber");
53         _language = xml->string_child ("Language");
54
55         xml->ignore_child ("LoadFont");
56
57         list<shared_ptr<dcp::Font> > font_nodes = type_children<dcp::Font> (xml, "Font");
58         _load_font_nodes = type_children<dcp::LoadFont> (xml, "LoadFont");
59
60         /* Now make Subtitle objects to represent the raw XML nodes
61            in a sane way.
62         */
63         
64         ParseState parse_state;
65         examine_font_nodes (xml, font_nodes, parse_state);
66 }
67
68 SubtitleContent::SubtitleContent (Fraction edit_rate, string movie_title, string language)
69         : Content (edit_rate)
70         , _movie_title (movie_title)
71         , _reel_number ("1")
72         , _language (language)
73         , _need_sort (false)
74 {
75
76 }
77
78 void
79 SubtitleContent::examine_font_nodes (
80         shared_ptr<const cxml::Node> xml,
81         list<shared_ptr<dcp::Font> > const & font_nodes,
82         ParseState& parse_state
83         )
84 {
85         for (list<shared_ptr<dcp::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<dcp::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 SubtitleContent::examine_text_nodes (
106         shared_ptr<const cxml::Node> xml,
107         list<shared_ptr<dcp::Text> > const & text_nodes,
108         ParseState& parse_state
109         )
110 {
111         for (list<shared_ptr<dcp::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 SubtitleContent::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         dcp::Font effective_font (parse_state.font_nodes);
134         dcp::Text effective_text (*parse_state.text_nodes.back ());
135         dcp::Subtitle effective_subtitle (*parse_state.subtitle_nodes.back ());
136
137         _subtitles.push_back (
138                 shared_ptr<SubtitleString> (
139                         new SubtitleString (
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<SubtitleString> >
159 SubtitleContent::subtitles_at (Time t) const
160 {
161         list<shared_ptr<SubtitleString> > s;
162         for (list<shared_ptr<SubtitleString> >::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 SubtitleContent::font_id_to_name (string id) const
173 {
174         list<shared_ptr<dcp::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 void
191 SubtitleContent::add (shared_ptr<SubtitleString> s)
192 {
193         _subtitles.push_back (s);
194         _need_sort = true;
195 }
196
197 struct SubtitleSorter {
198         bool operator() (shared_ptr<SubtitleString> a, shared_ptr<SubtitleString> b) {
199                 if (a->in() != b->in()) {
200                         return a->in() < b->in();
201                 }
202                 return a->v_position() < b->v_position();
203         }
204 };
205
206 void
207 SubtitleContent::write_xml () const
208 {
209         FILE* f = fopen_boost (file (), "r");
210         Glib::ustring const s = xml_as_string ();
211         fwrite (s.c_str(), 1, s.length(), f);
212         fclose (f);
213 }
214
215 Glib::ustring
216 SubtitleContent::xml_as_string () const
217 {
218         xmlpp::Document doc;
219         xmlpp::Element* root = doc.create_root_node ("DCSubtitle");
220         root->set_attribute ("Version", "1.0");
221
222         root->add_child("SubtitleID")->add_child_text (_id);
223         root->add_child("MovieTitle")->add_child_text (_movie_title);
224         root->add_child("ReelNumber")->add_child_text (lexical_cast<string> (_reel_number));
225         root->add_child("Language")->add_child_text (_language);
226
227         if (_load_font_nodes.size() > 1) {
228                 boost::throw_exception (MiscError ("multiple LoadFont nodes not supported"));
229         }
230
231         if (!_load_font_nodes.empty ()) {
232                 xmlpp::Element* load_font = root->add_child("LoadFont");
233                 load_font->set_attribute("Id", _load_font_nodes.front()->id);
234                 load_font->set_attribute("URI", _load_font_nodes.front()->uri);
235         }
236
237         list<shared_ptr<SubtitleString> > sorted = _subtitles;
238         if (_need_sort) {
239                 sorted.sort (SubtitleSorter ());
240         }
241
242         /* XXX: multiple fonts not supported */
243         /* XXX: script, underlined, weight not supported */
244
245         bool italic = false;
246         Color color;
247         int size = 0;
248         Effect effect = NONE;
249         Color effect_color;
250         int spot_number = 1;
251         Time last_in;
252         Time last_out;
253         Time last_fade_up_time;
254         Time last_fade_down_time;
255
256         xmlpp::Element* font = 0;
257         xmlpp::Element* subtitle = 0;
258
259         for (list<shared_ptr<SubtitleString> >::iterator i = sorted.begin(); i != sorted.end(); ++i) {
260
261                 /* We will start a new <Font>...</Font> whenever some font property changes.
262                    I suppose we should really make an optimal hierarchy of <Font> tags, but
263                    that seems hard.
264                 */
265
266                 bool const font_changed =
267                         italic       != (*i)->italic()       ||
268                         color        != (*i)->color()        ||
269                         size         != (*i)->size()         ||
270                         effect       != (*i)->effect()       ||
271                         effect_color != (*i)->effect_color();
272
273                 if (font_changed) {
274                         italic = (*i)->italic ();
275                         color = (*i)->color ();
276                         size = (*i)->size ();
277                         effect = (*i)->effect ();
278                         effect_color = (*i)->effect_color ();
279                 }
280
281                 if (!font || font_changed) {
282                         font = root->add_child ("Font");
283                         string id = "theFontId";
284                         if (!_load_font_nodes.empty()) {
285                                 id = _load_font_nodes.front()->id;
286                         }
287                         font->set_attribute ("Id", id);
288                         font->set_attribute ("Italic", italic ? "yes" : "no");
289                         font->set_attribute ("Color", color.to_argb_string());
290                         font->set_attribute ("Size", lexical_cast<string> (size));
291                         font->set_attribute ("Effect", effect_to_string (effect));
292                         font->set_attribute ("EffectColor", effect_color.to_argb_string());
293                         font->set_attribute ("Script", "normal");
294                         font->set_attribute ("Underlined", "no");
295                         font->set_attribute ("Weight", "normal");
296                 }
297
298                 if (!subtitle || font_changed ||
299                     (last_in != (*i)->in() ||
300                      last_out != (*i)->out() ||
301                      last_fade_up_time != (*i)->fade_up_time() ||
302                      last_fade_down_time != (*i)->fade_down_time()
303                             )) {
304
305                         subtitle = font->add_child ("Subtitle");
306                         subtitle->set_attribute ("SpotNumber", lexical_cast<string> (spot_number++));
307                         subtitle->set_attribute ("TimeIn", (*i)->in().to_string());
308                         subtitle->set_attribute ("TimeOut", (*i)->out().to_string());
309                         subtitle->set_attribute ("FadeUpTime", lexical_cast<string> ((*i)->fade_up_time().to_ticks()));
310                         subtitle->set_attribute ("FadeDownTime", lexical_cast<string> ((*i)->fade_down_time().to_ticks()));
311
312                         last_in = (*i)->in ();
313                         last_out = (*i)->out ();
314                         last_fade_up_time = (*i)->fade_up_time ();
315                         last_fade_down_time = (*i)->fade_down_time ();
316                 }
317
318                 xmlpp::Element* text = subtitle->add_child ("Text");
319                 text->set_attribute ("VAlign", valign_to_string ((*i)->v_align()));             
320                 text->set_attribute ("VPosition", lexical_cast<string> ((*i)->v_position()));
321                 text->add_child_text ((*i)->text());
322         }
323
324         return doc.write_to_string_formatted ("UTF-8");
325 }
326