Last active
April 7, 2025 23:04
-
-
Save jd-boyd/cc47f5c74ee9070dc882250a427c406f to your computer and use it in GitHub Desktop.
Update JPEG file creation dates based on Exif Dates, in Powershell
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Example usage: | |
# . .\UpdateJpegCreationTimes.ps1 | |
# Update-JpegCreationTimes -FolderPath "C:\Photos" -Recursive -WhatIf | |
# Remove -WhatIf to actually make the changes | |
function Convert-StringToDateTime { | |
param ( | |
[Parameter(Mandatory=$true)] | |
[string]$DateTimeString | |
) | |
try { | |
# Remove any hidden Unicode characters and normalize spaces | |
$CleanString = $DateTimeString -replace '[^\x20-\x7E]', '' | |
# Normalize spaces in the date portion (remove extra spaces between components) | |
$CleanString = $CleanString -replace '(\d+)\s*/\s*(\d+)\s*/\s*(\d+)', '$1/$2/$3' | |
# Normalize spaces between date and time and around the time | |
$CleanString = $CleanString -replace '\s+', ' ' | |
# Make sure there's exactly one space before AM/PM | |
$CleanString = $CleanString -replace '\s+(AM|PM)', ' $1' | |
Write-Verbose "Normalized string: '$CleanString'" | |
# Try to parse using the standard format | |
try { | |
$DateTime = [DateTime]::ParseExact( | |
$CleanString, | |
"MM/dd/yyyy h:mm tt", | |
[System.Globalization.CultureInfo]::InvariantCulture | |
) | |
return $DateTime | |
} | |
catch { | |
# If standard format fails, try more flexible parsing | |
Write-Verbose "Standard parsing failed, trying flexible parsing..." | |
# Extract date and time components using regex | |
if ($CleanString -match '(\d+)/(\d+)/(\d+)\s+(\d+):(\d+)\s+(AM|PM)') { | |
$month = [int]$Matches[1] | |
$day = [int]$Matches[2] | |
$year = [int]$Matches[3] | |
$hour = [int]$Matches[4] | |
$minute = [int]$Matches[5] | |
$ampm = $Matches[6] | |
# Adjust hour for PM | |
if ($ampm -eq "PM" -and $hour -ne 12) { | |
$hour += 12 | |
} | |
if ($ampm -eq "AM" -and $hour -eq 12) { | |
$hour = 0 | |
} | |
# Create DateTime object | |
$DateTime = New-Object DateTime $year, $month, $day, $hour, $minute, 0 | |
return $DateTime | |
} | |
else { | |
throw "Could not parse date time components from string: $CleanString" | |
} | |
} | |
} | |
catch { | |
Write-Error "Failed to parse date time string: '$DateTimeString'. Error: $_" | |
return $null | |
} | |
} | |
# Example usage with problem string | |
# $dateStr = "2/ 26/ 2022 9:46 PM" | |
# $dateTime = Convert-StringToDateTime -DateTimeString $dateStr | |
# Write-Output "Parsed DateTime: $dateTime" | |
function Get-DateTaken { | |
param ( | |
[Parameter(Mandatory=$true)] | |
[string]$FilePath | |
) | |
try { | |
# Get file object | |
$file = Get-Item $FilePath -ErrorAction Stop | |
# Create COM objects | |
$shellObject = New-Object -ComObject Shell.Application | |
$directoryObject = $shellObject.NameSpace($file.Directory.FullName) | |
$fileObject = $directoryObject.ParseName($file.Name) | |
# Find the index of the "Date taken" property | |
$property = 'Date taken' | |
$index = 5 # Start from index 5 as in the original code | |
while ($directoryObject.GetDetailsOf($directoryObject.Items, $index) -ne $property) { | |
++$index | |
# Safety check to avoid infinite loop | |
if ($index -gt 300) { | |
Write-Error "Property '$property' not found in file details" | |
return $null | |
} | |
} | |
# Get the value | |
$value = $directoryObject.GetDetailsOf($fileObject, $index) | |
# Release COM objects | |
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($shellObject) | Out-Null | |
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($directoryObject) | Out-Null | |
return $value | |
} | |
catch { | |
Write-Error "Failed to get date taken from file: $FilePath. Error: $_" | |
return $null | |
} | |
finally { | |
# Force garbage collection to clean up COM objects | |
[System.GC]::Collect() | |
[System.GC]::WaitForPendingFinalizers() | |
} | |
} | |
function Get-DateTakenAsDateTime { | |
param ( | |
[Parameter(Mandatory=$true)] | |
[string]$FilePath | |
) | |
try { | |
# Get the date taken string | |
$dateTakenString = Get-DateTaken -FilePath $FilePath | |
if ([string]::IsNullOrEmpty($dateTakenString)) { | |
Write-Warning "No 'Date taken' metadata found for file: $FilePath" | |
return $null | |
} | |
# Convert the date string to DateTime object | |
$dateTime = Convert-StringToDateTime -DateTimeString $dateTakenString | |
return $dateTime | |
} | |
catch { | |
Write-Error "Error processing file: $FilePath. Error: $_" | |
return $null | |
} | |
} | |
# Script to update creation times for all JPEG files in a directory | |
function Update-JpegCreationTimes { | |
param ( | |
[Parameter(Mandatory=$true)] | |
[string]$FolderPath, | |
[Parameter(Mandatory=$false)] | |
[switch]$Recursive, | |
[Parameter(Mandatory=$false)] | |
[switch]$WhatIf | |
) | |
# Get all JPEG files in the specified folder | |
$searchOption = if ($Recursive) { "Recurse" } else { "TopDirectoryOnly" } | |
$jpegFiles = Get-ChildItem -Path $FolderPath -Include "*.jpg","*.jpeg","*.JPG","*.JPEG" -File -Recurse:$Recursive | |
$totalFiles = $jpegFiles.Count | |
$processedFiles = 0 | |
$updatedFiles = 0 | |
$errorFiles = 0 | |
Write-Output "Found $totalFiles JPEG files to process." | |
foreach ($file in $jpegFiles) { | |
$processedFiles++ | |
Write-Progress -Activity "Updating creation times" -Status "Processing file $processedFiles of $totalFiles" -PercentComplete (($processedFiles / $totalFiles) * 100) | |
try { | |
# Get the date taken from the file | |
$dateTaken = Get-DateTakenAsDateTime -FilePath $file.FullName | |
if ($dateTaken) { | |
# Update file creation time if date taken was found | |
if ($WhatIf) { | |
Write-Output "WhatIf: Would update $($file.Name) creation time to $($dateTaken.ToString('yyyy-MM-dd HH:mm:ss'))" | |
} else { | |
$file.CreationTime = $dateTaken | |
Write-Output "Updated $($file.Name) creation time to $($dateTaken.ToString('yyyy-MM-dd HH:mm:ss'))" | |
} | |
$updatedFiles++ | |
} else { | |
Write-Warning "Skipping $($file.Name) - No valid date taken found" | |
$errorFiles++ | |
} | |
} catch { | |
Write-Error "Error processing $($file.Name): $_" | |
$errorFiles++ | |
} | |
# Force garbage collection every 10 files to prevent memory issues with COM objects | |
if ($processedFiles % 10 -eq 0) { | |
[System.GC]::Collect() | |
[System.GC]::WaitForPendingFinalizers() | |
} | |
} | |
Write-Output "Processing completed." | |
Write-Output "Total files processed: $processedFiles" | |
Write-Output "Files updated: $updatedFiles" | |
Write-Output "Files skipped/errors: $errorFiles" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment