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    }