〔Java〕简单的根据拍照时间重命名图片

 

一直使用ACDsee8的根据图片拍照时间来批量重命名图片,但是让人讨厌的是:

1.ACDsee8经常把前面的零省略,造成名称不能对齐,如2007-08-08 08-08-08它改成2007-08-08 8-8-8;

2.另外,ACDsee8使用了本地计算机的时间设置,如果你变动了时间设置,那么以后修改的样式也会跟着变动。

Bug:

1.由于一些图片的Exif信息过于靠后,在10KB后面,而我为了速度,只读取前面的10KB,所以一些图片可能不能改名。我现在准备分批读取前20480个字节(经过统计,已知最远的在16000字节左右),但要考虑隔断区的连接问题,有空再写了。

2.如果图片有重复,那么会在后面增加数字序列(从001到999999),然而第一个重复的,已经改好名的,没有数字序列的图片怎么办?添加序列号为000?添加完以后,第三个又会改成没有序列号的,莫非真要作二次循环?以后再写。

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileNotFoundException;


/*******************************************************************************
 *
 * 读出数码图片中的拍照时间
 *
 *
 * 原理:
 *
 * 1、由于数码图片的拍照时间可以直接从图片中搜索到,所以本例不需要分析EXIF的格式。
 *
 * 2、正常的(Normal)数码图片,会有三处到四处的时间,其中第二、第三处固定在一起,
 *
 *    第二处是图片拍摄时间,第三处是图片存储时间,一般两者相同,他们之间用NULL值
 *
 *    分隔(ASCII值为0),如下:
 *
 *    2001:01:02 12:23:56[NULL]2001:01:02 12:23:56[NULL]
 *
 *    两者位置不定,但格式好查找,八个冒号,两个空格,两个NULL值,其余为数字。
 *
 *    不用考虑01:09:34被压缩为1:9:34这样的特例,见EXIF2.1规范关于时间的三段:
 *
 *    1. DateTime(第一处,修改时间)
 *
 *    The date and time of image creation. In this standard it is the date and time
 *
 *    the file was changed.
 *
 *    The format is "YYYY:MM:DD HH:MM:SS" with time shown in 24-hour format, and
 *
 *    the date and time separated by one blank character [20.H].
 *
 *    When the date and time are unknown, all the character spaces except colons (":")
 *
 *    may be filled with blank characters, or else the Interoperability field may be
 *
 *    filled with blank characters.
 *
 *    The character string length is 20 bytes including NULL for termination.
 *
 *    When the field is left blank, it is treated as unknown.
 *
 *      Tag = 306 (132.H)
 *
 *      Type = ASCII
 *
 *      Count = 20
 *
 *      Default = none
 *
 *    2. DateTimeOriginal(第二处,拍摄时间)
 *
 *    The date and time when the original image data was generated. For a DSC the
 *
 *    date and time the picture was taken are recorded.
 *
 *    The format is "YYYY:MM:DD HH:MM:SS" with time shown in 24-hour format, and
 *
 *    the date and time separated by one blank character [20.H].
 *
 *    When the date and time are unknown, all the character spaces except colons (":")
 *
 *    may be filled with blank characters, or else the Interoperability field may be
 *
 *    filled with blank characters.
 *
 *    The character string length is 20 bytes including NULL for termination.
 *
 *    When the field is left blank, it is treated as unknown.
 *
 *      Tag = 36867 (9003.H)
 *
 *      Type = ASCII
 *
 *      Count = 20
 *
 *      Default = none
 *
 *    3. DateTimeDigitized(第三处,存储时间)
 *
 *    The date and time when the image was stored as digital data. If, for example,
 *
 *    an image was captured by DSC and at the same time the file was recorded, then
 *
 *    the DateTimeOriginal and DateTimeDigitized will have the same contents.
 *
 *    The format is "YYYY:MM:DD HH:MM:SS" with time shown in 24-hour format, and
 *
 *    the date and time separated by one blank character [20.H].
 *
 *    When the date and time are unknown, all the character spaces except colons (":")
 *
 *    may be filled with blank characters, or else the Interoperability field may be
 *
 *    filled with blank characters.
 *
 *    The character string length is 20 bytes including NULL for termination.
 *
 *    When the field is left blank, it is treated as unknown.
 *
 *      Tag = 36868 (9004.H)
 *
 *      Type = ASCII
 *
 *      Count = 20
 *
 *      Default = none
 *
 * 3、特别的(Special)相机,如富士系列FUJIFILM,使用XML格式记录EXIF,时间格式如下:
 *
 *    <exif:DateTimeOriginal>2006-12-24T12:33:08+08:00</exif:DateTimeOriginal>
 *
 *    对比正常的(Normal)相片,我们发现两者格式很类似:
 *
 *    2006:12:24 12:33:08   Normal
 *
 *    2006-12-24T12:33:08   Special
 *
 *
 * 另外:
 *
 * 1、由于本人测试的*.tif和*.tiff图片是由*.jpg通过ACDSee转换的,所以可能不准。
 *
 * 2、当然,网上有现成的包可供利用,详细方法请自行搜索,以下是包下载地址:
 *
 *   
http://www.drewnoakes.com/code/exif/releases/metadata-extractor-2.2.0.jar
 *
 * 3、此例没有按照标准的Java规范格式化。
 *
 * 4、开源的目的只有一个:我为人人,人人为我,欢迎开源!
 *
 *
 * Version: 1.00
 *
 * Author: NeedJava
 *
 * E-Mail:
NeedJava@126.com
 *
 * Modified: 2007.08.16/2007.08.29/2007.09.28
 *
 *
 * 你可以使用此程序于任何地方,但请保留程序作者及注释的完整。如果你改进了程序,
 *
 * 请在原作者后添加姓名,如:Author: NeedJava/Jack/Mike,版本及修改时间同理。
 *
 ******************************************************************************/
public final class Rename2PictrueOriginalDateTime
{
  private static char PRI_SEPARATOR = '-';  //主要分隔符

  private static char SUB_SEPARATOR = ' ';  //次要分隔符

  private long totalPictures;    //图片总数

  private long parsedPictures;   //含有有效时间的图片总数

  private long renamedPictures;  //成功修改名称的图片总数

  private long minimumSize;      //处理图片的最小字节数


