Skip to content

Instantly share code, notes, and snippets.

@jd-boyd
Last active April 7, 2025 23:04
Show Gist options
  • Save jd-boyd/cc47f5c74ee9070dc882250a427c406f to your computer and use it in GitHub Desktop.
Save jd-boyd/cc47f5c74ee9070dc882250a427c406f to your computer and use it in GitHub Desktop.
Update JPEG file creation dates based on Exif Dates, in Powershell
# 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