PSMemeGenerator.psm1
|
#Region './Private/Invoke-MemeImageModification.ps1' -1 function Invoke-MemeImageModification { [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$ImagePath, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$OutputPath, [Parameter()] [string]$TopText = '', [Parameter()] [string]$BottomText = '' ) begin { Write-Verbose "Starting $($MyInvocation.MyCommand)" if (-not $IsWindows) { throw 'This function requires Windows OS due to System.Drawing dependencies.' } } process { try { Write-Verbose "Modifying image $ImagePath and saving to $OutputPath" $bitmap = [System.Drawing.Image]::FromFile($ImagePath) # Normalize EXIF orientation so bitmap.Width/Height match the displayed dimensions. # Viewers (Windows Photo Viewer, browsers) auto-apply EXIF rotation, but # System.Drawing reports raw stored dimensions. Without this correction, # centering math uses the wrong axis for portrait images. $exifOrientationId = 274 if ($bitmap.PropertyIdList -contains $exifOrientationId) { $orientationValue = $bitmap.GetPropertyItem($exifOrientationId).Value[0] $rotateFlipType = switch ($orientationValue) { 2 { [System.Drawing.RotateFlipType]::RotateNoneFlipX } 3 { [System.Drawing.RotateFlipType]::Rotate180FlipNone } 4 { [System.Drawing.RotateFlipType]::Rotate180FlipX } 5 { [System.Drawing.RotateFlipType]::Rotate90FlipX } 6 { [System.Drawing.RotateFlipType]::Rotate90FlipNone } 7 { [System.Drawing.RotateFlipType]::Rotate270FlipX } 8 { [System.Drawing.RotateFlipType]::Rotate270FlipNone } default { $null } } if ($null -ne $rotateFlipType) { $bitmap.RotateFlip($rotateFlipType) $bitmap.RemovePropertyItem($exifOrientationId) Write-Verbose "Applied EXIF orientation correction (tag value: $orientationValue)" } } $graphics = [System.Drawing.Graphics]::FromImage($bitmap) $graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias $graphics.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::AntiAlias $brush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::White) $font = $null $padding = 10 $maxFontSize = [float][Math]::Min(40, $bitmap.Width / 5) # GenericTypographic gives true text bounds without GDI+ whitespace padding $typographicFormat = [System.Drawing.StringFormat]::GenericTypographic if (-not [string]::IsNullOrWhiteSpace($TopText)) { $text = $TopText.ToUpper() $fontSize = $maxFontSize $font = New-Object System.Drawing.Font('Impact', $fontSize, [System.Drawing.FontStyle]::Bold) $size = $graphics.MeasureString($text, $font, [System.Drawing.PointF]::Empty, $typographicFormat) while ($size.Width -gt ($bitmap.Width - 2 * $padding) -and $fontSize -gt 8) { $font.Dispose() $fontSize -= 1 $font = New-Object System.Drawing.Font('Impact', $fontSize, [System.Drawing.FontStyle]::Bold) $size = $graphics.MeasureString($text, $font, [System.Drawing.PointF]::Empty, $typographicFormat) } Write-Verbose "TopText font size: $fontSize pt" $x = [float][Math]::Max(($bitmap.Width - $size.Width) / 2, $padding) $point = New-Object System.Drawing.PointF($x, [float]$padding) $graphics.DrawString($text, $font, $brush, $point, $typographicFormat) $font.Dispose() $font = $null } if (-not [string]::IsNullOrWhiteSpace($BottomText)) { $text = $BottomText.ToUpper() $fontSize = $maxFontSize $font = New-Object System.Drawing.Font('Impact', $fontSize, [System.Drawing.FontStyle]::Bold) $size = $graphics.MeasureString($text, $font, [System.Drawing.PointF]::Empty, $typographicFormat) while ($size.Width -gt ($bitmap.Width - 2 * $padding) -and $fontSize -gt 8) { $font.Dispose() $fontSize -= 1 $font = New-Object System.Drawing.Font('Impact', $fontSize, [System.Drawing.FontStyle]::Bold) $size = $graphics.MeasureString($text, $font, [System.Drawing.PointF]::Empty, $typographicFormat) } Write-Verbose "BottomText font size: $fontSize pt" $x = [float][Math]::Max(($bitmap.Width - $size.Width) / 2, $padding) $y = [float]($bitmap.Height - $size.Height - $padding) $point = New-Object System.Drawing.PointF($x, $y) $graphics.DrawString($text, $font, $brush, $point, $typographicFormat) $font.Dispose() $font = $null } $jpegCodec = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.MimeType -eq 'image/jpeg' } $encoderParams = New-Object System.Drawing.Imaging.EncoderParameters(1) $encoderParams.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter([System.Drawing.Imaging.Encoder]::Quality, 100L) [void] ($bitmap.Save($OutputPath, $jpegCodec, $encoderParams)) } catch { Write-Verbose "$($MyInvocation.MyCommand) Operation failed: $_" Write-Verbose "StackTrace: $($_.ScriptStackTrace)" throw $_ } finally { if ($null -ne $graphics) { $graphics.Dispose() } if ($null -ne $bitmap) { $bitmap.Dispose() } if ($null -ne $brush) { $brush.Dispose() } if ($null -ne $font) { $font.Dispose() } if ($null -ne $encoderParams) { $encoderParams.Dispose() } } } end { Write-Verbose "Completed $($MyInvocation.MyCommand)" } } #EndRegion './Private/Invoke-MemeImageModification.ps1' 127 #Region './Public/Get-MemeTemplate.ps1' -1 function Get-MemeTemplate { <# .SYNOPSIS Gets a list of popular meme templates from Imgflip. .DESCRIPTION Retrieves the top 100 most popular meme templates from the Imgflip API. Returns objects containing the meme ID, name, URL, width, and height. .EXAMPLE Get-MemeTemplate Returns all available meme templates. .EXAMPLE Get-MemeTemplate -Name 'Drake' Finds meme templates with 'Drake' in the name. .OUTPUTS PSCustomObject #> [CmdletBinding()] param ( [Parameter(Position = 0)] [string] $Name ) begin { Write-Verbose "Starting $($MyInvocation.MyCommand)" } process { try { $uri = 'https://api.imgflip.com/get_memes' Write-Verbose "Fetching meme templates from $uri" $response = Invoke-RestMethod -Uri $uri -Method Get if ($response.success) { foreach ($meme in $response.data.memes) { if ([string]::IsNullOrEmpty($Name) -or $meme.name -match $Name) { [PSCustomObject]@{ Id = $meme.id Name = $meme.name Url = $meme.url Width = $meme.width Height = $meme.height BoxCount = $meme.box_count } } } } else { throw "Imgflip API returned an error: $($response.error_message)" } } catch { Write-Verbose "$($MyInvocation.MyCommand) Operation failed: $_" Write-Verbose "StackTrace: $($_.ScriptStackTrace)" throw $_ } } end { Write-Verbose "Completed $($MyInvocation.MyCommand)" } } #EndRegion './Public/Get-MemeTemplate.ps1' 66 #Region './Public/New-Meme.ps1' -1 function New-Meme { <# .SYNOPSIS Creates a new meme by applying text to an image template. .DESCRIPTION Downloads a meme template from a URL and applies top and bottom text using System.Drawing. Requires Windows OS due to System.Drawing dependencies in modern .NET. .PARAMETER Id The ID of the source meme template from Imgflip. .PARAMETER Name The name of the source meme template from Imgflip. .PARAMETER Url The URL of the source meme image. .PARAMETER TopText The text to display at the top of the meme. .PARAMETER BottomText The text to display at the bottom of the meme. .PARAMETER OutputPath The path where the generated meme image will be saved. .EXAMPLE New-Meme -Name "Drake" -TopText "WHEN YOU WRITE" -BottomText "A POWERSHELL MODULE" -OutputPath ".\meme.jpg" Creates a meme using the first template matching "Drake" and saves it to meme.jpg. .EXAMPLE New-Meme -Id "181913649" -TopText "WHEN YOU WRITE" -BottomText "A POWERSHELL MODULE" -OutputPath ".\meme.jpg" Creates a meme using the template with ID "181913649" and saves it to meme.jpg. .OUTPUTS System.IO.FileInfo #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'ByName')] param ( [Parameter(Mandatory, ParameterSetName = 'ById', ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string] $Id, [Parameter(Mandatory, ParameterSetName = 'ByName', ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string] $Name, [Parameter(Mandatory, ParameterSetName = 'ByUrl', ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string] $Url, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $OutputPath, [Parameter(Mandatory = $false)] [string] $TopText = '', [Parameter(Mandatory = $false)] [string] $BottomText = '' ) begin { Write-Verbose "Starting $($MyInvocation.MyCommand)" } process { try { $resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath) $targetUrl = $Url if ($PSCmdlet.ParameterSetName -eq 'ById') { Write-Verbose "Looking up meme template by ID: $Id" $template = Get-MemeTemplate | Where-Object Id -EQ $Id | Select-Object -First 1 if (-not $template) { throw "Could not find a meme template with ID '$Id'." } $targetUrl = $template.Url } elseif ($PSCmdlet.ParameterSetName -eq 'ByName') { Write-Verbose "Looking up meme template by Name: $Name" $template = Get-MemeTemplate -Name $Name | Select-Object -First 1 if (-not $template) { throw "Could not find a meme template matching Name '$Name'." } $targetUrl = $template.Url } if ($PSCmdlet.ShouldProcess($resolvedPath, "Create meme from $targetUrl")) { Write-Verbose "Downloading image from $targetUrl" $tempFile = [System.IO.Path]::GetTempFileName() try { Invoke-WebRequest -Uri $targetUrl -OutFile $tempFile Invoke-MemeImageModification -ImagePath $tempFile -OutputPath $resolvedPath -TopText $TopText -BottomText $BottomText Get-Item -Path $resolvedPath } finally { if (Test-Path $tempFile) { Remove-Item -Path $tempFile -Force } } } } catch { Write-Verbose "$($MyInvocation.MyCommand) Operation failed: $_" Write-Verbose "StackTrace: $($_.ScriptStackTrace)" throw $_ } } end { Write-Verbose "Completed $($MyInvocation.MyCommand)" } } #EndRegion './Public/New-Meme.ps1' 120 |