  /*****************************************************************************
   *
   * 构造函数,默认使用当前路径,不搜索100KB以下图片
   *
   ****************************************************************************/
  public Rename2PictrueOriginalDateTime()
  {
    this( ".", 102400L );
  }

  public Rename2PictrueOriginalDateTime( String fileName )
  {
    this( fileName, 102400L );
  }

  public Rename2PictrueOriginalDateTime( long minimumSize )
  {
    this( ".", minimumSize );
  }

  public Rename2PictrueOriginalDateTime( String fileName, long minimumSize )
  {
    this.totalPictures   = 0L;

    this.parsedPictures  = 0L;

    this.renamedPictures = 0L;

    this.minimumSize     = minimumSize;

    this.listPictures( null, ( fileName==null ? "." : fileName ) );
  }


  /*****************************************************************************
   *
   * 列出当前目录下的文件列表,包括文件和文件夹
   *
   * Windows操作系统中,File类中关键是抽象类FileSystem,而FileSystem关键如下:
   *
   * public static native FileSystem getFileSystem();
   *
   * 实际返回的是子类Win32FileSystem
   *
   ****************************************************************************/
  private final void listPictures( File path, String fileName )
  {
    File file=new File( path, fileName );

    if( file.isDirectory() )
      {
        //得到当前目录下的文件列表,包括文件和文件夹
        String[] children=file.list();

        //如果子集为空,就放弃后面的操作
        if( children==null )
          {
            return;
          }

        //排序
        java.util.Arrays.sort( children );

        //如果子集不为空,则显示
        for( int i=0; i<children.length; i++ )
           {
             listPictures( file, children[i] );
           }
      }
    else if( file.isFile() )
           {
             String suffix=getPictureSuffix( file.getPath() );

             //当前文件是图片
             if( suffix.length()>3 )
               {
                 //图片总数增加
                 setTotalPictures();

                 logger( "/r/nProcess/t["+file.getPath()+"]", true );

                 String datetime=getPictureDateTime( file );

                 if( datetime.length()==19 )  //去掉这行,就可以按序列号重新排序命名
                   {
                     //解析出时间的图片总数增加
                     setParsedPictures();

                     //检查新文件名是否被占用,如果没被占用,就修改名称
                     checkAndRename( file, file.getParent()+File.separatorChar, file.getName(), datetime, suffix, 0 );
                   }
               }
           }
  }


  /*****************************************************************************
   *
   * 根据后缀名判断是否是有效的图片,并且返回小写的后缀名
   *
   * lastIndexOf()和substring()可以完成,但是我还想把后缀名小写,并且减少无谓循环
   *
   ****************************************************************************/
  private final String getPictureSuffix( String fileName )
  {
    if( fileName==null )
      {
        return "";
      }

    int pointer=fileName.length()-1;

    //可能存在“.jpg”这样的文件,即文件名只有4个字符
    if( pointer>2 )
      {
        char c=fileName.charAt( pointer-- );

        if( c=='g'||c=='G' )  //1
          {
            c=fileName.charAt( pointer-- );

            if( c=='p'||c=='P' )  //2
              {
                c=fileName.charAt( pointer-- );

                if( ( c=='j'||c=='J' )&&( pointer>-1 ) )  //3
                  {
                    c=fileName.charAt( pointer );

                    if( c=='.' )  //4
                      {
                        return ".jpg";
                      }
                  }
              }
            else if( c=='e'||c=='E' )  //2
                   {
                     c=fileName.charAt( pointer-- );

                     if( c=='p'||c=='P' )  //3
                       {
                         c=fileName.charAt( pointer-- );

                         if( ( c=='j'||c=='J' )&&( pointer>-1 ) )  //4
                           {
                             c=fileName.charAt( pointer );

                             if( c=='.' )  //5
                               {
                                 return ".jpeg";
                               }
                           }
                       }
                   }
          }
        else if( c=='e'||c=='E' )  //1
               {
                 c=fileName.charAt( pointer-- );

                 if( c=='p'||c=='P' )  //2
                   {
                     c=fileName.charAt( pointer-- );

                     if( ( c=='j'||c=='J' )&&( pointer>-1 ) )  //3
                       {
                         c=fileName.charAt( pointer );

                         if( c=='.' )  //4
                           {
                             return ".jpe";
                           }
                       }
                   }
               }
        else if( c=='f'||c=='F' )  //1
               {
                 c=fileName.charAt( pointer-- );

                 if( c=='i'||c=='I' )  //2
                   {
                     c=fileName.charAt( pointer-- );

                     if( c=='f'||c=='F' )  //3
                       {
                         c=fileName.charAt( pointer-- );

                         if( ( c=='j'||c=='J' )&&( pointer>-1 ) )  //4
                           {
                             c=fileName.charAt( pointer );

                             if( c=='.' )  //5
                               {
                                 return ".jfif";
                               }
                           }
                       }
                     else if( ( c=='t'||c=='T' )&&( pointer>-1 ) )  //3
                            {
                              c=fileName.charAt( pointer );

                              if( c=='.' )  //4
                                {
                                  return ".tif";
                                }
                            }
                   }
                 else if( c=='f'||c=='F' )  //2
                        {
                          c=fileName.charAt( pointer-- );

                          if( c=='i'||c=='I' )  //3
                            {
                              c=fileName.charAt( pointer-- );

                              if( ( c=='t'||c=='T' )&&( pointer>-1 ) )  //4
                                {
                                  c=fileName.charAt( pointer );

                                  if( c=='.' )  //5
                                    {
                                      return ".tiff";
                                    }
                                }
                            }
                        }
               }
      }

    return "";
  }


