/* Copyright (C) 2023 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 the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ #include "compose.hpp" #include "exceptions.h" #include "raw_convert.h" #include "scc_reader.h" #include "sub_assert.h" #include "util.h" #include #include #include #include #include using std::pair; using std::set; using std::string; using std::unordered_map; using std::vector; using boost::function; using boost::optional; using namespace sub; static Rational const frame_rate{30000, 1001}; /** @param s Subtitle string encoded in UTF-8 */ SCCReader::SCCReader(string s) { this->read(boost::bind(&get_line_string, &s)); } /** @param f Subtitle file encoded in UTF-8 */ SCCReader::SCCReader(FILE* f) { this->read(boost::bind (&get_line_file, f)); } int convert_time(std::string time) { vector parts; boost::algorithm::split(parts, time, boost::is_any_of(":")); if (parts.size() != 4) { throw SCCReader(String::compose("Unrecognised time %1", time)); } int seconds = raw_convert(parts[0]) * 3600 + raw_convert(parts[1]) * 60 + raw_convert(parts[2]); return std::round(seconds * frame_rate.fraction() + raw_convert(parts[3])); } struct PAC { int row = 0; int column = 0; enum class Colour { WHITE, GREEN, BLUE, CYAN, RED, YELLOW, MAGENTA }; Colour colour = Colour::WHITE; bool underline = false; PAC() = default; PAC(int row_, int column_, Colour colour_ = Colour::WHITE, bool underline_ = false) : row(row_) , column(column_) , colour(colour_) , underline(underline_) {} }; optional preamble_address_code(string part) { int bytes[2]; if (sscanf(part.c_str(), "%2x%2x", bytes, bytes + 1) < 2) { throw SCCReader(String::compose("Failed to parse line part %1", part)); } auto adjust = [](PAC& pac, int byte) { switch (byte) { case 0x91: break; case 0x92: pac.row += 2; break; case 0x15: pac.row += 4; break; case 0x16: pac.row += 6; break; case 0x97: pac.row += 8; break; case 0x10: pac.row += 10; break; case 0x13: pac.row += 11; break; case 0x94: pac.row += 13; break; default: return false; } return true; }; PAC pac; switch (bytes[1]) { /* Column 0 - from http://www.theneitherworld.com/mcpoodle/SCC_TOOLS/DOCS/SCC_FORMAT.HTML */ case 0xd0: case 0x70: case 0x51: case 0xf1: case 0xc2: case 0x62: case 0x43: case 0xe3: case 0xc4: case 0x64: case 0x45: case 0xe5: case 0x46: case 0xe6: case 0xc7: case 0x67: case 0xc8: case 0x68: case 0x49: case 0xe9: case 0x4a: case 0xea: case 0xcb: case 0x6b: case 0x4c: case 0xec: case 0xcd: case 0x6d: /* RESUMEDIRECTCAPTIONING in ccextractor? */ // case 0x29: /* No documentation found for these... */ // case 0x2f: // case 0x40: // case 0xe0: pac.row = ((bytes[1] & 0x20) >> 5) + 1; pac.underline = bytes[1] & 0x1; switch (bytes[1] & 0xf) { case 0x0: case 0x1: pac.colour = PAC::Colour::WHITE; break; case 0x2: case 0x3: pac.colour = PAC::Colour::GREEN; break; case 0x4: case 0x5: pac.colour = PAC::Colour::BLUE; break; case 0x6: case 0x7: pac.colour = PAC::Colour::CYAN; break; case 0x8: case 0x9: pac.colour = PAC::Colour::RED; break; case 0xa: case 0xb: pac.colour = PAC::Colour::YELLOW; break; case 0xc: case 0xd: pac.colour = PAC::Colour::MAGENTA; break; } if (adjust(pac, bytes[0])) { return pac; } } switch (bytes[1]) { /* Columns 4 - 28 */ case 0x52: case 0xf2: case 0xd3: case 0x73: case 0x54: case 0xf4: case 0xd5: case 0x75: case 0xd6: case 0x76: case 0x57: case 0xf7: case 0x58: case 0xf8: case 0xd9: case 0x79: case 0xda: case 0x7a: case 0x5b: case 0xfb: case 0xdc: case 0x7c: case 0x5d: case 0xfd: case 0x5e: case 0xfe: case 0xdf: case 0x7f: pac.row = ((bytes[1] & 0x20) >> 5) + 1; pac.underline = bytes[1] & 0x1; pac.column = (bytes[1] & 0xe) * 2; if (adjust(pac, bytes[0])) { return pac; } } return {}; } void SCCReader::read(function ()> get_line) { set ignore; ignore.insert("102f"); ignore.insert("9420"); // RCL: resume caption loading; the next caption is "pop-on" ignore.insert("9440"); // should be a line break? set line_break; line_break.insert("94e0"); unordered_map replace; replace["9229"] = '\''; bool got_header = false; int to_fix = 0; while (true) { auto line = get_line (); if (!line) { break; } trim_right_if(*line, boost::is_any_of ("\n\r")); if (*line == "Scenarist_SCC V1.0" && !got_header) { got_header = true; continue; } if (!got_header) { throw SCCError("No header string found"); } if (line->empty()) { continue; } auto const tab = line->find('\t'); if (tab == string::npos) { throw SCCError(String::compose("No tab character found in line %1", *line)); } /* XXX: don't fuck about with this stuff, we just add a frame per two-bytes to the initial time, * and probably do this NDF adjustment. */ int time_in_frames = convert_time(line->substr(0, tab)); vector parts; boost::algorithm::split(parts, line->substr(tab + 1), boost::is_any_of(" ")); vector current_line; optional