同步照片的时候,发现照片名字不规则 。 以下类产生的格式 2014-04-20 09.44.43.jpg
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] 2001/01/02
* 12:23[NULL]2001/01/02 12:23[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,时间格式如下:
*
* 2006-12-24T12:33:08+08:00
*
* 对比正常的(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: [email protected]
*
* 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 PRI_SUB_SEPARATOR = '.'; // 32 次要分隔符
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,时间格式如下:
*
* 2006-12-24T13:55:42+08:00
*
* 一般在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
// / 47
// : 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 ]
// |
// [
// 2006-12-24T12:33:08+08:00]
// |
// 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 ]
// |
// [
// 2006-12-24T12:33:08+08:00]
// |
// 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) {
// //////////////////////////////////////////////////////////
//
// [
// 2006-12-24T12:33:08+08:00]
// |
// 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/* : */ && i <8) {
array[i] = PRI_SEPARATOR;
} // 由于“:”不能用于文件名,我们用“-”代替
else if (b == 58/* : */ && i > 2) {
array[i] = PRI_SUB_SEPARATOR;
}
else {
array[i] = SUB_SEPARATOR;
} // NULL值或其他非数值用空格代替
}
System.out.println(array);
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, "img");
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();
}
}
}