Functions/GenXdev.AI/GenerateMasonryLayoutHtml.ps1

###############################################################################
<#
.SYNOPSIS
Generates a responsive masonry layout HTML gallery from image data.
 
.DESCRIPTION
Creates an interactive HTML gallery with responsive masonry grid layout for
displaying images. Features include:
- Responsive grid layout that adapts to screen size
- Image tooltips showing descriptions and keywords
- Click-to-copy image path functionality
- Clean modern styling with hover effects
 
.PARAMETER Images
Array of image objects containing metadata. Each object requires:
- path: String with full filesystem path to image
- keywords: String array of descriptive tags
- description: Object containing short_description and long_description
 
.PARAMETER FilePath
Optional output path for the HTML file. If omitted, returns HTML as string.
 
.EXAMPLE
Create gallery from image array and save to file
$images = @(
    @{
        path = "C:\photos\sunset.jpg"
        keywords = @("nature", "sunset", "landscape")
        description = @{
            short_description = "Mountain sunset"
            long_description = "Beautiful sunset over mountain range"
        }
    }
)
GenerateMasonryLayoutHtml -Images $images -FilePath "C:\output\gallery.html"
 
.EXAMPLE
Generate HTML string without saving
$html = GenerateMasonryLayoutHtml $images
#>

function GenerateMasonryLayoutHtml {

    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        ###############################################################################
        [Parameter(
            Mandatory = $true,
            Position = 0,
            ValueFromPipeline = $true,
            HelpMessage = 'Array of image objects with path, keywords and description'
        )]
        [array]$Images,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            Position = 1,
            HelpMessage = 'Output path for the generated HTML file'
        )]
        [string]$FilePath = $null,
        ###############################################################################
        [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, object detection, and scene classification data',
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Whether editing is enabled'
        )]
        [Switch]$CanEdit = $false,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Whether deletion is enabled'
        )]
        [Switch]$CanDelete = $false,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Embed images as base64 data URLs instead of file:// URLs for better portability'
        )]
        [Switch]$EmbedImages = $false,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Show only pictures in a rounded rectangle, no text below.'
        )]
        [Alias('NoMetadata', 'OnlyPictures')]
        [switch] $ShowOnlyPictures,

        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Auto-scroll the page by this many pixels per second (null to disable)'
        )]
        [int]$AutoScrollPixelsPerSecond = $null,

        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Animate rectangles (objects/faces) in visible range, cycling every 300ms'
        )]
        [switch]$AutoAnimateRectangles,

        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Force single column layout (centered, 1/3 screen width)'
        )]
        [switch]$SingleColumnMode = $false,

        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Prefix to prepend to each image path (e.g. for remote URLs)'
        )]
        [string]$ImageUrlPrefix = '',

        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Number of images to load per page (for dynamic loading)'
        )]
        [int]$PageSize = 20,

        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Maximum number of images to load for print mode'
        )]
        [int]$MaxPrintImages = 50,

        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'IntersectionObserver rootMargin for infinite scroll trigger (e.g. "1200px")'
        )]
        [string]$RootMargin = '1200px',

        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'IntersectionObserver threshold for infinite scroll trigger'
        )]
        [double]$Threshold = 0.1
    )

    begin {
        $templatePath = "$PSScriptRoot\masonary.html"

        # Load System.Web for HTML encoding
        Microsoft.PowerShell.Utility\Add-Type -AssemblyName System.Web

        Microsoft.PowerShell.Utility\Write-Verbose "Starting HTML generation for $($Images.Count) images using template: $templatePath"

        # Verify template file exists
        if (-not (Microsoft.PowerShell.Management\Test-Path $templatePath)) {
            throw "Template file not found: $templatePath"
        }

        # Helper function to convert image to base64 data URL
        function ConvertTo-Base64DataUrl {
            param(
                [Parameter(Mandatory = $true)]
                [string]$ImagePath
            )

            try {
                # Check if file exists
                if (-not (Microsoft.PowerShell.Management\Test-Path $ImagePath)) {
                    Microsoft.PowerShell.Utility\Write-Verbose "Image file not found: $ImagePath"
                    return $null
                }

                # Determine MIME type based on file extension
                $extension = [System.IO.Path]::GetExtension($ImagePath).ToLower()
                $mimeType = switch ($extension) {
                    '.jpg'  { 'image/jpeg' }
                    '.gif'  { 'image/gif' }
                    '.jpeg' { 'image/jpeg' }
                    '.png'  { 'image/png' }
                    default {
                        Microsoft.PowerShell.Utility\Write-Verbose "Unsupported image format: $extension"
                        return $null
                    }
                }

                # Read image file and convert to base64
                $imageBytes = [System.IO.File]::ReadAllBytes($ImagePath)
                $base64String = [System.Convert]::ToBase64String($imageBytes)

                # Create data URL
                $dataUrl = "data:$mimeType;base64,$base64String"

                Microsoft.PowerShell.Utility\Write-Verbose "Converted image to base64 data URL: $ImagePath ($(($imageBytes.Length / 1KB).ToString('F1')) KB)"

                return $dataUrl
            }
            catch {
                Microsoft.PowerShell.Utility\Write-Verbose "Failed to convert image to base64: $ImagePath - $_"
                return $null
            }
        }
    }

    process {
        # Read the HTML template
        Microsoft.PowerShell.Utility\Write-Verbose "Reading HTML template from: $templatePath"
        $html = Microsoft.PowerShell.Management\Get-Content -Path $templatePath -Raw -Encoding UTF8
        # Convert image paths for browser compatibility
        if ($EmbedImages) {
            Microsoft.PowerShell.Utility\Write-Verbose 'Converting image paths to base64 data URLs'
        } else {
            Microsoft.PowerShell.Utility\Write-Verbose 'Converting image paths to file:// URLs'
        }

        [System.Collections.Generic.List[object]] $processedImages = @()
        foreach ($image in $Images) {
            $imageCopy = $image.PSObject.Copy()
            if ($imageCopy.path) {
                # Store original path for copy functionality
                $imageCopy | Microsoft.PowerShell.Utility\Add-Member -MemberType NoteProperty -Name 'originalPath' -Value $imageCopy.path -Force

                # If ImageUrlPrefix is provided, always use it + filename (with forward slashes)
                if ($ImageUrlPrefix) {
                    $prefix = $ImageUrlPrefix
                    if ($prefix[-1] -ne '/') { $prefix += '/' }
                    $filename = [System.IO.Path]::GetFileName($imageCopy.path)
                    $imageCopy.path = ($prefix + $filename) -replace '\\', '/'
                }
                # else, just normalize slashes
                else {
                    $imageCopy.path = $imageCopy.path -replace '\\', '/'
                }

                if ($EmbedImages) {
                    # Convert to base64 data URL for embedded display
                    $dataUrl = ConvertTo-Base64DataUrl -ImagePath $imageCopy.path
                    if ($null -ne $dataUrl) {
                        $imageCopy.path = $dataUrl
                    } else {
                        # Fallback to file:// URL if base64 conversion fails
                        $fileUrl = 'file:///' + ($imageCopy.path -replace '\\', '/')
                        $imageCopy.path = $fileUrl
                    }
                }
            }
            $processedImages.Add($imageCopy)
        }

        # Convert images array to JSON with proper escaping
        Microsoft.PowerShell.Utility\Write-Verbose "Converting $($processedImages.Count) images to JSON"
        $imagesJson = @($processedImages) | Microsoft.PowerShell.Utility\ConvertTo-Json -Compress -Depth 20 -WarningAction SilentlyContinue
        if ([string]::IsNullOrWhiteSpace($imagesJson) -or $imagesJson.Substring(0, 1) -ne '[') {
            # If the JSON does not start with an array, wrap it in an array
            $imagesJson = "[$imagesJson]"
        }
        # Escape the JSON for JavaScript string literal
        $escapedJson = $imagesJson | Microsoft.PowerShell.Utility\ConvertTo-Json -Compress
        # Replace the placeholder with actual image data
        Microsoft.PowerShell.Utility\Write-Verbose "Replacing placeholder JSON.parse(`"[]`") with actual image data"
        $html = "$html".Replace('images: JSON.parse("[]")', "images: JSON.parse($escapedJson)")

        # Replace other template variables if they exist
        if (-not [String]::IsNullOrWhiteSpace($Title)) {
            $escapedTitle = $Title | Microsoft.PowerShell.Utility\ConvertTo-Json
            # Replace JS property (mydata.title)
            $html = $html -replace '(title\s*:\s*)"[^"]*"', "`$1$escapedTitle"
            # Replace <title> tag
            $html = $html -replace '(<title>)(.*?)(</title>)', "`$1$Title`$3"
            Microsoft.PowerShell.Utility\Write-Verbose "Updated title to: $Title"
        }
        if (-not [String]::IsNullOrWhiteSpace($Description)) {
            $escapedDescription = $Description | Microsoft.PowerShell.Utility\ConvertTo-Json
            # Replace JS property (mydata.description)
            $html = $html -replace '(description\s*:\s*)"[^"]*"', "`$1$escapedDescription"
            # Replace meta description
            $html = $html -replace '(<meta name="description" content=")(.*?)(")', "`$1$Description`$3"
            Microsoft.PowerShell.Utility\Write-Verbose "Updated description to: $Description"
        }
        if ($CanEdit) {
            $html = "$html".Replace('canEdit: false', 'canEdit: true')
            Microsoft.PowerShell.Utility\Write-Verbose "Updated canEdit to: $CanEdit"
        }
        if ($CanDelete) {
            $html = "$html".Replace('canDelete: false', 'canDelete: true')
            Microsoft.PowerShell.Utility\Write-Verbose "Updated canDelete to: $CanDelete"
        }
        if ($ShowOnlyPictures) {
            $html = "$html".Replace('showOnlyPictures: false,', 'showOnlyPictures: true,')
            Microsoft.PowerShell.Utility\Write-Verbose "Updated showOnlyPictures to: $ShowOnlyPictures"
        }

        # Inject new mydata properties for dynamic loading
        $html = "$html".Replace('pageSize: 20', "pageSize: $PageSize")
        $html = "$html".Replace('maxPrintImages: 50', "maxPrintImages: $MaxPrintImages")
        $html = "$html".Replace('rootMargin: "1200px"', "rootMargin: `"$RootMargin`"")
        $html = "$html".Replace('threshold: 0.1', "threshold: $Threshold")

        # Inject new mydata properties
        if ($null -ne $AutoScrollPixelsPerSecond) {
            $autoScrollValue = if ($null -ne $AutoScrollPixelsPerSecond) { $AutoScrollPixelsPerSecond } else { 'null' }
            $html = "$html".replace('AutoScrollPixelsPerSecond: null', "AutoScrollPixelsPerSecond: $autoScrollValue")
        }
        if ($AutoAnimateRectangles) {
            $autoAnimateValue = if ($AutoAnimateRectangles) { 'true' } else { 'false' }
            $html = "$html".replace('AutoAnimateRectangles: false', "AutoAnimateRectangles: $autoAnimateValue")
        }
        # Inject SingleColumnMode property
        $singleColumnValue = if ($SingleColumnMode) { 'true' } else { 'false' }
        $html = "$html".replace('SingleColumnMode: false', "SingleColumnMode: $singleColumnValue")
    }

    end {
        # Either return HTML string or save to file based on parameters
        if ($null -eq $FilePath) {
            Microsoft.PowerShell.Utility\Write-Verbose 'Returning HTML as string output'
            return $html
        }
        else {
            Microsoft.PowerShell.Utility\Write-Verbose "Saving HTML gallery to: $FilePath"
            $html | Microsoft.PowerShell.Utility\Out-File -FilePath (GenXdev.FileSystem\Expand-Path $FilePath -CreateDirectory) -Encoding utf8
        }
    }
}