Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / MFC

Date & Time Batch Changer for Photos and Other Files

4.88/5 (30 votes)
13 Jul 2014CPOL8 min read 33.5K   1.4K  
How to develop a tool that will adjust mistaken date and time of multiple files, photos or videos
In this article, you will create an application that allows you to either state the extension of files searched for, the name (or part of it) and also process only files having a certain date/time stamp.

Background

I recently looked for photos and videos of an important event and couldn't understand why I can't find any video files, even though I recalled that my wife and my daughter took both photos and videos...

I then realized that our still camera (Nikon D5000) and camcorder (Sony) are both set up with the wrong time, each with a different wrong time... One was 7 hours and 36 minutes earlier and the other was 3 hours later. That was the reason for the confusion, and I spent a lot of time checking backups, etc. thinking my precious files were somehow deleted. I calculated the correct times of both video and photo files, adjusting their time stamp and happily found out that both occurred within the same time frame, so everything was OK. I then setup the time of the camcorder and the camera to the correct time and looked for a way to fix the incorrect time stamps of my photos and video files. That is when I decided to program such a tool myself...

Introduction

Even though there can be many occasions in which a program like the one I am about to introduce, can be useful, I originally developed it for the purpose of adjusting wrong time stamp of photos and videos, which are a result of incorrect settings or time zone in the camcorder or camera. The idea is to define the following:

  • Path - where to look (will bring a path dialog box where the user can type or choose the start path to search). For example: c:\ or c:\users\myuser\documents\
  • Query - what to look for (for example, all files ending with .AVI, or all files within a certain date, or files which contain a certain string in their name).
  • Date related attributes to apply to - which can be either:
    • Date Created
    • Date Last Modified
    • Date Last Accessed
    • Date Taken
  • Requested change - which can be either:
    • Fixed date and time, for example: 7.7.2014 01:00
    • Relative change of currently stamped date and time, for example: 7 and a half hours earlier (Dec 12, 2014 12:31AM will be adjusted to: Dec 11, 2014 5:01PM)

The Building Blocks

Searching for Files Based on a Given Criteria

Our application allows you to either state the extension of files searched for, the name (or part of it) but also to process only files having a certain date/time stamp. I will elaborate about the various attributes files have, which are date/time related in the next section, however, for the simplicity of this article, our program changes all of these attributes at once. A Combobox is used to allow the user to make the selection among the above options.

C++
// 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);

Changing a File's Date/time Related Attributes

There are several attributes which are relevant for our goal, among them:

  • Date Created - The date and time in which the file was first created
  • Date Modified - The date and time in which the file was last modified
  • Date Accessed - The date and time in which the file was last accessed

For photos, there is another important attribute: Date Taken. That is the date and time a photo was taken.

For the purpose of the article, "photos" are identified by their extension and include .jpg and .nef (Nokia) photos, but that of course can and should be enhanced.

C++
// 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))));  
}

The Date Taken Property

The Date Taken property appears as one of the optional columns Windows Explorer allows to choose. This property is only valid for photos. Unlike the other date related file properties, this one is taken from the EXIF (Exchange Image File format) of the image file.

In order to read and manipulate the EXIF of the file, I have used exif.cpp and exif.h written by Davide Pizzolato, which based his work on jhead-1.8 by Matthias Wandel.

When a file is identified as a photo, getTakenXap is called:

C++
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;
}

Then the current "Date Taken" attribute value is used to call parseXapTime.

C++
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;
}

Calculating Date and Time Differences

If you look at web sites like this one, you can check the possibilities covered in the code, for example, changing the date/time stamp of a file so it will show 7 and a half hours backward.

To do such calculations from an application, we can use CTime for storing the time, and CTimeSpan for the calculations.

CTime

The CTime class is used to hold an absolute time and date.

Microsoft provides 7 different constructors for the CTime class which amongst others, allows you to do the following:

  1. Create a time class using a Standard Library time_t calender time
  2. Create a time class using a dos date and time
  3. Create a time class using a Win32 SYSTEMTIME or FILETIME
  4. Create a time class using individual entries for year, month, day, hour, minute, and second

By incorporating the ANSI time_t data type, the CTime class provides all the functionalities discussed above in section 1. It also has methods to get the time in SYSTEMTIME or FILETIME or GMT format.

In addition, this class also overloads the +, -, = , ==, <, <<, >> operators to provide many more useful features.

You can find the definition of CTime class in afx.h header file and it is as follows:

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

The CTimeSpan class is used in conjunction with CTime to perform subtractions and additions. It represents a relative time span as the name suggests and provides four constructors, one of which incorporates the ANSI time_t data type. CTimeSpan is also defined in afx.h as follows:

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;
};

The Process

First, you select the search criteria, and / or a folder where you wish to start...

Image 1

Alternatively, files can be just dragged and dropped to the dialog box.

Note: In this version, only one file can be dragged, but that will be fixed later.

For the purpose of this article, I have created a folder named "test" and copied there many files, and folders. These files are both photos (.jpg) and non-photos (.txt).

After the files are found based on the search criteria, selected or dragged and dropped, the process starts. Each file is checked and its date/time attributes are changed.

  • If a certain date and time are requested, the change takes into consideration the local time zone and whether day light savings is on or off.
C++
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;
}
  • An "Undo" button allows reversing any change.
  • The log of the changes that have taken place is displayed on screen, along with the date/time before and after the process.
  • The user can check via checkboxes which dates should be changed.
  • The dates include the "Date Taken" attribute which is unique for photos (such as .jpg and camera specific files such as .NEF files (Nikon camera), etc. That is done via accessing the EXIF of the graphic file.

Image 2

User Interface

As part of my efforts to make my small application user friendly and easy to use, I have done the following:

  • Keeping last entered values

    Since there are two types of input from the user: a fixed date / time and a relative time (number of hours), which are indicated by setting a Combobox to either "Relative Date" or "Fixed Date", it is important that when the user switches between the two, the last value entered will be shown. For example, if you entered the fixed date "2000/01/01" and then entered 8:30 as a Relative Date, when you select "Fixed Date" again, the last value "2000/01/01" should be shown, and when switching back to Relative Date, the last relative value, "8:30" should be shown as well.

  • Allowing flexible data entry

    It should be possible to enter the following as a fixed date:

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

It should be possible to enter a relative time in various ways:

  • 10 (means 10 hours forward)
  • -5 (means 5 hours backward)
  • 10:30 (means 10 and a half hours forward)

and so on...

For example, see the following function for parsing the requested fixed date and time.

C++
// 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;
}
  • Error handling

    In case there is an error, such as a file being locked, the log entry of this specific file is marked as "Failed" and the process continues.

Code Signing

As I am involved with large scale projects, my software venture purchases a Code Signing Certificate from Verisign (they cost $499 a year, and are suitable also for Kernel drivers).

To sign an executable, I use a tool named kSign by Commodo.

Image 3

The difference between signing your executables and not signing them can be explained by the warning your customer will get when trying to download a non signed executable.

Image 4

and also:

Image 5

But if your executable is signed, the user will get this message:

Image 6

Which is better. Obtaining a Verisign certificate means that your identity (or your company's identity) are fully verified.

Final Notes

Thanks to Aha-Soft for the icon used for the demo application. Copyright © 2000-2014 Aha-Soft

If you find bugs, please feel free to send me the revised source code. :)

History

  • 13th July, 2014: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)