Rename Subtitle -> Text
[dcpomatic.git] / src / lib / text_content.cc
1 /*
2     Copyright (C) 2013-2018 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21 #include "text_content.h"
22 #include "util.h"
23 #include "exceptions.h"
24 #include "font.h"
25 #include "content.h"
26 #include <dcp/raw_convert.h>
27 #include <libcxml/cxml.h>
28 #include <libxml++/libxml++.h>
29 #include <boost/foreach.hpp>
30 #include <iostream>
31
32 #include "i18n.h"
33
34 using std::string;
35 using std::vector;
36 using std::cout;
37 using std::list;
38 using boost::shared_ptr;
39 using boost::dynamic_pointer_cast;
40 using boost::optional;
41 using dcp::raw_convert;
42
43 int const TextContentProperty::X_OFFSET = 500;
44 int const TextContentProperty::Y_OFFSET = 501;
45 int const TextContentProperty::X_SCALE = 502;
46 int const TextContentProperty::Y_SCALE = 503;
47 int const TextContentProperty::USE = 504;
48 int const TextContentProperty::BURN = 505;
49 int const TextContentProperty::LANGUAGE = 506;
50 int const TextContentProperty::FONTS = 507;
51 int const TextContentProperty::COLOUR = 508;
52 int const TextContentProperty::EFFECT = 509;
53 int const TextContentProperty::EFFECT_COLOUR = 510;
54 int const TextContentProperty::LINE_SPACING = 511;
55 int const TextContentProperty::FADE_IN = 512;
56 int const TextContentProperty::FADE_OUT = 513;
57 int const TextContentProperty::OUTLINE_WIDTH = 514;
58
59 TextContent::TextContent (Content* parent)
60         : ContentPart (parent)
61         , _use (false)
62         , _burn (false)
63         , _x_offset (0)
64         , _y_offset (0)
65         , _x_scale (1)
66         , _y_scale (1)
67         , _line_spacing (1)
68         , _outline_width (2)
69 {
70
71 }
72
73 shared_ptr<TextContent>
74 TextContent::from_xml (Content* parent, cxml::ConstNodePtr node, int version)
75 {
76         if (version < 34) {
77                 /* With old metadata FFmpeg content has the subtitle-related tags even with no
78                    subtitle streams, so check for that.
79                 */
80                 if (node->string_child("Type") == "FFmpeg" && node->node_children("SubtitleStream").empty()) {
81                         return shared_ptr<TextContent> ();
82                 }
83
84                 /* Otherwise we can drop through to the newer logic */
85         }
86
87         if (!node->optional_number_child<double>("SubtitleXOffset") && !node->optional_number_child<double>("SubtitleOffset")) {
88                 return shared_ptr<TextContent> ();
89         }
90
91         return shared_ptr<TextContent> (new TextContent (parent, node, version));
92 }
93
94 TextContent::TextContent (Content* parent, cxml::ConstNodePtr node, int version)
95         : ContentPart (parent)
96         , _use (false)
97         , _burn (false)
98         , _x_offset (0)
99         , _y_offset (0)
100         , _x_scale (1)
101         , _y_scale (1)
102         , _line_spacing (node->optional_number_child<double>("LineSpacing").get_value_or (1))
103         , _outline_width (node->optional_number_child<int>("OutlineWidth").get_value_or (2))
104 {
105         if (version >= 32) {
106                 _use = node->bool_child ("UseSubtitles");
107                 _burn = node->bool_child ("BurnSubtitles");
108         }
109
110         if (version >= 7) {
111                 _x_offset = node->number_child<double> ("SubtitleXOffset");
112                 _y_offset = node->number_child<double> ("SubtitleYOffset");
113         } else {
114                 _y_offset = node->number_child<double> ("SubtitleOffset");
115         }
116
117         if (node->optional_bool_child("Outline").get_value_or(false)) {
118                 _effect = dcp::BORDER;
119         } else if (node->optional_bool_child("Shadow").get_value_or(false)) {
120                 _effect = dcp::SHADOW;
121         } else {
122                 _effect = dcp::NONE;
123         }
124
125         optional<string> effect = node->optional_string_child("Effect");
126         if (effect) {
127                 if (*effect == "none") {
128                         _effect = dcp::NONE;
129                 } else if (*effect == "outline") {
130                         _effect = dcp::BORDER;
131                 } else if (*effect == "shadow") {
132                         _effect = dcp::SHADOW;
133                 }
134         }
135
136         if (version >= 10) {
137                 _x_scale = node->number_child<double> ("SubtitleXScale");
138                 _y_scale = node->number_child<double> ("SubtitleYScale");
139         } else {
140                 _x_scale = _y_scale = node->number_child<double> ("SubtitleScale");
141         }
142
143         optional<int> r = node->optional_number_child<int>("Red");
144         optional<int> g = node->optional_number_child<int>("Green");
145         optional<int> b = node->optional_number_child<int>("Blue");
146         if (r && g && b) {
147                 _colour = dcp::Colour (*r, *g, *b);
148         }
149
150         if (version >= 36) {
151                 optional<int> er = node->optional_number_child<int>("EffectRed");
152                 optional<int> eg = node->optional_number_child<int>("EffectGreen");
153                 optional<int> eb = node->optional_number_child<int>("EffectBlue");
154                 if (er && eg && eb) {
155                         _effect_colour = dcp::Colour (*er, *eg, *eb);
156                 }
157         } else {
158                 _effect_colour = dcp::Colour (
159                         node->optional_number_child<int>("OutlineRed").get_value_or(255),
160                         node->optional_number_child<int>("OutlineGreen").get_value_or(255),
161                         node->optional_number_child<int>("OutlineBlue").get_value_or(255)
162                         );
163         }
164
165         optional<Frame> fi = node->optional_number_child<Frame>("SubtitleFadeIn");
166         if (fi) {
167                 _fade_in = ContentTime (*fi);
168         }
169         optional<Frame> fo = node->optional_number_child<Frame>("SubtitleFadeOut");
170         if (fo) {
171                 _fade_out = ContentTime (*fo);
172         }
173
174         _language = node->optional_string_child ("SubtitleLanguage").get_value_or ("");
175
176         list<cxml::NodePtr> fonts = node->node_children ("Font");
177         for (list<cxml::NodePtr>::const_iterator i = fonts.begin(); i != fonts.end(); ++i) {
178                 _fonts.push_back (shared_ptr<Font> (new Font (*i)));
179         }
180
181         connect_to_fonts ();
182 }
183
184 TextContent::TextContent (Content* parent, vector<shared_ptr<Content> > c)
185         : ContentPart (parent)
186 {
187         shared_ptr<TextContent> ref = c[0]->subtitle;
188         DCPOMATIC_ASSERT (ref);
189         list<shared_ptr<Font> > ref_fonts = ref->fonts ();
190
191         for (size_t i = 1; i < c.size(); ++i) {
192
193                 if (c[i]->subtitle->use() != ref->use()) {
194                         throw JoinError (_("Content to be joined must have the same 'use subtitles' setting."));
195                 }
196
197                 if (c[i]->subtitle->burn() != ref->burn()) {
198                         throw JoinError (_("Content to be joined must have the same 'burn subtitles' setting."));
199                 }
200
201                 if (c[i]->subtitle->x_offset() != ref->x_offset()) {
202                         throw JoinError (_("Content to be joined must have the same subtitle X offset."));
203                 }
204
205                 if (c[i]->subtitle->y_offset() != ref->y_offset()) {
206                         throw JoinError (_("Content to be joined must have the same subtitle Y offset."));
207                 }
208
209                 if (c[i]->subtitle->x_scale() != ref->x_scale()) {
210                         throw JoinError (_("Content to be joined must have the same subtitle X scale."));
211                 }
212
213                 if (c[i]->subtitle->y_scale() != ref->y_scale()) {
214                         throw JoinError (_("Content to be joined must have the same subtitle Y scale."));
215                 }
216
217                 if (c[i]->subtitle->line_spacing() != ref->line_spacing()) {
218                         throw JoinError (_("Content to be joined must have the same subtitle line spacing."));
219                 }
220
221                 if ((c[i]->subtitle->fade_in() != ref->fade_in()) || (c[i]->subtitle->fade_out() != ref->fade_out())) {
222                         throw JoinError (_("Content to be joined must have the same subtitle fades."));
223                 }
224
225                 if ((c[i]->subtitle->outline_width() != ref->outline_width())) {
226                         throw JoinError (_("Content to be joined must have the same outline width."));
227                 }
228
229                 list<shared_ptr<Font> > fonts = c[i]->subtitle->fonts ();
230                 if (fonts.size() != ref_fonts.size()) {
231                         throw JoinError (_("Content to be joined must use the same fonts."));
232                 }
233
234                 list<shared_ptr<Font> >::const_iterator j = ref_fonts.begin ();
235                 list<shared_ptr<Font> >::const_iterator k = fonts.begin ();
236
237                 while (j != ref_fonts.end ()) {
238                         if (**j != **k) {
239                                 throw JoinError (_("Content to be joined must use the same fonts."));
240                         }
241                         ++j;
242                         ++k;
243                 }
244         }
245
246         _use = ref->use ();
247         _burn = ref->burn ();
248         _x_offset = ref->x_offset ();
249         _y_offset = ref->y_offset ();
250         _x_scale = ref->x_scale ();
251         _y_scale = ref->y_scale ();
252         _language = ref->language ();
253         _fonts = ref_fonts;
254         _line_spacing = ref->line_spacing ();
255         _fade_in = ref->fade_in ();
256         _fade_out = ref->fade_out ();
257         _outline_width = ref->outline_width ();
258
259         connect_to_fonts ();
260 }
261
262 /** _mutex must not be held on entry */
263 void
264 TextContent::as_xml (xmlpp::Node* root) const
265 {
266         boost::mutex::scoped_lock lm (_mutex);
267
268         root->add_child("UseSubtitles")->add_child_text (_use ? "1" : "0");
269         root->add_child("BurnSubtitles")->add_child_text (_burn ? "1" : "0");
270         root->add_child("SubtitleXOffset")->add_child_text (raw_convert<string> (_x_offset));
271         root->add_child("SubtitleYOffset")->add_child_text (raw_convert<string> (_y_offset));
272         root->add_child("SubtitleXScale")->add_child_text (raw_convert<string> (_x_scale));
273         root->add_child("SubtitleYScale")->add_child_text (raw_convert<string> (_y_scale));
274         root->add_child("SubtitleLanguage")->add_child_text (_language);
275         if (_colour) {
276                 root->add_child("Red")->add_child_text (raw_convert<string> (_colour->r));
277                 root->add_child("Green")->add_child_text (raw_convert<string> (_colour->g));
278                 root->add_child("Blue")->add_child_text (raw_convert<string> (_colour->b));
279         }
280         if (_effect) {
281                 switch (*_effect) {
282                 case dcp::NONE:
283                         root->add_child("Effect")->add_child_text("none");
284                         break;
285                 case dcp::BORDER:
286                         root->add_child("Effect")->add_child_text("outline");
287                         break;
288                 case dcp::SHADOW:
289                         root->add_child("Effect")->add_child_text("shadow");
290                         break;
291                 }
292         }
293         if (_effect_colour) {
294                 root->add_child("EffectRed")->add_child_text (raw_convert<string> (_effect_colour->r));
295                 root->add_child("EffectGreen")->add_child_text (raw_convert<string> (_effect_colour->g));
296                 root->add_child("EffectBlue")->add_child_text (raw_convert<string> (_effect_colour->b));
297         }
298         root->add_child("LineSpacing")->add_child_text (raw_convert<string> (_line_spacing));
299         if (_fade_in) {
300                 root->add_child("SubtitleFadeIn")->add_child_text (raw_convert<string> (_fade_in->get()));
301         }
302         if (_fade_out) {
303                 root->add_child("SubtitleFadeOut")->add_child_text (raw_convert<string> (_fade_out->get()));
304         }
305         root->add_child("OutlineWidth")->add_child_text (raw_convert<string> (_outline_width));
306
307         for (list<shared_ptr<Font> >::const_iterator i = _fonts.begin(); i != _fonts.end(); ++i) {
308                 (*i)->as_xml (root->add_child("Font"));
309         }
310 }
311
312 string
313 TextContent::identifier () const
314 {
315         string s = raw_convert<string> (x_scale())
316                 + "_" + raw_convert<string> (y_scale())
317                 + "_" + raw_convert<string> (x_offset())
318                 + "_" + raw_convert<string> (y_offset())
319                 + "_" + raw_convert<string> (line_spacing())
320                 + "_" + raw_convert<string> (fade_in().get_value_or(ContentTime()).get())
321                 + "_" + raw_convert<string> (fade_out().get_value_or(ContentTime()).get())
322                 + "_" + raw_convert<string> (outline_width())
323                 + "_" + raw_convert<string> (colour().get_value_or(dcp::Colour(255, 255, 255)).to_argb_string())
324                 + "_" + raw_convert<string> (dcp::effect_to_string(effect().get_value_or(dcp::NONE)))
325                 + "_" + raw_convert<string> (effect_colour().get_value_or(dcp::Colour(0, 0, 0)).to_argb_string());
326
327         /* XXX: I suppose really _fonts shouldn't be in here, since not all
328            types of subtitle content involve fonts.
329         */
330         BOOST_FOREACH (shared_ptr<Font> f, _fonts) {
331                 for (int i = 0; i < FontFiles::VARIANTS; ++i) {
332                         s += "_" + f->file(static_cast<FontFiles::Variant>(i)).get_value_or("Default").string();
333                 }
334         }
335
336         /* The language is for metadata only, and doesn't affect
337            how this content looks.
338         */
339
340         return s;
341 }
342
343 void
344 TextContent::add_font (shared_ptr<Font> font)
345 {
346         _fonts.push_back (font);
347         connect_to_fonts ();
348 }
349
350 void
351 TextContent::connect_to_fonts ()
352 {
353         BOOST_FOREACH (boost::signals2::connection& i, _font_connections) {
354                 i.disconnect ();
355         }
356
357         _font_connections.clear ();
358
359         BOOST_FOREACH (shared_ptr<Font> i, _fonts) {
360                 _font_connections.push_back (i->Changed.connect (boost::bind (&TextContent::font_changed, this)));
361         }
362 }
363
364 void
365 TextContent::font_changed ()
366 {
367         _parent->signal_changed (TextContentProperty::FONTS);
368 }
369
370 void
371 TextContent::set_colour (dcp::Colour colour)
372 {
373         maybe_set (_colour, colour, TextContentProperty::COLOUR);
374 }
375
376 void
377 TextContent::unset_colour ()
378 {
379         maybe_set (_colour, optional<dcp::Colour>(), TextContentProperty::COLOUR);
380 }
381
382 void
383 TextContent::set_effect (dcp::Effect e)
384 {
385         maybe_set (_effect, e, TextContentProperty::EFFECT);
386 }
387
388 void
389 TextContent::unset_effect ()
390 {
391         maybe_set (_effect, optional<dcp::Effect>(), TextContentProperty::EFFECT);
392 }
393
394 void
395 TextContent::set_effect_colour (dcp::Colour colour)
396 {
397         maybe_set (_effect_colour, colour, TextContentProperty::EFFECT_COLOUR);
398 }
399
400 void
401 TextContent::unset_effect_colour ()
402 {
403         maybe_set (_effect_colour, optional<dcp::Colour>(), TextContentProperty::EFFECT_COLOUR);
404 }
405
406 void
407 TextContent::set_use (bool u)
408 {
409         maybe_set (_use, u, TextContentProperty::USE);
410 }
411
412 void
413 TextContent::set_burn (bool b)
414 {
415         maybe_set (_burn, b, TextContentProperty::BURN);
416 }
417
418 void
419 TextContent::set_x_offset (double o)
420 {
421         maybe_set (_x_offset, o, TextContentProperty::X_OFFSET);
422 }
423
424 void
425 TextContent::set_y_offset (double o)
426 {
427         maybe_set (_y_offset, o, TextContentProperty::Y_OFFSET);
428 }
429
430 void
431 TextContent::set_x_scale (double s)
432 {
433         maybe_set (_x_scale, s, TextContentProperty::X_SCALE);
434 }
435
436 void
437 TextContent::set_y_scale (double s)
438 {
439         maybe_set (_y_scale, s, TextContentProperty::Y_SCALE);
440 }
441
442 void
443 TextContent::set_language (string language)
444 {
445         maybe_set (_language, language, TextContentProperty::LANGUAGE);
446 }
447
448 void
449 TextContent::set_line_spacing (double s)
450 {
451         maybe_set (_line_spacing, s, TextContentProperty::LINE_SPACING);
452 }
453
454 void
455 TextContent::set_fade_in (ContentTime t)
456 {
457         maybe_set (_fade_in, t, TextContentProperty::FADE_IN);
458 }
459
460 void
461 TextContent::unset_fade_in ()
462 {
463         maybe_set (_fade_in, optional<ContentTime>(), TextContentProperty::FADE_IN);
464 }
465
466 void
467 TextContent::set_fade_out (ContentTime t)
468 {
469         maybe_set (_fade_out, t, TextContentProperty::FADE_OUT);
470 }
471
472 void
473 TextContent::unset_fade_out ()
474 {
475         maybe_set (_fade_out, optional<ContentTime>(), TextContentProperty::FADE_OUT);
476 }
477
478 void
479 TextContent::set_outline_width (int w)
480 {
481         maybe_set (_outline_width, w, TextContentProperty::OUTLINE_WIDTH);
482 }
483
484 void
485 TextContent::take_settings_from (shared_ptr<const TextContent> c)
486 {
487         set_use (c->_use);
488         set_burn (c->_burn);
489         set_x_offset (c->_x_offset);
490         set_y_offset (c->_y_offset);
491         set_x_scale (c->_x_scale);
492         set_y_scale (c->_y_scale);
493         maybe_set (_fonts, c->_fonts, TextContentProperty::FONTS);
494         if (c->_colour) {
495                 set_colour (*c->_colour);
496         } else {
497                 unset_colour ();
498         }
499         if (c->_effect) {
500                 set_effect (*c->_effect);
501         }
502         if (c->_effect_colour) {
503                 set_effect_colour (*c->_effect_colour);
504         } else {
505                 unset_effect_colour ();
506         }
507         set_line_spacing (c->_line_spacing);
508         if (c->_fade_in) {
509                 set_fade_in (*c->_fade_in);
510         }
511         if (c->_fade_out) {
512                 set_fade_out (*c->_fade_out);
513         }
514         set_outline_width (c->_outline_width);
515 }