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 Scenes
Array of scene categories to search for in image metadata. Supports wildcards.
Used to filter images based on scene classification metadata stored in
scenes.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 InputObject
Accepts search results from a previous -PassThru call to regenerate the view.
 
.PARAMETER PictureType
Array of picture types to filter by (e.g., 'daylight', 'evening', 'indoor',
'outdoor'). Supports wildcards. Matches against the picture_type property in
description metadata.
 
.PARAMETER StyleType
Array of style types to filter by (e.g., 'casual', 'formal'). Supports
wildcards. Matches against the style_type property in description metadata.
 
.PARAMETER OverallMood
Array of overall moods to filter by (e.g., 'calm', 'cheerful', 'sad',
'energetic'). Supports wildcards. Matches against the overall_mood_of_image
property in description metadata.
 
.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 AcceptLang
Set the browser accept-lang http header.
 
.PARAMETER Monitor
The monitor to use, 0 = default, -1 is discard, -2 = Configured secondary
monitor, defaults to Global:DefaultSecondaryMonitor or 2 if not found.
 
.PARAMETER Width
The initial width of the webbrowser window.
 
.PARAMETER Height
The initial height of the webbrowser window.
 
.PARAMETER X
The initial X position of the webbrowser window.
 
.PARAMETER Y
The initial Y position of the webbrowser window.
 
.PARAMETER HasNudity
Switch to filter for images that contain nudity. Only returns images where the
has_nudity property is true in the metadata.
 
.PARAMETER NoNudity
Switch to filter for images that do NOT contain nudity. Only returns images
where the has_nudity property is false in the metadata.
 
.PARAMETER HasExplicitContent
Switch to filter for images that contain explicit content. Only returns images
where the has_explicit_content property is true in the metadata.
 
.PARAMETER NoExplicitContent
Switch to filter for images that do NOT contain explicit content. Only returns
images where the has_explicit_content property is false in the metadata.
 
.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
Connects to browser and adds additional buttons like Edit and Delete.
 
.PARAMETER Private
Opens in incognito/private browsing mode.
 
.PARAMETER Force
Force enable debugging port, stopping existing browsers if needed.
 
.PARAMETER Edge
Opens in Microsoft Edge.
 
.PARAMETER Chrome
Opens in Google Chrome.
 
.PARAMETER Chromium
Opens in Microsoft Edge or Google Chrome, depending on what the default browser
is.
 
.PARAMETER Firefox
Opens in Firefox.
 
.PARAMETER All
Opens in all registered modern browsers.
 
.PARAMETER FullScreen
Opens in fullscreen mode.
 
.PARAMETER Left
Place browser window on the left side of the screen.
 
.PARAMETER Right
Place browser window on the right side of the screen.
 
.PARAMETER Top
Place browser window on the top side of the screen.
 
.PARAMETER Bottom
Place browser window on the bottom side of the screen.
 
.PARAMETER Centered
Place browser window in the center of the screen.
 
.PARAMETER ApplicationMode
Hide the browser controls.
 
.PARAMETER NoBrowserExtensions
Prevent loading of browser extensions.
 
.PARAMETER DisablePopupBlocker
Disable the popup blocker.
 
.PARAMETER RestoreFocus
Restore PowerShell window focus.
 
.PARAMETER NewWindow
Don't re-use existing browser window, instead, create a new one.
 
.PARAMETER OnlyReturnHtml
Only return the generated HTML instead of displaying it in a browser.
 
.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.
 
.EXAMPLE
Find-Image -Scenes "beach","forest","mountain*" -ImageDirectories "C:\Nature" -ShowImageGallery
Searches for images classified as beach, forest, or mountain scenes and displays them in a gallery.
 
.EXAMPLE
Find-Image -NoNudity -NoExplicitContent -ImageDirectories "C:\Family" -ShowImageGallery
Searches for family-safe images (no nudity or explicit content) and displays them in a gallery.
 
.EXAMPLE
Find-Image -PictureType "daylight" -OverallMood "calm" -ImageDirectories "C:\Photos"
Searches for daylight photos with a calm/peaceful mood and returns the image objects.
 
