Template-ize collect so that any container can be used.
[libsub.git] / src / dcp_reader.cc
1 /*
2     Copyright (C) 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 "dcp_reader.h"
21 #include "vertical_reference.h"
22 #include "xml.h"
23 #include <libcxml/cxml.h>
24 #include <boost/algorithm/string.hpp>
25 #include <boost/lexical_cast.hpp>
26
27 using std::string;
28 using std::list;
29 using std::vector;
30 using std::istream;
31 using std::cout;
32 using boost::shared_ptr;
33 using boost::optional;
34 using boost::lexical_cast;
35 using boost::is_any_of;
36 using namespace sub;
37
38 namespace sub {
39
40 class DCPFont;
41
42 /** @class DCPText
43  *  @brief A DCP subtitle &lt;Text&gt; node.
44  */
45 class DCPText
46 {
47 public:
48         DCPText ()
49                 : v_position (0)
50                 , v_align (TOP_OF_SCREEN)
51         {}
52         
53         DCPText (shared_ptr<const cxml::Node> node)
54                 : v_align (CENTRE_OF_SCREEN)
55         {
56                 text = node->content ();
57                 v_position = node->number_attribute<float> ("VPosition");
58                 optional<string> v = node->optional_string_attribute ("VAlign");
59                 if (v) {
60                         v_align = string_to_vertical_reference (v.get ());
61                 }
62                 
63                 font_nodes = type_children<DCPFont> (node, "Font");
64         }
65
66         float v_position;
67         VerticalReference v_align;
68         string text;
69         shared_ptr<DCPFont> foo;
70         list<shared_ptr<DCPFont> > font_nodes;
71 };
72
73 /** @class DCPSubtitle
74  *  @brief A DCP subtitle &lt;Subtitle&gt; node.
75  */
76 class DCPSubtitle 
77 {
78 public:
79         DCPSubtitle () {}
80         DCPSubtitle (shared_ptr<const cxml::Node> node)
81         {
82                 in = MetricTime (time (node->string_attribute ("TimeIn")));
83                 out = MetricTime (time (node->string_attribute ("TimeOut")));
84                 font_nodes = type_children<DCPFont> (node, "Font");
85                 text_nodes = type_children<DCPText> (node, "Text");
86                 fade_up_time = fade_time (node, "FadeUpTime");
87                 fade_down_time = fade_time (node, "FadeDownTime");
88         }
89
90         MetricTime in;
91         MetricTime out;
92         MetricTime fade_up_time;
93         MetricTime fade_down_time;
94         list<shared_ptr<DCPFont> > font_nodes;
95         list<shared_ptr<DCPText> > text_nodes;
96
97 private:
98         static MetricTime time (std::string time)
99         {
100                 vector<string> b;
101                 split (b, time, is_any_of (":"));
102                 if (b.size() != 4) {
103                         boost::throw_exception (XMLError ("unrecognised time specification"));
104                 }
105
106                 return MetricTime (lexical_cast<int>(b[0]), lexical_cast<int> (b[1]), lexical_cast<int> (b[2]), lexical_cast<int> (b[3]) * 4);
107         }
108         
109         MetricTime fade_time (shared_ptr<const cxml::Node> node, string name)
110         {
111                 string const u = node->optional_string_attribute (name).get_value_or ("");
112                 MetricTime t;
113                 
114                 if (u.empty ()) {
115                         t = MetricTime (0, 0, 0, 80);
116                 } else if (u.find (":") != string::npos) {
117                         t = time (u);
118                 } else {
119                         t = MetricTime (0, 0, 0, lexical_cast<int>(u) * 4);
120                 }
121                 
122                 if (t > MetricTime (0, 0, 8, 0)) {
123                         t = MetricTime (0, 0, 8, 0);
124                 }
125                 
126                 return t;
127         }
128 };
129
130 /** @class DCPFont
131  *  @brief A DCP subtitle &lt;Font&gt; node.
132  */
133 class DCPFont 
134 {
135 public:
136         DCPFont ()
137                 : size (0)
138         {}
139         
140         DCPFont (shared_ptr<const cxml::Node> node)
141         {
142                 text = node->content ();
143                 
144                 id = node->optional_string_attribute ("Id").get_value_or ("");
145                 size = node->optional_number_attribute<int64_t> ("Size").get_value_or (0);
146                 italic = node->optional_bool_attribute ("Italic");
147                 optional<string> c = node->optional_string_attribute ("Color");
148                 if (c) {
149                         colour = Colour (c.get ());
150                 }
151                 optional<string> const e = node->optional_string_attribute ("Effect");
152                 if (e) {
153                         effect = string_to_effect (e.get ());
154                 }
155                 c = node->optional_string_attribute ( "EffectColor");
156                 if (c) {
157                         effect_colour = Colour (c.get ());
158                 }
159                 subtitle_nodes = type_children<DCPSubtitle> (node, "Subtitle");
160                 font_nodes = type_children<DCPFont> (node, "Font");
161                 text_nodes = type_children<DCPText> (node, "Text");
162         }
163         
164         DCPFont (list<shared_ptr<DCPFont> > const & font_nodes)
165                 : size (0)
166                 , italic (false)
167                 , colour ("FFFFFFFF")
168                 , effect_colour ("FFFFFFFF")
169         {
170                 for (list<shared_ptr<DCPFont> >::const_iterator i = font_nodes.begin(); i != font_nodes.end(); ++i) {
171                         if (!(*i)->id.empty ()) {
172                                 id = (*i)->id;
173                         }
174                         if ((*i)->size != 0) {
175                                 size = (*i)->size;
176                         }
177                         if ((*i)->italic) {
178                                 italic = (*i)->italic.get ();
179                         }
180                         if ((*i)->colour) {
181                                 colour = (*i)->colour.get ();
182                         }
183                         if ((*i)->effect) {
184                                 effect = (*i)->effect.get ();
185                         }
186                         if ((*i)->effect_colour) {
187                                 effect_colour = (*i)->effect_colour.get ();
188                         }
189                 }
190         }
191
192         string text;
193         string id;
194         int size;
195         optional<bool> italic;
196         optional<Colour> colour;
197         optional<Effect> effect;
198         optional<Colour> effect_colour;
199         
200         list<shared_ptr<DCPSubtitle> > subtitle_nodes;
201         list<shared_ptr<DCPFont> > font_nodes;
202         list<shared_ptr<DCPText> > text_nodes;
203 };
204
205 /** @class DCPLoadFont
206  *  @brief A DCP subtitle &lt;LoadFont&gt; node.
207  */
208 class DCPLoadFont 
209 {
210 public:
211         DCPLoadFont () {}
212         DCPLoadFont (shared_ptr<const cxml::Node> node)
213         {
214                 id = node->string_attribute ("Id");
215                 uri = node->string_attribute ("URI");
216         }
217
218         string id;
219         string uri;
220 };
221
222 /** @class DCPReader::ParseState
223  *  @brief Holder of state for use while reading DCP subtitles.
224  */
225 struct DCPReader::ParseState {
226         list<shared_ptr<DCPFont> > font_nodes;
227         list<shared_ptr<DCPText> > text_nodes;
228         list<shared_ptr<DCPSubtitle> > subtitle_nodes;
229 };
230
231 }
232
233 /** @param s A string.
234  *  @return true if the string contains only space, newline or tab characters, or is empty.
235  */
236 static bool
237 empty_or_white_space (string s)
238 {
239         for (size_t i = 0; i < s.length(); ++i) {
240                 if (s[i] != ' ' && s[i] != '\n' && s[i] != '\t') {
241                         return false;
242                 }
243         }
244
245         return true;
246 }
247
248 string
249 DCPReader::font_id_to_name (string id) const
250 {
251         list<shared_ptr<DCPLoadFont> >::const_iterator i = _load_font_nodes.begin();
252         while (i != _load_font_nodes.end() && (*i)->id != id) {
253                 ++i;
254         }
255
256         if (i == _load_font_nodes.end ()) {
257                 return "";
258         }
259
260         if ((*i)->uri == "arial.ttf" || (*i)->uri == "Arial.ttf") {
261                 return "Arial";
262         }
263
264         return (*i)->uri;
265 }
266
267 /** @class DCPReader
268  *  @brief A class to read DCP subtitles.
269  */
270 DCPReader::DCPReader (istream& in)
271 {
272         shared_ptr<cxml::Document> xml (new cxml::Document ("DCSubtitle"));
273         xml->read_stream (in);
274
275         xml->ignore_child ("SubtitleID");
276         xml->ignore_child ("MovieTitle");
277         xml->ignore_child ("ReelNumber");
278         xml->ignore_child ("Language");
279
280         list<shared_ptr<DCPFont> > font_nodes = type_children<DCPFont> (xml, "Font");
281         _load_font_nodes = type_children<DCPLoadFont> (xml, "LoadFont");
282         
283         /* Now make Subtitle objects to represent the raw XML nodes
284            in a sane way.
285         */
286
287         ParseState parse_state;
288         examine_font_nodes (xml, font_nodes, parse_state);
289 }
290
291 void
292 DCPReader::examine_font_nodes (
293         shared_ptr<const cxml::Node> xml,
294         list<shared_ptr<DCPFont> > const & font_nodes,
295         ParseState& parse_state
296         )
297 {
298         for (list<shared_ptr<DCPFont> >::const_iterator i = font_nodes.begin(); i != font_nodes.end(); ++i) {
299
300                 parse_state.font_nodes.push_back (*i);
301                 maybe_add_subtitle ((*i)->text, parse_state);
302
303                 for (list<shared_ptr<DCPSubtitle> >::iterator j = (*i)->subtitle_nodes.begin(); j != (*i)->subtitle_nodes.end(); ++j) {
304                         parse_state.subtitle_nodes.push_back (*j);
305                         examine_text_nodes (xml, (*j)->text_nodes, parse_state);
306                         examine_font_nodes (xml, (*j)->font_nodes, parse_state);
307                         parse_state.subtitle_nodes.pop_back ();
308                 }
309         
310                 examine_font_nodes (xml, (*i)->font_nodes, parse_state);
311                 examine_text_nodes (xml, (*i)->text_nodes, parse_state);
312                 
313                 parse_state.font_nodes.pop_back ();
314         }
315 }
316
317 void
318 DCPReader::examine_text_nodes (
319         shared_ptr<const cxml::Node> xml,
320         list<shared_ptr<DCPText> > const & text_nodes,
321         ParseState& parse_state
322         )
323 {
324         for (list<shared_ptr<DCPText> >::const_iterator i = text_nodes.begin(); i != text_nodes.end(); ++i) {
325                 parse_state.text_nodes.push_back (*i);
326                 maybe_add_subtitle ((*i)->text, parse_state);
327                 examine_font_nodes (xml, (*i)->font_nodes, parse_state);
328                 parse_state.text_nodes.pop_back ();
329         }
330 }
331
332 void
333 DCPReader::maybe_add_subtitle (string text, ParseState& parse_state)
334 {
335         if (empty_or_white_space (text)) {
336                 return;
337         }
338         
339         if (parse_state.text_nodes.empty() || parse_state.subtitle_nodes.empty ()) {
340                 return;
341         }
342
343         DCPFont effective_font (parse_state.font_nodes);
344         DCPText effective_text (*parse_state.text_nodes.back ());
345         DCPSubtitle effective_subtitle (*parse_state.subtitle_nodes.back ());
346
347         RawSubtitle sub;
348
349         sub.vertical_position.proportional = float (effective_text.v_position) / 100;
350         sub.vertical_position.reference = effective_text.v_align;
351         sub.from.set_metric (effective_subtitle.in);
352         sub.to.set_metric (effective_subtitle.out);
353         sub.fade_up = effective_subtitle.fade_up_time;
354         sub.fade_down = effective_subtitle.fade_down_time;
355                 
356         sub.text = text;
357         sub.font = font_id_to_name (effective_font.id);
358         sub.font_size.set_proportional (float (effective_font.size) / (72 * 11));
359         sub.effect = effective_font.effect;
360         sub.effect_colour = effective_font.effect_colour;
361         sub.colour = effective_font.colour.get ();
362         sub.italic = effective_font.italic.get ();
363
364         _subs.push_back (sub);
365 }