  /*****************************************************************************
   *
   * 解析出图片中存储的照相时间
   *
   *
   * 正常相片(Normal),时间格式如下:
   *
   * 2001:01:02 12:23:56[NULL]2001:01:02 12:23:56
   *
   * 一般都是400到800字节之间,极个别在200和1500左右
   *
   * 还有些相机(如HP PhotoSmart R607)竟然在3100左右
   *
   *
   * 特殊相片(Special),如富士系列相片FUJIFILM,时间格式如下:
   *
   * <exif:DateTimeOriginal>2006-12-24T13:55:42+08:00</exif:DateTimeOriginal>
   *
   * 一般在8000以内任意地方,非常臃长
   *
   *
   * 综上考虑,我不得不用10240来代替原来的2048
   *
   * 或者我可以使用分段方法,将出现最多的段放在前面,最少的段放后面,我需要统计
   *
   * 现在我遇到的最大的为16000,也就是从2500到16000都有,很少,所以忽略了
   *
   ****************************************************************************/
  private final String getPictureDateTime( File file )
  {
    try{ FileInputStream fis=new FileInputStream( file );

         int fileLength=fis.available();

         ///
         //
         // 由ACDSee剪裁生成的图片,仍然保留着详细的EXIF信息,哪怕图片只有一个像素大小,
         //
         // 但是这样的图片有意义吗?我觉得既然是数码相片,至少应该大于100KB。
         //
         // 当图片大小小于一定值时不予理会,当然你可以取消这样的限制
         //
         ///
         if( fileLength<minimumSize )
           {
             //及时关闭
             fis.close();

             logger( "Less than "+minimumSize+" bytes, Ignore.", false );

             return "";
           }

         ///
         //
         // 时间信息全在文件前10240字节内,我们一次读入,一个一个字节分析,
         //
         // 但是当需要读入的内容大于10240字节时,最好分批读入,每次2048或1024
         //
         ///
         byte[] buffer=new byte[10240];

         //为防止溢出,减去240
         int readLength=fis.read( buffer )-240;

         //及时关闭
         fis.close();

         if( readLength>0&&readLength<buffer.length )
           {
             ///
             //
             // 为速度,不准备转换成char,直接比较数字,如下:
             //
             //  NULL    0
             //
             //  -       45
             //
             //  0       48
             //  1       49
             //  2       50
             //  3       51
             //  4       52
             //  5       53
             //  6       54
             //  7       55
             //  8       56
             //  9       57
             //
             //  :       58
             //
             //  空格    32
             //
             //  T       84
             //  t       116
             //
             //  D       68
             //  d       100
             //
             //  O       79
             //  o       111
             //
             ///

             //找到有效时间的次数,我们使用第二次找到的时间
             int times=0;

             //为防止溢出,而且没必要搜索最初的100字节
             int n=100;

             //从当前位置向后偏移18,就是时间的最后一位,判断字符是否符合要求
             int offset18;

             while( n<readLength )
                  {
                    offset18=n+18;

                    //末尾是数字
                    if( buffer[offset18]>47&&buffer[offset18]<58 )
                      {
                        //必须是“19”或“20”开头,并且时钟与分钟、分钟与秒钟之间是“:”
                        if( ( buffer[n]==50&&buffer[n+1]==48||buffer[n]==49&&buffer[n+1]==57 )
                          &&( buffer[n+16]==58 )
                          &&( buffer[n+13]==58 ) )
                          {
                            //日期与时间分隔的是空格“ ”,并且年与月、月与日之间是“:”,也就是2006:06:06 06:06:06
                            if( buffer[n+10]==32&&buffer[n+7]==58&&buffer[n+4]==58 )
                              {
                                times++;

                                //Normal,两个时间在一起,或是第二次找到的时间
                                if( ( buffer[n+36]==58&&buffer[n+33]==58&&buffer[n+30]==32&&buffer[n+27]==58&&buffer[n+24]==58 )
                                  ||( times==2 ) )
                                  {
                                    return parseDateTime( buffer, n, 19 );
                                  }
                              }
                            //日期与时间分隔的是“T”或“t”,并且年与月、月与日之间是“-”,也就是2006-06-06T06:06:06
                            else if( ( buffer[n+10]==84||buffer[n+10]==116 )
                                   &&( buffer[n+7]==45 )
                                   &&( buffer[n+4]==45 ) )
                                   {
                                     times++;

                                     //Special,含有“DateTimeOriginal”,只判断“D”或“d”、“T”或“t”、“O”或“o”三个字符
                                     if( ( buffer[n-9]==79||buffer[n-9]==111 )
                                       &&( buffer[n-13]==84||buffer[n-13]==116 )
                                       &&( buffer[n-17]==68||buffer[n-17]==100 ) )
                                       {
                                         return parseDateTime( buffer, n, 19 );
                                       }
                                   }
                          }

                        //别忘了移位
                        n++;
                      }
                    //末尾是“:”,向后移动2位
                    else if( buffer[offset18]==58 )
                           {
                             /
                             //
                             // [     CANON  42  H        2006:12:24 12:33:08 2006:12:24 12:33:08     ]
                             //                                        |
                             // [  <exif:DateTimeOriginal>2006-12-24T12:33:08+08:00</exif:DateTimeOriginal>]
                             //                                        |
                             //                      0000:00:00 00:00:0A
                             //                                        |
                             //                      0000-00-00T00:00:0A
                             //                                        |
                             //                        0000:00:00 00:00:0A
                             //                                        |
                             //                        0000-00-00T00:00:0A
                             //
                             /
                             n+=2;
                           }
                    //末尾是空格“ ”、“T”、“t”,向后移动8位
                    else if( buffer[offset18]==32||buffer[offset18]==84||buffer[offset18]==116 )
                           {
                             /
                             //
                             // [     CANON  42  H        2006:12:24 12:33:08 2006:12:24 12:33:08     ]
                             //                                     |
                             // [  <exif:DateTimeOriginal>2006-12-24T12:33:08+08:00</exif:DateTimeOriginal>]
                             //                                     |
                             //                   0000:00:00 00:00:0A
                             //                                     |
                             //                   0000-00-00T00:00:0A
                             //                                     |
                             //                           0000:00:00 00:00:0A
                             //                                     |
                             //                           0000-00-00T00:00:0A
                             //
                             /
                             n+=8;
                           }
                    //末尾是“-”,向后移动11位
                    else if( buffer[offset18]==45 )
                           {
                             /
                             //
                             // [  <exif:DateTimeOriginal>2006-12-24T12:33:08+08:00</exif:DateTimeOriginal>]
                             //                               |
                             //             0000-00-00T00:00:0A
                             //                               |
                             //                        0000-00-00T00:00:0A
                             //
                             /
                             n+=11;
                           }
                         else{ //末尾不包含以上任何字符,整块向后移动19位

                               ///
                               //
                               // [      CANON  42  H        2   f light 24 F       55       ]
                               //                                |                  |
                               //              0000-00-00T00:00:0A                  |
                               //                                                   |
                               //                                 0000-00-00T00:00:0A
                               //
                               ///
                               n+=19;
                             }
                  }
           }
       }
    catch( FileNotFoundException fnfe )
         {
           fnfe.printStackTrace();
         }
    catch( IOException ioe )
         {
           ioe.printStackTrace();
         }
    catch( Exception e )
         {
           e.printStackTrace();
         }

    return "";
  }


  /*****************************************************************************
   *
   * 将已经定位好的日期时间提取出来
   *
   ****************************************************************************/
  private final String parseDateTime( byte[] buffer, int offset, int length )
  {
    if( ( buffer!=null )&&( offset>-1&&offset<buffer.length )&&( length>0&&length<buffer.length ) )
      {
        char[] parse=new char[length];

        byte b;

        for( int i=0; i<length; i++ )
           {
             b=buffer[offset+i];

             if( b>=48&&b<=57 )  //数字,没有检查日期合法性
               {
                 parse[i]=(char)b;  //由于此处始终大于零,所以不用考虑byte的正负符号位
               }
             else if( b==58 )  //由于“:”不能用于文件名,我们用“-”代替
                    {
                      parse[i]=this.PRI_SEPARATOR;
                    }
                  else{ parse[i]=this.SUB_SEPARATOR;  //NULL值或其他非数值用空格代替
                      }
           }

        return new String( parse );
      }

    return "";
  }


  /*****************************************************************************
   *
   * 检查新文件名称是否已被占用,如果没被占用,则修改名称
   *
   * Some bug here.
   *
   ****************************************************************************/
  private final void checkAndRename( File file, String path, String oldName, String prefix, String suffix, int number )
  {
    if( file!=null&&path!=null&&oldName!=null&&prefix!=null&&suffix!=null&&number>-1 )
      {
        String newName=( number>0 ? prefix+this.SUB_SEPARATOR+getNumberSequence( number )+suffix : prefix+suffix );

        //如果图片已经改好了,不需要再次修改
        if( newName.equals( oldName ) )
          {
            logger( "Rename/t["+newName+"]/tNo Need", false );

            return;
          }

        File newFile=new File( path+newName );

        if( newFile.exists() )  //已经存在同名但本质不同的图片
          {
            checkAndRename( file, path, oldName, prefix, suffix, number+1 );
          }
        else{ if( file.renameTo( newFile ) )
                {
                  //修改名称成功的图片总数增加
                  setRenamedPictures();

                  logger( "Rename/t["+newName+"]/tSucceed", false );
                }
              else{ logger( "Rename/t["+newName+"]/tFailed", false );
                  }
            }
      }
  }


  /*****************************************************************************
   *
   * 得到诸如001、002、012、569、999、0102、1345、4567、56789这样的数字序列
   *
   * 从1000开始递增,当小于2000时,提取千位后面的三位,组成000到999的数字序列
   *
   * 当递增大于等于2000时,取其减去1000的值,组成1000到99999的数字序列
   *
   ****************************************************************************/
  private final String getNumberSequence( int number )
  {
    if( number>-1&&number<100000 )  //十万左右的图片放一个文件夹?
      {
        int temp=number+1000;

        char[] buffer=new char[7];  //7在此足够了

        int pointer=6;  //buffer.length-1=7-1=6

        while( temp>0 )
             {
               buffer[pointer--]=(char)( temp%10+48 );  //48是'0'的ASCII码

               temp/=10;

               if( pointer==3 )  //到千位了(7-4=3),此时需要把人为增加的1000减去
                 {
                   temp--;
                 }
             }

        pointer++;

        return new String( buffer, pointer, 7-pointer );  //7为buffer的长度
      }

    return "";
  }


  /*****************************************************************************
   *
   * 总共的图片数,无须考虑同步
   *
   ****************************************************************************/
  private final void setTotalPictures()
  {
    this.totalPictures++;
  }

  public final long getTotalPictures()
  {
    return this.totalPictures;
  }

  /*****************************************************************************
   *
   * 解析出时间的图片数,无须考虑同步
   *
   ****************************************************************************/
  private final void setParsedPictures()
  {
    this.parsedPictures++;
  }

  public final long getParsedPictures()
  {
    return this.parsedPictures;
  }

  /*****************************************************************************
   *
   * 改名成功的图片数,无须考虑同步
   *
   ****************************************************************************/
  private final void setRenamedPictures()
  {
    this.renamedPictures++;
  }

  public final long getRenamedPictures()
  {
    return this.renamedPictures;
  }


  /*****************************************************************************
   *
   * 既向控制台显示,又向日志写入
   *
   ****************************************************************************/
  private final static void logger( String message, boolean both )
  {
    if( message!=null )
      {
        if( both )
          {
            System.out.println( message );
          }

        System.err.println( message );
      }
  }


  /*****************************************************************************
   *
   * 主函数入口
   *
   ****************************************************************************/
  public static void main( String[] args )
  {
    try{ //标准错误输出重新定向到日志文件
         System.setErr( new PrintStream( new FileOutputStream( "log.txt" ) ) );

         //程序总耗时统计
         long begin=System.currentTimeMillis();

         Rename2PictrueOriginalDateTime rpodt=new Rename2PictrueOriginalDateTime();

         logger( "/r/n共有图片:"+( rpodt.getTotalPictures() )+"张", true );
         logger( "/r/n有效图片:"+( rpodt.getParsedPictures() )+"张", true );
         logger( "/r/n修改成功:"+( rpodt.getRenamedPictures() )+"张", true );
         logger( "/r/n修改失败:"+( rpodt.getParsedPictures()-rpodt.getRenamedPictures() )+"张", true );

         logger( "/r/n总共耗时:"+( System.currentTimeMillis()-begin )+"毫秒", true );
       }
    catch( FileNotFoundException fnfe )
         {
           fnfe.printStackTrace();
         }
    catch( IOException ioe )
         {
           ioe.printStackTrace();
         }
    catch( Exception e )
         {
           e.printStackTrace();
         }
  }
}

 

 

///

 

以下是最新版本,修改了一些Bug,优化了速度:

 

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileNotFoundException;


