/* * This file is part of Popcorn Time. * * Popcorn Time 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 3 of the License, or * (at your option) any later version. * * Popcorn Time 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 Popcorn Time. If not, see <http://www.gnu.org/licenses/>. */ package pct.droid.base.subs; import java.io.IOException; import java.util.ArrayList; /** * Class that represents the .ASS and .SSA subtitle file format * <p/> * <br><br> * Copyright (c) 2012 J. David Requejo <br> * j[dot]david[dot]requejo[at] Gmail * <br><br> * Permission is hereby granted, free of charge, to any person obtaining a copy of this software * and associated documentation files (the "Software"), to deal in the Software without restriction, * including without limitation the rights to use, copy, modify, merge, publish, distribute, * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software * is furnished to do so, subject to the following conditions: * <br><br> * The above copyright notice and this permission notice shall be included in all copies * or substantial portions of the Software. * <br><br> * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * * @author J. David REQUEJO */ public class FormatASS extends TimedTextFileFormat { public TimedTextObject parseFile(String fileName, String[] inputString) throws IOException { TimedTextObject tto = new TimedTextObject(); tto.fileName = fileName; Caption caption; Style style; //for the clock timer float timer = 100; //if the file is .SSA or .ASS boolean isASS = false; //variables to store the formats String[] styleFormat; String[] dialogueFormat; String line; int lineCounter = 0; int stringIndex = 0; try { //we scour the file line = getLine(inputString, stringIndex++); lineCounter++; while (line != null && stringIndex < inputString.length) { line = line.trim(); //we skip any line until we find a section [section name] if (line.startsWith("[")) { //now we must identify the section if (line.equalsIgnoreCase("[Script info]")) { //its the script info section section lineCounter++; line = getLine(inputString, stringIndex++).trim(); //Each line is scanned for useful info until a new section is detected while (!line.startsWith("[")) { if (line.startsWith("Title:")) //We have found the title tto.title = line.split(":")[1].trim(); else if (line.startsWith("Original Script:")) //We have found the author tto.author = line.split(":")[1].trim(); else if (line.startsWith("Script Type:")) { //we have found the version if (line.split(":")[1].trim().equalsIgnoreCase("v4.00+")) isASS = true; //we check the type to set isASS or to warn if it comes from an older version than the studied specs else if (!line.split(":")[1].trim().equalsIgnoreCase("v4.00")) tto.warnings += "Script version is older than 4.00, it may produce parsing errors."; } else if (line.startsWith("Timer:")) //We have found the timer timer = Float.parseFloat(line.split(":")[1].trim().replace(',', '.')); //we go to the next line lineCounter++; line = getLine(inputString, stringIndex++).trim(); } } else if (line.equalsIgnoreCase("[v4 Styles]") || line.equalsIgnoreCase("[v4 Styles+]") || line.equalsIgnoreCase("[v4+ Styles]")) { //its the Styles description section if (line.contains("+") && !isASS) { //its ASS and it had not been noted isASS = true; tto.warnings += "ScriptType should be set to v4:00+ in the [Script Info] section.\n\n"; } lineCounter++; line = getLine(inputString, stringIndex++).trim(); //the first line should define the format if (!line.startsWith("Format:")) { //if not, we scan for the format. tto.warnings += "Format: (format definition) expected at line " + line + " for the styles section\n\n"; while (!line.startsWith("Format:")) { lineCounter++; line = getLine(inputString, stringIndex++).trim(); } } // we recover the format's fields styleFormat = line.split(":")[1].trim().split(","); lineCounter++; line = getLine(inputString, stringIndex++).trim(); // we parse each style until we reach a new section while (!line.startsWith("[")) { //we check it is a style if (line.startsWith("Style:")) { //we parse the style style = parseStyleForASS(line.split(":")[1].trim().split(","), styleFormat, lineCounter, isASS, tto.warnings); //and save the style tto.styling.put(style.iD, style); } //next line lineCounter++; line = getLine(inputString, stringIndex++).trim(); } } else if (line.trim().equalsIgnoreCase("[Events]")) { //its the events specification section lineCounter++; line = getLine(inputString, stringIndex++).trim(); tto.warnings += "Only dialogue events are considered, all other events are ignored.\n\n"; //the first line should define the format of the dialogues if (!line.startsWith("Format:")) { //if not, we scan for the format. tto.warnings += "Format: (format definition) expected at line " + line + " for the events section\n\n"; while (!line.startsWith("Format:")) { lineCounter++; line = getLine(inputString, stringIndex++).trim(); } } // we recover the format's fields dialogueFormat = line.split(":")[1].trim().split(","); //next line lineCounter++; line = getLine(inputString, stringIndex++).trim(); // we parse each style until we reach a new section while (!line.startsWith("[")) { //we check it is a dialogue //WARNING: all other events are ignored. if (line.startsWith("Dialogue:")) { //we parse the dialogue caption = parseDialogueForASS(line.split(":", 2)[1].trim().split(",", 10), dialogueFormat, timer, tto); //and save the caption int key = caption.start.mseconds; //in case the key is already there, we increase it by a millisecond, since no duplicates are allowed while (tto.captions.containsKey(key)) key++; tto.captions.put(key, caption); } //next line lineCounter++; line = getLine(inputString, stringIndex++).trim(); } } else if (line.trim().equalsIgnoreCase("[Fonts]") || line.trim().equalsIgnoreCase("[Graphics]")) { //its the custom fonts or embedded graphics section //these are not supported tto.warnings += "The section " + line.trim() + " is not supported for conversion, all information there will be lost.\n\n"; line = getLine(inputString, stringIndex++).trim(); } else { tto.warnings += "Unrecognized section: " + line.trim() + " all information there is ignored."; line = getLine(inputString, stringIndex++).trim(); } } else { line = getLine(inputString, stringIndex++); lineCounter++; } } // parsed styles that are not used should be eliminated tto.cleanUnusedStyles(); } catch (NullPointerException e) { tto.warnings += "unexpected end of file, maybe last caption is not complete.\n\n"; } tto.built = true; return tto; } public String[] toFile(TimedTextObject tto) { //first we check if the TimedTextObject had been built, otherwise... if (!tto.built) return null; //we will write the lines in an ArrayList int index = 0; //the minimum size of the file is the number of captions and styles + lines for sections and formats and the script info, so we'll take some extra space. ArrayList<String> file = new ArrayList<String>(30 + tto.styling.size() + tto.captions.size()); //header is placed file.add(index++, "[Script Info]"); //title next String title = "Title: "; if (tto.title == null || tto.title.isEmpty()) title += tto.fileName; else title += tto.title; file.add(index++, title); //author next String author = "Original Script: "; if (tto.author == null || tto.author.isEmpty()) author += "Unknown"; else author += tto.author; file.add(index++, author); //additional info if (tto.copyright != null && !tto.copyright.isEmpty()) file.add(index++, "; " + tto.copyright); if (tto.description != null && !tto.description.isEmpty()) file.add(index++, "; " + tto.description); file.add(index++, "; Converted by the Online Subtitle Converter developed by J. David Requejo"); //mandatory info if (tto.useASSInsteadOfSSA) file.add(index++, "Script Type: V4.00+"); else file.add(index++, "Script Type: V4.00"); file.add(index++, "Collisions: Normal"); file.add(index++, "Timer: 100,0000"); if (tto.useASSInsteadOfSSA) file.add(index++, "WrapStyle: 1"); //an empty line is added file.add(index++, ""); //Styles section if (tto.useASSInsteadOfSSA) file.add(index++, "[V4+ Styles]"); else file.add(index++, "[V4 Styles]"); //define the format if (tto.useASSInsteadOfSSA) file.add(index++, "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding"); else file.add(index++, "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding"); //Next we iterate over the styles for (Style style : tto.styling.values()) { String styleLine = "Style: "; //name styleLine += style.iD + ","; styleLine += style.font + ","; styleLine += style.fontSize + ","; styleLine += getColorsForASS(tto.useASSInsteadOfSSA, style); styleLine += getOptionsForASS(tto.useASSInsteadOfSSA, style); //BorderStyle, Outline, Shadow styleLine += "1,2,2,"; styleLine += getAlignForASS(tto.useASSInsteadOfSSA, style.textAlign); //MarginL, MarginR, MarginV styleLine += ",0,0,0,"; //AlphaLevel if (!tto.useASSInsteadOfSSA) styleLine += "0,"; //Encoding styleLine += "0"; //and we add the style definition line file.add(index++, styleLine); } //an empty line is added file.add(index++, ""); //Events section file.add(index++, "[Events]"); //define the format if (tto.useASSInsteadOfSSA) file.add(index++, "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text"); else file.add(index++, "Format: Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text"); //Next we iterate over the captions for (Caption caption : tto.captions.values()) { //for each caption String line = "Dialogue: 0,"; //offset is applied if (tto.offset != 0) { caption.start.mseconds += tto.offset; caption.end.mseconds += tto.offset; } //start time line += caption.start.getTime("h:mm:ss.cs") + ","; //end time line += caption.end.getTime("h:mm:ss.cs") + ","; //offset is undone if (tto.offset != 0) { caption.start.mseconds -= tto.offset; caption.end.mseconds -= tto.offset; } //style if (caption.style != null) line += caption.style.iD; else line += "Default"; //default margins are used, no name or effect is recognized line += ",,0000,0000,0000,,"; //we add the caption text with \N as line breaks and clean of XML line += caption.content.replaceAll("<br />", "\\N").replaceAll("<.*?>", ""); //and we add the caption line file.add(index++, line); } //an empty line is added file.add(index++, ""); //we return the expected file as an array of String String[] toReturn = new String[file.size()]; for (int i = 0; i < toReturn.length; i++) { toReturn[i] = file.get(i); } return toReturn; } /* PRIVATEMETHODS */ /** * This methods transforms a format line from ASS according to a format definition into an Style object. * * @param line the format line without its declaration * @param styleFormat the list of attributes in this format line * @return a new Style object. */ private Style parseStyleForASS(String[] line, String[] styleFormat, int index, boolean isASS, String warnings) { Style newStyle = new Style(Style.defaultID()); if (line.length != styleFormat.length) { //both should have the same size warnings += "incorrectly formated line at " + index + "\n\n"; } else { for (int i = 0; i < styleFormat.length; i++) { //we go through every format parameter and save the interesting values if (styleFormat[i].trim().equalsIgnoreCase("Name")) { //we save the name newStyle.iD = line[i].trim(); } else if (styleFormat[i].trim().equalsIgnoreCase("Fontname")) { //we save the font newStyle.font = line[i].trim(); } else if (styleFormat[i].trim().equalsIgnoreCase("Fontsize")) { //we save the size newStyle.fontSize = line[i].trim(); } else if (styleFormat[i].trim().equalsIgnoreCase("PrimaryColour")) { //we save the color String color = line[i].trim(); if (isASS) { if (color.startsWith("&H")) newStyle.color = Style.getRGBValue("&HAABBGGRR", color); else newStyle.color = Style.getRGBValue("decimalCodedAABBGGRR", color); } else { if (color.startsWith("&H")) newStyle.color = Style.getRGBValue("&HBBGGRR", color); else newStyle.color = Style.getRGBValue("decimalCodedBBGGRR", color); } } else if (styleFormat[i].trim().equalsIgnoreCase("BackColour")) { //we save the background color String color = line[i].trim(); if (isASS) { if (color.startsWith("&H")) newStyle.backgroundColor = Style.getRGBValue("&HAABBGGRR", color); else newStyle.backgroundColor = Style.getRGBValue("decimalCodedAABBGGRR", color); } else { if (color.startsWith("&H")) newStyle.backgroundColor = Style.getRGBValue("&HBBGGRR", color); else newStyle.backgroundColor = Style.getRGBValue("decimalCodedBBGGRR", color); } } else if (styleFormat[i].trim().equalsIgnoreCase("Bold")) { //we save if bold newStyle.bold = Boolean.parseBoolean(line[i].trim()); } else if (styleFormat[i].trim().equalsIgnoreCase("Italic")) { //we save if italic newStyle.italic = Boolean.parseBoolean(line[i].trim()); } else if (styleFormat[i].trim().equalsIgnoreCase("Underline")) { //we save if underlined newStyle.underline = Boolean.parseBoolean(line[i].trim()); } else if (styleFormat[i].trim().equalsIgnoreCase("Alignment")) { //we save the alignment int placement = Integer.parseInt(line[i].trim()); if (isASS) { switch (placement) { case 1: newStyle.textAlign = "bottom-left"; break; case 2: newStyle.textAlign = "bottom-center"; break; case 3: newStyle.textAlign = "bottom-right"; break; case 4: newStyle.textAlign = "mid-left"; break; case 5: newStyle.textAlign = "mid-center"; break; case 6: newStyle.textAlign = "mid-right"; break; case 7: newStyle.textAlign = "top-left"; break; case 8: newStyle.textAlign = "top-center"; break; case 9: newStyle.textAlign = "top-right"; break; default: warnings += "undefined alignment for style at line " + index + "\n\n"; } } else { switch (placement) { case 9: newStyle.textAlign = "bottom-left"; break; case 10: newStyle.textAlign = "bottom-center"; break; case 11: newStyle.textAlign = "bottom-right"; break; case 1: newStyle.textAlign = "mid-left"; break; case 2: newStyle.textAlign = "mid-center"; break; case 3: newStyle.textAlign = "mid-right"; break; case 5: newStyle.textAlign = "top-left"; break; case 6: newStyle.textAlign = "top-center"; break; case 7: newStyle.textAlign = "top-right"; break; default: warnings += "undefined alignment for style at line " + index + "\n\n"; } } } } } return newStyle; } /** * This methods transforms a dialogue line from ASS according to a format definition into an Caption object. * * @param line the dialogue line without its declaration * @param dialogueFormat the list of attributes in this dialogue line * @param timer % to speed or slow the clock, above 100% span of the subtitles is reduced. * @return a new Caption object */ private Caption parseDialogueForASS(String[] line, String[] dialogueFormat, float timer, TimedTextObject tto) { Caption newCaption = new Caption(); //all information from fields 10 onwards are the caption text therefore needn't be split String captionText = line[9]; //text is cleaned before being inserted into the caption newCaption.content = captionText.replaceAll("\\{.*?\\}", "").replace("\n", "<br />").replace("\\N", "<br />"); for (int i = 0; i < dialogueFormat.length; i++) { //we go through every format parameter and save the interesting values if (dialogueFormat[i].trim().equalsIgnoreCase("Style")) { //we save the style Style s = tto.styling.get(line[i].trim()); if (s != null) newCaption.style = s; else tto.warnings += "undefined style: " + line[i].trim() + "\n\n"; } else if (dialogueFormat[i].trim().equalsIgnoreCase("Start")) { //we save the starting time newCaption.start = new Time("h:mm:ss.cs", line[i].trim()); } else if (dialogueFormat[i].trim().equalsIgnoreCase("End")) { //we save the starting time newCaption.end = new Time("h:mm:ss.cs", line[i].trim()); } } //timer is applied if (timer != 100) { newCaption.start.mseconds /= (timer / 100); newCaption.end.mseconds /= (timer / 100); } return newCaption; } /** * returns a string with the correctly formated colors * * @param useASSInsteadOfSSA true if formated for ASS * @return the colors in the decimal format */ private String getColorsForASS(boolean useASSInsteadOfSSA, Style style) { String colors; if (useASSInsteadOfSSA) //primary color(BBGGRR) with Alpha level (00) in front + 00FFFFFF + 00000000 + background color(BBGGRR) with Alpha level (80) in front colors = Integer.parseInt("00" + style.color.substring(4, 6) + style.color.substring(2, 4) + style.color.substring(0, 2), 16) + ",16777215,0," + Long.parseLong("80" + style.backgroundColor.substring(4, 6) + style.backgroundColor.substring(2, 4) + style.backgroundColor.substring(0, 2), 16) + ","; else { //primary color(BBGGRR) + FFFFFF + 000000 + background color(BBGGRR) String color = style.color.substring(4, 6) + style.color.substring(2, 4) + style.color.substring(0, 2); String bgcolor = style.backgroundColor.substring(4, 6) + style.backgroundColor.substring(2, 4) + style.backgroundColor.substring(0, 2); colors = Long.parseLong(color, 16) + ",16777215,0," + Long.parseLong(bgcolor, 16) + ","; } return colors; } /** * returns a string with the correctly formated options * * @param useASSInsteadOfSSA Use ASS or SSA? * @param style ASS styles * @return Options */ private String getOptionsForASS(boolean useASSInsteadOfSSA, Style style) { String options; if (style.bold) options = "-1,"; else options = "0,"; if (style.italic) options += "-1,"; else options += "0,"; if (useASSInsteadOfSSA) { if (style.underline) options += "-1,"; else options += "0,"; options += "0,100,100,0,0,"; } return options; } /** * converts the string explaining the alignment into the ASS equivalent integer offering bottom-center as default value * * @param useASSInsteadOfSSA Use Use ASS or SSA? * @param align String alignment * @return Placement */ private int getAlignForASS(boolean useASSInsteadOfSSA, String align) { if (useASSInsteadOfSSA) { int placement = 2; if ("bottom-left".equals(align)) placement = 1; else if ("bottom-center".equals(align)) placement = 2; else if ("bottom-right".equals(align)) placement = 3; else if ("mid-left".equals(align)) placement = 4; else if ("mid-center".equals(align)) placement = 5; else if ("mid-right".equals(align)) placement = 6; else if ("top-left".equals(align)) placement = 7; else if ("top-center".equals(align)) placement = 8; else if ("top-right".equals(align)) placement = 9; return placement; } else { int placement = 10; if ("bottom-left".equals(align)) placement = 9; else if ("bottom-center".equals(align)) placement = 10; else if ("bottom-right".equals(align)) placement = 11; else if ("mid-left".equals(align)) placement = 1; else if ("mid-center".equals(align)) placement = 2; else if ("mid-right".equals(align)) placement = 3; else if ("top-left".equals(align)) placement = 5; else if ("top-center".equals(align)) placement = 6; else if ("top-right".equals(align)) placement = 7; return placement; } } }