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 package sears.file; 019 020 import java.io.BufferedWriter; 021 import java.io.File; 022 import java.io.FileOutputStream; 023 import java.io.IOException; 024 import java.io.OutputStreamWriter; 025 import java.util.ArrayList; 026 import java.util.Iterator; 027 import java.util.StringTokenizer; 028 029 import sears.file.exception.io.FileConversionException; 030 import sears.tools.Trace; 031 import sears.tools.Utils; 032 033 /** 034 * This class represents a ssa subtitle file. 035 * Specialize the SubtitleFile for ssa type subtitles. 036 * It represents too an ass subtitle file too. 037 */ 038 public class SsaFile extends SubtitleFile { 039 040 public static final String DIALOGUE_KEY = "Dialogue:"; 041 042 public static final String FORMAT_KEY = "Format:"; 043 044 /** Identifier key for the events section */ 045 public static final String EVENTS_SECTION_DELIMITER = "[Events]"; 046 047 // the events key used by the ssa file: 048 private String[] fieldsKey; 049 // the first part of the ssa file: 050 private String beginSsaFilePart; 051 // the last part of the ssa file: 052 private String endSsaFilePart; 053 054 /** 055 * Constructor SsaFile. 056 * <br><b>Summary:</b><br> 057 * Constructor of the class. 058 * Beware not to use this file directly, because it does contains no ST. 059 * You will have to fill the list of ST, and save the File first. 060 */ 061 public SsaFile() { 062 super(); 063 } 064 065 /** 066 * Constructor SsaFile. 067 * <br><b>Summary:</b><br> 068 * Constructor of the class. 069 * @param file The <b>(File)</b> to open. 070 * @param subtitleList The <b>(ArrayList)</b> List of subtitles. 071 * @throws FileConversionException 072 */ 073 public SsaFile(File file, ArrayList<Subtitle> subtitleList) throws FileConversionException { 074 super(file, subtitleList); 075 } 076 077 /** 078 * Constructor SsaFile. 079 * <br><b>Summary:</b><br> 080 * Constructor of the class. 081 * @param file The <b>(String)</b> path to file to open. 082 * @param subtitleList The <b>(ArrayList)</b> List of subtitles. 083 * @throws FileConversionException 084 */ 085 public SsaFile(String file, ArrayList<Subtitle> subtitleList) throws FileConversionException { 086 super(file, subtitleList); 087 } 088 089 /** 090 * 091 * @param file 092 * @param subtitleList 093 * @param charset 094 * @throws FileConversionException 095 */ 096 public SsaFile(File file, ArrayList<Subtitle> subtitleList, String charset) throws FileConversionException { 097 super(file, subtitleList, charset); 098 } 099 100 /* 101 * (non-Javadoc) 102 * @see sears.file.SubtitleFile#getNewInstance() 103 */ 104 protected SubtitleFile getNewInstance() { 105 return new SsaFile(); 106 } 107 108 protected void setFieldsKey(String[] fieldsKey) { 109 this.fieldsKey = fieldsKey; 110 } 111 112 protected String[] getFieldsKey() { 113 return fieldsKey; 114 } 115 116 protected void setBeginPart(String beginPart) { 117 this.beginSsaFilePart = beginPart; 118 } 119 120 protected void setEndPart(String endPart) { 121 this.endSsaFilePart = endPart; 122 } 123 124 /* 125 * (non-Javadoc) 126 * @see sears.file.SubtitleFile#parse() 127 */ 128 protected void parse() throws FileConversionException { 129 FileConversion pm = null; 130 String charset = DEFAULT_CHARSET; 131 if( !charset.equals(getCharset()) ) { 132 pm = new FileToSsaFile(file, getCharset(), this); 133 // try to fill the list of subtitles: 134 pm.parse(subtitleList); 135 } else { 136 int count = -1; 137 while( count < BASIC_CHARSETS.length ) { 138 try { 139 pm = new FileToSsaFile(file, charset, this); 140 // try to fill the list of subtitles: 141 pm.parse(subtitleList); 142 // parse succeed so, stop the loop: 143 count = BASIC_CHARSETS.length; 144 setCharset(charset); 145 } catch (FileConversionException e) { 146 // NOTE FOR DEVELOPER: 147 // 148 // the catching exception must be tested 149 // if the nature of the nature is a basic IO exception 150 // it is not necessary to parse again with a different charset 151 // the error will occur again 152 count++; 153 if( count >= BASIC_CHARSETS.length ) { 154 // stop trying to decode: 155 throw e; 156 } else { 157 // change the charset 158 charset = BASIC_CHARSETS[count]; 159 } 160 } 161 } 162 } 163 } 164 165 /* 166 * (non-Javadoc) 167 * @see sears.file.SubtitleFile#writeToFile(java.io.File) 168 */ 169 public void writeToFile(File fileToWrite) throws FileConversionException { 170 // First is to know which end of line to use. 171 // by default use the linux one. 172 String lineSeparator = getLineSeparator(); 173 try { 174 // keep the initial encoding: 175 BufferedWriter out = new BufferedWriter( 176 new OutputStreamWriter( 177 new FileOutputStream(fileToWrite), super.getCharset())); 178 179 // ************************** 180 // we write the first part of ssa file: 181 out.write(beginSsaFilePart); 182 183 // ************************** 184 // we write the key [Events]: 185 out.write(EVENTS_SECTION_DELIMITER + lineSeparator); 186 187 // ************************** 188 // we write the format line: 189 String str = FORMAT_KEY + " "; 190 for( int i=0;i<fieldsKey.length - 1;i++ ) { 191 str = str + fieldsKey[i] + ", "; 192 } 193 str = str + fieldsKey[fieldsKey.length - 1] + lineSeparator; 194 out.write(str); 195 196 // ************************** 197 // we write all subtitles: 198 Iterator<Subtitle> subtitles = subtitleList.iterator(); 199 while (subtitles.hasNext()) { 200 SsaSubtitle ssaSubtitle = (SsaSubtitle) subtitles.next(); 201 202 out.write(DIALOGUE_KEY + " "); 203 for( int i=0;i<fieldsKey.length;i++ ) { 204 if( fieldsKey[i].contentEquals("Start") ) { 205 out.write(timeToString(ssaSubtitle.getStartDate()) + ","); 206 } else if( fieldsKey[i].contentEquals("End") ) { 207 out.write(timeToString(ssaSubtitle.getEndDate()) + ","); 208 } else if( fieldsKey[i].contentEquals("Text") ) { 209 out.write(ssaSubtitle.getSubtitle().replace(Utils.LINE_SEPARATOR, lineSeparator)); 210 //Check wether subtitle has an end of line. 211 if( !ssaSubtitle.getSubtitle().endsWith(lineSeparator) ){ 212 //If it doesn't have one, put one. 213 //out.write(lineSeparator); 214 } 215 } else { 216 out.write(ssaSubtitle.getEntrie(fieldsKey[i]) + ","); 217 } 218 } 219 out.write(endSsaFilePart); 220 } 221 222 out.close(); 223 //indicate file is good. 224 fileChanged = false; 225 } catch (IOException e) { 226 throw FileConversionException.getAccessException( 227 FileConversionException.WRITE_ACCESS, file); 228 } 229 } 230 231 /* 232 * (non-Javadoc) 233 * @see sears.file.SubtitleFile#writeToTemporaryFile() 234 */ 235 public void writeToTemporaryFile() { 236 try { 237 // Create the temporary subtitle file 238 if (temporaryFile == null) { 239 temporaryFile = java.io.File.createTempFile( 240 extension().toUpperCase() + "Subtitle", null); 241 temporaryFile.deleteOnExit(); 242 } 243 // Store to the temporary file 244 boolean oldFileChangedStatus = fileChanged; 245 writeToFile(temporaryFile); 246 // Restore the old file changed value 247 fileChanged = oldFileChangedStatus; 248 } catch (IOException e) { 249 Trace.trace("Error while writing temporary " + extension().toUpperCase() + " file !", 250 Trace.ERROR_PRIORITY); 251 Trace.trace(e.getMessage(), Trace.ERROR_PRIORITY); 252 } 253 } 254 255 /** 256 * Method stringToTime. 257 * <br><b>Summary:</b><br> 258 * Return the number of miliseconds that correspond to the given String time representation. 259 * @param time The string ssa time representation. 260 * @return <b>(int)</b> The corresponding number of miliseconds. 261 */ 262 public static int stringToTime(String time) throws NumberFormatException{ 263 // we add a 0 for time conversion compatibility: 264 return SubtitleFile.stringToTime(time + 0); 265 } 266 267 /** 268 * Method timeToString. 269 * <br><b>Summary:</b><br> 270 * This method transform a number of milliseconds in a string representation. 271 * @param milliseconds The number of milliseconds to transform 272 * @return <b>(String)</b> The corresponding String representation of the number of milliseconds. 273 */ 274 public static String timeToString(int milliseconds){ 275 String result = SubtitleFile.timeToString(milliseconds); 276 // 00:00:00,000 --> 0:00:00.00 277 // we erase the first and last number (no rounded): 278 result = result.substring(1, result.length()-1); 279 // and replace comma by point: 280 result = result.replace(",", "."); 281 return result; 282 } 283 284 /* 285 * (non-Javadoc) 286 * @see sears.file.SubtitleFile#extension() 287 */ 288 public String extension() { 289 return "ssa"; 290 } 291 292 // there's maybe a problem with ass file: 293 294 /* 295 * (non-Javadoc) 296 * @see sears.file.SubtitleFile#split(java.io.File[], int, int) 297 */ 298 public SubtitleFile[] split(File[] destinationFiles, int subtitleIndex, int secondPartDelay) { 299 //The result of the method 300 SubtitleFile[] result = new SubtitleFile[2]; 301 //Construct first part Subtitle File. 302 result[0] = getNewInstance(); 303 //set its file. 304 result[0].setFile(destinationFiles[0]); 305 //and second part. 306 result[1] = getNewInstance(); 307 //set its file. 308 result[1].setFile(destinationFiles[1]); 309 //Fill in the subtitle Files. 310 int index = 0; 311 //by parsing current STs list, and add to one or other file. 312 for (Subtitle currentSubtitle : subtitleList) { 313 //If number is before limit, add to first part. 314 if (index < subtitleIndex) { 315 result[0].addSubtitle(new SsaSubtitle(currentSubtitle)); 316 } else { 317 //else, add to the second part. 318 result[1].addSubtitle(new SsaSubtitle(currentSubtitle), true); 319 } 320 index++; 321 } 322 323 //Apply delay. 324 if(secondPartDelay >= 0){ 325 //Delay second part, so first ST is at time 0 326 result[1].shiftToZero(); 327 result[1].delay(secondPartDelay); 328 } 329 //return the result; 330 return result; 331 } 332 } 333 334 /** 335 * Parsing Ssa subtitle 336 */ 337 class FileToSsaFile extends FileConversion { 338 339 // there's no subtitle number store in a sub file 340 // so a meter is necessary: 341 private int subtitleCount; 342 343 private SsaFile ssaFile; 344 345 /** 346 * Constructs a new instance of <tt>SsaFileConversion</tt> 347 * @param file the file to convert 348 * @param charset the charset used for the conversion 349 * @param ssaFile the resulting ssaFile, use to fill some part of <tt>file</tt> 350 * @throws FileConversionException 351 */ 352 public FileToSsaFile(File file, String charset, SsaFile ssaFile) throws FileConversionException { 353 super(file, charset); 354 if( ssaFile == null ) { 355 throw new NullPointerException("ssaFile cannot be null"); 356 } 357 this.ssaFile = ssaFile; 358 subtitleCount = 0; 359 } 360 361 /* 362 * (non-Javadoc) 363 * @see sears.file.FileConversion#parse(java.util.ArrayList) 364 */ 365 public void parse(ArrayList<Subtitle> subtitleList) throws FileConversionException { 366 ssaFile.setBeginPart(getBeginPart()); 367 ssaFile.setFieldsKey(getFieldsKey()); 368 super.parse(subtitleList); 369 ssaFile.setEndPart(getEndPart()); 370 } 371 372 // Dialogue: Marked=0,0:00:20.03,0:00:21.03,Default,NTP,0000,0000,0000,!Effect,Tales of Earthsea 373 protected Subtitle getSubtitle(String line) throws FileConversionException { 374 SsaSubtitle ssaSubtitle = null; 375 String str = ""; 376 if( line != null && line.trim().length() != 0 ) { 377 if( !line.startsWith(SsaFile.DIALOGUE_KEY)) { 378 throw FileConversionException.getMalformedSubtitleFileException( 379 FileConversionException.MALFORMED_SUBTITLE_FILE, file, lineCount, line); 380 } else { 381 // we remove string array "Dialogue:" 382 str = line.replace(SsaFile.DIALOGUE_KEY, ""); 383 } 384 385 // ******** 386 // NUMBER * 387 // ******** 388 // we create an instance of the SubtitleStyle class with the 389 // specified number of subtitle: 390 ssaSubtitle = new SsaSubtitle(++subtitleCount); 391 392 393 // ******** 394 // FIELDS * 395 // ******** 396 // it's needed to prepare line before tokenize it 397 // if sub-array ',,' exists is not considered like a token ! 398 // so we put a space instead of nothing... 399 str = str.replace(",,", ", ,"); 400 // we create a tokenizer: 401 StringTokenizer stk = new StringTokenizer(str, ","); 402 // and stores the elements: 403 String aToken = ""; 404 String[] fieldsKey = ssaFile.getFieldsKey(); 405 for(int i=0; i<fieldsKey.length - 1; i++){ 406 aToken = stk.nextToken(); 407 // if token does not represent the subtitle array text, 408 // we remove all spaces: 409 ssaSubtitle.putEntrie(fieldsKey[i], aToken.replace(" ", "")); 410 } 411 412 // ****** 413 // TEXT * 414 // ****** 415 // a precaution if the subtitle field is empty: 416 if( stk.hasMoreTokens() ) { 417 aToken = stk.nextToken(); 418 while( stk.hasMoreTokens() ) { 419 aToken = aToken + "," + stk.nextToken(); 420 } 421 } else { 422 // empty string 423 aToken = ""; 424 } 425 ssaSubtitle.putEntrie(fieldsKey[fieldsKey.length - 1], aToken); 426 427 // ************* 428 // "TIME LINE" * 429 // ************* 430 String startDate = ssaSubtitle.getEntrie("Start"); 431 String endDate = ssaSubtitle.getEntrie("End"); 432 if( startDate == null && endDate == null ) { 433 throw FileConversionException.getMalformedSubtitleFileException( 434 FileConversionException.NO_SUBTITLE_TIME, file, lineCount, line); 435 } 436 437 // ******************** 438 // START AND END TIME * 439 // ******************** 440 try { 441 // START TIME 442 try { 443 ssaSubtitle.putEntrie("Start", String.valueOf(( 444 SsaFile.stringToTime(startDate)))); 445 } catch (NumberFormatException e) { 446 // time string is not formatted like a time 447 throw FileConversionException.getMalformedSubtitleFileException( 448 FileConversionException.MALFORMED_START_TIME, file, lineCount, line); 449 } 450 451 // END TIME 452 try { 453 ssaSubtitle.putEntrie("End", String.valueOf(( 454 SsaFile.stringToTime(ssaSubtitle.getEntrie("End"))))); 455 } catch (NumberFormatException e) { 456 // time string is not formatted like a time 457 throw FileConversionException.getMalformedSubtitleFileException( 458 FileConversionException.MALFORMED_END_TIME, file, lineCount, line); 459 } 460 } catch (NullPointerException e) { 461 // Start or end time doesn't not exists in the fields key 462 throw FileConversionException.getMalformedSubtitleFileException( 463 FileConversionException.MALFORMED_TIME_LINE, file, lineCount, line); 464 } 465 } 466 return ssaSubtitle; 467 } 468 469 /** 470 * Gets the begin part of the file, until the events section delimiter 471 * <br>The reader state is on the events section delimiter line 472 * @return the begin part as a <tt>String</tt> 473 * @throws FileConversionException if an error occurs 474 * @see SsaFile#EVENTS_SECTION_DELIMITER 475 */ 476 private String getBeginPart() throws FileConversionException { 477 String str = getTheNextNonEmptyLine(); 478 if( str == null ) { 479 throw FileConversionException.getMalformedSubtitleFileException( 480 FileConversionException.EMPTY_SUBTITLE_FILE, file); 481 } // else 482 483 try { 484 String line = str; 485 while( !line.startsWith(SsaFile.EVENTS_SECTION_DELIMITER) ) { 486 str += line + Utils.LINE_SEPARATOR; 487 line = readLine(); 488 } 489 } catch (NullPointerException e) { 490 // USE EXCEPTION (Rare) 491 // str == null we reach end of file without founded the EVENTS_SECTION_DELIMITER 492 // that means the subtitle is malformed, there's no subtitle part 493 throw FileConversionException.getMalformedSubtitleFileException( 494 FileConversionException.UNEXPECTED_END_OF_FILE, file); 495 } 496 497 return str; 498 // Reader is on the line which contains the EVENTS_SECTION_DELIMITER 499 } 500 501 private String getEndPart() throws FileConversionException { 502 String str = ""; 503 // while there's line to be read: 504 String line = ""; 505 while( line != null ) { 506 // we read line: 507 str += line + Utils.LINE_SEPARATOR; 508 line = readLine(); 509 } 510 return str; 511 } 512 513 // Format: Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text 514 private String[] getFieldsKey() throws FileConversionException { 515 // Reader positioned on the line which contains the EVENTS_SECTION_DELIMITER 516 String initialLine = getTheNextNonEmptyLine(); 517 if( initialLine == null ) { 518 // Events section do not contains anything 519 throw FileConversionException.getMalformedSubtitleFileException( 520 FileConversionException.UNEXPECTED_END_OF_FILE, file); 521 } 522 523 // ****************** 524 // STRING "FORMAT:" * 525 // ****************** 526 String str = ""; 527 if( initialLine.startsWith(SsaFile.FORMAT_KEY) ) { 528 str = initialLine.replace(SsaFile.FORMAT_KEY, ""); 529 } else { 530 // the line is malformed: 531 FileConversionException.getMalformedSubtitleFileException( 532 FileConversionException.MALFORMED_SUBTITLE_FILE, file, lineCount, initialLine); 533 } 534 535 // ****************** 536 // 'TOKENIZE' LINE: * 537 // ****************** 538 539 // TEST THE VALIDITY OF THE LINE: 540 // remove all spaces present in the string: 541 str = str.replaceAll(" ", ""); 542 // we create a tokenizer: 543 StringTokenizer stk = new StringTokenizer(str, ","); 544 int countTokens = stk.countTokens(); 545 if( countTokens == 0 ) { 546 // there's no field and so no text field: 547 FileConversionException.getMalformedSubtitleFileException( 548 FileConversionException.MALFORMED_SUBTITLE_FILE, file, lineCount, initialLine); 549 } 550 551 // PARSING LINE 552 int linePosition = 0; 553 // else there's at least one field 554 String[] fieldsKey = new String[countTokens]; 555 //stores the first element: 556 str = stk.nextToken(); 557 fieldsKey[linePosition++] = str; 558 // and finally stores the others elements: 559 while( stk.hasMoreElements() ) { 560 str = stk.nextToken(); 561 fieldsKey[linePosition] = str; 562 linePosition++; 563 } 564 565 return fieldsKey; 566 } 567 }