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