.EXAMPLE
findimages -StyleType "casual" -HasNudity -ImageDirectories "C:\Art"
Searches for casual style images that contain nudity 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 = "Scene categories to look for, wildcards allowed."
        )]
        [string[]] $Scenes = @(),
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            Position = 4,
            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 = ("Picture type to filter by (e.g., 'daylight', " +
                "'evening', 'indoor', etc). Supports wildcards.")
        )]
        [string[]] $PictureType = @(),
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ("Style type to filter by (e.g., 'casual', 'formal', " +
                "etc). Supports wildcards.")
        )]
        [string[]] $StyleType = @(),
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ("Overall mood to filter by (e.g., 'calm', 'cheerful', " +
                "'sad', etc). Supports wildcards.")
        )]
        [string[]] $OverallMood = @(),
        ###############################################################################
        [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,
        ###############################################################################
        [Alias("lang", "locale")]
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Set the browser accept-lang http header"
        )]
        [string] $AcceptLang = $null,
        ###############################################################################
        [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,
        ###############################################################################
        [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 = "Filter images that contain nudity."
        )]
        [switch] $HasNudity,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Filter images that do NOT contain nudity."
        )]
        [switch] $NoNudity,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Filter images that contain explicit content."
        )]
        [switch] $HasExplicitContent,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Filter images that do NOT contain explicit content."
        )]
        [switch] $NoExplicitContent,
        ###############################################################################
        [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,
        ###############################################################################
        [Alias("incognito", "inprivate")]
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Opens in incognito/private browsing mode"
        )]
        [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("fs", "f")]
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Opens in fullscreen mode"
        )]
        [switch] $FullScreen,
        ###############################################################################
        [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("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 {

        # enable interactive mode when interactive switch is used
        if ($Interactive) {

            $ShowImageGallery = $true
        }

        # initialize results collection for all found images
        $results = [System.Collections.Generic.List[Object]] @()

        # get configured directories and language using Get-ImageDirectories
        $config = GenXdev.AI\Get-ImageDirectories -DefaultValue $ImageDirectories

        # use provided directories or get from configuration
        if ($ImageDirectories) {

            $directories = $ImageDirectories
        }
        else {

            $directories = $config.ImageDirectories
        }

        # resolve default language if not explicitly provided
        if ([string]::IsNullOrEmpty($Language)) {

            $Language = $config.Language
        }
    }

    process {

        # define internal function to process individual image files
        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

            # 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 = @{}
                    }
                }
            }

            # initialize scenes metadata container with default structure
            $scenesFound = @{
                success = $false;
                scene = "unknown";
                confidence = 0.0
            }

            # try to load scenes metadata if alternate data stream exists
            if ([System.IO.File]::Exists("$($image):scenes.json")) {

                try {

                    # read and parse scenes json from alternate data stream
                    $parsedScenes = [System.IO.File]::ReadAllText(
                        "$($image):scenes.json") |
                        Microsoft.PowerShell.Utility\ConvertFrom-Json

                    # if parsed data is not null and has scene data
                    if ($null -ne $parsedScenes -and
                        $null -ne $parsedScenes.scene) {

                        $scenesFound = $parsedScenes
                    }
                }
                catch {

                    # reset scenes data if json parsing fails
                    $scenesFound = @{
                        success = $false;
                        scene = "unknown";
                        confidence = 0.0
                    }
                }
            }

            # determine if image has search criteria or metadata
            $hasSearchCriteria = (($null -ne $Keywords) -and
                ($Keywords.Length -gt 0)) -or
                (($null -ne $People) -and ($People.Count -gt 0)) -or
                (($null -ne $Objects) -and ($Objects.Count -gt 0)) -or
                (($null -ne $Scenes) -and ($Scenes.Count -gt 0)) -or
                $HasNudity -or $NoNudity -or $HasExplicitContent -or
                $NoExplicitContent -or
                (($null -ne $PictureType) -and ($PictureType.Count -gt 0)) -or
                (($null -ne $StyleType) -and ($StyleType.Count -gt 0)) -or
                (($null -ne $OverallMood) -and ($OverallMood.Count -gt 0))

            $hasAnyMetadata = (($null -ne $keywordsFound) -and
                ($keywordsFound.length -gt 0)) -or
                ($null -ne $descriptionFound) -or
                ($peopleFound.count -gt 0) -or
                ($objectsFound.count -gt 0) -or
                ($scenesFound.success -eq $true)

            # only skip if we have no search criteria AND no metadata
            if (-not $hasSearchCriteria -and -not $hasAnyMetadata) {
                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 }
                }
            }

            # perform additional scenes filtering if scenes criteria specified
            if ($found -and ($null -ne $Scenes) -and ($Scenes.Count -gt 0)) {

                # reset found flag to require scene match
                $found = $false

                # debug output for scene filtering
                if ($VerbosePreference -eq 'Continue') {

                    Microsoft.PowerShell.Utility\Write-Verbose (
                        "Scene filtering - Searching for: $($Scenes -join ', ')")

                    Microsoft.PowerShell.Utility\Write-Verbose (
                        "Scene filtering - Found scene: $($scenesFound.scene)")

                    Microsoft.PowerShell.Utility\Write-Verbose (
                        "Scene filtering - Scene success: $($scenesFound.success)")
                }

                # check if the found scene matches any of the search criteria
                foreach ($searchedForScene in $Scenes) {

                    # use wildcard matching for flexible scene search
                    if ($scenesFound.scene -like $searchedForScene) {

                        $found = $true

                        if ($VerbosePreference -eq 'Continue') {

                            Microsoft.PowerShell.Utility\Write-Verbose (
                                "Scene filtering - Match found: " +
                                "'$($scenesFound.scene)' matches " +
                                "'$searchedForScene'")
                        }

                        break
                    }

                    # special handling for wildcard scenes with other metadata
                    if ($searchedForScene -eq "*" -and
                        $scenesFound.success -eq $false) {

                        # check if we have any other metadata that makes this interesting
                        $hasOtherMetadata = (($null -ne $keywordsFound) -and
                            ($keywordsFound.length -gt 0)) -or
                            ($null -ne $descriptionFound) -or
                            ($peopleFound.count -gt 0) -or
                            ($objectsFound.count -gt 0)

                        if ($hasOtherMetadata) {

                            $found = $true

                            if ($VerbosePreference -eq 'Continue') {

                                Microsoft.PowerShell.Utility\Write-Verbose (
                                    "Scene filtering - Wildcard match: including " +
                                    "image with other metadata despite missing " +
                                    "scene data")
                            }

                            break
                        }
                    }
                }

                if (-not $found -and $VerbosePreference -eq 'Continue') {

                    Microsoft.PowerShell.Utility\Write-Verbose (
                        "Scene filtering - No match found for scene: " +
                        "$($scenesFound.scene)")
                }
            }

            # perform content filtering based on description metadata properties
            if ($found -and ($HasNudity -or $NoNudity -or $HasExplicitContent -or
                $NoExplicitContent -or
                (($null -ne $PictureType) -and ($PictureType.Count -gt 0)) -or
                (($null -ne $StyleType) -and ($StyleType.Count -gt 0)) -or
                (($null -ne $OverallMood) -and ($OverallMood.Count -gt 0)))) {

                # check if we have description metadata required for these filters
                if ($null -eq $descriptionFound) {

                    $found = $false
                }
                else {

                    # nudity filtering
                    if ($HasNudity -and $descriptionFound.has_nudity -ne $true) {

                        $found = $false
                    }

                    if ($NoNudity -and $descriptionFound.has_nudity -eq $true) {

                        $found = $false
                    }

                    # explicit content filtering
                    if ($HasExplicitContent -and
                        $descriptionFound.has_explicit_content -ne $true) {

                        $found = $false
                    }

                    if ($NoExplicitContent -and
                        $descriptionFound.has_explicit_content -eq $true) {

                        $found = $false
                    }

                    # picture type filtering
                    if ($found -and ($null -ne $PictureType) -and
                        ($PictureType.Count -gt 0)) {

                        $pictureTypeFound = $false

                        foreach ($requiredPictureType in $PictureType) {

                            if ($descriptionFound.picture_type -like
                                $requiredPictureType) {

                                $pictureTypeFound = $true

                                break
                            }
                        }

                        if (-not $pictureTypeFound) {

                            $found = $false
                        }
                    }

                    # style type filtering
                    if ($found -and ($null -ne $StyleType) -and
                        ($StyleType.Count -gt 0)) {

                        $styleTypeFound = $false

                        foreach ($requiredStyleType in $StyleType) {

                            if ($descriptionFound.style_type -like
                                $requiredStyleType) {

                                $styleTypeFound = $true

                                break
                            }
                        }

                        if (-not $styleTypeFound) {

                            $found = $false
                        }
                    }

                    # overall mood filtering
                    if ($found -and ($null -ne $OverallMood) -and
                        ($OverallMood.Count -gt 0)) {

                        $overallMoodFound = $false

                        foreach ($requiredMood in $OverallMood) {

                            if ($descriptionFound.overall_mood_of_image -like
                                $requiredMood) {

                                $overallMoodFound = $true

                                break
                            }
                        }

                        if (-not $overallMoodFound) {

                            $found = $false
                        }
                    }
                }
            }

            # 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
                    scenes      = $scenesFound
                }
            }
        }

        # handle input object processing from pipeline
        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
            $null = GenXdev.AI\Show-ImageGallery @params -InputObject $results
        }

        if ((-not $ShowImageGallery) -or $PassThru) {

            return $results
        }
    }
}
################################################################################