/*******************************************************************************
 *
 * 读出数码图片中的拍照时间
 *
 *
 * 原理:
 *
 * 1、由于数码图片的拍照时间可以直接从图片中搜索到,所以本例不需要分析EXIF的格式。
 *
 * 2、正常的(Normal)数码图片,会有三处到四处的时间,其中第二、第三处固定在一起,
 *
 *    第二处是图片拍摄时间,第三处是图片存储时间,一般两者相同,他们之间用NULL值
 *
 *    分隔(ASCII值为0),如下:
 *
 *    2001:01:02 12:23:56[NULL]2001:01:02 12:23:56[NULL]
 *
 *    两者位置不定,但格式好查找,八个冒号,两个空格,两个NULL值,其余为数字。
 *
 *    不用考虑01:09:34被压缩为1:9:34这样的特例,见EXIF2.1规范关于时间的三段:
 *
 *    1. DateTime(第一处,修改时间)
 *
 *    The date and time of image creation. In this standard it is the date and time
 *
 *    the file was changed.
 *
 *    The format is "YYYY:MM:DD HH:MM:SS" with time shown in 24-hour format, and
 *
 *    the date and time separated by one blank character [20.H].
 *
 *    When the date and time are unknown, all the character spaces except colons (":")
 *
 *    may be filled with blank characters, or else the Interoperability field may be
 *
 *    filled with blank characters.
 *
 *    The character string length is 20 bytes including NULL for termination.
 *
 *    When the field is left blank, it is treated as unknown.
 *
 *      Tag = 306 (132.H)
 *
 *      Type = ASCII
 *
 *      Count = 20
 *
 *      Default = none
 *
 *    2. DateTimeOriginal(第二处,拍摄时间)
 *
 *    The date and time when the original image data was generated. For a DSC the
 *
 *    date and time the picture was taken are recorded.
 *
 *    The format is "YYYY:MM:DD HH:MM:SS" with time shown in 24-hour format, and
 *
 *    the date and time separated by one blank character [20.H].
 *
 *    When the date and time are unknown, all the character spaces except colons (":")
 *
 *    may be filled with blank characters, or else the Interoperability field may be
 *
 *    filled with blank characters.
 *
 *    The character string length is 20 bytes including NULL for termination.
 *
 *    When the field is left blank, it is treated as unknown.
 *
 *      Tag = 36867 (9003.H)
 *
 *      Type = ASCII
 *
 *      Count = 20
 *
 *      Default = none
 *
 *    3. DateTimeDigitized(第三处,存储时间)
 *
 *    The date and time when the image was stored as digital data. If, for example,
 *
 *    an image was captured by DSC and at the same time the file was recorded, then
 *
 *    the DateTimeOriginal and DateTimeDigitized will have the same contents.
 *
 *    The format is "YYYY:MM:DD HH:MM:SS" with time shown in 24-hour format, and
 *
 *    the date and time separated by one blank character [20.H].
 *
 *    When the date and time are unknown, all the character spaces except colons (":")
 *
 *    may be filled with blank characters, or else the Interoperability field may be
 *
 *    filled with blank characters.
 *
 *    The character string length is 20 bytes including NULL for termination.
 *
 *    When the field is left blank, it is treated as unknown.
 *
 *      Tag = 36868 (9004.H)
 *
 *      Type = ASCII
 *
 *      Count = 20
 *
 *      Default = none
 *
 * 3、特别的(Special)相机,如富士系列FUJIFILM,使用XML格式记录EXIF,时间格式如下:
 *
 *    <exif:DateTimeOriginal>2006-12-24T12:33:08+08:00</exif:DateTimeOriginal>
 *
 *    对比正常的(Normal)相片,我们发现两者格式很类似:
 *
 *    2006:12:24 12:33:08   Normal
 *
 *    2006-12-24T12:33:08   Special
 *
 *
 * 另外:
 *
 * 1、由于本人测试的*.tif和*.tiff图片是由*.jpg通过ACDSee转换的,所以可能不准。
 *
 * 2、当然,网上有现成的包可供利用,详细方法请自行搜索,以下是包下载地址:
 *
 *    http://www.drewnoakes.com/code/exif/releases/metadata-extractor-2.2.0.jar
 *
 * 3、此例没有按照标准的Java规范格式化。
 *
 * 4、开源的目的只有一个:我为人人,人人为我,欢迎开源!
 *
 *
 * Version: 1.10
 *
 * Author: NeedJava
 *
 * E-Mail: NeedJava@126.com
 *
 * Modified: 2007.08.16/2007.08.29/2007.09.28/2010.03.15
 *
 *
 * 你可以使用此程序于任何地方,但请保留程序作者及注释的完整。如果你改进了程序,
 *
 * 请在原作者后添加姓名,如:Author: NeedJava/Jack/Mike,版本及修改时间同理。
 *
 ******************************************************************************/
public final class PictureRenamer
{
    public static final int FILE_MIN_LENGTH = 102400;  //查找文件的最小字节数

    public static final int SEQUENCE_LENGTH = 3;  //默认图片序列号的长度

    private static final char[] SUFFIX_JPG  = { '.', 'j', 'p', 'g' };  //{ 46, 106, 112, 103 }

    private static final char[] SUFFIX_JPEG = { '.', 'j', 'p', 'e', 'g' };  //{ 46, 106, 112, 101, 103 }

    private static final char[] SUFFIX_JPE  = { '.', 'j', 'p', 'e' };  //{ 46, 106, 112, 101 }

    private static final char[] SUFFIX_JFIF = { '.', 'j', 'f', 'i', 'f' };  //{ 46, 106, 102, 105, 102 }

    private static final char[] SUFFIX_TIF  = { '.', 't', 'i', 'f' };  //{ 46, 116, 105, 102 }

    private static final char[] SUFFIX_TIFF = { '.', 't', 'i', 'f', 'f' };  //{ 46, 116, 105, 102, 102 }

    private static final char PRI_SEPARATOR = '-';  //45 主要分隔符

    private static final char SUB_SEPARATOR = ' ';  //32 次要分隔符

    private int minLength;  //处理图片的最小字节数

    private int sequenceLength;  //图片序列号的长度

    private int totalFolders;  //文件夹总数

    private int totalFiles;  //文件总数

    private int totalPictures;  //图片总数

