Functions/GenXdev.AI.Queries/Find-Image.ps1
################################################################################ <# .SYNOPSIS Scans image files for keywords and descriptions using metadata files. .DESCRIPTION Searches for image files (jpg, jpeg, png) in the specified directory and its subdirectories. For each image, checks associated description.json, keywords.json, people.json, and objects.json files for metadata. Can filter images based on keyword matches, people recognition, and object detection, then return the results as objects. Use -ShowImageGallery to display results in a browser-based masonry layout. The function searches through image directories and examines alternate data streams containing metadata in JSON format. It can match keywords using wildcard patterns, filter for specific people, and search for detected objects. By default, returns image data objects. Use -ShowImageGallery to display in a web browser. .PARAMETER Keywords Array of keywords to search for in image metadata. Supports wildcards. If empty, returns all images with any metadata. Keywords are matched against both the description content and keywords arrays in metadata files. .PARAMETER People Array of people names to search for in image metadata. Supports wildcards. Used to filter images based on face recognition metadata stored in people.json files. .PARAMETER Objects Array of object names to search for in image metadata. Supports wildcards. Used to filter images based on object detection metadata stored in objects.json files. .PARAMETER ImageDirectories Array of directory paths to search for images. Each directory is searched recursively for jpg, jpeg, and png files. Relative paths are converted to absolute paths automatically. .PARAMETER ShowImageGallery Switch to display the search results in a browser-based masonry layout gallery. When used, the results are shown in an interactive web view. Can be combined with -PassThru to also return the objects. .PARAMETER PassThru Switch to return image data as objects. When used with -ShowImageGallery, both displays the gallery and returns the objects. When used alone with -ShowImageGallery, only displays the gallery without returning objects. .PARAMETER Interactive When specified with -ShowImageGallery, connects to browser and adds additional buttons like Edit and Delete. .PARAMETER Title The title to display at the top of the image gallery. .PARAMETER Description The description text to display in the image gallery. .PARAMETER Language The language for retrieving descriptions and keywords. Will try to find metadata in the specified language first, then fall back to English if not available. This allows you to have metadata in multiple languages for the same images. .PARAMETER EmbedImages Switch to embed images as base64 data URLs instead of file:// URLs. This makes the generated HTML file completely self-contained and portable, but results in larger file sizes. Useful when the HTML needs to be shared or viewed on different systems where the original image files may not be accessible. .EXAMPLE Find-Image -Keywords "cat","dog" -ImageDirectories "C:\Photos" Searches for images containing 'cat' or 'dog' keywords and returns the image objects. .EXAMPLE findimages cat,dog "C:\Photos" Same as above using the alias and positional parameters. .EXAMPLE Find-Image -People "John","Jane" -ImageDirectories "C:\Family" -ShowImageGallery Searches for photos containing John or Jane and displays them in a web gallery. .EXAMPLE Find-Image -Objects "car","bicycle" -ImageDirectories "C:\Photos" -ShowImageGallery -PassThru Searches for images containing detected cars or bicycles, displays them in a gallery, and also returns the objects. .EXAMPLE findimages -Language "Spanish" -Keywords "playa","sol" -ImageDirectories "C:\Vacations" -ShowImageGallery Searches for images with Spanish metadata containing the keywords "playa" (beach) or "sol" (sun) and displays in gallery. .EXAMPLE Find-Image -Keywords "vacation" -People "John" -Objects "beach*" -ImageDirectories "C:\Photos" Searches for vacation photos with John in them that also contain beach-related objects and returns the data objects. #> ############################################################################### function Find-Image { [CmdletBinding()] [OutputType([Object[]], [System.Collections.Generic.List[Object]], [string])] [Alias("findimages", "li")] param( ############################################################################### [Parameter( Mandatory = $false, Position = 0, HelpMessage = "The keywords to look for, wildcards allowed." )] [string[]] $Keywords = @(), ############################################################################### [Parameter( Mandatory = $false, Position = 1, HelpMessage = "People to look for, wildcards allowed." )] [string[]] $People = @(), ############################################################################### [Parameter( Mandatory = $false, Position = 2, HelpMessage = "Objects to look for, wildcards allowed." )] [string[]] $Objects = @(), ############################################################################### [Parameter( Mandatory = $false, Position = 3, HelpMessage = "The image directory paths to search." )] [Alias("ImageDirectory")] [string[]] $ImageDirectories, ############################################################################### [Parameter( Mandatory = $false, ValueFromPipeline = $true, HelpMessage = "Accepts search results from a previous -PassThru call to regenerate the view." )] [object[]] $InputObject, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "Display the search results in a browser-based image gallery." )] [switch] $ShowImageGallery, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = ("Return image data as objects. When used with -ShowImageGallery, " + "both displays the gallery and returns the objects.") )] [switch] $PassThru, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = ("Will connect to browser and adds additional buttons like Edit and Delete. " + "Only effective when used with -ShowImageGallery.") )] [switch] $Interactive, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "Title for the gallery" )] [string]$Title = "Photo Gallery", ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "Description for the gallery" )] [string]$Description = "Hover over images to see face recognition and object detection data", ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "The language for retrieving descriptions and keywords." )] [PSDefaultValue(Value = "English")] [ValidateSet( "Afrikaans", "Akan", "Albanian", "Amharic", "Arabic", "Armenian", "Azerbaijani", "Basque", "Belarusian", "Bemba", "Bengali", "Bihari", "Bosnian", "Breton", "Bulgarian", "Cambodian", "Catalan", "Cherokee", "Chichewa", "Chinese (Simplified)", "Chinese (Traditional)", "Corsican", "Croatian", "Czech", "Danish", "Dutch", "English", "Esperanto", "Estonian", "Ewe", "Faroese", "Filipino", "Finnish", "French", "Frisian", "Ga", "Galician", "Georgian", "German", "Greek", "Guarani", "Gujarati", "Haitian Creole", "Hausa", "Hawaiian", "Hebrew", "Hindi", "Hungarian", "Icelandic", "Igbo", "Indonesian", "Interlingua", "Irish", "Italian", "Japanese", "Javanese", "Kannada", "Kazakh", "Kinyarwanda", "Kirundi", "Kongo", "Korean", "Krio (Sierra Leone)", "Kurdish", "Kurdish (Soranî)", "Kyrgyz", "Laothian", "Latin", "Latvian", "Lingala", "Lithuanian", "Lozi", "Luganda", "Luo", "Macedonian", "Malagasy", "Malay", "Malayalam", "Maltese", "Maori", "Marathi", "Mauritian Creole", "Moldavian", "Mongolian", "Montenegrin", "Nepali", "Nigerian Pidgin", "Northern Sotho", "Norwegian", "Norwegian (Nynorsk)", "Occitan", "Oriya", "Oromo", "Pashto", "Persian", "Polish", "Portuguese (Brazil)", "Portuguese (Portugal)", "Punjabi", "Quechua", "Romanian", "Romansh", "Runyakitara", "Russian", "Scots Gaelic", "Serbian", "Serbo-Croatian", "Sesotho", "Setswana", "Seychellois Creole", "Shona", "Sindhi", "Sinhalese", "Slovak", "Slovenian", "Somali", "Spanish", "Spanish (Latin American)", "Sundanese", "Swahili", "Swedish", "Tajik", "Tamil", "Tatar", "Telugu", "Thai", "Tigrinya", "Tonga", "Tshiluba", "Tumbuka", "Turkish", "Turkmen", "Twi", "Uighur", "Ukrainian", "Urdu", "Uzbek", "Vietnamese", "Welsh", "Wolof", "Xhosa", "Yiddish", "Yoruba", "Zulu")] [string] $Language = "English", ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "Opens in incognito/private browsing mode" )] [Alias("incognito", "inprivate")] [switch] $Private, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "Force enable debugging port, stopping existing browsers if needed" )] [switch] $Force, ############################################################################### [Alias("e")] [Parameter( Mandatory = $false, HelpMessage = "Opens in Microsoft Edge" )] [switch] $Edge, ############################################################################### [Alias("ch")] [Parameter( Mandatory = $false, HelpMessage = "Opens in Google Chrome" )] [switch] $Chrome, ############################################################################### [Alias("c")] [Parameter( Mandatory = $false, HelpMessage = "Opens in Microsoft Edge or Google Chrome, depending on what the default browser is" )] [switch] $Chromium, ############################################################################### [Alias("ff")] [Parameter( Mandatory = $false, HelpMessage = "Opens in Firefox" )] [switch] $Firefox, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "Opens in all registered modern browsers" )] [switch] $All, ############################################################################### [Alias("m", "mon")] [Parameter( Mandatory = $false, HelpMessage = "The monitor to use, 0 = default, -1 is discard, -2 = Configured secondary monitor, defaults to `Global:DefaultSecondaryMonitor or 2 if not found" )] [int] $Monitor = -2, ############################################################################### [Alias("fs", "f")] [Parameter( Mandatory = $false, HelpMessage = "Opens in fullscreen mode" )] [switch] $FullScreen, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "The initial width of the webbrowser window" )] [int] $Width = -1, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "The initial height of the webbrowser window" )] [int] $Height = -1, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "The initial X position of the webbrowser window" )] [int] $X = -999999, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "The initial Y position of the webbrowser window" )] [int] $Y = -999999, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "Place browser window on the left side of the screen" )] [switch] $Left, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "Place browser window on the right side of the screen" )] [switch] $Right, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "Place browser window on the top side of the screen" )] [switch] $Top, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "Place browser window on the bottom side of the screen" )] [switch] $Bottom, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "Place browser window in the center of the screen" )] [switch] $Centered, ############################################################################### [Alias("a", "app", "appmode")] [Parameter( Mandatory = $false, HelpMessage = "Hide the browser controls" )] [switch] $ApplicationMode, ############################################################################### [Alias("de", "ne", "NoExtensions")] [Parameter( Mandatory = $false, HelpMessage = "Prevent loading of browser extensions" )] [switch] $NoBrowserExtensions, ############################################################################### [Alias("allowpopups")] [Parameter( Mandatory = $false, HelpMessage = "Disable the popup blocker" )] [switch] $DisablePopupBlocker, ############################################################################### [Alias("lang", "locale")] [Parameter( Mandatory = $false, HelpMessage = "Set the browser accept-lang http header" )] [string] $AcceptLang = $null, ############################################################################### [Alias("bg")] [Parameter( Mandatory = $false, HelpMessage = "Restore PowerShell window focus" )] [switch] $RestoreFocus, ############################################################################### [Alias("nw", "new")] [Parameter( Mandatory = $false, HelpMessage = "Don't re-use existing browser window, instead, create a new one" )] [switch] $NewWindow, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "Only return the generated HTML instead of displaying it in a browser." )] [switch] $OnlyReturnHtml, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "Embed images as base64 data URLs instead of file:// URLs for better portability." )] [switch] $EmbedImages ) begin { # initialize results collection for all found images $results = [System.Collections.Generic.List[Object]] @() # use provided directories or default system directories if ($ImageDirectories) { # convert provided directories to simple path array $directories = $ImageDirectories } else { # Get default image directories from preference store $imageDirectoriesPreference = $null try { $json = GenXdev.Data\Get-GenXdevPreference ` -Name "ImageDirectories" ` -DefaultValue $null ` -ErrorAction SilentlyContinue if (-not [string]::IsNullOrEmpty($json)) { $imageDirectoriesPreference = $json | Microsoft.PowerShell.Utility\ConvertFrom-Json } } catch { $imageDirectoriesPreference = $null } # Use preference value if available if ($null -ne $imageDirectoriesPreference -and $imageDirectoriesPreference.Count -gt 0) { $directories = $imageDirectoriesPreference } else { # Fallback to default system directories $picturesPath = GenXdev.FileSystem\Expand-Path "~\Pictures" try { # attempt to get known folder path for Pictures $picturesPath = GenXdev.Windows\Get-KnownFolderPath Pictures } catch { # fallback to default if known folder retrieval fails $picturesPath = GenXdev.FileSystem\Expand-Path "~\Pictures" } # define default directories for processing $directories = @( (GenXdev.FileSystem\Expand-Path '~\downloads'), (GenXdev.FileSystem\Expand-Path '~\\onedrive'), $picturesPath ) } } } process { function processImageFile { param($item) # get full path of current image file being processed $image = $PSItem.FullName # output current image being processed for debugging Microsoft.PowerShell.Utility\Write-Verbose ( "Processing image: $image") # initialize metadata containers for this image $keywordsFound = @() $descriptionFound = $null $metadataFile = $null # First try to load description metadata in requested language if not English if ($Language -ne "English" -and [System.IO.File]::Exists("$($image):description.$Language.json")) { Microsoft.PowerShell.Utility\Write-Verbose "Found $Language metadata for $image" $metadataFile = "$($image):description.$Language.json" } # Fallback to English if language-specific file doesn't exist elseif ([System.IO.File]::Exists("$($image):description.json")) { Microsoft.PowerShell.Utility\Write-Verbose "Found English metadata for $image" $metadataFile = "$($image):description.json" } # Try to load description metadata if any file was found if ($metadataFile) { try { # read and parse description json from alternate data stream $descriptionFound = [System.IO.File]::ReadAllText($metadataFile) | Microsoft.PowerShell.Utility\ConvertFrom-Json # extract keywords from description if they exist $keywordsFound = ($null -eq $descriptionFound.keywords) ? @() : $descriptionFound.keywords } catch { # reset description if json parsing fails $descriptionFound = $null Microsoft.PowerShell.Utility\Write-Verbose "Failed to parse metadata from $metadataFile" } } # initialize people metadata container with default structure $peopleFound = @{count = 0; faces = @() } # try to load people metadata if alternate data stream exists if ([System.IO.File]::Exists("$($image):people.json")) { try { # read and parse people json from alternate data stream $peopleFound = [System.IO.File]::ReadAllText( "$($image):people.json") | Microsoft.PowerShell.Utility\ConvertFrom-Json # ensure people data has proper structure or reset to default $peopleFound = (($null -eq $peopleFound) -or ($peopleFound.count -eq 0)) ? @{count = 0; faces = @() } : $peopleFound } catch { # reset people data if json parsing fails $peopleFound = @{count = 0; faces = @() } } } # initialize objects metadata container with default structure $objectsFound = @{count = 0; objects = @(); object_counts = @{} } # try to load objects metadata if alternate data stream exists if ([System.IO.File]::Exists("$($image):objects.json")) { try { # read and parse objects json from alternate data stream $parsedObjects = [System.IO.File]::ReadAllText( "$($image):objects.json") | Microsoft.PowerShell.Utility\ConvertFrom-Json # if parsed data is not null and has predictions if ($null -ne $parsedObjects -and $null -ne $parsedObjects.predictions) { # Remap to the structure the script expects $objectsFound = @{ count = $parsedObjects.predictions.Count objects = $parsedObjects.predictions object_counts = $parsedObjects.object_counts } } } catch { # reset objects data if json parsing fails $objectsFound = @{count = 0; objects = @(); object_counts = @{} } } } # skip processing if no metadata exists and no search criteria provided if ( (($null -eq $Keywords) -or ($Keywords.Length -eq 0)) -and (($null -eq $keywordsFound) -or ($keywordsFound.length -eq 0)) -and ($null -eq $descriptionFound) -and (($null -eq $People) -or ($People.Count -eq 0)) -and (($null -eq $Objects) -or ($Objects.Count -eq 0)) ) { return } # assume match if no keyword search criteria specified $found = (($null -eq $Keywords) -or ($Keywords.Length -eq 0)) # perform keyword matching if keywords were specified for search if (-not $found) { # convert description to json string for wildcard matching $descriptionFound = ($null -ne $descriptionFound) ? $descriptionFound : "" | Microsoft.PowerShell.Utility\ConvertTo-Json ` -Compress ` -Depth 10 ` -WarningAction SilentlyContinue # check each required keyword against available metadata foreach ($requiredKeyword in $Keywords) { # first check if keyword matches in description content $found = "$descriptionFound" -like $requiredKeyword # if not found in description, check individual keywords array if (-not $found) { # skip keyword array check if no keywords exist if (($null -eq $keywordsFound) -or ($keywordsFound.Length -eq 0)) { continue } # check each image keyword against required keyword pattern foreach ($imageKeyword in $keywordsFound) { # use wildcard matching for flexible keyword search if ($imageKeyword -like $requiredKeyword) { $found = $true break } } } # exit early if any required keyword matches if ($found) { break } } } # perform additional people filtering if people criteria specified if ($found -and ($null -ne $People) -and ($People.Length -gt 0)) { # reset found flag to require people match $found = $false # check each found person against search criteria foreach ($foundPerson in $peopleFound.faces) { # check each searched person against found person foreach ($searchedForPerson in $People) { # use wildcard matching for flexible people search if ($foundPerson -like $searchedForPerson) { $found = $true break } } # exit early if any person matches if ($found) { break } } } # perform additional objects filtering if objects criteria specified if ($found -and ($null -ne $Objects) -and ($Objects.Length -gt 0)) { # reset found flag to require objects match $found = $false # check each found object against search criteria foreach ($foundObject in $objectsFound.objects) { # check each searched object against found object foreach ($searchedForObject in $Objects) { # use wildcard matching for flexible objects search if ($foundObject.label -like $searchedForObject) { $found = $true break } } # exit early if any object matches if ($found) { break } } } # return image data if all criteria matched if ($found) { # output match found for debugging purposes Microsoft.PowerShell.Utility\Write-Verbose ( "Found matching image: $image") # return hashtable with all image metadata Microsoft.PowerShell.Utility\Write-Output @{ path = $image keywords = $keywordsFound description = $descriptionFound people = $peopleFound objects = $objectsFound } } } if ($PSBoundParameters.ContainsKey('InputObject')) { $null = $InputObject | Microsoft.PowerShell.Core\ForEach-Object { # process each input object as an image file $path = $_.Path if ($null -eq $path) { return; } if ($path.StartsWith("file://")) { $path = $path.Substring(7).Replace('/', '\') } processImageFile $path } return; } # iterate through each specified image directory foreach ($imageDirectory in $directories) { # convert relative path to absolute path for consistency $path = GenXdev.FileSystem\Expand-Path $imageDirectory # output directory being scanned for debugging purposes Microsoft.PowerShell.Utility\Write-Verbose "Scanning directory: $path" # validate directory exists before proceeding with search if (-not [System.IO.Directory]::Exists($path)) { Microsoft.PowerShell.Utility\Write-Host ( "The directory '$path' does not exist.") continue } # search for jpg/jpeg/png files and process each one found Microsoft.PowerShell.Management\Get-ChildItem ` -Path "$path\*.jpg", "$path\*.jpeg", "$path\*.png" ` -Recurse ` -File ` -ErrorAction SilentlyContinue | Microsoft.PowerShell.Core\ForEach-Object { processImageFile $_ | Microsoft.PowerShell.Core\ForEach-Object { if (-not $ShowImageGallery) { Microsoft.PowerShell.Utility\Write-Output $_ } else { $null = $results.Add($_) } } } } } end { # Check if any results were found if ((-not $results) -or ($null -eq $results) -or ($results.Length -eq 0)) { # Provide appropriate message based on search criteria if (($null -eq $Keywords) -or ($Keywords.Length -eq 0)) { Microsoft.PowerShell.Utility\Write-Host "No images found." } else { Microsoft.PowerShell.Utility\Write-Host "No images found with the specified keywords." } return } # If ShowImageGallery is requested, display the gallery if ($ShowImageGallery) { if ([String]::IsNullOrWhiteSpace($Title)) { $Title = "Image Search Results" } if ([String]::IsNullOrWhiteSpace($Description)) { $Description = $MyInvocation.Statement }` $params = GenXdev.Helpers\Copy-IdenticalParamValues ` -BoundParameters $PSBoundParameters ` -FunctionName "Show-ImageGallery" ` -DefaultValues (Microsoft.PowerShell.Utility\Get-Variable -Scope Local -ErrorAction SilentlyContinue) # Pass the results to Show-ImageGallery GenXdev.AI\Show-ImageGallery @params -InputObject $results } if ((-not $ShowImageGallery) -or $PassThru) { return $results } } } |