65.9K
CodeProject 正在变化。 阅读更多。
Home

照片和其他文件的日期和时间批量修改器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (30投票s)

2014年7月13日

CPOL

8分钟阅读

viewsIcon

34101

downloadIcon

1403

如何开发一个工具来调整多个文件、照片或视频错误的日期和时间

背景

我最近在查找一张重要事件的照片和视频时,不明白为什么找不到任何视频文件,尽管我记得我的妻子和我女儿既拍了照片也录了视频……

我当时意识到,我们的数码相机(Nikon D5000)和摄像机(Sony)的时间都设置错了,而且错的时间还不一样……一个早了 7 小时 36 分钟,另一个晚了 3 小时。这就是造成混乱的原因,我花了大量时间检查备份等,以为我珍贵的照片文件不知怎么被删除了。我计算了视频和照片文件的正确时间,调整了它们的时间戳,并很高兴地发现它们都发生在同一时间段内,一切都正常。然后,我将摄像机和相机的时钟设置回正确时间,并寻找一种方法来修复我照片和视频文件不正确的时间戳。这时我决定自己编程开发这样一个工具……

引言

虽然我即将介绍的程序可以在很多场合派上用场,但最初开发它的目的是为了调整照片和视频错误的时间戳,这些错误是由摄像机或相机设置不正确或时区造成的。其思路是定义以下几项:

  • 路径 - 搜索位置(会弹出一个路径对话框,用户可以在其中输入或选择开始搜索的路径)。例如:c:\c:\users\myuser\documents\
  • 查询 - 搜索内容(例如,所有以 .AVI 结尾的文件,或特定日期内的所有文件,或文件名中包含特定字符串的文件)。
  • 要应用日期相关属性 - 可以是
    • 创建日期
    • 最后修改日期
    • 最后访问日期
    • 拍摄日期
  • 请求的更改 - 可以是
    • 固定的日期和时间,例如:2014.7.7 01:00
    • 相对更改当前戳上的日期和时间,例如:提前 7 个半小时2014 年 12 月 12 日 12:31 AM 将调整为:2014 年 12 月 11 日 5:01 PM

构建块

根据给定标准搜索文件

我们的应用程序允许您指定搜索文件的扩展名、文件名(或其中一部分),还可以仅处理具有特定日期/时间戳的文件。我将在下一节详细介绍文件具有的各种日期/时间相关属性,但为了本文的简洁性,我们的程序会一次性更改所有这些属性。使用一个 Combobox 控件允许用户在上述选项中进行选择。

// Initialize combo box for search query
    m_cmbQuery.AddString(TYPE_CERTAIN_DATE);
    m_cmbQuery.AddString(TYPE_CONTAIN_STRING);
    m_cmbQuery.AddString(TYPE_FILE_TYPE);
    m_cmbQuery.AddString(TYPE_ALL_FILES);
    m_cmbQuery.SetCurSel(0);

更改文件的日期/时间相关属性

有几个属性与我们的目标相关,其中包括

  • 创建日期 - 文件首次创建的日期和时间
  • 修改日期 - 文件最后修改的日期和时间
  • 访问日期 - 文件最后访问的日期和时间

对于照片,还有一个重要的属性:拍摄日期。这是照片拍摄的日期和时间。

为方便本文论述,“照片”以其扩展名识别,包括 .jpg.nef(Nokia)照片,但这当然可以并且应该得到扩展。

// IsPhoto - returns TRUE if the file extension indicates it is a photo file.
BOOL CChangeFileTimeDlg::IsPhoto(CString sFile)
{
    int len_fname=sFile.GetLength();
    
    return ((len_fname > 3 && (_T(".jpg") == sFile.MakeLower().Right(4)))
        || (len_fname > 4 && (_T(".jpeg") == sFile.MakeLower().Right(5)))
        || (len_fname > 3 && (_T(".nef") == sFile.MakeLower().Right(4))));  
}

拍摄日期属性

拍摄日期属性作为 Windows 资源管理器允许选择的可选列之一出现。此属性仅对照片有效。与其他日期相关的文件属性不同,此属性是从图像文件的 EXIF(Exchange Image File 格式)中获取的。

为了读取和操作文件的 EXIF 信息,我使用了 Davide Pizzolato 编写的 exif.cppexif.h,他的工作是基于 Matthias Wandel 的 jhead-1.8。

当文件被识别为照片时,会调用 getTakenXap 方法。