    private int parsedPictures;  //含有有效时间的图片总数

    private int renamedPictures;  //成功修改名称的图片总数

    /***************************************************************************
     *
     * 构造函数,默认使用当前路径,不搜索100KB以下图片
     *
     * 由ACDSee剪裁生成的图片,仍然保留着详细的EXIF信息,哪怕图片只有一个像素,
     *
     * 但是这样的图片有意义吗?我觉得既然是数码相片,至少应该大于100KB。
     *
     * 当图片大小小于一定值时不予理会,当然你可以取消这样的限制
     *
     **************************************************************************/
    public PictureRenamer(){ this( FILE_MIN_LENGTH, SEQUENCE_LENGTH ); }

    public PictureRenamer( int minLength, int sequenceLength )
    {
        this.totalFolders    = 0;

        this.totalFiles      = 0;

        this.totalPictures   = 0;

        this.parsedPictures  = 0;

        this.renamedPictures = 0;

        this.minLength = ( minLength < 0 ? FILE_MIN_LENGTH : minLength );

        this.sequenceLength = ( sequenceLength < 0 ? SEQUENCE_LENGTH : sequenceLength );
    }

    /***************************************************************************
     *
     * 列出当前目录下的文件列表,包括文件和文件夹
     *
     * Windows操作系统中,File类中关键是抽象类FileSystem,而FileSystem关键如下:
     *
     * public static native FileSystem getFileSystem();
     *
     * 实际返回的是子类Win32FileSystem
     *
     **************************************************************************/
    public final void listPictures( File parent, String fileName ) throws FileNotFoundException, IOException
    {
        File file = new File( parent, fileName );

        if( file.isDirectory() )
        {
            totalFolders ++;

            String[] children = file.list(); if( children == null ){ return; }

            //java.util.Arrays.sort( children );  //没必要排序

            for( int i = 0; i < children.length; i ++ ){ listPictures( file, children[i] ); }
        }
        else
        {
            totalFiles ++;

            char[] suffix = getPictureSuffix( fileName ); if( suffix == null ){ return; }

            if( suffix.length > 0 )  //当前文件是图片
            {
                totalPictures ++;

                //logger( "/r/nProcess/t[" + file.getPath() + "]", false );

                //TODO:增加观察者,代替logger,使用线程wait和notify

                //TODO:判断是否是格式化过的图片,如果是,就退出,或者强制修改。XXXX:放弃这个,因为AcdSee处理的经常是错的

                long length = file.length(); if( length < minLength ){ /*logger( length + " is less than " + minLength + " bytes, Ignore.", false );*/ return; }

                char[] datetime = getPictureDateTime( file ); if( datetime == null ){ return; }  //fileName.substring( 0, fileName.length() - suffix.length ).toCharArray();

                if( datetime.length == 19/**/ )  //注释掉这行,就可以按序列号重新排序命名
                {
                    parsedPictures ++;

                    String newName = rename( parent, file, fileName, datetime, suffix, 0/*-1*/ );  //检查新文件名是否被占用,如果没被占用,就修改名称

                    if( newName == null ){ /*logger( "Rename/t[" + newName + "]/tFailed", false );*/ return; }

                    renamedPictures ++;

                    //logger( "Rename/t[" + newName + "]/tSucceed", false );
                }
            }
        }
    }

    /***************************************************************************
     *
     * 根据后缀名判断是否是有效的图片,并且返回小写的后缀名
     *
     * lastIndexOf()和substring()可以完成,但是我还想把后缀名小写,并且减少无谓循环
     *
     **************************************************************************/
    private final char[] getPictureSuffix( String fileName )
    {
        if( fileName == null ){ return null; }

        int pointer = fileName.length() - 1;

        if( pointer > 2 )  //可能存在“.jpg”这样的文件,即文件名只有4个字符
        {
            char c = fileName.charAt( pointer -- );

            if( c == 'g' || c == 'G' )  //1
            {
                c = fileName.charAt( pointer -- );

                if( c == 'p' || c == 'P' )  //2
                {
                    c = fileName.charAt( pointer -- );

                    if( ( c == 'j' || c == 'J' ) && ( pointer > -1 ) )  //3
                    {
                        if( fileName.charAt( pointer ) == '.' ){ return SUFFIX_JPG; }  //4
                    }
                }
                else if( c == 'e' || c == 'E' )  //2
                {
                    c = fileName.charAt( pointer -- );

                    if( c == 'p' || c == 'P' )  //3
                    {
                        c = fileName.charAt( pointer -- );

                        if( ( c == 'j' || c == 'J' ) && ( pointer > -1 ) )  //4
                        {
                            if( fileName.charAt( pointer ) == '.' ){ return SUFFIX_JPEG; }  //5
                        }
                    }
                }
            }
            else if( c == 'e' || c == 'E' )  //1
            {
                c = fileName.charAt( pointer -- );

                if( c == 'p' || c == 'P' )  //2
                {
                    c = fileName.charAt( pointer -- );

                    if( ( c == 'j' || c == 'J' ) && ( pointer > -1 ) )  //3
                    {
                        if( fileName.charAt( pointer ) == '.' ){ return SUFFIX_JPE; }  //4
                    }
                }
            }
            else if( c == 'f' || c == 'F' )  //1
            {
                c = fileName.charAt( pointer -- );

                if( c == 'i' || c == 'I' )  //2
                {
                    c = fileName.charAt( pointer -- );

                    if( c == 'f' || c == 'F' )  //3
                    {
                        c = fileName.charAt( pointer -- );

                        if( ( c == 'j' || c == 'J' ) && ( pointer > -1 ) )  //4
                        {
                            if( fileName.charAt( pointer ) == '.' ){ return SUFFIX_JFIF; }  //5
                        }
                    }
                    else if( ( c == 't' || c == 'T' ) && ( pointer > -1 ) )  //3
                    {
                        if( fileName.charAt( pointer ) == '.' ){ return SUFFIX_TIF; }  //4
                    }
                }
                else if( c == 'f' || c == 'F' )  //2
                {
                    c = fileName.charAt( pointer -- );

                    if( c == 'i' || c == 'I' )  //3
                    {
                        c = fileName.charAt( pointer -- );

                        if( ( c == 't' || c == 'T' ) && ( pointer > -1 ) )  //4
                        {
                            if( fileName.charAt( pointer ) == '.' ){ return SUFFIX_TIFF; }  //5
                        }
                    }
                }
            }
        }

        return null;
    }

