X-Git-Url: https://git.carlh.net/gitweb/?a=blobdiff_plain;f=src%2Fssa_reader.cc;h=863225112d2fab532e2271d939f82a15b908a8cf;hb=fa12ed65527770e2adf300e39526dd0b36fce8c7;hp=acaa46be4b0d5dc714474ffece2e9c09918f84b0;hpb=df1f122492dcb1a66f3ac1f3fd734f5790a04bea;p=libsub.git diff --git a/src/ssa_reader.cc b/src/ssa_reader.cc index acaa46b..8632251 100644 --- a/src/ssa_reader.cc +++ b/src/ssa_reader.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2016 Carl Hetherington + Copyright (C) 2016-2019 Carl Hetherington This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,29 +22,28 @@ #include "sub_assert.h" #include "raw_convert.h" #include "subtitle.h" +#include "compose.hpp" #include -#include -#include -#include +#include #include #include using std::string; -using std::stringstream; using std::vector; using std::map; using std::cout; -using std::list; using boost::optional; using boost::function; using namespace boost::algorithm; +#if BOOST_VERSION >= 106100 +using namespace boost::placeholders; +#endif using namespace sub; /** @param s Subtitle string encoded in UTF-8 */ -SSAReader::SSAReader (string const & s) +SSAReader::SSAReader (string s) { - stringstream str (s); - this->read (boost::bind (&get_line_stringstream, &str)); + this->read (boost::bind(&get_line_string, &s)); } /** @param f Subtitle file encoded in UTF-8 */ @@ -53,26 +52,50 @@ SSAReader::SSAReader (FILE* f) this->read (boost::bind (&get_line_file, f)); } +Colour +h_colour (string s) +{ + /* There are both BGR and ABGR versions of these colours */ + if ((s.length() != 8 && s.length() != 10) || s[0] != '&' || s[1] != 'H') { + throw SSAError(String::compose("Badly formatted colour tag %1", s)); + } + int ir, ig, ib; + /* XXX: ignoring alpha channel here; note that 00 is opaque and FF is transparent */ + int const off = s.length() == 10 ? 4 : 2; + if (sscanf(s.c_str() + off, "%2x%2x%2x", &ib, &ig, &ir) < 3) { + throw SSAError(String::compose("Badly formatted colour tag %1", s)); + } + return sub::Colour(ir / 255.0, ig / 255.0, ib / 255.0); +} + class Style { public: Style () - : font_size (24) + : font_size (72) , primary_colour (255, 255, 255) , bold (false) , italic (false) + , underline (false) + , horizontal_reference (HORIZONTAL_CENTRE_OF_SCREEN) + , vertical_reference (BOTTOM_OF_SCREEN) + , vertical_margin (0) {} Style (string format_line, string style_line) - : font_size (24) + : font_size (72) , primary_colour (255, 255, 255) , bold (false) , italic (false) + , underline (false) + , horizontal_reference (HORIZONTAL_CENTRE_OF_SCREEN) + , vertical_reference (BOTTOM_OF_SCREEN) + , vertical_margin (0) { vector keys; - split (keys, format_line, is_any_of (",")); + split (keys, format_line, boost::is_any_of (",")); vector style; - split (style, style_line, is_any_of (",")); + split (style, style_line, boost::is_any_of (",")); SUB_ASSERT (!keys.empty()); SUB_ASSERT (!style.empty()); @@ -88,39 +111,69 @@ public: } else if (keys[i] == "Fontsize") { font_size = raw_convert (style[i]); } else if (keys[i] == "PrimaryColour") { - primary_colour = colour (raw_convert (style[i])); + primary_colour = colour (style[i]); } else if (keys[i] == "BackColour") { - back_colour = colour (raw_convert (style[i])); + back_colour = colour (style[i]); } else if (keys[i] == "Bold") { bold = style[i] == "-1"; } else if (keys[i] == "Italic") { italic = style[i] == "-1"; + } else if (keys[i] == "Underline") { + underline = style[i] == "-1"; } else if (keys[i] == "BorderStyle") { if (style[i] == "1") { effect = SHADOW; } + } else if (keys[i] == "Alignment") { + if (style[i] == "7" || style[i] == "8" || style[i] == "9") { + vertical_reference = TOP_OF_SCREEN; + } else if (style[i] == "4" || style[i] == "5" || style[i] == "6") { + vertical_reference = VERTICAL_CENTRE_OF_SCREEN; + } else { + vertical_reference = BOTTOM_OF_SCREEN; + } + if (style[i] == "1" || style[i] == "4" || style[i] == "7") { + horizontal_reference = LEFT_OF_SCREEN; + } else if (style[i] == "3" || style[i] == "6" || style[i] == "9") { + horizontal_reference = RIGHT_OF_SCREEN; + } else { + horizontal_reference = HORIZONTAL_CENTRE_OF_SCREEN; + } + } else if (keys[i] == "MarginV") { + vertical_margin = raw_convert (style[i]); } } } string name; optional font_name; - int font_size; + int font_size; ///< points Colour primary_colour; /** outline colour */ optional back_colour; bool bold; bool italic; + bool underline; optional effect; + HorizontalReference horizontal_reference; + VerticalReference vertical_reference; + int vertical_margin; private: - Colour colour (int c) const + Colour colour (string c) const { - return Colour ( - ((c & 0x0000ff) >> 0) / 255.0, - ((c & 0x00ff00) >> 8) / 255.0, - ((c & 0xff0000) >> 16) / 255.0 - ); + if (c.length() > 0 && c[0] == '&') { + /* &Hbbggrr or &Haabbggrr */ + return h_colour (c); + } else { + /* integer */ + int i = raw_convert(c); + return Colour ( + ((i & 0x0000ff) >> 0) / 255.0, + ((i & 0x00ff00) >> 8) / 255.0, + ((i & 0xff0000) >> 16) / 255.0 + ); + } } }; @@ -138,12 +191,74 @@ SSAReader::parse_time (string t) const ); } +void +SSAReader::parse_style (RawSubtitle& sub, string style, int play_res_x, int play_res_y) +{ + if (style == "\\i1") { + sub.italic = true; + } else if (style == "\\i0" || style == "\\i") { + sub.italic = false; + } else if (style == "\\b1") { + sub.bold = true; + } else if (style == "\\b0") { + sub.bold = false; + } else if (style == "\\u1") { + sub.underline = true; + } else if (style == "\\u0") { + sub.underline = false; + } else if (style == "\\an1") { + sub.horizontal_position.reference = sub::LEFT_OF_SCREEN; + sub.vertical_position.reference = sub::BOTTOM_OF_SCREEN; + } else if (style == "\\an2") { + sub.horizontal_position.reference = sub::HORIZONTAL_CENTRE_OF_SCREEN; + sub.vertical_position.reference = sub::BOTTOM_OF_SCREEN; + } else if (style == "\\an3") { + sub.horizontal_position.reference = sub::RIGHT_OF_SCREEN; + sub.vertical_position.reference = sub::BOTTOM_OF_SCREEN; + } else if (style == "\\an4") { + sub.horizontal_position.reference = sub::LEFT_OF_SCREEN; + sub.vertical_position.reference = sub::VERTICAL_CENTRE_OF_SCREEN; + } else if (style == "\\an5") { + sub.horizontal_position.reference = sub::HORIZONTAL_CENTRE_OF_SCREEN; + sub.vertical_position.reference = sub::VERTICAL_CENTRE_OF_SCREEN; + } else if (style == "\\an6") { + sub.horizontal_position.reference = sub::RIGHT_OF_SCREEN; + sub.vertical_position.reference = sub::VERTICAL_CENTRE_OF_SCREEN; + } else if (style == "\\an7") { + sub.horizontal_position.reference = sub::LEFT_OF_SCREEN; + sub.vertical_position.reference = sub::TOP_OF_SCREEN; + } else if (style == "\\an8") { + sub.horizontal_position.reference = sub::HORIZONTAL_CENTRE_OF_SCREEN; + sub.vertical_position.reference = sub::TOP_OF_SCREEN; + } else if (style == "\\an9") { + sub.horizontal_position.reference = sub::RIGHT_OF_SCREEN; + sub.vertical_position.reference = sub::TOP_OF_SCREEN; + } else if (boost::starts_with(style, "\\pos")) { + vector bits; + boost::algorithm::split (bits, style, boost::is_any_of("(,")); + SUB_ASSERT (bits.size() == 3); + sub.horizontal_position.reference = sub::LEFT_OF_SCREEN; + sub.horizontal_position.proportional = raw_convert(bits[1]) / play_res_x; + sub.vertical_position.reference = sub::TOP_OF_SCREEN; + sub.vertical_position.proportional = raw_convert(bits[2]) / play_res_y; + } else if (boost::starts_with(style, "\\fs")) { + SUB_ASSERT (style.length() > 3); + sub.font_size.set_proportional(raw_convert(style.substr(3)) / play_res_y); + } else if (boost::starts_with(style, "\\c")) { + /* \c&Hbbggrr& */ + if (style.length() <= 2) { + throw SSAError(String::compose("Badly formatted colour tag %1", style)); + } + sub.colour = h_colour (style.substr(2, style.length() - 3)); + } +} + /** @param base RawSubtitle filled in with any required common values. - * @param line SSA line string. + * @param line SSA line string (i.e. just the subtitle, possibly with embedded stuff) * @return List of RawSubtitles to represent line with vertical reference TOP_OF_SUBTITLE. */ -list -SSAReader::parse_line (RawSubtitle base, string line) +vector +SSAReader::parse_line (RawSubtitle base, string line, int play_res_x, int play_res_y) { enum { TEXT, @@ -151,12 +266,41 @@ SSAReader::parse_line (RawSubtitle base, string line) BACKSLASH } state = TEXT; - list subs; + vector subs; RawSubtitle current = base; string style; - current.vertical_position.line = 0; - current.vertical_position.reference = TOP_OF_SUBTITLE; + if (!current.vertical_position.reference) { + current.vertical_position.reference = BOTTOM_OF_SCREEN; + } + + /* Any vertical_position that is set in base (and therefore current) is a margin, which + * we need to ignore if we end up vertically centering this subtitle. + * Clear out vertical_position from current; we'll re-add it from base later + * if required. + */ + current.vertical_position.proportional = 0; + + /* We must have a font size, as there could be a margin specified + in pixels and in that case we must know how big the subtitle + lines are to work out the position on screen. + */ + if (!current.font_size.proportional()) { + current.font_size.set_proportional(72.0 / play_res_y); + } + + /* Count the number of line breaks */ + int line_breaks = 0; + if (line.length() > 0) { + for (size_t i = 0; i < line.length() - 1; ++i) { + if (line[i] == '\\' && (line[i+1] == 'n' || line[i+1] == 'N')) { + ++line_breaks; + } + } + } + + /* There are vague indications that with ASS 1 point should equal 1 pixel */ + double const line_size = current.font_size.proportional(play_res_y) * 1.2; for (size_t i = 0; i < line.length(); ++i) { char const c = line[i]; @@ -166,32 +310,38 @@ SSAReader::parse_line (RawSubtitle base, string line) state = STYLE; } else if (c == '\\') { state = BACKSLASH; - } else { + } else if (c != '\r' && c != '\n') { current.text += c; } break; case STYLE: - if (c == '}') { + if (c == '}' || c == '\\') { if (!current.text.empty ()) { subs.push_back (current); current.text = ""; } - if (style == "i1") { - current.italic = true; - } else if (style == "i0") { - current.italic = false; - } + parse_style (current, style, play_res_x, play_res_y); style = ""; + } + + if (c == '}') { state = TEXT; } else { style += c; } break; case BACKSLASH: - if (c == 'n' && !current.text.empty ()) { - subs.push_back (current); - current.text = ""; - current.vertical_position.line = current.vertical_position.line.get() + 1; + if (c == 'n' || c == 'N') { + if (!current.text.empty ()) { + subs.push_back (current); + current.text = ""; + } + /* Move down one line (1.2 times the font size) */ + if (current.vertical_position.reference.get() == BOTTOM_OF_SCREEN) { + current.vertical_position.proportional = current.vertical_position.proportional.get() - line_size; + } else { + current.vertical_position.proportional = current.vertical_position.proportional.get() + line_size; + } } state = TEXT; break; @@ -202,6 +352,28 @@ SSAReader::parse_line (RawSubtitle base, string line) subs.push_back (current); } + /* Now we definitely know the vertical position reference we can finish off the position */ + for (auto& sub: subs) { + switch (sub.vertical_position.reference.get()) { + case TOP_OF_SCREEN: + case TOP_OF_SUBTITLE: + /* Just re-add any margins we came in with */ + sub.vertical_position.proportional = sub.vertical_position.proportional.get() + base.vertical_position.proportional.get_value_or(0); + break; + case VERTICAL_CENTRE_OF_SCREEN: + /* Margins are ignored, but we need to centre */ + sub.vertical_position.proportional = sub.vertical_position.proportional.get() - ((line_breaks + 1) * line_size) / 2; + break; + case BOTTOM_OF_SCREEN: + /* Re-add margins and account for each line */ + sub.vertical_position.proportional = + sub.vertical_position.proportional.get() + + base.vertical_position.proportional.get_value_or(0) + + line_breaks * line_size; + break; + } + } + return subs; } @@ -214,6 +386,8 @@ SSAReader::read (function ()> get_line) EVENTS } part = INFO; + int play_res_x = 288; + int play_res_y = 288; map styles; string style_format_line; vector event_format; @@ -225,6 +399,7 @@ SSAReader::read (function ()> get_line) } trim (*line); + remove_unicode_bom (line); if (starts_with (*line, ";") || line->empty ()) { continue; @@ -234,7 +409,7 @@ SSAReader::read (function ()> get_line) /* Section heading */ if (line.get() == "[Script Info]") { part = INFO; - } else if (line.get() == "[V4 Styles]") { + } else if (line.get() == "[V4 Styles]" || line.get() == "[V4+ Styles]") { part = STYLES; } else if (line.get() == "[Events]") { part = EVENTS; @@ -244,12 +419,17 @@ SSAReader::read (function ()> get_line) size_t const colon = line->find (":"); SUB_ASSERT (colon != string::npos); - SUB_ASSERT (line->length() > colon + 1); string const type = line->substr (0, colon); - string const body = line->substr (colon + 2); + string body = line->substr (colon + 1); + trim (body); switch (part) { case INFO: + if (type == "PlayResX") { + play_res_x = raw_convert (body); + } else if (type == "PlayResY") { + play_res_y = raw_convert (body); + } break; case STYLES: if (type == "Format") { @@ -263,7 +443,7 @@ SSAReader::read (function ()> get_line) case EVENTS: if (type == "Format") { split (event_format, body, is_any_of (",")); - BOOST_FOREACH (string& i, event_format) { + for (auto& i: event_format) { trim (i); } } else if (type == "Dialogue") { @@ -271,6 +451,15 @@ SSAReader::read (function ()> get_line) vector event; split (event, body, is_any_of (",")); + /* There may be commas in the subtitle part; reassemble any extra parts + from when we just split it. + */ + while (event.size() > event_format.size()) { + string const ex = event.back (); + event.pop_back (); + event.back() += "," + ex; + } + SUB_ASSERT (!event.empty()); SUB_ASSERT (event_format.size() == event.size()); @@ -283,27 +472,36 @@ SSAReader::read (function ()> get_line) } else if (event_format[i] == "End") { sub.to = parse_time (event[i]); } else if (event_format[i] == "Style") { + /* libass trims leading '*'s from style names, commenting that + "they seem to mean literally nothing". Go figure... + */ + trim_left_if (event[i], boost::is_any_of ("*")); SUB_ASSERT (styles.find(event[i]) != styles.end()); Style style = styles[event[i]]; sub.font = style.font_name; - sub.font_size = FontSize::from_points (style.font_size); + sub.font_size = FontSize::from_proportional(static_cast(style.font_size) / play_res_y); sub.colour = style.primary_colour; sub.effect_colour = style.back_colour; sub.bold = style.bold; sub.italic = style.italic; + sub.underline = style.underline; sub.effect = style.effect; - - /* XXX: arbitrary */ - sub.vertical_position.lines = 32; - sub.vertical_position.reference = TOP_OF_SUBTITLE; - sub.vertical_position.line = 0; - + sub.horizontal_position.reference = style.horizontal_reference; + sub.vertical_position.reference = style.vertical_reference; + if (sub.vertical_position.reference != sub::VERTICAL_CENTRE_OF_SCREEN) { + sub.vertical_position.proportional = float(style.vertical_margin) / play_res_y; + } + } else if (event_format[i] == "MarginV") { + if (event[i] != "0" && sub.vertical_position.reference != sub::VERTICAL_CENTRE_OF_SCREEN) { + /* Override the style if its non-zero */ + sub.vertical_position.proportional = raw_convert(event[i]) / play_res_y; + } } else if (event_format[i] == "Text") { - sub.text = event[i]; + for (auto j: parse_line (sub, event[i], play_res_x, play_res_y)) { + _subs.push_back (j); + } } } - - _subs.push_back (sub); } }