Functions/GenXdev.AI/GenerateMasonryLayoutHtml.ps1
<##############################################################################
Part of PowerShell module : GenXdev.AI Original cmdlet filename : GenerateMasonryLayoutHtml.ps1 Original author : René Vaessen / GenXdev Version : 1.268.2025 ################################################################################ MIT License Copyright 2021-2025 GenXdev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ################################################################################> ############################################################################### <# .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' )] [System.Collections.Generic.IEnumerable[GenXdev.Helpers.ImageSearchResult]] $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 -LiteralPath $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 -LiteralPath $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' } '.bmp' { 'image/bmp' } '.webp' { 'image/webp' } '.tiff' { 'image/tiff' } '.tif' { 'image/tiff' } 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 -LiteralPath $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[GenXdev.Helpers.ImageSearchResult]] $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 } } } $processedImages.Add($imageCopy) } # Convert images array to JSON with proper escaping Microsoft.PowerShell.Utility\Write-Verbose "Converting $($processedImages.Count) images to JSON" $imagesJson = [GenXdev.Helpers.ImageSearchResultSerialize]::ToJson(($processedImages)) 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 ([string]::IsNullOrWhiteSpace($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 (GenXdev.FileSystem\Expand-Path $FilePath -CreateDirectory) -Encoding utf8 } } } |