Support MarginL and MarginR in SSA subtitles (DoM #2811).
[libsub.git] / src / ssa_reader.cc
1 /*
2     Copyright (C) 2016-2019 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 "ssa_reader.h"
21 #include "util.h"
22 #include "sub_assert.h"
23 #include "raw_convert.h"
24 #include "subtitle.h"
25 #include "compose.hpp"
26 #include <boost/algorithm/string.hpp>
27 #include <boost/bind/bind.hpp>
28 #include <cstdlib>
29 #include <iostream>
30 #include <vector>
31
32 using std::string;
33 using std::vector;
34 using std::map;
35 using std::cout;
36 using boost::optional;
37 using boost::function;
38 using namespace boost::algorithm;
39 #if BOOST_VERSION >= 106100
40 using namespace boost::placeholders;
41 #endif
42 using namespace sub;
43
44 /** @param s Subtitle string encoded in UTF-8 */
45 SSAReader::SSAReader (string s)
46 {
47         this->read (boost::bind(&get_line_string, &s));
48 }
49
50 /** @param f Subtitle file encoded in UTF-8 */
51 SSAReader::SSAReader (FILE* f)
52 {
53         this->read (boost::bind (&get_line_file, f));
54 }
55
56 Colour
57 h_colour (string s)
58 {
59         if (s.empty() || s[0] != '&' || s[1] != 'H') {
60                 throw SSAError(String::compose("Badly formatted colour tag %1", s));
61         }
62
63         auto start = s.c_str();
64         auto const end = start + s.length();
65         while (start < end && (*start == '&' || *start == 'H')) {
66                 ++start;
67         }
68
69         auto const colour = strtoll(start, nullptr, 16);
70
71         /* XXX: ignoring alpha channel here; note that 00 is opaque and FF is transparent */
72         return sub::Colour(
73                 ((colour & 0x000000ff) >> 0) / 255.0,
74                 ((colour & 0x0000ff00) >> 8) / 255.0,
75                 ((colour & 0x00ff0000) >> 16) / 255.0
76                 );
77 }
78
79 class Style
80 {
81 public:
82         Style ()
83                 : font_size (72)
84                 , primary_colour (255, 255, 255)
85                 , bold (false)
86                 , italic (false)
87                 , underline (false)
88                 , horizontal_reference (HORIZONTAL_CENTRE_OF_SCREEN)
89                 , vertical_reference (BOTTOM_OF_SCREEN)
90                 , vertical_margin (0)
91         {}
92
93         Style (string format_line, string style_line)
94                 : font_size (72)
95                 , primary_colour (255, 255, 255)
96                 , bold (false)
97                 , italic (false)
98                 , underline (false)
99                 , horizontal_reference (HORIZONTAL_CENTRE_OF_SCREEN)
100                 , vertical_reference (BOTTOM_OF_SCREEN)
101                 , vertical_margin (0)
102         {
103                 vector<string> keys;
104                 split (keys, format_line, boost::is_any_of (","));
105                 vector<string> style;
106                 split (style, style_line, boost::is_any_of (","));
107
108                 SUB_ASSERT (!keys.empty());
109                 SUB_ASSERT (!style.empty());
110                 SUB_ASSERT (keys.size() == style.size());
111
112                 for (size_t i = 0; i < style.size(); ++i) {
113                         trim (keys[i]);
114                         trim (style[i]);
115                         if (keys[i] == "Name") {
116                                 name = style[i];
117                         } else if (keys[i] == "Fontname") {
118                                 font_name = style[i];
119                         } else if (keys[i] == "Fontsize") {
120                                 font_size = raw_convert<int> (style[i]);
121                         } else if (keys[i] == "PrimaryColour") {
122                                 primary_colour = colour (style[i]);
123                         } else if (keys[i] == "BackColour") {
124                                 back_colour = colour (style[i]);
125                         } else if (keys[i] == "Bold") {
126                                 bold = style[i] == "-1";
127                         } else if (keys[i] == "Italic") {
128                                 italic = style[i] == "-1";
129                         } else if (keys[i] == "Underline") {
130                                 underline = style[i] == "-1";
131                         } else if (keys[i] == "BorderStyle") {
132                                 if (style[i] == "1") {
133                                         effect = SHADOW;
134                                 }
135                         } else if (keys[i] == "Alignment") {
136                                 if (style[i] == "7" || style[i] == "8" || style[i] == "9") {
137                                         vertical_reference = TOP_OF_SCREEN;
138                                 } else if (style[i] == "4" || style[i] == "5" || style[i] == "6") {
139                                         vertical_reference = VERTICAL_CENTRE_OF_SCREEN;
140                                 } else {
141                                         vertical_reference = BOTTOM_OF_SCREEN;
142                                 }
143                                 if (style[i] == "1" || style[i] == "4" || style[i] == "7") {
144                                         horizontal_reference = LEFT_OF_SCREEN;
145                                 } else if (style[i] == "3" || style[i] == "6" || style[i] == "9") {
146                                         horizontal_reference = RIGHT_OF_SCREEN;
147                                 } else {
148                                         horizontal_reference = HORIZONTAL_CENTRE_OF_SCREEN;
149                                 }
150                         } else if (keys[i] == "MarginV") {
151                                 vertical_margin = raw_convert<int> (style[i]);
152                         } else if (keys[i] == "MarginL") {
153                                 left_margin = raw_convert<int>(style[i]);
154                         } else if (keys[i] == "MarginR") {
155                                 right_margin = raw_convert<int>(style[i]);
156                         }
157                 }
158         }
159
160         string name;
161         optional<string> font_name;
162         int font_size; ///< points
163         Colour primary_colour;
164         /** outline colour */
165         optional<Colour> back_colour;
166         bool bold;
167         bool italic;
168         bool underline;
169         optional<Effect> effect;
170         HorizontalReference horizontal_reference;
171         VerticalReference vertical_reference;
172         int vertical_margin;
173         int left_margin = 0;
174         int right_margin = 0;
175
176 private:
177         Colour colour (string c) const
178         {
179                 if (c.length() > 0 && c[0] == '&') {
180                         /* &Hbbggrr or &Haabbggrr */
181                         return h_colour (c);
182                 } else {
183                         /* integer */
184                         int i = raw_convert<int>(c);
185                         return Colour (
186                                 ((i & 0x0000ff) >>  0) / 255.0,
187                                 ((i & 0x00ff00) >>  8) / 255.0,
188                                 ((i & 0xff0000) >> 16) / 255.0
189                                 );
190                 }
191         }
192 };
193
194
195 void
196 SSAReader::Context::update_horizontal_position(RawSubtitle& sub) const
197 {
198         switch (sub.horizontal_position.reference) {
199         case LEFT_OF_SCREEN:
200                 sub.horizontal_position.proportional = static_cast<float>(left_margin) / play_res_x;
201                 break;
202         case HORIZONTAL_CENTRE_OF_SCREEN:
203                 sub.horizontal_position.proportional = static_cast<float>(left_margin - right_margin) / (2 * play_res_x);
204                 break;
205         case RIGHT_OF_SCREEN:
206                 sub.horizontal_position.proportional = static_cast<float>(right_margin) / play_res_x;
207                 break;
208         }
209 }
210
211
212 Time
213 SSAReader::parse_time (string t) const
214 {
215         vector<string> bits;
216         split (bits, t, is_any_of (":."));
217         SUB_ASSERT (bits.size() == 4);
218         return Time::from_hms (
219                 raw_convert<int> (bits[0]),
220                 raw_convert<int> (bits[1]),
221                 raw_convert<int> (bits[2]),
222                 raw_convert<int> (bits[3]) * 10
223                 );
224 }
225
226
227 void
228 SSAReader::parse_tag(RawSubtitle& sub, string tag, Context const& context)
229 {
230         if (tag == "\\i1") {
231                 sub.italic = true;
232         } else if (tag == "\\i0" || tag == "\\i") {
233                 sub.italic = false;
234         } else if (tag == "\\b1") {
235                 sub.bold = true;
236         } else if (tag == "\\b0") {
237                 sub.bold = false;
238         } else if (tag == "\\u1") {
239                 sub.underline = true;
240         } else if (tag == "\\u0") {
241                 sub.underline = false;
242         } else if (tag == "\\an1") {
243                 sub.horizontal_position.reference = sub::LEFT_OF_SCREEN;
244                 sub.vertical_position.reference = sub::BOTTOM_OF_SCREEN;
245                 context.update_horizontal_position(sub);
246         } else if (tag == "\\an2") {
247                 sub.horizontal_position.reference = sub::HORIZONTAL_CENTRE_OF_SCREEN;
248                 sub.vertical_position.reference = sub::BOTTOM_OF_SCREEN;
249                 context.update_horizontal_position(sub);
250         } else if (tag == "\\an3") {
251                 sub.horizontal_position.reference = sub::RIGHT_OF_SCREEN;
252                 sub.vertical_position.reference = sub::BOTTOM_OF_SCREEN;
253                 context.update_horizontal_position(sub);
254         } else if (tag == "\\an4") {
255                 sub.horizontal_position.reference = sub::LEFT_OF_SCREEN;
256                 sub.vertical_position.reference = sub::VERTICAL_CENTRE_OF_SCREEN;
257                 context.update_horizontal_position(sub);
258         } else if (tag == "\\an5") {
259                 sub.horizontal_position.reference = sub::HORIZONTAL_CENTRE_OF_SCREEN;
260                 sub.vertical_position.reference = sub::VERTICAL_CENTRE_OF_SCREEN;
261                 context.update_horizontal_position(sub);
262         } else if (tag == "\\an6") {
263                 sub.horizontal_position.reference = sub::RIGHT_OF_SCREEN;
264                 sub.vertical_position.reference = sub::VERTICAL_CENTRE_OF_SCREEN;
265                 context.update_horizontal_position(sub);
266         } else if (tag == "\\an7") {
267                 sub.horizontal_position.reference = sub::LEFT_OF_SCREEN;
268                 sub.vertical_position.reference = sub::TOP_OF_SCREEN;
269                 context.update_horizontal_position(sub);
270         } else if (tag == "\\an8") {
271                 sub.horizontal_position.reference = sub::HORIZONTAL_CENTRE_OF_SCREEN;
272                 sub.vertical_position.reference = sub::TOP_OF_SCREEN;
273                 context.update_horizontal_position(sub);
274         } else if (tag == "\\an9") {
275                 sub.horizontal_position.reference = sub::RIGHT_OF_SCREEN;
276                 sub.vertical_position.reference = sub::TOP_OF_SCREEN;
277                 context.update_horizontal_position(sub);
278         } else if (boost::starts_with(tag, "\\pos")) {
279                 vector<string> bits;
280                 boost::algorithm::split (bits, tag, boost::is_any_of("(,"));
281                 SUB_ASSERT (bits.size() == 3);
282                 sub.horizontal_position.reference = sub::LEFT_OF_SCREEN;
283                 sub.horizontal_position.proportional = raw_convert<float>(bits[1]) / context.play_res_x;
284                 sub.vertical_position.reference = sub::TOP_OF_SCREEN;
285                 sub.vertical_position.proportional = raw_convert<float>(bits[2]) / context.play_res_y;
286         } else if (boost::starts_with(tag, "\\fs")) {
287                 SUB_ASSERT (tag.length() > 3);
288                 sub.font_size.set_proportional(raw_convert<float>(tag.substr(3)) / context.play_res_y);
289         } else if (boost::starts_with(tag, "\\c")) {
290                 /* \c&Hbbggrr& */
291                 if (tag.length() > 2) {
292                         sub.colour = h_colour(tag.substr(2, tag.length() - 3));
293                 } else if (tag.length() == 2) {
294                         sub.colour = context.primary_colour;
295                 } else {
296                         throw SSAError(String::compose("Badly formatted colour tag %1", tag));
297                 }
298         }
299 }
300
301 /** @param base RawSubtitle filled in with any required common values.
302  *  @param line SSA line string (i.e. just the subtitle, possibly with embedded stuff)
303  *  @return List of RawSubtitles to represent line with vertical reference TOP_OF_SUBTITLE.
304  */
305 vector<RawSubtitle>
306 SSAReader::parse_line(RawSubtitle base, string line, Context const& context)
307 {
308         enum {
309                 TEXT,
310                 TAG,
311                 BACKSLASH
312         } state = TEXT;
313
314         vector<RawSubtitle> subs;
315         RawSubtitle current = base;
316         string tag;
317
318         if (!current.vertical_position.reference) {
319                 current.vertical_position.reference = BOTTOM_OF_SCREEN;
320         }
321
322         /* Any vertical_position that is set in base (and therefore current) is a margin, which
323          * we need to ignore if we end up vertically centering this subtitle.
324          * Clear out vertical_position from current; we'll re-add it from base later
325          * if required.
326          */
327         current.vertical_position.proportional = 0;
328
329         context.update_horizontal_position(current);
330
331         /* We must have a font size, as there could be a margin specified
332            in pixels and in that case we must know how big the subtitle
333            lines are to work out the position on screen.
334         */
335         if (!current.font_size.proportional()) {
336                 current.font_size.set_proportional(72.0 / context.play_res_y);
337         }
338
339         /* Count the number of line breaks */
340         int line_breaks = 0;
341         if (line.length() > 0) {
342                 for (size_t i = 0; i < line.length() - 1; ++i) {
343                         if (line[i] == '\\' && (line[i+1] == 'n' || line[i+1] == 'N')) {
344                                 ++line_breaks;
345                         }
346                 }
347         }
348
349         /* There are vague indications that with ASS 1 point should equal 1 pixel */
350         double const line_size = current.font_size.proportional(context.play_res_y) * 1.2;
351
352         for (size_t i = 0; i < line.length(); ++i) {
353                 char const c = line[i];
354                 switch (state) {
355                 case TEXT:
356                         if (c == '{') {
357                                 state = TAG;
358                         } else if (c == '\\') {
359                                 state = BACKSLASH;
360                         } else if (c != '\r' && c != '\n') {
361                                 current.text += c;
362                         }
363                         break;
364                 case TAG:
365                         if (c == '}' || c == '\\') {
366                                 if (!current.text.empty ()) {
367                                         subs.push_back (current);
368                                         current.text = "";
369                                 }
370                                 parse_tag(current, tag, context);
371                                 tag = "";
372                         }
373
374                         if (c == '}') {
375                                 state = TEXT;
376                         } else {
377                                 tag += c;
378                         }
379                         break;
380                 case BACKSLASH:
381                         if (c == 'n' || c == 'N') {
382                                 if (!current.text.empty ()) {
383                                         subs.push_back (current);
384                                         current.text = "";
385                                 }
386                                 /* Move down one line (1.2 times the font size) */
387                                 if (current.vertical_position.reference.get() == BOTTOM_OF_SCREEN) {
388                                         current.vertical_position.proportional = current.vertical_position.proportional.get() - line_size;
389                                 } else {
390                                         current.vertical_position.proportional = current.vertical_position.proportional.get() + line_size;
391                                 }
392                         }
393                         state = TEXT;
394                         break;
395                 }
396         }
397
398         if (!current.text.empty ()) {
399                 subs.push_back (current);
400         }
401
402         /* Now we definitely know the vertical position reference we can finish off the position */
403         for (auto& sub: subs) {
404                 switch (sub.vertical_position.reference.get()) {
405                 case TOP_OF_SCREEN:
406                 case TOP_OF_SUBTITLE:
407                         /* Just re-add any margins we came in with */
408                         sub.vertical_position.proportional = sub.vertical_position.proportional.get() + base.vertical_position.proportional.get_value_or(0);
409                         break;
410                 case VERTICAL_CENTRE_OF_SCREEN:
411                         /* Margins are ignored, but we need to centre */
412                         sub.vertical_position.proportional = sub.vertical_position.proportional.get() - ((line_breaks + 1) * line_size) / 2;
413                         break;
414                 case BOTTOM_OF_SCREEN:
415                         /* Re-add margins and account for each line */
416                         sub.vertical_position.proportional =
417                                 sub.vertical_position.proportional.get()
418                                 + base.vertical_position.proportional.get_value_or(0)
419                                 + line_breaks * line_size;
420                         break;
421                 }
422         }
423
424         return subs;
425 }
426
427 void
428 SSAReader::read (function<optional<string> ()> get_line)
429 {
430         enum {
431                 INFO,
432                 STYLES,
433                 EVENTS
434         } part = INFO;
435
436         int play_res_x = 288;
437         int play_res_y = 288;
438         map<string, Style> styles;
439         string style_format_line;
440         vector<string> event_format;
441
442         while (true) {
443                 optional<string> line = get_line ();
444                 if (!line) {
445                         break;
446                 }
447
448                 trim (*line);
449                 remove_unicode_bom (line);
450
451                 if (starts_with (*line, ";") || line->empty ()) {
452                         continue;
453                 }
454
455                 if (starts_with (*line, "[")) {
456                         /* Section heading */
457                         if (line.get() == "[Script Info]") {
458                                 part = INFO;
459                         } else if (line.get() == "[V4 Styles]" || line.get() == "[V4+ Styles]") {
460                                 part = STYLES;
461                         } else if (line.get() == "[Events]") {
462                                 part = EVENTS;
463                         }
464                         continue;
465                 }
466
467                 size_t const colon = line->find (":");
468                 SUB_ASSERT (colon != string::npos);
469                 string const type = line->substr (0, colon);
470                 string body = line->substr (colon + 1);
471                 trim (body);
472
473                 switch (part) {
474                 case INFO:
475                         if (type == "PlayResX") {
476                                 play_res_x = raw_convert<int> (body);
477                         } else if (type == "PlayResY") {
478                                 play_res_y = raw_convert<int> (body);
479                         }
480                         break;
481                 case STYLES:
482                         if (type == "Format") {
483                                 style_format_line = body;
484                         } else if (type == "Style") {
485                                 SUB_ASSERT (!style_format_line.empty ());
486                                 Style s (style_format_line, body);
487                                 styles[s.name] = s;
488                         }
489                         break;
490                 case EVENTS:
491                         if (type == "Format") {
492                                 split (event_format, body, is_any_of (","));
493                                 for (auto& i: event_format) {
494                                         trim (i);
495                                 }
496                         } else if (type == "Dialogue") {
497                                 SUB_ASSERT (!event_format.empty ());
498                                 vector<string> event;
499                                 split (event, body, is_any_of (","));
500
501                                 /* There may be commas in the subtitle part; reassemble any extra parts
502                                    from when we just split it.
503                                 */
504                                 while (event.size() > event_format.size()) {
505                                         string const ex = event.back ();
506                                         event.pop_back ();
507                                         event.back() += "," + ex;
508                                 }
509
510                                 SUB_ASSERT (!event.empty());
511                                 SUB_ASSERT (event_format.size() == event.size());
512
513                                 RawSubtitle sub;
514                                 optional<Style> style;
515                                 int left_margin = 0;
516                                 int right_margin = 0;
517
518                                 for (size_t i = 0; i < event.size(); ++i) {
519                                         trim (event[i]);
520                                         if (event_format[i] == "Start") {
521                                                 sub.from = parse_time (event[i]);
522                                         } else if (event_format[i] == "End") {
523                                                 sub.to = parse_time (event[i]);
524                                         } else if (event_format[i] == "Style") {
525                                                 /* libass trims leading '*'s from style names, commenting that
526                                                    "they seem to mean literally nothing".  Go figure...
527                                                 */
528                                                 trim_left_if (event[i], boost::is_any_of ("*"));
529                                                 /* Use the specified style unless it's not defined, in which case use
530                                                  * "Default" (if it exists).
531                                                  */
532                                                 if (styles.find(event[i]) != styles.end()) {
533                                                         style = styles[event[i]];
534                                                 } else if (styles.find("Default") != styles.end()) {
535                                                         style = styles["Default"];
536                                                 } else {
537                                                         continue;
538                                                 }
539                                                 sub.font = style->font_name;
540                                                 sub.font_size = FontSize::from_proportional(static_cast<float>(style->font_size) / play_res_y);
541                                                 sub.colour = style->primary_colour;
542                                                 sub.effect_colour = style->back_colour;
543                                                 sub.bold = style->bold;
544                                                 sub.italic = style->italic;
545                                                 sub.underline = style->underline;
546                                                 sub.effect = style->effect;
547                                                 sub.horizontal_position.reference = style->horizontal_reference;
548                                                 sub.vertical_position.reference = style->vertical_reference;
549                                                 if (sub.vertical_position.reference != sub::VERTICAL_CENTRE_OF_SCREEN) {
550                                                         sub.vertical_position.proportional = float(style->vertical_margin) / play_res_y;
551                                                 }
552                                                 left_margin = style->left_margin;
553                                                 right_margin = style->right_margin;
554                                         } else if (event_format[i] == "MarginV") {
555                                                 if (event[i] != "0" && sub.vertical_position.reference != sub::VERTICAL_CENTRE_OF_SCREEN) {
556                                                         /* Override the style if its non-zero */
557                                                         sub.vertical_position.proportional = raw_convert<float>(event[i]) / play_res_y;
558                                                 }
559                                         } else if (event_format[i] == "MarginL") {
560                                                 if (event[i] != "0") {
561                                                         left_margin = raw_convert<int>(event[i]);
562                                                 }
563                                         } else if (event_format[i] == "MarginR") {
564                                                 if (event[i] != "0") {
565                                                         right_margin = raw_convert<int>(event[i]);
566                                                 }
567                                         } else if (event_format[i] == "Text") {
568                                                 auto context = Context(play_res_x, play_res_y, style ? style->primary_colour : Colour(1, 1, 1), left_margin, right_margin);
569                                                 for (auto j: parse_line(sub, event[i], context)) {
570                                                         _subs.push_back (j);
571                                                 }
572                                         }
573                                 }
574                         }
575                 }
576
577         }
578 }