一直使用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(); }
}
}