(view source code of capturedate.cs as plain text)
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
namespace RobvanderWoude{ class CaptureDate {static readonly string progver = "1.05";
#region Global Variablesstatic bool confirmsettimestamp = true;
static bool filedatereplacescapturedate = false;
static bool readabletimestamp = true;
static bool recursive = false;
static bool settimestamp = false;
static bool useexiftool = false;
static bool wildcards = false;
static double timediffthreshold = 3600;
static int renamecount = 0;
static string exiftool = string.Empty;
static string searchpattern = "*.*";
#endregion Global Variablesstatic int Main( string[] args )
{ #region Initialize Variablesstring startdir = Environment.CurrentDirectory; // in case no directory is specified
string[] filespec;
#endregion Initialize Variables #region Parse Command Lineif ( args.Length == 0 )
{return ShowHelp( );
}foreach ( string arg in args )
{if ( arg == "/?" || arg.Length < 2 )
{return ShowHelp( );
}if ( arg[0] == '/' )
{switch ( arg.Substring( 0, 2 ).ToUpper( ) )
{case "/D":
if ( arg.Length > 3 && arg[2] == ':' )
{if ( timediffthreshold != 3600 )
{return ShowHelp( "Duplicate switch: /D" );
} try {timediffthreshold = Convert.ToDouble( arg.Substring( 3 ) );
} catch {return ShowHelp( "Invalid value: \"{0}\"", arg );
} } else {return ShowHelp( "Invalid value: \"{0}\"", arg );
}break;
case "/F":
if ( filedatereplacescapturedate )
{return ShowHelp( "Duplicate switch: /F" );
}filedatereplacescapturedate = true;
break;
case "/R":
if ( recursive )
{return ShowHelp( "Duplicate switch: /R" );
}recursive = true;
break;
case "/S":
if ( settimestamp )
{return ShowHelp( "Duplicate switch: /S" );
}settimestamp = true;
break;
case "/T":
if ( !readabletimestamp )
{return ShowHelp( "Duplicate switch: /T" );
}readabletimestamp = false;
break;
case "/X":
if ( useexiftool )
{return ShowHelp( "Duplicate switch: /X" );
}useexiftool = true;
break;
case "/Y":
if ( !confirmsettimestamp )
{return ShowHelp( "Duplicate switch: /Y" );
}confirmsettimestamp = false;
break;
default:
return ShowHelp( "Invalid switch: \"{0}\"", arg );
} } else {searchpattern = Path.GetFileName( arg );
wildcards = ( searchpattern.IndexOfAny( "*?".ToCharArray( ) ) > -1 );
startdir = Directory.GetParent( arg ).FullName;
if ( string.IsNullOrEmpty( startdir ) && !Directory.Exists( startdir ) )
{return ShowHelp( "Folder not found: \"{0}\"", startdir );
}filespec = Directory.GetFiles( startdir, searchpattern );
} }if ( !confirmsettimestamp && !settimestamp )
{return ShowHelp( "/Y switch is valid only when combined with /S switch." );
}if ( timediffthreshold != 3600 && !settimestamp )
{return ShowHelp( "/D switch is valid only when combined with /S switch." );
}if ( useexiftool )
{string[] path = string.Format( "{0};{1}", startdir, Environment.GetEnvironmentVariable( "PATH" ) ).Split( ";".ToCharArray( ), StringSplitOptions.RemoveEmptyEntries );
foreach ( string dir in path )
{if ( string.IsNullOrWhiteSpace( exiftool ) && File.Exists( Path.Combine( dir, "exiftool.exe" ) ) )
{exiftool = Path.Combine( dir, "exiftool.exe" );
} }if ( string.IsNullOrWhiteSpace( exiftool ) )
{return ShowHelp( "ExifTool.exe not found, which makes /X switch invalid" );
} }if ( filedatereplacescapturedate && !( useexiftool && settimestamp ) )
{return ShowHelp( "/F switch is valid only when combined with /S and /X switches." );
} #endregion Parse Command LineProcessFolder( startdir );
return renamecount;
}static string GetTimestampEXIF( string filename, bool usefiletags = true )
{string photodatedelimited = string.Empty;
string pattern = "\\b[12]\\d{3}:[01]\\d:[0-3]\\d [0-2]\\d:[0-5]\\d:[0-5]\\d(\\b|\\+)";
Regex regexp = new Regex( pattern, RegexOptions.None );
// First try the standard EXIF tag (should return 1 timestamp)ProcessStartInfo si = new ProcessStartInfo( )
{ FileName = exiftool,Arguments = "-EXIF:DateTimeOriginal " + filename,
UseShellExecute = false,
RedirectStandardOutput = true,
};
Process proc = Process.Start( si );
proc.WaitForExit( );
string exif = proc.StandardOutput.ReadToEnd( );
MatchCollection matches = regexp.Matches( exif );
if ( matches.Count > 0 )
{photodatedelimited = matches[0].Value;
}else if ( usefiletags )
{ // If querying the standard EXIF tag fails, try the file related // EXIF tags (will return 3 timestamps, we need the oldest one)si = new ProcessStartInfo( )
{ FileName = exiftool,Arguments = "-FILE:* " + filename,
UseShellExecute = false,
RedirectStandardOutput = true,
};
proc = Process.Start( si );
proc.WaitForExit( );
exif = proc.StandardOutput.ReadToEnd( );
#region Find Earliest Date Stringregexp = new Regex( pattern, RegexOptions.None );
matches = regexp.Matches( exif );
if ( matches.Count == 0 )
{ShowHelp( "No capture date found in EXIF data" );
return String.Empty;
}foreach ( Match match in matches )
{int compare = string.Compare( photodatedelimited, match.Value, StringComparison.Ordinal );
if ( string.IsNullOrEmpty( photodatedelimited ) || compare > 0 )
{photodatedelimited = match.Value;
} } #endregion Find Earliest Date String }return photodatedelimited;
}static void ProcessFolder( string folder )
{string[] filespec = Directory.GetFiles( folder, searchpattern );
foreach ( string filename in filespec )
{if ( filedatereplacescapturedate )
{int rc = SetTimestampEXIF( filename );
if ( rc != 0 )
{ShowHelp( "Returncode {0} for \"{1}\"", rc.ToString( ), filename );
} } else {ProcessImage( filename );
} }if ( recursive )
{string[] subdirs = Directory.GetDirectories( folder );
foreach ( string subdir in subdirs )
{ProcessFolder( subdir );
} } }static void ProcessImage( string filename )
{ #region Read First MB of Filelong filesize = new FileInfo( filename ).Length;
int buffersize = Convert.ToInt32( Math.Min( filesize, 1048576 ) ); // Buffer size is 1 MB or file size, whichever is the smallest
StreamReader file = new StreamReader( filename );
char[] buffer = new Char[buffersize];
_ = file.Read( buffer, 0, buffersize - 1 ); // Read only the first 1 MB of the file (or the entire file if smaller than 1 MB)
file.Close( );
string header = new String( buffer );
if ( String.IsNullOrEmpty( header ) )
{ShowHelp( "Could not open file \"{0}\"", filename );
return;
} #endregion Read First MB of File #region Find Earliest Date Stringstring photodatedelimited = string.Empty;
string pattern = "\\b[12]\\d{3}:[01]\\d:[0-3]\\d [0-2]\\d:[0-5]\\d:[0-5]\\d\\b";
Regex regexp = new Regex( pattern, RegexOptions.None );
MatchCollection matches = regexp.Matches( header );
if ( matches.Count == 0 )
{if ( useexiftool )
{photodatedelimited = GetTimestampEXIF( filename );
} else {ShowHelp( "No capture date found in file header" );
return;
} } else {foreach ( Match match in matches )
{if ( string.IsNullOrEmpty( photodatedelimited ) || string.Compare( photodatedelimited, match.Value, StringComparison.Ordinal ) > 0 )
{photodatedelimited = match.Value;
} } }photodatedelimited = photodatedelimited.Substring( 0, 10 ).Replace( ':', '-' ) + photodatedelimited.Substring( 10 );
string photodatenodelims = photodatedelimited.Replace( ":", "" ).Replace( "-", "" ).Replace( " ", "T" );
#endregion Find Earliest Date String #region Set File Timestampif ( settimestamp )
{string timeformat;
if ( readabletimestamp )
{timeformat = "{0:yyyy}-{0:MM}-{0:dd} {0:HH}:{0:mm}:{0:ss}";
} else {timeformat = "{0:yyyy}{0:MM}{0:dd}T{0:HH}{0:mm}{0:ss}";
}DateTime currenttimestamp = File.GetLastWriteTime( filename );
if ( DateTime.TryParse( photodatedelimited, out DateTime newtimestamp ) ) // Try parsing the new file timestamp using the capture timestamp string
{if ( DateTime.Compare( currenttimestamp, newtimestamp ) == 0 ) // File and capture timestamps are equal
{string photodate = photodatenodelims;
if ( readabletimestamp )
{photodate = photodatedelimited;
}if ( wildcards )
{Console.WriteLine( "{0}\t\"{1}\"", photodate, filename );
} else {Console.WriteLine( photodate );
} } else {double timediff = Math.Abs( ( currenttimestamp - newtimestamp ).TotalSeconds ); // Calculate absolute value of timestamps' difference in seconds
if ( timediff > timediffthreshold ) // Ignore time differences up to threshold (default 1 hour, or value set with /D switch)
{string blanks = "\n" + new String( ' ', 70 ) + new String( '\b', 70 ); // String to erase the first 70 characters on the next line
Console.WriteLine( "Image file name : {0}", filename );
Console.WriteLine( "Current file timestamp : " + timeformat, currenttimestamp );
Console.WriteLine( "Capture timestamp : " + timeformat, newtimestamp );
if ( confirmsettimestamp )
{Console.Write( "Do you want to change the file's timestamp to the capture time? [yN] " );
string answer = Console.ReadKey( ).KeyChar.ToString( ).ToUpper( );
if ( answer == "Y" )
{File.SetLastWriteTime( filename, newtimestamp );
Console.CursorTop -= 1; // Move the cursor 1 line up
Console.WriteLine( blanks + "New file timestamp : " + timeformat, File.GetLastWriteTime( filename ) ); // Overwrite prompt with new timestamp
renamecount += 1;
} else {Console.WriteLine( "\nskipping . . ." );
} } else {File.SetLastWriteTime( filename, newtimestamp );
Console.CursorTop -= 1; // Move the cursor 1 line up
Console.WriteLine( blanks + "New file timestamp : " + timeformat, File.GetLastWriteTime( filename ) ); // Overwrite prompt with new timestamp
renamecount += 1;
} } else {Console.WriteLine( photodatedelimited ); // Timespans' difference is not above threshold (default 1 hour, or value set with /D switch)
} } } else {ShowHelp( "Could not determine timestamp of \"{0}\"", filename );
return;
} } else {Console.WriteLine( "{0}\t\"{1}\"", photodatedelimited, filename );
} #endregion Set File Timestamp }static int SetTimestampEXIF( string filename )
{DateTime filetimestamp = File.GetLastWriteTime( filename );
string capturetimestamp = GetTimestampEXIF( filename, false );
string timeformat;
if ( readabletimestamp )
{timeformat = "{0:yyyy}-{0:MM}-{0:dd} {0:HH}:{0:mm}:{0:ss}";
if ( capturetimestamp.Length > 10 )
{capturetimestamp = capturetimestamp.Substring( 0, 10 ).Replace( ':', '-' ) + capturetimestamp.Substring( 10 );
} } else {timeformat = "{0:yyyy}{0:MM}{0:dd}T{0:HH}{0:mm}{0:ss}";
if ( capturetimestamp.Length > 10 )
{capturetimestamp = capturetimestamp.Replace( ":", "" ).Replace( " ", "T" );
} }Console.WriteLine( "Image file name : {0}", filename );
Console.WriteLine( "Current capture timestamp : {0}", capturetimestamp );
Console.WriteLine( "Current file timestamp : " + timeformat, filetimestamp );
if ( capturetimestamp == string.Format( timeformat, filetimestamp ) )
{return 0;
}string blanks = "\n" + new String( ' ', 70 ) + new String( '\b', 70 ); // String to erase the first 70 characters on the next line
if ( confirmsettimestamp )
{Console.Write( "Do you want to change the file's capture time to its file timestamp? [yN] " );
string answer = Console.ReadKey( ).KeyChar.ToString( ).ToUpper( );
if ( answer == "N" )
{Console.WriteLine( "\nskipping . . ." );
return -1;
} else {Console.WriteLine( );
} }ProcessStartInfo si = new ProcessStartInfo( )
{ FileName = exiftool,Arguments = string.Format( "-EXIF:DateTimeOriginal=\"{0:yyyy}:{0:MM}:{0:dd} {0:HH}:{0:mm}:{0:ss}\" \"{1}\"", filetimestamp, filename ),
UseShellExecute = false,
RedirectStandardOutput = true,
};
Process proc = Process.Start( si );
proc.WaitForExit( );
// restore file timestampif ( proc.ExitCode == 0 )
{File.SetLastWriteTime( filename, filetimestamp );
if ( confirmsettimestamp )
{capturetimestamp = GetTimestampEXIF( filename, false );
if ( readabletimestamp )
{capturetimestamp = capturetimestamp.Substring( 0, 10 ).Replace( ':', '-' ) + capturetimestamp.Substring( 10 );
} else {capturetimestamp = capturetimestamp.Replace( ":", "" ).Replace( " ", "T" );
}Console.CursorTop -= 1; // Move the cursor 1 line up
Console.WriteLine( blanks + "New capture timestamp : {0}", capturetimestamp ); // Overwrite prompt with new timestamp
}renamecount += 1;
}return proc.ExitCode;
}static string Today( bool usedelims = true )
{string dateformat;
if ( usedelims )
{dateformat = "{0:yyyy}-{0:MM}-{0:dd} {0:HH}:{0:mm}:{0:ss}";
} else {dateformat = "{0:yyyy}{0:MM}{0:dd}T{0:HH}{0:mm}{0:ss}";
}return String.Format( dateformat, DateTime.Now );
} #region Error handlingpublic static int ShowHelp( params string[] errmsg )
{ #region Error Messageif ( errmsg.Length > 0 )
{List<string> errargs = new List<string>( errmsg );
errargs.RemoveAt( 0 );
Console.Error.WriteLine( );
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.Write( "ERROR:\t" );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.WriteLine( errmsg[0], errargs.ToArray( ) );
Console.ResetColor( );
} #endregion Error Message #region Help Text /* CaptureDate, Version 1.05 Return the capture date and time for the specified image file Usage: CAPTUREDATE image [ options ] Where: image specifies the image file(s) (wildcards allowed) Options: /D:seconds minimum Difference in seconds between current file timestamp and capture date/time; if the difference exceeds the specified number of seconds, the file timestamp will be set to the capture date/time (default: 3600 seconds = 1 hour; requires /S switch) /F set capture date/time to current File timestamp (requires /S and /X switches) /R Recursive (include subdirectories); you probably want to use wildcards for image with this option /S Set the image file timestamp to the capture date/time /T return the timestamp without "-" and ":" delimiters, e.g. 20171114T135628 instead of 2017-11-14 13:56:28 /X use eXiftool by Phil Harvey (https://exiftool.org/; requires exiftool.exe in the current directory or in a directory listed in the PATH) /Y do not ask for confirmation before changing the image file's timestamp (requires /S switch) Notes: Result will be displayed on screen, e.g. 2017-11-14 13:56:28. The date/time is extracted by searching for the earliest date/time string in the first 1048576 bytes (1 MB) of the image file. If no capture date/time is found that way, and the /X switch is used, exiftool.exe is used to try and read the capture date/time from the image's EXIF data. If this also fails, you can use the /F switch to set a new capture date/time in EXIF equal to the file date/time. With /S switch used, the timestamp is changed only if the difference between the current timestamp and the capture time exceeds 1 hour or the threshold set with the /D switch. The program will ask for confirmation before changing the file's timestamp, unless the /Y switch is used. Return code ("errorlevel") is -1 in case of errors, or with /S it equals the number of files renamed, or 0 otherwise. Written by Rob van der Woude http://www.robvanderwoude.com */ #endregion Help Text #region Display Helpstring timestampDelimited = Today( true );
string timestampNodelims = Today( false );
Console.Error.WriteLine( );
Console.Error.WriteLine( "CaptureDate, Version {0}", progver );
Console.Error.WriteLine( "Return the capture date and time for the specified image file" );
Console.Error.WriteLine( );
Console.Error.Write( "Usage: " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.WriteLine( "CAPTUREDATE image [ options ]" );
Console.ResetColor( );
Console.Error.WriteLine( );
Console.Error.Write( "Where: " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( "image" );
Console.ResetColor( );
Console.Error.WriteLine( " specifies the image file(s) (wildcards allowed)" );
Console.Error.WriteLine( );
Console.Error.Write( "Options: " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( "/D:seconds" );
Console.ResetColor( );
Console.Error.Write( " minimum " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( "D" );
Console.ResetColor( );
Console.Error.Write( "ifference in " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( "seconds" );
Console.ResetColor( );
Console.Error.WriteLine( " between current file" );
Console.Error.WriteLine( " timestamp and capture date/time; if the difference" );
Console.Error.WriteLine( " exceeds the specified number of seconds, the file" );
Console.Error.WriteLine( " timestamp will be set to the capture date/time" );
Console.Error.Write( " (default: 3600 seconds = 1 hour; requires " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( "/S" );
Console.ResetColor( );
Console.Error.WriteLine( " switch)" );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( " /F" );
Console.ResetColor( );
Console.Error.Write( " set capture date/time to current " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( "F" );
Console.ResetColor( );
Console.Error.WriteLine( "ile timestamp" );
Console.Error.Write( " (requires " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( "/S" );
Console.ResetColor( );
Console.Error.Write( " and " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( "/X" );
Console.ResetColor( );
Console.Error.WriteLine( " switches)" );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( " /R R" );
Console.ResetColor( );
Console.Error.WriteLine( "ecursive (include subdirectories); you probably want" );
Console.Error.Write( " to use wildcards for " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( "image" );
Console.ResetColor( );
Console.Error.WriteLine( " with this option" );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( " /S S" );
Console.ResetColor( );
Console.Error.WriteLine( "et the image file's timestamp to the capture date/time" );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( " /T" );
Console.ResetColor( );
Console.Error.WriteLine( " return the timestamp without \"-\" and \":\" delimiters," );
Console.Error.Write( " e.g. " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( timestampNodelims );
Console.ResetColor( );
Console.Error.Write( " instead of " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( timestampDelimited );
Console.ResetColor( );
Console.Error.WriteLine( "." );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( " /X" );
Console.ResetColor( );
Console.Error.Write( " use e" );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( "X" );
Console.ResetColor( );
Console.Error.Write( "iftool by Phil Harvey (" );
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.Error.Write( "https://exiftool.org/" );
Console.ResetColor( );
Console.Error.WriteLine( ";" );
Console.Error.WriteLine( " requires exiftool.exe in the current directory or in" );
Console.Error.WriteLine( " a directory listed in the PATH)" );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( " /Y" );
Console.ResetColor( );
Console.Error.WriteLine( " do not ask for confirmation before changing the image" );
Console.Error.Write( " file's timestamp (requires " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( "/S" );
Console.ResetColor( );
Console.Error.WriteLine( " switch)" );
Console.Error.WriteLine( );
Console.Error.Write( "Notes: Result will be displayed on screen, e.g. " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( timestampDelimited );
Console.ResetColor( );
Console.Error.WriteLine( "." );
Console.Error.WriteLine( " The date/time is extracted by searching for the earliest date/time" );
Console.Error.WriteLine( " in the first 1048576 bytes (1 MB) of the image file. If no" );
Console.Error.Write( " capture date/time is found that way, and the " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( "/X" );
Console.ResetColor( );
Console.Error.WriteLine( " switch is used," );
Console.Error.WriteLine( " exiftool.exe is used to try and read the capture date/time from the" );
Console.Error.Write( " image's EXIF data. If this also fails, you can use the " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( "/F" );
Console.ResetColor( );
Console.Error.WriteLine( " switch to" );
Console.Error.Write( " set a " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( "new" );
Console.ResetColor( );
Console.Error.WriteLine( " capture date/time in EXIF equal to the file date/time." );
Console.Error.Write( " With " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( "/S" );
Console.ResetColor( );
Console.Error.WriteLine( " switch used, the timestamp is changed only if the difference" );
Console.Error.WriteLine( " between the current timestamp and the capture time exceeds 1 hour or" );
Console.Error.Write( " the threshold set with the " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( "/D" );
Console.ResetColor( );
Console.Error.WriteLine( " switch." );
Console.Error.WriteLine( " The program will ask for confirmation before changing the file's" );
Console.Error.Write( " timestamp, unless the " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( "/Y" );
Console.ResetColor( );
Console.Error.WriteLine( " switch is used." );
Console.Error.Write( " Return code (\"errorlevel\") is -1 in case of errors, or with " );
Console.ForegroundColor = ConsoleColor.White;
Console.Error.Write( "/S" );
Console.ResetColor( );
Console.Error.WriteLine( " it" );
Console.Error.WriteLine( " equals the number of files renamed, or 0 otherwise." );
Console.Error.WriteLine( );
Console.Error.WriteLine( "Written by Rob van der Woude" );
Console.Error.WriteLine( "http://www.robvanderwoude.com" );
#endregion Display Helpreturn -1;
} #endregion Error handling }}page last modified: 2025-10-11; loaded in 0.0165 seconds