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    }