(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 Variables
static 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 Variables
static int Main( string[] args )
{
#region Initialize Variables
string startdir = Environment.CurrentDirectory; // in case no directory is specified
string[] filespec;
#endregion Initialize Variables
#region Parse Command Line
if ( 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 Line
ProcessFolder( 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 String
regexp = 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 File
long 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 String
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 );
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 Timestamp
if ( 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 timestamp
if ( 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 handling
public static int ShowHelp( params string[] errmsg )
{
#region Error Message
if ( 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 Help
string 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 Help
return -1;
}
#endregion Error handling
}
}
page last modified: 2024-04-16; loaded in 0.0180 seconds