├── .gitignore ├── LICENSE.txt ├── README.md ├── pom.xml └── submerge-api ├── META-INF └── MANIFEST.MF ├── pom.xml └── src ├── main └── java │ └── com │ └── github │ └── dnbn │ └── submerge │ └── api │ ├── constant │ └── FontName.java │ ├── parser │ ├── ASSParser.java │ ├── BaseParser.java │ ├── ParserFactory.java │ ├── SRTParser.java │ ├── SubtitleParser.java │ └── exception │ │ ├── InvalidAssSubException.java │ │ ├── InvalidColorCode.java │ │ ├── InvalidFileException.java │ │ ├── InvalidSRTSubException.java │ │ └── InvalidSubException.java │ ├── subtitle │ ├── ass │ │ ├── ASSSub.java │ │ ├── ASSTime.java │ │ ├── Events.java │ │ ├── ScriptInfo.java │ │ └── V4Style.java │ ├── common │ │ ├── SubtitleLine.java │ │ ├── SubtitleTime.java │ │ ├── TimedLine.java │ │ ├── TimedObject.java │ │ └── TimedTextFile.java │ ├── config │ │ ├── Font.java │ │ └── SimpleSubConfig.java │ └── srt │ │ ├── SRTLine.java │ │ ├── SRTSub.java │ │ └── SRTTime.java │ └── utils │ ├── ColorUtils.java │ ├── ConvertionUtils.java │ └── FileUtils.java └── test └── java └── com.github.dnbn.submerge.api ├── Test.java └── TestJunit.java /.gitignore: -------------------------------------------------------------------------------- 1 | .metadata 2 | target/ 3 | *~ 4 | application.properties 5 | application.prod.properties 6 | hibernate.cfg.xml 7 | hibernate.reveng.xml 8 | bin/ 9 | .project 10 | .settings 11 | .classpath 12 | release.properties 13 | .idea 14 | *.iml 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) [2015] [Damien Benon] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SubtitleParserForAndroid 2 | Subtitle file parser. support .ass, .srt format 3 | 4 | reference from [submerge](https://github.com/linjonh/submerge) 5 | and then approve it to support android platform at least Android OS API 15, JAVA SDK 1.7. 6 |

because Submerge is for console program and using new JAVA SDK API 1.8, so it not support below android 26. 7 | 8 | 9 | # Usage 10 | 11 | Parsing ASS subtitles: 12 | ```java 13 | 14 | File file = new File("subtitle.ass"); 15 | 16 | ASSParser parser = new ASSParser(); 17 | ASSSub subtitle = parser.parse(file); 18 | 19 | System.out.println(subtitle.toString()); 20 | Parsing SRT subtitles: 21 | 22 | File file = new File("subtitle.srt"); 23 | SRTParser parser = new SRTParser(); 24 | 25 | SRTSub subtitle = parser.parse(file); 26 | 27 | System.out.println(subtitle.toString()); 28 | ``` 29 | Using interfaces: 30 | ```java 31 | File file = new File("subtitle.srt"); 32 | String extension = FilenameUtils.getExtension(file.getName()); 33 | 34 | SubtitleParser parser = ParserFactory.getParser(extension); 35 | TimedTextFile subtitle = parser.parse(file); 36 | 37 | System.out.println(subtitle.toString()); 38 | ``` -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.sonatype.oss 5 | oss-parent 6 | 9 7 | 8 | 9 | 4.0.0 10 | com.github.dnbn.submerge 11 | submerge-root 12 | 1.9.3-SNAPSHOT 13 | submerge-root 14 | pom 15 | 16 | Submerge is a tool that aims to merge two subtitles into one ASS displaying both: one at the top of the screen, the other at the bottom. It can also be used to convert SRT to ASS or ASS to SRT, or to change the encoding of a file. 17 | https://github.com/dnbn/submerge 18 | 19 | 20 | 21 | dnbn 22 | submerge.contact@gmail.com 23 | github 24 | https://github.com/dnbn/ 25 | 26 | 27 | 28 | 29 | 30 | MIT License 31 | http://www.opensource.org/licenses/mit-license.php 32 | 33 | 34 | 35 | 36 | scm-server 37 | 38 | 39 | 40 | scm:git:ssh://git@github.com/dnbn/submerge.git 41 | scm:git:ssh://git@github.com/dnbn/submerge.git 42 | https://github.com/dnbn/submerge 43 | 44 | 45 | 46 | submerge-api 47 | 48 | 49 | 50 | 51 | 52 | org.apache.maven.plugins 53 | maven-source-plugin 54 | 55 | 56 | attach-sources 57 | 58 | jar 59 | 60 | 61 | 62 | 63 | 64 | org.apache.maven.plugins 65 | maven-javadoc-plugin 66 | 67 | 68 | attach-javadocs 69 | 70 | jar 71 | 72 | 73 | 74 | 75 | 76 | org.apache.maven.plugins 77 | maven-release-plugin 78 | 79 | -Dgpg.passphrase=${gpg.passphrase} 80 | 81 | 82 | 83 | org.apache.maven.plugins 84 | maven-gpg-plugin 85 | 1.5 86 | 87 | ${gpg.passphrase} 88 | 89 | 90 | 91 | sign-artifacts 92 | verify 93 | 94 | sign 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | sonatype-nexus-snapshots 105 | Sonatype Nexus snapshot repository 106 | https://oss.sonatype.org/content/repositories/snapshots 107 | 108 | 109 | sonatype-nexus-staging 110 | Sonatype Nexus release repository 111 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /submerge-api/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | 3 | -------------------------------------------------------------------------------- /submerge-api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | com.github.dnbn.submerge 4 | submerge-root 5 | 1.9.3-SNAPSHOT 6 | 7 | 8 | Library to manage SRT and ASS subtitles 9 | 10 | 4.0.0 11 | submerge-api 12 | submerge-api 13 | jar 14 | 15 | 16 | 2.6 17 | 18 | 1.3.2 19 | 1.0.3 20 | 1.8 21 | 22 | 23 | 24 | 25 | 26 | 27 | org.apache.maven.plugins 28 | maven-eclipse-plugin 29 | 2.9 30 | 31 | true 32 | false 33 | 34 | 35 | 36 | 37 | org.apache.maven.plugins 38 | maven-compiler-plugin 39 | 2.3.2 40 | 41 | ${jdk.version} 42 | ${jdk.version} 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | commons-lang 52 | commons-lang 53 | ${commons-lang.version} 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | org.apache.commons 64 | commons-io 65 | ${commons-io} 66 | 67 | 68 | 69 | com.googlecode.juniversalchardet 70 | juniversalchardet 71 | ${juniversalchardet.version} 72 | 73 | 74 | 78 | 79 | 80 | org.junit.jupiter 81 | junit-jupiter-api 82 | RELEASE 83 | test 84 | 85 | 86 | junit 87 | junit 88 | 4.12 89 | test 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/constant/FontName.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.constant; 2 | 3 | /** 4 | * Enum all the supported font names of the application 5 | * 6 | */ 7 | public enum FontName { 8 | 9 | Arial("Arial"), 10 | CourierNew("Courier New"), 11 | Times("Times"), 12 | Helvetica("Helvetica"), 13 | DroidSans("Droid Sans"), 14 | Cursive("cursive"), 15 | Monospace("monospace"), 16 | Serif("serif"), 17 | SansSerif("sans-serif"), 18 | Fantasy("fantasy"), 19 | Courier("Courier"), 20 | Georgia("Georgia"), 21 | LucidaConsole("Lucida Console"), 22 | Papyrus("Papyrus"), 23 | Tahoma("Tahoma"), 24 | TeX("TeX"), 25 | Verdana("Verdana"), 26 | Verona("Verona"), 27 | SimSun("SimSun"), 28 | Ubuntu("Ubuntu"), 29 | UbuntuMono("Ubuntu Mono"), 30 | FreeMono("FreeMono"), 31 | LiberationSerif("Liberation Serif"), 32 | Purisa("Purisa"), 33 | TimesNewRoman("Times New Roman"); 34 | 35 | private String name; 36 | 37 | FontName(String name) { 38 | this.name = name; 39 | } 40 | 41 | /** 42 | * @return the name 43 | */ 44 | public String getName() { 45 | return this.name; 46 | } 47 | 48 | @Override 49 | public String toString() { 50 | return this.name; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/parser/ASSParser.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.parser; 2 | 3 | import com.github.dnbn.submerge.api.parser.exception.InvalidAssSubException; 4 | import com.github.dnbn.submerge.api.subtitle.ass.*; 5 | import com.github.dnbn.submerge.api.utils.ColorUtils; 6 | import org.apache.commons.lang.StringUtils; 7 | import org.apache.commons.lang.math.NumberUtils; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.IOException; 11 | import java.lang.reflect.Field; 12 | import java.lang.reflect.InvocationTargetException; 13 | import java.lang.reflect.Method; 14 | import java.util.*; 15 | 16 | /** 17 | * Parse SSA/ASS subtitles 18 | */ 19 | public class ASSParser extends BaseParser { 20 | 21 | /** 22 | * Comments: lines that start with this character are ignored 23 | */ 24 | private static final String COMMENTS_MARK = ";"; 25 | 26 | @Override 27 | protected void parse(BufferedReader br, ASSSub sub) throws IOException, InvalidAssSubException { 28 | 29 | String line = readFirstTextLine(br); 30 | 31 | if (line != null && !("[script info]").equalsIgnoreCase(line.trim())) { 32 | throw new InvalidAssSubException("The line that says “[Script Info]” must be the first line in the script."); 33 | } 34 | 35 | // [Script Info] 36 | sub.setScriptInfo(parseScriptInfo(br)); 37 | 38 | while ((line = readFirstTextLine(br)) != null) { 39 | if (line.matches("(?i:^\\[v.*styles\\+?]$)")) { 40 | // [V4+ Styles] 41 | sub.setStyle(parseStyle(br)); 42 | } else if (line.equalsIgnoreCase("[events]")) { 43 | // [Events] 44 | sub.setEvents(parseEvents(br)); 45 | } 46 | } 47 | 48 | if (sub.getStyle().isEmpty()) { 49 | throw new InvalidAssSubException("Missing style definition"); 50 | } 51 | 52 | if (sub.getEvents().isEmpty()) { 53 | throw new InvalidAssSubException("No text line found"); 54 | } 55 | } 56 | 57 | /** 58 | * Parse the events section from the reader.
59 | *

60 | * Example of events section: 61 | * 62 | *

 63 |      * [Events]
 64 |      * Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
 65 |      * Dialogue: 0,0:02:30.84,0:02:34.70,StlyeOne,,0000,0000,0000,,A text line
 66 |      * Dialogue: 0,0:02:34.92,0:02:37.54,StyleTwo,,0000,0000,0000,,Another text line
 67 |      * 
68 | * 69 | * @param br: the buffered reader 70 | * @throws IOException 71 | * @throws InvalidAssSubException 72 | * @throws IOException 73 | */ 74 | private static Set parseEvents(BufferedReader br) throws IOException, InvalidAssSubException { 75 | String[] eventsFormat = findFormat(br, "events"); 76 | 77 | Set events = new TreeSet<>(); 78 | String line = readFirstTextLine(br); 79 | 80 | while (line != null && !line.startsWith("[")) { 81 | 82 | if (line.startsWith(Events.DIALOGUE) && !line.startsWith(COMMENTS_MARK)) { 83 | String info = findInfo(line, Events.DIALOGUE); 84 | String[] dialogLine = StringUtils.splitByWholeSeparatorPreserveAllTokens(info, Events.SEP); 85 | 86 | // The last field will always be the Text field, so that it can contain 87 | // commas. 88 | int lengthDialog = dialogLine.length; 89 | int lengthFormat = eventsFormat.length; 90 | 91 | if (lengthDialog < lengthFormat) { 92 | throw new InvalidAssSubException("Incorrect dialog line : " + info); 93 | } 94 | 95 | if (lengthDialog > lengthFormat) { 96 | // The text field contains commas 97 | StringBuilder joiner = new StringBuilder(); 98 | for (int i = lengthFormat - 1; i < lengthDialog; i++) { 99 | joiner.append(dialogLine[i]) 100 | .append(Events.SEP); 101 | } 102 | joiner.deleteCharAt(joiner.length() - 1); 103 | dialogLine[lengthFormat - 1] = joiner.toString(); 104 | dialogLine = Arrays.copyOfRange(dialogLine, 0, lengthFormat); 105 | } 106 | 107 | events.add(parseDialog(eventsFormat, dialogLine)); 108 | } 109 | 110 | line = markAndRead(br); 111 | 112 | } 113 | 114 | reset(br, line); 115 | 116 | return events; 117 | } 118 | 119 | /** 120 | * Parse the style section from the reader.
121 | *

122 | * Example of style section: 123 | * 124 | *

125 |      * [V4+ Styles]
126 |      * Format: Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,OutlineColour
127 |      * Style: StyleOne,Arial,16,64250,16777215,0
128 |      * Style: StyleTwo,Arial,16,16383999,16777215,0
129 |      * 
130 | * 131 | * @param br: the buffered reader 132 | * @throws IOException 133 | * @throws InvalidAssSubException 134 | */ 135 | private static List parseStyle(BufferedReader br) throws IOException, InvalidAssSubException { 136 | String[] styleFormat = findFormat(br, "styles"); 137 | 138 | List styles = new ArrayList<>(); 139 | String line = readFirstTextLine(br); 140 | int index = 1; 141 | while (line != null && !line.startsWith("[")) { 142 | if (line.startsWith(V4Style.STYLE) && !line.startsWith(COMMENTS_MARK)) { 143 | String[] textLine = line.split(":"); 144 | if (textLine.length > 1) { 145 | String[] styleLine = textLine[1].split(V4Style.SEP); 146 | styles.add(parseV4Style(styleFormat, styleLine, index)); 147 | index++; 148 | } 149 | } 150 | 151 | line = markAndRead(br); 152 | } 153 | 154 | reset(br, line); 155 | 156 | return styles; 157 | } 158 | 159 | /** 160 | * Return the Events object from text dialog line 161 | * 162 | * @param eventsFormat: the format definition 163 | * @param dialogLine: the dialog line 164 | * @return the Events object 165 | * @throws InvalidAssSubException 166 | */ 167 | private static Events parseDialog(String[] eventsFormat, String[] dialogLine) throws InvalidAssSubException { 168 | 169 | Events events = new Events(); 170 | 171 | for (int i = 0; i < eventsFormat.length; i++) { 172 | String property = StringUtils.uncapitalize(eventsFormat[i].trim()); 173 | String value = dialogLine[i].trim(); 174 | 175 | try { 176 | switch (property) { 177 | case "start": 178 | events.getTime().setStart(ASSTime.fromString(value)); 179 | break; 180 | case "end": 181 | events.getTime().setEnd(ASSTime.fromString(value)); 182 | break; 183 | case "text": 184 | List textLines = Arrays.asList(value.split("\\\\N")); 185 | events.setTextLines(new ArrayList<>(textLines)); 186 | break; 187 | default: 188 | String error = callProperty(Events.class, events, property, value); 189 | if (error != null) { 190 | throw new InvalidAssSubException("Invalid property (" + property + ") " + value); 191 | } 192 | break; 193 | } 194 | } catch (Throwable e) { 195 | throw new InvalidAssSubException("Invalid time for property " + property + " : " + value); 196 | } 197 | 198 | } 199 | 200 | return events; 201 | } 202 | 203 | /** 204 | * Return the V4Style object from text style line 205 | * 206 | * @param styleFormat: format line 207 | * @param styleLine: the style line 208 | * @param lineIndex: the line index 209 | * @return the style object 210 | * @throws InvalidAssSubException 211 | */ 212 | private static V4Style parseV4Style(String[] styleFormat, String[] styleLine, int lineIndex) 213 | throws InvalidAssSubException { 214 | 215 | String message = "Style at index " + lineIndex + ": "; 216 | 217 | if (styleFormat.length != styleLine.length) { 218 | throw new InvalidAssSubException(message + "does not match style definition"); 219 | } 220 | 221 | V4Style style = new V4Style(); 222 | for (int i = 0; i < styleFormat.length; i++) { 223 | String property = StringUtils.uncapitalize(styleFormat[i].trim()); 224 | String value = styleLine[i].trim(); 225 | 226 | if (property.toLowerCase().contains("colour")) { 227 | // Colors can be number (bgr) or string (&HBBGGRR or &HAABBGGRR) 228 | try { 229 | Integer.parseInt(value); 230 | } catch (NumberFormatException e) { 231 | int bgr = getRGB(value); 232 | if (bgr != -1) { 233 | value = Integer.toString(bgr); 234 | } 235 | } 236 | } 237 | 238 | String error = callProperty(V4Style.class, style, property, value); 239 | 240 | if (error != null) { 241 | throw new InvalidAssSubException(message + error); 242 | } 243 | } 244 | 245 | if (StringUtils.isEmpty(style.getName())) { 246 | throw new InvalidAssSubException(message + " missing name"); 247 | } 248 | 249 | return style; 250 | } 251 | 252 | /** 253 | * Get the RGB code from the &HBBGGRR or &HAABBGGRR pattern 254 | * 255 | * @param value: the value to convert 256 | * @return the bgr code 257 | */ 258 | private static int getRGB(String value) { 259 | 260 | int length = value.length(); 261 | int rgb = -1; 262 | if (length == 10) { 263 | // From ASS 264 | rgb = ColorUtils.HAABBGGRRToRGB(value); 265 | } else if (length == 8) { 266 | // From SSA 267 | rgb = ColorUtils.HBBGGRRToRGB(value); 268 | } 269 | return rgb; 270 | } 271 | 272 | /** 273 | * Parse the script info section from the reader.
274 | *

275 | * Example of script info section: 276 | * 277 | *

278 |      * [Script Info]
279 |      * ScriptType: v4.00+
280 |      * Collisions: Normal
281 |      * Timer: 100,0000
282 |      * Title: My movie title
283 |      * 
284 | * 285 | * @param br: the buffered reader 286 | * @throws IOException 287 | * @throws InvalidAssSubException 288 | */ 289 | private static ScriptInfo parseScriptInfo(BufferedReader br) throws IOException, InvalidAssSubException { 290 | 291 | ScriptInfo scriptInfo = new ScriptInfo(); 292 | String line = readFirstTextLine(br); 293 | 294 | while (line != null && !line.startsWith("[")) { 295 | 296 | if (!line.startsWith(COMMENTS_MARK)) { 297 | 298 | String[] split = line.split(ScriptInfo.SEP); 299 | if (split.length > 1) { 300 | String property = StringUtils.deleteWhitespace(split[0]); 301 | property = StringUtils.uncapitalize(property); 302 | 303 | StringBuilder joiner = new StringBuilder(); 304 | for (int i = 1; i < split.length; i++) { 305 | joiner.append(split[i]) 306 | .append(ScriptInfo.SEP); 307 | } 308 | joiner.delete(joiner.length() - ScriptInfo.SEP.length(), joiner.length()); 309 | // StringJoiner joiner = new StringJoiner(ScriptInfo.SEP); 310 | // for (int i = 1; i < split.length; i++) { 311 | // joiner.add(split[i]); 312 | // } 313 | String value = joiner.toString().trim(); 314 | 315 | String error = callProperty(ScriptInfo.class, scriptInfo, property, value); 316 | 317 | if (error != null) { 318 | throw new InvalidAssSubException("Script info : " + error); 319 | } 320 | 321 | } 322 | 323 | } 324 | 325 | line = markAndRead(br); 326 | } 327 | 328 | reset(br, line); 329 | 330 | return scriptInfo; 331 | } 332 | 333 | private static String callProperty(Class clazz, Object object, String property, String value) { 334 | String error = null; 335 | 336 | try { 337 | // System.out.println("clazz = [" + clazz + "], object = [" + object + "], property = [" + property + "], value = [" + value + "]"); 338 | 339 | Field declaredField = clazz.getDeclaredField(property); 340 | Class fieldType = declaredField.getType(); 341 | 342 | Method declaredMethod = clazz.getDeclaredMethod("set" + StringUtils.capitalize(property), fieldType); 343 | 344 | String type = fieldType.getSimpleName(); 345 | switch (type) { 346 | case "String": 347 | // PropertyUtils.setProperty(object, property, value); 348 | declaredMethod.invoke(object, value); 349 | break; 350 | case "int": 351 | declaredMethod.invoke(object, NumberUtils.toInt(value)); 352 | 353 | // PropertyUtils.setProperty(object, property, NumberUtils.toInt(value)); 354 | break; 355 | case "boolean": 356 | boolean boolValue = NumberUtils.toInt(value) == -1; 357 | declaredMethod.invoke(object, boolValue); 358 | 359 | // PropertyUtils.setProperty(object, property, boolValue); 360 | break; 361 | case "double": 362 | double doubleValue = NumberUtils.toDouble(value.replace(",", ".").trim()); 363 | declaredMethod.invoke(object, doubleValue); 364 | // PropertyUtils.setProperty(object, property, doubleValue); 365 | break; 366 | default: 367 | break; 368 | } 369 | } catch (NoSuchFieldException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { 370 | e.printStackTrace(); 371 | System.out.println("clazz = [" + clazz + "], property = [" + property + "], value = [" + value + "]"); 372 | } 373 | 374 | return error; 375 | } 376 | 377 | /** 378 | * Call a specific property of an object with reflection 379 | * 380 | * @param object: the object to set a property 381 | * @param property: the property to define 382 | * @param value: the value to set 383 | * @return the error message if an error has occured, null otherwise 384 | */ 385 | // private static String callProperty(Object object, String property, String value) { 386 | // 387 | // String error = null; 388 | // try { 389 | // PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(object, property); 390 | // 391 | // if (descriptor != null) { 392 | // String type = descriptor.getPropertyType().getSimpleName(); 393 | // switch (type) { 394 | // case "String": 395 | // PropertyUtils.setProperty(object, property, value); 396 | // break; 397 | // case "int": 398 | // PropertyUtils.setProperty(object, property, NumberUtils.toInt(value)); 399 | // break; 400 | // case "boolean": 401 | // boolean boolValue = NumberUtils.toInt(value) == -1; 402 | // PropertyUtils.setProperty(object, property, boolValue); 403 | // break; 404 | // case "double": 405 | // double doubleValue = NumberUtils.toDouble(value.replace(",", ".").trim()); 406 | // PropertyUtils.setProperty(object, property, doubleValue); 407 | // break; 408 | // default: 409 | // break; 410 | // } 411 | // } 412 | // 413 | // } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { 414 | // // Property not supported, do nothing 415 | // } 416 | // 417 | // return error; 418 | // } 419 | 420 | /** 421 | * Get the format string definition 422 | * 423 | * @param br: the buffered reader 424 | * @param sectionName: the name of the section to parse 425 | * @return the format string definition 426 | * @throws IOException 427 | * @throws InvalidAssSubException 428 | */ 429 | private static String[] findFormat(BufferedReader br, String sectionName) throws IOException, 430 | InvalidAssSubException { 431 | 432 | String line = readFirstTextLine(br); 433 | if (StringUtils.isEmpty(line)) { 434 | throw new InvalidAssSubException("Missing format definition in " + sectionName + " section"); 435 | } 436 | if (!line.trim().startsWith(ASSSub.FORMAT)) { 437 | String capitalized = StringUtils.capitalize(sectionName); 438 | throw new InvalidAssSubException(capitalized + " definition must start with 'Format' line"); 439 | } 440 | return findInfo(line, ASSSub.FORMAT).split(V4Style.SEP); 441 | } 442 | 443 | /** 444 | * Find the information after ":" in a text line 445 | * 446 | * @param line: the line 447 | * @param search: the information to search 448 | * @return info or null if the info is empty / not found 449 | */ 450 | private static String findInfo(String line, String search) { 451 | 452 | String info = null; 453 | String sep = ":"; 454 | if (line.trim().toLowerCase().startsWith(search.toLowerCase()) && line.indexOf(sep) > 0) { 455 | info = line.substring(line.indexOf(sep) + 1, line.length()).trim(); 456 | } 457 | return StringUtils.isEmpty(info) ? null : info; 458 | } 459 | 460 | } 461 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/parser/BaseParser.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.parser; 2 | 3 | import com.github.dnbn.submerge.api.parser.exception.InvalidFileException; 4 | import com.github.dnbn.submerge.api.parser.exception.InvalidSubException; 5 | import com.github.dnbn.submerge.api.subtitle.common.TimedTextFile; 6 | import com.github.dnbn.submerge.api.utils.FileUtils; 7 | import org.apache.commons.io.IOUtils; 8 | import org.apache.commons.lang.StringUtils; 9 | 10 | import java.io.*; 11 | import java.lang.reflect.ParameterizedType; 12 | import java.lang.reflect.Type; 13 | 14 | public abstract class BaseParser implements SubtitleParser { 15 | 16 | /** 17 | * UTF-8 BOM Marker 18 | */ 19 | private static final char BOM_MARKER = '\ufeff'; 20 | 21 | @Override 22 | public T parse(File file, String encodingCharSet) { 23 | 24 | if (!file.isFile()) { 25 | throw new InvalidFileException("File " + file.getName() + " is invalid"); 26 | } 27 | 28 | try (FileInputStream fis = new FileInputStream(file)) { 29 | return parse(fis, file.getName(), encodingCharSet); 30 | } catch (Exception e) { 31 | throw new RuntimeException(e); 32 | } 33 | } 34 | 35 | @SuppressWarnings("unchecked") 36 | @Override 37 | public T parse(InputStream is, String fileName, String encodingCharSet) { 38 | 39 | try { 40 | Type type = this.getClass().getGenericSuperclass(); 41 | T sub = ((Class) ((ParameterizedType) type).getActualTypeArguments()[0]).newInstance(); 42 | 43 | byte[] bytes = IOUtils.toByteArray(is); 44 | 45 | try (InputStream nis = new ByteArrayInputStream(bytes); 46 | InputStreamReader isr = new InputStreamReader(nis, encodingCharSet != null ? encodingCharSet : FileUtils.guessEncoding(bytes)); 47 | BufferedReader br = new BufferedReader(isr)) { 48 | 49 | skipBom(br); 50 | sub.setFileName(fileName); 51 | parse(br, sub); 52 | } 53 | 54 | return sub; 55 | 56 | } catch (IOException e) { 57 | throw new InvalidFileException(e); 58 | } catch (InstantiationException | IllegalAccessException e) { 59 | throw new RuntimeException(e); 60 | } 61 | } 62 | 63 | /** 64 | * Parse the subtitle file into a ParsableSubtitle object 65 | * 66 | * @param br: the buffered reader 67 | * @param sub : the subtitle object to fill 68 | * @throws IOException 69 | * @throws InvalidSubException if an error has occured when parsing the subtitle file 70 | */ 71 | protected abstract void parse(BufferedReader br, T sub) throws IOException; 72 | 73 | /** 74 | * Ignore blank spaces and return the first text line 75 | * 76 | * @param br: the buffered reader 77 | * @throws IOException 78 | */ 79 | protected static String readFirstTextLine(BufferedReader br) throws IOException { 80 | 81 | String line = null; 82 | while ((line = br.readLine()) != null) { 83 | if (!StringUtils.isEmpty(line.trim())) { 84 | break; 85 | } 86 | } 87 | return line; 88 | } 89 | 90 | /** 91 | * Remove the byte order mark if exists 92 | * 93 | * @param br: the buffered reader 94 | * @throws IOException 95 | */ 96 | private static void skipBom(BufferedReader br) throws IOException { 97 | 98 | br.mark(4); 99 | if (BOM_MARKER != br.read()) { 100 | br.reset(); 101 | } 102 | } 103 | 104 | /** 105 | * Reset the reader at the previous mark if the current line is a new section 106 | * 107 | * @param br: the reader 108 | * @param line: the current line 109 | * @throws IOException 110 | */ 111 | protected static void reset(BufferedReader br, String line) throws IOException { 112 | 113 | if (line != null && line.startsWith("[")) { 114 | br.reset(); 115 | } 116 | } 117 | 118 | /** 119 | * Mark the position in the reader and read the next text line 120 | * 121 | * @param br: the buffered reader 122 | * @return the next text line 123 | * @throws IOException 124 | */ 125 | protected static String markAndRead(BufferedReader br) throws IOException { 126 | 127 | br.mark(32); 128 | return readFirstTextLine(br); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/parser/ParserFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.parser; 2 | 3 | import org.apache.commons.lang.NotImplementedException; 4 | 5 | public final class ParserFactory { 6 | 7 | /** 8 | * Return the subtitle parser for the subtitle format matching the extension 9 | * 10 | * @param extension the subtitle extention 11 | * @return the subtitle parser, null if no matching parser 12 | */ 13 | public static SubtitleParser getParser(String extension) { 14 | 15 | SubtitleParser parser = null; 16 | String lowerExt = extension.toLowerCase(); 17 | 18 | if ("ass".equals(lowerExt) || "ssa".equals(lowerExt)) { 19 | parser = new ASSParser(); 20 | } else if ("srt".equalsIgnoreCase(lowerExt)) { 21 | parser = new SRTParser(); 22 | } else { 23 | throw new NotImplementedException(extension + " format not supported"); 24 | } 25 | 26 | return parser; 27 | } 28 | 29 | /** 30 | * Private constructor 31 | */ 32 | private ParserFactory() { 33 | 34 | throw new AssertionError(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/parser/SRTParser.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.parser; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | import org.apache.commons.lang.StringUtils; 9 | 10 | import com.github.dnbn.submerge.api.parser.exception.InvalidSRTSubException; 11 | import com.github.dnbn.submerge.api.parser.exception.InvalidSubException; 12 | import com.github.dnbn.submerge.api.subtitle.srt.SRTLine; 13 | import com.github.dnbn.submerge.api.subtitle.srt.SRTSub; 14 | import com.github.dnbn.submerge.api.subtitle.srt.SRTTime; 15 | 16 | /** 17 | * Parse SRT subtitles 18 | */ 19 | public final class SRTParser extends BaseParser { 20 | 21 | @Override 22 | protected void parse(BufferedReader br, SRTSub sub) throws IOException, InvalidSubException { 23 | 24 | boolean found = true; 25 | while (found) { 26 | SRTLine line = firstIn(br); 27 | if (found = (line != null)) { 28 | sub.add(line); 29 | } 30 | } 31 | } 32 | 33 | /** 34 | * Extract the firt SRTLine found in a buffered reader.
35 | * 36 | * Example of SRT line: 37 | * 38 | *
 39 | 	 * 1
 40 | 	 * 00:02:46,813 --> 00:02:50,063
 41 | 	 * A text line
 42 | 	 * 
43 | * 44 | * @param br 45 | * @return SRTLine the line extracted, null if no SRTLine found 46 | * @throws IOException 47 | * @throws InvalidSRTSubException 48 | */ 49 | private static SRTLine firstIn(BufferedReader br) throws IOException, InvalidSRTSubException { 50 | 51 | String idLine = readFirstTextLine(br); 52 | String timeLine = br.readLine(); 53 | 54 | if (idLine == null || timeLine == null) { 55 | return null; 56 | } 57 | 58 | int id = parseId(idLine); 59 | SRTTime time = parseTime(timeLine); 60 | 61 | List textLines = new ArrayList<>(); 62 | String testLine; 63 | while ((testLine = br.readLine()) != null) { 64 | if (StringUtils.isEmpty(testLine.trim())) { 65 | break; 66 | } 67 | textLines.add(testLine); 68 | } 69 | 70 | return new SRTLine(id, time, textLines); 71 | } 72 | 73 | /** 74 | * Extract a subtitle id from string 75 | * 76 | * @param textLine ex 1 77 | * @return the id extracted 78 | * @throws InvalidSRTSubException 79 | */ 80 | private static int parseId(String textLine) throws InvalidSRTSubException { 81 | 82 | int idSRTLine; 83 | try { 84 | idSRTLine = Integer.parseInt(textLine.trim()); 85 | } catch (NumberFormatException e) { 86 | throw new InvalidSRTSubException("Expected id not found -> " + textLine); 87 | } 88 | 89 | return idSRTLine; 90 | } 91 | 92 | /** 93 | * Extract a subtitle time from string 94 | * 95 | * @param timeLine: ex 00:02:08,822 --> 00:02:11,574 96 | * @return the SRTTime object 97 | * @throws InvalidSRTSubException 98 | */ 99 | private static SRTTime parseTime(String timeLine) throws InvalidSRTSubException { 100 | 101 | SRTTime time = null; 102 | String times[] = timeLine.split(SRTTime.DELIMITER.trim()); 103 | 104 | if (times.length != 2) { 105 | throw new InvalidSRTSubException("Subtitle " + timeLine + " - invalid times : " + timeLine); 106 | } 107 | 108 | try { 109 | long start = SRTTime.fromString(times[0]); 110 | long end = SRTTime.fromString(times[1]); 111 | time = new SRTTime(start, end); 112 | } catch (Throwable e) { 113 | throw new InvalidSRTSubException("Invalid time string : " + timeLine, e); 114 | } 115 | 116 | return time; 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/parser/SubtitleParser.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.parser; 2 | 3 | import com.github.dnbn.submerge.api.parser.exception.InvalidFileException; 4 | import com.github.dnbn.submerge.api.parser.exception.InvalidSubException; 5 | import com.github.dnbn.submerge.api.subtitle.common.TimedTextFile; 6 | 7 | import java.io.File; 8 | import java.io.InputStream; 9 | 10 | public interface SubtitleParser { 11 | 12 | /** 13 | * Parse a subtitle file and return the corresponding subtitle object 14 | * 15 | * @param file the subtitle file 16 | * @return the subtitle object 17 | * @throws InvalidSubException if the subtitle is not valid 18 | * @throws InvalidFileException if the file is not valid 19 | */ 20 | TimedTextFile parse(File file, String encodingCharSet); 21 | 22 | /** 23 | * Parse a subtitle file from an inputstream and return the corresponding subtitle 24 | * object 25 | * 26 | * @param is the input stream 27 | * @param fileName the fileName 28 | * @return the subtitle object 29 | * @throws InvalidSubException if the subtitle is not valid 30 | * @throws InvalidFileException if the file is not valid 31 | */ 32 | TimedTextFile parse(InputStream is, String fileName, String encodingCharSet); 33 | } 34 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/parser/exception/InvalidAssSubException.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.parser.exception; 2 | 3 | public class InvalidAssSubException extends InvalidSubException { 4 | 5 | private static final long serialVersionUID = 8942033846085284666L; 6 | 7 | public InvalidAssSubException() { 8 | } 9 | 10 | public InvalidAssSubException(String arg0) { 11 | super(arg0); 12 | } 13 | 14 | public InvalidAssSubException(Throwable arg0) { 15 | super(arg0); 16 | } 17 | 18 | public InvalidAssSubException(String arg0, Throwable arg1) { 19 | super(arg0, arg1); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/parser/exception/InvalidColorCode.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.parser.exception; 2 | 3 | public class InvalidColorCode extends RuntimeException { 4 | 5 | private static final long serialVersionUID = -4904697807940273825L; 6 | 7 | public InvalidColorCode() { 8 | } 9 | 10 | public InvalidColorCode(String message) { 11 | super(message); 12 | } 13 | 14 | public InvalidColorCode(Throwable cause) { 15 | super(cause); 16 | } 17 | 18 | public InvalidColorCode(String message, Throwable cause) { 19 | super(message, cause); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/parser/exception/InvalidFileException.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.parser.exception; 2 | 3 | public class InvalidFileException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = -943455563476464982L; 6 | 7 | public InvalidFileException() { 8 | } 9 | 10 | public InvalidFileException(String message) { 11 | super(message); 12 | } 13 | 14 | public InvalidFileException(Throwable cause) { 15 | super(cause); 16 | } 17 | 18 | public InvalidFileException(String message, Throwable cause) { 19 | super(message, cause); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/parser/exception/InvalidSRTSubException.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.parser.exception; 2 | 3 | public class InvalidSRTSubException extends InvalidSubException { 4 | 5 | private static final long serialVersionUID = -8672533341983848962L; 6 | 7 | public InvalidSRTSubException() { 8 | } 9 | 10 | public InvalidSRTSubException(String arg0) { 11 | super(arg0); 12 | } 13 | 14 | public InvalidSRTSubException(Throwable arg0) { 15 | super(arg0); 16 | } 17 | 18 | public InvalidSRTSubException(String arg0, Throwable arg1) { 19 | super(arg0, arg1); 20 | } 21 | 22 | 23 | } 24 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/parser/exception/InvalidSubException.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.parser.exception; 2 | 3 | public class InvalidSubException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = -8431409375872882596L; 6 | 7 | public InvalidSubException() { 8 | } 9 | 10 | public InvalidSubException(String arg0) { 11 | super(arg0); 12 | } 13 | 14 | public InvalidSubException(Throwable arg0) { 15 | super(arg0); 16 | } 17 | 18 | public InvalidSubException(String arg0, Throwable arg1) { 19 | super(arg0, arg1); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/subtitle/ass/ASSSub.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.subtitle.ass; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.InputStream; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Set; 8 | import java.util.TreeSet; 9 | 10 | import com.github.dnbn.submerge.api.subtitle.common.TimedLine; 11 | import com.github.dnbn.submerge.api.subtitle.common.TimedTextFile; 12 | 13 | /** 14 | * The class ASSSub represents a SubStation Alpha subtitle 15 | * 16 | */ 17 | public class ASSSub implements TimedTextFile { 18 | 19 | /** 20 | * Serial 21 | */ 22 | private static final long serialVersionUID = 8812933867812351549L; 23 | 24 | 25 | /** 26 | * Format 27 | */ 28 | public static final String FORMAT = "Format"; 29 | 30 | /** 31 | * Events section 32 | */ 33 | private static final String EVENTS = "[Events]"; 34 | 35 | /** 36 | * Styles section 37 | */ 38 | private static final String V4_STYLES = "[V4+ Styles]"; 39 | 40 | /** 41 | * Script info section 42 | */ 43 | private static final String SCRIPT_INFO = "[Script Info]"; 44 | 45 | /** 46 | * Line separator 47 | */ 48 | private static final String NEW_LINE = "\n"; 49 | 50 | /** 51 | * Key / Value info separator. Ex : "Color: red" 52 | */ 53 | public static final String SEP = ": "; 54 | 55 | /** 56 | * Subtitle name 57 | */ 58 | private String filename; 59 | 60 | /** 61 | * Headers and general information about the script 62 | */ 63 | private ScriptInfo scriptInfo = new ScriptInfo(); 64 | 65 | /** 66 | * Style definitions required by the script 67 | */ 68 | private List style = new ArrayList<>(); 69 | 70 | /** 71 | * Events for the script - all the subtitles, comments, pictures, sounds, movies and 72 | * commands 73 | */ 74 | private Set events = new TreeSet<>(); 75 | 76 | @Override 77 | public String toString() { 78 | StringBuilder sb = new StringBuilder(); 79 | 80 | // [Script Info] 81 | sb.append(SCRIPT_INFO).append(NEW_LINE).append(this.scriptInfo.toString()); 82 | sb.append(NEW_LINE).append(NEW_LINE); 83 | 84 | // [V4 Styles] 85 | sb.append(V4_STYLES).append(NEW_LINE); 86 | sb.append(FORMAT).append(SEP).append(V4Style.FORMAT_STRING).append(NEW_LINE); 87 | this.style.forEach(s -> sb.append(s.toString()).append(NEW_LINE)); 88 | sb.append(NEW_LINE); 89 | 90 | // [Events] 91 | sb.append(EVENTS).append(NEW_LINE); 92 | sb.append(FORMAT).append(SEP).append(Events.FORMAT_STRING).append(NEW_LINE); 93 | this.events.forEach(e -> sb.append(e.toString()).append(NEW_LINE)); 94 | 95 | return sb.toString(); 96 | } 97 | 98 | /** 99 | * Get the ass file as an input stream 100 | * 101 | * @return the file 102 | */ 103 | public InputStream toInputStream() { 104 | return new ByteArrayInputStream(toString().getBytes()); 105 | } 106 | 107 | // ===================== getter and setter start ===================== 108 | 109 | public ScriptInfo getScriptInfo() { 110 | return this.scriptInfo; 111 | } 112 | 113 | public void setScriptInfo(ScriptInfo scriptInfo) { 114 | this.scriptInfo = scriptInfo; 115 | } 116 | 117 | public List getStyle() { 118 | return this.style; 119 | } 120 | 121 | public void setStyle(List style) { 122 | this.style = style; 123 | } 124 | 125 | public Set getEvents() { 126 | return this.events; 127 | } 128 | 129 | public void setEvents(Set events) { 130 | this.events = events; 131 | } 132 | 133 | public String getFilename() { 134 | return this.filename; 135 | } 136 | 137 | @Override 138 | public void setFileName(String fileName) { 139 | this.filename = fileName; 140 | } 141 | 142 | @Override 143 | public String getFileName() { 144 | return this.filename; 145 | } 146 | 147 | @Override 148 | public Set getTimedLines() { 149 | return this.events; 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/subtitle/ass/ASSTime.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.subtitle.ass; 2 | 3 | //import java.time.LocalTime; 4 | //import java.time.format.DateTimeFormatter; 5 | 6 | import com.github.dnbn.submerge.api.subtitle.common.SubtitleTime; 7 | 8 | import java.util.Arrays; 9 | 10 | /** 11 | * The class ASSTime represents a SubStation Alpha time : meaning the time at 12 | * which the text will appear and disappear onscreen 13 | */ 14 | public class ASSTime extends SubtitleTime { 15 | 16 | /** 17 | * Serial 18 | */ 19 | private static final long serialVersionUID = -8393452818120120069L; 20 | 21 | /** 22 | * The time pattern 23 | */ 24 | public static final String TIME_PATTERN = "H:mm:ss.SS"; 25 | private static final String TS_PATTERN = "%02d:%02d:%02d.%02d"; 26 | 27 | /** 28 | * The time pattern formatter 29 | */ 30 | // public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(TIME_PATTERN); 31 | 32 | /** 33 | * Constructor 34 | */ 35 | public ASSTime(long start, long end) { 36 | super(start, end); 37 | } 38 | 39 | /** 40 | * Constructor 41 | */ 42 | public ASSTime() { 43 | super(); 44 | } 45 | @Override 46 | public String toString() { 47 | StringBuilder sb = new StringBuilder(); 48 | sb.append(format(this.start)); 49 | sb.append(" "); 50 | sb.append(format(this.end)); 51 | return sb.toString(); 52 | } 53 | /** 54 | * Convert a LocalTime to string 55 | * 56 | * @param time: the time to format 57 | * @return the formatted time 58 | */ 59 | public static String format(long time) { 60 | // if (time != null) { 61 | // return time.format(FORMATTER); 62 | // } else { 63 | // System.out.println("time = [" + time + "]"); 64 | // return "00:00:00"; 65 | // } 66 | long ms = time % 1000; 67 | long s = time / 1000; 68 | long hh = s / 60 / 60; 69 | long min = (s / 60) % 60; 70 | long sec = s % 60; 71 | 72 | return String.format(TS_PATTERN, hh, min, sec, ms); 73 | } 74 | 75 | /** 76 | * @param in string like 10:23:56.90 最小为0.01秒 77 | * @return long milliseconds 78 | */ 79 | public static long fromString(String in) { 80 | String[] split1 = in.split(":"); 81 | long hours = Long.parseLong(split1[0].trim()); 82 | long minutes = Long.parseLong(split1[1].trim()); 83 | long seconds = 0; 84 | long millies = 0; 85 | if (split1.length > 2) { 86 | String[] split = split1[2].split("\\.");//ASS format 87 | seconds = Long.parseLong(split[0].trim()); 88 | if (split.length > 1) { 89 | millies = Long.parseLong(split[1].trim()); 90 | } else { 91 | System.out.println("in = [" + in + "]"); 92 | } 93 | } else { 94 | System.out.println("in = [" + in + "] split:" + Arrays.toString(split1)); 95 | } 96 | return hours * 60 * 60 * 1000 + minutes * 60 * 1000 + seconds * 1000 + millies; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/subtitle/ass/Events.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.subtitle.ass; 2 | 3 | import com.github.dnbn.submerge.api.subtitle.common.SubtitleLine; 4 | import org.apache.commons.lang.StringUtils; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Contain the subtitle text, their timings, and how it should be displayed. The fields 10 | * which appear in each Dialogue line are defined by a Format: line, which must appear 11 | * before any events in the section. The format line specifies how SSA will interpret all 12 | * following Event lines. 13 | *

14 | * The field names must be spelled correctly, and are as follows: 15 | *

16 | * Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text 17 | *

18 | * The last field will always be the Text field, so that it can contain commas. The format 19 | * line allows new fields to be added to the script format in future, and yet allow old 20 | * versions of the software to read the fields it recognises - even if the field order is 21 | * changed. 22 | */ 23 | public class Events extends SubtitleLine { 24 | 25 | /** 26 | * Serial 27 | */ 28 | private static final long serialVersionUID = -6706119890451628726L; 29 | 30 | /** 31 | * Format declaration 32 | */ 33 | public static final String FORMAT_STRING = "Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text"; 34 | 35 | /** 36 | * New line separator 37 | */ 38 | private static final String ESCAPED_RETURN = "\\N"; 39 | 40 | /** 41 | * Dialog 42 | */ 43 | public static final String DIALOGUE = "Dialogue: "; 44 | 45 | /** 46 | * Separator 47 | */ 48 | public static final String SEP = ","; 49 | 50 | /** 51 | * Subtitles having different layer number will be ignored during the collusion 52 | * detection. 53 | *

54 | * Higher numbered layers will be drawn over the lower numbered. 55 | */ 56 | private int layer; 57 | 58 | /** 59 | * Style name. If it is "Default", then your own *Default style will be subtituted. 60 | *

61 | * However, the Default style used by the script author IS stored in the script even 62 | * though SSA ignores it - so if you want to use it, the information is there - you 63 | * could even change the Name in the Style definition line, so that it will appear in 64 | * the list of "script" styles. 65 | */ 66 | private String style; 67 | 68 | /** 69 | * Character name. This is the name of the character who speaks the dialogue. It is 70 | * for information only, to make the script is easier to follow when editing/timing. 71 | */ 72 | private String name = StringUtils.EMPTY; 73 | 74 | /** 75 | * 4-figure Left Margin override. The values are in pixels. All zeroes means the 76 | * default margins defined by the style are used. 77 | */ 78 | private String marginL = "0000"; 79 | 80 | /** 81 | * 4-figure Right Margin override. The values are in pixels. All zeroes means the 82 | * default margins defined by the style are used. 83 | */ 84 | private String marginR = "0000"; 85 | 86 | /** 87 | * 4-figure Bottom Margin override. The values are in pixels. All zeroes means the 88 | * default margins defined by the style are used. 89 | */ 90 | private String marginV = "0000"; 91 | 92 | /** 93 | * Transition Effect. This is either empty, or contains information for one of the 94 | * three transition effects implemented in SSA v4.x 95 | *

96 | * The effect names are case sensitive and must appear exactly as shown. The effect 97 | * names do not have quote marks around them. 98 | *

99 | * "Scroll up;y1;y2;delay[;fadeawayheight]"means that the text/picture will scroll up 100 | * the screen. The parameters after the words "Scroll up" are separated by semicolons. 101 | *

102 | * “Banner;delay” means that text will be forced into a single line, regardless of 103 | * length, and scrolled from right to left accross the screen. 104 | */ 105 | private String effect = StringUtils.EMPTY; 106 | 107 | /** 108 | * Constructor 109 | * 110 | * @param style style name to apply 111 | * @param time Start Time of the Event 112 | * @param textLines End Time of the Event 113 | */ 114 | public Events(String style, ASSTime time, List textLines) { 115 | this.style = style; 116 | this.time = time; 117 | this.textLines = textLines; 118 | } 119 | 120 | /** 121 | * Constructor 122 | */ 123 | public Events() { 124 | super(); 125 | this.style = StringUtils.EMPTY; 126 | this.time = new ASSTime(); 127 | } 128 | 129 | @Override 130 | public String toString() { 131 | StringBuilder sb = new StringBuilder(); 132 | sb.append(DIALOGUE); 133 | 134 | sb.append(this.layer).append(SEP); 135 | sb.append(ASSTime.format(this.time.getStart())).append(SEP); 136 | sb.append(ASSTime.format(this.time.getEnd())).append(SEP); 137 | sb.append(this.style).append(SEP); 138 | sb.append(this.name).append(SEP); 139 | sb.append(this.marginL).append(SEP); 140 | sb.append(this.marginR).append(SEP); 141 | sb.append(this.marginV).append(SEP); 142 | sb.append(this.effect).append(SEP); 143 | for (String tl : textLines) { 144 | sb.append(tl).append(ESCAPED_RETURN); 145 | } 146 | 147 | return StringUtils.removeEnd(sb.toString(), ESCAPED_RETURN); 148 | } 149 | 150 | // ===================== getter and setter start ===================== 151 | 152 | public int getLayer() { 153 | return this.layer; 154 | } 155 | 156 | public void setLayer(int layer) { 157 | this.layer = layer; 158 | } 159 | 160 | public String getStyle() { 161 | return this.style; 162 | } 163 | 164 | public void setStyle(String style) { 165 | this.style = style; 166 | } 167 | 168 | public String getName() { 169 | return this.name; 170 | } 171 | 172 | public void setName(String name) { 173 | this.name = name; 174 | } 175 | 176 | public String getMarginL() { 177 | return this.marginL; 178 | } 179 | 180 | public void setMarginL(String marginL) { 181 | this.marginL = marginL; 182 | } 183 | 184 | public String getMarginR() { 185 | return this.marginR; 186 | } 187 | 188 | public void setMarginR(String marginR) { 189 | this.marginR = marginR; 190 | } 191 | 192 | public String getMarginV() { 193 | return this.marginV; 194 | } 195 | 196 | public void setMarginV(String marginV) { 197 | this.marginV = marginV; 198 | } 199 | 200 | public String getEffect() { 201 | return this.effect; 202 | } 203 | 204 | public void setEffect(String effect) { 205 | this.effect = effect; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/subtitle/ass/ScriptInfo.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.subtitle.ass; 2 | 3 | import java.io.Serializable; 4 | import java.text.DecimalFormat; 5 | 6 | /** 7 | * The ScriptInfo section contains headers and general information about the 8 | * script 9 | */ 10 | public class ScriptInfo implements Serializable { 11 | 12 | /** 13 | * Serial 14 | */ 15 | private static final long serialVersionUID = -6613873382621648995L; 16 | 17 | /** 18 | * Timer declaration 19 | */ 20 | private static final String TIMER = "Timer"; 21 | 22 | /** 23 | * PlayDepth declaration 24 | */ 25 | private static final String PLAY_DEPTH = "PlayDepth"; 26 | 27 | /** 28 | * PlayResX declaration 29 | */ 30 | private static final String PLAY_RES_X = "PlayResX"; 31 | 32 | /** 33 | * PlayResY declaration 34 | */ 35 | private static final String PLAY_RES_Y = "PlayResY"; 36 | 37 | /** 38 | * Collisions declaration 39 | */ 40 | private static final String COLLISIONS = "Collisions"; 41 | 42 | /** 43 | * Script Type declaration 44 | */ 45 | private static final String SCRIPT_TYPE = "ScriptType"; 46 | 47 | /** 48 | * Update Details declaration 49 | */ 50 | private static final String UPDATE_DETAILS = "Update Details"; 51 | 52 | /** 53 | * Script Updated By declaration 54 | */ 55 | private static final String SCRIPT_UPDATED_BY = "Script Updated By"; 56 | 57 | /** 58 | * Synch Point declaration 59 | */ 60 | private static final String SYNCH_POINT = "Synch Point"; 61 | 62 | /** 63 | * Original Timing declaration 64 | */ 65 | private static final String ORIGINAL_TIMING = "Original Timing"; 66 | 67 | /** 68 | * Original Editing declaration 69 | */ 70 | private static final String ORIGINAL_EDITING = "Original Editing"; 71 | 72 | /** 73 | * Original Translation declaration 74 | */ 75 | private static final String ORIGINAL_TRANSLATION = "Original Translation"; 76 | 77 | /** 78 | * Original Script declaration 79 | */ 80 | private static final String ORIGINAL_SCRIPT = "Original Script"; 81 | 82 | /** 83 | * Title declaration 84 | */ 85 | private static final String TITLE = "Title"; 86 | 87 | /** 88 | * Separator 89 | */ 90 | public static final String SEP = ": "; 91 | 92 | /** 93 | * New line separator 94 | */ 95 | private static final String NEW_LINE = "\n"; 96 | 97 | /** 98 | * Decimal time formater 99 | */ 100 | private static final DecimalFormat timeFormatter = new DecimalFormat("#.0000"); 101 | 102 | public enum Collision { 103 | 104 | /** 105 | * position subtitles in the position specified by the "margins" 106 | */ 107 | NORMAL("Normal"), 108 | 109 | /** 110 | * subtitles will be shifted upwards to make room for subsequent overlapping 111 | * subtitles 112 | */ 113 | REVERSE("Reverse"); 114 | 115 | private String type; 116 | 117 | Collision(String type) { 118 | this.type = type; 119 | } 120 | 121 | @Override 122 | public String toString() { 123 | return this.type; 124 | } 125 | 126 | } 127 | 128 | /** 129 | * This is a description of the script. If the original author(s) did not provide this 130 | * information then is automatically substituted. 131 | */ 132 | private String title; 133 | 134 | /** 135 | * The original author(s) of the script. If the original author(s) did not provide 136 | * this information then is automatically substituted. 137 | */ 138 | private String originalScript; 139 | 140 | /** 141 | * (optional) The original translator of the dialogue. This entry does not appear if 142 | * no information was entered by the author. 143 | */ 144 | private String originalTranslation; 145 | 146 | /** 147 | * (optional) The original script editor(s), typically whoever took the raw 148 | * translation and turned it into idiomatic english and reworded for readability. This 149 | * entry does not appear if no information was entered by the author. 150 | */ 151 | private String originalEditing; 152 | 153 | /** 154 | * (optional) Whoever timed the original script. This entry does not appear if no 155 | * information was entered by the author. 156 | */ 157 | private String originalTiming; 158 | 159 | /** 160 | * (optional) Description of where in the video the script should begin playback. 161 | */ 162 | private String synchPoint; 163 | /** 164 | * (optional) The original script editor(s), typically whoever took the raw 165 | * translation and turned it into idiomatic english and reworded for readability. This 166 | * entry does not appear if no information was entered by the author. 167 | */ 168 | private String originalScriptChecking; 169 | 170 | /** 171 | * (optional) Names of any other subtitling groups who edited the original script. 172 | */ 173 | private String scriptUpdatedBy; 174 | 175 | /** 176 | * The details of any updates to the original script made by other subtilting groups. 177 | */ 178 | private String userDetails; 179 | 180 | /** 181 | * This is the SSA script format version eg. "V4.00". It is used by SSA to give a 182 | * warning if you are using a version of SSA older than the version that created the 183 | * script. 184 | */ 185 | private String scriptType = "v4.00+"; 186 | 187 | /** 188 | * This determines how subtitles are moved, when automatically preventing onscreen 189 | * collisions. 190 | * 191 | * If the entry says "Normal" then SSA will attempt to position subtitles in the 192 | * position specified by the "margins". However, subtitles can be shifted vertically 193 | * to prevent onscreen collisions. With "normal" collision prevention, the subtitles 194 | * will "stack up" one above the other - but they will always be positioned as close 195 | * the vertical (bottom) margin as possible - filling in "gaps" in other subtitles if 196 | * one large enough is available. 197 | * 198 | * If the entry says "Reverse" then subtitles will be shifted upwards to make room for 199 | * subsequent overlapping subtitles. This means the subtitles can nearly always be 200 | * read top-down - but it also means that the first subtitle can appear half way up 201 | * the screen before the subsequent overlapping subtitles appear. It can use a lot of 202 | * screen area. 203 | */ 204 | private Collision collisions = Collision.NORMAL; 205 | 206 | /** 207 | * This is the height of the screen used by the script's author(s) when playing the 208 | * script. SSA v4 will automatically select the nearest enabled setting, if you are 209 | * using Directdraw playback. 210 | */ 211 | private int playResY; 212 | 213 | /** 214 | * This is the width of the screen used by the script's author(s) when playing the 215 | * script. SSA will automatically select the nearest enabled, setting if you are using 216 | * Directdraw playback. 217 | */ 218 | private int playResX; 219 | 220 | /** 221 | * This is the colour depth used by the script's author(s) when playing the script. 222 | * SSA will automatically select the nearest enabled setting if you are using 223 | * Directdraw playback. 224 | */ 225 | private int playDepth; 226 | 227 | /** 228 | * This is the Timer Speed for the script, as a percentage. 229 | */ 230 | private double timer = 100.0000; 231 | 232 | @Override 233 | public String toString() { 234 | StringBuilder sb = new StringBuilder(); 235 | appendNotNull(sb, TITLE, this.title); 236 | appendNotNull(sb, ORIGINAL_SCRIPT, this.originalScript); 237 | appendNotNull(sb, ORIGINAL_TRANSLATION, this.originalTranslation); 238 | appendNotNull(sb, ORIGINAL_EDITING, this.originalEditing); 239 | appendNotNull(sb, ORIGINAL_TIMING, this.originalTiming); 240 | appendNotNull(sb, SYNCH_POINT, this.synchPoint); 241 | appendNotNull(sb, SCRIPT_UPDATED_BY, this.scriptUpdatedBy); 242 | appendNotNull(sb, UPDATE_DETAILS, this.userDetails); 243 | appendNotNull(sb, SCRIPT_TYPE, this.scriptType); 244 | appendNotNull(sb, COLLISIONS, this.collisions.toString()); 245 | appendPositive(sb, PLAY_RES_Y, this.playResY); 246 | appendPositive(sb, PLAY_RES_X, this.playResX); 247 | appendPositive(sb, PLAY_DEPTH, this.playDepth); 248 | sb.append(TIMER).append(SEP).append(timeFormatter.format(this.timer)); 249 | return sb.toString(); 250 | } 251 | 252 | // ======================= private methods ======================= 253 | 254 | /** 255 | * Append a value in a StringBuilder if the value is not null 256 | * 257 | * @param sb: the string builder 258 | * @param desc: the description 259 | * @param val: the value 260 | */ 261 | private static void appendNotNull(StringBuilder sb, String desc, String val) { 262 | if (val != null) { 263 | sb.append(desc).append(SEP).append(val).append(NEW_LINE); 264 | } 265 | } 266 | 267 | /** 268 | * Append a value in a StringBuilder if the value is positive 269 | * 270 | * @param sb: the string builder 271 | * @param desc: the description 272 | * @param val: the value 273 | */ 274 | private static void appendPositive(StringBuilder sb, String desc, int val) { 275 | if (val > 0) { 276 | sb.append(desc).append(SEP).append(Integer.toString(val)).append(NEW_LINE); 277 | } 278 | } 279 | 280 | // ===================== getter and setter start ===================== 281 | 282 | public String getTitle() { 283 | return this.title; 284 | } 285 | 286 | public void setTitle(String title) { 287 | this.title = title; 288 | } 289 | 290 | public String getOriginalScript() { 291 | return this.originalScript; 292 | } 293 | 294 | public void setOriginalScript(String originalScript) { 295 | this.originalScript = originalScript; 296 | } 297 | 298 | public String getOriginalTranslation() { 299 | return this.originalTranslation; 300 | } 301 | 302 | public void setOriginalTranslation(String originalTranslation) { 303 | this.originalTranslation = originalTranslation; 304 | } 305 | 306 | public String getOriginalEditing() { 307 | return this.originalEditing; 308 | } 309 | 310 | public void setOriginalEditing(String originalEditing) { 311 | this.originalEditing = originalEditing; 312 | } 313 | 314 | public String getOriginalTiming() { 315 | return this.originalTiming; 316 | } 317 | 318 | public void setOriginalTiming(String originalTiming) { 319 | this.originalTiming = originalTiming; 320 | } 321 | 322 | public String getSynchPoint() { 323 | return this.synchPoint; 324 | } 325 | 326 | public void setSynchPoint(String synchPoint) { 327 | this.synchPoint = synchPoint; 328 | } 329 | 330 | public String getOriginalScriptChecking() { 331 | return this.originalScriptChecking; 332 | } 333 | 334 | public void setOriginalScriptChecking(String originalScriptChecking) { 335 | this.originalScriptChecking = originalScriptChecking; 336 | } 337 | 338 | public String getScriptUpdatedBy() { 339 | return this.scriptUpdatedBy; 340 | } 341 | 342 | public void setScriptUpdatedBy(String scriptUpdatedBy) { 343 | this.scriptUpdatedBy = scriptUpdatedBy; 344 | } 345 | 346 | public String getUserDetails() { 347 | return this.userDetails; 348 | } 349 | 350 | public void setUserDetails(String userDetails) { 351 | this.userDetails = userDetails; 352 | } 353 | 354 | public String getScriptType() { 355 | return this.scriptType; 356 | } 357 | 358 | public void setScriptType(String scriptType) { 359 | this.scriptType = scriptType; 360 | } 361 | 362 | public Collision getCollisions() { 363 | return this.collisions; 364 | } 365 | 366 | public void setCollisions(Collision collisions) { 367 | this.collisions = collisions; 368 | } 369 | 370 | public int getPlayResY() { 371 | return this.playResY; 372 | } 373 | 374 | public void setPlayResY(int playResY) { 375 | this.playResY = playResY; 376 | } 377 | 378 | public int getPlayResX() { 379 | return this.playResX; 380 | } 381 | 382 | public void setPlayResX(int playResX) { 383 | this.playResX = playResX; 384 | } 385 | 386 | public int getPlayDepth() { 387 | return this.playDepth; 388 | } 389 | 390 | public void setPlayDepth(int playDepth) { 391 | this.playDepth = playDepth; 392 | } 393 | 394 | public double getTimer() { 395 | return this.timer; 396 | } 397 | 398 | public void setTimer(double timer) { 399 | this.timer = timer; 400 | } 401 | 402 | } 403 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/subtitle/ass/V4Style.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.subtitle.ass; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Styles define the appearance and position of subtitles. All styles used by the script 7 | * are are defined by a Style line in the script. 8 | * 9 | * Any of the the settings in the Style, (except shadow/outline type and depth) can 10 | * overridden by control codes in the subtitle text. 11 | * 12 | * The fields which appear in each Style definition line are named in a special line with 13 | * the line type “Format:”. The Format line must appear before any Styles - because it 14 | * defines how SSA will interpret the Style definition lines. The field names listed in 15 | * the format line must be correctly spelled! 16 | * 17 | * The fields are as follows: 18 | * 19 | * Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, 20 | * Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, 21 | * Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding 22 | * 23 | * The format line allows new fields to be added to the script format in future, and yet 24 | * allow old versions of the software to read the fields it recognises - even if the field 25 | * order is changed. 26 | */ 27 | public class V4Style implements Serializable { 28 | 29 | /** 30 | * Serial 31 | */ 32 | private static final long serialVersionUID = -4910432063071707768L; 33 | 34 | /** 35 | * Style declaration 36 | */ 37 | public static final String STYLE = "Style: "; 38 | 39 | /** 40 | * Format declaration 41 | */ 42 | public static final String FORMAT_STRING = "Name,Fontname,Fontsize,PrimaryColour," 43 | + "SecondaryColour,OutlineColour,BackColour,Bold,Italic,Underline," 44 | + "StrikeOut,ScaleX,ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow," 45 | + "Alignment,MarginL,MarginR,MarginV,Encoding"; 46 | 47 | /** 48 | * Separator 49 | */ 50 | public static final String SEP = ","; 51 | 52 | /** 53 | * The name of the Style. Case sensitive. Cannot include commas. 54 | */ 55 | private String name; 56 | 57 | /** 58 | * The fontname as used by Windows. Case-sensitive. 59 | */ 60 | private String fontname = "Arial"; 61 | 62 | /** 63 | * The font size 64 | */ 65 | private int fontsize; 66 | 67 | /** 68 | * A long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal 69 | * equivelent of this number is BBGGRR 70 | * 71 | * The color format contains the alpha channel, too. (AABBGGRR) 72 | */ 73 | private int primaryColour; 74 | 75 | /** 76 | * long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal 77 | * equivelent of this number is BBGGRR 78 | * 79 | * This colour may be used instead of the Primary colour when a subtitle is 80 | * automatically shifted to prevent an onscreen collsion, to distinguish the different 81 | * subtitles. 82 | * 83 | * The color format contains the alpha channel, too. (AABBGGRR) 84 | */ 85 | private int secondaryColour = 16777215; // #FFFFFF (white) 86 | 87 | /** 88 | * A long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal 89 | * equivelent of this number is BBGGRR 90 | * 91 | * This colour may be used instead of the Primary or Secondary colour when a subtitle 92 | * is automatically shifted to prevent an onscreen collsion, to distinguish the 93 | * different subtitles. 94 | * 95 | * The color format contains the alpha channel, too. (AABBGGRR) 96 | */ 97 | private int outlineColour; 98 | 99 | /** 100 | * This is the colour of the subtitle outline or shadow, if these are used. A long 101 | * integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal 102 | * equivelent of this number is BBGGRR. 103 | * 104 | * The color format contains the alpha channel, too. (AABBGGRR) 105 | */ 106 | private int backColour; 107 | 108 | /** 109 | * This defines whether text is bold (true) or not (false). -1 is True, 0 is False. 110 | * This is independant of the Italic attribute - you can have have text which is both 111 | * bold and italic. 112 | */ 113 | private boolean bold; 114 | 115 | /** 116 | * This defines whether text is italic (true) or not (false). -1 is True, 0 is False. 117 | * This is independant of the bold attribute - you can have have text which is both 118 | * bold and italic. 119 | */ 120 | private boolean italic; 121 | 122 | /** 123 | * -1 is True, 0 is False 124 | */ 125 | private boolean underline; 126 | 127 | /** 128 | * -1 is True, 0 is False 129 | */ 130 | private boolean strikeOut; 131 | 132 | /** 133 | * Modifies the width of the font. [percent] 134 | */ 135 | private int scaleX = 100; 136 | 137 | /** 138 | * Modifies the height of the font. [percent] 139 | */ 140 | private int scaleY = 100; 141 | 142 | /** 143 | * Extra space between characters. [pixels] 144 | */ 145 | private int spacing; 146 | 147 | /** 148 | * The origin of the rotation is defined by the alignment. Can be a floating point 149 | * number. [degrees] 150 | */ 151 | private double angle; 152 | 153 | /** 154 | * 1=Outline + drop shadow, 3=Opaque box 155 | */ 156 | private int borderStyle = 1; 157 | 158 | /** 159 | * If BorderStyle is 1, then this specifies the width of the outline around the text, 160 | * in pixels. Values may be 0, 1, 2, 3 or 4. 161 | */ 162 | private int outline = 2; 163 | 164 | /** 165 | * If BorderStyle is 1, then this specifies the depth of the drop shadow behind the 166 | * text, in pixels. Values may be 0, 1, 2, 3 or 4. Drop shadow is always used in 167 | * addition to an outline - SSA will force an outline of 1 pixel if no outline width 168 | * is given. 169 | */ 170 | private int shadow; 171 | 172 | /** 173 | * This sets how text is "justified" within the Left/Right onscreen margins, and also 174 | * the vertical placing. Values may be 1=Left, 2=Centered, 3=Right. Add 4 to the value 175 | * for a "Toptitle". Add 8 to the value for a "Midtitle". eg. 5 = left-justified 176 | * toptitle 177 | */ 178 | private int alignment = 2; 179 | 180 | /** 181 | * This defines the Left Margin in pixels. It is the distance from the left-hand edge 182 | * of the screen.The three onscreen margins (MarginL, MarginR, MarginV) define areas 183 | * in which the subtitle text will be displayed. 184 | */ 185 | private int marginL = 10; 186 | 187 | /** 188 | * This defines the Right Margin in pixels. It is the distance from the right-hand 189 | * edge of the screen. The three onscreen margins (MarginL, MarginR, MarginV) define 190 | * areas in which the subtitle text will be displayed. 191 | */ 192 | private int marginR = 10; 193 | 194 | /** 195 | * This defines the vertical Left Margin in pixels. For a subtitle, it is the distance 196 | * from the bottom of the screen. For a toptitle, it is the distance from the top of 197 | * the screen. For a midtitle, the value is ignored - the text will be vertically 198 | * centred 199 | */ 200 | private int marginV = 10; 201 | 202 | /** 203 | * This specifies the font character set or encoding and on multi-lingual Windows 204 | * installations it provides access to characters used in multiple than one languages. 205 | * It is usually 0 (zero) for English (Western, ANSI) Windows. 206 | * 207 | * When the file is Unicode, this field is useful during file format conversions. 208 | */ 209 | private int encoding; 210 | 211 | /** 212 | * Default constructor 213 | */ 214 | public V4Style() { 215 | } 216 | 217 | /** 218 | * Constructor 219 | * 220 | * @param name: the style name 221 | */ 222 | public V4Style(String name) { 223 | this.name = name; 224 | } 225 | 226 | @Override 227 | public String toString() { 228 | StringBuilder sb = new StringBuilder(); 229 | sb.append(STYLE); 230 | sb.append(this.name).append(SEP); 231 | sb.append(this.fontname).append(SEP); 232 | sb.append(this.fontsize).append(SEP); 233 | sb.append(this.primaryColour).append(SEP); 234 | sb.append(this.secondaryColour).append(SEP); 235 | sb.append(this.outlineColour).append(SEP); 236 | sb.append(this.backColour).append(SEP); 237 | sb.append(this.bold ? -1 : 0).append(SEP); 238 | sb.append(this.italic ? -1 : 0).append(SEP); 239 | sb.append(this.underline ? -1 : 0).append(SEP); 240 | sb.append(this.strikeOut ? -1 : 0).append(SEP); 241 | sb.append(this.scaleX).append(SEP); 242 | sb.append(this.scaleY).append(SEP); 243 | sb.append(this.spacing).append(SEP); 244 | sb.append(this.angle).append(SEP); 245 | sb.append(this.borderStyle).append(SEP); 246 | sb.append(this.outline).append(SEP); 247 | sb.append(this.shadow).append(SEP); 248 | sb.append(this.alignment).append(SEP); 249 | sb.append(this.marginL).append(SEP); 250 | sb.append(this.marginR).append(SEP); 251 | sb.append(this.marginV).append(SEP); 252 | sb.append(this.encoding); 253 | return sb.toString(); 254 | } 255 | 256 | // ===================== getter and setter start ===================== 257 | 258 | public String getName() { 259 | return this.name; 260 | } 261 | 262 | public void setName(String name) { 263 | this.name = name; 264 | } 265 | 266 | public String getFontname() { 267 | return this.fontname; 268 | } 269 | 270 | public void setFontname(String fontname) { 271 | this.fontname = fontname; 272 | } 273 | 274 | public int getFontsize() { 275 | return this.fontsize; 276 | } 277 | 278 | public void setFontsize(int fontsize) { 279 | this.fontsize = fontsize; 280 | } 281 | 282 | public int getPrimaryColour() { 283 | return this.primaryColour; 284 | } 285 | 286 | public void setPrimaryColour(int primaryColour) { 287 | this.primaryColour = primaryColour; 288 | } 289 | 290 | public int getSecondaryColour() { 291 | return this.secondaryColour; 292 | } 293 | 294 | public void setSecondaryColour(int secondaryColour) { 295 | this.secondaryColour = secondaryColour; 296 | } 297 | 298 | public int getOutlineColour() { 299 | return this.outlineColour; 300 | } 301 | 302 | public void setOutlineColor(int outlineColor) { 303 | this.outlineColour = outlineColor; 304 | } 305 | 306 | public int getBackColour() { 307 | return this.backColour; 308 | } 309 | 310 | public void setBackColour(int backColour) { 311 | this.backColour = backColour; 312 | } 313 | 314 | public boolean isBold() { 315 | return this.bold; 316 | } 317 | 318 | public void setBold(boolean bold) { 319 | this.bold = bold; 320 | } 321 | 322 | public boolean isItalic() { 323 | return this.italic; 324 | } 325 | 326 | public void setItalic(boolean italic) { 327 | this.italic = italic; 328 | } 329 | 330 | public boolean isUnderline() { 331 | return this.underline; 332 | } 333 | 334 | public void setUnderline(boolean underline) { 335 | this.underline = underline; 336 | } 337 | 338 | public boolean isStrikeOut() { 339 | return this.strikeOut; 340 | } 341 | 342 | public void setStrikeOut(boolean strikeOut) { 343 | this.strikeOut = strikeOut; 344 | } 345 | 346 | public void setOutlineColour(int outlineColour) { 347 | this.outlineColour = outlineColour; 348 | } 349 | 350 | public int getScaleX() { 351 | return this.scaleX; 352 | } 353 | 354 | public void setScaleX(int scaleX) { 355 | this.scaleX = scaleX; 356 | } 357 | 358 | public int getScaleY() { 359 | return this.scaleY; 360 | } 361 | 362 | public void setScaleY(int scaleY) { 363 | this.scaleY = scaleY; 364 | } 365 | 366 | public int getSpacing() { 367 | return this.spacing; 368 | } 369 | 370 | public void setSpacing(int spacing) { 371 | this.spacing = spacing; 372 | } 373 | 374 | public double getAngle() { 375 | return this.angle; 376 | } 377 | 378 | public void setAngle(double angle) { 379 | this.angle = angle; 380 | } 381 | 382 | public int getOutline() { 383 | return this.outline; 384 | } 385 | 386 | public void setOutline(int outline) { 387 | this.outline = outline; 388 | } 389 | 390 | public int getShadow() { 391 | return this.shadow; 392 | } 393 | 394 | public void setShadow(int shadow) { 395 | this.shadow = shadow; 396 | } 397 | 398 | public int getAlignment() { 399 | return this.alignment; 400 | } 401 | 402 | public void setAlignment(int alignment) { 403 | this.alignment = alignment; 404 | } 405 | 406 | public int getMarginL() { 407 | return this.marginL; 408 | } 409 | 410 | public void setMarginL(int marginL) { 411 | this.marginL = marginL; 412 | } 413 | 414 | public int getMarginR() { 415 | return this.marginR; 416 | } 417 | 418 | public void setMarginR(int marginR) { 419 | this.marginR = marginR; 420 | } 421 | 422 | public int getMarginV() { 423 | return this.marginV; 424 | } 425 | 426 | public void setMarginV(int marginV) { 427 | this.marginV = marginV; 428 | } 429 | 430 | public int getEncoding() { 431 | return this.encoding; 432 | } 433 | 434 | public void setEncoding(int encoding) { 435 | this.encoding = encoding; 436 | } 437 | 438 | public int getBorderStyle() { 439 | return this.borderStyle; 440 | } 441 | 442 | public void setBorderStyle(int borderStyle) { 443 | this.borderStyle = borderStyle; 444 | } 445 | 446 | } 447 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/subtitle/common/SubtitleLine.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.subtitle.common; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Comparator; 5 | import java.util.List; 6 | 7 | public class SubtitleLine implements TimedLine { 8 | 9 | /** 10 | * Serial Id 11 | */ 12 | private static final long serialVersionUID = 288560648398584309L; 13 | 14 | /** 15 | * Subtitle Text. This is the actual text which will be displayed as a subtitle 16 | * onscreen. 17 | */ 18 | protected List textLines = new ArrayList<>(); 19 | 20 | /** 21 | * Timecodes 22 | */ 23 | protected T time; 24 | 25 | /** 26 | * Comparator that only compare timings 27 | * 28 | * @return the comparator 29 | */ 30 | public static Comparator timeComparator = new Comparator() { 31 | 32 | @Override 33 | public int compare(TimedLine o1, TimedLine o2) { 34 | return o1.getTime().compareTo(o2.getTime()); 35 | } 36 | }; 37 | 38 | /** 39 | * Constructor 40 | */ 41 | public SubtitleLine() { 42 | super(); 43 | } 44 | 45 | /** 46 | * Constructor 47 | */ 48 | public SubtitleLine(T time) { 49 | 50 | super(); 51 | this.time = time; 52 | } 53 | 54 | @Override 55 | public boolean equals(Object obj) { 56 | 57 | if (this == obj) { 58 | return true; 59 | } 60 | if (obj == null || getClass() != obj.getClass()) { 61 | return false; 62 | } 63 | 64 | TimedLine other = (TimedLine) obj; 65 | return compareTo(other) == 0; 66 | } 67 | 68 | @Override 69 | public int compare(TimedLine o1, TimedLine o2) { 70 | 71 | return o1.compareTo(o2); 72 | } 73 | 74 | @Override 75 | public int compareTo(TimedLine o) { 76 | 77 | int compare = this.time.compareTo(o.getTime()); 78 | if (compare == 0) { 79 | StringBuilder stringBuilder = new StringBuilder(); 80 | for (String textLine : textLines) { 81 | stringBuilder.append(textLine) 82 | .append(","); 83 | } 84 | 85 | StringBuilder stringBuilderO = new StringBuilder(); 86 | List textLines = o.getTextLines(); 87 | for (String textLine : textLines) { 88 | stringBuilderO.append(textLine) 89 | .append(","); 90 | } 91 | String thisText = stringBuilder.toString(); 92 | String otherText = stringBuilderO.toString(); 93 | compare = thisText.compareTo(otherText); 94 | } 95 | 96 | return compare; 97 | } 98 | 99 | // ===================== getter and setter start ===================== 100 | 101 | @Override 102 | public T getTime() { 103 | return this.time; 104 | } 105 | 106 | public void setTime(T time) { 107 | this.time = time; 108 | } 109 | 110 | @Override 111 | public List getTextLines() { 112 | return this.textLines; 113 | } 114 | 115 | public void setTextLines(List textLines) { 116 | this.textLines = textLines; 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/subtitle/common/SubtitleTime.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.subtitle.common; 2 | 3 | //import java.time.LocalTime; 4 | 5 | public class SubtitleTime implements TimedObject { 6 | 7 | private static final long serialVersionUID = -2283115927128309201L; 8 | 9 | /** 10 | * Start Time of the Event, in 0:00:00:00 format ie. Hrs:Mins:Secs:hundredths. This is 11 | * the time elapsed during script playback at which the text will appear onscreen. 12 | */ 13 | protected long start; 14 | 15 | /** 16 | * End Time of the Event, in 0:00:00:00 format ie. Hrs:Mins:Secs:hundredths. This is 17 | * the time elapsed during script playback at which the text will disappear offscreen. 18 | */ 19 | protected long end; 20 | 21 | public SubtitleTime() { 22 | } 23 | 24 | public SubtitleTime(long start, long end) { 25 | 26 | super(); 27 | this.start = start; 28 | this.end = end; 29 | } 30 | 31 | @Override 32 | public int compare(TimedObject o1, TimedObject o2) { 33 | 34 | return o1.compareTo(o2); 35 | } 36 | 37 | @Override 38 | public boolean equals(Object obj) { 39 | 40 | if (this == obj) { 41 | return true; 42 | } 43 | if (obj == null || getClass() != obj.getClass()) { 44 | return false; 45 | } 46 | 47 | TimedObject other = (TimedObject) obj; 48 | return compareTo(other) == 0; 49 | } 50 | 51 | @Override 52 | public int compareTo(TimedObject other) { 53 | long start = this.start - other.getStart(); 54 | if (start == 0) 55 | start = this.end - other.getEnd(); 56 | if (start > 0) { 57 | return 1; 58 | } else if (start < 0) { 59 | return -1; 60 | } else { 61 | return 0; 62 | } 63 | } 64 | 65 | // ===================== getter and setter start ===================== 66 | 67 | @Override 68 | public long getStart() { 69 | return this.start; 70 | } 71 | 72 | @Override 73 | public void setStart(long start) { 74 | this.start = start; 75 | } 76 | 77 | @Override 78 | public long getEnd() { 79 | return this.end; 80 | } 81 | 82 | @Override 83 | public void setEnd(long end) { 84 | this.end = end; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/subtitle/common/TimedLine.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.subtitle.common; 2 | 3 | import java.io.Serializable; 4 | import java.util.Comparator; 5 | import java.util.List; 6 | 7 | /** 8 | * Simple object that contains a text line with a time 9 | * 10 | */ 11 | public interface TimedLine extends Serializable, Comparable, Comparator { 12 | 13 | /** 14 | * Get the text lines 15 | * 16 | * @return textLines 17 | */ 18 | List getTextLines(); 19 | 20 | /** 21 | * Get the timed object 22 | * 23 | * @return the time 24 | */ 25 | TimedObject getTime(); 26 | 27 | } -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/subtitle/common/TimedObject.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.subtitle.common; 2 | 3 | import java.io.Serializable; 4 | import java.util.Comparator; 5 | 6 | /** 7 | * 8 | * Simple object that contains timed start ant end 9 | */ 10 | public interface TimedObject extends Serializable, Comparable, Comparator { 11 | 12 | /** 13 | * Return the time elapsed during script playback at which the text will appear 14 | * onscreen. 15 | * 16 | * @return start time 17 | */ 18 | long getStart(); 19 | 20 | /** 21 | * Return the time elapsed during script playback at which the text will disappear 22 | * offscreen. 23 | * 24 | * @return end time 25 | */ 26 | long getEnd(); 27 | 28 | /** 29 | * Set the time elapsed during script playback at which the text will appear onscreen. 30 | * 31 | * @param start time 32 | */ 33 | void setStart(long start); 34 | 35 | /** 36 | * Set the time elapsed during script playback at which the text will disappear 37 | * offscreen. 38 | * 39 | * @param end time 40 | */ 41 | void setEnd(long end); 42 | } -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/subtitle/common/TimedTextFile.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.subtitle.common; 2 | 3 | import java.io.Serializable; 4 | import java.util.Set; 5 | 6 | /** 7 | * Object that represents a text file containing timed lines 8 | */ 9 | public interface TimedTextFile extends Serializable { 10 | 11 | /** 12 | * Get the filename 13 | * 14 | * @return the filename 15 | */ 16 | String getFileName(); 17 | 18 | /** 19 | * Set the filename 20 | * 21 | * @param fileName: the filename 22 | */ 23 | void setFileName(String fileName); 24 | 25 | /** 26 | * Get the timed lines 27 | * 28 | * @return lines 29 | */ 30 | Set getTimedLines(); 31 | 32 | } -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/subtitle/config/Font.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.subtitle.config; 2 | 3 | import java.io.Serializable; 4 | 5 | import com.github.dnbn.submerge.api.constant.FontName; 6 | 7 | public class Font implements Serializable { 8 | 9 | /** 10 | * Serial 11 | */ 12 | private static final long serialVersionUID = -3711480706383195193L; 13 | 14 | /** 15 | * Font name 16 | */ 17 | private String name = FontName.Arial.toString(); 18 | 19 | /** 20 | * Font size 21 | */ 22 | private int size = 16; 23 | 24 | /** 25 | * Font color 26 | */ 27 | private String color = "#fffff9"; 28 | 29 | /** 30 | * Outline color 31 | */ 32 | private String outlineColor = "#000000"; 33 | 34 | /** 35 | * Outline width 36 | */ 37 | private int outlineWidth = 2; 38 | 39 | // ===================== getter and setter start ===================== 40 | 41 | public String getName() { 42 | return this.name; 43 | } 44 | 45 | public void setName(String name) { 46 | this.name = name; 47 | } 48 | 49 | public int getSize() { 50 | return this.size; 51 | } 52 | 53 | public void setSize(int size) { 54 | this.size = size; 55 | } 56 | 57 | public String getColor() { 58 | return this.color; 59 | } 60 | 61 | public void setColor(String color) { 62 | this.color = color; 63 | } 64 | 65 | public String getOutlineColor() { 66 | return this.outlineColor; 67 | } 68 | 69 | public void setOutlineColor(String outlineColor) { 70 | this.outlineColor = outlineColor; 71 | } 72 | 73 | public int getOutlineWidth() { 74 | return this.outlineWidth; 75 | } 76 | 77 | public void setOutlineWidth(int outlineWidth) { 78 | this.outlineWidth = outlineWidth; 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/subtitle/config/SimpleSubConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.subtitle.config; 2 | 3 | import java.io.Serializable; 4 | 5 | import com.github.dnbn.submerge.api.subtitle.common.TimedTextFile; 6 | 7 | public class SimpleSubConfig implements Serializable { 8 | 9 | private static final long serialVersionUID = -485125721913729063L; 10 | 11 | private String styleName; 12 | private TimedTextFile sub; 13 | private Font fontconfig = new Font(); 14 | private int alignment; 15 | private int verticalMargin = 10; 16 | 17 | public SimpleSubConfig() { 18 | } 19 | 20 | public SimpleSubConfig(TimedTextFile sub, Font fontConfig) { 21 | this.sub = sub; 22 | this.fontconfig = fontConfig; 23 | } 24 | 25 | // ===================== getter and setter start ===================== 26 | 27 | public TimedTextFile getSub() { 28 | return this.sub; 29 | } 30 | 31 | public void setSub(TimedTextFile sub) { 32 | this.sub = sub; 33 | } 34 | 35 | public Font getFontconfig() { 36 | return this.fontconfig; 37 | } 38 | 39 | public void setFontconfig(Font fontconfig) { 40 | this.fontconfig = fontconfig; 41 | } 42 | 43 | public int getAlignment() { 44 | return this.alignment; 45 | } 46 | 47 | public void setAlignment(int alignment) { 48 | this.alignment = alignment; 49 | } 50 | 51 | public String getStyleName() { 52 | return this.styleName; 53 | } 54 | 55 | public void setStyleName(String styleName) { 56 | this.styleName = styleName; 57 | } 58 | 59 | public int getVerticalMargin() { 60 | return this.verticalMargin; 61 | } 62 | 63 | public void setVerticalMargin(int verticalMargin) { 64 | this.verticalMargin = verticalMargin; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/subtitle/srt/SRTLine.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.subtitle.srt; 2 | 3 | import com.github.dnbn.submerge.api.subtitle.common.SubtitleLine; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Class represents an abstract line of SRT, meaning text, timecodes and index 9 | */ 10 | public class SRTLine extends SubtitleLine { 11 | 12 | private static final long serialVersionUID = -1220593401999895814L; 13 | 14 | private static final String NEW_LINE = "\n"; 15 | 16 | private int id; 17 | 18 | public SRTLine(int id, SRTTime time, List textLines) { 19 | 20 | this.id = id; 21 | this.time = time; 22 | this.textLines = textLines; 23 | } 24 | 25 | @Override 26 | public String toString() { 27 | 28 | StringBuilder sb = new StringBuilder(); 29 | sb.append(this.id).append(NEW_LINE); 30 | sb.append(this.time).append(NEW_LINE); 31 | for (String textLine : textLines) { 32 | sb.append(textLine).append(NEW_LINE); 33 | } 34 | return sb.append(NEW_LINE).toString(); 35 | } 36 | 37 | // ===================== getter and setter start ===================== 38 | 39 | public int getId() { 40 | return this.id; 41 | } 42 | 43 | public void setId(int id) { 44 | this.id = id; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/subtitle/srt/SRTSub.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.subtitle.srt; 2 | 3 | import java.util.Set; 4 | import java.util.TreeSet; 5 | 6 | import com.github.dnbn.submerge.api.subtitle.common.TimedLine; 7 | import com.github.dnbn.submerge.api.subtitle.common.TimedTextFile; 8 | 9 | /** 10 | * Class represents an SRT file, meandin a complete set of subtitle lines 11 | * 12 | */ 13 | public class SRTSub implements TimedTextFile { 14 | 15 | private static final long serialVersionUID = -2909833999376537734L; 16 | 17 | private String fileName; 18 | private Set lines = new TreeSet<>(); 19 | 20 | // ======================== Public methods ========================== 21 | 22 | public void add(SRTLine line) { 23 | 24 | this.lines.add(line); 25 | } 26 | 27 | public void remove(TimedLine line) { 28 | 29 | this.lines.remove(line); 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | 35 | StringBuilder sb = new StringBuilder(); 36 | for (SRTLine srtLine : lines) { 37 | sb.append(srtLine); 38 | } 39 | return sb.toString(); 40 | } 41 | 42 | // ===================== getter and setter start ===================== 43 | 44 | public Set getLines() { 45 | return this.lines; 46 | } 47 | 48 | @Override 49 | public Set getTimedLines() { 50 | return this.lines; 51 | } 52 | 53 | public void setLines(Set lines) { 54 | this.lines = lines; 55 | } 56 | 57 | @Override 58 | public String getFileName() { 59 | return this.fileName; 60 | } 61 | 62 | @Override 63 | public void setFileName(String fileName) { 64 | this.fileName = fileName; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/subtitle/srt/SRTTime.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.subtitle.srt; 2 | 3 | import com.github.dnbn.submerge.api.subtitle.common.SubtitleTime; 4 | 5 | //import java.time.LocalTime; 6 | import java.util.Arrays; 7 | 8 | public class SRTTime extends SubtitleTime { 9 | 10 | private static final long serialVersionUID = -5787808223967579723L; 11 | 12 | // public static DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(SRTTime.PATTERN); 13 | public static final String PATTERN = "HH:mm:ss,SSS"; 14 | private static final String TS_PATTERN = "%02d:%02d:%02d,%03d"; 15 | public static final String DELIMITER = " --> "; 16 | 17 | public SRTTime() { 18 | super(); 19 | } 20 | 21 | public SRTTime(long start, long end) { 22 | 23 | super(start, end); 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | 29 | StringBuilder sb = new StringBuilder(); 30 | sb.append(format(this.start)); 31 | sb.append(DELIMITER); 32 | sb.append(format(this.end)); 33 | return sb.toString(); 34 | } 35 | 36 | /** 37 | * Convert a LocalTime to string 38 | * 39 | * @param time: the time to format 40 | * @return the formatted time 41 | */ 42 | public static String format(long time) { 43 | // 44 | // int hr = time.get(ChronoField.HOUR_OF_DAY); 45 | // int min = time.get(ChronoField.MINUTE_OF_HOUR); 46 | // int sec = time.get(ChronoField.SECOND_OF_MINUTE); 47 | // int ms = time.get(ChronoField.MILLI_OF_SECOND); 48 | long ms = time % 1000; 49 | long s = time / 1000; 50 | long hh = s / 60 / 60; 51 | long min = (s / 60) % 60; 52 | long sec = s % 60; 53 | 54 | return String.format(TS_PATTERN, hh, min, sec, ms); 55 | } 56 | 57 | 58 | /** 59 | * @param in string like 10:23:56,909 60 | * @return long milliseconds 61 | */ 62 | public static long fromString(String in) { 63 | String[] split1 = in.split(":"); 64 | long hours = Long.parseLong(split1[0].trim()); 65 | long minutes = Long.parseLong(split1[1].trim()); 66 | long seconds = 0; 67 | long millies = 0; 68 | if (split1.length > 2) { 69 | String[] split = split1[2].split(",");//SRT format 70 | seconds = Long.parseLong(split[0].trim()); 71 | if (split.length > 1) { 72 | millies = Long.parseLong(split[1].trim()); 73 | } else { 74 | System.out.println("in = [" + in + "]"); 75 | } 76 | } else { 77 | System.out.println("in = [" + in + "] split1:" + Arrays.toString(split1)); 78 | } 79 | return hours * 60 * 60 * 1000 + minutes * 60 * 1000 + seconds * 1000 + millies; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/utils/ColorUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.utils; 2 | 3 | 4 | import com.github.dnbn.submerge.api.parser.exception.InvalidColorCode; 5 | 6 | 7 | public final class ColorUtils { 8 | 9 | /** 10 | * Convert the hexadecimal color code to BGR code 11 | * 12 | * @param hex 13 | * @return 14 | */ 15 | public static int hexToRGB(String hex) { 16 | // TODO: 2019/1/4 revert alpha 17 | System.out.println("hex = [" + hex + "]"); 18 | // int in = Color.parseColor(hex); 19 | // int red = (in) & 0xFF; 20 | // int green = (in >> 8) & 0xFF; 21 | // int blue = (in >> 16) & 0xFF; 22 | // return (red << 16) | (blue) | (green << 8); 23 | return 0; 24 | } 25 | 26 | /** 27 | * Convert a &HAABBGGRR to hexadecimal 28 | * 29 | * @param haabbggrr: the color code 30 | * @return the hexadecimal code 31 | * @throws InvalidColorCode 32 | */ 33 | public static String HAABBGGRRToHex(String haabbggrr) { 34 | if (haabbggrr.length() != 10) { 35 | throw new InvalidColorCode("Invalid pattern, must be &HAABBGGRR"); 36 | } 37 | StringBuilder sb = new StringBuilder(); 38 | sb.append("#"); 39 | sb.append(haabbggrr.substring(8)); 40 | sb.append(haabbggrr, 6, 8); 41 | sb.append(haabbggrr, 4, 6); 42 | sb.append(haabbggrr, 2, 4); 43 | return sb.toString().toLowerCase(); 44 | } 45 | 46 | /** 47 | * Convert a &HBBGGRR to hexadecimal 48 | * 49 | * @param hbbggrr: the color code 50 | * @return the hexadecimal code 51 | * @throws InvalidColorCode 52 | */ 53 | public static String HBBGGRRToHex(String hbbggrr) { 54 | if (hbbggrr.length() != 8) { 55 | throw new InvalidColorCode("Invalid pattern, must be &HBBGGRR"); 56 | } 57 | StringBuilder sb = new StringBuilder(); 58 | sb.append("#"); 59 | sb.append(hbbggrr.substring(6)); 60 | sb.append(hbbggrr, 4, 6); 61 | sb.append(hbbggrr, 2, 4); 62 | return sb.toString().toLowerCase(); 63 | } 64 | 65 | /** 66 | * Convert a &HAABBGGRR to BGR 67 | * 68 | * @param haabbggrr: the color code 69 | * @return the BGR code 70 | * @throws InvalidColorCode 71 | */ 72 | public static int HAABBGGRRToRGB(String haabbggrr) { 73 | return hexToRGB(HAABBGGRRToHex(haabbggrr)); 74 | } 75 | 76 | /** 77 | * Convert a &HBBGGRR to BGR 78 | * 79 | * @param hbbggrr: the color code 80 | * @return the BGR code 81 | * @throws InvalidColorCode 82 | */ 83 | public static int HBBGGRRToRGB(String hbbggrr) { 84 | return hexToRGB(HBBGGRRToHex(hbbggrr)); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/utils/ConvertionUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.utils; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.apache.commons.lang.StringUtils; 7 | 8 | import com.github.dnbn.submerge.api.subtitle.ass.ASSTime; 9 | import com.github.dnbn.submerge.api.subtitle.ass.Events; 10 | import com.github.dnbn.submerge.api.subtitle.ass.V4Style; 11 | import com.github.dnbn.submerge.api.subtitle.common.TimedLine; 12 | import com.github.dnbn.submerge.api.subtitle.common.TimedObject; 13 | import com.github.dnbn.submerge.api.subtitle.config.Font; 14 | import com.github.dnbn.submerge.api.subtitle.config.SimpleSubConfig; 15 | 16 | public class ConvertionUtils { 17 | 18 | private static final String RGX_XML_TAG = "<[^>]+>"; 19 | private static final String RGX_ASS_FORMATTING = "\\{[^\\}]*\\}"; 20 | private static final String SRT_ITALIC_CLOSE = "\\"; 21 | private static final String SRT_ITALIC_OPEN = "\\"; 22 | private static final String ASS_ITALIC_CLOSE = "\\{\\\\i0\\}"; 23 | private static final String ASS_ITALIC_OPEN = "\\{\\\\i1\\}"; 24 | 25 | /** 26 | * Create an Events object from a timed line 27 | * 28 | * @param line: a timed line 29 | * @param style: the style name 30 | * @return the corresponding Events 31 | */ 32 | public static Events createEvent(TimedLine line, String style) { 33 | 34 | List newLine = new ArrayList<>(); 35 | 36 | for (String text : line.getTextLines()) { 37 | newLine.add(toASSString(text)); 38 | } 39 | TimedObject timeLine = line.getTime(); 40 | ASSTime time = new ASSTime(timeLine.getStart(), timeLine.getEnd()); 41 | 42 | return new Events(style, time, newLine); 43 | } 44 | 45 | /** 46 | * Create a V4Style object from SubInput 47 | * 48 | * @param config: the configuration object 49 | * @return the corresponding style 50 | */ 51 | public static V4Style createV4Style(SimpleSubConfig config) { 52 | 53 | V4Style style = new V4Style(config.getStyleName()); 54 | Font font = config.getFontconfig(); 55 | style.setFontname(font.getName()); 56 | style.setFontsize(font.getSize()); 57 | style.setAlignment(config.getAlignment()); 58 | style.setPrimaryColour(ColorUtils.hexToRGB(font.getColor())); 59 | style.setOutlineColor(ColorUtils.hexToRGB(font.getOutlineColor())); 60 | style.setOutline(font.getOutlineWidth()); 61 | style.setMarginV(config.getVerticalMargin()); 62 | return style; 63 | } 64 | 65 | /** 66 | * Format a text line to be srt compliant 67 | * 68 | * @param textLine the text line 69 | * @return the formatted text line 70 | */ 71 | public static String toSRTString(String textLine) { 72 | 73 | String formatted = textLine.replaceAll(ASS_ITALIC_OPEN, SRT_ITALIC_OPEN); 74 | formatted = formatted.replaceAll(ASS_ITALIC_CLOSE, SRT_ITALIC_CLOSE); 75 | formatted = formatted.replaceAll(RGX_ASS_FORMATTING, StringUtils.EMPTY); 76 | 77 | return formatted; 78 | } 79 | 80 | /** 81 | * Format a text line to be ass compliant 82 | * 83 | * @param textLine the text line 84 | * @return 85 | */ 86 | public static String toASSString(String textLine) { 87 | 88 | String formatted = textLine.replaceAll(SRT_ITALIC_OPEN, ASS_ITALIC_OPEN); 89 | formatted = formatted.replaceAll(SRT_ITALIC_CLOSE, ASS_ITALIC_CLOSE); 90 | 91 | return formatted.replaceAll(RGX_XML_TAG, StringUtils.EMPTY); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /submerge-api/src/main/java/com/github/dnbn/submerge/api/utils/FileUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api.utils; 2 | 3 | import org.apache.commons.io.IOUtils; 4 | import org.mozilla.universalchardet.UniversalDetector; 5 | 6 | import java.io.File; 7 | import java.io.FileInputStream; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | 11 | 12 | public class FileUtils { 13 | 14 | /** 15 | * Detect charset encoding of a file 16 | * 17 | * @param file: the file to detect encoding from 18 | * @return the charset encoding 19 | * @throws IOException 20 | */ 21 | public static String guessEncoding(File file) throws IOException { 22 | try (FileInputStream is = new FileInputStream(file)) { 23 | return guessEncoding(is); 24 | } 25 | } 26 | 27 | /** 28 | * Detect charset encoding of an input stream 29 | * 30 | * @param is: the InputStream to detect encoding from 31 | * @return the charset encoding 32 | * @throws IOException 33 | */ 34 | public static String guessEncoding(InputStream is) throws IOException { 35 | return guessEncoding(IOUtils.toByteArray(is)); 36 | } 37 | 38 | /** 39 | * Detect charset encoding of a byte array 40 | * 41 | * @param bytes: the byte array to detect encoding from 42 | * @return the charset encoding 43 | */ 44 | public static String guessEncoding(byte[] bytes) { 45 | UniversalDetector detector = new UniversalDetector(null); 46 | 47 | detector.handleData(bytes, 0, bytes.length); 48 | detector.dataEnd(); 49 | 50 | String encoding = detector.getDetectedCharset(); 51 | detector.reset(); 52 | 53 | if (encoding == null || "MACCYRILLIC".equals(encoding)) { 54 | // juniversalchardet incorrectly detects windows-1256 as MACCYRILLIC 55 | // If encoding is MACCYRILLIC or null, we use ICU4J 56 | // CharsetMatch detected = new CharsetDetector().setText(bytes).detect(); 57 | // if (detected != null) { 58 | // encoding = detected.getName(); 59 | // } else { 60 | encoding = "UTF-8"; 61 | // } 62 | } 63 | 64 | return encoding; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /submerge-api/src/test/java/com.github.dnbn.submerge.api/Test.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api; 2 | 3 | import com.github.dnbn.submerge.api.parser.ASSParser; 4 | import com.github.dnbn.submerge.api.parser.SRTParser; 5 | import com.github.dnbn.submerge.api.subtitle.ass.ASSSub; 6 | import com.github.dnbn.submerge.api.subtitle.common.TimedLine; 7 | import com.github.dnbn.submerge.api.subtitle.srt.SRTSub; 8 | 9 | import java.io.File; 10 | import java.util.ArrayList; 11 | import java.util.Arrays; 12 | import java.util.List; 13 | import java.util.Set; 14 | 15 | public class Test { 16 | 17 | private static boolean isSrt; 18 | 19 | public static void main(String args[]) { 20 | 21 | Set timedLines = getTimedLines(); 22 | printTimeLine(timedLines, isSrt); 23 | 24 | } 25 | 26 | public static Set getTimedLines() { 27 | File assFile = new File("E:\\有字幕的视频\\Game.of.Thrones.S06E10.1080p.HDTV.x264-BATV.简体.ass"); 28 | File srtFile = new File("C:\\Users\\jianyou.lin\\Downloads\\interstellar.(2014).chi.1cd.(7601884)\\interstellar.srt"); 29 | isSrt = true; 30 | 31 | 32 | // for (TimedLine next : parse.getTimedLines()) { 33 | // LocalTime start = next.getTime().getStart(); 34 | // LocalTime end = next.getTime().getEnd(); 35 | // List textLines = next.getTextLines(); 36 | // System.out.println("next start:" + start); 37 | // System.out.println("next end:" + end); 38 | // for (int i = 0; i < textLines.size(); i++) { 39 | // System.out.println("next line:" + textLines.get(i)); 40 | // } 41 | // } 42 | System.out.println("effect:"); 43 | Set timedLines; 44 | if (isSrt) { 45 | SRTParser srtParser = new SRTParser(); 46 | SRTSub srtSub = srtParser.parse(srtFile,"utf-8"); 47 | timedLines = srtSub.getTimedLines(); 48 | } else { 49 | ASSParser assParser = new ASSParser(); 50 | ASSSub parse = assParser.parse(assFile,"utf-8"); 51 | String filename = parse.getFilename(); 52 | System.out.println("filename:" + filename); 53 | timedLines = parse.getTimedLines(); 54 | } 55 | return timedLines; 56 | } 57 | 58 | public static List printTimeLine(Set timedLines, boolean isSrt) { 59 | List arrayList = new ArrayList<>(); 60 | for (TimedLine timedLine : timedLines) { 61 | long startMs = timedLine.getTime().getStart(); 62 | long endMs = timedLine.getTime().getEnd(); 63 | 64 | // String toString = start.toString(); 65 | // if (isSrt) { 66 | // startMs = parseSRTSubtitleTime(toString); 67 | // endMs = parseSRTSubtitleTime(end.toString()); 68 | // } else { 69 | // startMs = parseASSSubtitleTime(toString); 70 | // endMs = parseASSSubtitleTime(end.toString()); 71 | // } 72 | List textLines = timedLine.getTextLines(); 73 | StringBuilder stringBuilder = new StringBuilder(); 74 | for (String line : textLines) { 75 | stringBuilder.append(line); 76 | } 77 | // String orgin = ""; 78 | // if (timedLine.getTime() instanceof SRTTime) { 79 | // SRTTime srtTime = (SRTTime) timedLine.getTime(); 80 | // orgin = srtTime.toString(); 81 | // } else if (timedLine.getTime() instanceof ASSTime) { 82 | // ASSTime srtTime = (ASSTime) timedLine.getTime(); 83 | // orgin = srtTime.toString(); 84 | // } 85 | String printText = "origin " + timedLine.getTime().toString() + " start:" + startMs + " end:" + endMs + " text:" + stringBuilder.toString(); 86 | System.out.println(printText); 87 | // System.out.println( " start:" + startMs + " end:" + endMs + " text:" + stringBuilder.toString()); 88 | arrayList.add(printText); 89 | } 90 | return arrayList; 91 | } 92 | 93 | /** 94 | * @param in string like 10:23:56.90 最小为0.01秒 95 | * @return long milliseconds 96 | */ 97 | public static long parseASSSubtitleTime(String in) { 98 | String[] split1 = in.split(":"); 99 | long hours = Long.parseLong(split1[0].trim()); 100 | long minutes = Long.parseLong(split1[1].trim()); 101 | long seconds = 0; 102 | long millies = 0; 103 | if (split1.length > 2) { 104 | String[] split = split1[2].split("\\.");//ASS format 105 | seconds = Long.parseLong(split[0].trim()); 106 | if (split.length > 1) { 107 | millies = Long.parseLong(split[1].trim()); 108 | } else { 109 | System.out.println("has not ms in = [" + in + "]"); 110 | } 111 | } else { 112 | System.out.println("in = [" + in + "] split:" + Arrays.toString(split1)); 113 | } 114 | return hours * 60 * 60 * 1000 + minutes * 60 * 1000 + seconds * 1000 + millies * 1000; 115 | } 116 | 117 | /** 118 | * @param in string like 10:23:56,909 119 | * @return long milliseconds 120 | */ 121 | public static long parseSRTSubtitleTime(String in) { 122 | String[] split1 = in.split(":"); 123 | long hours = Long.parseLong(split1[0].trim()); 124 | long minutes = Long.parseLong(split1[1].trim()); 125 | long seconds = 0; 126 | long millies = 0; 127 | if (split1.length > 2) { 128 | String[] split = split1[2].split(",");//SRT format 129 | seconds = Long.parseLong(split[0].trim()); 130 | if (split.length > 1) { 131 | millies = Long.parseLong(split[1].trim()); 132 | } else { 133 | System.out.println("in = [" + in + "]"); 134 | } 135 | } else { 136 | System.out.println("in = [" + in + "] split1:" + Arrays.toString(split1)); 137 | } 138 | return hours * 60 * 60 * 1000 + minutes * 60 * 1000 + seconds * 1000 + millies; 139 | } 140 | 141 | 142 | } 143 | -------------------------------------------------------------------------------- /submerge-api/src/test/java/com.github.dnbn.submerge.api/TestJunit.java: -------------------------------------------------------------------------------- 1 | package com.github.dnbn.submerge.api; 2 | 3 | import com.github.dnbn.submerge.api.subtitle.common.TimedLine; 4 | import org.junit.Test; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Set; 9 | 10 | public class TestJunit { 11 | @Test 12 | public void assembleTimedLineText() { 13 | Set timedLines = com.github.dnbn.submerge.api.Test.getTimedLines(); 14 | List textLines = com.github.dnbn.submerge.api.Test.printTimeLine(timedLines, false); 15 | StringBuilder stringBuilder = new StringBuilder(); 16 | for (String line : textLines) { 17 | int start; 18 | int end; 19 | StringBuilder stringLeft = new StringBuilder(); 20 | while ((start = line.indexOf("{")) != -1 && (end = line.indexOf("}")) != -1) { 21 | int nextLineStringStartIndex = end + 1; 22 | stringLeft.append(line, 0, start); 23 | // String blockingWords = line.substring(start, nextLineStringStartIndex); 24 | // System.out.println("blockingWords:" + blockingWords); 25 | if (nextLineStringStartIndex < line.length()) 26 | line = line.substring(nextLineStringStartIndex); 27 | // System.out.println("stringLeft:" + stringLeft); 28 | } 29 | stringBuilder.append(stringLeft) 30 | .append(line) 31 | .append("\n"); 32 | } 33 | if (stringBuilder.length() > 0) { 34 | stringBuilder.delete(stringBuilder.lastIndexOf("\n"), stringBuilder.length()); 35 | } 36 | System.out.println(stringBuilder.toString()); 37 | } 38 | } 39 | --------------------------------------------------------------------------------