    /***************************************************************************
     *
     * 解析出图片中存储的照相时间
     *
     *
     * 正常相片(Normal),时间格式如下:
     *
     * 2001:01:02 12:23:56[NULL]2001:01:02 12:23:56
     *
     * 一般都是400到800字节之间,极个别在200和1500左右
     *
     * 还有些相机(如HP PhotoSmart R607)竟然在3100左右
     *
     *
     * 特殊相片(Special),如富士系列相片FUJIFILM,时间格式如下:
     *
     * <exif:DateTimeOriginal>2006-12-24T13:55:42+08:00</exif:DateTimeOriginal>
     *
     * 一般在8000以内任意地方,非常臃长
     *
     *
     * 综上考虑,我不得不用10240来代替原来的2048
     *
     * 或者我可以使用分段方法,将出现最多的段放在前面,最少的段放后面,我需要统计
     *
     * 现在我遇到的最大的为16000,也就是从2500到16000都有,很少,所以忽略了
     *
     * 现在已经分段了
     *
     **************************************************************************/
    private final char[] getPictureDateTime( File file ) throws FileNotFoundException, IOException
    {
        FileInputStream fis = new FileInputStream( file );

       
        //
        // 时间信息全在文件前10240字节内,我们一次读入,一个一个字节分析,
        //
        // 但是当需要读入的内容大于10240字节时,最好分批读入,每次2048或1024
        //
       
        byte[] buffer = new byte[2048];

        int readLength = 0;

        int remain = 0;

        int n = 128;  //为防止溢出,而且没必要搜索最初的128字节

        int readTimes = 8;  //readTimes和buffer.length的乘积应当在10000至16000左右,保证只搜索图片前16000字节

        int foundTimes = 0;  //找到有效时间的次数,我们使用第二次找到的时间

        int foundOffset = 0;  //在哪找到的

        byte tailByte = 0;  //从当前位置向后偏移18,就是时间的最后一位,判断字符是否符合要求

        while( -- readTimes >= 0 && ( readLength = fis.read( buffer, remain, buffer.length - remain ) + remain ) >= 0 )
        {
           
            //
            // 为速度,不准备转换成char,直接比较数字,如下:
            //
            //  NULL    0
            //
            //  -       45
            //
            //  0       48
            //  1       49
            //  2       50
            //  3       51
            //  4       52
            //  5       53
            //  6       54
            //  7       55
            //  8       56
            //  9       57
            //
            //  :       58
            //
            //  空格    32
            //
            //  T       84
            //  t       116
            //
            //  D       68
            //  d       100
            //
            //  O       79
            //  o       111
            //
           

            while( ( remain = readLength - n ) > 0 )
            {
                if( remain <= 38 && n >= 19 )  //快要到末尾了,把剩余的字节复制到头部,我们重新开始
                {
                    System.arraycopy( buffer, n -= 19, buffer, 0, remain += 19 );

                    foundOffset += n; n = 0; break;
                }

                tailByte = buffer[n + 18];

                //末尾是数字
                if( tailByte > 47 && tailByte < 58 )
                {
                    //必须是数字开头,并且时钟与分钟、分钟与秒钟之间是“:”
                    if( buffer[n] > 47 && buffer[n] < 58 && buffer[n + 16] == 58 && buffer[n + 13] == 58 )
                    {
                        //日期与时间分隔的是空格“ ”,并且年与月、月与日之间是“:”,也就是2006:06:06 06:06:06
                        if( buffer[n + 10] == 32 && buffer[n + 7] == 58 && buffer[n + 4] == 58 )
                        {
                            foundTimes ++;

                            //Normal,两个时间在一起,或是第二次找到的时间
                            if( ( buffer[n + 36] == 58 && buffer[n + 33] == 58 && buffer[n + 30] == 32 && buffer[n + 27] == 58 && buffer[n + 24] == 58 ) || ( foundTimes == 2 ) )
                            {
                                foundOffset += n;

                                //System.err.println( foundOffset );

                                fis.close(); return parseDateTime( buffer, n, 19 );
                            }
                        }

                        //日期与时间分隔的是“T”或“t”,并且年与月、月与日之间是“-”,也就是2006-06-06T06:06:06
                        else if( ( buffer[n + 10] == 84 || buffer[n + 10] == 116 ) && buffer[n + 7] == 45 && buffer[n + 4] == 45 )
                        {
                            foundTimes ++;

                            //Special,含有“DateTimeOriginal”,只判断“D”或“d”、“T”或“t”、“O”或“o”三个字符
                            if( ( buffer[n - 9] == 79 || buffer[n - 9] == 111 ) && ( buffer[n - 13] == 84 || buffer[n - 13] == 116 ) && ( buffer[n - 17] == 68 || buffer[n - 17] == 100 ) )
                            {
                                foundOffset += n;

                                //System.err.println( foundOffset );

                                fis.close(); return parseDateTime( buffer, n, 19 );
                            }
                        }
                    }

                    //别忘了移位
                    n ++;
                }

                //末尾是“:”,向后移动2位
                else if( tailByte == 58 )
                {
                   
                    //
                    // [     CANON  42  H        2006:12:24 12:33:08 2006:12:24 12:33:08     ]
                    //                                        |
                    // [  <exif:DateTimeOriginal>2006-12-24T12:33:08+08:00</exif:DateTimeOriginal>]
                    //                                        |
                    //                      0000:00:00 00:00:0A
                    //                                        |
                    //                      0000-00-00T00:00:0A
                    //                                        |
                    //                        0000:00:00 00:00:0A
                    //                                        |
                    //                        0000-00-00T00:00:0A
                    //
                   
                    n += 2;
                }

                //末尾是空格“ ”、“T”、“t”,向后移动8位
                else if( tailByte == 32 || tailByte == 84 || tailByte == 116 )
                {
                   
                    //
                    // [     CANON  42  H        2006:12:24 12:33:08 2006:12:24 12:33:08     ]
                    //                                     |
                    // [  <exif:DateTimeOriginal>2006-12-24T12:33:08+08:00</exif:DateTimeOriginal>]
                    //                                     |
                    //                   0000:00:00 00:00:0A
                    //                                     |
                    //                   0000-00-00T00:00:0A
                    //                                     |
                    //                           0000:00:00 00:00:0A
                    //                                     |
                    //                           0000-00-00T00:00:0A
                    //
                   
                    n += 8;
                }

                //末尾是“-”,向后移动11位
                else if( tailByte == 45 )
                {
                   
                    //
                    // [  <exif:DateTimeOriginal>2006-12-24T12:33:08+08:00</exif:DateTimeOriginal>]
                    //                               |
                    //             0000-00-00T00:00:0A
                    //                               |
                    //                        0000-00-00T00:00:0A
                    //
                   
                    n += 11;
                }

                //末尾不包含以上任何字符,整块向后移动19位
                else
                {
                   
                    //
                    // [      CANON  42  H        2   f light 24 F       55       ]
                    //                                |                  |
                    //              0000-00-00T00:00:0A                  |
                    //                                                   |
                    //                                 0000-00-00T00:00:0A
                    //
                   
                    n += 19;
                }
            }
        }

        fis.close(); return null;
    }

