001 ///////////////////////////////////////////////// 002 //This file is part of Sears project. 003 //Subtitle Editor And Re-Synch 004 //A tool to easily modify and resynch movies subtitles. 005 ///////////////////////////////////////////////// 006 //This program is free software; 007 //you can redistribute it and/or modify it under the terms 008 //of the GNU General Public License 009 //as published by the Free Software Foundation; 010 //either version 2 of the License, or (at your option) any later version. 011 ///////////////////////////////////////////////// 012 //Sears project is available under sourceforge 013 //at adress: http://sourceforge.net/projects/sears/ 014 //Copyright (C) 2005 Booba Skaya 015 //Mail: booba.skaya@gmail.com 016 ///////////////////////////////////////////////// 017 018 //some suggestions about this class: floriaen@gmail.com 019 020 package sears.file; 021 022 import java.io.BufferedWriter; 023 import java.io.File; 024 import java.io.FileOutputStream; 025 import java.io.IOException; 026 import java.io.OutputStreamWriter; 027 import java.util.ArrayList; 028 import java.util.Iterator; 029 import java.util.StringTokenizer; 030 import java.util.regex.Matcher; 031 import java.util.regex.Pattern; 032 033 import sears.file.exception.io.FileConversionException; 034 import sears.tools.Trace; 035 import sears.tools.Utils; 036 037 /** 038 * Class SubFile. 039 * <br><b>Summary:</b><br> 040 * This class represents a sub subtitle file. 041 * Specialize the SubtitleFile for sub type subtitles. 042 */ 043 044 /* 045 * { xxx } xxx represents the frame apparition/disparition on video 046 * xxx/fps is the time in seconds of the apparition/disparition of subtitle on screen 047 */ 048 public class SubFile extends SubtitleFile { 049 050 /** the line separator in the sub subtitle text: '|' */ 051 protected static final String LINE_SEPARATOR = "|"; 052 053 /** The time separator, used by <tt>StrinkTokenozer</tt> object to gets back start and end time*/ 054 protected static final String TIME_SEPARATOR = "{}"; 055 056 // ^ begin of line 057 // looking for '\\{\\d+\\}' ( '\d' a number, '\d+' 1 or more number ) 058 // {2} 2 time 059 /** pattern that represents sub subtitle time line, a string that begin by: {number}{number}*/ 060 protected static final String TIME_LINE_PATTERN = "^(\\{\\d+\\}){2}"; 061 062 063 /** Standards framerate */ 064 protected static final double MOVIE_FRAMERATE = 23.976; 065 //protected static final double ??_FRAMERATE = 24; 066 protected static final double PAL_FRAMERATE = 25; 067 protected static final double NTSC_FRAMERATE = 29.97; 068 //protected static final double ??_FRAMERATE = 30; 069 070 /* the framerate used to calculate subtitle time */ 071 private double usedFramerate; 072 073 /** 074 * Constructor SubFile. 075 * <br><b>Summary:</b><br> 076 * Constructor of the class. 077 * Beware not to use this file directly, because it does contains no ST. 078 * You will have to fill the list of ST, and save the File first. 079 */ 080 public SubFile(){ 081 super(); 082 usedFramerate = PAL_FRAMERATE; 083 } 084 085 /** 086 * Constructor SubFile. 087 * <br><b>Summary:</b><br> 088 * Constructor of the class. 089 * @param file The <b>(File)</b> to open. 090 * @param subtitleList The <b>(ArrayList)</b> List of subtitles. 091 * @throws FileConversionException 092 */ 093 public SubFile(File file, ArrayList<Subtitle> subtitleList) throws FileConversionException { 094 super(file, subtitleList); 095 usedFramerate = PAL_FRAMERATE; 096 } 097 098 /** 099 * Constructor SubFile. 100 * <br><b>Summary:</b><br> 101 * Constructor of the class. 102 * @param file The <b>(String)</b> path to file to open. 103 * @param subtitleList The <b>(ArrayList)</b> List of subtitles. 104 * @throws FileConversionException 105 */ 106 public SubFile(String file, ArrayList<Subtitle> subtitleList) throws FileConversionException { 107 super(file, subtitleList); 108 usedFramerate = PAL_FRAMERATE; 109 } 110 111 public SubFile(File file, ArrayList<Subtitle> subtitleList, String charset) throws FileConversionException { 112 super(file, subtitleList, charset); 113 usedFramerate = PAL_FRAMERATE; 114 } 115 116 /* 117 * (non-Javadoc) 118 * @see sears.file.SubtitleFile#extension() 119 */ 120 public String extension() { 121 return "sub"; 122 } 123 124 /* 125 * (non-Javadoc) 126 * @see sears.file.SubtitleFile#getNewInstance() 127 */ 128 protected SubtitleFile getNewInstance() { 129 return new SubFile(); 130 } 131 132 /* 133 * (non-Javadoc) 134 * @see sears.file.SubtitleFile#parse() 135 */ 136 protected void parse() throws FileConversionException { 137 // AUTOMATION OF CHARSET 138 FileConversion pm = null; 139 String charset = DEFAULT_CHARSET; 140 if( !charset.equals(getCharset()) ) { 141 pm = new FileToSubFile(file, getCharset()); 142 // try to fill the list of subtitles: 143 pm.parse(subtitleList); 144 } else { 145 int count = -1; 146 while( count < BASIC_CHARSETS.length ) { 147 try { 148 pm = new FileToSubFile(file, charset); 149 // try to fill the list of subtitles: 150 pm.parse(subtitleList); 151 // parse succeed so, stop the loop: 152 count = BASIC_CHARSETS.length; 153 setCharset(charset); 154 } catch (FileConversionException e) { 155 // NOTE FOR DEVELOPER: 156 // 157 // the catching exception must be tested 158 // if the nature of the nature is a basic IO exception 159 // it is not necessary to parse again with a different charset 160 // the error will occur again 161 count++; 162 if( count >= BASIC_CHARSETS.length ) { 163 // stop trying to decode: 164 throw e; 165 } else { 166 // use another charset to parsing the file 167 charset = BASIC_CHARSETS[count]; 168 } 169 } 170 } 171 } 172 } 173 174 /* 175 * (non-Javadoc) 176 * @see sears.file.SubtitleFile#writeToFile(java.io.File) 177 */ 178 public void writeToFile(File fileToWrite) throws FileConversionException { 179 //First is to know which end of line to use. 180 //by default use the linux one. 181 String endOfLine = getLineSeparator(); 182 try { 183 // keep the initial encoding: 184 BufferedWriter out = new BufferedWriter( 185 new OutputStreamWriter( 186 new FileOutputStream(fileToWrite), super.getCharset())); 187 188 Iterator<Subtitle> subtitles = subtitleList.iterator(); 189 while (subtitles.hasNext()) { 190 Subtitle subtitle = subtitles.next(); 191 192 // format START DATE and write it: 193 String date = String.valueOf(subtitle.getStartDate()); 194 /* int dateLength = date.length(); 195 if( dateLength > 2 ) { 196 date = date.substring(0, date.length()-2); 197 }*/ 198 String startFrame = millisecondsToFrame( date, usedFramerate ); 199 out.write("{" + startFrame + "}"); 200 201 // format END DATE and write it: 202 date = String.valueOf(subtitle.getEndDate()); 203 //date = date.substring(0, date.length()-2); 204 String endFrame = millisecondsToFrame( date, usedFramerate ); 205 out.write("{" + endFrame + "}"); 206 207 // SUBTITLE: 208 String subtitleText = subtitle.getSubtitle(); 209 // remove the last line separator 210 if( subtitleText.endsWith(Utils.LINE_SEPARATOR) ) { 211 int length = subtitleText.length(); 212 subtitleText = subtitleText.substring(0, length - Utils.LINE_SEPARATOR.length()); 213 } 214 out.write(subtitleText.replace(Utils.LINE_SEPARATOR, SubFile.LINE_SEPARATOR)); 215 // write the last line separator 216 out.write(endOfLine); 217 218 } 219 out.close(); 220 //indicate file is good. 221 fileChanged = false; 222 } catch (IOException e) { 223 // FileNotFoundException 224 // IOException 225 throw FileConversionException.getAccessException( 226 FileConversionException.WRITE_ACCESS, file); 227 } catch( SecurityException e) { 228 throw FileConversionException.getAccessException( 229 FileConversionException.WRITE_ACCESS, file); 230 } 231 } 232 233 private String millisecondsToFrame( String milliseconds, double frameRate ) { 234 double convertedMilliseconds = 0; 235 try { 236 convertedMilliseconds = Double.parseDouble(milliseconds); 237 } catch ( NumberFormatException e ) { 238 Trace.trace(milliseconds+", invalid integer", Trace.ERROR_PRIORITY); 239 // stop the processus ---> TODO 240 } 241 double frameDouble = ( (convertedMilliseconds /1000) * frameRate ); 242 int frameInteger = ( int ) frameDouble; 243 //System.out.println( "DOUBLE: "+frameDouble ); 244 //System.out.println( convertedMilliseconds+" ms, "+frameInteger+" frame, FRAMERATE="+frameRate); 245 return String.valueOf(frameInteger); 246 247 } 248 249 /* 250 * (non-Javadoc) 251 * @see sears.file.SubtitleFile#writeToTemporaryFile() 252 */ 253 public void writeToTemporaryFile() { 254 try { 255 // Create the temporary subtitle file 256 if (temporaryFile == null) { 257 temporaryFile = File.createTempFile("SUBSubtitle", null); 258 temporaryFile.deleteOnExit(); 259 } 260 // Store to the temporary file 261 boolean oldFileChangedStatus = fileChanged; 262 writeToFile(temporaryFile); 263 // Restore the old file changed value 264 fileChanged = oldFileChangedStatus; 265 } catch (IOException e) { 266 Trace.trace("Error while writing temporary SUB file !", 267 Trace.ERROR_PRIORITY); 268 Trace.trace(e.getMessage(), Trace.ERROR_PRIORITY); 269 } 270 } 271 } 272 273 class FileToSubFile extends FileConversion { 274 275 // there's no subtitle number store in a sub file 276 // so a meter is necessary: 277 private int subtitleCount; 278 279 public FileToSubFile(File file, String charset) throws FileConversionException { 280 super(file, charset); 281 subtitleCount = 0; 282 } 283 284 /* 285 * see bug #1901242 286 * sub frame: 1114 / (frameRate) fps ---> time on screen 287 * { xxx } xxx represents the frame apparition on video 288 * 289 */ 290 private int convertFrameToMilliseconds(double subFrame, double frameRate ) { 291 return (int) ( (subFrame/frameRate)*1000 ); 292 } 293 294 // AUTOMATIC CORRECTION 295 296 // 297 // time: No double, no long 298 // subtitle text could be empty 299 protected Subtitle getSubtitle(String line) throws FileConversionException { 300 Subtitle subtitle = null; 301 if( line != null) { 302 boolean endOfFile = false; 303 // if the line is empty, try to read subtitle until the next non empty line 304 if( line.trim().length() == 0 ) { 305 // gets the next non empty line 306 // could be null if it is the end of file 307 line = super.getTheNextNonEmptyLine(); 308 if( line == null ) { 309 // END OF FILE 310 // WE STOP PARSING FILE 311 endOfFile = true; 312 } 313 } 314 315 // ************* 316 // PARSING JOB * 317 // ************* 318 // Line is non null and non empty, 319 // begins parsing the line. 320 // 321 // A valid SUB subtitle line looks like: 322 // "{1286}{1329}- There you are." 323 // 324 if( !endOfFile ) { 325 String timeLine = null; 326 327 int startTime = 0; 328 int endTime = 0; 329 String text = null; 330 331 // ******************** 332 // GETS THE TIME LINE * 333 // ******************** 334 Pattern pattern = Pattern.compile(SubFile.TIME_LINE_PATTERN); 335 Matcher matcher = pattern.matcher(line); 336 if( matcher.find() ) { 337 // time line looks like : {number}{number} 338 timeLine = matcher.group(); 339 } else { 340 // STOP PARSING FILE, 341 // there's no time line in this line, or it's corrupt 342 // like {89UYh0}{89 for example 343 // 344 // --------------------------------------------------------------------- 345 // NOTE FOR DEVELOPER: 346 // maybe we can try to correct the time line: 347 // - if a '{' or '}' is missing, we can try to add it 348 // - in the case that's there's letter in time part, we could remove it. 349 // --------------------------------------------------------------------- 350 throw FileConversionException.getMalformedSubtitleFileException( 351 FileConversionException.MALFORMED_TIME_LINE, file, lineCount, line); 352 } 353 354 355 // ************************* 356 // GETS START AND END TIME * 357 // ************************* 358 StringTokenizer stk = new StringTokenizer(timeLine, SubFile.TIME_SEPARATOR); 359 // There's two tokens, timeLine is looks like {7879}{7890} 360 // The two tokens are integer, if there's too long (double or long) 361 // an exception is throws 362 // 363 // --------------------------------------------------------------------- 364 // NOTE FOR DEVELOPER: 365 // maybe we can accept long and double, and so the parse could continue 366 // --------------------------------------------------------------------- 367 try { 368 double startFrame = Double.parseDouble(stk.nextToken()); 369 startTime = convertFrameToMilliseconds( startFrame, SubFile.PAL_FRAMERATE ); 370 371 //System.out.println( startFrame +" frame-->"+startTime+" milliseconds"); 372 373 } catch( NumberFormatException e ) { 374 // stk.nextToken() is long or double and non null 375 throw FileConversionException.getMalformedSubtitleFileException( 376 FileConversionException.MALFORMED_START_TIME, file, lineCount, line); 377 } 378 379 try { 380 double endFrame = Double.parseDouble(stk.nextToken()); 381 endTime = convertFrameToMilliseconds( endFrame, SubFile.PAL_FRAMERATE ); 382 } catch( NumberFormatException e ) { 383 // stk.nextToken() is long or double and non null 384 throw FileConversionException.getMalformedSubtitleFileException( 385 FileConversionException.MALFORMED_END_TIME, file, lineCount, line); 386 } 387 388 // ******************** 389 // GETS SUBTITLE TEXT * 390 // ******************** 391 // remove the time line from the line 392 // the line is non null so subtitle could not be null 393 // it could be an empty string, accept it 394 text = matcher.replaceAll(""); 395 // IMPORTANT POINT: 396 // replaces all the sub file specific line separator by the generic line separator 397 text = text.replace(SubFile.LINE_SEPARATOR, Utils.LINE_SEPARATOR); 398 399 // ***************** 400 // CREATE SUBTITLE * 401 // ***************** 402 subtitle = new Subtitle(++subtitleCount, startTime, endTime, text); 403 } // else newSubtitle is return as null 404 } // else newSubtitle is return as null 405 return subtitle; 406 } 407 }