BOOL CChangeFileTimeDlg::getTakenXap(CString strName, CStringA& sTaken)
{
    CFile file (strName, CFile::modeReadWrite);
    
    byte buf[4096];
    bool bEnd = false;
    int nRet;
    int nRead;
    int nPos = 0;
    char szTaken[20];
    while (!bEnd)
    {
        memset(buf, 0, sizeof(buf));
        nRead = file.Read(buf, sizeof(buf));
        if ( nRead != 4096 )
            bEnd = true;
        nRet = findTag(buf,nRead,"x?p:CreateDate", nPos);
        if ( nRet == 0 ) //not found
        {
        }

        if ( nRet == 1 )// find tag
            break;
        
        if ( nRet == 2 )// find tag and require 
        {
            file.Seek(-100, CFile::current);
        }
    }

    if ( nRet == 1 )
    {
        
        file.Seek(nPos - nRead+1, CFile::current);
        file.Read(szTaken, 20);
        szTaken[19] = 0;
        sTaken= szTaken;
        file.Close();
        return TRUE;
    }
    
    file.Close();
    return FALSE;
}

然后使用当前的“拍摄日期”属性值来调用 parseXapTime

void CChangeFileTimeDlg::parseXapTime(CStringA sOrgTaken, CTime& dtTaken)
{
    CString sVal = CA2W(sOrgTaken);
    sVal.Replace(_T("-"), _T(" "));
    sVal.Replace(_T(":"), _T(" "));
    sVal.Replace(_T("T"), _T(" "));

    CArray<CString,CString> v;
    CString field;
    int index = 0;
    
    while (AfxExtractSubString(field,sVal,index,_T(' ')))
    {
        v.Add(field);
        ++index;
    }
    CTime dtTime(_ttoi(v[0]), _ttoi(v[1]), _ttoi(v[2]), _ttoi(v[3]), _ttoi(v[4]), _ttoi(v[5]) );
    dtTaken = dtTime;
}

计算日期和时间差

如果您查看像 这个网站 这样的网站,可以检查代码中包含的可能性,例如,更改文件的日期/时间戳,使其显示为提前 7 个半小时。

要在应用程序中执行此类计算,我们可以使用 CTime 来存储时间,并使用 CTimeSpan 来进行计算。

CTime

CTime 类用于保存绝对时间和日期。

Microsoft 提供了 7 种不同的 CTime 类构造函数,其中一些可以执行以下操作:

  1. 使用标准库 time_t 日历时间创建时间类
  2. 使用 DOS 日期和时间创建时间类
  3. 使用 Win32 SYSTEMTIMEFILETIME 创建时间类
  4. 使用单独的年、月、日、小时、分钟和秒值创建时间类

通过整合 ANSI time_t 数据类型,CTime 类提供了上面第 1 部分讨论的所有功能。它还有方法以 SYSTEMTIMEFILETIME 或 GMT 格式获取时间。

此外,此类还重载了 +, -, = , ==, <, <<, >> 运算符,提供了更多有用的功能。

您可以在 afx.h 头文件中找到 CTime 类的定义,如下所示:

class CTime
{
public:

// Constructors
    static CTime PASCAL GetCurrentTime();

    CTime();
    CTime(time_t time);
    CTime(int nYear, int nMonth, int nDay, int nHour, int nMin, int nSec,
        int nDST = -1);
    CTime(WORD wDosDate, WORD wDosTime, int nDST = -1);
    CTime(const CTime& timeSrc);

    CTime(const SYSTEMTIME& sysTime, int nDST = -1);
    CTime(const FILETIME& fileTime, int nDST = -1);
    const CTime& operator=(const CTime& timeSrc);
    const CTime& operator=(time_t t);

// Attributes
    struct tm* GetGmtTm(struct tm* ptm = NULL) const;
    struct tm* GetLocalTm(struct tm* ptm = NULL) const;
    BOOL GetAsSystemTime(SYSTEMTIME& timeDest) const;

    time_t GetTime() const;
    int GetYear() const;
    int GetMonth() const;       // month of year (1 = Jan)
    int GetDay() const;         // day of month
    int GetHour() const;
    int GetMinute() const;
    int GetSecond() const;
    int GetDayOfWeek() const;   // 1=Sun, 2=Mon, ..., 7=Sat

// Operations
    // time math
    CTimeSpan operator-(CTime time) const;
    CTime operator-(CTimeSpan timeSpan) const;
    CTime operator+(CTimeSpan timeSpan) const;
    const CTime& operator+=(CTimeSpan timeSpan);
    const CTime& operator-=(CTimeSpan timeSpan);
    BOOL operator==(CTime time) const;
    BOOL operator!=(CTime time) const;
    BOOL operator<(CTime time) const;
    BOOL operator>(CTime time) const;
    BOOL operator<=(CTime time) const;
    BOOL operator>=(CTime time) const;

    // formatting using "C" strftime
    CString Format(LPCTSTR pFormat) const;
    CString FormatGmt(LPCTSTR pFormat) const;
    CString Format(UINT nFormatID) const;
    CString FormatGmt(UINT nFormatID) const;

#ifdef _UNICODE
    // for compatibility with MFC 3.x
    CString Format(LPCSTR pFormat) const;
    CString FormatGmt(LPCSTR pFormat) const;
#endif