    /***************************************************************************
     *
     * 将已经定位好的日期时间提取出来
     *
     **************************************************************************/
    private final char[] parseDateTime( byte[] buf, int off, int len )
    {
        if( buf == null || off < 0 || len < 0 || off + 1 > buf.length || off + len > buf.length ){ return null; }

        char[] array = new char[len];

        byte b;

        for( int i = 0; i < len; i ++ )
        {
            b = buf[off + i];

            if( b >= 48/*0*/ && b <= 57/*9*/ ){ array[i] = (char)b; }  //数字,没有检查日期合法性

            else if( b == 58/*:*/ ){ array[i] = PRI_SEPARATOR; }  //由于“:”不能用于文件名,我们用“-”代替

            else{ array[i] = SUB_SEPARATOR; }  //NULL值或其他非数值用空格代替
        }

        return array;
    }

    /***************************************************************************
     *
     * 检查新文件名称是否已被占用,如果没被占用,则修改名称
     *
     **************************************************************************/
    private final String rename( File parent, File file, String name, char[] prefix, char[] suffix, int number )
    {
        if( parent == null || file == null || prefix == null || suffix == null ){ return null; }

        char[] array = new char[64/**/];

       

        int pointer = array.length - suffix.length;

        System.arraycopy( suffix, 0, array, pointer, suffix.length );  //[                       .jpg]

        if( number > 0/*-1*/ )
        {
            pointer = writeNumberSequence( array, -- pointer, number );  //[                  001.jpg]

            array[pointer] = SUB_SEPARATOR/**/;  //[                   _001.jpg]
        }

        int temp = pointer = pointer - prefix.length;

        System.arraycopy( prefix, 0, array, pointer, prefix.length );  //[2008-01-01 01-01-01_001.jpg]

        //System.out.println( "Get new file name: " + new String( array, pointer, array.length - pointer ) );

       

        if( array.length - temp == name.length() )
        {
            for( int i = 0; temp < array.length; temp ++, i ++ )
            {
                //从pointer的点开始字符比较

                if( array[temp] != name.charAt( i ) ){ break; }
            }
        }

        if( temp == array.length ){ return null/**/; }  //如果图片已经改好了,不需要再次修改

       

        String newName = new String( array, pointer, array.length - pointer );

        File newFile = new File( parent, newName );

        if( newFile.exists() ){ return rename( parent, file, name, prefix, suffix, number + 1 ); }  //已经存在同名但本质不同的图片

        else if( file.renameTo( newFile ) ){ return newName; }  //改名成功,测试发现renameTo很耗时间

        return null;
    }

    /***************************************************************************
     *
     * 得到诸如001、002、012、569、999、0102、1345、4567、56789这样的数字序列
     *
     **************************************************************************/
    private final int writeNumberSequence( char[] array, int offset, int number )
    {
        if( array == null || offset < 0 || number < 0 ){ return offset; }

        int i = 0, pointer = offset, temp = number;

        for( ; i < sequenceLength; i ++, temp /= 10 )
        {
            array[pointer --] = (char)( temp % 10 + 48 );  //48是'0'的ASCII码
        }

        for( ; temp > 0; temp /= 10 )
        {
            array[pointer --] = (char)( temp % 10 + 48 );  //48是'0'的ASCII码
        }

        return pointer;
    }

    public final int getTotalFolders(){ return totalFolders; }

    public final int getTotalFiles(){ return totalFiles; }

    public final int getTotalPictures(){ return totalPictures; }

    public final int getParsedPictures(){ return parsedPictures; }

    public final int getRenamedPictures(){ return renamedPictures; }

    /***************************************************************************
     *
     * 既向控制台显示,又向日志写入
     *
     **************************************************************************/
    private final static void logger( String message, boolean both )
    {
        if( message == null || message.length() < 1 ){ return; }

        if( both ){ System.out.println( message ); }

        System.err.println( message );
    }

    public static void main( String[] args )
    {
        try
        {
            System.setErr( new PrintStream( new FileOutputStream( "log.txt" ) ) );

            long start = System.currentTimeMillis();

            PictureRenamer rpodt = new PictureRenamer( 10240, 3 );

            rpodt.listPictures( null, "." );

            logger( "/r/n共有文件夹:" + ( rpodt.getTotalFolders() ) + "个", true );

            logger( "/r/n共有文件:" + ( rpodt.getTotalFiles() ) + "个", true );

            logger( "/r/n共有图片:" + ( rpodt.getTotalPictures() ) + "张", true );

            logger( "/r/n有效图片:" + ( rpodt.getParsedPictures() ) + "张", true );

            logger( "/r/n修改成功:" + ( rpodt.getRenamedPictures() ) + "张", true );

            logger( "/r/n修改失败:" + ( rpodt.getParsedPictures() - rpodt.getRenamedPictures() ) + "张", true );

            logger( "/r/n总共耗时:" + ( System.currentTimeMillis() - start ) + "毫秒", true );
        }
        catch( FileNotFoundException fnfe ){ fnfe.printStackTrace(); }

        catch( IOException ioe ){ ioe.printStackTrace(); }

        catch( Exception e ){ e.printStackTrace(); }
    }
}

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值