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