    // serialization
#ifdef _DEBUG
    friend CDumpContext& AFXAPI operator<<(CDumpContext& dc, CTime time);
#endif
    friend CArchive& AFXAPI operator<<(CArchive& ar, CTime time);
    friend CArchive& AFXAPI operator>>(CArchive& ar, CTime& rtime);

private:
    time_t m_time;
};

CTimeSpan

CTimeSpan 类与 CTime 结合使用以执行减法和加法。顾名思义,它代表一个相对时间跨度,并提供了四个构造函数,其中一个整合了 ANSI time_t 数据类型。CTimeSpan 也在 afx.h 中定义,如下所示:

class CTimeSpan
{
public:

// Constructors
    CTimeSpan();
    CTimeSpan(time_t time);
    CTimeSpan(LONG lDays, int nHours, int nMins, int nSecs);

    CTimeSpan(const CTimeSpan& timeSpanSrc);
    const CTimeSpan& operator=(const CTimeSpan& timeSpanSrc);

// Attributes
    // extract parts
    LONG GetDays() const;   // total # of days
    LONG GetTotalHours() const;
    int GetHours() const;
    LONG GetTotalMinutes() const;
    int GetMinutes() const;
    LONG GetTotalSeconds() const;
    int GetSeconds() const;

// Operations
    // time math
    CTimeSpan operator-(CTimeSpan timeSpan) const;
    CTimeSpan operator+(CTimeSpan timeSpan) const;
    const CTimeSpan& operator+=(CTimeSpan timeSpan);
    const CTimeSpan& operator-=(CTimeSpan timeSpan);
    BOOL operator==(CTimeSpan timeSpan) const;
    BOOL operator!=(CTimeSpan timeSpan) const;
    BOOL operator<(CTimeSpan timeSpan) const;
    BOOL operator>(CTimeSpan timeSpan) const;
    BOOL operator<=(CTimeSpan timeSpan) const;
    BOOL operator>=(CTimeSpan timeSpan) const;

#ifdef _UNICODE
    // for compatibility with MFC 3.x
    CString Format(LPCSTR pFormat) const;
#endif
    CString Format(LPCTSTR pFormat) const;
    CString Format(UINT nID) const;

    // serialization
#ifdef _DEBUG
    friend CDumpContext& AFXAPI operator<<(CDumpContext& dc,CTimeSpan timeSpan);
#endif
    friend CArchive& AFXAPI operator<<(CArchive& ar, CTimeSpan timeSpan);
    friend CArchive& AFXAPI operator>>(CArchive& ar, CTimeSpan& rtimeSpan);

private:
    time_t m_timeSpan;
    friend class CTime;
};

流程

首先,您选择搜索条件,和/或要开始的文件夹……

或者,文件可以直接拖放到对话框中。

注意:在此版本中,一次只能拖放一个文件,但这将在后续版本中修复。

为方便本文论述,我创建了一个名为“test”的文件夹,并将许多文件和文件夹复制到其中。这些文件既有照片(.jpg),也有非照片(.txt)。

在根据搜索条件找到文件,或者选择或拖放文件后,过程就开始了。每个文件都会被检查并更改其日期/时间属性。

  • 如果请求了特定的日期和时间,更改将考虑本地时区以及是否开启夏令时。
