Scripts/GFK.Image.ExifTool.psm1
#Requires -PSEdition Core #Requires -Module GFK.Image Set-StrictMode -Version Latest class ImageMetadata { [string] $FilePath [PSObject] $Tags } function Get-ImageMetadata { <# .SYNOPSIS Uses ExifTool for getting tags or shortcut tags by tag names .DESCRIPTION This command outputs metadata tags for one or multiple files The input path can be a folder and can contain wildcards .EXAMPLE There are two modes (parameter sets) in which this command can run: PS C:\> Get-ImageMetadata -FilePath 'C:\Users\Gordon Freeman\Pictures\Black Mesa Research Center.jpg' -TagName Artist,CreateDate The command returns one string per tag (including those not found in the file, as empty strings) as an array, except if: - the path resolves to more than one file - one of the tags used is a shortcut tag Additionnally, tag values that are just a single caret '-' will be returned as empty strings Choose this if you want the fastest mode to use with fully defined single files and without shortcut tags (You could actually work with more than one file is you splice the resulting array. This would theoretically be faster than running the command multiple times) PS C:\> Get-ImageMetadata -FilePath 'C:\Users\Gordon Freeman\Pictures\Black Mesa Research Center.jpg' -TagName Artist,CreateDate -Full In full mode, the command returns one ImageMetadata object per file found (each contains the file path and a Tags property) Tags not found on the file will not be in the output at all (even as empty strings) The Tags property of each ImageMetadata object contains the top level groups, each of which contains the properties for the tags found There will be no file or tag collisions in this mode Choose this if you want to handle multiple files and want an exhaustive report on the tags available on the file .NOTES - You can show the actual ExifTool command with the -Verbose switch - Recursion is available as a switch. The option will have no effect if the path is not a directory - An optional ExifTool configuration can be specified as a parameter #> [CmdletBinding(PositionalBinding = $false, DefaultParameterSetName = 'Basic')] [OutputType([string[]], ParameterSetName = 'Basic')] [OutputType([ImageMetadata[]], ParameterSetName = 'Full')] Param( [SupportsWildcards()] [Parameter(Mandatory, ValueFromPipeline)] [string] $FilePath, [Parameter(Mandatory)] [string[]] $TagNames, [Parameter(ParameterSetName = 'Full')] [switch] $Full, [switch] $Recurse, [string] $ConfigurationPath ) Begin { Test-ExifTool } Process { if ($ConfigurationPath) { $arguments = @('-config', $ConfigurationPath) } else { $arguments = @() } if ($Full) { $arguments += '-j', '-G', '-a' } else { $arguments += '-s3', '-f' } if ($Recurse) { $arguments += '-r' } $arguments = @($arguments; $TagNames | Get-TagNameArgument; '-c', '%+.6f', '--', $FilePath) Write-Verbose "exiftool $(($arguments | Foreach-Object { "'$($_ -replace "'","''")'" }) -join ' ')" $toolResults = &exiftool $arguments if ($Full) { return ConvertFrom-Json ($toolResults -join "`n") | New-ImageMetadata } else { return $toolResults | ConvertFrom-ImageValue } } } function Set-ImageMetadata() { <# .SYNOPSIS Uses ExifTool for setting tags or shortcut tags .DESCRIPTION This command writes metadata tags to one or multiple files The input path can be a folder and can contain wildcards Arrays will be concatenated with ';' .EXAMPLE Set-ImageMetadata ` -FilePath 'C:\Users\Gordon Freeman\Pictures\Black Mesa Research Center.jpg' ` -Tags @{ Artist = 'Gordon Freeman','Adrian Shephard'; '-XMP-xmp:MetadataDate' = Get-Date } .NOTES - You can show the actual ExifTool command with the -Verbose switch, and show only the command without running it with -Verbose -WhatIf - An optional ExifTool configuration can be specified as a parameter. - Shortcut tag values cannot be set with the `=` operator and need to be set with the `<` operator. In turn, the `<` operator does not play well with constant values when they contain special characters such as `$`. Therefore to support both constant and non-constant values on tags and on shortcut tags, we use the '-userParam' option. #> [CmdletBinding(SupportsShouldProcess, PositionalBinding = $false)] Param( [SupportsWildcards()] [Parameter(Mandatory, ValueFromPipeline)] [string]$FilePath, [Parameter(Mandatory)] [hashtable] $Tags, [switch] $Recurse, [string] $ConfigurationPath ) Begin { Test-ExifTool } Process { if ($ConfigurationPath) { $arguments = @('-config', $ConfigurationPath) } else { $arguments = @() } $arguments += '-overwrite_original' $index = 0 foreach ($tagName in $Tags.Keys) { $tagNameArgument = Get-TagNameArgument -TagName $tagName $paramName = "Param$(($index++))" $tagValue = ConvertTo-ImageValue -TagValue $Tags[$tagName] $arguments += "$tagNameArgument<`$$paramName", '-userParam', "$paramName=`"$tagValue`"" } $arguments += '--', $FilePath Write-Verbose "exiftool $(($arguments | Foreach-Object { "'$($_ -replace "'","''")'" }) -join ' ')" if ($PSCmdlet.ShouldProcess($FilePath)) { &exiftool $arguments } } } function ConvertFrom-ImageDateTime { <# .SYNOPSIS Converts a metadata date/time or date+time into a [datetime] object .DESCRIPTION Relies on ExifTool's default formats for such fields. Supports EXIF (naive full date), IPTC (date + naive or local time), XMP (naive or local full date) .EXAMPLE ConvertFrom-ImageDateTime -Date '2022:01:19' -Time '15:16:17' ConvertFrom-ImageDateTime -DateTime '2022:01:19 15:16:17+03:00' ConvertFrom-ImageDateTime -DateTime (Get-ImageMetadata $filePath XMP:CreateDate) #> [CmdletBinding(PositionalBinding = $false)] [OutputType([datetime])] Param ( [Parameter(Mandatory, ParameterSetName = 'OneField', ValueFromPipelineByPropertyName)] [string]$DateTime, [Parameter(Mandatory, ParameterSetName = 'TwoFields', ValueFromPipelineByPropertyName)] [string]$Date, [Parameter(Mandatory, ParameterSetName = 'TwoFields', ValueFromPipelineByPropertyName)] [string]$Time ) Process { if ($PsCmdlet.ParameterSetName -EQ 'TwoFields') { $DateTime = "$Date $Time" } $formats = 'yyyy:MM:dd HH:mm:ss', 'yyyy:MM:dd HH:mm:sszzz' return [datetime]::ParseExact($DateTime, [string[]] $formats, [System.Globalization.CultureInfo]::InvariantCulture) } } #region Private functions function Test-ExifTool { if (-not (Get-Command exiftool)) { throw 'ExifTool not found in the environment Path' } } function Get-TagNameArgument { [CmdletBinding(PositionalBinding = $false)] [OutputType([string])] Param ( [Parameter(Mandatory, ValueFromPipeline)] [string]$TagName ) Process { if (-not ($TagName -match '^(?:[\w-]+:)?\w+$')) { throw "Expected 'TagName' or 'Namespace:TagName' but found '$TagName'" } return "-$TagName" } } function ConvertFrom-ImageValue { [CmdletBinding(PositionalBinding = $false)] Param ( [Parameter(Mandatory, ValueFromPipeline)] [string]$TagValue ) Process { if ($TagValue -eq '-') { return '' } return $TagValue } } function ConvertTo-ImageValue { [CmdletBinding(PositionalBinding = $false)] Param ( [Parameter(Mandatory, ValueFromPipeline)] [object]$TagValue ) Process { if ($TagValue -is [datetime] -or $TagValue -is [System.DateTimeOffset]) { return '{0:yyyy-MM-ddTHH:mm:sszzz}' -f $TagValue } if ($TagValue -is [array]) { return $TagValue -join ';' } return [string] $TagValue } } function New-ImageMetadata { [CmdletBinding(PositionalBinding = $false)] Param ( [Parameter(Mandatory, ValueFromPipeline)] [PSObject] $ExifToolResult ) Process { $tagGroups = @{} foreach ($member in Get-Member -InputObject $ExifToolResult -MemberType NoteProperty) { if ($member.Name -EQ 'SourceFile') { continue } $groupName, $tagName = $member.Name -split ':' $tagValue = Select-Object -InputObject $ExifToolResult -ExpandProperty $member.Name if (-not $tagGroups[$groupName]) { $tagGroups[$groupName] = @{} } $tagGroups[$groupName][$tagName] = $tagValue } $tags = @{} foreach ($groupName in $tagGroups.Keys) { $tags[$groupName] = [PSCustomObject] ($tagGroups[$groupName]) } return [ImageMetadata] @{ FilePath = Convert-Path $ExifToolResult.SourceFile; Tags = [PSCustomObject] $tags } } } #endregion |