BOOL CChangeFileTimeDlg::SetStatus(CString sFile, CFileStatus& status)
{
    HANDLE hFile;        // File name without full path
    CString sFilePath = sFile;
    if ( sFile.Right(2) == _T("\\*"))
        sFilePath.Delete(sFilePath.GetLength()-1, 1); // Generate file name from full path

    // Open file 
    hFile = CreateFile(sFilePath, GENERIC_READ|GENERIC_WRITE,
                       FILE_SHARE_READ|FILE_SHARE_WRITE, NULL,
        OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
     if(hFile == INVALID_HANDLE_VALUE)
         return FALSE;

     SYSTEMTIME stCreate, stModify, stAccess;
     FILETIME ftCreate, ftModify, ftAccess;
     FILETIME ftCreate2, ftModify2, ftAccess2;

     // Taking into account the time zone and day light saving
     TIME_ZONE_INFORMATION tzi;
     DWORD dwRet = GetTimeZoneInformation(&tzi);
     
     CTime ctCreate(status.m_ctime.GetTime() - tzi.DaylightBias*60);
     CTime ctModify(status.m_mtime.GetTime() - tzi.DaylightBias*60);
     CTime ctAccess(status.m_atime.GetTime() - tzi.DaylightBias*60);

     ctCreate.GetAsSystemTime(stCreate);
     ctModify.GetAsSystemTime(stModify);
     ctAccess.GetAsSystemTime(stAccess);
     
     SystemTimeToFileTime(&stCreate, &ftCreate);
     SystemTimeToFileTime(&stModify, &ftModify);
     SystemTimeToFileTime(&stAccess, &ftAccess);
     

     LocalFileTimeToFileTime(&ftCreate, &ftCreate2);
     LocalFileTimeToFileTime(&ftModify, &ftModify2);
     LocalFileTimeToFileTime(&ftAccess, &ftAccess2);

     if ( !SetFileTime(hFile,&ftCreate2,&ftAccess2,&ftModify2) )
     {
         // Failed to change date/time for file
         CloseHandle(hFile);
         return FALSE;
     }
     // Success
     CloseHandle(hFile);
    return TRUE;
}
  • 一个“撤销”按钮允许恢复任何更改。
  • 已发生更改的日志显示在屏幕上,以及处理前后的日期/时间。
  • 用户可以通过复选框选择要更改的日期。
  • 这些日期包括“拍摄日期”属性,该属性对于照片(如 .jpg)和相机特定文件(如 .NEF 文件(尼康相机)等)是独一无二的。这是通过访问图形文件的 EXIF 来实现的。

用户界面

作为我努力使我的小型应用程序用户友好且易于使用的部分,我做了以下工作:

  • 保留上次输入的值

    由于用户有两种输入方式:固定日期/时间,以及相对时间(小时数),这可以通过将 Combobox 设置为“相对日期”或“固定日期”来指示,重要的是当用户在这两者之间切换时,会显示上次输入的值。例如,如果您输入了固定日期“2000/01/01”,然后输入了 8:30 作为相对日期,当您再次选择“固定日期”时,应该会显示上次的值“2000/01/01”,而切换回相对日期时,也应该显示上次的相对值“8:30”。

  • 允许灵活的数据输入

    应允许输入以下固定日期:

    • 2000/01/01 00:00:00
    • 2000/01/01 00:00
    • 2000/01/01

应允许以多种方式输入相对时间:

  • 10(表示向前推迟 10 小时)
  • -5(表示提前 5 小时)
  • 10:30(表示向前推迟 10.5 小时)

等等...

例如,请参阅以下用于解析请求的固定日期和时间的函数。

// ParseRelativeTime - Parses the requested fixed date and time from user's input
bool CChangeFileTimeDlg::ParseFixedDateTime(CString sTime, CTime& dtTime)
{
    CString sVal = sTime;
    sVal.Replace(_T("/"), _T(" "));
    sVal.Replace(_T(":"), _T(" "));
    CArray<CString,CString> v;
    CString field;
    int index = 0;
    while (AfxExtractSubString(field,sVal,index,_T(' ')))
    {
        v.Add(field);
        ++index;
    }
    // This part is to make things easier and a bit more user friendly
    if (v.GetCount() == 5) // Only date was entered 
                           // with an hour and minutes but without seconds
    {
        v.Add((CString)(L"00"));    // Seconds
        ++index;
    }
    else
    if (v.GetCount() == 4) // Only date was entered 
                           // with an hour but without minutes and seconds
    {
        v.Add((CString)(L"00"));    // Minutes
        v.Add((CString)(L"00"));    // Seconds
        index += 2;
    }
    else
    if (v.GetCount() == 3) // Only date was entered without any time
    {
        v.Add((CString)(L"00"));    // Hours
        v.Add((CString)(L"00"));    // Minutes
        v.Add((CString)(L"00"));    // Seconds
        index += 3;
    }
    if ( v.GetCount() != 6 )        // I give up, date/time entered incorrectly
    {
        return false;
    }

    CTime dtCertain(_ttoi(v[0]), _ttoi(v[1]), _ttoi(v[2]), _ttoi(v[3]), 
                                 _ttoi(v[4]), _ttoi(v[5]));
    dtTime = dtCertain;
}
  • 错误处理

    如果出现错误,例如文件被锁定,该特定文件的日志条目将被标记为“失败”,然后继续处理。

代码签名

由于我参与大型项目,我的软件事业从 Verisign 购买了代码签名证书(每年 499 美元,也适用于内核驱动程序)。

我使用 Commodo 的 kSign 工具来签名可执行文件。

对可执行文件进行签名和不签名的区别可以解释为客户在尝试下载未签名可执行文件时会收到的警告。

还有

但如果您的可执行文件已签名,用户将收到此消息:

哪一个更好。获得 Verisign 证书意味着您的身份(或贵公司的身份)已得到全面验证。

最终注释

感谢 Aha-Soft 提供演示应用程序的图标。版权所有 © 2000-2014 Aha-Soft

如果您发现错误,请随时将修改后的源代码发送给我。 :)

历史

  • 2014 年 7 月 13 日:初始版本
© . All rights reserved.